深入布局优化

前言

我们在使用App的过程中 经常会遇到一些跳转页面显示比较慢的情况 今天就深入分析一下布局优化 提高我们的布局加载速度

Activity加载布局过程

我们跟踪一下setContentView过程 看看系统setContentView做了哪些事情

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

我们发现会将setContentView交给代理类来实现
我们接着看一下代理类实现

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

交给LayoutInflater实现

 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;
        }
        //生成xml解析器 
        XmlResourceParser parser = res.getLayout(resource);
        try {
                //continue
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }

上面我们看到会生成一个xml解析器 解析我们的layout xml文件 这边有一个IO过程 是一个隐藏的优化点

然后会调用createViewFromTag->tryCreateView

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;
        //先调用Factory2 这边是一个Hook点 这里AppCompatActivity 和Activity是不一样的 AppCompatActivity会自己创建一个Factory2对象
        if (mFactory2 != null) {
            view = mFactory2.onCreateView(parent, name, context, attrs);
        //调用Factory 这边是一个Hook点 
        } else if (mFactory != null) {
            view = mFactory.onCreateView(name, context, attrs);
        } else {
            view = null;
        }
          //调用mPrivateFactory.onCreateView
        if (view == null && mPrivateFactory != null) {
            view = mPrivateFactory.onCreateView(parent, name, context, attrs);
        }

        return view;
    }

这边会先调用tryCreateView,查看是否存在Factory2,这边也是我们可以hook点 我们可以拦截系统生成View的过程

如果没有Hook点 将会调用系统createView

 @Nullable
    public final View createView(@NonNull Context viewContext, @NonNull String name,
            @Nullable String prefix, @Nullable AttributeSet attrs)
            throws ClassNotFoundException, InflateException {
       
       
          ......
       
            try {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);
              //缓存一些构造器 放在sConstructorMap中 加快创建速度
            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 {
                    //反射生成View
                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;
            }
            
            ...... 
             
       }

省略了一些代码 我们可以看到 这边是通过反射的方式 生成View对象 这边也是一个优化的点

你以为这就结束了??? 不不不 如果我们使用的是AppCompatActivityAppCompatActivity会创建一个Factory2来代理创建view过程 那么在tryCreateView方法中 会调用Factory2createView方法

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;

        // We can emulate Lollipop's android:theme attribute propagating down the view hierarchy
        // by using the parent's context
        if (inheritContext && parent != null) {
            context = parent.getContext();
        }
        if (readAndroidTheme || readAppTheme) {
            // We then apply the theme on the context, if specified
            context = themifyContext(context, attrs, readAndroidTheme, readAppTheme);
        }
        if (wrapContext) {
            context = TintContextWrapper.wrap(context);
        }

        View view = null;

        // We need to 'inject' our tint aware Views in place of the standard framework versions
        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);
        }

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

我们可以看到AppCompatActivitynew一些AppCompatTextView包装类 来兼容低版本 而且这边使用了new的方式 而不是反射 也算是一点优化

优化工具

分析完View的生成过程 我们来看一下有哪些工具 可以帮助我们分析定位一些布局过程中异常情况

1.Systrace

使用Systrace 我们可以通过frame 直观查看加载耗时 是否掉帧等情况
通过命令

python systrace.py -t 10 -o ~/Downloads/mytrace.html -a com.dsg.androidperformance gfx view wm am res sync

可以生成html文件 我们打开主要查看frame帧 绿色表示没有问题 主要查看黄色和红色的帧
关于Systrace可以查看之前的启动优化文章

Systrace.png

我们可以看到 一帧的耗时是8ms左右 因为我的是120HZ的手机 如果60HZ的手机 一帧就是16ms 我们还可以查看Alert来帮助我们排查问题

2.Layout Inspector

Layout Inspector 是AS提供的工具 可以很方便的查看视图层次结构 我们可以查看当前页面的布局 优化布局层次 也可以打开开发者模式的布局层次 查看我们时候存在不合理的过度绘制

3. Choregrapher

Choregrapher是在Api16以上提供的一个类 负责管理每一帧的绘制,动画以及vsync信号的同步
可以参考另一篇《Choregrapher源码解析》

我们可以在每一帧插入我们自己的代码 统计我们的Fps 这种方法不怎么消耗性能 我们可以带到线上监控

@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
private void getFPS() {
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
        return;
    }
    Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
        @Override
        public void doFrame(long frameTimeNanos) {
            if (mStartFrameTime == 0) {
                mStartFrameTime = frameTimeNanos;
            }
            long interval = frameTimeNanos - mStartFrameTime;
            if (interval > MONITOR_INTERVAL_NANOS) {
                double fps = (((double) (mFrameCount * 1000L * 1000L)) / interval) * MAX_INTERVAL;
                mFrameCount = 0;
                mStartFrameTime = 0;
            } else {
                ++mFrameCount;
            }
            Choreographer.getInstance().postFrameCallback(this);
        }
    });
}
                

监控View创建时间

1.AOP方式

我们这里还是以AspectJ为参考 直接看关键代码

//Around表示前后都要加上代码
//execution表示切入代码内部实现 而不是方法调用前后
 @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();
        }
        LogUtils.i(name + " costAAA: " + (System.currentTimeMillis() - time));
    }

我们就可以查看setContentView实现 具体AspectJ的其他配置实现请查看AspectJ官网

2.Hook系统创建View

我们在之前分析setContentView的过程中 留意过 系统提供了一个hook点 Factory2 我们可以代理系统生成View的过程 直接看关键代码

