目录 人到中年不得已,莫愁前路有知己! 本篇是Android性能优化系列专栏第三篇,上一篇中通过图文加实战的方式介绍了Android的内存优化,有需要的可以翻看一下: 《你的应用内存优化了吗?》,今天我们就继续来说说Android中的布局优化相关的知识。 对于Android手机来说,它的画面渲染依赖于两个硬件:①、CPU;②、GPU: ①、Systrace 上面这张图是我找的一个使用Systrace生成的.html文件,图中每一个F的出现就表明出现了一帧,可以看到这两个F之间的时间间隔比16ms多了不少,Alert type这里面就是Systrace自动给出的一些提示信息,我们可以根据提示信息来查找修改的方向。 ②、Layout Inspector 菜单栏——>Tools——>Layout Inspector 如果进程中存在多个Activity,还会提示让我们选择具体哪个Activity,当然了,我这里只有一个Activity,直接点击该进程即可: 上面这张图就是Layout Inspector为我们生成的具体检测界面的信息,这个界面总共是分成了三个部分: 左侧:View Tree,即:视图在布局中的层次结构 中间:Load Overlay,即:相当于屏幕截图,并且这个截图自带各个视图控件的边距 右侧:Properties Table,即:选中视图的布局属性,比如我这里选中的是一个TextView,里面就会显示出这个TextView所显示出的具体信息 对于这个工具我们主要关注的是最左侧的这一栏层次结构,有了它就可以很方便的看到当前布局的层级,比如我这里的LinearLayout这是当前界面的根布局,它里面是一个RecyclerView,对于RecyclerView的每一个Item都有三个控件,这三个控件都处在同一层级,整个条目的层级相对还是比较简单的。 ③、Choreographer 获取FPS,线上使用,具备实时性 这里写了一个方法getFPS()来获取这个APP的FPS情况,方法内部一开始是做了一个保护性操作,确保使用的Choreographer发生在API16之后,然后在doFrame回调中首先判断是不是统计周期的第一次,如果是就记录第一次回调的时间,接下来就是判断时间间隔是否超过预设的阀值160ms,如果超过则计算FPS,计算方式是间隔时间除以间隔时间内发生的次数,如果没有超过则直接将次数加1。 输出的结果可以看到基本上都是59和60之间的数值。 ①、源码解析 这一部分我们来看下源码,因为内容比较多,我就尽可能的简单说,对于源码阅读的流程我们之前已经说过几次了,这里就不再介绍了,基本上就是找到你需要的入口方法,然后一路跟踪下去,把整个流程串起来,不需要你把每一行的代码都读懂。 既然说的是布局加载,那么我们首先肯定是找入口方法,这个方法你回想一下每个页面加载布局都是调用的什么方法呢?很简单啦: 然后点击这个方法进入源码中去就到了AppCompatActivity类的setContentView()方法中: 继续跟踪点击setContentView()方法: 发现这是一个抽象方法,此时你需要去找它的实现类AppCompatDelegateImpl中的方法了,点击左侧向下的fx🔽向下箭头: 这个方法中由于传递进来的resId也就是布局文件的id,它只在LayoutInflater这一行用到了,所以接着跟踪这一行,点击inflate()方法: 这个方法内部又调用了另一个inflate()方法,所以继续点击: 这里面又有一个inflate()方法,入参有一个parser,看了看上下的代码,知道了它其实是XmlResourceParser的实例,那我们先不去看这个inflate()方法具体的实现,先来看下这个parser究竟是什么?找到res.getLayout()方法,里面传入了我们的资源id,返回的是XmlResourceParser,看名字XML资源解析器,就知道这玩意应该很屌,来吧,继续点击getLayout(): 没啥实质性的内容,继续点击它的实现方法loadXmlResourceParser(): 这个方法开始是一些对象的声明,后面是异常的处理,所以看下来真正有用的就是if判断里面的,它判断了value.type如果是String类型的,然后继续调用了impl的loadXmlResourceParser()方法,我们点进去看下: 主要看注释那里的说明哈,Android中的布局都是写在XML文件中的,这个方法就是为我们具体所写的布局文件准备一个XML的解析器,所以它实际上就是一个XML的Pull解析的过程。需要注意的是:android的布局实际上是一个XML文件,它在加载的时候会首先将它读取到内存中,这个过程实际上就是一个IO过程,一般在android开发中操作IO都会将其置于工作线程中,所以这里可能会成为我们优化的一个方向。 关于这个XmlResourceParser就说到这里,下面继续回到上面说的那个inflate()方法中: 这里同样的省略了部分代码,我们知道日常开发中经常会碰到一些报错,其实这些报错在Android的源码中都是有所体现的,比如这里定义的关于merge标签的一个异常信息。接着看createViewFromTag()这个方法,看名字我们应该能大致猜测出来它是干嘛的了,它应该就是通过一系列的Tag来创建相对应的View,我们点击该方法跟进: 这里面又调用了另一个createViewFromTag()方法,继续跟进: 这里就到了重点的地方了,这里面就是创建View的过程了: 首先:View view = tryCreateView(parent, name, context, attrs); 它通过这个tryCreateView()方法构建出View对象,进到这个方法中: 这个方法里面就是判断了几个factory是否为空,首先是Factory2,如果Factory2不为空则调用Factory2的onCreateView()方法创建View对象,否则判断Factory是否为空,如果Factory不为空则调用Factory的onCreateView()创建View对象,如果都为空,则View为空。如果view为空并且PrivateFactory不为空,则调用PrivateFactory的onCreateView()方法构建View,需要注意的是PrivateFactory它只用于Fragment标签的加载。当这些条件都不满足的时候,我们回到上面的createViewFromTag()方法中接着看,它会走到view==null的条件判断中去,它会走onCreateView()或者createView(),点击createView()继续跟踪: 这个方法里面constructor = clazz.getConstructor(mConstructorSignature); constructor.setAccessible(true); 这两行首先找到clazz的构造方法,通过反射的方式将其设置为外部可调用的,然后下面final View view = constructor.newInstance(args); 这一行它通过构造函数反射创建了View,在这个方法中是真正进行了View的创建,当然这是在没有使用Factory的情况下哦。这个过程实际上它是使用了反射,反射是有可能导致程序变慢的一个因素,所以这里也可以作为我们的一个优化点。 ②、布局加载流程总结 布局文件解析:IO过程(文件过大时可能会导致卡顿) 创建View对象:反射(使用过多也会导致变慢) 在上面解读setContentView的源码时,我们知道创建View的过程优先是使用Factory2和Factory进行创建,下面对这两个类作简要说明: LayoutInflater.Factory: Factory与Factory2 我们来看一下它们的源码,首先来看Factory2: 可以看到Factory2是一个接口,并且它是继承自Factory的,来看一下Factory: 入参中有个name,来看一下它的注释,意思就是我们要加载的Tag,比如这个Tag是TextView,那么通过这个方法返回的就是TextView,实际上如果你继续跟踪的话,你会发现这个Tag实际上就是我们平时在布局中写的一个个的控件:比如TextView、ImageView等等,它会根据具体的Tag来进行对应View的创建: 并且我们对比两个接口,可以发现Factory2比Factory就是入参多了一个parent,这个parent就是你创建的View的parent,所以综上可得Factory2比Factory功能上更加强大。 随着项目的不断升级,项目体量逐渐变大,页面可能也变的越来越多,然后我们希望能够在线上进行统计,了解到具体哪些页面用户在进入时会出现卡顿,布局文件加载也可能会导致卡顿。 常规方式:覆写方法(setContentView)、手动埋点上报服务端(不够优雅,代码具有侵入性) AOP方式:切Activity的setContentView(切面点) @ Around(“execution(*android.app.Activity.setContentView(..))”) 具体实现: 结果如下: 对AOP忘记了的可以去看本专栏的第一篇启动优化部分有介绍:《Android启动优化你真的了解吗?》 思考:如何获取每一个控件加载耗时? 我们在上面使用setContentView获取到的是页面中所有控件的耗时情况,那现在我想要知道这个页面中各个控件的耗时分布情况,以便于整体的把控分析并且可以对耗时较多的控件做针对性的优化,这样一个场景该如何实现呢?由于每个页面布局中的控件都是不可控的,有可能多也有可能少,所以我们应该尽量做到低侵入性,这个问题大家可以好好想想,看看有什么解决方案。 解决方案:使用LayoutInflaterCompat.Factory2(LayoutInflaterCompat是LayoutInflater的兼容类)让它在创建View时进行Hook: 结果如下:可以看到我们确实获取到了列表Item中的每个控件的耗时情况 在上面我们已经说过了布局文件加载慢主要的原因是有以下两点: 针对上面说的这两种情况,相对应的解决套路也就是两种: 这里针对侧面缓解的方案来介绍一种实现方式:AsyncLayoutInflater,谷歌提供的一个类,简称异步Inflate 使用方式:首先导入asynclayoutinflater的依赖库,这里我们参考谷歌官方文档中androidx的使用: 然后来修改我们的MainActivity中的onCreate()方法: 然后运行我们的应用发现也是可以正常跑起来的: 有兴趣的可以去看一下AsyncLayoutInflater的源码,理解起来应该不难,这个类内部有一个Handler对象,一个InflateThread类继承于Thread,还有一个inflate方法,该方法有三个入参resid、parent、callback,同时将这三个参数封装成了InflateRequest的数据结构,然后加到线程的队列中,线程中同时有一个run()方法在不断执行,它会从队列中取出一条InflateRequest,然后这个request.inflate开始执行inflate()方法并返回request.view,这个方法是执行在子线程中的,最后通过Handler将它回调到主线程中,同时有一个相关联的Callback,在Callback中进行判断如果没有创建完成的话,会回退到主线程中进行布局的加载,最后将request.view回调到onInflateFinished()方法中,这样主线程就可以在该方法中拿到对应的view了。 总结:①、不能设置LayoutInflater.Factory(),需要自定义AsyncLayoutInflater解决;②、注意View中不能有依赖主线程的操作 上面这一部分是介绍了一种侧面缓解的方式,那这一部分我们来思考一下从根本上解决该如何实现? 首先来说一下思路哈,其实也没啥思路,就是利用Java代码写布局,这种方案的特点如下: 思路有了但是看着实现起来却不太现实哈,那咋办呢?咋办呢?咋办呢?嗯,这样拌,大神还是很多的,我们使用开源方案X2C: X2C框架介绍:保留XML优点,解决其性能问题 X2C框架的使用方式: ①、添加依赖:app/build.gradle中添加 ②、添加注解:在使用布局的任意java类或方法上面添加: ③、代码实战 将原有的setContentView注释掉,然后使用X2C.setContentView()来设置布局,运行之后发现是可以正常加载的,图中左侧圈出来的是使用X2C编译之后的产物,这个其实就是它的底层实现原理了,我们来看一下: 首先是布局文件: 然后是编译之后的代码: 可以看到它内部就是将我们布局文件中的控件全都以Java对象的形式给new出来了。 X2C存在的问题: ①、视图绘制流程 可能存在的性能问题: ②、布局层级及复杂度 编写布局的准则:减少View树层级 这里推荐使用:ConstraintLayout,网上关于它有很多的文章,后面我也准备专门写一篇它的使用总结 ③、过度绘制 避免过度绘制方法: ④、布局绘制的其它优化技巧 关于布局优化相关的知识点就总结这么多了,有些地方还是通过实际代码实践之后才能有所体会,好了,天也不早了,人也都走了,今天就先到这里吧,下期再会!
写在前面
一、Android绘制原理及工具选择
1.1、Android绘制原理
1.2、优化工具
二、Android布局加载原理
2.1、布局加载流程
setContentView(R.layout.activity_main);
@Override public void setContentView(@LayoutRes int layoutResID) { getDelegate().setContentView(layoutResID); }
@Override public void setContentView(int resId) { ensureSubDecor(); ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content); contentParent.removeAllViews(); LayoutInflater.from(mContext).inflate(resId, contentParent); mAppCompatWindowCallback.getWrapped().onContentChanged(); }
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) { return inflate(resource, root, root != null); }
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) + ")"); } View view = tryInflatePrecompiled(resource, res, root, attachToRoot); if (view != null) { return view; } XmlResourceParser parser = res.getLayout(resource); try { return inflate(parser, root, attachToRoot); } finally { parser.close(); } }
@NonNull public XmlResourceParser getLayout(@LayoutRes int id) throws NotFoundException { return loadXmlResourceParser(id, "layout"); }
@NonNull @UnsupportedAppUsage 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) { 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); } }
/** * 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 { ... //代码有点多就不贴了,不然文章会很长,大家有需要的自己对照这个过程读一下源码,敬请谅解 }
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) { synchronized (mConstructorArgs) { 。。。 if (TAG_MERGE.equals(name)) { if (root == null || !attachToRoot) { throw new InflateException("<merge /> 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 final View temp = createViewFromTag(root, name, inflaterContext, attrs); 。。。 } 。。。 return result; } }
@UnsupportedAppUsage private View createViewFromTag(View parent, String name, Context context, AttributeSet attrs) { return createViewFromTag(parent, name, context, attrs, false); }
@UnsupportedAppUsage View createViewFromTag(View parent, String name, Context context, AttributeSet attrs, boolean ignoreThemeAttr) { if (name.equals("view")) { name = attrs.getAttributeValue(null, "class"); } // Apply a theme wrapper, if allowed and one is specified. if (!ignoreThemeAttr) { final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME); final int themeResId = ta.getResourceId(0, 0); if (themeResId != 0) { context = new ContextThemeWrapper(context, themeResId); } ta.recycle(); } try { View view = tryCreateView(parent, name, context, attrs); if (view == null) { final Object lastContext = mConstructorArgs[0]; mConstructorArgs[0] = context; try { if (-1 == name.indexOf('.')) { view = onCreateView(context, parent, name, attrs); } else { view = createView(context, name, null, attrs); } } finally { mConstructorArgs[0] = lastContext; } } return view; } catch (InflateException e) { throw e; } catch (ClassNotFoundException e) { final InflateException ie = new InflateException( getParserStateDescription(context, attrs) + ": Error inflating class " + name, e); ie.setStackTrace(EMPTY_STACK_TRACE); throw ie; } catch (Exception e) { final InflateException ie = new InflateException( getParserStateDescription(context, attrs) + ": Error inflating class " + name, e); ie.setStackTrace(EMPTY_STACK_TRACE); throw ie; } }
@UnsupportedAppUsage(trackingBug = 122360734) @Nullable public final View tryCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) { if (name.equals(TAG_1995)) { // Let's party like it's 1995! return new BlinkLayout(context, attrs); } 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); } return view; }
@Nullable public final View createView(@NonNull Context viewContext, @NonNull String name, @Nullable String prefix, @Nullable AttributeSet attrs) throws ClassNotFoundException, InflateException { Objects.requireNonNull(viewContext); Objects.requireNonNull(name); Constructor<? extends View> constructor = sConstructorMap.get(name); if (constructor != null && !verifyClassLoader(constructor)) { constructor = null; sConstructorMap.remove(name); } Class<? extends View> clazz = null; 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 clazz = Class.forName(prefix != null ? (prefix + name) : name, false, mContext.getClassLoader()).asSubclass(View.class); if (mFilter != null && clazz != null) { boolean allowed = mFilter.onLoadClass(clazz); if (!allowed) { failNotAllowed(name, prefix, viewContext, attrs); } } constructor = clazz.getConstructor(mConstructorSignature); constructor.setAccessible(true); sConstructorMap.put(name, constructor); } else { // If we have a filter, apply it to cached constructor if (mFilter != null) { // Have we seen this name before? Boolean allowedState = mFilterMap.get(name); if (allowedState == null) { // New class -- remember whether it is allowed clazz = Class.forName(prefix != null ? (prefix + name) : name, false, mContext.getClassLoader()).asSubclass(View.class); boolean allowed = clazz != null && mFilter.onLoadClass(clazz); mFilterMap.put(name, allowed); if (!allowed) { failNotAllowed(name, prefix, viewContext, attrs); } } else if (allowedState.equals(Boolean.FALSE)) { failNotAllowed(name, prefix, viewContext, attrs); } } } Object lastContext = mConstructorArgs[0]; mConstructorArgs[0] = viewContext; Object[] args = mConstructorArgs; args[1] = attrs; try { 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])); } return view; } finally { mConstructorArgs[0] = lastContext; } } 。。。 }
2.2、性能瓶颈
2.3、LayoutInflater.Factory
public interface Factory2 extends Factory { @Nullable View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs); }
public interface Factory { /** * Hook you can supply that is called when inflating from a LayoutInflater. * You can use this to customize the tag names available in your XML * layout files. * * <p> * Note that it is good practice to prefix these custom names with your * package (i.e., com.coolcompany.apps) to avoid conflicts with system * names. * * @param name Tag name to be inflated. * @param context The context the view is being created in. * @param attrs Inflation attributes as specified in XML file. * * @return View Newly created view. Return null for the default * behavior. */ @Nullable View onCreateView(@NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs); }
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: // The fallback that allows extending class to take over view inflation // for other tags. Note that we don't check that the result is not-null. // That allows the custom inflater path to fall back on the default one // later in this method. view = createView(context, name, attrs); }
三、优雅获取界面布局耗时
@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(); } Log.i(name, " cost " + (System.currentTimeMillis() - time)); }
LayoutInflaterCompat.setFactory2(getLayoutInflater(), new LayoutInflater.Factory2() { @Override public View onCreateView(View parent, String name, Context context, AttributeSet attrs) { long time = System.currentTimeMillis(); View view = getDelegate().createView(parent, name, context, attrs); Log.i(name,"控件耗时:" + (System.currentTimeMillis() - time)); return view; } @Override public View onCreateView(String name, Context context, AttributeSet attrs) { return null; } });
四、异步Inflate实战
@Override protected void onCreate(Bundle savedInstanceState) { new AsyncLayoutInflater(this).inflate(R.layout.activity_main, null, new AsyncLayoutInflater.OnInflateFinishedListener() { @Override public void onInflateFinished(@NonNull View view, int resid, @Nullable ViewGroup parent) { setContentView(view); mRecycler = findViewById(R.id.mRecycler); mRecycler.setLayoutManager(new LinearLayoutManager(MainActivity.this)); mRecycler.addItemDecoration(new DividerItemDecoration(MainActivity.this, DividerItemDecoration.VERTICAL)); mRecycler.setAdapter(mAdapter); mAdapter.setOnFeedShowCallBack(MainActivity.this); } }); super.onCreate(savedInstanceState); // setContentView(R.layout.activity_main); mAdapter = new FeedAdapter(this, mList); initData(); // getFPS(); }
五、X2C框架使用
annotationProcessor 'com.zhangyue.we:x2c-apt:1.1.2' implementation 'com.zhangyue.we:x2c-lib:1.0.6'
@Xml(layouts = "activity_main")
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="https://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/mRecycler" android:layout_width="match_parent" android:layout_height="match_parent" /> </LinearLayout>
public class X2C0_activity_main implements IViewCreator { @Override public View createView(Context context) { return new com.zhangyue.we.x2c.layouts.X2C0_Activity_Main().createView(context); } }
public class X2C0_Activity_Main implements IViewCreator { @Override public View createView(Context ctx) { Resources res = ctx.getResources(); LinearLayout linearLayout0 = new LinearLayout(ctx); linearLayout0.setOrientation(LinearLayout.VERTICAL); RecyclerView recyclerView1 = new RecyclerView(ctx); LinearLayout.LayoutParams layoutParam1 = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT); recyclerView1.setId(R.id.mRecycler); recyclerView1.setLayoutParams(layoutParam1); linearLayout0.addView(recyclerView1); return linearLayout0; } }
六、视图绘制优化
本网页所有视频内容由 imoviebox边看边下-网页视频下载, iurlBox网页地址收藏管理器 下载并得到。
ImovieBox网页视频下载器 下载地址: ImovieBox网页视频下载器-最新版本下载
本文章由: imapbox邮箱云存储,邮箱网盘,ImageBox 图片批量下载器,网页图片批量下载专家,网页图片批量下载器,获取到文章图片,imoviebox网页视频批量下载器,下载视频内容,为您提供.
阅读和此文章类似的: 全球云计算