Part 4 App布局优化化

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zsjlovesm521/article/details/88549988

Part 4 App布局优化化

一 Android绘制原理及工具选择

1、绘制原理

硬件

CPU负责计算显示内容(视图的创建,布局计算,图片解码,文本绘制等)
GPU负责棚格化(UI元素绘制到屏幕上,也就是将一些组件,如button,bitmap拆分成不同的像素进行显示,然后完成绘制,比较耗时)

原则

16ms发出VSync信号触发UI渲染
大多数的Android设备屏幕刷新帧率:60Hz

2、优化工具

Systrace

关注Frames
正常:绿色圆点
丢帧:黄色或红色
Alerts栏(自动分析标注异常问题的条目)

参考文档
https://www.jianshu.com/p/f83d84dcd0b8 (Android Systrace使用介绍)

Layout Inspector
在这里插入图片描述

Choreographer

获取fps,线上使用,具备实时性
Api 16之后才可以使用
Choreographer.getInstance().postFrameCallback();

    private long mStartFrameTime = 0;
    private int mFrameCount = 0;
    private static final long MONITOR_INTERVAL = 160L; //单次计算FPS使用160毫秒
    private static final long MONITOR_INTERVAL_NANOS = MONITOR_INTERVAL * 1000L * 1000L;
    private static final long MAX_INTERVAL = 1000L; //设置计算fps的单位时间间隔1000ms,即fps/s;

 @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;
                    Log.e("fps", "fps = " + fps);
                    mFrameCount = 0;
                    mStartFrameTime = 0;
                } else {
                    ++mFrameCount;
                }

                Choreographer.getInstance().postFrameCallback(this);
            }
        });
    }

二 Android布局加载原理

1、布局加载源码追踪

布局加载流程
在这里插入图片描述

/**
     * Low-level function for instantiating a view by name. This attempts to
     * instantiate a view class of the given <var>name</var> found in this
     * LayoutInflater's ClassLoader.
     * 
     * <p>
     * There are two things that can happen in an error case: either the
     * exception describing the error will be thrown, or a null will be
     * returned. You must deal with both possibilities -- the former will happen
     * the first time createView() is called for a class of a particular name,
     * the latter every time there-after for that class name.
     * 
     * @param name The full name of the class to be instantiated.
     * @param attrs The XML attributes supplied for this instance.
     * 
     * @return View The newly instantiated view, or null.
     */
    public final View createView(String name, String prefix, AttributeSet attrs)
            throws ClassNotFoundException, InflateException {
        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 = mContext.getClassLoader().loadClass(
                        prefix != null ? (prefix + name) : name).asSubclass(View.class);
                
                if (mFilter != null && clazz != null) {
                    boolean allowed = mFilter.onLoadClass(clazz);
                    if (!allowed) {
                        failNotAllowed(name, prefix, 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 = mContext.getClassLoader().loadClass(
                                prefix != null ? (prefix + name) : name).asSubclass(View.class);
                        
                        boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
                        mFilterMap.put(name, allowed);
                        if (!allowed) {
                            failNotAllowed(name, prefix, attrs);
                        }
                    } else if (allowedState.equals(Boolean.FALSE)) {
                        failNotAllowed(name, prefix, attrs);
                    }
                }
            }

            Object[] args = mConstructorArgs;
            args[1] = attrs;

            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;

        } catch (NoSuchMethodException e) {
            final InflateException ie = new InflateException(attrs.getPositionDescription()
                    + ": Error inflating class " + (prefix != null ? (prefix + name) : name), e);
            ie.setStackTrace(EMPTY_STACK_TRACE);
            throw ie;

        } catch (ClassCastException e) {
            // If loaded class is not a View subclass
            final InflateException ie = new InflateException(attrs.getPositionDescription()
                    + ": Class is not a View " + (prefix != null ? (prefix + name) : name), e);
            ie.setStackTrace(EMPTY_STACK_TRACE);
            throw ie;
        } catch (ClassNotFoundException e) {
            // If loadClass fails, we should propagate the exception.
            throw e;
        } catch (Exception e) {
            final InflateException ie = new InflateException(
                    attrs.getPositionDescription() + ": Error inflating class "
                            + (clazz == null ? "<unknown>" : clazz.getName()), e);
            ie.setStackTrace(EMPTY_STACK_TRACE);
            throw ie;
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }

性能瓶颈

布局文件的解析:是通过XmlResourceParser进行的,属于IO操作
创建view对象:使用了反射

LayoutInflater.Factory

LayoutInflater创建view的一个Hook(挂钩)
定制创建view的过程:例如全局替换自定义Textview等

2、LayoutInflater.Factory介绍

LayoutInflater包含Factory和Factory2两个
Factory和Factory2:Factory2继承自Factory,并且多了一个参数parent

四 优雅获取界面布局耗时

1、常规方式

背景:获取每个界面加载耗时
实现:覆写方法、手动埋点

2、AOP/ArtHook实现

Aop实现:

//切面点: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();
        }
        LogUtils.i(name + " cost " + (System.currentTimeMillis() - time));
    }

ArtHook实现:
参考 part3中第六部分的ARTHook优雅检测不合理图片的实现方式。

3、获取任一控件的耗时

