博客结构

  • 1.前言
  • 2.启动类型
    • 1.冷启动
    • 2.温启动
    • 3.热启动
  • 3.App启动
    • 1.App启动语言描述
    • 2.App启动进程图述
    • 3.App启动类图述
  • 4.冷启动分析工具及方法
    • 1.命令行
    • 2.代码打印
    • 3.高清摄像机
  • 5.通用优化策略
    • 1.前置加载优化
      • 1.原理
      • 2.优化时间
    • 2.异步加载优化
    • 3.延迟加载优化
    • 1.原理
    • 2.优化耗时
  • 6.业务优化方案
    • 1.原理
    • 2.优化场景
      • 1.布局复杂度高
      • 2.并发加载渲染图片
      • 3.Arouter初始化优化
      • 4.redex 重排列 class 文件
      • 5.针对verifyClass的hook处理
  • 7.站在巨人肩膀

用简单通俗易懂的话来记录自己对对象存活判断算法的理解

1.前言

一个应用的性能如何,冷启动是个重要的衡量指标,毕竟用户第一次使用应用时,多久能进入页面(包括首页、启动页、广告页等),是用户的第一感官。对于一般应用,有启动页和广告页,对启动速度的要求相对于来说降低了一些,毕竟,进来就是启动页,只是时间停留时间长短问题(当然,不是说对时间不敏感,你要是在启动页停留个3,5s,谁也受不了。不过,话说,谷歌统计的所有应用的平均时间竟然是4000多ms,这还是挺吃惊的)。
我所做的是个系统工具类,没有启动页,直接就是干到首页。又因为是系统应用,要有和桌面有一体化的感觉,所以,对启动速度要求极高。同时,又由于要适配全机型(Android5.0 -Android 12.0),前期没有做版本区分,所以,挑战来说更大。无论怎么说吧,经过一轮又一轮的优化,低端机达到了550ms以内,高端机更是在300ms以内。整体启动速度提供65%左右。
其实,现在写冷启动的并不多,为啥呢?有一些是公司并不在意,个人也就不在意,还有就是业务有启动页,可能并不明显。最后也有一部分原因来讲就是应用层去优化有限,更多的是业务逻辑的优化,感觉写不出创意来,借鉴于此,我这篇博客也是先简单讲下冷启动的流程,看下那些我们可以去做,以及一些通用通吃的方案,一些结合业务做的优化(图片的加载优化、耗时业务的巧妙处理),期望大家看到后引起更多的思考和更多的交流学习。

2.启动类型

根据启动的场景,简单的将应用的启动分为三种类型,而我们一般的处理的都是在冷启动这种场景

1.冷启动

应用从零开始,进程和页面都没有创建。
场景:任务后台杀死进程、桌面长安点击应用信息,强制停止(以OPPO手机为例)

2.温启动

再次启动时,只是重建首页页面。
场景:首页点击返回键,进入后台。再次点击应用图标或者从后台任务里点击

3.热启动

再次启动时,进程和页面都未曾销毁,直接显示页面。
场景:点击Home键,应用进入后台任务

3.App启动

谈到App的启动,说实话,每次要梳理它的时候,都要重新看一遍,每次都感觉好像懂了,好想知道了,但是,又好像哪点不知道。所以,这次,只是为了冷启动的分析,所以,咱们用图梳理它的逻辑,期望能加深印象。

1.App启动语言描述

1.点击图标,Launcher向AMS请求启动app
2.AMS收到请求后,记录app的信息,并告知Launcher进入pause状态
3.Launcher进入pause状态后,告知AMS
4.AMS检测新的app进程时候已经启动,否则通过Zygote创建新的进程并启动ActivityThread的main方法
5.进程创建好后,调用上面的ActivityThread.main()
6.ActivityThread中H处理需要启动Activity的请求消息

2.App启动进程图述

在这个进程启动图中,只是节选了我们正常冷启动的四个进程间的唤起与创建,详细的描述了App进程的调用图,是为了让大家对流程优化有更清晰的认知。
有图可以知道,View的绘制是在生命周期的onResume之后

3.App启动类图述

