关于Android UI绘制优化你应该了解的知识点

·  阅读 858
关于Android UI绘制优化你应该了解的知识点

本文已收录到 GitHub/Android-Notes 中,这里有 Android 进阶成长知识体系笔记,有志同道合的朋友,关注公众号 [小尘Android专栏] 跟我一起成长。

一、Android绘制原理及工具选择

1.1、Android绘制原理

对于Android手机来说,它的画面渲染依赖于两个硬件:1.CPU;2.GPU:

  • CPU负责计算显示内容,比如:视图创建、布局计算、图片解码、文本绘制等
  • GPU负责栅格化(UI元素绘制到屏幕上),栅格化:将一些组件比如Button、Bitmap拆分成不同的像素进行显示然后完成绘制,这个操作相对比较耗时,所以引入GPU来加快栅格化操作
  • 16ms发出VSync信号触发UI渲染,意思就是Android系统要求每一帧都要在16ms内完成,具体到项目中就是不管业务代码或者其他逻辑代码有多复杂,想要保证每一帧都很平滑,渲染代码就应该在16ms内完成
  • 大多数的Android设备屏幕刷新频率:60Hz ,60帧/秒是人眼和大脑之间协作的极限

1.2、优化工具

1.Systrace

  • 关注Frames
  • 正常:绿色圆点,丢帧:黄色或红色
  • Alerts:Systrace中自动分析并且标注异常性能的条目

image.png

上面这张图是我找的一个使用Systrace生成的.html文件,图中每一个F的出现就表明出现了一帧,可以看到这两个F之间的时间间隔比16ms多了不少,Alert type这里面就是Systrace自动给出的一些提示信息,我们可以根据提示信息来查找修改的方向。

②、Layout Inspector

菜单栏——>Tools——>Layout Inspector

  • Android Studio自带的工具
  • 查看试图层次结构

③、Choreographer

获取FPS,线上使用,具备实时性

  • Api 16之后
  • 使用方式是:Choreographer.getInstance().postFrameCallback

这里写了一个方法getFPS()来获取这个APP的FPS情况,方法内部一开始是做了一个保护性操作,确保使用的Choreographer发生在API16之后,然后在doFrame回调中首先判断是不是统计周期的第一次,如果是就记录第一次回调的时间,接下来就是判断时间间隔是否超过预设的阀值160ms,如果超过则计算FPS,计算方式是间隔时间除以间隔时间内发生的次数,如果没有超过则直接将次数加1。

image.png

输出的结果可以看到基本上都是59和60之间的数值。

二、Android布局加载原理

2.1、布局加载流程

1.源码解析

这一部分我们来看下源码,因为内容比较多,我就尽可能的简单说,对于源码阅读的流程我们之前已经说过几次了,这里就不再介绍了,基本上就是找到你需要的入口方法,然后一路跟踪下去,把整个流程串起来,不需要你把每一行的代码都读懂。

既然说的是布局加载,那么我们首先肯定是找入口方法,这个方法你回想一下每个页面加载布局都是调用的什么方法呢?很简单啦:

setContentView(R.layout.activity_main);

然后点击这个方法进入源码中去就到了AppCompatActivity类的setContentView()方法中:

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

继续跟踪点击setContentView()方法:

image.png

发现这是一个抽象方法,此时你需要去找它的实现类AppCompatDelegateImpl中的方法了,点击左侧向下的fx向下箭头:

    @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();
    }

这个方法中由于传递进来的resId也就是布局文件的id,它只在LayoutInflater这一行用到了,所以接着跟踪这一行,点击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) + ")");
        }
 
        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();
        }
    }

这里面又有一个inflate()方法,入参有一个parser,看了看上下的代码,知道了它其实是XmlResourceParser的实例,那我们先不去看这个inflate()方法具体的实现,先来看下这个parser究竟是什么?找到res.getLayout()方法,里面传入了我们的资源id,返回的是XmlResourceParser,看名字XML资源解析器,就知道这玩意应该很屌,来吧,继续点击getLayout():

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

没啥实质性的内容,继续点击它的实现方法loadXmlResourceParser():

    @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);
        }
    }

这个方法开始是一些对象的声明,后面是异常的处理,所以看下来真正有用的就是if判断里面的,它判断了value.type如果是String类型的,然后继续调用了impl的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 {
        ...
        //代码有点多就不贴了,不然文章会很长,大家有需要的自己对照这个过程读一下源码,敬请谅解
    }

主要看注释那里的说明哈,Android中的布局都是写在XML文件中的,这个方法就是为我们具体所写的布局文件准备一个XML的解析器,所以它实际上就是一个XML的Pull解析的过程。需要注意的是:android的布局实际上是一个XML文件,它在加载的时候会首先将它读取到内存中,这个过程实际上就是一个IO过程,一般在android开发中操作IO都会将其置于工作线程中,所以这里可能会成为我们优化的一个方向。

