深入理解 Activty 加载速度优化,android 开发实战 - 记账本清风紫雪
}}
//mTraversalRunnable 就是这个类的对象 final class TraversalRunnable implements Runnable {@Overridepublic void run() {doTraversal();}}final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
void doTraversal() {if (mTraversalScheduled) {mTraversalScheduled = false;mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {Debug.startMethodTracing("ViewAncestor");}//这个方法应该很敏感,很有名的一个方法 就不分析他了 太长了,超出篇幅。performTraversals();
if (mProfile) {Debug.stopMethodTracing();mProfile = false;}}}
分析到这里,应该可以稍微理一理 activity 绘制的一个大概流程:
1.activitythread 调用 handleresumeactivity 方法 也就是 先回调 onresume 方法 2.scheduleTraversals post 了一个 TraversalRunnable 消息。 3.post 的这个消息做了一件事 调用了绘制 ui 的核心方法 performTraversals。
这个流程也再次验证了方案 a 利用 oncreate 和 onresume 时间差的不靠谱
方案 C:IdleHandler
方案 C 是一个接近靠谱的方法。在阐述这个方法之前,我们先用一张图回归一下 Handler Looper 和 MessageQueue 这个东西。
简单来说一下这三者之间的关系: Handler 通过 sendMessage 将消息投递给 MessageQueue,Looper 通过消息循环(loop)不断的从 MessageQueue 中取出消息,然后消息被 Handler 的 dispatchMessage 分发到 handleMessage 方法消费掉。
然后我们看一个特殊的源码,来自于 MessageQueue:
注意看他的注释:
其实意思就是说,如果我们 looper 里的消息都处理完了,那么就会回调这个接口,如果这个方法返回 false,那么回调这一次以后就会把这个 idleHandler 给干掉,如果返回 true,那么消息处理完毕就继续调用这个 iderHandler 接口的 queueidle 方法。
so:我们的正确方案 C 就呼之欲出了:
t1 就是 oncreate 方法的时间戳。 第一个标注红线的 显然是被证明过错误的做法。 而第二个标注红线的 显然是正确的做法。 前面已经分析过,activity 的绘制正是从往 ui 线程的 handler 里 post 的 一个消息开始,那么这个消息对应的动作全部处理结束以后, 显然就回回调我们这个 idleHandler 的了。所以这个方法是目前为止最通用最准确 获取 activity 启动以后到显示东西到屏幕这一段时间 最准确的方法。
知道 activity 启动时间了以后能做什么?
简单来说,在大部分低端手机中,我们总是希望用户进入一个新页面的时候能尽快看到这个页面想要展示的内容,尤其在弱网环境 或者大量数据需要从网络中获取时,我们总是希望界面能先展示一些固定的结构,甚至基本要素。然后等对应的接口回来以后再进去 填充数据,否则页面白白的区域显示时间过长,体验不佳(这点头条新浪微博微信等做的尤其出色)
如何加快 activity 的启动时间?
cpu 的时间片总是固定的,硬件所限,为了让 ui 线程尽快的处理完毕,我们总是希望这一段时间内尽可能的只有 ui 线程在跑, 这样 ui 线程获取的时间片更多,执行速度起来就会很快,如果你一开始就在 oncreate 方法里做了太多的诸如网络操作, io 操作,数据库操作,那必然的是 ui 线程获取 cpu 时间变少,速度变慢。
确定我们的延迟加载方案
我们来看这样一段程序:
TextView textView;
@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);textView = (TextView) findViewById(R.id.tv);Log.v("wuyue", "textView height==" + textView.getWidth());}
@Overrideprotected void onResume() {super.onResume();if (Build.VERSION.SDK_INT >= Build.VERSION
_CODES.M) {Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {@Overridepublic boolean queueIdle() {Log.v("wuyue", "textView height2==" + textView.getWidth());return false;}});}}
很显然,第一种在 oncreate 方法里获取 tv 的高度肯定获取不到因为这会还没绘制结束呢。 第二种就可以拿到了,原因前面已经说过了。不多讲。
日志也反应了我们的正确性。
那么有没有更好的方法来证明这个是正确的呢?
可以用 android studio 的 method trace 来看方法的执行轨迹,ddms 的 method profiling 也可以。这 2 个工具在这里不多介绍了。 是查卡顿的很重要的方法,各位自行百度谷歌使用方法即可。
除了启动优化以外,我们还可以做些什么?
前面讲述的是 activity 的启动优化,实际上,我们更希望实时的知道我们 app 运行的具体情况,比如滑动的时候到底有没有卡顿? 如果有卡顿发生,怎么知道大概在哪里出现了问题以便我们迅速定位到问题代码?
adb shell dumpsys gfxinfo
这个命令大家都很熟悉,可获取最新 128 帧的绘制信息,详细包括每一帧绘制的 Draw,Process,Execute 三个过程的耗时,如果这三个时间总和超过 16.6ms 即认为是发生了卡顿。 但是我们不可能每次到一个页面都去手动执行以下这个命令,太麻烦了,而且 不同的手机还要多次打这个命令,线上实际生产版本也没办法让用户来打这个命令获取结果,所以实际上这个方法并不使用。 还是需要在代码层面下功夫
Looper 代码揭秘
ui 线程绑定的 looper 的 loop 方法 无限循环跑这段代码,执行 dispatch 方法,注意这个方法的前后都有 logging 的输出。 那么这 2 个 logging 输出的时间差 是不是就可以认为这是我们执行 ui 线程的时间吗?这个时间长不就代表了 ui 线程有卡顿现象么?
同时我们到 这个 me.mLogging 还可以通过 public 的 set 方法来设置。
确定思路设计抓取卡顿信息的方案。
通过 setMessageLogging 方法来设置我们自定义的 printer。
自定义的 printer 要重写 println 方法,判断如果是 dispatch 方法前后的日志格式输出,那么就要计算时间戳。
超过这个时间戳就认为卡顿了,输出线程上下文堆栈信息 看看是哪里,哪个方法出现了卡顿。
重要代码
自定义 printer
package com.suning.mobile.ebuy;
import android.os.Looper;import android.util.Printer;
public class CustomPrinterForGetBlockInfo {public static void start() {Looper.getMainLooper().setMessageLogging(new Printer() {//日志输出有很多种格式,我们这里只捕获 ui 线程中 dispatch 上下文的日志信息//所以这里定义了 2 个 key 值,注意不同的手机这 2 个 key 值可能不一样,有需要的话这里要做机型适配,//否则部分手机这里可能抓取不到日志信息 private static final String START = ">>>>> Dispatching";private static final String END = "<<<<< Finished";@Overridepublic void println(String x) {//这里的思路就是如果发现在打印 dispatch 方法的 start 信息,//那么我们就在 “时间戳” 之后 post 一个 runnableif (x.startsWith(START)) {LogMonitor.getInstance().startMonitor();}//因为我们 start 不是立即 start runnable 而是在“时间戳” 之后 那么如果在这个时间戳之内//dispacth 方法执行完毕以后的 END 到来,那么就会 remove 掉这个 runnable//所以 这里就知道 如果 dispatch 方法执行时间在时间戳之内 那么我们就认为这个 ui 没卡顿,不输出任何卡顿信息//否则就输出卡顿信息 这里卡顿信息主要用 StackTraceElement 来输出 if (x.startsWith(END)) {LogMonitor.getInstance().removeMonitor();}}});}}
看看我们的 LogMoniter
package com.suning.mobile.ebuy;
import android.os.Handler;import android.os.HandlerThread;import android.os.Looper;import android.util.Log;
public class LogMonitor {private static LogMonitor sInstance = new LogMonitor();//HandlerThread 这个其实就是一个 thread,只不过相对于普通的 thread 他对外暴露了一个 looper 而已。方便//我们和 handler 配合使用 private HandlerThread mLogThread = new HandlerThread("BLOCKINFO");private Handler mIoHandler;//这个时间戳的值,通常设置成不超过 1000,你可以调低这个数值来优化你的代码。数值越低 暴露的信息就越多 private static final long TIME_BLOCK = 1000L;
private LogMonitor() {mLogThread.start();mIoHandler = new Handler(mLogThread.getLooper());}
private static Runnable mLogRunnable = new Runnable() {@Overridepublic void run() {StringBuilder sb = new StringBuilder();//把 ui 线程的 block 的堆栈信息都打印出来 方便我们定位问题 StackTraceElement[] stackTrace = Looper.getMainLooper().getThread().getStackTrace();for (StackTraceElement s : stackTrace) {
评论