类图是从网上down下来的,如有雷同,纯属巧合。这是基于Android8.0的源码类图

这是一个详细的类图,对于类图关键的几个类做个简单解释
1.ActivityThread是个Thread么?

/*** This manages the execution of the main thread in an* application process, scheduling and executing activities,* broadcasts, and other operations on it as the activity* manager requests.** {@hide}*/

不是哟,正如注释,它只是主线程的执行管理类,对四大组件进行调用。
2.View的测量绘制在Activity的onResume之后?
是的,一验证一源码
一验证方式:
(1)在onResume中进行控件高宽的获取
(2)post发送个消息后再测量

  protected void onResume() {super.onResume();LogUtils.d("haha","measureSize begin");measureSize();LogUtils.d("haha","measureSize end");viewDelegate.mRadioGroup.post(new Runnable() {@Overridepublic void run() {LogUtils.d("haha","after sleep measureSize begin");measureSize();}});}private void measureSize() {int radioGroupHeight = viewDelegate.mRadioGroup.getMeasuredHeight();int radioGroupWidth = viewDelegate.mRadioGroup.getMeasuredWidth();int radioButtonHeight = viewDelegate.rbUser.getMeasuredHeight();int radioButtonWidth = viewDelegate.rbUser.getMeasuredWidth();LogUtils.d("haha","radioGroupHeight:"+radioGroupHeight+"radioGroupWidth:"+radioGroupWidth+"radioButtonHeight:"+radioButtonHeight+"radioButtonWidth:"+radioButtonWidth);}

結果如下:

2021-05-30 10:49:28.909 12221-12221/ D/haha: packageName;com.fcbox.hivestation postion:0
2021-05-30 10:49:29.168 12221-12221/ D/haha: measureSize begin
2021-05-30 10:49:29.168 12221-12221/ D/haha: radioGroupHeight:0radioGroupWidth:0radioButtonHeight:0radioButtonWidth:0
2021-05-30 10:49:29.168 12221-12221/ D/haha: measureSize end
2021-05-30 10:49:34.169 12221-12221/? D/haha: after sleep measureSize begin
2021-05-30 10:49:34.170 12221-12221/? D/haha: radioGroupHeight:186radioGroupWidth:1080radioButtonHeight:186radioButtonWidth:360

这个结果说明了什么?说明在onResume中,View竟没有测量,布局,绘制。而是在super.onResume的一些队列事件处理后,才能测量出宽高。好了,接下来,从代码层面去解释这个问题

一源码方式:

