博客结构
用简单通俗易懂的话来记录自己对对象存活判断算法的理解
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() {
@Override
public 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 consideration
r = 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);//---------------2
if (!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年,上百万行代码!首次揭秘手淘全链路性能优化(格式逻辑很清晰)