How does an XML layout file become a View and populate the View tree? With this question in mind, I read the source code and found a solution to optimize the layout build time.

This is the third article in the Android performance optimization series, and the list is as follows:

  1. Android performance optimization | animation to OOM? Optimized SurfaceView resolution for frame animation frame by frame
  2. A larger Android performance optimization | do animation to caton? Optimized SurfaceView sliding window frame multiplexing for frame animation
  3. Android performance optimization | to shorten the building layout is 20 times (on)
  4. Android performance optimization | to shorten the building layout is 20 times (below)

The time spent building the layout is an essential part of optimizing the speed at which an Activity starts.

To optimize, measure first. Is there a way to accurately measure layout time?

Read layout file

SetContentView () : setContentView()

public class AppCompatActivity
    @Override
    public void setContentView(View view) { getDelegate().setContentView(view); }}Copy the code

Open the setContentView() source code, and the implementation is handed to a proxy, tracing down the call chain, and the final implementation is in AppCompatDelegateImpl:

class AppCompatDelegateImpl{
    @Override
    public void setContentView(int resId) {
        ensureSubDecor();
        //'1. Get the Content view from the top view '
        ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
        //'2. Remove all subviews'
        contentParent.removeAllViews();
        //'3. Parse the layout file and populate it in the Content view 'LayoutInflater.from(mContext).inflate(resId, contentParent); mAppCompatWindowCallback.getWrapped().onContentChanged(); }}Copy the code

The most time-consuming operation of the three is “Parse layout file”. Check it out:

public abstract class LayoutInflater {
    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
        finalResources res = getContext().getResources(); .//' Get the layout file parser '
        final XmlResourceParser parser = res.getLayout(resource);
        try {
            //' Fill layout '
            return inflate(parser, root, attachToRoot);
        } finally{ parser.close(); }}}Copy the code

We call getLayout() to get the parser that corresponds to the layout file, and continue down the call chain:

public class ResourcesImpl {
    XmlResourceParser loadXmlResourceParser(@NonNull String file, @AnyRes int id, int assetCookie,@NonNull String type) throws NotFoundException {
        if(id ! =0) {
            try {
                synchronized (mCachedXmlBlocks) {
                    ...
                    //' Get the layout file object from AssetManager '
                    final XmlBlock block = mAssets.openXmlBlockAsset(assetCookie, file);
                    if(block ! =null) {
                        final int pos = (mLastCachedXmlBlockIndex + 1) % num;
                        mLastCachedXmlBlockIndex = pos;
                        final XmlBlock oldBlock = cachedXmlBlocks[pos];
                        if(oldBlock ! =null) {
                            oldBlock.close();
                        }
                        cachedXmlBlockCookies[pos] = assetCookie;
                        cachedXmlBlockFiles[pos] = file;
                        cachedXmlBlocks[pos] = block;
                        returnblock.newParser(); }}}catch(Exception e) { ... }}... }}Copy the code

Along the chain, and ultimately reached the ResourcesImpl. LoadXmlResourceParser (), it through the AssetManager. OpenXmlBlockAsset () will XmlBlock XML layout files into Java objects:

public final class AssetManager implements AutoCloseable {
    @NonNull XmlBlock openXmlBlockAsset(int cookie, @NonNull String fileName) throws IOException {Preconditions. CheckNotNull (fileName, "fileName");synchronized (this) {
            ensureOpenLocked();
            //' Open XML layout file '
            final long xmlBlock = nativeOpenXmlAsset(mObject, cookie, fileName);
            if (xmlBlock == 0) {
                //' no exception found '
                throw new FileNotFoundException(“Asset XML file: ” + fileName);
            }
            final XmlBlock block = new XmlBlock(this, xmlBlock);
            incRefsLocked(block.hashCode());
            returnblock; }}}Copy the code

The layout file is read into memory using a native method. One thing is for sure: “Before parsing the XML layout file, you need to do IO operations to read it into memory.”

Parsing layout files

Reading the source code is like recursing, just by recursing over and over again, and now by returning to the key method:

public abstract class LayoutInflater {
    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
        finalResources res = getContext().getResources(); .//' Get the layout file parser '
        final XmlResourceParser parser = res.getLayout(resource);
        try {
            //' Fill layout '
            return inflate(parser, root, attachToRoot);
        } finally{ parser.close(); }}}Copy the code

