GTLC全球技术领导力峰会·上海站,首批讲师正式上线! 了解详情
写点什么

Android 性能优化之活动启动耗时分析

2019 年 9 月 18 日

Android性能优化之活动启动耗时分析

活动的启动速度是很多开发者关心的问题,当页面跳转耗时过长时,App 就会给人一种非常笨重的感觉。在遇到某个页面启动过慢的时候,开发的第一直觉一般是 onCreate 执行速度太慢了,然后在 onCreate 方法前后记录下时间戳计算出耗时。不过有时候即使把 onCreate 方法的耗时优化了,效果仍旧不明显。实际上影响到活动启动速度的原因是多方面的,需要从活动的启动流程入手,才能找到真正问题所在。


0。目录

活动启动流程


  • ActiivtyA 暂停流程

  • ActivityB 启动流程

  • ActivityB 渲染流程


耗时统计方案


  • 系统耗时统计

  • 三种耗时

  • 暂停耗时

  • 启动耗时

  • 渲染耗时

  • 应用内统计方案

  • 钩仪表

  • Hook Looper-Printer

  • Hook ActivityThread $ H


总结


1。活动启动流程

如果要给活动的“启动”做一个定义的话,个人觉得应该是:从调用 startActivity 到活动可被操作为止,代表启动成功。所谓的可被操作,是指可接受各种输入事件,比如手势,键盘输入之类的。换个角度来说,也可以看成是主线程处于空闲状态,能执行后续进入的各种消息。


活动的启动可以分为三个步骤,以 ActivityA 启动 ActivityB 为例,三步骤分别为:


  • 以 ActivityA 调用 startActivity,到 ActivityA 成功暂停为止

  • ActivityB 成功初始化,到执行完恢复为止

  • ActivityB 向 WSM 注册窗口,到第一帧绘制完成为止


活性启动涉及到应用进程与 ActivityManagerService(AMS),WindowManagerService(WMS)的通信,网上关于这个流程的文章很多,这边就不再具体描述了,只列一下关键方法的调用链路。


ActiivtyA 暂停流程

当 ActivityA 使用 startActivity 方法启动 ActivityB 时,执行函数链路如下:


ActivityA.startActivity->Instrumentation.execStartActivity->ActivityManagerNative.getDefault.startActivity->ActivityManagerService.startActivityAsUser->ActivityStarter.startActivityMayWait->ActivityStarter.startActivityLocked->ActivityStarter.startActivityUnchecked->ActivityStackSupervisor.resumeFocusedStackTopActivityLocked->ActivityStack.resumeTopActivityUncheckedLocked->ActivityStack.resumeTopActivityInnerLocked->ActivityStack.startPausingLocked->ActivityThread$$ApplicationThread.schedulePauseActivity->ActivityThread.handlePauseActivity-> └ActivityA.onPauseActivityManagerNative.getDefault().activityPaused
复制代码


当 App 请求 AMS 要启动一个新页面的时候,AMS 首先会暂停掉当前正在显示的活动,当然,这个活动可能与请求要开启的活动不在一个进程,比如点击桌面图标启动 App,当前要暂停的活动就是桌面程序 Launcher。在 onPause 内执行耗时操作是一种很不推荐的做法,从上述调用链路可以看出,如果在 onPause 内执行了耗时操作,会直接影响到 ActivityManagerNative.getDefault()。 activityPaused()方法的执行,而这个方法的作用就是通知 AMS,“当前活动已经已经成功暂停,可以启动新活动了”。


ActivityB 启动流程

在 AMS 接收到应用程序进程对于 activityPaused 方法的调用后,执行函数链路如下


ActivityManagerService.activityPaused->ActivityStack.activityPausedLocked->ActivityStack.completePauseLocked->ActivityStackSupervisor.resumeFocusedStackTopActivityLocked->ActivityStackSupervisor.resumeFocusedStackTopActivityLocked->ActivityStack.resumeTopActivityUncheckedLocked->ActivityStack.resumeTopActivityInnerLocked->ActivityStackSupervisor.startSpecificActivityLocked-> └1.启动新进程:ActivityManagerService.startProcessLocked 暂不展开 └2.当前进程:ActivityStackSupervisor.realStartActivityLocked->ActivityThread$$ApplicationThread.scheduleLaunchActivity->Activity.handleLaunchActivity-> └Activity.onCreate └Activity.onRestoreInstanceState └handleResumeActivity   └Activity.onStart->   └Activity.onResume->   └WindowManager.addView->
复制代码