LayoutInflaterCompat.setFactory2(getLayoutInflater(), new LayoutInflater.Factory2() {
            @Nullable
            @Override
            public View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
                long startTime = System.currentTimeMillis();
                //使用系统创建view的方法 我们只是想监控创建view耗时 并不需要代理创建view
                View view = getDelegate().createView(parent, name, context, attrs);
                Log.d("onCreateView", "createView  " + name + " costTime :" + (System.currentTimeMillis() - startTime));
                return view;
            }

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

必须要在super.onCreate()之前调用 因为super会创建自己的Factory2对象 如果在之后调用 会报一个只能有一个Factory2的错误
这样我们就可以查看每个View创建的耗时

小总结

我们分析了Activity的setContentView 的过程 我们知道setContentView的主要两个步骤:

  1. 加载解析xml文件
  2. 反射生成View对象

这也是我们主要优化的方向 接下来我们就分别对着两个方向做一些优化

减少IO速度

IO耗时 我们可以将IO放入子线程 就可以减少IO的耗时
Android 提供了一个异步布局类AsyncLayoutInflater
看一下使用

 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);
                mRecyclerView = findViewById(R.id.recycler_view);
                mRecyclerView.setLayoutManager(new LinearLayoutManager(MainActivity.this, LinearLayoutManager.VERTICAL, false));
                mRecyclerView.setAdapter(mNewsAdapter);
                mNewsAdapter.setOnFeedShowCallBack(MainActivity.this);

            }
        });

使用AsyncLayoutInflater我们会将加载布局的过程 放入一个子线程中 简单看一下源码

    @UiThread
    public void inflate(@LayoutRes int resid, @Nullable ViewGroup parent,
            @NonNull OnInflateFinishedListener callback) {
        if (callback == null) {
            throw new NullPointerException("callback argument may not be null!");
        }
        InflateRequest request = mInflateThread.obtainRequest();
        request.inflater = this;
        request.resid = resid;
        request.parent = parent;
        request.callback = callback;
        mInflateThread.enqueue(request);
    } 

会将布局请求包装成一个InflateRequest对象 然后加入一个队列mInflateThread
InflateThread继承Thread 会一直运行runInner方法

public void runInner() {
            InflateRequest request;
            try {
                    从队列中取出我们包装的请求
                request = mQueue.take();
            } catch (InterruptedException ex) {
                // Odd, just continue
                Log.w(TAG, ex);
                return;
            }

            try {
               //子线程执行生成View
                request.view = request.inflater.mInflater.inflate(
                        request.resid, request.parent, false);
            } catch (RuntimeException ex) {
                // Probably a Looper failure, retry on the UI thread
                Log.w(TAG, "Failed to inflate resource in the background! Retrying on the UI"
                        + " thread", ex);
            }
            //发送生成的View
            Message.obtain(request.inflater.mHandler, 0, request)
                    .sendToTarget();
        }

这样就将创建View的过程放在子线程执行 完成之后回调给主线程 加快view的构建

减少反射

使用X2C框架 X2C框架会在项目编译期间 将xml文件解析 然后通过new的方式来生成view对象
X2C官网

使用方法:
1.使用@XML注解声明xml文件
2.使用X2C.setContentView

使用起来非常简单 而且开发的时候还是可以写xml文件 编译期间自动生成new代码 但是X2C框架还是有一些弊端

  1. 不支持Merge标签
  2. 不支持系统Style 只能使用应用Style

X2C源码解析

X2C源码核心就是通过@XML注解获取Layout文件
然后解析Xml文件生成java文件(通过new的方式生成view对象)

我们来看一下生成代码之后的例子
主要生成了两个类

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

        ConstraintLayout constraintLayout0 = new ConstraintLayout(ctx);

        RecyclerView recyclerView1 = new RecyclerView(ctx);
        ConstraintLayout.LayoutParams layoutParam1 = new ConstraintLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT);
        recyclerView1.setId(R.id.recycler_view);
        recyclerView1.setLayoutParams(layoutParam1);
        constraintLayout0.addView(recyclerView1);

        return constraintLayout0;
  }
}

我们可以看到 已经解析xml为java代码了

我们再看一下X2C.setContentView方法

 public static void setContentView(Activity activity, int layoutId) {
        if (activity == null) {
            throw new IllegalArgumentException("Activity must not be null");
        }
        //这里会调用apt生成的代码 根据layoutId生成view
        View view = getView(activity, layoutId);
        if (view != null) {
            activity.setContentView(view);
        } else {
        //如果没有找到layoutId生成的view 就还是交给系统的setContentView方法
            activity.setContentView(layoutId);
        }
    }

看一下核心getView方法

public static View getView(Context context, int layoutId) {
        IViewCreator creator = sSparseArray.get(layoutId);
        if (creator == null) {
            try {
                    //右移24位 所以我们可以看到生成的文件名都有一个数字
                int group = generateGroupId(layoutId);
                String layoutName = context.getResources().getResourceName(layoutId);
                layoutName = layoutName.substring(layoutName.lastIndexOf("/") + 1);
                String clzName = "com.zhangyue.we.x2c.X2C" + group + "_" + layoutName;
                //获取apt生成的X2C0_activity_main类
                creator = (IViewCreator) context.getClassLoader().loadClass(clzName).newInstance();
            } catch (Exception e) {
                e.printStackTrace();
            }

            //如果creator为空,放一个默认进去,防止每次都调用反射方法耗时
            if (creator == null) {
                creator = new DefaultCreator();
            }
            sSparseArray.put(layoutId, creator);
        }
        return creator.createView(context);
    }

总结

我们分析了AppCompatActivity 的setContentView过程 知道了布局过程中 主要耗时的点就在于IO 和反射耗时 所以我们可以针对这两个点去做优化
布局优化可以提高我们的View创建速度 提高我们的使用体验