美文网首页
Android 性能优化 06---UI卡顿优化

Android 性能优化 06---UI卡顿优化

作者: 沪漂意哥哥 | 来源:发表于2022-04-03 17:33 被阅读0次

    一.CPU/GPU

    CPU的任务繁多,做逻辑计算外,还要做内存管理、显示操作,因此 在实际运算的时候性能会大打折扣,在没有 GPU 的时代,不能显示复 杂的图形,其运算速度远跟不上今天复杂三维游戏的要求。即使 CPU 的工作频率超过 2GHz 或更高,对它绘制图形提高也不大。这时 GPU 的设计就出来了 。


    image.png

    二.XML布局显示到屏幕流程

    image.png
    image.png

    三.卡顿原理

    image.png

    四.16ms主要处理两件事

    • 将UI对象转换成多边形和纹理
    • CPU传递数据到GPU,GPU进行绘制

    五.如何减少时间

    • CPU减少XML转换成对象时间
    • GPU减少重复绘制(GPU傻)

    六.卡顿的原因

    两个单位的运行时间超出16.66ms就会跳帧

    • XML文件加载解析,到传输至底层到最终post到通知surfaceflinger的时间
    • GPU绘制产生数据的时间
      所以想要处理就要控制这两块时间在16.66mm内为最优。
      卡顿优化的唯一核心就是让CPU的数据处理和GPU的数据处理降低至16.66ms内全部处理完。

    七.常用问题解决方案01-布局优化

    层级越深--->infalate--->递归走法---》内存--->栈去里面也要消耗。层级越多CPU算力需要越多。

    • 常用标签
      include:头尾等同质化严重的可复用XML最好做成一个独立布局文件引用。
      merge:被include的具体布局采用merge,作用是会在加载时直接嵌入到父布局中,和include配合使用。
      viewstub:常规像密码提示框那些东西只有在需要特定条件下触发才展示的用ViewStub这个组件只用在状态为visible时才会加载。
    • 常用方案
      1.调整布局结构。
      2.背景色匹配。
      3.使用约束布局。
      约束布局优点:
      a.极大程度减少布局层级
      b.可以实现一些其他布局管理器不能实现的样式
      约束布局缺点:
      每个被参考的控件都需要设置id

    八.常用问题解决方案02-过度绘制

    • GPU过度绘制检查
      手机开发者功能中自带检测工具。
    • 解决方案
      1.移除布局中不必要的背景。
      2.是视图层次结构扁平化。
      3.裁剪不必要的绘制元素。
      src = 一次opengl绘制
      background = 一次opengl绘制
      过度绘制的几种情况:
      1: 布局层级太深, 用户看不到的区域也会被绘制
      2: 自定义控件中,onDraw方法做了过多的绘制

    九.检测工具01layout inspector

    查看布局层次结构,主要用于布局优化。
    具体使用请查看:

    https://blog.csdn.net/cadi2011/article/details/85212762
    

    十.检测工具02systrace

    具体使用请查看:

    https://www.cnblogs.com/wangjie1990/p/11327220.html
    

    十一.检测工具03Looper机制

    因为在Looper进行消息转发的时候,会涉及到打印问题,且在执行前后都会打印,利用这个机制在Looper中他提供能够自定义Loging相关机制。

    public class LogMonitor implements Printer {
    
    
        private StackSampler mStackSampler;
        private boolean mPrintingStarted = false;
        private long mStartTimestamp;
        // 卡顿阈值
        private long mBlockThresholdMillis = (long) (5 * 16.66);
        //采样频率
        private long mSampleInterval = 1000;
    
        private Handler mLogHandler;
    
        public LogMonitor() {
            mStackSampler = new StackSampler(mSampleInterval);
            HandlerThread handlerThread = new HandlerThread("block-canary-io");
            handlerThread.start();
            mLogHandler = new Handler(handlerThread.getLooper());
        }
    
        @Override
        public void println(String x) {
            //从if到else会执行 dispatchMessage,如果执行耗时超过阈值,输出卡顿信息
            if (!mPrintingStarted) {
                //记录开始时间
                mStartTimestamp = System.currentTimeMillis();
                mPrintingStarted = true;
                mStackSampler.startDump();
            } else {
                final long endTime = System.currentTimeMillis();
                mPrintingStarted = false;
                //出现卡顿
                if (isBlock(endTime)) {
                    notifyBlockEvent(endTime);
                }
                mStackSampler.stopDump();
            }
        }
    
        private void notifyBlockEvent(final long endTime) {
            mLogHandler.post(new Runnable() {
                @Override
                public void run() {
                    //获得卡顿时主线程堆栈
                    List<String> stacks = mStackSampler.getStacks(mStartTimestamp, endTime);
                    for (String stack : stacks) {
                        Log.e("block-canary", stack);
                    }
                }
            });
        } 
    
        private boolean isBlock(long endTime) {
            return endTime - mStartTimestamp > mBlockThresholdMillis;
        } 
    }
    
    
    /**
     * 适用于耗时代码检测
     */
    public class BlockCanary {
        public static void install() {
            LogMonitor logMonitor = new LogMonitor();
            Looper.getMainLooper().setMessageLogging(logMonitor);
        }
    }
    
    
    /**
     * 适用于耗时代码检测
     */
    public class BlockCanary {
        public static void install() {
            LogMonitor logMonitor = new LogMonitor();
            Looper.getMainLooper().setMessageLogging(logMonitor);
        }
    }
    
    
    public class StackSampler {
        public static final String SEPARATOR = "\r\n";
        public static final SimpleDateFormat TIME_FORMATTER =
                new SimpleDateFormat("MM-dd HH:mm:ss.SSS");
    
    
        private Handler mHandler;
        private Map<Long, String> mStackMap = new LinkedHashMap<>();
        private int mMaxCount = 100;
        private long mSampleInterval;
        //是否需要采样
        protected AtomicBoolean mShouldSample = new AtomicBoolean(false);
    
        public StackSampler(long sampleInterval) {
            mSampleInterval = sampleInterval;
            HandlerThread handlerThread = new HandlerThread("block-canary-sampler");
            handlerThread.start();
            mHandler = new Handler(handlerThread.getLooper());
        }
    
        /**
         * 开始采样 执行堆栈
         */
        public void startDump() {
            //避免重复开始
            if (mShouldSample.get()) {
                return;
            }
            mShouldSample.set(true);
    
            mHandler.removeCallbacks(mRunnable);
            mHandler.postDelayed(mRunnable, mSampleInterval);
        }
    
        public void stopDump() {
            if (!mShouldSample.get()) {
                return;
            }
            mShouldSample.set(false);
    
            mHandler.removeCallbacks(mRunnable);
        }
    
    
        public List<String> getStacks(long startTime, long endTime) {
            ArrayList<String> result = new ArrayList<>();
            synchronized (mStackMap) {
                for (Long entryTime : mStackMap.keySet()) {
                    if (startTime < entryTime && entryTime < endTime) {
                        result.add(TIME_FORMATTER.format(entryTime)
                                + SEPARATOR
                                + SEPARATOR
                                + mStackMap.get(entryTime));
                    }
                }
            }
            return result;
        }
    
        private Runnable mRunnable = new Runnable() {
            @Override
            public void run() {
                StringBuilder sb = new StringBuilder();
                StackTraceElement[] stackTrace = Looper.getMainLooper().getThread().getStackTrace();
                for (StackTraceElement s : stackTrace) {
                    sb.append(s.toString()).append("\n");
                }
                synchronized (mStackMap) {
                    //最多保存100条堆栈信息
                    if (mStackMap.size() == mMaxCount) {
                        mStackMap.remove(mStackMap.keySet().iterator().next());
                    }
                    mStackMap.put(System.currentTimeMillis(), sb.toString());
                }
    
                if (mShouldSample.get()) {
                    mHandler.postDelayed(mRunnable, mSampleInterval);
                }
            }
        };
    
    }
    
    

    十二.ChoreograhperHelper编舞者监听帧率

    注意目的是看整个运行情况定位到大块,然后结合上面设定阈值进行处理。

    /**
     * 细化帧数
     * 适用于快速定位帧率监控
     */
    public class ChoreographerHelper {
        private static final String TAG = "ChoreographerHelper";
        static long lastFrameTimeNanos = 0;
    
        public static void start() {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
                    @Override
                    public void doFrame(long frameTimeNanos) {
                        //上次回调时间
                        if (lastFrameTimeNanos == 0) {
                            lastFrameTimeNanos = frameTimeNanos;
                            Choreographer.getInstance().postFrameCallback(this);
                            return;
                        }
                        long diff = (frameTimeNanos - lastFrameTimeNanos) / 1_000_000;
                        if (diff > 16.6f) {
    
                            //掉帧数
                            int droppedCount = (int) (diff / 16.6);
                            if (droppedCount > 2) {
                                Log.w(TAG, "UI线程超时(超过16ms)当前:" + diff + "ms" + " , 丢帧:" + droppedCount);
                            }
                        }
                        lastFrameTimeNanos = frameTimeNanos;
                        Choreographer.getInstance().postFrameCallback(this);
                    }
                });
            }
        }
    }
    
    

    十三.UI优化思路与核心决策

    • 如何研判是否需要做调整
      优化的方案一定是抠空间或者时间:
      加入20个Fragment:优化每一个Fragment到极致(1:数据能不能少加载点?2:流程能不能优化?视觉能不能优化?)
      20个Fragment能不能少加载点?流程优化?
      空间重要还是时间 ?流程上能不能优化?
    • 方案上如何去做妥协
      1.稳定第一位
      2.成本/用户体验

    卡顿分析与布局优化

    卡顿分析

    Systrace

    Systrace 是Android平台提供的一款工具,用于记录短期内的设备活动。该工具会生成一份报告,其中汇总了
    Android 内核中的数据,例如 CPU 调度程序、磁盘活动和应用线程。Systrace主要用来分析绘制性能方面的问题。在发生卡顿时,通过这份报告可以知道当前整个系统所处的状态,从而帮助开发者更直观的分析系统瓶颈,改进性能。

    App层面监控卡顿

    systrace可以让我们了解应用所处的状态,了解应用因为什么原因导致的。若需要准确分析卡顿发生在什么函数,
    资源占用情况如何,目前业界两种主流有效的app监控方式如下:

    1、 利用UI线程的Looper打印的日志匹配;
    2、 使用Choreographer.FrameCallback

    Looper日志检测卡顿
    Android主线程更新UI。如果界面1秒钟刷新少于60次,即FPS小于60,用户就会产生卡顿感觉。简单来说,
    Android使用消息机制进行UI更新,UI线程有个Looper,在其loop方法中会不断取出message,调用其绑定的
    Handler在UI线程执行。如果在handler的dispatchMesaage方法里有耗时操作,就会发生卡顿。
    其实这种方式也就是 BlockCanary 原理。

    Choreographer.FrameCallback
    Android系统每隔16ms发出VSYNC信号,来通知界面进行重绘、渲染,每一次同步的周期约为16.6ms,代表一帧
    的刷新频率。通过Choreographer类设置它的FrameCallback函数,当每一帧被渲染时会触发回调FrameCallback.doFrame (long frameTimeNanos) 函数。frameTimeNanos是底层VSYNC信号到达的时间戳 。
    通过 ChoreographerHelper 可以实时计算帧率和掉帧数,实时监测App页面的帧率数据,发现帧率过低,还可以自动保存现场堆栈信息。

    Looper比较适合在发布前进行测试或者小范围灰度测试然后定位问题,ChoreographerHelper适合监控线上环境的 app 的掉帧情况来计算 app 在某些场景的流畅度然后有针对性的做性能优化。

    布局优化

    层级优化

    measure、layout、draw这三个过程都包含自顶向下的View Tree遍历耗时,如果视图层级太深自然需要更多的时间来完成整个绘测过程,从而造成启动速度慢、卡顿等问题。而onDraw在频繁刷新时可能多次出发,因此onDraw更不能做耗时操作,同时需要注意内存抖动。对于布局性能的检测,依然可以使用systrace与traceview按照绘制流程检查绘制耗时函数。
    Layout Inspector
    在较早的时代SDK中有一个hierarchy viewer 工具,但是早在 Android Studio 3.1 配套的SDK中(具体SDK版本不记得了)就已经被弃用。现在应在运行时改用 Layout Inspector来检查应用的视图层次结构
    使用merge标签
    当我们有一些布局元素需要被多处使用时,这时候我们会考虑将其抽取成一个单独的布局文件。在需要使用的地方通过 include 加载。
    使用ViewStub 标签
    当我们布局中存在一个View/ViewGroup,在某个特定时刻才需要他的展示时,可能会有同学把这个元素在xml中定义为invisible或者gone,在需要显示时再设置为visible可见。比如在登陆时,如果密码错误在密码输入框上显示提示。
    invisible
    view设置为invisible时,view在layout布局文件中会占用位置,但是view为不可见,该view还是会创建对
    象,会被初始化,会占用资源。
    gone
    view设置gone时,view在layout布局文件中不占用位置,但是该view还是会创建对象,会被初始化,会占
    用资源。

    如果view不一定会显示,此时可以使用 ViewStub 来包裹此View 以避免不需要显示view但是又需要加载view消耗资
    源。
    viewstub是一个轻量级的view,它不可见,不用占用资源,只有设置viewstub为visible或者调用其inflater()方法
    时,其对应的布局文件才会被初始化。

    过度渲染

    过度绘制是指系统在渲染单个帧的过程中多次在屏幕上绘制某一个像素。例如,如果我们有若干界面卡片堆叠在一
    起,每张卡片都会遮盖其下面一张卡片的部分内容。但是,系统仍然需要绘制堆叠中的卡片被遮盖的部分。

    GPU 过度绘制检查
    手机开发者选项中能够显示过度渲染检查功能,通过对界面进行彩色编码来帮我们识别过度绘制。
    解决过度绘制问题
    可以采取以下几种策略来减少甚至消除过度绘制:

    • 移除布局中不需要的背景。
      移除不必要的背景可以快速提高渲染性能。不必要的背景可能永远不可见,因为它会被应用在该视图上
      绘制的任何其他内容完全覆盖。例如,当系统在父视图上绘制子视图时,可能会完全覆盖父视图的背
      景。

    • 使视图层次结构扁平化。
      可以通过优化视图层次结构来减少重叠界面对象的数量,从而提高性能。

    • 降低透明度。
      对于不透明的 view ,只需要渲染一次即可把它显示出来。但是如果这个 view 设置了 alpha 值,则至少需要渲染两次。这是因为使用了 alpha 的 view 需要先知道混合 view 的下一层元素是什么,然后再结合上层的 view 进行Blend混色处理。透明动画、淡入淡出和阴影等效果都涉及到某种透明度,这就会造成了过度绘制。可以通过减少要渲染的透明对象的数量,来改善这些情况下的过度绘制。例如,如需获得灰色文本,可以在 TextView 中绘制黑色文本,再为其设置半透明的透明度值。但是,简单地通过用灰色绘制文本也能获得同样的效果,而且能够大幅提升性能。

    布局加载优化

    异步加载
    LayoutInflater加载xml布局的过程会在主线程使用IO读取XML布局文件进行XML解析,再根据解析结果利用反射创建布局中的View/ViewGroup对象。这个过程随着布局的复杂度上升,耗时自然也会随之增大。Android为我们提供了 Asynclayoutinflater 把耗时的加载操作在异步线程中完成,最后把加载结果再回调给主线程。

    dependencies { 
      implementation "androidx.asynclayoutinflater:asynclayoutinflater:1.0.0" 
    }
    
    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); //...... } 
    });
    

    1、使用异步 inflate,那么需要这个 layout 的 parent 的 generateLayoutParams 函数是线程安全的;
    2、所有构建的 View 中必须不能创建 Handler 或者是调用 Looper.myLooper;(因为是在异步线程中加载的,异步线程默认没有调用 Looper.prepare );
    3、AsyncLayoutInflater 不支持设置 LayoutInflater.Factory 或者 LayoutInflater.Factory2;
    4、不支持加载包含 Fragment 的 layout

    5、如果 AsyncLayoutInflater 失败,那么会自动回退到UI线程来加载布局;

    掌阅X2C思路
    https://github.com/iReaderAndroid/X2C/blob/master/README_CN.md

    相关文章

      网友评论

          本文标题:Android 性能优化 06---UI卡顿优化

          本文链接:https://www.haomeiwen.com/subject/sswfsrtx.html