AMS 在经过一系列方法调用后,通知 App 进程正式启动一个活动所在进程不存在,比如点击桌面图标第一次打开应用,或者 App 本身就是多进程的,要启动的新页面处于活动生命周期内的 onCreate,onRestoreInstanceState,onStart,onResume 方法,


这一步的耗时基本也可以看成就是这四个方法的耗时,由于这四个方法是同步调用的,所以可以通过以 onCreate 方法为起点,onResume 方法为终点,统计出这一步骤的总耗时。


ActivityBRender 流程

在 ActivityB 执行完的 onResume 方法后,就可以显示该活动了,调用流程如下:


WindowManager.addView->WindowManagerImpl.addView->ViewRootImpl.setView->ViewRootImpl.requestLayout-> └ViewRootImpl.scheduleTraversals-> └Choreographer.postCallback->WindowManagerSerivce.add
复制代码


这一步的核心实际上是 Choreographer.postCallback,向编导注册了一个回调,当垂直同步事件到来时,就会执行下面的回调进行 UI 的渲染。


ViewRootImpl.doTraversal->ViewRootImpl.performTraversals-> └ViewRootImpl.relayoutWindow └ViewRootImpl.performMeasure └ViewRootImpl.performLayout └ViewRootImpl.performDrawViewRootImpl.reportDrawFinished
复制代码


这里分别执行了 performMeasure,performLayout,performDraw,实际上就是是对应到 DecorView 的测量,布局,绘制三个流程。由于 Android 的 UI 是个树状结构,作为根查看的 DecorView 的测量,布局,绘制,会调用到所有子查看相应的方法,因此,这一步的总耗时就是所有子视图在测量,布局,绘制中的耗时之和,如果某个子视图在这三个方法中如果进行了耗时操作,就会拖慢整个 UI 的渲染,进而影响活动第一帧的渲染速度。


2。耗时统计方案

知道了 Actviity 启动流程的三个步骤和对应的方法耗时统计方法,那该如何设计一个统计方案呢?在这之前,可以先看看系统提供的耗时统计方法。


系统-耗时统计

打开 Android Studio 的 Logcat,输入过滤关键字 ActivityManager,在启动一个 Actviity 后就能看到如下日志:


末尾的+ 59ms 便是启动该活动的耗时。这个日志是 Android 系统在 AMS 端直接输出的,“WMS 常见问题一(活动显示延迟)” 这篇文章分析了系统耗时统计的方法,简单来说,上述日志是通过 ActivityRecord.reportLaunchTimeLocked 方法打印出来的。


 ActivityRecord.java
private void reportLaunchTimeLocked(final long curTime) { ...... final long thisTime = curTime - displayStartTime; final long totalTime = stack.mLaunchStartTime != 0 ? (curTime - stack.mLaunchStartTime) : thisTime; if (SHOW_ACTIVITY_START_TIME) { Trace.asyncTraceEnd(TRACE_TAG_ACTIVITY_MANAGER, "launching: " + packageName, 0); EventLog.writeEvent(AM_ACTIVITY_LAUNCH_TIME, userId, System.identityHashCode(this), shortComponentName, thisTime, totalTime); StringBuilder sb = service.mStringBuilder; sb.setLength(0); sb.append("Displayed "); sb.append(shortComponentName); sb.append(": "); TimeUtils.formatDuration(thisTime, sb); if (thisTime != totalTime) { sb.append(" (total "); TimeUtils.formatDuration(totalTime, sb); sb.append(")"); } Log.i(TAG, sb.toString()); } ...... }
复制代码


其中 displayStartTime 是在 ActivityStack.setLaunchTime()方法中设置的,具体调用链路:


