Android布局原理与优化

Android布局原理与优化

目录:

  1. 绘制原理
    1. CPU与GPU
    2. Android 图形系统的整体架构
    3. RenderThread
  2. 硬件加速和软件绘制
    1. invalidate软件绘制流程
    2. invalidate硬件加速流程
    3. requestLayout流程
  3. 布局加载原理
  4. 获取界面布局耗时
    1. 常规方式
    2. AOP
    3. LayoutInflaterCompat.setFactory2
  5. 布局优化常规方案
    1. 代码创建View
    2. 替换MessageQueue来实现异步创建View
    3. AsynclayoutInflater异步创建布局
    4. X2C
    5. 使用ConstraintLayout降低布局嵌套层级
    6. 过度绘制优化
  6. 布局优化进阶方案
  7. 布局优化常见问题

一、绘制原理

Android的绘制实现主要是借助CPU与GPU结合刷新机制共同完成的。

1、CPU与GPU
  1. CPU负责计算显示内容,包括Measure、Layout、Record、Execute等操作。在UI绘制上的缺陷在于容易显示重复的视图组件,这样不仅带来重复的计算操作,而且会占用额外的GPU资源。
  2. GPU负责栅格化(用于将UI元素绘制到屏幕上,即将UI组件拆分到不同的像素上显示)。

这里举两个栗子来讲解一些CPU和GPU的作用:

  1. 文字的显示首先经过CPU换算成纹理,然后再传给GPU进行渲染。
  2. 而图片的显示首先是经过CPU的计算,然后加载到内存当中,最后再传给GPU进行渲染。

那么,软件绘制和硬件绘制有什么区别呢?我们先看看下图:

Android布局原理与优化_第1张图片

这里软件绘制使用的是Skia库(一款在低端设备如手机上呈现高质量的 2D 图形的 跨平台图形框架)进行绘制的,而硬件绘制本质上是使用的OpenGl ES接口去利用GPU进行绘制的。OpenGL是一种跨平台的图形API,它为2D/3D图形处理硬件指定了标准的软件接口。而OpenGL ES是用于嵌入式设备的,它是OpenGL规范的一种形式,也可称为其子集。

并且,由于OpenGl ES系统版本的限制,有很多 绘制API 都有相应的 Android API level 的限制,此外,在Android 7.0 把 OpenGL ES 升级到最新的 3.2 版本的时候,还添加了对Vulkan(一套适用于高性能 3D 图形的低开销、跨平台 API)的支持。Vulan作为下一代图形API以及OpenGL的继承者,它的优势在于大幅优化了CPU上图形驱动相关的性能。

2、Android 图形系统的整体架构

Android官方的架构图如下:
Android布局原理与优化_第2张图片

为了比较好的描述它们之间的作用,我们可以把应用程序图形渲染过程当作一次绘画过程,那么绘画过程中 Android 的各个图形组件的作用分别如下:

  • 画笔:Skia 或者 OpenGL。我们可以用 Skia去绘制 2D 图形,也可以用 OpenGL 去绘制 2D/3D 图形。
  • 画纸:Surface。所有的元素都在 Surface 这张画纸上进行绘制和渲染。在 Android 中,Window 是 View 的容器,每个窗口都会关联一个 Surface。而 WindowManager 则负责管理这些窗口,并且把它们的数据传递给 SurfaceFlinger。
  • 画板:Graphic Buffer。Graphic Buffer 缓冲用于应用程序图形的绘制,在 Android 4.1 之前使用的是双缓冲机制,而在 Android 4.1 之后使用的是三缓冲机制。
  • 显示:SurfaceFlinger。它将 WindowManager 提供的所有 Surface,通过硬件合成器 Hardware Composer 合成并输出到显示屏。
3、RenderThread

在Android系统的显示过程中,虽然我们利用了GPU的图形高性能计算的能力,但是从计算Display到通过GPU绘制到Frame Buffer都在UI线程中完成,此时如果能让GPU在不同的线程中进行绘制渲染图形,那么绘制将会更加地流畅。

于是,在Android 5.0之后,引入了RenderNode和RenderThread的概念,它们的作用如下:

  • RenderNode:进一步封装了Display和某些View的属性。
  • RenderThread:渲染线程,负责执行所有的OpenGl命令,其中的RenderNode保存有渲染帧的所有信息,能在主线程有耗时操作的前提下保证动画流畅。

CPU将数据同步给GPU之后,通常不会阻塞等待RenderThread去利用GPU去渲染完视图,而是通知结束之后就返回。加入ReaderThread之后的整个显示调用流程图如下图所示:

Android布局原理与优化_第3张图片
在Android 6.0之后,其在adb shell dumpsys gxinfo命令中添加了更加详细的信息,在优化工具一节中我将详细分析下它的使用。

2、硬件加速和软件绘制

硬件加速相比软件绘制的几个优点:

  1. 硬件加速收集绘制指令后,统一发送给RenderThread进行GPU绘制,分担UI线程压力
  2. 硬件加速只绘制标记为PFLAG_INVALIDATED的View,不会绘制与该View相交的区域

硬件加速存在哪些问题:
我们都知道,硬件加速的原理就是将CPU不擅长的图形计算转换成GPU专用指令。

invalidate软件绘制流程

Android布局原理与优化_第4张图片
从时序图可知:

  1. View如果不可见&&无动画,ViewGroup不可见 && 无动画 && 无过渡动画不会执行invalidate
  2. 软件绘制会触发与dirty区域相交的所有View(硬件加速优化点)
    根据第一点,ViewGroup#invalidate时会触发其包括自身和所有子View重绘
  3. 同一个draw时序内连续调用同一View的invalidate时,会被Flag阻挡,不再向下走
  4. 同一个draw时序内不同View调用invalidate时只会调为一个,不会重复执行
