Android性能优化 -- 应用启动优化之DelayLoad

    对于应用启动优化,其实核心思想就是在启动过程中少做事情,具体实践的时候无非就是下面几种:

    1. 异步加载;

    2. 延时加载;

    3. 懒加载。

    我们这篇博客主要学习一下一种延时加载(DelayLoad)的实现及其原理。DelayLoad的实现是非常简单的,但是原理比较复杂,其中还涉及到Looper、Handler、MessageQueue、VSYNC等。

一、优化后的DelayLoad的实现

    我们这里先引出一个问题,如下。

    一提到DelayLoad,大家可能第一时间想到的就是在onCreate()方法里面调用Handler.postDelayed()方法,将需要Delay加载的代码放到这里面去初始化,这也是一个比较方便的方法。delay一段时间再去执行,这时候应用已经加载完成,界面已经显示出来了。不过,这个方法有一个致命的问题:延迟多久?

    在Android的高端机型上,应用的启动速度非常快,这时候只需要Delay很短的时间即可;但是在低端机器上,应用的启动速度相对较慢,而且现在应用为了兼容旧的机器,往往需要Delay较长的时间,这样在用户体验上带来的差异还是比较明显的。

    这里先说说优化方案。

  1. 首先,创建Handler和Runnable对象,其中Runnable对象的run()方法里面去更新UI线程。

private Handler myHandler = new Handler();
private Runnable mLoadingRunnable = new Runnable() {
  @Override
  public void run() {
    updateText(); //更新UI线程
  }
};
    2. 在主 Activity 的 onCreate 中加入下面的代码。

getWindow().getDecorView().post(new Runnable() {
  @Override
  public void run() {
    myHandler.post(mLoadingRunnable);
  }
});
    其实实现的话非常简单,我们来对比一下三种方案的效果。

二、三种写法的差异对比

    为了验证我们优化的 DelayLoad的效果,我们写了一个简单的app,这个 App 中包含三张不同大小的图片,每张图片下面都会有一个 TextView,来标记图片的显示高度和宽度。MainActivity的代码如下:

package com.android.test;

import android.os.Bundle;
import android.os.Handler;
import android.support.v4.os.TraceCompat;
import android.support.v7.app.AppCompatActivity;
import android.widget.ImageView;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {
    private static final int DELAY_TIME = 300;

    private ImageView zhihuImg;
    private TextView imageWidthTxt;
    private TextView imageHeightTxt;

    private Handler myHandler = new Handler();

    private Runnable mLoadingRunnable = new Runnable() {

        @Override
        public void run() {
            updateText();
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        zhihuImg = (ImageView) findViewById(R.id.zhihu_img);

        imageWidthTxt = (TextView) findViewById(R.id.img_width_txt);
        imageHeightTxt = (TextView) findViewById(R.id.img_height_txt);

        //      第一种写法:直接Post
        //      myHandler.post(mLoadingRunnable);

        //      第二种写法:直接PostDelay 300ms.
        //      myHandler.postDelayed(mLoadingRunnable, DELAY_TIME);

        //      第三种写法:
        //      优化的DelayLoad
        getWindow().getDecorView().post(new Runnable() {
            @Override
            public void run() {
                myHandler.post(mLoadingRunnable);
            }
        });
    }

    private void updateText() {
        TraceCompat.beginSection("updateText");

        imageWidthTxt.setText("image : w=" + zhihuImg.getWidth());
        imageHeightTxt.setText("image : h=" + zhihuImg.getWidth());

        TraceCompat.endSection();
    }
}
我们需要关注一下几点:

  1. updateText()这个函数是什么时候被执行的?
  2. App 启动后,三个图片的长宽是否可以被正确地显示出来?
  3. 是否有 Delay Load 的效果?

关于详细对比可参考:Android应用启动优化:一种DelayLoad的实现和原理(上篇)

实现原理

    这个过程涉及到Activity的启动,可参考之前写的一篇博客熟悉应用启动流程:Android组件管理--应用程序启动流程

    另外还会涉及到View的工作原理,之后会通过博客深入总结,这里先简单了解一下。

View工作原理简述

    一个Window中View根节点DecorView,它的mParent成为ViewRoot,对应ViewRootImpl类,他不是View的子类,而是个ViewParent。ViewRootImpl是连接Window和DecorView的纽带,View的焦点、按键、布局、渲染等流程都是从ViewRoot中开始的。

    View的绘制流程从requestLayout触发,View系统中所有会改变布局的方法都会触发requestLayout,如Textview改变文字,ViewGroup添加View等。

    通过源码查看,View的requestLayout()最终调用到ViewRootImpl的requestLayout()方法。

    @Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }
    从scheduleTraversals名字来看,requestLayout只是触发一个异步的任务。事实上,View真正的绘制流程是从ViewRootImpl的performTraversals()方法开始,里面会经过measure、layout和draw三个过程。其中measure用来测量View的宽高,layout用来确定View的位置, 而draw负责渲染View到屏幕上。大致流程如下:

    performTraversals()方法会依次调用performMeasure(),performlayout()和performDraw()方法。父容器measure方法会调用onMeasure(),onMeasure方法会对所有子元素进行measure过程,以此遍历完整个View树。layout和draw流程类似。