关于这个XmlResourceParser就说到这里,下面继续回到上面说的那个inflate()方法中:

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;
        }
    }

这里同样的省略了部分代码,我们知道日常开发中经常会碰到一些报错,其实这些报错在Android的源码中都是有所体现的,比如这里定义的关于merge标签的一个异常信息。接着看createViewFromTag()这个方法,看名字我们应该能大致猜测出来它是干嘛的了,它应该就是通过一系列的Tag来创建相对应的View,我们点击该方法跟进:

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

这里面又调用了另一个createViewFromTag()方法,继续跟进:

    @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;
        }
    }

这里就到了重点的地方了,这里面就是创建View的过程了:

首先:View view = tryCreateView(parent, name, context, attrs); 它通过这个tryCreateView()方法构建出View对象,进到这个方法中:

    @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;
    }

这个方法里面就是判断了几个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()继续跟踪:

@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;
            }
        } 
        。。。
    }

这个方法里面constructor = clazz.getConstructor(mConstructorSignature); constructor.setAccessible(true); 这两行首先找到clazz的构造方法,通过反射的方式将其设置为外部可调用的,然后下面final View view = constructor.newInstance(args); 这一行它通过构造函数反射创建了View,在这个方法中是真正进行了View的创建,当然这是在没有使用Factory的情况下哦。这个过程实际上它是使用了反射,反射是有可能导致程序变慢的一个因素,所以这里也可以作为我们的一个优化点。

2.布局加载流程总结

image.png

2.2、性能瓶颈

  • 布局文件解析:IO过程(文件过大时可能会导致卡顿)
  • 创建View对象:反射(使用过多也会导致变慢)

2.3、LayoutInflater.Factory

在上面解读setContentView的源码时,我们知道创建View的过程优先是使用Factory2和Factory进行创建,下面对这两个类作简要说明:

LayoutInflater.Factory:

  • LayoutInflater创建View的一个Hook,Hook其实就是我们可以将自己的代码挂在它的原始代码之上,可以对它的流程进行更改
  • 定制创建View的过程:比如全局替换自定义TextView等

Factory与Factory2

  • Factory2继承于Factory
  • 多了一个参数:parent

我们来看一下它们的源码,首先来看Factory2:

    public interface Factory2 extends Factory {
        @Nullable
        View onCreateView(@Nullable View parent, @NonNull String name,
                @NonNull Context context, @NonNull AttributeSet attrs);
    }

可以看到Factory2是一个接口,并且它是继承自Factory的,来看一下Factory:

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);
    }

入参中有个name,来看一下它的注释,意思就是我们要加载的Tag,比如这个Tag是TextView,那么通过这个方法返回的就是TextView,实际上如果你继续跟踪的话,你会发现这个Tag实际上就是我们平时在布局中写的一个个的控件:比如TextView、ImageView等等,它会根据具体的Tag来进行对应View的创建:

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);
        }

并且我们对比两个接口,可以发现Factory2比Factory就是入参多了一个parent,这个parent就是你创建的View的parent,所以综上可得Factory2比Factory功能上更加强大。

三、优雅获取界面布局耗时

随着项目的不断升级,项目体量逐渐变大,页面可能也变的越来越多,然后我们希望能够在线上进行统计,了解到具体哪些页面用户在进入时会出现卡顿,布局文件加载也可能会导致卡顿。

常规方式:覆写方法(setContentView)、手动埋点上报服务端(不够优雅,代码具有侵入性)

AOP方式:切Activity的setContentView(切面点)

@ Around("execution(*android.app.Activity.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();
        }
        Log.i(name, " cost " + (System.currentTimeMillis() - time));
    }

结果如下:

image.png

思考:如何获取每一个控件加载耗时?

我们在上面使用setContentView获取到的是页面中所有控件的耗时情况,那现在我想要知道这个页面中各个控件的耗时分布情况,以便于整体的把控分析并且可以对耗时较多的控件做针对性的优化,这样一个场景该如何实现呢?由于每个页面布局中的控件都是不可控的,有可能多也有可能少,所以我们应该尽量做到低侵入性,这个问题大家可以好好想想,看看有什么解决方案。

解决方案:使用LayoutInflaterCompat.Factory2(LayoutInflaterCompat是LayoutInflater的兼容类)让它在创建View时进行Hook:

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;
            }
        });

结果如下:可以看到我们确实获取到了列表Item中的每个控件的耗时情况

image.png

四、异步Inflate实战

在上面我们已经说过了布局文件加载慢主要的原因是有以下两点:

  • 布局文件读取慢:IO过程
  • 创建View慢:通过反射创建一个对象比直接new一个对象要慢3倍,布局嵌套层级复杂则反射更多