invalidate硬件加速流程Android布局原理与优化_第5张图片

从时序图可知:

  1. View如果不可见&&无动画,ViewGroup不可见 && 无动画 && 无过渡动画不会执行invalidate
  2. 硬件绘制只会触发标记了PFLAG_INVALIDATED的View的draw()或dispatchDraw()
  3. View#invalidate时(非ViewGroup),因为被dispatchGetDisplayList接管了,不会调用dispatchDraw
  4. 同一个draw时序内连续调用同一View的invalidate时,会被Flag阻挡,不再向下走
  5. 同一个draw时序内不同View调用invalidate时只会调为一个,不会重复执行
requestLayout流程

Android布局原理与优化_第6张图片
从时序图可知:

  1. 清除measureCache,标记PFLAG_FORCE_LAYOUT,递归向上调用,整条路径都被标记,整条路径都会重薪measure、layout
  2. 可能会重新触发draw(),layout时大小变化后会触发invalidate
  3. 同一个layout时序内连续调用同一View的requestLayout时,会被isLayoutRequested阻挡,不再向上走
  4. 同一个layout时序内不同View调用requestLayout时只会调为一个,不会重复执行

3、布局加载原理

为什么要了解Android布局加载原理?
知其然知其所以然,不仅要明白在平时开发过程中是怎样对布局API进行调用,还要知道它内部的实现原理是什么。明白具体的实现原理与流程之后,我们可能会发现更多可优化的点。

我们都知道,Android的布局都是通过setContentView()这个方法进行设置的,那么它的内部肯定实现了布局的加载,接下来,我们就详细分析下它内部的实现原理与流程。

setContentView(getLayoutId())

点进去,发现是调用了AppCompatActivity的setContentView方法:

    @Override
    public void setContentView(@LayoutRes int layoutResID) {
     
        getDelegate().setContentView(layoutResID);
    }

这里的setContentView其实是AppCompatDelegate这个代理类的抽象方法:

 /**
     * Should be called instead of {@link Activity#setContentView(int)}}
     */
    public abstract void setContentView(@LayoutRes int resId);

在这个抽象方法的左边,会有一个绿色的小圆圈,点击它就可以查看到对应的实现类与方法,在Androidx中,实现类是AppCompatDelegateImpl,实现方法如下所示:
Android布局原理与优化_第7张图片

 @Override
public void setContentView(int resId) {
     
    ensureSubDecor();
    ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
    contentParent.removeAllViews();
    LayoutInflater.from(mContext).inflate(resId, contentParent);
    mOriginalWindowCallback.onContentChanged();
}

setContentView方法中主要是获取到了content父布局,也就是id是R.id.content的布局移除其内部所有视图之后并最终调用了LayoutInflater对象的inflate去加载对应的布局。

接下来,我们关注inflate内部的实现:

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
     
    return inflate(resource, root, root != null);
}

这里只是调用了inflate另一个的重载方法:

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
     
    final Resources res = getContext().getResources();
    if (DEBUG) {
     
        Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
                + Integer.toHexString(resource) + ")");
    }

    // 1
    final XmlResourceParser parser = res.getLayout(resource);
    try {
     
        // 2
        return inflate(parser, root, attachToRoot);
    } finally {
     
        parser.close();
    }
}

在注释1处,通过Resources的getLayout方法获取到了一个XmlResourceParser对象,继续跟踪下getLayout方法:

public XmlResourceParser getLayout(@LayoutRes int id) throws NotFoundException {
     
    return loadXmlResourceParser(id, "layout");
}

这里继续调用了loadXmlResourceParser方法,注意第二个参数传入的为layout,说明此时加载的是一个Xml资源布局解析器。我们继续跟踪loadXmlResourceParse方法:

@NonNull
XmlResourceParser loadXmlResourceParser(@AnyRes int id, @NonNull String type)
        throws NotFoundException {
     
    final TypedValue value = obtainTempTypedValue();
    try {
     
        final ResourcesImpl impl = mResourcesImpl;
        impl.getValue(id, value, true);
        if (value.type == TypedValue.TYPE_STRING) {
     
            // 1
            return impl.loadXmlResourceParser(value.string.toString(), id,
                    value.assetCookie, type);
        }
        throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id)
                + " type #0x" + Integer.toHexString(value.type) + " is not valid");
    } finally {
     
        releaseTempTypedValue(value);
    }
}

在注释1处,如果值类型为字符串的话,则调用了ResourcesImpl实例的loadXmlResourceParser方法。我们首先看看这个方法的注释:

/**
 * Loads an XML parser for the specified file.
 *
 * @param file the path for the XML file to parse
 * @param id the resource identifier for the file
 * @param assetCookie the asset cookie for the file
 * @param type the type of resource (used for logging)
 * @return a parser for the specified XML file
 * @throws NotFoundException if the file could not be loaded
 */
@NonNull
XmlResourceParser loadXmlResourceParser(@NonNull String file, @AnyRes int id, int assetCookie,
        @NonNull String type)
        throws NotFoundException {
     

        ...

        final XmlBlock block = mAssets.openXmlBlockAsset(assetCookie, file);

        ...

        return block.newParser();

        ...
}

注释的意思说明了这个方法是用于加载指定文件的Xml解析器,这里我们之间查看关键的mAssets.openXmlBlockAsset方法,这里的mAssets对象是AssetManager类型的,看看AssetManager实例的openXmlBlockAsset方法做了什么处理:

