内容简介:前段时间做冷启动优化,刚好也很久没写博文了,觉得还是很有必要记录下。大部分开发者在遇到页面冷启动耗时初始化时,会首先考虑通过Handler.postDelay()方法延迟执行。但延迟多久合适?100ms?500ms?还是1s?延迟过晚,可能会有体验问题;延迟过早,对冷启动没效果。延迟的时间(比如200ms)在三星手机上测试时没问题,换了在华为手机试了就有问题了,然后就围绕着机型的适配不断调整延迟的时间,试图寻找最合适的值,结果发现根本就是不可能的。
前段时间做冷启动优化,刚好也很久没写博文了,觉得还是很有必要记录下。
一.常规操作
public class MainActivity extends Activity { private static final Handler sHandler = new Handler(Looper.getMainLooper()); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); sHandler.postDelay(new Runnable() { @Override public void run() { // 页面启动所需耗时初始化 doSomething(); } }, 200); } }复制代码
大部分开发者在遇到页面冷启动耗时初始化时,会首先考虑通过Handler.postDelay()方法延迟执行。但延迟多久合适?100ms?500ms?还是1s?
延迟过晚,可能会有体验问题;延迟过早,对冷启动没效果。延迟的时间(比如200ms)在三星手机上测试时没问题,换了在华为手机试了就有问题了,然后就围绕着机型的适配不断调整延迟的时间,试图寻找最合适的值,结果发现根本就是不可能的。
二.起始终止点
先来看一张图
上图是Google提供的冷启动流程图,可以看到冷启动的起始点时Application.onCreate()方法,结束点在ActivityRecord.reportLanuchTimeLocked()方法。
我们可以通过以下两种方式查看冷启动的耗时
1.查看Logcat
在 Android Studio Logcat 过滤关键字 “Displayed”,可以查看到如下日志:
2019-07-03 01:49:46.748 1678-1718/? I/ActivityManager: Displayed com.tencent.qqmusic/.activity.AppStarterActivity: +12s449ms
后面的12s449ms就是冷启动耗时
2.adb dump
通过终端执行“adb shell am start -W -S <包名/完整类名> ”
“ThisTime:1370”即为本次冷启动耗时(单位ms)
三、寻找有效结束回调
上面知道,冷启动计时起始点是Application.onCreate(),结束点是ActivityRecord.reportLanuchTimeLocked(),但这不是我们可以写业务写逻辑的地方啊,大部分应用业务都以Activity为载体,那么结束回调在哪?
1.IdleHandler
从冷启动流程图看,结束时间是在UI渲染完计算的,所以很明显,Activity生命周期中的onCreate()、onResume()、onStart()都不能作为冷启动的结束回调。
常规操作中用Handler.postDelay()问题在于Delay的时间不固定,但我们知道消息处理机制中,MessageQueue有个ArrayList<IdleHandler>
public final class MessageQueue { Message mMessages; priavte final ArrayList<IdleHandler> mIdelHandlers = new ArrayList<IdelHandler>(); Message next() { ... int pendingIdelHandlerCount = -1; // -1 only during first iteration for(;;) { ... // If first time idle, then get the number of idlers to run. // Idle handles only run if the queue is empty or if the first message // in the queue (possibly a barrier) is due to be handled in the future. if (pendingIdleHandlerCount < 0 && (mMessages == null || now < mMessages.when)) { pendingIdleHandlerCount = mIdleHandlers.size(); } if (pendingIdleHandlerCount <= 0) { // No idle handlers to run. Loop and wait some more. mBlocked = true; continue; } // Run the idle handlers. // We only ever reach this code block during the first iteration. for (int i = 0; i < pendingIdleHandlerCount; i++) { final IdleHandler idler = mPendingIdleHandlers[i]; mPendingIdleHandlers[i] = null; // release the reference to the handler boolean keep = false; try { keep = idler.queueIdle(); } catch (Throwable t) { Log.wtf(TAG, "IdleHandler threw exception", t); } } ... } } }复制代码
可以在列表中添加Idle任务,Idle任务列表只有MessageQueue队列为空时才会执行,也就是所在线程任务已经执行完时,线程处于空闲状态时才会执行Idle列表中的任务。
冷启动过程中,在Activity.onCreate()中将耗时初始化任务放置到Idle中
public class MainActivity extends Activity { private static final Handler sHandler = new Handler(Looper.getMainLooper()); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() { @Override public boolean queueIdle() { // 页面启动所需耗时初始化 doSomething(); return false; }}); } }复制代码
正常情况下,初始化任务是在UI线程所有任务执行完才开始执行,且该方案也不用考虑机型问题。但有个问题,如果UI线程的任务一直不执行完呢?会有这情况?举个:chestnut:,Activity首页顶部有个滚动的Banner,banner的滚动是通过不断增加延迟Runnable实现。那么,初始化任务就可能一直没法执行。
另外,如果初始化的任务会关系到UI的刷新,这时,在Activity显示后再去执行,在体验上也可能会有所折损。
回顾冷启动流程图,冷启动结束时,刚好是UI渲染完,如果我们能确保在UI渲染完再去执行任务,这样,既能提升冷启动数据,又能解决UI上的问题。
因此,解铃还须系铃人,要想找到最合适的结束回调,还是得看源码。
2.onWindowFocusChanged()
首先,我们找到了第一种方案
public class BaseActivity extends Activity { private static final Handler sHandler = new Handler(Looper.getMainLooper()); private boolean onCreateFlag; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); onCreateFlag = true; setContentView(R.layout.activity_main); } @Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); if (onCreateFlag && hasFocus) { onCreateFlag = false; sHandler.post(new Runnable() { @Override public void run() { onFullyDrawn(); } }) } } @CallSuper protected void onFullyDrawn() { // TODO your logic } }复制代码
关于onWindowFocusChanged()的系统调用流程感兴趣的可以看看我的上一篇文章 《Activity.onWindowFocusChanged()调用流程》
至于为什么要在onWindowFocusChanged()再通过Handler.post()延后一个任务,一开始我是通过打点,发现没post()时,onWindowFocusChanged()打点在Log“Displayed”之前,增加post()便在Log“Displayed”之后,梳理了下调用流程,大概是渲染调用requestLayout()也是增加任务监听,只有SurfaceFlinger渲染信号回来时才会触发渲染,因此延后一个任务,刚好在其之后
3.View.post(Runnable runnable)
第二种方案,我们通过View.post(Runnable runnable)方法实现
public class BaseActivity extends Activity { private static final Handler sHandler = new Handler(Looper.getMainLooper()); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } // 方案只有在onResume()或之前调用有效 protected void postAfterFullDrawn(final Runnable runnable) { if (runnable == null) { return; } getWindow().getDecorView().post(new Runnable() { @Override public void run() { sHandler.post(runnable); } }); } }复制代码
需要注意的是,该方案只有在onResume()或之前调用有效。为什么?
先看View.post()源码实现
public boolean post(Runnable action) { final AttachInfo attachInfo = mAttachInfo; // 这里要注意啦!attachInfo 不为空,实际是通过Handler.post()延迟一个任务 if (attachInfo != null) { return attachInfo.mHandler.post(action); } // Postpone the runnable until we know on which thread it needs to run. // Assume that the runnable will be successfully placed after attach. getRunQueue().post(action); return true; } private HandlerActionQueue mRunQueue; private HandlerActionQueue getRunQueue() { if (mRunQueue == null) { mRunQueue = new HandlerActionQueue(); } return mRunQueue; }复制代码
通过View.post()调用了HandlerActionQueue.post()
public class HandlerActionQueue { private HandlerAction[] mActions; private int mCount; public void post(Runnable action) { postDelayed(action, 0); } /** * 该方法仅仅是将传入的任务Runnable存放到数组中 **/ public void postDelayed(Runnable action, long delayMillis) { final HandlerAction handlerAction = new HandlerAction(action, delayMillis); synchronized (this) { if (mActions == null) { mActions = new HandlerAction[4]; } mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction); mCount++; } } }复制代码
到此,我们调用View.post(Runnable runnable)仅仅是把任务Runnable以HandlerAction姿势存放在HandlerActionQueue的HandlerAction[]数组中。那这个数组什么时候会被访问调用?
既然是冷启动,那还是得看冷启动系统的回调,直接看ActivityThread.handleResumeActivity()
final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) { ActivityClientRecord r = mActivities.get(token); ... r = performResumeActivity(token, clearHide, reason); ... if (r != null) { 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; ViewRootImpl impl = decor.getViewRootImpl(); if (impl != null) { impl.notifyChildRebuilt(); } } if (a.mVisibleFromClient) { if (!a.mWindowAdded) { a.mWindowAdded = true; // 上面一大串操作基本可以不看,因为到这我们基本都知道下一步是渲染,也就是ViewRootImpl上场了 wm.addView(decor, l); } else { a.onWindowAttributesChanged(l); } } } } }复制代码
到渲染了,直接进ViewRootImpl.performTraversals()
public final class ViewRootImpl implements ViewParent, View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks { boolean mFirst; public ViewRootImpl(Context context, Display display) { ... mFirst = true; // true for the first time the view is added ... } private void performTraversals() { final View host = mView; ... if (mFirst) { ... host.dispatchAttachedToWindow(mAttachInfo, 0); ... } ... performMeasure(); performLayout(); preformDraw(); ... mFirst = false; } }复制代码
再进到View.dispatchAttachedToWindow()去瞧瞧
void dispatchAttachedToWindow(AttachInfo info, int visibility) { // 倒车请注意!倒车请注意!这里mAttachInfo != null啦! mAttachInfo = info; ... // Transfer all pending runnables. // 系统也提示了,到这里执行pending的任务runnbales if (mRunQueue != null) { mRunQueue.executeActions(info.mHandler); mRunQueue = null; } ... } // 开始访问前面存放的任务,看看executeActions()怎么工作 public class HandlerActionQueue { private HandlerAction[] mActions; /** * 我裤子都脱了,你给我看这些?实际也是调用Handler.post()执行任务 **/ public void executeActions(Handler handler) { synchronized (this) { final HandlerAction[] actions = mActions; for (int i = 0, count = mCount; i < count; i++) { final HandlerAction handlerAction = actions[i]; handler.postDelayed(handlerAction.action, handlerAction.delay); } mActions = null; mCount = 0; } } }复制代码
也就是说,View内部维护了一个HandlerActionQueue,我们可以在DecorView attachToWindow前,通过View.post()将任务Runnables存放到HandlerActionQueue中。当DecorView attachToWindow时会先遍历先前存放在HandlerActionQueue的任务数组,通过handler挨个执行。
1.在View.dispatchAttachedToWindow()时mAttachInfo就被赋值了,因此,之后通过View.post()实际就是直接调用Handler.post()执行任务。再往前看,performResumeActivity()在渲染之前先执行,也就说明了为什么只有在onResume()或之前调用有效
2.在View.post()的Runnable run()方法回调中在延迟一个任务,从performTraverals()
调用顺序看刚好是在渲染完后下一个任务执行
四.被忽略的Theme
先来看两张效果图
第一张点击完桌面Icon后并没有马上拉起应用,而是停顿了下,给人感觉是手机卡顿了;
第二张点击完桌面Icon后立即出现白屏,然后隔了一段时间后才出现背景图,体验上很明显觉得是应用卡了。
那是什么导致它们的差异?答案就是把闪屏Activity主题设置成全屏无标题栏透明样式
<activity android:name="com.huison.test.MainActivity" ... android:theme="@style/TranslucentTheme" /> <style name="TranslucentTheme" parent="android:Theme.Translucent.NoTitleBar.Fullscreen" />复制代码
这样可以解决冷启动白屏或黑屏问题,体验上会更好。
五.总结
关于冷启动优化,总结为12个字“ 减法为主,异步为辅,延迟为补 ”
减法为主
尽量做减法,能不做的尽量不做!
Application.onCreate()一定要轻!一定要轻!一定要轻!项目中多多少少会涉及到第三方SDK的接入,但不要全部在Application.onCreate()中初始化,尽量懒加载。
Debug包可以加日志打印和部分统计,但Release能不加的就不加
异步为辅
耗时任务尽量异步!见过好多RD都不怎么喜欢做回调,获取某个状态值时,即使调用的函数很耗时,也是直接调用,异步回调重新刷新转态值也能满足业务需求。
当然也不是所有的场景都采用异步回调,因为异步就涉及线程切换,在某些场景下可能会出现闪动,UI体验极差,所以说要尽量!
延迟为补
其实前面找结束点都是为延迟铺路的,但延迟方案并不是最佳的,当我们把冷启动的任务都延迟到结束时执行,冷启动是解决了,但有可能出现结束时任务过多、负载过大而引发其他问,比如ANR、交互卡顿。以前做服务端时,前端(当时几百万DAU)有一个哥们直接写死早上9点请求某个接口,导致接口直接报警了,如果他把9点改为10点,结果肯定一样,后面改成了区段性随机请求,这样就把峰值磨平了。同样,冷启动过程如果把任务都延迟到结束点,那结束点也有可能负载过大出问题。
削峰填谷,离散化任务,合理地利用计算机资源才是解决根本问题!
其他
1.冷启动尽量减少SharedPreferences使用,尤其是和文件操作一起,底层ContextImpl同步锁经常直接卡死。网上有人说用微信的MMKV替换SP,我试了下,效果不是很明显,可能和项目有关系吧,不过MMKV初始化也需要时间。
2.关注冷启动的常驻内存和GC情况,如果GC过于频繁也会有所影响,支付宝做过这方面的分析
支付宝客户端架构解析:Android 客户端启动速度优化之「垃圾回收」到此,冷启动优化总结也算告一段落,有人会问做了那么多,效果到底如何?好像是哦,最怕就是"一顿操作猛如虎,上线review二百五"!GP-Vitals有冷启动指标,项目优化前冷启动时间过长(>5s)百分比为3.63%,一顿操作后百分比下降到0.95%,哇!Surprise!
以上所述就是小编给大家介绍的《老生常谈——Android冷启动优化》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 老生常谈分布式锁
- 老生常谈,HashMap的死循环
- 老生常谈:Promise 用法与源码分析
- 《iOS面试题 - 老生常谈》之提示答案
- 老生常谈,TCP 为啥要三次握手?
- 老生常谈,TCP 为啥要三次握手?
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
运营其实很简单:互联网运营进阶之道
郑文博 / 人民邮电出版社 / 2018-2 / 49.80元
为了帮助从事运营或即将从事运营的广大读者更好、更快地了解运营、学习运营、入职运营,本书详细阐述运营对于用户、企业的帮助,同时以单个理论点 单个实战案例的方式详细分析了社群运营、活动运营、新媒体运营、内容运营、渠道运营、精细化运营、场景化运营、用户化运营、商业化运营等模块及运营工作、渠道整合、社群知识、渠道优化、SOP流程等细节,力求让读者在求职路上快速上手,在迷茫途中快速定位。 《运营其实很简单 ......一起来看看 《运营其实很简单:互联网运营进阶之道》 这本书的介绍吧!