阅读 85

Adnroid 卡顿分析与布局优化

1 卡顿分析

1 Systrace

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

2 android profile 中的cpu监测

App层面监测卡顿 1 利用UI线程的Looper打印日志匹配 

2 使用Choreographer.FrameCallback 

Looper日志监测卡顿** 

Android 主线程更新UI,如果界面1室内刷新少于60次,即FPS小于60,用户就会产生卡顿的感觉,简单来说Android使用消息机制进行UI更新,UI线程有个Looper,在其loop方法中会不断去除message,调用其他绑定的UI线程执行,如果在_handler的dispatchMessage_方法里面有耗时操作,就会发生卡顿.

只要监测_msg.target.dispatchmessage_的执行时间,就能检车就能检测到部分UI线程是否有耗时的操作。

注意到这行 执行代码的前后,有两个logging.println函数,如果设置了logging,会分别打印出>>>>> Dispatching to和 

<<<<< Finished to 这样的日志,

这样我们就可以通过两次log的时间差值,来计算dispatchMessage的执行时 间,从而设置阈值判断是否发生了卡顿。 

Looper 提供了 setMessageLogging(@Nullable Printer printer) 方法,所以我们可以自己实现一个Printer,在 通过setMessageLogging()方法传入即可

package com.dy.safetyinspectionforengineer.block;

import android.os.Looper;
public class BlockCanary {
    public static void install() {
        LogMonitor logMonitor = new LogMonitor();
        Looper.getMainLooper().setMessageLogging(logMonitor);
    }
}


package com.dy.safetyinspectionforengineer.block;

import android.os.Handler;
import android.os.HandlerThread;
import android.util.Log;
import android.util.Printer;
import java.util.List;

public class LogMonitor implements Printer {

    private StackSampler mStackSampler;
    private boolean mPrintingStarted = false;
    private long mStartTimestamp;
    // 卡顿阈值
    private long mBlockThresholdMillis = 3000;
    //采样频率
    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;
    }
}


package com.dy.safetyinspectionforengineer.block;

import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;

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


public class MyApplication extends Application{
    @Override
    public void onCreate() {
        super.onCreate();
        BlockCanary.install();
    }
}
复制代码

Choreographer.FrameCallback

Android系统每隔16ms发出VSYNC信号,来通知界面进行重绘、渲染,每一次同步的周期约为16.6ms,代表一帧 的刷新频率。通过Choreographer类设置它的FrameCallback函数,当每一帧被渲染时会触发回调 FrameCallback.doFrame (long frameTimeNanos) 函数。frameTimeNanos是底层VSYNC信号到达的时间戳 。

import android.os.Build;
import android.view.Choreographer;

import java.util.concurrent.TimeUnit;

public class 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);
                    }
                    lastFrameTimeNanos = frameTimeNanos;
                    Choreographer.getInstance().postFrameCallback(this);
                }
            });
        }
    }
}
复制代码

通过 ChoreographerHelper 可以实时计算帧率和掉帧数,实时监测App页面的帧率数据,发现帧率过低,还可以自 动保存现场堆栈信息。 Looper比较适合在发布前进行测试或者小范围灰度测试然后定位问题,ChoreographerHelper适合监控线上环境 的 app 的掉帧情况来计算 app 在某些场景的流畅度然后有针对性的做性能优化。

布局优化
1 层级优化

可以使用工具layoutinspector 查看层级,或这看源码查看层级
Tools - layoutINspector

2 使用merge标签

当我们有一些布局元素需要被多处使用时,我们可以将其抽取成一个单独的布局文件,在需要的地方include加载,这是就可以使用merge标签,吧这些抽离的标签进行包裹

3 使用viewstub标签

在不显示及不可见的情况下 用viewstub来包裹,被包裹后,如果visible=gone 则该view不会立即加载,等到需要显示的时候,设置viewstub为visible 或调用其inflater()方法,该view才会初始化

过度渲染
1进入开发则选项
2调用调试GPU过度绘制
3 选择显示过度绘制区域
3.1 蓝色 为一次绘制 绿色为两次绘制 粉色为3次绘制,红色为4次或更多次绘制

解决过度绘制问题
1 移除不需要的背景
2 使视图层次结构扁平化
3 降低透明度

布局加载优化
1 异步加载
setContentView 时 可以异步加载

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);
            }
        });
复制代码

2 掌阅X2C思路
通过注解,把XM代码编辑成java代码
github.com/iReaderAndr…

文章分类
Android