/**
 * {@hide}
 * Retrieve a non-asset as a compiled XML file.  Not for use by
 * applications.
 * 
 * @param cookie Identifier of the package to be opened.
 * @param fileName Name of the asset to retrieve.
 */
/*package*/ final XmlBlock openXmlBlockAsset(int cookie, String fileName)
    throws IOException {
     
    synchronized (this) {
     
        if (!mOpen) {
     
            throw new RuntimeException("Assetmanager has been closed");
        }
        // 1
        long xmlBlock = openXmlAssetNative(cookie, fileName);
        if (xmlBlock != 0) {
     
            XmlBlock res = new XmlBlock(this, xmlBlock);
            incRefsLocked(res.hashCode());
            return res;
        }
    }
    throw new FileNotFoundException("Asset XML file: " + fileName);
}

可以看到,最终是调用了注释1处的openXmlAssetNative方法,这是定义在AssetManager中的一个Native方法:

private native final long openXmlAssetNative(int cookie, String fileName);

与此同时,我们可以猜到读取Xml文件肯定是通过IO流的方式进行的,而openXmlBlockAsset方法后抛出的IOException异常也验证了我们的想法。因为涉及到IO流的读取,所以这里是Android布局加载流程一个耗时点
,也有可能是我们后续优化的一个方向。

分析完Resources实例的getLayout方法的实现之后,我们继续跟踪inflate方法的注释2处:

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
     
    final Resources res = getContext().getResources();
    if (DEBUG) {
     
        Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
                + Integer.toHexString(resource) + ")");
    }

    // 1
    final XmlResourceParser parser = res.getLayout(resource);
    try {
     
        // 2
        return inflate(parser, root, attachToRoot);
    } finally {
     
        parser.close();
    }
}

infalte的实现代码如下:

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
     
    synchronized (mConstructorArgs) {
     

        ...

        try {
     
            // Look for the root node.
            int type;
            while ((type = parser.next()) != XmlPullParser.START_TAG &&
                    type != XmlPullParser.END_DOCUMENT) {
     
                // Empty
            }

            if (type != XmlPullParser.START_TAG) {
     
                throw new InflateException(parser.getPositionDescription()
                        + ": No start tag found!");
            }

            final String name = parser.getName();

            ...

            // 1
            if (TAG_MERGE.equals(name)) {
     
                if (root == null || !attachToRoot) {
     
                    throw new InflateException(" can be used only with a valid "
                            + "ViewGroup root and attachToRoot=true");
                }

                rInflate(parser, root, inflaterContext, attrs, false);
            } else {
     
                // Temp is the root view that was found in the xml
                // 2
                final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                ...
            }
            ...
        }
        ...
    }
    ...
}

可以看到,infalte内部是通过XmlPull解析的方式对布局的每一个节点进行创建对应的视图的。首先,在注释1处会判断节点是否是merge标签,如果是,则对merge标签进行校验,如果merge节点不是当前布局的父节点,则抛出异常。然后,在注释2处,通过createViewFromTag方法去根据每一个标签创建对应的View视图。

我们继续跟踪下createViewFromTag方法的实现:

private View createViewFromTag(View parent, String name, Context context, AttributeSet attrs) {
     
    return createViewFromTag(parent, name, context, attrs, false);
}

 View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
        boolean ignoreThemeAttr) {
     

    ...

    try {
     
        View view;
        if (mFactory2 != null) {
     
            view = mFactory2.onCreateView(parent, name, context, attrs);
        } else if (mFactory != null) {
     
            view = mFactory.onCreateView(name, context, attrs);
        } else {
     
            view = null;
        }

        if (view == null && mPrivateFactory != null) {
     
            view = mPrivateFactory.onCreateView(parent, name, context, attrs);
        }

        if (view == null) {
     
            final Object lastContext = mConstructorArgs[0];
            mConstructorArgs[0] = context;
            try {
     
                if (-1 == name.indexOf('.')) {
     
                    view = onCreateView(parent, name, attrs);
                } else {
     
                    view = createView(name, null, attrs);
                }
            } finally {
     
                mConstructorArgs[0] = lastContext;
            }
        }

        return view;
    } 
    ...
}

在createViewFromTag方法中,首先会判断mFactory2是否存在,存在就会使用mFactory2的onCreateView方法区创建视图,否则就会调用mFactory的onCreateView方法,接下来,如果此时的tag是一个Fragment,则会调用mPrivateFactory的onCreateView方法,否则的话,最终都会调用LayoutInflater实例的createView方法:

public final View createView(String name, String prefix, AttributeSet attrs)
        throws ClassNotFoundException, InflateException {
     

   ...

    try {
     
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);

        if (constructor == null) {
     
            // Class not found in the cache, see if it's real, and try to add it
            // 1
            clazz = mContext.getClassLoader().loadClass(
                    prefix != null ? (prefix + name) : name).asSubclass(View.class);

            if (mFilter != null && clazz != null) {
     
                boolean allowed = mFilter.onLoadClass(clazz);
                if (!allowed) {
     
                    failNotAllowed(name, prefix, attrs);
                }
            }
            // 2
            constructor = clazz.getConstructor(mConstructorSignature);
            constructor.setAccessible(true);
            sConstructorMap.put(name, constructor);
        } else {
     
            ...
        }

        ...

        // 3
        final View view = constructor.newInstance(args);
        if (view instanceof ViewStub) {
     
            // Use the same context when inflating ViewStub later.
            final ViewStub viewStub = (ViewStub) view;
            viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
        }
        mConstructorArgs[0] = lastContext;
        return view;

    } 
   ...
}