ActivityStackSupervisor.startSpecificActivityLocked->   └ActivityStack.setLaunchTimeActivityStackSupervisor.realStartActivityLocked->ActivityThread$$ApplicationThread.scheduleLaunchActivity->Activity.handleLaunchActivity->ActivityThread$$ApplicationThread.scheduleLaunchActivity->
Activity.handleLaunchActivity->
复制代码


在 ActivityStackSupervisor.startSpecificActivityLocked 方法中调用了 ActivityStack.setLaunchTime(),而 startSpecificActivityLocked 方法最终会走到 App 端的 Activity.onCreate 方法,所以统计开始的时间实际上就是是 App 启动中的第二步开始的时间。


而 ActivityRecord.reportLaunchTimeLocked 方法自身的调用链如下:


ViewRootImpl.reportDrawFinished->Session.finishDrawing->WindowManagerService.finishDrawingWindow->WindowSurfacePlacer.requestTraversal->WindowSurfacePlacer.performSurfacePlacement->WindowSurfacePlacer.performSurfacePlacementLoop->RootWindowContainer.performSurfacePlacement->WindowSurfacePlacer.handleAppTransitionReadyLocked->WindowSurfacePlacer.handleOpeningApps->AppWindowToken.updateReportedVisibilityLocked->AppWindowContainerController.reportWindowsDrawn->ActivityRecord.onWindowsDrawn->ActivityRecord.reportLaunchTimeLocked
复制代码


在启动流程第三步 UI 渲染完成后,App 会通知 WMS,紧接着 WMS 执行一系列和切换动画相关的方法后,调用到 ActivityRecord.reportLaunchTimeLocked,最终打印出启动耗时。


由上述流程可以看到,系统统计并没有把 ActivityA 的暂停操作耗时计入活动启动耗时中。不过,如果我们在 ActivityA 的 onPause 中做一个 Thread.sleep(2000)操作,会很神奇地看到系统打印的耗时也跟着变了。


这次启动耗时变成了 1.571s,明显是把 onPause 的时间算进去了,但是却小于 onPause 内休眠的 2 秒。其实,这是由于 AMS 对于暂停操作的超时处理导致的,在 ActivityStack.startPausingLocked 方法中,会执行到 schedulePauseTimeout 方法。


    ActivityThread.java