DelayLoad原理

    下面我们回到本篇博客的主题,上一篇中我们最终使用的 DelayLoad 的核心方法是在 Activity 的 onCreate 函数中加入下面的方法 :

getWindow().getDecorView().post(new Runnable() {
    @Override
    public void run() {
        myHandler.post(mLoadingRunnable);
    }
});
    我们看上面的代码,调用到getWindow()等方法,一步一步分析。

getWindow()&getDecorView()

    我们在之前的博客中也分析过,这里的getWindow()方法调用的就是Activity中的getWindow()方法,如下:

    public Window getWindow() {
        return mWindow;
    }
    Activity的getWindow()方法获取到的是一个PhoneWindow对象,其初始化是在Activity的attach()方法中。

    final void attach(......) {
        ......

        mWindow = new PhoneWindow(this, window);
        mWindow.setWindowControllerCallback(this);
        mWindow.setCallback(this);
        ......

        mWindow.setWindowManager(
                (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                mToken, mComponent.flattenToString(),
                (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
        ......
    }

    DecorView是PhoneWindow的内部对象,DecorView是一个窗口的顶级视图。

    那么 DecorView 是什么时候初始化的呢?DecorView 是在 Activity 的父类的 onCreate 方法中调用setContentView()方法时被初始化的,可参考: Android 从setContentView谈Activity界面的加载过程

View.post

    当我们调用DecorView的post()方法的时候,最终调用的是View类中的post()方法,因为DecorView最终继承View。

    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;
    }

    注意这里的mAttachInfo,AttachInfo表示View与Window之间的绑定信息,如何确定这里的mAttachInfo是否为空呢?我们搜索下给mAttachInfo赋值的代码,我们可以找到两处给mAttachInfo赋值的地方,如下:

    void dispatchAttachedToWindow(AttachInfo info, int visibility) {
        mAttachInfo = info;
    void dispatchDetachedFromWindow() {

        mAttachInfo = null;
        ......
    }
    在dispatchAttachedToWindow()方法中为mAttachInfo赋值,在dispatchDetachedFromWindow()方法中置空。

    我们上面调用post()方法时,是在Activity的onCreate()方法中调用的,此时这个View的dispatchAttachedToWindow()方法还没有被调用,mAttachInfo这是还为null。我们稍后在分析dispatchAttachedToWindow()方法在哪调用以及mAttachInfo在哪里赋值。

    这里有一点需要思考下:就是Activity的各个回调函数都是干嘛的?我们平时写应用的时候,貌似在onCreate方法里面搞定一切就OK了,onResume以及onStart等方法没怎么涉及到,其实不然。

    onCreate顾名思义就是Create,我们在前面看到,Activity的onCreate方法做了很多初始化的操作,包括PhoneWindow、DecorView、setContentView等,但是onCreate()只是初始化了这些对象。真正要设置为显示则在Resume的时候,可以查看ActivityThread的handleResumeActivity方法,该方法中除了调用Activity的onResume()回调方法之外,还初始化了几个比较重要的类:ViewRootImpl、ThreadRender。

ActivityThread.handleResumeActivity

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 (a.mVisibleFromClient) {
        a.mWindowAdded = true;
        wm.addView(decor, l);
    }
    主要是wm.addView(decor, l);这句,将decorView与WindowManagerImpl联系起来,这句最终会调用到WindowManagerGlobal的addView()方法:

public void addView(View view, ViewGroup.LayoutParams params,
        Display display, Window parentWindow) {
    ......
    ViewRootImpl root;
    View panelParentView = null;
    ......
        root = new ViewRootImpl(view.getContext(), display);
        view.setLayoutParams(wparams);
        mViews.add(view);
        mRoots.add(root);
        mParams.add(wparams);
    }
    // do this last because it fires off messages to start doing things
    try {
        root.setView(view, wparams, panelParentView);
    } catch (RuntimeException e) {
      ......
    }
}
    我们知道 ViewRootImpl 是 View 系统的一个核心类,其定义如下:

public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks
    ViewRootImpl初始化的时候会对AttachInfo进行初始化,这就是为什么之前的在onCreate的时候mAttachInfo为空。

    我们继续看addView方法中的root.setView(view, wparams, panelParentView);,传入的view为DecorView,root为ViewRootImpl,这个setView()方法中将ViewRootImpl的mView变量设置为传入的view,也就是DecorView。这样看,ViewRootImpl与DecorView的关系我们就清楚了。

getRunQueue.post()

    我们继续回到主题post函数上,在上面说过post调用的是View的post函数,由于在onCreate()的时候mAttachInfo为空,所以会走下面的分支:getRunQueue().post(action);

    /**
     * Returns the queue of runnable for this view.
     *
     * @return the queue of runnables for this view
     */
    private HandlerActionQueue getRunQueue() {
        if (mRunQueue == null) {
            mRunQueue = new HandlerActionQueue();
        }
        return mRunQueue;
    }
    注意这里的getRunQueue()方法得到的并不是Looper里面的那个MessageQueue,也不是API23中的ViewRootImpl。这里getRunQueue()方法返回的是HandlerActionQueue对象,getRunQueue().post()方法调用的其实也就是HandlerActionQueue的post()方法,我们来看下这个HandlerActionQueue。
HandlerActionQueue

/**
 * Class used to enqueue pending work from Views when no Handler is attached.
 *
 * @hide Exposed for test framework only.
 */
public class HandlerActionQueue {
    private HandlerAction[] mActions;
    private int mCount;

    public void post(Runnable action) {
        postDelayed(action, 0);
    }

    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++;
        }
    }
    post(Runnable) 方法内部调用了 postDelayed(Runnable, long),postDelayed() 内部则是将 Runnable 和 long 作为参数创建一个 HandlerAction 对象,然后添加到 mActions 数组里,这个数组默认大小是4,GrowingArrayUtils.append()其实就是一个工具类,如果不够就扩充。下面先看看 HandlerAction:

    private static class HandlerAction {
        final Runnable action;
        final long delay;

        public HandlerAction(Runnable action, long delay) {
            this.action = action;
            this.delay = delay;
        }

        public boolean matches(Runnable otherAction) {
            return otherAction == null && action == null
                    || action != null && action.equals(otherAction);
        }
    }
    很简单的数据结构,就一个 Runnable 成员变量和一个 long 成员变量。这个类作用可以理解为用于包装 View.post(Runnable) 传入的 Runnable 操作的,当然因为还有 View.postDelay(),所以就还需要一个 long 类型的变量来保存延迟的时间了,这样一来这个数据结构就不难理解了吧。

    所以,我们调用View.post(Runnable)传进去的Runnable操作,在传到HandlerActionQueue里面会先经过HandlerAction包装一下,然后再缓存起来。HandlerActionQueue是通过一个默认大小为4的数组保存这些Runnable操作,如果数组不够用时,就会通过GrowingArrayUtils来扩充数组。

    到此,我们先来梳理下:

    当我们在Activity的onCreate()方法中调用getWindow().getDecorView().post(Runnable)时,因为DecorView继承View,所以最终调用的是View中的post()方法;

    执行View.post(Runnable)时,因为这时候View还没有attachedToWindow,所以这些Runnable操作其实并没有被执行,而是先通过HandlerActionQueue缓存起来。

    那么问题来了,这些Runnable什么时候才会被执行呢?

executeActions()

    我们上面讲到的HandlerActionQueue这个类中,还有executeActions()方法,这个方法就是用来执行这些被缓存起来的Runnable操作的。

    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;
        }
    }
    这里我们看到了Handler,并且是调用Handler的postDelayed()方法。从这里就可以看出来被缓存起来没有被执行的Runnable最后还是通过Handler来执行的。

    此时的关键点就是在哪里调用了executeActions()。我们可以在source insight中全局搜索“executeActions”关键字,最后发现在View类的dispatchAttachedToWindow()方法中会调用到。