LayoutInflater的createView方法中,首先,在注释1处,使用类加载器创建了对应的Class实例,然后在注释2处根据Class实例获取到了对应的构造器实例,并最终在注释3处通过构造器实例constructor的newInstance方法创建了对应的View对象。可以看到,在视图节点的创建过程中采用到了反射,我们都知道反射是比较耗性能的,过多的反射可能会导致布局加载过程变慢,这个点可能是后续优化的一个方向。

最后,我们来总结下Android中的布局加载流程:

  1. 在setContentView方法中,会通过LayoutInflater的inflate方法去加载对应的布局。
  2. inflate方法中首先会通过AssessManager调用Resources的getLayout方法去通过IO的方式去加载对应的Xml布局解析器到内存中。
  3. 接着,会通过createViewFromTag根据每一个tag创建具体的View对象。
  4. 它内部主要是按优先顺序为Factory2和Factory的onCreatView、createView方法进行View的创建,而createView方法内部采用了构造器反射的方式实现。

从以上分析可知,在Android的布局加载流程中,性能瓶颈主要存在两个地方:

  1. 布局文件解析中的IO过程。
  2. 创建View对象时的反射过程。

四、获取界面布局耗时

常规方式

如果要获取每个界面的加载耗时,我们就必需在setContentView方法前后进行手动埋点。但是它有如下缺点

  1. 不够优雅。
  2. 代码有侵入性。
AOP

我们要使用AOP去获取界面布局的耗时,那么我们的切入点就是setContentView方法,声明一个@Aspect注解的PerformanceAop类,然后,我们就可以在里面实现对setContentView进行切面的方法,如下所示:

@Around("execution(* android.app.Activity.setContentView(..))")
public void getSetContentViewTime(ProceedingJoinPoint joinPoint) {
     
    Signature signature = joinPoint.getSignature();
    String name = signature.toShortString();
    long time = System.currentTimeMillis();
    try {
     
        joinPoint.proceed();
    } catch (Throwable throwable) {
     
        throwable.printStackTrace();
    }
    LogHelper.i(name + " cost " + (System.currentTimeMillis() - time));
}

为了获取方法的耗时,我们必须使用@Around注解,这样第一个参数ProceedingJoinPoint就可以提供proceed方法去执行我们的setContentView方法,在此方法的前后就可以获取setContentView方法的耗时。后面的execution表明了在setContentView方法执行内部去调用我们写好的getSetContentViewTime方法,后面括号内的*是通配符,表示匹配任何Activity的setContentView方法,并且方法参数的个数和类型不做限定。

LayoutInflaterCompat.setFactory2

上面我们使用了AOP的方式监控了Activity的布局加载耗时,那么,如果我们需要监控每一个控件的加载耗时,该怎么实现呢?

答案是使用LayoutInflater.Factory2,我们在基类Activity的onCreate方法中直接使用LayoutInflaterCompat.setFactory2方法对Factory2的onCreateView方法进行重写,代码如下所示:

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
     

    // 使用LayoutInflaterCompat.Factory2全局监控Activity界面每一个控件的加载耗时,
    // 也可以做全局的自定义控件替换处理,比如:将TextView全局替换为自定义的TextView。
    LayoutInflaterCompat.setFactory2(getLayoutInflater(), new LayoutInflater.Factory2() {
     
        @Override
        public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
     

            if (TextUtils.equals(name, "TextView")) {
     
                // 生成自定义TextView
            }
            long time = System.currentTimeMillis();
            // 1
            View view = getDelegate().createView(parent, name, context, attrs);
            LogHelper.i(name + " cost " + (System.currentTimeMillis() - time));
            return view;
        }

        @Override
        public View onCreateView(String name, Context context, AttributeSet attrs) {
     
            return null;
        }
    });

    // 2、setFactory2方法需在super.onCreate方法前调用,否则无效        
    super.onCreate(savedInstanceState);
    setContentView(getLayoutId());
}

这样我们就实现了利用LayoutInflaterCompat.Factory2全局监控Activity界面每一个控件加载耗时的处理,后续我们可以将这些数据上传到我们自己的APM服务端,作为监控数据可以分析出哪些控件加载比较耗时。当然,这里我们也可以做全局的自定义控件替换处理,比如在上述代码中,我们可以将TextView全局替换为自定义的TextView。

然后,我们注意到这里我们使用getDelegate().createView方法来创建对应的View实例,跟踪进去发现这里的createView是一个抽象方法:

public abstract View createView(@Nullable View parent, String name, @NonNull Context context,
       @NonNull AttributeSet attrs);

在最新的Androidx中它对应的实现方法为AppCompatDelegateImpl对象的createView方法,代码如下所示:

@Override
public View createView(View parent, final String name, @NonNull Context context,
        @NonNull AttributeSet attrs) {
     

    ...

    return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
            IS_PRE_LOLLIPOP, /* Only read android:theme pre-L (L+ handles this anyway) */
            true, /* Read read app:theme as a fallback at all times for legacy reasons */
            VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */
    );
}