针对上面说的这两种情况,相对应的解决套路也就是两种:

  • 根本性解决:去掉IO过程、不使用反射
  • 侧面缓解:让主线程不耗时,不影响主线程

这里针对侧面缓解的方案来介绍一种实现方式:AsyncLayoutInflater,谷歌提供的一个类,简称异步Inflate

  • WorkThread加载布局,原生是在UI Thread加载布局
  • 加载完成之后回调主线程,此时主线程拿到的是创建完成的View对象可以直接使用
  • 节约主线程时间,因为耗时是发生在了异步线程中,主线程的响应能够得到保障

使用方式:首先导入asynclayoutinflater的依赖库,这里我们参考谷歌官方文档中androidx的使用:

image.png 然后来修改我们的MainActivity中的onCreate()方法:

    @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();
    }

有兴趣的可以去看一下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中不能有依赖主线程的操作

五、X2C框架使用

上面这一部分是介绍了一种侧面缓解的方式,那这一部分我们来思考一下从根本上解决该如何实现?

首先来说一下思路哈,其实也没啥思路,就是利用Java代码写布局,这种方案的特点如下:

  • 本质上解决了性能问题(没有xml文件也就没有了IO的过程,直接new对象没有了反射的过程)
  • 引入新问题:不便于开发、可维护性差

思路有了但是看着实现起来却不太现实哈,那咋办呢?咋办呢?咋办呢?嗯,这样拌,大神还是很多的,我们使用开源方案X2C:

X2C框架介绍:保留XML优点,解决其性能问题

  • 开发人员写XML,加载Java代码
  • 原理:APT编译期翻译XML为Java代码

X2C框架的使用方式:

  1. 添加依赖:app/build.gradle中添加
annotationProcessor 'com.zhangyue.we:x2c-apt:1.1.2'
implementation 'com.zhangyue.we:x2c-lib:1.0.6'
  1. 添加注解:在使用布局的任意java类或方法上面添加:
@Xml(layouts = "activity_main")
  1. 代码实战

image.png 将原有的setContentView注释掉,然后使用X2C.setContentView()来设置布局,运行之后发现是可以正常加载的,图中左侧圈出来的是使用X2C编译之后的产物,这个其实就是它的底层实现原理了,我们来看一下:

首先是布局文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://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 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;
  }
}

可以看到它内部就是将我们布局文件中的控件全都以Java对象的形式给new出来了。

X2C存在的问题:

  • XML中有的部分属性Java不支持(虽然不多但是也有)
  • 失去了系统的兼容(AppCompat,如果你需要使用AppCompatXXX下面的控件可以通过修改X2C源码来定制化实现相关功能)

六、视图绘制优化

1.视图绘制流程

  • 测量:确定大小(自顶向下进行视图树的遍历,确定ViewGroup和View应该有多大)
  • 布局:确定位置(执行另一个自顶向下的遍历操作,ViewGroup会根据测量阶段测定的大小确定自己应该摆放的位置)
  • 绘制:绘制视图(对于视图树中的每个对象系统都会为它创建一个Canvas对象,然后向GPU发送一条绘制命令进行绘制)

可能存在的性能问题:

  • 每个阶段耗时
  • 自顶而下的遍历(如果Layout层级比较深则遍历也是很耗时的)
  • 触发多次(比如嵌套使用RelativeLayout有可能会导致绘制环节触发多次)

2.布局层级及复杂度

编写布局的准则:减少View树层级

  • 不嵌套使用RelativeLayout
  • 不在嵌套的LinearLayout中使用weight
  • merge标签:减少一个层级,只能用于根View

这里推荐使用:ConstraintLayout,网上关于它有很多的文章,后面我也准备专门写一篇它的使用总结

  • 实现几乎完全扁平化布局
  • 构建复杂布局性能更高
  • 具有RelativeLayout和LinearLayout特性

3.过度绘制

  • 一个像素最好只被绘制一次
  • 调试GPU过度绘制
  • 蓝色可接受

避免过度绘制方法:

  • 去掉多余背景色,减少复杂shape使用
  • 避免层级叠加
  • 自定义View使用clipRect屏蔽被遮盖View绘制(当覆写onDraw()之后,系统就无法知道View中各个元素的位置和层级关系,就无法做自动优化,即无法自动忽略绘制那些不可见的元素)

4.布局绘制的其它优化技巧

  • ViewStub:高效占位符、延迟初始化(这个标签没有大小,也没有绘制功能不参与measure和layout过程,资源消耗非常低,一般用于延迟初始化)
  • onDraw中避免:创建大对象、耗时操作
  • TextView相关优化(setText显示静态文本)

如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙: 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。微信搜索公众号 [小尘Android专栏] ,第一时间阅读更多干货知识!

收藏成功!
已添加到「」, 点击更改