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

2022-04-03  本文已影响0人  沪漂意哥哥

一.CPU/GPU

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


image.png

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

image.png
image.png

三.卡顿原理

image.png

四.16ms主要处理两件事

五.如何减少时间

六.卡顿的原因

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

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

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

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

九.检测工具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优化思路与核心决策

卡顿分析与布局优化

卡顿分析

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

布局加载优化

异步加载
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

上一篇下一篇

猜你喜欢

热点阅读