这里最终又调用了AppCompatViewInflater对象的createView方法:

 public final View createView(View parent, final String name, @NonNull Context context,
        @NonNull AttributeSet attrs, boolean inheritContext,
        boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
     

    ...

    // We need to 'inject' our tint aware Views in place of the standard framework versions
    switch (name) {
     
        case "TextView":
            view = new AppCompatTextView(context, attrs);
            break;
        case "ImageView":
            view = new AppCompatImageView(context, attrs);
            break;
        case "Button":
            view = new AppCompatButton(context, attrs);
            break;
        case "EditText":
            view = new AppCompatEditText(context, attrs);
            break;
        case "Spinner":
            view = new AppCompatSpinner(context, attrs);
            break;
        case "ImageButton":
            view = new AppCompatImageButton(context, attrs);
            break;
        case "CheckBox":
            view = new AppCompatCheckBox(context, attrs);
            break;
        case "RadioButton":
            view = new AppCompatRadioButton(context, attrs);
            break;
        case "CheckedTextView":
            view = new AppCompatCheckedTextView(context, attrs);
            break;
        case "AutoCompleteTextView":
            view = new AppCompatAutoCompleteTextView(context, attrs);
            break;
        case "MultiAutoCompleteTextView":
            view = new AppCompatMultiAutoCompleteTextView(context, attrs);
            break;
        case "RatingBar":
            view = new AppCompatRatingBar(context, attrs);
            break;
        case "SeekBar":
            view = new AppCompatSeekBar(context, attrs);
            break;
    }

    if (view == null && originalContext != context) {
     
        // If the original context does not equal our themed context, then we need to manually
        // inflate it using the name so that android:theme takes effect.
        view = createViewFromTag(context, name, attrs);
    }

    if (view != null) {
     
        // If we have created a view, check its android:onClick
        checkOnClickListener(view, attrs);
    }

    return view;
}

在AppCompatViewInflater对象的createView方法中系统根据不同的tag名字创建出了对应的AppCompat兼容控件。看到这里,我们明白了Android系统是使用了LayoutInflater的Factor2/Factory结合了AppCompat兼容类来进行高级版本控件的适配。

接下来,我们注意到注释1处,setFactory2方法需在super.onCreate方法前调用,否则无效,这是为什么呢?

这里可以先大胆猜测一下,可能是因为在super.onCreate()方法中就需要将Factory2实例存储到内存中。下面,我们就跟踪一下super.onCreate()的源码,看看是否如我们所假设的一样。AppCompatActivity的onCreate方法如下所示:

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
     
    final AppCompatDelegate delegate = getDelegate();
    delegate.installViewFactory();
    delegate.onCreate(savedInstanceState);
    if (delegate.applyDayNight() && mThemeId != 0) {
     
        // If DayNight has been applied, we need to re-apply the theme for
        // the changes to take effect. On API 23+, we should bypass
        // setTheme(), which will no-op if the theme ID is identical to the
        // current theme ID.
        if (Build.VERSION.SDK_INT >= 23) {
     
            onApplyThemeResource(getTheme(), mThemeId, false);
        } else {
     
            setTheme(mThemeId);
        }
    }
    super.onCreate(savedInstanceState);
}

第一行的delegate实例的installViewFactory()方法就吸引了我们的注意,因为它包含了一个敏感的关键字“Factory“,这里我们继续跟踪进installViewFactory()方法:

public abstract void installViewFactory();

这里一个是抽象方法,点击左边绿色圆圈,可以看到这里具体的实现类为AppCompatDelegateImpl,其实现的installViewFactory()方法如下所示:

@Override
public void installViewFactory() {
     
    LayoutInflater layoutInflater = LayoutInflater.from(mContext);
    if (layoutInflater.getFactory() == null) {
     
        LayoutInflaterCompat.setFactory2(layoutInflater, this);
    } else {
     
        if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImplV9)) {
     
            Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
                    + " so we can not install AppCompat's");
        }
    }
}

可以看到,如果我们在super.onCreate()方法前没有设置LayoutInflater的Factory2实例的话,这里就会设置一个默认的Factory2。最后,我们再来看下默认Factory2的onCreateView方法的实现:

/**
 * From {@link LayoutInflater.Factory2}.
 */
@Override
public final View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
     
    // 1、First let the Activity's Factory try and inflate the view
    final View view = callActivityOnCreateView(parent, name, context, attrs);
    if (view != null) {
     
        return view;
    }

    // 2、If the Factory didn't handle it, let our createView() method try
    return createView(parent, name, context, attrs);
}

在注释1处,我们首先会尝试让Activity的Facotry实例去加载对应的View实例,如果Factory不能够处理它,在注释2处,就会调用createView方法去创建对应的View,AppCompatDelegateImplV9类的createView方法的实现上面我们已经分析过了,此处就不再赘述了。

五、布局优化常规方案

1.布局Inflate优化方案演进

1、代码动态创建View

使用Java代码动态添加控件的简单示例如下:

Button button=new Button(this);        
button.setBackgroundColor(Color.RED);
button.setText("Hello World");
ViewGroup viewGroup = (ViewGroup) LayoutInflater.from(this).inflate(R.layout.activity_main, null);
viewGroup.addView(button);

2、替换MessageQueue来实现异步创建View

在使用子线程创建视图控件的时候,我们可以把子线程Looper的MessageQueue替换成主线程的MessageQueue,在创建完需要的视图控件后记得将子线程Looper中的MessageQueue恢复为原来的。在Awesome-WanAndroid项目下的UiUtils的Ui优化工具类中,提供了相应的实现,代码如下所示:

 /**
 * 实现将子线程Looper中的MessageQueue替换为主线程中Looper的
 * MessageQueue,这样就能够在子线程中异步创建UI。
 *
 * 注意:需要在子线程中调用。
 *
 * @param reset 是否将子线程中的MessageQueue重置为原来的,false则表示需要进行替换
 * @return 替换是否成功
 */
