简述
当App流畅度不高时,会导致用户使用体验变差,最终导致用户流失,所以要对卡顿进行优化
Fps 监控
这个可以看之前的文章
Fps 堆栈抓取
堆栈抓取我们可以采用BlockCanary
做法进行抓取,BlockCanary
通过替换Looper的Printer来实现在每一个执行消息前后打印日志,通过俩次的时间间隔,作为一个消息的执行耗时,然后去dump 主线程的堆栈,然后上报,检测分析堆栈,来分析是哪些函数导致耗时
这种方案有一个缺点,就是存在字符串拼接,性能消耗比较严重
具体分析下Looper的Printer
首先我们看一下Loop#loop()方法 删减版
java
public static void loop() {
...
for (;;) {
...
// This must be in a local variable, in case a UI event sets the logger
Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
msg.target.dispatchMessage(msg);
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
...
}
}
主线程所有的任务都在msg.target.dispatchMessage(msg);
中执行完成,我们看到在msg.target.dispatchMessage(msg);
前后会调用logging.println
方法打印,这样的话,我们就可以替换系统的logging 然后重写println
方法,然后计算俩次println
方法的间隔时间,来判断是否卡顿
然后再另一条线程轮训获取主线程堆栈,并储存,如果检测到耗时就抓取堆栈,进行上报
实战
java
public class LoopPrinter implements Printer {
private final RxBlock.RxBlockListener mRxBlockListener;
private boolean mPrintingStarted = false;
private long mStartTimestamp;
private long mStartThreadTimestamp;
private static final int DEFAULT_BLOCK_THRESHOLD_MILLIS = 2000;
private long mBlockThresholdMillis = DEFAULT_BLOCK_THRESHOLD_MILLIS;
public LoopPrinter(RxBlock.RxBlockListener rxBlockListener) {
this.mRxBlockListener = rxBlockListener;
}
@Override
public void println(String x) {
if (!mPrintingStarted) {
mStartTimestamp = System.currentTimeMillis();
mStartThreadTimestamp = SystemClock.currentThreadTimeMillis();
mPrintingStarted = true;
startDump();
} else {
final long endTime = System.currentTimeMillis();
mPrintingStarted = false;
if (isBlock(endTime)) {
notifyBlockEvent(endTime);
}
stopDump();
}
}
private void notifyBlockEvent(long endTime) {
final long startTime = mStartTimestamp;
final long startThreadTime = mStartThreadTimestamp;
final long endThreadTime = SystemClock.currentThreadTimeMillis();
RxHandlerThreadFactory.getWriteLogThreadHandler().post(new Runnable() {
@Override
public void run() {
mRxBlockListener.onBlockEvent(startTime, endTime, startThreadTime, endThreadTime);
}
});
}
private void stopDump() {
if (RxBlock.getInstance().mRxStackSampler != null) {
RxBlock.getInstance().mRxStackSampler.stop();
}
}
private boolean isBlock(long endTime) {
return endTime - mStartTimestamp > mBlockThresholdMillis;
}
private void startDump() {
if (RxBlock.getInstance().mRxStackSampler != null) {
RxBlock.getInstance().mRxStackSampler.start();
}
}
}
java
package com.renxh.kit.log.fps;
import android.os.Looper;
import android.util.Log;
import com.immomo.biz.util.toast.ToastUtils;
import java.util.ArrayList;
public class RxBlock {
private static final RxBlock sRxBlock = new RxBlock();
public RxStackSampler mRxStackSampler;
private LoopPrinter loopPrinter;
private boolean mMonitorStarted;
private RxBlock() {
}
public static RxBlock getInstance() {
return sRxBlock;
}
public RxBlock init(Thread thread) {
mRxStackSampler = new RxStackSampler(thread, 0);
loopPrinter = new LoopPrinter(new RxBlockListener() {
@Override
public void onBlockEvent(long realTimeStart, long realTimeEnd, long threadTimeStart, long threadTimeEnd) {
ToastUtils.show("好像出现卡顿了哦");
ArrayList<String> threadStackEntries = mRxStackSampler.getThreadStackEntries(realTimeStart, realTimeEnd);
if (!threadStackEntries.isEmpty()) {
RxBlockInfo blockInfo = new RxBlockInfo().setMainThreadTimeCost(realTimeStart, realTimeEnd, threadTimeStart, threadTimeEnd).
setThreadStackEntries(threadStackEntries).flushString();
Log.i("Block", "【UI阻塞了】耗时:" + blockInfo.timeCost + "\n" + blockInfo.toString());
}
}
});
return sRxBlock;
}
public void start() {
if (!mMonitorStarted) {
mMonitorStarted = true;
Looper.getMainLooper().setMessageLogging(loopPrinter);
}
}
public void stop() {
if (mMonitorStarted) {
mMonitorStarted = false;
Looper.getMainLooper().setMessageLogging(null);
mRxStackSampler.stop();
}
}
public interface RxBlockListener {
void onBlockEvent(long realStartTime,
long realTimeEnd,
long threadTimeStart,
long threadTimeEnd);
}
}
java
package com.renxh.kit.log.fps;
import android.os.Handler;
import android.os.HandlerThread;
public class RxHandlerThreadFactory {
private static HandlerThreadWrapper sLoopThread = new HandlerThreadWrapper("loop");
private static HandlerThreadWrapper sWriteLogThread = new HandlerThreadWrapper("writer");
private RxHandlerThreadFactory() {
throw new InstantiationError("Must not instantiate this class");
}
public static Handler getTimerThreadHandler() {
return sLoopThread.getHandler();
}
public static Handler getWriteLogThreadHandler() {
return sWriteLogThread.getHandler();
}
private static class HandlerThreadWrapper {
private Handler handler = null;
public HandlerThreadWrapper(String threadName) {
HandlerThread handlerThread = new HandlerThread("BlockCanary-" + threadName);
handlerThread.start();
handler = new Handler(handlerThread.getLooper());
}
public Handler getHandler() {
return handler;
}
}
}
java
package com.renxh.kit.log.fps;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
public class RxStackSampler {
public static final String SEPARATOR = "\r\n";
//要检测的线程
private final Thread mCurrentThread;
//map最大数据
private final int mMaxEntryCount;
//map默认存储最大数据
private static final int DEFAULT_MAX_ENTRY_COUNT = 100;
//默认延迟时间
private static final int DEFAULT_SAMPLE_INTERVAL = 300;
//原子类
protected AtomicBoolean mShouldSample = new AtomicBoolean(false);
protected long mSampleInterval;
private static final LinkedHashMap<Long, String> sStackMap = new LinkedHashMap<>();
public RxStackSampler(Thread thread, long sampleIntervalMillis) {
this(thread, DEFAULT_MAX_ENTRY_COUNT, sampleIntervalMillis);
}
public RxStackSampler(Thread thread, int maxEntryCount, long sampleIntervalMillis) {
mCurrentThread = thread;
mMaxEntryCount = maxEntryCount;
if (0 == sampleIntervalMillis) {
sampleIntervalMillis = DEFAULT_SAMPLE_INTERVAL;
}
mSampleInterval = sampleIntervalMillis;
}
private Runnable mRunnable = new Runnable() {
@Override
public void run() {
doSample();
if (mShouldSample.get()) {
RxHandlerThreadFactory.getTimerThreadHandler()
.postDelayed(mRunnable, mSampleInterval);
}
}
};
public void start() {
if (mShouldSample.get()) {
return;
}
mShouldSample.set(true);
RxHandlerThreadFactory.getTimerThreadHandler().removeCallbacks(mRunnable);
RxHandlerThreadFactory.getTimerThreadHandler().post(mRunnable);
}
public void stop() {
if (!mShouldSample.get()) {
return;
}
mShouldSample.set(false);
RxHandlerThreadFactory.getTimerThreadHandler().removeCallbacks(mRunnable);
}
private void doSample() {
StringBuilder stringBuilder = new StringBuilder();
for (StackTraceElement stackTraceElement : mCurrentThread.getStackTrace()) {
stringBuilder
.append(stackTraceElement.toString())
.append(SEPARATOR);
}
synchronized (sStackMap) {
if (sStackMap.size() == mMaxEntryCount && mMaxEntryCount > 0) {
sStackMap.remove(sStackMap.keySet().iterator().next());
}
sStackMap.put(System.currentTimeMillis(), stringBuilder.toString());
}
}
public ArrayList<String> getThreadStackEntries(long startTime, long endTime) {
ArrayList<String> result = new ArrayList<>();
synchronized (sStackMap) {
for (Long entryTime : sStackMap.keySet()) {
if (startTime < entryTime && entryTime < endTime) {
result.add(sStackMap.get(entryTime));
}
}
}
return result;
}
}
java
package com.renxh.kit.log.fps;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Locale;
public class RxBlockInfo {
public static final String SEPARATOR = "\r\n";
public long timeCost;
private long threadTimeCost;
public static final SimpleDateFormat TIME_FORMATTER =
new SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US);
private String timeStart;
private String timeEnd;
private ArrayList<String> threadStackEntries;
private StringBuilder timeSb = new StringBuilder();
private StringBuilder stackSb = new StringBuilder();
public static final String KEY_TIME_COST = "time";
public static final String KEY_THREAD_TIME_COST = "thread-time";
public static final String KEY_TIME_COST_START = "time-start";
public static final String KEY_TIME_COST_END = "time-end";
public static final String KV = " = ";
public static final String KEY_STACK = "stack";
public RxBlockInfo setMainThreadTimeCost(long realTimeStart, long realTimeEnd, long threadTimeStart, long threadTimeEnd) {
timeCost = realTimeEnd - realTimeStart;
threadTimeCost = threadTimeEnd - threadTimeStart;
timeStart = TIME_FORMATTER.format(realTimeStart);
timeEnd = TIME_FORMATTER.format(realTimeEnd);
return this;
}
public RxBlockInfo setThreadStackEntries(ArrayList<String> threadStackEntries) {
this.threadStackEntries = threadStackEntries;
return this;
}
public RxBlockInfo flushString() {
String separator = SEPARATOR;
timeSb.append(KEY_TIME_COST).append(KV).append(timeCost).append(separator);
timeSb.append(KEY_THREAD_TIME_COST).append(KV).append(threadTimeCost).append(separator);
timeSb.append(KEY_TIME_COST_START).append(KV).append(timeStart).append(separator);
timeSb.append(KEY_TIME_COST_END).append(KV).append(timeEnd).append(separator);
if (threadStackEntries != null && !threadStackEntries.isEmpty()) {
StringBuilder temp = new StringBuilder();
for (String s : threadStackEntries) {
temp.append(s);
temp.append(separator);
}
stackSb.append(KEY_STACK).append(KV).append(temp.toString()).append(separator);
}
return this;
}
public String toString() {
return String.valueOf(timeSb) + stackSb;
}
}
缺点1 堆栈采集不准
因为上述方案是要等待消息执行完,才知道是否会卡顿,如果任务执行完之后再去采集堆栈,就会导致堆栈偏移
解决这个问题我们可以单独线程,轮训获取堆栈
缺点2 Printer 被覆盖
由于不能持有多个Printer实例,所以存在互相覆盖的情况
这个解决方案可以添加一个IdleHanlder,在主线程空闲的时候定期检查Printer是否被替换,如果被替换,就抢回来
线程运行情况监控
除了Fps 我们还需要监控主线程的CPU time 等数据,以及确认是否存在CPU被其他线程不正常抢夺,导致主线程CPU时间过段问题,我们可以用下面数据来判断主线程调度情况
- CPU time 和 Wall time
- 主线程的优先级
- 主线程被抢夺的次数
Wall time 客观过去的时间,CPU timp 指的是进程真正在CPU上执行的时间,一个进程的CPU time 包括 用户时间(utime)和 内核时间(stime)
- 用户时间 : 执行APP代码的时间
- 内核时间 : App调用系统花费的时间
要回去当前进程的CPU 时间 我们可以通过 /proc/${pid}/stat
获取主进程的优先级和调度情况,可以通过/proc/ <math xmlns="http://www.w3.org/1998/Math/MathML"> p i d / t a s k / {pid}/task/ </math>pid/task/{pid}/sched
流畅度优化
App流畅度影响因素主要包括以下几点
- 绘制线程获取的CPU时间过少
- 主线程消息队列,绘制无关任务过多(如I/O binder 锁)的耗时太多
- 绘制任务过久
针对上面问题,我们从三方面进行优化
- 增加绘制线程相关运行时间
- 减少主线程非绘制任务
- 减少绘制任务耗时
增加绘制线程相关运行时间
APP绘制界面,首先要CPU 计算绘制信息,然后同步给GPU 进行绘制,APP内绘制线程主要是主线程和RenderThread, 如果这俩个线程处于运行时间过少,就会导致绘制无法在规定时间完成,导致卡顿
提升线程优先级
Android 上线程可以分配多少CPU时间,取决于线程的优先级,我们一般会通过下面俩中方式修改
- Process.setThreadPriority();
- Thread # setPriority
他们最终会通过执行native 的 setPriority 来实现
除了修改,我们还需要监控 而Matrix 中就提供监控修改线程优先级,他主要是通过Xhook 拦截所有.so 对setPriority 调用,如果修改的是主线程的优先级,会回调到java测方法,然后再java测获取调用栈,这样就实现监控
减少线程抢占
另一个影响因素就是线程抢占,Linux 中目前主流的进程/线程调度方式是CFS 完全公平调度器, 会根据当前线程数及其优先级计算权重,如果线程很多那么单个线程就会分配时间过少,导致还没有执行完。就被抢占,导致卡顿
主需要通过一下方式解决
-
减少线程数
减少线程数主要通过线程池复用,把线程控制在一定范围内,减少new Thread 的调用,并在编译期间修改字节码,将new Thread 修改为提交线程池
-
及时停止子线程
线程任务结束后及时停止线程,比如HandlerThread 内部有Loop循环,如果不主动停止,会一直存在, 线程池核心线程会一直存活,设置核心线程数超时时间
减少主线程非绘制任务
一般情况下主线程非绘制任务如下
- 缓存读取 文件IO 等锁
- 布局解析 文件IO 反射 类加载
- 发起数据请求 文件IO Binder 调用
- 数据解析更新 等锁
减少主线程布局解析耗时
布局解析,需要先读取解析XML 然后反射创建对象,如果布局层级复杂会花费很多时间,这种我们可以使用AsyncLayoutInflater 把布局解析放到子线程
减少主线程文件读写耗时
除了布局解析,还有很多其他操作会触发文件读写 比如
- 读取Assets文件
- 数据库读写
- 调用ContentResolver
我们需要避免这些操作,首先我们需要检测我们可以通过
-
开始StrictMode来检测
StrictMode(严格模式)是Android 官方提供的不合理代码检测工具,可以帮助我我们发现主线程磁盘操作,只能检测java不能检测native
javaStrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() .detectCustomSlowCalls() .detectDiskReads()//主线程磁盘读 .detectDiskWrites() .detectNetwork()//主线程网络请求 .penaltyDialog()//检测后如何处理 .penaltyLog() .penaltyFlashScreen() .build());
-
拦截Linux read/weite Api
可以借用Xhook 拦截
减少主线程阻塞等锁耗时
在面临多线程问题,我们通常会使用锁来解决同步问题,会出现主线程阻塞,等待其他线程锁的情况,极端情况会互锁造成ANR
所以我们要合理使用锁
-
减少锁的范围
-
减少主线程取锁情况
-
合理使用数据结构
如果在主线程,我们对使用同步锁要非常谨慎,能不能用就不用,或者放到子线程,如果必须要用就减少所得范围,比如synchronized 代码块 除此之外我们需要合理使用数据结构比如 CopyOnWriteArrayList,这种数据结构在多线程并发场景不会阻塞其他操作而是对复制数据进行改动,操作完在进行原数据更新
减少主线程Binder调用
其实就是减少系统API 调用,因为也会阻塞主线程,我们尽量调用一次然后缓存数据
减少主线程其他不必要操作
- 减少四大组件生命周期的耗时操作
- 调试日志引入不必要开销
- 多层嵌套重复抛出异常
- 监听文字输入播放进度滑动距离频繁执行的函数
减少绘制任务耗时
- 懒加载Viewpager 数据
- 使用Viewstub 加载不可见的布局
- 动画不可见停止
- 减少布局层级
- 简化或者预计算复杂绘制逻辑等