dispatchAttachedToWindow()

    /**
     * @param info the {@link android.view.View.AttachInfo} to associated with
     *        this view
     */
    void dispatchAttachedToWindow(AttachInfo info, int visibility) {
        mAttachInfo = info;
        ......
        
        // Transfer all pending runnables.
        if (mRunQueue != null) {
            mRunQueue.executeActions(info.mHandler);
            mRunQueue = null;
        }
        .......
    }
    那我们继续看下,在哪里会调用到dispatchAttachedToWindow()方法呢?

    我们在使用View.post()方法时,其实分为两种情况,当View还没有attachedToWindow时,通过View.post(Runnable)把传进来的Runnable操作都先缓存在HandlerActionQueue中;然后等View的dispatchAttachedToWindow()被调用时,就通过mAttachInfo.mHandler.postDelay()来执行这些被缓存起来的Runnbale操作。从此刻起,到View被detachedFromWindow期间,如果再次调用View.post(Runnable)的话,那么这些Runnable不再缓存了,而是直接交给mAttachInfo.mHandler来执行。

    View.post(Runnable)的操作之所以可以保证肯定是在View宽高计算完毕之后才执行的,是因为这些Runnable操作只有在View的dispatchAttachedToWindow()到dispatchDetachedFromWindow()期间才会执行。

    那么,接下去就还剩两个关键点需要搞清楚了:

  1. dispatchAttachedToWindow() 是什么时候被调用的?
  2. mAttachInfo 是在哪里初始化的?
    我们上面说到View真正的绘制流程是从ViewRootImpl的performTraversals()方法开始,里面会经过measure、layout和draw三个过程。