public static boolean replaceLooperWithMainThreadQueue(boolean reset) {
     
    if (CommonUtils.isMainThread()) {
     
        return true;
    } else {
     
        // 1、获取子线程的ThreadLocal实例
        ThreadLocal<Looper> threadLocal = ReflectUtils.reflect(Looper.class).field("sThreadLocal").get();
        if (threadLocal == null) {
     
            return false;
        } else {
     
            Looper looper = null;
            if (!reset) {
     
                Looper.prepare();
                looper = Looper.myLooper();
                // 2、通过调用MainLooper的getQueue方法区获取主线程Looper中的MessageQueue实例
                Object queue = ReflectUtils.reflect(Looper.getMainLooper()).method("getQueue").get();
                if (!(queue instanceof MessageQueue)) {
     
                    return false;
                }
                // 3、将子线程中的MessageQueue字段的值设置为主线的MessageQueue实例
                ReflectUtils.reflect(looper).field("mQueue", queue);
            }

            // 4、reset为false,表示需要将子线程Looper中的MessageQueue重置为原来的。
            ReflectUtils.reflect(threadLocal).method("set", looper);
            return true;
        }
    }
}

3、AsynclayoutInflater异步创建View

在第三小节中,我们对Android的布局加载原理进行了深入地分析,从中我们得出了布局加载过程中的两个耗时点:

  1. 布局文件读取慢:IO过程。
  2. 创建View慢:使用反射,比直接new的方式要慢3倍。布局嵌套层级越多,控件个数越多,反射的次数就会越频繁。

很明显,我们无法从根本上去解决这两个问题,但是Google提供了一个从侧面解决的方案:使用AsyncLayoutInflater去异步加载对应的布局,它的特点如下:

  1. 工作线程加载布局。
  2. 回调主线程。
  3. 节省主线程时间。

接下来,我将详细地介绍AsynclayoutInflater的使用。

首先,在项目的build.gradle中进行配置:

implementation ‘com.android.support:asynclayoutinflater:28.0.0’

然后,在Activity中的onCreate方法中将setContentView注释:

super.onCreate(savedInstanceState);
// 内部分别使用了IO和反射的方式去加载布局解析器和创建对应的View
// setContentView(R.layout.activity_main);

接着,在super.onCreate方法前继续布局的异步加载:

// 使用AsyncLayoutInflater进行布局的加载
new AsyncLayoutInflater(MainActivity.this).inflate(R.layout.activity_main, null, new AsyncLayoutInflater.OnInflateFinishedListener() {
     
        @Override
        public void onInflateFinished(@NonNull View view, int i, @Nullable ViewGroup viewGroup) {
     
            setContentView(view);
            // findViewById、视图操作等
    }
});
super.onCreate(savedInstanceState);

AsyncLayoutInflater是通过侧面缓解的方式去缓解布局加载过程中的卡顿,但是它依然存在一些问题:

  1. 不能设置LayoutInflater.Factory,通过自定义AsyncLayoutInflater的方式解决,由于它是一个final,所以需要将代码直接拷处进行修改。
  2. 因为是异步加载,所以需要注意在布局加载过程中不能有依赖于主线程的操作。

Android AsyncLayoutInflater 限制及改进

4、使用掌阅团队推出的X2C进行布局加载优化

X2C框架保留了XML的优点,并解决了其IO操作和反射的性能问题。开发人员只需要正常写XML代码即可,在编译期,X2C会利用APT工具将XML代码翻译为Java代码。

X2C项目地址
但是,X2C框架还存在一些问题:

  1. 部分Java属性不支持。
  2. 失去了系统的兼容(AppCompat)
  3. merge标签 ,在编译期间无法确定xml的parent,所以无法支持。
  4. 系统style,在编译期间只能查到应用的style列表,无法查询系统style,所以只支持应用内style。

5、使用ConstraintLayout降低布局嵌套层级

六、 布局优化进阶方案

1、使用异步布局框架Litho
Litho是Facebook开源的一款在Android上高效建立UI的声明式框架,它具有以下特点:

  1. 声明式:它使用了声明式的API来定义UI组件。
  2. 异步布局:它可以提前布局UI,而不会阻塞UI线程。
  3. 视图扁平化:它使用了Facebook开源的另一款布局引擎Yoga进行布局,以自动减少UI包含的ViewGroup数量。
  4. 细粒度的回收:可以回收文本或图形等任何组件,并可以在用户界面的任何位置重复使用。

项目地址:
https://github.com/facebook/litho

Litho的缺点:

使用Litho,在布局性能上有很大的提升,但是开发成本太高,因为需要自己去实现很多的组件,并且其组件需要在编译时才能生成,不能够进行实时预览,但是可以把Litho封装成Flexbox布局的底层渲染引擎,以此实现上层的动态化,具体实现原理可参见Litho在美团动态化方案MTFlexbox中的实践。

2、使用Flutter实现高性能的UI布局

Flutter可以说是2019最火爆的框架之一了,它是 Google 开源的 UI 工具包,帮助开发者通过一套代码库高效构建多平台精美应用,支持移动、Web、桌面和嵌入式平台。对于Android来说,FLutter能够创作媲美原生的高性能应用,应用使用 Dart语言进行 开发。Flutter的架构类似于Android的层级架构,每一层都建立在前一层之上,其架构图如下所示:

Android布局原理与优化_第8张图片
在Framework层中,Flutter通过在 widgets 层组合基础 widgets 来构建 Material 层,而 widgets 层本身则是通过对来自 Rendering 层的低层次对象组合而来。而在Engine层,Flutter集成了Skia引擎用于进行栅格化,并且使用了Dart虚拟机。

