Android性能分析与优化学习(四)布局优化

一、绘制原理

cpu负责计算显示内容
GPU负责棚格化(UI元素绘制到屏幕上)

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

二、优化工具

(1)Systrace
a) 关注Frames
b) 正常:绿色原点,丢帧:黄色或红色
c) Alerts栏

(2)Layout Inspector
a)AndroidStudio自带工具

b)查看视图层次结构

(3)Choreographer
a)是一个类库,android自带的。

b)可以获取FPS,具备实时性

c)可以带到线上。

Choregrapher.getInstance().postFrameCallback

Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
            @Override
            public void doFrame(long frameTimeNanos) {
                Log.e("TAG", "starTime=" + starTime + ", frameTimeNanos=" + frameTimeNanos + ", frameDueTime=" + (frameTimeNanos - starTime) / 1000000);
            }
        });

输出

starTime=1757438563915168, frameTimeNanos=1757438654798396, frameDueTime=90

三、布局加载原理

从setContentView开始

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


public abstract void setContentView(@LayoutRes int resId);

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

LayoutInflater是真正对resId加载,进入inflate方法

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

XmlResourceParser loadXmlResourceParser(@AnyRes int id, @NonNull String type)
        throws NotFoundException {
   ...
            return impl.loadXmlResourceParser(value.string.toString(), id,
                    value.assetCookie, type);
        ...
}

loadXmlResourceParser加载指定文件的 XML 解析器。返回XmlResourceParser。
回到LayoutInflater的inflate方法,找到createViewFromTag方法,根据标签创建view,进入tryCreateView方法

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

mPrivateFactory只用于fragment标签, 如果view == null

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

进入createView方法,里面是反射创建View

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

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

在这里插入图片描述
(1)布局文件读取慢:IO过程
(2)创建View慢:反射(比new慢3倍)
LayoutInflater.Factory:
LayoutInflater创建view的一个Hook(挂钩)
定制创建view的过程:例如全局替换自定义Textview等
LayoutInflater包含Factory和Factory2两个
Factory和Factory2:Factory2继承自Factory,并且多了一个参数parent

四 优雅获取界面布局耗时

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

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

获取任一控件的耗时

低侵入性、使用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、优化布局加载:异步AsyncLayoutInflater
在WorkThread加载布局,(原生使用的办法是在UI线程中加载布局),(2)加载结束后,回调主线程。
优点:节约主线程的时间。
使用 AsyncLayoutInflater 类实现步骤:

implementation 'com.android.support:asynclayoutinflater:28.0.0'
// 在Activity中的onCreate()方法中,屏蔽下面语句
setContentView(R.layout.activity_main);
 
// 然后,添加如下代码
        new AsyncLayoutInflater(MainActivity.this).inflate(R.layout.activity_main, null, new AsyncLayoutInflater.OnInflateFinishedListener() {
            @Override
            public void onInflateFinished(@NonNull View view, int i, @Nullable ViewGroup viewGroup) {
                setContentView(view);
                mRecyclerView = findViewById(R.id.recycler_view);
                mRecyclerView.setLayoutManager(new LinearLayoutManager(MainActivity.this));
                mRecyclerView.setAdapter(mNewsAdapter);
                mNewsAdapter.setOnFeedShowCallBack(MainActivity.this);
            }
        });

​侧面缓解卡顿
使用了AsyncLayoutInflater就失去了向下兼容的特性,不能设置 LayoutInflater.Factory(自定义解决)
注意view中不能有依赖主线程的操作
这个方法只是缓解的作用,并没有从根本上解决(IO操作 和 反射)。

2、X2C介绍:
保留xml优点,解决性能问题(开发人员写xml,加载java代码),原理:通过APT编译期翻译xml为java代码
优点:本质上解决了性能问题,(不会IO操作,同时使用new创建)
缺点:引入新问题,不便于开发,可维护性差。(XML的优点是方便预览,维护性好)
部分属性Java不支持
失去了系统的兼容(AppCompat)如:TextView、ImageView系统在高版本向低版本有兼容
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);
    }
}

六、视图绘制优化实战

布局绘制回顾:

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

性能瓶颈:

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

减少布局层级和复杂度:

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

避免过度绘制

一个像素最好只被绘制一次、调试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();
    }

其他

ViewStub:高效占位符、延迟初始化(没有测量和布局的过程)
onDraw中避免:创建大量对象、耗时操作

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值