随笔博文

Android性能优化 - 把构建布局耗时缩短 20 倍

2022-12-14 12:57:41 michael007js 179

布局构建耗时是优化 Activity 启动速度中不可缺少的一个环节。

欲优化,先度量。有啥办法可以精确地度量布局耗时?

读布局文件

以熟悉的setContentView()为切入点,看看有没有突破口:

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

点开setContentView()源码,它的实现交给了一个代理,沿着调用链往下追查,最终的实现代码在AppCompatDelegateImpl中:

class AppCompatDelegateImpl{
   @Override
   public void setContentView(int resId) {
       ensureSubDecor();
       //'1.从顶层视图获得content视图'
       ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
       //'2.移除所有子视图'
       contentParent.removeAllViews();
       //'3.解析布局文件并填充到content视图中'
       LayoutInflater.from(mContext).inflate(resId, contentParent);
       mAppCompatWindowCallback.getWrapped().onContentChanged();
 }
}

这三部中,最耗时操作应该是“解析布局文件”,点进去看看:

public abstract class LayoutInflater {
   public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
       final Resources res = getContext().getResources();
     ...
       //'获取布局文件解析器'
       final XmlResourceParser parser = res.getLayout(resource);
       try {
           //'填充布局'
           return inflate(parser, root, attachToRoot);
     } finally {
           parser.close();
     }
 }
}

先调用了getLayout()获取了和布局文件对应的解析器,沿着调用链继续追查:

public class ResourcesImpl {
   XmlResourceParser loadXmlResourceParser(@NonNull String file, @AnyRes int id, int assetCookie,@NonNull String type) throws NotFoundException {
       if (id != 0) {
           try {
               synchronized (mCachedXmlBlocks) {
                 ...
                   //'通过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;
                       return block.newParser();
                 }
             }
         } catch (Exception e) {
             ...
         }
     }
     ...
 }
}

沿着调用链,最终走到了ResourcesImpl.loadXmlResourceParser(),它通过AssetManager.openXmlBlockAsset()将 xml 布局文件转化成 Java 对象XmlBlock

public final class AssetManager implements AutoCloseable {
   @NonNull XmlBlock openXmlBlockAsset(int cookie, @NonNull String fileName) throws IOException {
       Preconditions.checkNotNull(fileName, ”fileName“);
       synchronized (this) {
           ensureOpenLocked();
           //'打开 xml 布局文件'
           final long xmlBlock = nativeOpenXmlAsset(mObject, cookie, fileName);
           if (xmlBlock == 0) {
               //'若打开失败则抛文件未找到异常'
               throw new FileNotFoundException(“Asset XML file: + fileName);
         }
           final XmlBlock block = new XmlBlock(this, xmlBlock);
           incRefsLocked(block.hashCode());
           return block;
     }
 }
}

通过一个 native 方法,将布局文件读取到内存。走查到这里,有一件事可以确定,即 “解析 xml 布局文件前需要进行 IO 操作,将其读取至内存中”

解析布局文件

读原码就好像“递归”,刚才通过不断地“递”,现在通过“归”回到那个关键方法:

public abstract class LayoutInflater {
   public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
       final Resources res = getContext().getResources();
     ...
       //'获取布局文件解析器'
       final XmlResourceParser parser = res.getLayout(resource);
       try {
           //'填充布局'
           return inflate(parser, root, attachToRoot);
     } finally {
           parser.close();
     }
 }
}

通过 IO 操作将布局文件读到内存后,调用了inflate()

public abstract class LayoutInflater {
   public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
       synchronized (mConstructorArgs) {
         ...
           try {
                   //'根据布局文件的声明控件的标签构建 View'
                   final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                   ViewGroup.LayoutParams params = null;

                   //'构建 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);
                     }
                 }

                 ...
                   //'将 View 填充到 View 树'
                   if (root != null && attachToRoot) {
                       root.addView(temp, params);
                 }
                 ...
         } catch (XmlPullParserException e) {
             ...
         }  finally {
             ...
         }
           return result;
     }
 }

这个方法解析布局文件并根据其中声明控件的标签构建 View实例,然后将其填充到 View 树中。解析布局文件的细节在createViewFromTag()中:

public abstract class LayoutInflater {
   View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,boolean ignoreThemeAttr) {
     ...

       try {
           View view;
           //'通过Factory2.onCreateView()构建 View'
           if (mFactory2 != null) {
               view = mFactory2.onCreateView(parent, name, context, attrs);
         }
         ...
           return view;
     } catch (InflateException e) {
           throw e;

     }
     ...
 }
}

onCreateView()的具体实现在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 {
                   //'通过反射获取AppCompatViewInflater实例'
                   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()创建View实例'
       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 */
     );
 }
}

AppCompatDelegateImpl又把构建 View 委托给了 AppCompatViewInflater.createView()

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

       //'以布局文件中控件的名称分别创建对应控件实例'
       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;
 }
   
   //'构建 AppCompatTextView 实例'
   protected AppCompatTextView createTextView(Context context, AttributeSet attrs) {
       return new AppCompatTextView(context, attrs);
 }
 ...
}

没想到,最终居然是通过switch-case的方法来 new View 实例。

而且我们没有必要手动将布局文件中的TextView都换成AppCompatTextView,只要使用AppCompatActivity,它在Factory2.onCreateView()接口中完成了控件转换。

测量构建布局耗时

通过上面的分析,可以得出两条结论:

1. Activity 构建布局时,需要先进行 IO 操作,将布局文件读取至内存中。

2. 遍历内存布局文件中每一个标签,并根据标签名 new 出对应视图实例,再把它们 addView 到 View 树中。

这两个步骤都是耗时的!到底有多耗时呢?

LayoutInflaterCompat提供了setFactory2(),可以拦截布局文件中每一个 View 的创建过程:

class Factory2Activity : AppCompatActivity() {
   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? {
               //'测量构建单个View耗时'
               val (view, duration) = measureTimedValue { delegate.createView(parent, name, context!!, attrs!!) }
               //'累加构建视图耗时'
               sum += duration.inMilliseconds
               Log.v(“test”, “view=${view?.let { it::class.simpleName }} duration=${duration}  sum=${sum})
               return view
         }

           //'该方法用于兼容Factory,直接返回null就好'
           override fun onCreateView(name: String?, context: Context?, attrs: AttributeSet?): View? {
               return null
         }
     })
       super.onCreate(savedInstanceState)
       setContentView(R.layout.factory2_activity2)
 }
}

super.onCreate(savedInstanceState)之前,将自定义的Factory2接口注入到LayoutInflaterCompat中。

调用delegate.createView(parent, name, context!!, attrs!!),就是手动触发源码中构建布局的逻辑。

measureTimedValue()是 Kotlin 提供的库方法,用于测量一个方法的耗时,定义如下:

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

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

   val mark = markNow()
   //'执行原方法'
   val result = block()
   return TimedValue(result, mark.elapsedNow())
}

public data class TimedValue<T>(val value: T, val duration: Duration)

方法返回一个TimedValue对象,其第一个属性是原方法的返回值,第二个是执行原方法的耗时。测试代码中通过解构声明分别将返回值和耗时赋值给viewduration。然后把构建每个视图的耗时累加打印。

了解了构建布局的过程,就有了对症下药优化的方向。

有了测量构建布局耗时的方法,就有了对比优化效果的工具。


首页
关于博主
我的博客
搜索