那么Flutter的图形性能为何能够媲美原生应用呢?
接下来,我们以Flutter、原生Android、其它跨平台框架如RN来做比较,它们的图形绘制调用层级图如下所示:
Android布局原理与优化_第9张图片

可以看到,Flutter框架的代码完全取代了Java层的框架代码,所以只要当Flutter框架中Dart代码的效率可以媲美原生框架的Java代码的时候,那么总体的Flutter App的性能就能够媲美原生的APP。而反观其它流行的跨平台框架如RN,它首先需要调用自身的Js代码,然后再去调用Java层的代码,这里比原生和Flutter的App显然多出来一个步骤,所以它的性能肯定是不及原生的APP的。此外,Flutter App不同于原生、RN,它内部是直接包含了Skia渲染引擎的,只要Flutter SDK进行升级,Skia就能够升级,这样Skia的性能改进就能够同步到Flutter框架之中。而对于Android原生和RN来说,只能等到Android系统升级才能同步Skia的性能改进。

而Flutter又是如何实现高性能UI布局的呢?
接下来,我们来大致了解一下Flutter的UI绘制原理,它主要是通过VSYNC信号来使UI线程和GPU线程有条不紊的周期性的去渲染界面,其绘制原理图如下所示:

Android布局原理与优化_第10张图片

绘制步骤大致如下:

  1. 首先 UI Runner 会执行 root isolate(可简单理解为Dart VM的线程),它会告诉引擎层有帧要渲染,当需要渲染则会调用到Engine的ScheduleFrame()来注册VSYNC信号回调,一旦触发回调doFrame(),并当它执行完成后,便会移除回调方法,也就是说一次注册一次回调。
  2. 当需要再次绘制则需要重新调用到ScheduleFrame()方法,该方法的唯一重要参数regenerate_layer_tree决定在帧绘制过程是否需要重新生成layer tree,还是直接复用上一次的layer tree。
  3. 接着,执行的是UI线程绘制过程中最核心的WidgetsBinding的drawFrame()方法,然后会创建layer tree视图树。
  4. 然后 Layer Tree 会交给 GPU Task Runner 进行合成和栅格化。
  5. 最后,GPU Task Runner会利用Skia库结合GL或Vu’lkan将layer tree提供的信息转化为平台可执行的GPU指令。

此外,Flutter 也采用了类似 Litho 的props属性不可变、Reat单向数据流的方案,用于将视图与数据分离。对于Flutter这一大前端领域的核心技术,笔者也是充满兴趣,后续会有计划对此进行深入研究,敬请期待。

3、使用RenderThread 与 RenderScript

在Android 5.0之后,Android引进了RenderThread,它能够实现动画的异步渲染。但是目前支持RenderThread完全渲染的动画,只有两种,即ViewPropertyAnimator和CircularReveal(揭露动画)。对于CircularReveal使用比较简单且功能较为单一,就不多做过多的描述了。下面我简单说下ViewPropertyAnimator中如何去利用RenderThread。

  1. 在ViewPropertyAnimator类系中,有一个ViewPropertyAnimatorRT ,它的主要作用就把动画交给RenderThread去处理。因此,我们需要先去创建对应view的ViewPropertyAnimatorRT,代码如下所示:
 /**
 * 使用反射的方式去创建对应View的ViewPropertyAnimatorRT(非hide类)
 */
private static Object createViewPropertyAnimatorRT(View view) {
     
    try {
                
        Class<?> animRtClazz = Class.forName("android.view.ViewPropertyAnimatorRT");
        Constructor<?> animRtConstructor = animRtClazz.getDeclaredConstructor(View.class);
        animRtConstructor.setAccessible(true);
        Object animRt = animRtConstructor.newInstance(view);            
        return animRt;
    } catch (Exception e) {
                 
        Log.d(TAG, "创建ViewPropertyAnimatorRT出错,错误信息:" + e.toString());           
        return null;
    }
}

2、接下来,我们需要将ViewPropertyAnimatorRT设置给ViewPropertyAnimator的mRTBackend字段,这样ViewPropertyAnimator才能利用它去将动画交给RenderThread处理,如下所示:

private static void setViewPropertyAnimatorRT(ViewPropertyAnimator animator, Object rt) {
            
 try {
     
        Class<?> animClazz = Class.forName("android.view.ViewPropertyAnimator");
        Field animRtField = animClazz.getDeclaredField("mRTBackend");
        animRtField.setAccessible(true);
        animRtField.set(animator, rt);
    } catch (Exception e) {
     
        Log.d(TAG, "设置ViewPropertyAnimatorRT出错,错误信息:" + e.toString());
    }
}

/**
 * 在animator.start()即执行动画开始之前配置的方法
 */
public static void onStartBeforeConfig(ViewPropertyAnimator animator, View view) {
     
    Object rt = createViewPropertyAnimatorRT(view);
    setViewPropertyAnimatorRT(animator, rt);
}

3、最后,在开启动画之前将ViewPropertyAnimatorRT实例设置进去即可,如下所示:

ViewPropertyAnimator animator = v.animate().scaleY(2).setDuration(2000);
AnimHelper.onStartBeforeConfig(animator, v);
animator.start();

现在,如果是做音视频或图像处理的工作,经常需要对图片进行高斯模糊、放大、锐化等操作,但是这里涉及大量的图片变换操作,例如缩放、裁剪、二值化以及降噪等。而图片的变换又涉及大量的计算任务,这个时候我们可以通过RenderScript去充分利用手机的GPU计算能力,以实现高效的图片处理。