1.ActivityThread.handleResumeActivity做了啥?final void handleResumeActivity(IBinder token,boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {ActivityClientRecord r = mActivities.get(token);if (!checkAndUpdateLifecycleSeq(seq, r, "resumeActivity")) {return;}// If we are getting ready to gc after going to the background, well// we are back active so skip it.unscheduleGcIdler();mSomeActivitiesChanged = true;// TODO Push resumeArgs into the activity for considerationr = performResumeActivity(token, clearHide, reason);if (r != null) {final Activity a = r.activity;if (localLOGV) Slog.v(TAG, "Resume " + r + " started activity: " +a.mStartedActivity + ", hideForNow: " + r.hideForNow+ ", finished: " + a.mFinished);final int forwardBit = isForward ?WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION : 0;// If the window hasn't yet been added to the window manager,// and this guy didn't finish itself or start another activity,// then go ahead and add the window.boolean willBeVisible = !a.mStartedActivity;if (!willBeVisible) {try {willBeVisible = ActivityManagerNative.getDefault().willActivityBeVisible(a.getActivityToken());} catch (RemoteException e) {throw e.rethrowFromSystemServer();}}if (r.window == null && !a.mFinished && willBeVisible) {r.window = r.activity.getWindow();View decor = r.window.getDecorView();decor.setVisibility(View.INVISIBLE);ViewManager wm = a.getWindowManager();WindowManager.LayoutParams l = r.window.getAttributes();a.mDecor = decor;l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;l.softInputMode |= forwardBit;if (r.mPreserveWindow) {a.mWindowAdded = true;r.mPreserveWindow = false;// Normally the ViewRoot sets up callbacks with the Activity// in addView->ViewRootImpl#setView. If we are instead reusing// the decor view we have to notify the view root that the// callbacks may have changed.ViewRootImpl impl = decor.getViewRootImpl();if (impl != null) {impl.notifyChildRebuilt();}}if (a.mVisibleFromClient && !a.mWindowAdded) {a.mWindowAdded = true;wm.addView(decor, l);// -----------------------------1}// If the window has already been added, but during resume// we started another activity, then don't yet make the// window visible.} else if (!willBeVisible) {if (localLOGV) Slog.v(TAG, "Launch " + r + " mStartedActivity set");r.hideForNow = true;}// Get rid of anything left hanging around.cleanUpPendingRemoveWindows(r, false /* force */);// The window is now visible if it has been added, we are not// simply finishing, and we are not starting another activity.if (!r.activity.mFinished && willBeVisible&& r.activity.mDecor != null && !r.hideForNow) {if (r.newConfig != null) {performConfigurationChangedForActivity(r, r.newConfig, REPORT_TO_ACTIVITY);if (DEBUG_CONFIGURATION) Slog.v(TAG, "Resuming activity "+ r.activityInfo.name + " with newConfig " + r.activity.mCurrentConfig);r.newConfig = null;}if (localLOGV) Slog.v(TAG, "Resuming " + r + " with isForward="+ isForward);WindowManager.LayoutParams l = r.window.getAttributes();if ((l.softInputMode& WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION)!= forwardBit) {l.softInputMode = (l.softInputMode& (~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION))| forwardBit;if (r.activity.mVisibleFromClient) {ViewManager wm = a.getWindowManager();View decor = r.window.getDecorView();wm.updateViewLayout(decor, l);}}r.activity.mVisibleFromServer = true;mNumVisibleActivities++;if (r.activity.mVisibleFromClient) {r.activity.makeVisible();}}if (!r.onlyLocalRequest) {r.nextIdle = mNewActivities;mNewActivities = r;if (localLOGV) Slog.v(TAG, "Scheduling idle handler for " + r);Looper.myQueue().addIdleHandler(new Idler());}r.onlyLocalRequest = false;// Tell the activity manager we have resumed.if (reallyResume) {try {ActivityManagerNative.getDefault().activityResumed(token);} catch (RemoteException ex) {throw ex.rethrowFromSystemServer();}}} else {// If an exception was thrown when trying to resume, then// just end this activity.try {ActivityManagerNative.getDefault().finishActivity(token, Activity.RESULT_CANCELED, null,Activity.DONT_FINISH_TASK_WITH_ACTIVITY);} catch (RemoteException ex) {throw ex.rethrowFromSystemServer();}}}

大家可以看到//----------------1的地方才开始建立window与View的关联,然后在ViewRootImpl中调用
requestLayout—>scheduleTraversals去渲染第一帧。

ViewRootImpl:public void requestLayout() {if (!mHandlingLayoutInLayoutRequest) {checkThread();mLayoutRequested = true;scheduleTraversals();}}void scheduleTraversals() {if (!mTraversalScheduled) {mTraversalScheduled = true;mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);//---------------2if (!mUnbufferedInputDispatch) {scheduleConsumeBatchedInput();}notifyRendererOfFramePending();pokeDrawLockIfNeeded();}}Choreographer:private void postCallbackDelayedInternal(int callbackType,Object action, Object token, long delayMillis) {if (DEBUG_FRAMES) {Log.d(TAG, "PostCallback: type=" + callbackType+ ", action=" + action + ", token=" + token+ ", delayMillis=" + delayMillis);}synchronized (mLock) {final long now = SystemClock.uptimeMillis();final long dueTime = now + delayMillis;mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);if (dueTime <= now) {scheduleFrameLocked(now);} else {Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);msg.arg1 = callbackType;msg.setAsynchronous(true);mHandler.sendMessageAtTime(msg, dueTime);//--------3}}}

大家看到了2和3的标记了吗?这也就是为啥在super.onResume后,post一个消息可以读取到控件大小的原因。
3.setCotentView是不是完成了绘制?
这个方法只是完成了Activity的ContentView的创建,而并没有执行View的绘制流程。

4.冷启动分析工具及方法

1.命令行

//查看冷启动时间
adb shell am start -S -W 包名/全包名launchActivity

以启动camera为例:这个包名和启动页面有些的手机不是的,所以,可以拿自己项目进行测试,这里只为了说明分析
adb shell am start -W com.mediatek.camera/com.android.camera.CameraActivity
结果;

该命令具体实现在/frameworks/base/cmds/am/src/com/android/commands/am/Am.java,原理是跨Binder调用ActivityManagerService.startActivityAndWait() 接口,其中返回数据分别调用对应

startTime: 调用startActivityAndWait()的时间点

endTime: 调用startActivityAndWait()函数调用返回的时间点

WaitTime: 调用startActivityAndWait()调用耗时。

再通过之间的计算得到。

来,让我们在一起温故一下咱们四个进程的调用逻辑。那么,这有什么用。知其所以然,比如,我们的测试同学本质兢兢业业的本分工作原则,根据高清摄像头的耗时和命令行耗时做对比,这样肯定不行,因为高清摄像拍照的计算是点击触发开始,而命令行是从AMS的启动开始咯。

2.代码打印

这种方式呢?是大家根据android提供的方法提供的打印,个人并没有使用过。

2021-05-30 12:24:16.917 1165-1334/? I/ActivityManager: Displayed /.ui.activity.MainActivity: +1s1ms (total +2s52ms)

我理解这种方式应该也是和命令行是样的,是从AMS计算的,但是暂时,没有找到有力证据。可以参考下面这个图

3.高清摄像机

就是利用高清摄像机一帧一帧的拍摄,这种更结合用户的体验,但是,对于应用端的同学是不友好,为啥呢?因为Launcher进程到ASM进程根据机型是不同的。

5.通用优化策略

1.前置加载优化

1.原理

利用Application.attachBaseContext提前进行初始化

ActivityThread.attath()->AMS.attachApplication()->sendMessage(CONTENT_PROVIDER_PUBLISH_TIMEOUT_MSG)->ActivityThread.handleBindApplication()->Application.attachBaseContext()->ActivityThread.installContentProviders->ContentProvider.onCreate()->AMS.publishContentProviders()->removeMessage(CONTENT_PROVIDER_PUBLISH_TIMEOUT_MSG)->Application.onCreate()

2.优化时间

2021-05-30 15:58:34.812 7269-7269/ D/haha: attachBaseContext.time:1622361514811
2021-05-30 15:58:34.836 7269-7269/ D/haha: onCreate.time:1622361514836
2021-05-30 16:01:23.952 8065-8065/? D/haha: attachBaseContext.time:1622361683952
2021-05-30 16:01:23.974 8065-8065/? D/haha: onCreate.time:1622361683974
2021-05-30 16:01:35.144 8171-8171/? D/haha: attachBaseContext.time:1622361695143
2021-05-30 16:01:35.166 8171-8171/? D/haha: onCreate.time:1622361695166

使用的是华为HUAWEI P20 Pro android 9.0,提速平均下来23ms。虽然不是很大,但是,蚊子腿也是肉。何况,很多场景为了优化1个毫秒,还把数据库给建索引呢!

2.异步加载优化

这个应该是最早大家想到的老三套的方案,虽然是老三套,但是,它的确是能有效提高的很多方式,比如,我们的有些WebView内核初始化需要500ms。因为它必须在主线程中,所以,只能选择延后加载。
2和3的方式需要梳理我们的一些需要初始化的业务的划分。
我司划分的维度如下四个:
1.在Application的主线程
2.在Application的异步线程
3.在Activity的空闲线程
4.即用即初始化

3.延迟加载优化

1.原理

利用 IdleHandler进行空闲处理:I
dleHandler 说白了,就是 Handler 机制提供的一种,可以在 Looper 事件循环的过程中,当出现空闲的时候,允许我们执行任务的一种机制。

    /*** Callback interface for discovering when a thread is going to block* waiting for more messages.*/public static interface IdleHandler {/*** Called when the message queue has run out of messages and will now* wait for more.  Return true to keep your idle handler active, false* to have it removed.  This may be called if there are still messages* pending in the queue, but they are all scheduled to be dispatched* after the current time.*/boolean queueIdle();}

2.优化耗时

和业务执行有关,业务在主线程耗时多少,就能优化多少

6.业务优化方案

1.原理

所谓业务优化策略,就是要结合具体的业务,结合TraceView的耗时市场,进行定位和优化。

2.优化场景

1.布局复杂度高

1.降低布局的层级。
2.对于可延后的场景使用ViewStub。

2.并发加载渲染图片

如果图片的加载使用了AsyncTask,建议将AsyncTask的线程池改为并发。

MyAsyncTask asynct = newMyAsyncTask(task);
asynct.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,0);

有些朋友知道AsyncTask串并行的发展史,就会困惑为啥现在又改回来,其实,我猜测是为了避免大量的抱怨,当并发同事访问一个资源时,有些同学并不会取出并发处理,从而会感觉组件的不好用。

3.Arouter初始化优化

1.原理
Arouter自动加载路由表的插件是使用的通过gradle插桩技术在编译期插入代码来达到自动加载路由表信息
2.实现

1.在 app module 的 build.gradle 中 加入:
apply plugin: 'com.alibaba.arouter'2.在项目的 build.gradle中加入:
buildscript {repositories {jcenter()}dependencies {classpath "com.alibaba:arouter-register:1.0.2"}
}

4.redex 重排列 class 文件

redex 是 Facebook 开源的一款字节码优化工具。
原理:
简单的说,通过文件重排列的目的,就是将启动阶段需要用到的文件在 APK 文件中排布在一起,尽可能的利用 Linux 文件系统的 pagecache 机制,用最少的磁盘 IO 次数,读取尽可能多的启动阶段需要的文件,减少 IO 开销,从而达到提升启动性能的目的。
问题:
对于大型项目会报一些异常,因此未曾引入测试,腾讯进行了处理,大家可以参考

5.针对verifyClass的hook处理

在类加载的过程中通过 Hook 去掉类验证的过程,可以在 systrace 生成的文件中看到 verifyClass 过程,因为需要校验方法的每一个指令,所以是一个比较耗时的操作(是一种思路,但是,随着对非官方API反射的限制,方案的路更短)

7.站在巨人肩膀

1.App Start Time
这篇博客大家有时间还是建议看下的,看下官方建议咱们怎么玩。
2.市场上最全的冷启动优化方案
3.冷启动优化
4.Bugly的字节码层面redex优化
5.支付宝的重排布优化启动速度
6.Redex官网
7.当前大厂使用的冷启动优化方案
8.应用性能衡量指标
9.Android热修复技术选择和原理分析
10.微信tinker导致冷启动变慢的问题优化
11.历时1年,上百万行代码!首次揭秘手淘全链路性能优化(格式逻辑很清晰)

启动优化之一——启动分析及优化方案相关推荐

  1. mysql性能优化-慢查询分析、优化索引和配置

    目录 一.优化概述 二.查询与索引优化分析 1性能瓶颈定位 Show命令 慢查询日志 explain分析查询 profiling分析查询 2索引及查询优化 三.配置优化 1)      max_con ...

  2. 真是环境下阿里云RDS实例mysql性能优化-慢查询分析、优化索引和配置

    1.实战线上数据服务器参数: 数据库类型:MySQL 5.6 : CPU:10核: 数据库内存:12000MB: 最大IOPS:100000: 最大连接数:2000: 存储空间:共200.00G,数据 ...

  3. 有了这篇你还说你不会redis性能优化、内存分析及优化

    点击上方 好好学java ,选择 星标 公众号重磅资讯,干货,第一时间送达 今日推荐:推荐19个github超牛逼项目!个人原创100W +访问量博客:点击前往,查看更多 来源: https://bl ...

  4. mysql索引分析和优化_MySQL索引分析和优化

    什么是索引? 索 引用来快速地寻找那些具有特定值的记录,所有MySQL索引都以B-树的形式保存.如果没有索引,执行查询时MySQL必须从第一个记录开始扫描整个表的 所有记录,直至找到符合要求的记录.表 ...

  5. 袁宝华 oracle,关键词优化难易分析_SEO优化难度分析 - 站长工具

    关键词优化难度预估 关键词优化难度预估即根据某个关键词的多项数据,通过算法算出该关键词的优化难度.优化难度数值越高,则说明该关键词优化难度越大. 关键词指数 关键词指数即关键词搜索次数数值,其反映出该 ...

  6. Java关键词去重,关键词优化难易分析_SEO优化难度分析 - 站长工具

    关键词优化难度预估 关键词优化难度预估即根据某个关键词的多项数据,通过算法算出该关键词的优化难度.优化难度数值越高,则说明该关键词优化难度越大. 关键词指数 关键词指数即关键词搜索次数数值,其反映出该 ...

  7. 启科php淘宝客系统,关键词优化难易分析_SEO优化难度分析 - 站长工具

    关键词优化难度预估 关键词优化难度预估即根据某个关键词的多项数据,通过算法算出该关键词的优化难度.优化难度数值越高,则说明该关键词优化难度越大. 关键词指数 关键词指数即关键词搜索次数数值,其反映出该 ...

  8. php base64 站长工具,关键词优化难易分析_SEO优化难度分析 - 站长工具

    关键词优化难度预估 关键词优化难度预估即根据某个关键词的多项数据,通过算法算出该关键词的优化难度.优化难度数值越高,则说明该关键词优化难度越大. 关键词指数 关键词指数即关键词搜索次数数值,其反映出该 ...

  9. linux启动时间极限优化,Linux启动时间的极限优化

    在上次完成嵌入式应用的Linux裁减后,Linux的启动时间仍需要7s左右,虽然勉强可以接受,但仍然没有达到我个人所追求的目标--2s以内.况且,在实际的商用环境中,设备可靠性的要求可是"5 ...

  10. 安卓性能优化之启动优化

    安卓性能优化之启动优化 两个定律 2-5-8原则 八秒定律 启动方式 冷启动 热启动 温启动 启动耗时统计 系统日志 adb命令 启动耗时分析 CPU Profile 工具介绍 使用方式 数据分析 C ...

最新文章

  1. bzoj 4025 二分图——线段树分治+LCT
  2. jQuery快速学习
  3. php-fpm:No pool defined解决方法
  4. 【API进阶之路】无法想象!大龄码农的硬盘里有这么多宝藏
  5. springMVC 解决硬编码问题
  6. C# dataTable实用例
  7. 微信支付开发(1)--普通商户申请、账户验证、签约、公众号授权流程详解
  8. 双IP双线路实现方式 先来说说双线单IP和双线双IP的区别
  9. matlab的解线性方程组
  10. 如何在不清空原有配置的情况下修改路由器密码??????
  11. 哈工大计算机学院张宏莉,计算机科学与技术学科博士研究生培养方案2014-哈工大计算机学院.DOC...
  12. python---打包exe文件运行自动化
  13. Python模块之---Pexpect
  14. Kafka 实战 (3):kafka安装部署·2
  15. 万卷书 - 21世纪的投资 21st Century Investing
  16. 图像处理算法(二)---图像常用颜色空间
  17. 关于MFC 绘制背景闪烁
  18. 对接主流ERP,喜报用报销串联业务场景
  19. 攻防世界WEB进阶之ics-05
  20. CVAT——2. CVAT简单使用

热门文章

  1. 拆分单元格怎么弄?合并起来了怎么办?
  2. java获得文件的md5码_java获取文件md5码
  3. 带协议解析的串口调试助手
  4. 【Java】【MySnake】仿贪吃蛇小游戏开源代码(持续更新)
  5. 携手李连杰壹基金计划 创慈善博客
  6. 学习新方法:帅到没朋友
  7. 幕布图像大小与投影仪亮度关系对应表,--怎样选投影机和银幕不用愁了
  8. 【论文阅读|浅读】DeepEmLAN: Deep embedding learning for attributed networks
  9. win10升级助手_详解win7升级win10系统方法
  10. i3cpu驱动xp_Intel英特尔Core i3/Core i5/Core i7系列CPU核芯显卡驱动 32Bit