private static final int PAUSE_TIMEOUT = 500;
private void schedulePauseTimeout(ActivityRecord r) { final Message msg = mHandler.obtainMessage(PAUSE_TIMEOUT_MSG); msg.obj = r; r.pauseTime = SystemClock.uptimeMillis(); mHandler.sendMessageDelayed(msg, PAUSE_TIMEOUT); if (DEBUG_PAUSE) Slog.v(TAG_PAUSE, "Waiting for pause to complete..."); }
...
private class ActivityStackHandler extends Handler {
@Override public void handleMessage(Message msg) { switch (msg.what) { case PAUSE_TIMEOUT_MSG: { ActivityRecord r = (ActivityRecord)msg.obj; // We don't at this point know if the activity is fullscreen, // so we need to be conservative and assume it isn't. Slog.w(TAG, "Activity pause timeout for " + r); synchronized (mService) { if (r.app != null) { mService.logAppTooSlow(r.app, r.pauseTime, "pausing " + r); } activityPausedLocked(r.appToken, true); } } break;
复制代码


这个方法的作用在于,如果过了 500ms,上一个要暂停活动的进程还没有回调 activityPausedLocked 方法,AMS 就会自己调用 activityPausedLocked 方法,继续之后的启动流程。所以过了 500ms 之后,AMS 就会通知 App 进程启动 ActivityB 的操作,然而此时 App 进程仍旧被 onPause 的 Thread.sleep 阻塞着,所以只能再等待 1.5s 才能继续操作,因此打印出来的时间是 2s-0.5s +正常的耗时。


三种耗时

说完了系统的统计方案,接下去介绍下应用内的统计方案。根据前面的介绍,若想自己实现活动的启动耗时统计功能,只需要以 startActivity 执行为起始点,以第一帧渲染为结束点,就能得出一个较为准确的耗时。不过,这种统计方式无法帮助我们定位具体的问题,当遇到一个页面启动较慢时,我们可能需要知道它具体慢在哪里。而且,由于启动过程中涉及到大量的系统进程耗时和 App 端 Framework 层的方法耗时,这块耗时又是难以对其进行干涉的,所以接下去会会统计的重点放在通过编码能影响到的耗时上,按照启动流程的三个步骤,划分为三种耗时。


暂停耗时

尽管启动活动的起点是 startActivity 方法,但是从调用这个方法开始,到 onPause 被执行到为止,其实都是 App 端框架层与 AMS 之间的交互,所以这里把第一阶段 Pause 的耗时统计放在 onPause 方法开始时候。这一块的统计也很简单,只需要计算一下 onPause 方法的耗时就足够了。


有些同学可能会疑惑:是否 onStop 也要计入 Pause 耗时。并且需要,onStop 操作其实是在主线程空余时才会执行的,在 Activity.handleResumeActivity 方法中,会执行 Looper.myQueue()。addIdleHandler (new Idler())方法,Idler 定义如下:


 ActivityThread.java
private class Idler implements MessageQueue.IdleHandler { @Override public final boolean queueIdle() { ...... am.activityIdle(a.token, a.createdConfig, ...... return false; } }
复制代码


addIdleHandler 表示会放入一个低优先级的任务,只有在线程空闲的时候才去执行,而 am.activityIdle 方法会通知 AMS 找到处于停止状态的活动,通过 Binder 回调 ActivityThread.scheduleStopActivity,最终执行到 onStop。而这个时候,UI 第一帧已经渲染完毕。


启动耗时

启动耗时可以通过 onCreate,onRestoreInstanceState,onStart,onResume 四个函数的耗时相加得出。在这四个方法中,onCreate 一般是最重的那个方法,因为很多变量的初始化都会放在这里进行。


另外,onCreate 方法中还有个耗时大户是 LayoutInfalter.infalte 方法,调用 setContentView 会执行到这个方法,对于一些复杂布局的第一次解析,会消耗大量时间。由于这四个方法是同步顺序执行的,单独把某些操作从 onCreate 移到 onResume 之类的并没有什么意义,启动耗时只关心这几个方法的总耗时。


渲染耗时

从 onResume 执行完成到第一帧渲染完成所花费的时间就是 Render 耗时.Render 耗时可以用三种方式计算出来。


第一种方法-IdleHandler:


Activity.java
@Override protected void onResume() { super.onResume(); final long start = System.currentTimeMillis(); Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() { @Override public boolean queueIdle() { Log.d(TAG, "onRender cost:" + (System.currentTimeMillis() - start)); return false; } }); }
复制代码


前面说过 IdleHandler 只会在线程处于空闲的时候被执行。


第二种方法-DecorView 的两次帖子:


Activity.java
@Override protected void onResume() { super.onResume(); final long start = System.currentTimeMillis(); getWindow().getDecorView().post(new Runnable() { @Override public void run() { new Hanlder().post(new Runnable() { @Override public void run() { Log.d(TAG, "onPause cost:" + (System.currentTimeMillis() - start)); } }); } }); }
View.java
public boolean post(Runnable action) { final AttachInfo attachInfo = mAttachInfo; 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; }
void dispatchAttachedToWindow(AttachInfo info, int visibility) { mAttachInfo = info; ...... // Transfer all pending runnables. if (mRunQueue != null) { mRunQueue.executeActions(info.mHandler); mRunQueue = null; } ...... }
ViewRootImpl.java
private void performTraversals() { ...... // host即DecorView host.dispatchAttachedToWindow(mAttachInfo, 0); ....... performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); ....... performLayout(lp, mWidth, mHeight); ....... performDraw(); ....... }
复制代码


通过 getWindow()。getDecorView()获取到 DecorView 后,调用 post 方法,此时由于 DecorView 的 attachInfo 为空,会将这个 Runnable 放置 runQueue 中.runQueue 内的任务会在 ViewRootImpl.performTraversals 的开始阶段被依次取出执行,我们知道这个方法内会执行到 DecorView 的测量,布局,绘制操作,不过 runQueue 的执行顺序会在这之前,所以需要再进行一次 post 操作。第二次的 post 操作可以继续用 DecorView()。post 或者其普通 Handler.post(),并无影响。此时 mAttachInfo 已不为空,DecorView()。post 也是调用了 mHandler.post()。


第三种方法-new Handler 的两次帖子:


  Activity.java
@Override protected void onResume() { super.onResume(); final long start = System.currentTimeMillis(); new Handler.post(new Runnable() { @Override public void run() { getWindow().getDecorView().post(new Runnable() { @Override public void run() { Log.d(TAG, "onPause cost:" + (System.currentTimeMillis() - start)); } }); } }); }
复制代码


乍看一下第三种方法和第二种方法区别不大,实际上原理大不相同。这是因为 ViewRootImpl.scheduleTraversals 方法会往主线程队列插入一个屏障消息,代码如下所示:


 ViewRootImpl.java
void scheduleTraversals() { ...... mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier(); mChoreographer.postCallback( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); ...... } }
复制代码


屏障消息的作用在于阻塞在它之后的同步消息的执行,当我们在 onResume 方法中执行第一次 new Handler()。post 方法,向主线程消息队列放入一条消息时,从前面的内容可以知道 onResume 是在 ViewRootImpl.scheduleTraversals 方法之前执行的,所以这条消息会在屏障消息之前,能被正常执行; 而第二次 post 的消息就在屏障消息之后了,必须等待屏障消息被移除掉才能执行。屏障消息的移除操作在 ViewRootImpl.doTraversal 方法。


   ViewRootImpl.java void doTraversal() {           .......           mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);           .......           performTraversals();           .......       }   }
复制代码


在这之后就将执行 performTraversals 方法,所以移除屏障消息后,等待 performTraversals 执行完毕,就能正常执行第二次后操作了。在这个地方,其实有个小技巧可以只进行一次岗位操作,就是在第一次交的时候进行一次小的延迟:


 Activity.java

@Override protected void onResume() { super.onResume(); final long start = System.currentTimeMillis(); new Handler.postDelay(new Runnable() { @Override public void run() { Log.d(TAG, "onPause cost:" + (System.currentTimeMillis() - start)); } },10); }
复制代码


通过添加一点小延迟,可以把消息的执行时间延迟到屏障消息之后,这条消息就会被屏障消息阻塞,直到屏障消息被移除时才执行了。不过由于系统函数执行时间不可控,这种方式并不保险。


另外,正是由于这条屏障消息的存在,在第一帧渲染完成以前,用户的操作都会被阻塞。


应用内统计方案

耗时统计是非常适合使用 AOP 思想来实现的功能。我们当然不希望在每个活动的 onPause,onCreate,onResume 等方法中进行手动方法统计,第一这会增加编码量,第二这对代码有侵入,第三对于第三方 sdk 内的活动代码,无法进行修改。使用 AOP,表示需要找到一个切入点,这个切入点是活动生命周期回调的入口。这里推荐三种方案。


钩子仪表


Hook Instrumentation 是指通过反射将 ActivtyThread 内的仪器对象替换成我们自定义的仪器对象。在插件化方案中,Hook Instrumentation 是种很常见的方式。由于所有活动生命周期的回调都要经过 Instrumentation 对象,因此通过 Hook Instrumentation 对象,可以很方便地统计出 Actvity 每个生命周期的耗时。以启动流程第一阶段的 Pause 耗时为例,可以这么修修仪器:


public class TestInstrumentation extends Instrumentation {   private static final String TAG="TestInstrumentation";   private static final Instrumentation mBase;

public TestInstrumentation(Instrumentation base){ mBase = base; } .......

@Override public void callActivityOnPause(Activity activity) { long startTime = System.currentTimeMillis(); mBase.callActivityOnPause(activity); Log.d(TAG,"onPause cost:"+(System.currentTimeMillis()-startTime)); }

.......}
复制代码


而 Render 耗时,可以在 callActivityOnResume 方法最后,通过 Post Message 的方式进行统计。


Hook Instrumentation 是种很理想的解决方案,唯一的问题是太多人喜欢 Hook 它了。由于很多功能,比如插件化都喜欢 Hook Instrumentation,为了不影响他们的使用,不得不重写大量的方法执行 mBase .xx()。如果 Instrumentation 是个接口,能够使用动态代理就更理想了。


Hook Looper-Printer


H ook Looper 是种比较取巧的方案,做法是通过 Looper.getMainLooper()。setMessageLogging(Printer)方法设置一个日志对象。


public static void loop() {       ......       for (;;) {           ......           final Printer logging = me.mLogging;           if (logging != null) {               logging.println(">>>>> Dispatching to " + msg.target + " " +                       msg.callback + ": " + msg.what);           }           ......           try {               msg.target.dispatchMessage(msg);               end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();           } finally {               if (traceTag != 0) {                   Trace.traceEnd(traceTag);               }           }           .......

if (logging != null) { logging.println("<<<<< Finished to " + msg.target + " " + msg.callback); } ....... } }
复制代码


在 Looper 执行消息前后,如果打印机对象不为空,就会各输出一段日志,而我们知道活动的生命周期回调的起点其实都是 ActviityThread 内的 mH 这个 Handler,通过解析日志,就能知道当前 msg 是否是相应的生命周期任务,解析大致流程如下:


  • 匹配“>>>>>发送到”和“<<<<< Finished to”,区分 msg 开始和结束节点

  • 匹配 msg.target 是否等于“android.app.ActivityThread $ H”,确定是否为生命周期调消息

  • 匹配 msg.what,确定当前消息码,不同生命周期回调对应不同消息码,比如 LAUNCH_ACTIVITY = 100,PAUSE_ACTIVITY = 101

  • 统计开始节点和结束节点之前的耗时,就能得出响应生命周期的耗时。同样的,渲染耗时需要在启动结束时,通过 Post Message 的方式得出。


这个方案的优点是不需要通过反射等方式,修改系统对象,所以安全性很高。但是通过该方法只能区分暂停,启动,渲染三个步骤的相应耗时,无法细分启动方法中各个生命周期的耗时,因为是以每个消息的执行为统计单位,而启动消息实际上同时包含了 onCreate,onStart,onResume 等的回调。更致命的一点是在 Android P 中,系统对生命周期的处理做了一次大的重构,不再细分暂停,发射,停止,完成等消息,统一使用 EXECUTE_TRANSACTION = 159 来处理,而具体生命周期的处理则是用多态的方式实现。所以该方案无法兼容 Android P 及以上版本。


Hook ActivityThread $ H


每当 ASM 通过 Binder 调用到到 App 端时,会根据不同的调用方法转化成不同的消息放入 ActivityThread H,就能得到所有生命周期的起点。


另外,Handler 事实上可以设置一个 mCallback 字段(需要通过反射设置),在执行 dispatchMessage 方法时,如果 mCallback 不为空,则优先执行 mCallback。


Handler.java 
public void dispatchMessage(Message msg) { if (msg.callback != null) { handleCallback(msg); } else { if (mCallback != null) { if (mCallback.handleMessage(msg)) { return; } } handleMessage(msg); } }



因此,可以通过反射获取ActivityThread中的ħ对象,将mCallback修改为自己实现的Handler.Callback对象,实现消息的拦截,而不需要替换Hanlder对象。


class ProxyHandlerCallback implements Handler.Callback {

//设置当前的callback,防止其他sdk也同时设置了callback被覆盖 public final Handler.Callback mOldCallback; public final Handler mHandler;

ProxyHandlerCallback(Handler.Callback oldCallback, Handler handler) { mOldCallback = oldCallback; mHandler = handler; }

@Override public boolean handleMessage(Message msg) { // 处理消息开始,同时返回消息类型,主要为了兼容Android P,把159消息转为101(Pause)和100(Launch) int msgType = preDispatch(msg); // 如果旧的callback返回true,表示已经被它拦截,而它内部必定调用了Handler.handleMessage,直接返回 if (mOldCallback != null && mOldCallback.handleMessage(msg)) { postDispatch(msgType); return true; } // 直接调用handleMessage执行消息处理 mHandler.handleMessage(msg); // 处理消息结束 postDispatch(msgType); // 返回true,表示callback会拦截消息,Hanlder不需要再处理消息因为我们上一步已经处理过了 return true; } .......}
复制代码


为了统计 mHandler.handleMessage(msg)方法耗时,Callback 的 handleMessage 方法会返回 true.preDispatch 和 postDispatch 的处理和 Hook Looper 流程差不多,不过增加了 Android P 下,消息类行为 159 时的处理,方案可以参考“ Android 的插件化兼容性“。


和 Hook Looper 一样,Hook Hanlder 也有个缺点是无法分别获取启动中各个生命周期的耗时。


3。总结

最后做下总结:


活动的启动分为暂停,启动和渲染三个步骤,在启动一个新的活动时,会先暂停前一个正在显示的活动,再加载新的活动,然后开始渲染,直到第一帧渲染成功,活动才算启动完毕。


可以利用 Logcat 查看系统输出的活动启动耗时,系统会统计活动启动+渲染的时间做为耗时时间,而系统最多允许 Pause 操作超时 500ms,到时见就会自己调用 Pause 完成方法进行后续流程。


可以使用 Hook Instrumentation,Hook Looper,Hook Handler 三种方式实现 AOP 的耗时统计,其中 Hook Looper 方式无法兼容 Android P.


推广下 DoraemonKit ,是一款功能齐全的客户端(iOS,Android)研发助手,已集成耗时统计功能。



本文转载自公众号滴滴技术(ID:didi_tech)。


原文链接:


https://mp.weixin.qq.com/s/6CyQsY06Ny7Fi-jy19fjEA


2019 年 9 月 18 日 23:131507

评论

发布
暂无评论
发现更多内容

UC01 用户 购买课程

克比

用例

z

产品训练营-第四次作业

Geek_娴子

第四周作业

Geek_72d5ab

第四周学习心得

Trigger

极客时间 产品经理训练营

并发编程系列:关于线程中断

程序员架构进阶

Java 并发 28天写作 2月春节不断更

产品经理训练营-第四周作业

玖玖

产品经理训练营 - 第四次作业

Jophie

产品经理训练营

抽奖小程序-活动发布用例分析及流程图

思亭

漂亮壁纸

小马哥

七日更 二月春节不断更 壁纸

Spring中经典的9种设计模式,一定要记牢

Crud的程序员

spring 程序员 架构 设计模式

《零基础看得懂的Python入门教程 》——(五)我的魔法竟然有了一丝逻辑

1_bit

Python

产品经理第 0 期训练营第四周作业提交

Krystal

产品经理训练营第四次作业

庞玉坤

第四周作业-核销优惠券用例

隋泽

产品经理训练营

1分钟内的Linux性能分析法

Gopher指北

Linux 后端

产品经理训练营第四周作业

happy-黑皮

产品经理训练营

第四次作业及总结

青葵

学习笔记

0期产品训练营第四周作业-学情周报用例

skylar

产品经理第四周总结

克比

产品经理训练营第四章作业(一)

猫。

作业

云随心

作业

日记 2021年2月10日(周三)

Changing Lin

2月春节不断更

手把手教你玩华为eNSP模拟器

0期产品训练营-第4周小结

skylar

百度闯关,照见互联网巨头造芯之路

脑极体

【LeetCode】字符串的排列题解

HQ数字卡

算法 LeetCode 2月春节不断更

「产品经理训练营」作业 04:知识星球加入星球用例

狷介

产品经理训练营

如何在不辞职的情况下,改变不喜欢的工作?

熊斌

2月春节不断更

产品经理训练营第四章作业(一)

新盛

抽奖助手小程序 发起抽奖用例

Shine

产品

DNSPod与开源应用专场

DNSPod与开源应用专场

Android性能优化之活动启动耗时分析-InfoQ