低侵入性、使用LayoutInflater.Factory

        //带有Compat表示的是兼容类,一般都有比较好的兼容性
        LayoutInflaterCompat.setFactory2(getLayoutInflater(), new LayoutInflater.Factory2() {
            @Override
            public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
                //替换示例:将布局中的某一个控件替换成我们自定义的控件(如Textview)伪代码如下
                if (TextUtils.equals(name, "TextView")) {
                    // 生成自定义TextView,然后将Textview return回去
                }
                
                //每个控件的耗时    可以将此方法放入base类onCreate中  必须在super.onCreate(savedInstanceState)之前设置才有效
                long time = System.currentTimeMillis();
                View view = getDelegate().createView(parent, name, context, attrs);
                LogUtils.i(name + " cost " + (System.currentTimeMillis() - time));
                return view;
            }

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

在这里插入图片描述

五 异步Inflate实战

1、背景

布局文件读取慢:是一个IO过程
创建View过程慢:反射(比new慢3倍)

思路介绍

根本性解决:不用反射,不用IO
侧面缓解:主线程不耗时,使用异步的方式

2、AsyncLayoutInflater实战

google提供的类(简称异步Inflate)

在WorkThread加载布局(原生的是在MainThread加载)
加载完成回调至主线程
节约主线程的时间

AsyncLayoutInflater的使用

implementation ‘com.android.support:asynclayoutinflater:28.0.0-alpha1’

        new AsyncLayoutInflater(this).inflate(layoutId, null, new AsyncLayoutInflater.OnInflateFinishedListener() {
            @Override
            public void onInflateFinished(@NonNull View view, int resid, @Nullable ViewGroup parent) {
                setContentView(view);
                overridePendingTransition(R.anim.enter_default_transition_animation, 0);
                initData();
                mPresenter = createPresenter();
                initView(savedInstanceState);
            }
        });

总结:

侧面缓解卡顿
使用了AsyncLayoutInflater就失去了向下兼容的特性,不能设置 LayoutInflater.Factory(自定义解决)
注意view中不能有依赖主线程的操作

参考文章:
https://www.jianshu.com/p/f0c0eda06ae4 (Android AsyncLayoutInflater 限制及改进)
https://blog.csdn.net/theone10211024/article/details/52781079 (AsyncLayoutInflater的简单介绍和源码分析)

六 布局加载优化实战

1、背景介绍

问题
IO操作、反射
AsyncLayoutInflater只是缓解

办法
java代码写布局:本质上解决了xml上的性能问题,但是不便于开发、可维护性差

2、X2C

X2C介绍:
保留xml优点,解决性能问题(开发人员写xml,加载java代码),原理:通过APT编译期翻译xml为java代码

x2c使用:

依赖:
annotationProcessor 'com.zhangyue.we:x2c-apt:1.1.2'
implementation 'com.zhangyue.we:x2c-lib:1.0.6'
    
使用方式:    
@Xml(layouts = "activity_main")//添加Xml注解标明使用布局
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        setTheme(R.style.AppTheme);
        super.onCreate(savedInstanceState);
//        setContentView(R.layout.activity_main);
        //使用X2C.setContentView,传入上下文和布局文件
        X2C.setContentView(MainActivity.this, R.layout.activity_main);
    }
}

x2c问题:
部分属性Java不支持
失去了系统的兼容(AppCompat)如:TextView、ImageView系统在高版本向低版本有兼容。解决办法:修改x2c源码,如发现是TextView直接new AppCompatTextView。

参考文档:https://github.com/iReaderAndroid/X2C (github项目源码)

七 视图绘制优化实战

1、优化布局层级及复杂度

布局绘制回顾:
测量:确定大小(遍历视图树,确认viewgroup和view元素的大小)
布局:确定位置(遍历视图树,每个viewgroup根据测量阶段的大小确认自己的位置)
绘制:绘制视图(视图树中的每个对象都会创建一个canvas对象,向GPU发送绘制命令)

性能瓶颈:
每个阶段耗时
自顶向下的遍历
触发多次(如嵌套RelativeLayout)

减少布局层级和复杂度:
准则:减少view树层级、宽而浅,避免窄而深
ConstraintLayout:实现几乎完全扁平化布局、构建复杂布局性能更高、具有RelativeLayout和LinearLayout特性
其他:不嵌套使用elativeLayout、不在嵌套LinearLayout中使用weight、merge标签可减少一个层级(只能用于根view)

2、避免过度绘制

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

方法:
去掉多余背景色,减少复杂shape使用
避免层级叠加(控件不要重叠)
自定义view使用clipRect屏蔽被遮盖View绘制

    //自定义view的onDraw优化方法
    /**
     * Custom implementation to do drawing in this view. Waits for the AsyncTasks to fetch
     * bitmaps for each Droid and populate mDroidCards, a list of DroidCard objects. Then, draws
     * overlapping droid cards.
     */
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        // Don't draw anything until all the Asynctasks are done and all the DroidCards are ready.
        if (mDroids.length > 0 && mDroidCards.size() == mDroids.length) {
            // Loop over all the droids, except the last one.
            int i;
            for (i = 0; i < mDroidCards.size() - 1; i++) {

                mCardLeft = i * mCardSpacing;
                canvas.save();
                // 指定绘制区域
                
                canvas.clipRect(mCardLeft,0,mCardLeft+mCardSpacing,mDroidCards.get(i).getHeight());

                // Draw the card. Only the parts of the card that lie within the bounds defined by
                // the clipRect() get drawn.
                drawDroidCard(canvas, mDroidCards.get(i), mCardLeft, 0);

                canvas.restore();
            }

            // Draw the final card. This one doesn't get clipped.
            drawDroidCard(canvas, mDroidCards.get(mDroidCards.size() - 1),
                    mCardLeft + mCardSpacing, 0);
        }

        // Invalidate the whole view. Doing this calls onDraw() if the view is visible.
        invalidate();
    }

在这里插入图片描述

3、其他

ViewStub:高效占位符、延迟初始化(没有测量和布局的过程)
onDraw中避免:创建大量对象、耗时操作
TextView优化:参考文档:https://blog.csdn.net/self_study/article/details/42297573 (android textview settext卡顿深层次原因)

猜你喜欢

转载自blog.csdn.net/zsjlovesm521/article/details/88549988