After reading the layout file into memory through an IO operation, we call inflate() :

public abstract class LayoutInflater {
    public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            ...
            try {
                    //' Build the View from the label of the control declared in the layout file '
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                    ViewGroup.LayoutParams params = null;

                    //' Build layout parameters for View '
                    if(root ! =null) {
                        // Create layout params that match root, if supplied
                        params = root.generateLayoutParams(attrs);
                        if(! attachToRoot) {// Set the layout params for temp if we are not
                            // attaching. (If we are, we use addView, below)temp.setLayoutParams(params); }}...//' fill the View tree with views'
                    if(root ! =null&& attachToRoot) { root.addView(temp, params); }... }catch (XmlPullParserException e) {
                ...
            }  finally{... }returnresult; }}Copy the code

This method parses the layout file and builds a View instance from the label in which the control is declared, then populates it into the View tree. Parse the details of the layout file in createViewFromTag() :

public abstract class LayoutInflater {
    View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,boolean ignoreThemeAttr) {...try {
            View view;
            //' Build View from factory2.onCreateView () '
            if(mFactory2 ! =null) { view = mFactory2.onCreateView(parent, name, context, attrs); }...return view;
        } catch (InflateException e) {
            throwe; }... }}Copy the code

The specific implementation of onCreateView() is in AppCompatDelegateImpl:

