【性能优化】安卓性能优化之CPU优化
- CPU优化及常用工具
- 原理与文章参考
- 常用ADB
- 常用原理、监控手段
- 原理
- 监控手段
- 多线程并发解决耗时
- UI相关
- 常见场景
- 排查CPU占用过高
- 常用系统/开源分析工具
- AndroidStudio Profiler
- Systrace
- Btrace
- Perfetto
- TraceView和 Profile
- ANR相关
- ANR原理及常见场景
- ANR/卡顿检测
- 卡顿检测
- CPU 优化案例
CPU优化及常用工具
原理与文章参考
- 编舞者、looper、JankStats方法
常用ADB
含义 | 命令 | 备注 |
---|---|---|
查看CPU状态 | adb shell top -H -d 1 -p pid -O cpu | -O cpu 查看对应在那个核心 ;修改采样间隔为1s |
导出当前进程所有线程状态到tombstoned | adb shell run-as kill -3 | 实际上是以一个异常状态导出了,利用了墓碑机制 |
查看进程的所有线程 | adb shell "ps -T | grep pid" |
查看进程占用cpu情况 | adb shell dumpsys cpuinfo | grep [进程名] |
查看进程内线程占用cpu的情况 | adb shell top -n 1 -d 0.5 | grep proc_ id |
获取设备cpu信息 | adb shell cat /proc/cpuinfo | 或者查看 /sys/devices/system/cpu 目录下的文件夹 |
常用原理、监控手段
原理
- 普通手机默认60帧刷新率,相当于每帧16.6ms
- 利用系统预留接口 对每个帧率/handler消息等 进行统计
监控手段
- 设置looperPrinter
- 字节码插桩检测慢函数(martix dokit)
- 编舞者获取frame帧率
- jetpack JankStats,获取丢帧信息
多线程并发解决耗时
线程池/数量参考
- CPU密集:线程数设置为CPU核心数 + 1
- IO密集:线程数设置为CPU核心数 * 2
UI相关
- 利用
<font style="color:rgb(77, 77, 77);">IdelHandler</font>
对一些常用view进行预绘制 - 通过排查布局,减少过度绘制
常见场景
- 过度绘制
- 频繁IO
- 主线程耗时任务
排查CPU占用过高
- 规范线程命名,定位线程
- 抓取top数据,查看具体哪个线程占用高
- cpu指标含义解释
- 线程各参数详解
常用系统/开源分析工具
AndroidStudio Profiler
- 抓取CPU火焰图,卡顿/ANR 主要监测主线程,是否会出现耗时操作
Systrace
- 官方指令参考
- 官方推荐指令
$ python systrace.py -o mynewtrace.html sched freq idle am wm gfx view binder_driver hal dalvik camera input res
- 要求环境
python2.7 安装
python six 模块,命令 :pip install six
“No module named win32con” 问题,安装相关:pip install pypiwin32
- 拉取到信息后用perfetto 打开即可 但是这个主要是针对系统的 对应用开发帮助不大,分析自己应用可以用btrace
Btrace
- 官方链接
Perfetto
- 官方-快速开始
- 工具界面
- 入门使用
- 线程状态
TraceView和 Profile
- traceview官方参考
- traceview使用
- 导出的日志分析
- 使用DDMS查看
新版路径:Sdk\tools\monitor.bat
- Incl Cpu Time:方法在CPU中执行所有时间(包含其调用的方法所消耗的时间)
- Excl Cpu Time: 方法在CPU中执行的时间(不包含其调用的方法所消耗的时间)
- Incl Real Time:方法运行消耗的所有时间(包含子方法)
- Excl Real Time:方法运行消耗的时间(不包含子方法)
- Calls + Recur Calls/Total :方法调用、递归次数(重要指标,防止死循环)
- Cpu Time/Call :该方法平均占用 CPU 的时间(重要指标,可以看出单个方法占用CPU的平均时间,但是要防止在个别调用处出现长时间占用,然后被平均了)
- Real Time/Call :平均执行时间,包括切换、阻塞的时间(重要指标,可以看出单个方法执行的平均时间值,但是要防止在个别调用处出现长时间调用,然后被平均了)
- TraceView优势
可以精确埋点
Debug.startMethodTracing("sample");
...
Debug.stopMethodTracing();
ANR相关
ANR原理及常见场景
- 原理
ANR(Application Not Responding)的监测原理本质上是消息机制,设定一个delay消息,超时未被移除则触发ANR。具体逻辑处理都在system server端,包括发送超时消息,移除超时消息,处理超时消息以及ANR弹框展示等;对于app而言,触发ANR的条件是主线程阻塞。
- 常见场景
- Service ANR:前台20s,后台200s;startForeground超时10s
- Broadcast ANR:前台10s,后台60s
- Input ANR:按键或触摸事件在5s内无响应
- ContentProvider ANR:10s,少见
ANR/卡顿检测
- 通过设置Looper的printer可以检测耗时
- WatchDog机制,子线程发送消息自增,休眠后检查
- 参考
- ANR日志导出
// 安卓21以下有权限可以获取到 anr 日志
private FileObserver fileObserver = null;
void initialize(....){
// 实例化FileObserver ,监控路径"/data/anr/",监听文件被写入
fileObserver = new FileObserver("/data/anr/", CLOSE_WRITE) {
public void onEvent(int event, String path) {
try {
if (path != null) {
String filepath = "/data/anr/" + path;
// 写入的文件是否有关键字 “trace”
if (filepath.contains("trace")) {
// 处理anr异常
handleAnr(filepath);
}
}
} catch (Exception e) {
XCrash.getLogger().e(Util.TAG, "AnrHandler fileObserver onEvent failed", e);
}
}
};
try {
// 启动FileObserver 监控
fileObserver.startWatching();
} catch (Exception e) {
fileObserver = null;
XCrash.getLogger().e(Util.TAG, "AnrHandler fileObserver startWatching failed", e);
}
}
private void handleAnr(String filepath) {
...
// 读取anr文件 /data/anr/trace*.txt。返回文件内容
String trace = getTrace(filepath, anrTime.getTime());
//删除其他的anr异常日志文件
if (!FileManager.getInstance().maintainAnr()) {
return;
}
//获取 tombstone 的文件头
String emergency = null;
try {
emergency = getEmergency(anrTime, trace);
} catch (Exception e) {
XCrash.getLogger().e(Util.TAG, "AnrHandler getEmergency failed", e);
}
// 创建anr异常日志保存文件
File logFile = null;
try {
String logPath = String.format(Locale.US, "%s/%s_%020d_%s__%s%s", logDir, Util.logPrefix, anrTime.getTime() * 1000, appVersion, processName, Util.anrLogSuffix);
logFile = FileManager.getInstance().createLogFile(logPath);
} catch (Exception e) {
XCrash.getLogger().e(Util.TAG, "AnrHandler createLogFile failed", e);
}
if (logFile != null){
// 根据配置将日志文件头,traces,logcat日志保存在文件中。
}
}
// 高版本通过AMS获取日志
public class ANRMoniter implements Runnable {
private final String TAG = "ANRMoniter";
private HandlerThread handlerThread = new HandlerThread("WatchMainHandler");
private ILog logImpl;
private Application app;
private Handler watchHandler;
private Handler mainHandler;
private ScheduleCheckTask scheduleCheckTask;
private int CHECK_INTERVAL = 5_000;
public ANRMoniter(Application app, ILog logImpl) {
this.app = app;
this.logImpl = logImpl;
init();
}
private void init() {
handlerThread.start();
Looper looper = handlerThread.getLooper();
watchHandler = new Handler(looper);
mainHandler = new Handler(Looper.getMainLooper());
scheduleCheckTask = new ScheduleCheckTask();
}
public void start() {
watchHandler.post(this);
}
@Override
public void run() {
mainHandler.post(scheduleCheckTask);
long endTime = System.currentTimeMillis() + CHECK_INTERVAL;
long sleepTime = endTime - System.currentTimeMillis();
while (sleepTime > 0) {
try {
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
sleepTime = endTime - System.currentTimeMillis();
}
if (scheduleCheckTask.isBlocking()) {
logImpl.Loge(TAG,"main handler blocking");
checkRealANR(mainHandler.getLooper().getThread().getStackTrace());
}
scheduleCheckTask.reset();
watchHandler.post(this);
}
private void checkRealANR(StackTraceElement[] stack) {
ThreadPool.getInstance().execute(new Runnable() {
@Override
public void run() {
ActivityManager.ProcessErrorStateInfo processErrorStateInfo = getANRInfo(app);
if (processErrorStateInfo != null) {
logImpl.Loge(TAG,"ANR action");
//real ANR
RuntimeException e = new RuntimeException(processErrorStateInfo.shortMsg);
e.setStackTrace(stack);
e.printStackTrace();
logImpl.Loge(TAG,e.getMessage());
}
}
});
}
private ActivityManager.ProcessErrorStateInfo getANRInfo(Application app) {
try {
final long sleepTime = 500L;
final long loop = 20;
long times = 0;
do {
ActivityManager activityManager = (ActivityManager) app.getSystemService(Context.ACTIVITY_SERVICE);
List<ActivityManager.ProcessErrorStateInfo> processesInErrorState = activityManager.getProcessesInErrorState();
if (processesInErrorState != null) {
for (ActivityManager.ProcessErrorStateInfo proc : processesInErrorState) {
if (proc.condition == ActivityManager.ProcessErrorStateInfo.NOT_RESPONDING) {
return proc;
}
}
}
Thread.sleep(sleepTime);
} while (times++ < loop);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
private class ScheduleCheckTask implements Runnable {
private boolean isBlocking;
ScheduleCheckTask() {
isBlocking = true;
}
@Override
public void run() {
isBlocking = false;
}
public boolean isBlocking() {
return isBlocking;
}
public void reset() {
isBlocking = true;
}
}
}
- 自定义线程WatchDog参考
package com.aispeech.util;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import com.aispeech.common.ThreadNameUtil;
import com.aispeech.lite.BaseKernel;
import java.util.Vector;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Description: 检测Kernel层 是否阻塞的工具类
* Author: junlong.huang
* CreateTime: 2023/8/21
*/
public class KernelWatchDog {
private static final String TAG = "KernelWatchDog";
HandlerThread innerThread;
Handler innerHandler;
long timeoutMillis = 2000;
static final int MSG_INCREMENT = 0x01;
private static volatile KernelWatchDog mInstance;
private ConcurrentHashMap<BaseKernel, AtomicInteger> monitorMap;
private Vector<BaseKernel> removeList;
public static KernelWatchDog getInstance() {
if (mInstance == null) {
synchronized (KernelWatchDog.class) {
if (mInstance == null) {
mInstance = new KernelWatchDog();
}
}
}
return mInstance;
}
private KernelWatchDog() {
init();
}
private void init() {
monitorMap = new ConcurrentHashMap<>();
removeList = new Vector<>();
innerThread = new HandlerThread(ThreadNameUtil.getSimpleThreadName("watchdog-k"));
innerThread.start();
innerHandler = new InnerHandler(innerThread.getLooper());
innerHandler.sendMessage(innerHandler.obtainMessage(MSG_INCREMENT));
}
public void addChecker(BaseKernel baseKernel) {
Log.i(TAG, "addChecker:" + baseKernel.getInnerThreadName());
monitorMap.put(baseKernel, new AtomicInteger(baseKernel.getTick()));
}
public void removeChecker(BaseKernel baseKernel) {
if (monitorMap.containsKey(baseKernel)) {
Log.i(TAG, "removeChecker: " + baseKernel.getInnerThreadName());
monitorMap.remove(baseKernel);
}
}
class InnerHandler extends Handler {
public InnerHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (innerHandler == null) return;
switch (msg.what) {
case MSG_INCREMENT:
if (monitorMap == null || monitorMap.size() == 0) {
innerHandler.sendMessageDelayed(innerHandler.obtainMessage(MSG_INCREMENT), timeoutMillis);
break;
}
for (BaseKernel baseKernel : monitorMap.keySet()) {
if (baseKernel.getInnerThread() != null &&
!baseKernel.getInnerThread().isAlive()) {
Log.i(TAG, "Detected thread quit,Add to list to be removed");
removeList.add(baseKernel);
continue;
}
AtomicInteger lastTick = monitorMap.get(baseKernel);
if (lastTick == null) lastTick = new AtomicInteger(baseKernel.getTick());
if (lastTick.get() != baseKernel.getTick()) {
Log.w(TAG, "Detected target thread may blocked,export thread stack");
Thread innerThread = baseKernel.getInnerThread();
if (innerThread != null) {
Log.w(TAG, getThreadStack(innerThread.getStackTrace()));
}
}
lastTick.incrementAndGet();
baseKernel.tick();
}
for (BaseKernel baseKernel : removeList) {
monitorMap.remove(baseKernel);
}
removeList.clear();
innerHandler.sendMessageDelayed(innerHandler.obtainMessage(MSG_INCREMENT), timeoutMillis);
break;
}
}
}
public void release() {
innerHandler.removeMessages(MSG_INCREMENT);
innerThread.quit();
monitorMap.clear();
removeList.clear();
}
private String getThreadStack(StackTraceElement[] elements) {
StringBuilder stackTraceString = new StringBuilder();
for (StackTraceElement element : elements) {
stackTraceString.append(element.toString()).append("\n");
}
return stackTraceString.toString();
}
}
卡顿检测
- matrix 字节码插桩,慢函数检测
- 采样率法,通过一个外置的工作线程Handler,按一段时间采样,如果大部分都是某个方法,则这个方法可能存在风险点
/**
* 按照一定频率采样
* 目标是找到卡顿时刻前后的堆栈,做大致定位,无法做到精准定位
* 原则上采样越高,定位越精准
* 还有,目前只采样了java层的堆栈,c层的需要另外实现,这个后续补充
*/
public class CallstackSampler {
private static final String TAG = "CallstackSampler";
private final Thread thread;
private final Handler mHandler;
private final long sThreshold = 1000;
private final Runnable mRunnable = new Runnable() {
@Override
public void run() {
doSample();
mHandler.postDelayed(this, sThreshold);
}
};
public CallstackSampler(Thread thread) {
this.thread = thread;
HandlerThread mWorkThread = new HandlerThread("StackSampler" + thread.getName());
mWorkThread.start();
mHandler = new Handler(mWorkThread.getLooper());
}
private void doSample() {
// 采集指定线程当前堆栈信息
StackTraceElement[] stackTrace = thread.getStackTrace();
String stackTraceString = Arrays.toString(stackTrace);
if (!stackTraceString.contains("nativePollOnce")) {
Log.d(TAG, thread.getName() + " Callstack sample taken at time: " + System.currentTimeMillis() + " " + stackTraceString);
}
}
public void startSampling() {
mHandler.postDelayed(mRunnable, sThreshold);
}
public void stopSampling() {
mHandler.removeCallbacks(mRunnable);
}
}
- 主线程耗时检测:设置一个printer
CPU 优化案例
- 线程池复用,减少CPU调度开销
- 资源拷贝优化,减少读取IO时间
- 线程命名,方便定位问题
- 非必要内容,延迟初始化
- 初始化任务优先级分配,削峰填谷