您的位置:首页 > 移动开发 > Android开发

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

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

    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();
}
}
我们需要关注一下几点:

updateText()这个函数是什么时候被执行的?

App 启动后,三个图片的长宽是否可以被正确地显示出来?

是否有 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()期间才会执行。

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

dispatchAttachedToWindow() 是什么时候被调用的?
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()到底干了啥
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  Android