class AppCompatDelegateImpl{
    @Override
    public final View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        return createView(parent, name, context, attrs);
    }
    
    @Override
    public View createView(View parent, final String name, @NonNull Context context,
            @NonNull AttributeSet attrs) {
        if (mAppCompatViewInflater == null) {
            TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
            String viewInflaterClassName =
                    a.getString(R.styleable.AppCompatTheme_viewInflaterClass);
            if ((viewInflaterClassName == null){
                ...
            } else {
                try {
                    //' Get AppCompatViewInflater instance via reflection 'Class<? > viewInflaterClass = Class.forName(viewInflaterClassName); mAppCompatViewInflater = (AppCompatViewInflater) viewInflaterClass.getDeclaredConstructor() .newInstance(); }catch(Throwable t) { ... }}}boolean inheritContext = false;
        if (IS_PRE_LOLLIPOP) {
            inheritContext = (attrs instanceof XmlPullParser)
                    // If we have a XmlPullParser, we can detect where we are in the layout
                    ? ((XmlPullParser) attrs).getDepth() > 1
                    // Otherwise we have to use the old heuristic
                    : shouldInheritContext((ViewParent) parent);
        }

        //' createView instance with createView() '
        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 */); }}Copy the code

AppCompatDelegateImpl entrusting construct View to again AppCompatViewInflater. CreateView () :

 final View createView(View parent, final String name, @NonNull Context context,
            @NonNull AttributeSet attrs, boolean inheritContext,
            boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
        finalContext originalContext = context; . View view =null;

        //' create control instances with the names of the controls in the layout file '
        switch (name) {
            case "TextView":
                view = createTextView(context, attrs);
                verifyNotNull(view, name);
                break;
            case "ImageView":
                view = createImageView(context, attrs);
                verifyNotNull(view, name);
                break;
            case "Button":
                view = createButton(context, attrs);
                verifyNotNull(view, name);
                break;
            case "EditText":
                view = createEditText(context, attrs);
                verifyNotNull(view, name);
                break;
            case "Spinner":
                view = createSpinner(context, attrs);
                verifyNotNull(view, name);
                break;
            case "ImageButton":
                view = createImageButton(context, attrs);
                verifyNotNull(view, name);
                break;
            case "CheckBox":
                view = createCheckBox(context, attrs);
                verifyNotNull(view, name);
                break;
            case "RadioButton":
                view = createRadioButton(context, attrs);
                verifyNotNull(view, name);
                break;
            case "CheckedTextView":
                view = createCheckedTextView(context, attrs);
                verifyNotNull(view, name);
                break;
            case "AutoCompleteTextView":
                view = createAutoCompleteTextView(context, attrs);
                verifyNotNull(view, name);
                break;
            case "MultiAutoCompleteTextView":
                view = createMultiAutoCompleteTextView(context, attrs);
                verifyNotNull(view, name);
                break;
            case "RatingBar":
                view = createRatingBar(context, attrs);
                verifyNotNull(view, name);
                break;
            case "SeekBar":
                view = createSeekBar(context, attrs);
                verifyNotNull(view, name);
                break;
            case "ToggleButton":
                view = createToggleButton(context, attrs);
                verifyNotNull(view, name);
                break;
            default: view = createView(context, name, attrs); }...return view;
    }
    
    //' Build AppCompatTextView instance '
    protected AppCompatTextView createTextView(Context context, AttributeSet attrs) {
        return newAppCompatTextView(context, attrs); }... }Copy the code

The new View instance was created using switch-case.

And there’s no need to manually convert all textViews in the layout file to AppCompatTextView, just use AppCompatActivity, which does the conversion in the factory2.onCreateView () interface.

Measure the time it takes to build the layout

From the above analysis, two conclusions can be drawn:

1. When building a layout, you need to perform IO operations to read the layout file into memory.

2. Iterate through each label in the memory layout file, addView instances to the View tree according to the label name.

Both steps are time consuming! How time-consuming is it?

LayoutInflaterCompat provides setFactory2(), which intercepts the creation of every View in the layout file:

class Factory2Activity : AppCompatActivity(a){
    private var sum: Double = 0.0

    @ExperimentalTime
    override fun onCreate(savedInstanceState: Bundle?) {
        LayoutInflaterCompat.setFactory2(LayoutInflater.from(this@Factory2Activity), object : LayoutInflater.Factory2 {
            
            override fun onCreateView(parent: View? , name: String? , context: Context? , attrs: AttributeSet?): View? {
                //' Measure the time it takes to build a single View 'val (view, duration) = measureTimedValue { delegate.createView(parent, name, context!! , attrs!!) }//' add up the time to build the view 'Sum += duration.inmilliseconds log. v(" test ", "view=${view? .let {it::class. SimpleName}} duration=${duration} sum=${sum} ")return view
            }

            //' This method is compatible with Factory and returns null '
            override fun onCreateView(name: String? , context: Context? , attrs: AttributeSet?): View? {
                return null}})super.onCreate(savedInstanceState)
        setContentView(R.layout.factory2_activity2)
    }
}
Copy the code

Before super.onCreate(savedInstanceState), inject a custom Factory2 interface into LayoutInflaterCompat.

Call delegate.createView(parent, name, context!! , attrs!!) Manually trigger the logic in the source code to build the layout.

MeasureTimedValue () is a library method provided by Kotlin that measures the time spent on a method, defined as follows:

public inline fun <T> measureTimedValue(block: () -> T): TimedValue<T> {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    //' delegate to MonoClock'
    return MonoClock.measureTimedValue(block)
}

public inline fun <T> Clock.measureTimedValue(block: () -> T): TimedValue<T> {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }

    val mark = markNow()
    //' execute the original method '
    val result = block()
    return TimedValue(result, mark.elapsedNow())
}

public data class TimedValue<T> (val value: T.val duration: Duration)
Copy the code

Method returns a TimedValue object whose first property is the return value of the original method and the second is the time it took to execute the original method. The test code assigns the return value and time to view and Duration, respectively, through a destruct declaration. It then prints the time it took to build each view.

Once you understand the process of building a layout, you have the right direction for optimization.

With a way to measure the time it takes to build a layout, you have a tool to compare optimization results.

Due to space constraints, the 20-fold reduction in the time it takes to build a layout will have to be saved for the next article.