而RenderScript的工作流程需要经历如下三个步骤:

  1. RenderScript运行时API:提供进行运算的API。
  2. 反射层:相当于NDK中的JNI胶水代码,它是一些由Android编译工具自动生成的类,对我们写的RenderScript代码进行包装,以使得安卓层能够和RenderScript进行交互。
  3. 安卓框架:通过调用反射层来访问RenderScript运行时。
  4. 由于RenderScript主要是用于音视频、图像处理等细分领域,这里笔者就不继续深入扩展了

七、布局优化的常见问题

1、你在做布局优化的过程中用到了哪些工具?
我在做布局优化的过程中,用到了很多的工具,但是每一个工具都有它不同的使用场景,不同的场景应该使用不同的工具。下面我从线上和线下两个角度来进行分析。

比如说,我要统计线上的FPS,我使用的就是Choreographer这个类,它具有以下特性:

  1. 能够获取整体的帧率。
  2. 能够带到线上使用。
  3. 它获取的帧率几乎是实时的,能够满足我们的需求

同时,在线下,如果要去优化布局加载带来的时间消耗,那就需要检测每一个布局的耗时,对此我使用的是AOP的方式,它没有侵入性,同时也不需要别的开发同学进行接入,就可以方便地获取每一个布局加载的耗时。如果还要更细粒度地去检测每一个控件的加载耗时,那么就需要使用LayoutInflaterCompat.setFactory2这个方法去进行Hook。

此外,我还使用了LayoutInspectorSystrace这两个工具,Systrace可以很方便地看到每帧的具体耗时以及这一帧在布局当中它真正做了什么。而LayoutInspector可以很方便地看到每一个界面的布局层级,帮助我们对层级进行优化。

2、布局为什么会导致卡顿,你又是如何优化的?

分析完布局的加载流程之后,我们发现有如下四点可能会导致布局卡顿:

  1. 系统会将我们的Xml文件通过IO的方式映射的方式加载到我们的内存当中,而IO的过程可能会导致卡顿。
  2. 其次,布局加载的过程是一个反射的过程,而反射的过程也会可能会导致卡顿。
  3. 同时,这个布局的层级如果比较深,那么进行布局遍历的过程就会比较耗时。
  4. 最后,不合理的嵌套RelativeLayout布局也会导致重绘的次数过多。

对此,我们的优化方式有如下几种:

  1. 针对布局加载Xml文件的优化,我们使用了异步Inflate的方式,即AsyncLayoutInflater。它的核心原理是在子线程中对我们的Layout进行加载,而加载完成之后会将View通过Handler发送到主线程来使用。所以不会阻塞我们的主线程,加载的时间全部是在异步线程中进行消耗的。而这仅仅是一个从侧面缓解的思路。
  2. 后面,我们发现了一个从根源解决上述痛点的方式,即使用X2C框架。它的一个核心原理就是在开发过程我们还是使用的XML进行编写布局,但是在编译的时候它会使用APT的方式将XML布局转换为Java的方式进行布局,通过这样的方式去写布局,它有以下优点:1、它省去了使用IO的方式去加载XML布局的耗时过程。2、它是采用Java代码直接new的方式去创建控件对象,所以它也没有反射带来的性能损耗。这样就从根本上解决了布局加载过程中带来的问题。
  3. 然后,我们可以使用ConstraintLayout去减少我们界面布局的嵌套层级,如果原始布局层级越深,它能减少的层级就越多。而使用它也能避免嵌套RelativeLayout布局导致的重绘次数过多。
  4. 最后,我们可以使用AspectJ框架(即AOP)和LayoutInflaterCompat.setFactory2的方式分别去建立线下全局的布局加载速度和控件加载速度的监控体系。

3、做完布局优化有哪些成果产出?

  1. 首先,我们建立了一个体系化的监控手段,这里的体系还指的是线上加线下的一个综合方案,针对线下,我们使用AOP或者ARTHook,可以很方便地获取到每一个布局的加载耗时以及每一个控件的加载耗时。针对线上,我们通过Choreographer.getInstance().postFrameCallback的方式收集到了FPS,这样我们可以知道用户在哪些界面出现了丢帧的情况。
  2. 然后,对于布局监控方面,我们设立了FPS、布局加载时间、布局层级等一系列指标。
  3. 最后,在每一个版本上线之前,我们都会对我们的核心路径进行一次Review,确保我们的FPS、布局加载时间、布局层级等达到一个合理的状态。

八、总结

对于Android的布局优化,笔者以一种自顶向下,层层递进的方式和大家一起深入地去探索了Android中如何将布局优化做到极致,其中主要涉及以下七大主题:

  1. 绘制原理:CPU\GPU、Android图形系统的整体架构、绘制线程、刷新机制。
  2. 布局加载原理:布局加载源码分析、LayoutInflater.Factory分析。
  3. 获取界面布局耗时:使用AOP的方式去获取界面加载的耗时、利用LayoutInflaterCompat.setFactory2去监控每一个控件加载的耗时。
  4. 布局优化常规方案:使用AOP的方式去获取界面加载的耗时、利用LayoutInflaterCompat.setFactory2去监控每一个控件加载的耗时。
  5. 布局优化的进阶方案:使用异步布局框架Litho、使用Flutter实现高性能的UI布局、使用RenderThread实现动画的异步渲染与 利用RenderScript实现高效的图片处理。
  6. 布局优化的常见问题。

你可能感兴趣的:(Android系统源码解析,性能优化,布局优化,源码解析,android)