performTraversals()



    private void performTraversals() {
        // cache mView since it is used so much below...
        final View host = mView;
        ......

        if (mFirst) {
            ......
            host.dispatchAttachedToWindow(mAttachInfo, 0);
            mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true);
            dispatchApplyInsets(host);
            //Log.i(mTag, "Screen on initialized: " + attachInfo.mKeepScreenOn);

        }......
        
                    // Ask host how big it wants to be
                    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
        ......

            performLayout(lp, mWidth, mHeight);
        ......

            performDraw();
        ......
    }
    这里的mView就是DecorView,而DecorView继承FrameLayout,也是个ViewGroup,在ViewGroup的dispatchAttachedToWindow()方法里面会将mAttachInfo传给所有的子View。也就是说,在Activity首次进行View树的遍历绘制时,ViewRootImpl会将自己的mAttachInfo通过根布局DecorView传递给所有的子View。

    我们再来看看 ViewRootImpl 的 mAttachInfo 什么时候初始化的呢?

mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this);
    通过源码,可以看到在构造函数里对 mAttachInfo 进行初始化,传入了很多参数,我们关注的应该是 mHandler 这个变量,所以看看这个变量定义:

    final ViewRootHandler mHandler = new ViewRootHandler();
    这个Handler调用的是无参构造函数,默认绑定的就是当前线程的Looper,而这里是在主线程中执行的,因此绑定的是主线程的Looper。这也就是为什么View.post(Runnable)的操作可以更新UI的原因,因为这些Runnbale都是通过ViewRootImpl的mHandler切到主线程来执行的。

原理总结

    1. View.post(Runnable)内部会自动分两种情况处理,当View还没有dispatchAttachedToWindow()时,会先将这些Runnable操作缓存下来;否则就直接通过mAttachInfo.mHandler将这些Runnbale操作post到主线程的MessageQueue中等待执行。

    2. 如果View.post(Runnable)的Runnbale操作被缓存下来了,那么这些操作将会在dispatchAttachedToWindow()被回调时,通过mAttachInfo.mHandler.post()发送到主线程的MessageQueue中等待执行。

    3. mAttachInfo是ViewRootImpl的成员变量,在构造函数中初始化,Activity的View树里所有的子View中的mAttachInfo都是ViewRootImpl.mAttachInfo的引用。

    4. mAttachInfo.mHandler也是ViewRootImpl中的成员变量,在声明时就初始化了,所以这个mHandler绑定的是主线程的Looper,因此View.post()的操作都会发送到主线程中执行,那么也就支持UI操作了。

    5. dispatchAttachedToWindow()方法被调用的时机是在ViewRootImpl的performTraversals()中,该方法会进行View树的测量、布局、绘制三大流程的操作。

    6. Handler消息机制通常情况下是一个Message执行完后才去取下一个Message来执行,所以View.post(Runnable)中的Runnbale操作肯定会在performMeasure()(该方法在TraversalRunnable中)之后才执行,所以此时可以获取到View的宽高。

相关链接:

Android应用启动优化:一种DelayLoad的实现和原理(下篇)

通过View.post()获取View的宽高引发的两个问题

View.post()到底干了啥

你可能感兴趣的:(性能优化)