如果App的FPS平均值小于30,最小值小于24,即表明应用发生了卡顿。
adb shell
// 获取 CPU 核心数
cat /sys/devices/system/cpu/possible
// 获取第一个 CPU 的最大频率
cat /sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq <
// 获取第二个CPU的最小频率
cat /sys/devices/system/cpu/cpu1/cpufreq/cpuinfo_min_freq <
//整个系统的 CPU 使用情况
cat /proc/[pid]/stat
top 命令可以帮助我们查看哪个进程是 CPU 的消耗大户;
vmstat 命令可以实时动态监视操作系统的虚拟内存和 CPU 活动;
strace 命令可以跟踪某个进程中所有的系统调用
vmstat命令或者/proc/[pid]/schedstat文件来查看 CPU 上下文切换次数
/proc/[pid]/stat // 进程CPU使用情况
/proc/[pid]/task/[tid]/stat // 进程下面各个线程的CPU使用情况
/proc/[pid]/sched // 进程CPU调度相关
/proc/loadavg // 系统平均负载,uptime命令对应文件
Traceview
Nanoscope
systrace
Simpleperf
private void initStrictMode() {
// 1、设置Debug标志位,仅仅在线下环境才使用StrictMode
if (BuildConfig.isDebug) {
// 2、设置线程策略
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectCustomSlowCalls() //API等级11,使用StrictMode.noteSlowCode
.detectDiskReads()
.detectDiskWrites()
.detectNetwork() // or .detectAll() for all detectable problems
.penaltyLog() //在Logcat 中打印违规异常信息
//.penaltyDialog() //也可以直接跳出警报dialog
//.penaltyDeath() //或者直接崩溃
.build());
// 3、设置虚拟机策略
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
// 给Person对象的实例数量限制为1
.setClassInstanceLimit(Person.class, 1)
.detectLeakedClosableObjects() //API等级11
.penaltyLog()
.build());
}
}
卡顿监控
假设一个消息循环里面顺序执行了 A、B、C 三个函数,当整个消息执行超过 3 秒时,因为函数 A 和 B 已经执行完毕,
我们只能得到的正在执行的函数 C 的堆栈,事实上它可能并不耗时,不过对于线上大数据来说,因为函数 A 和 B 相对
比较耗时,所以抓取到它们的概率会更大一些,通过后台聚合后捕获到函数 A 和 B 的卡顿日志会更多一些;
如果跟 Traceview 一样,可以拿到整个卡顿过程所有运行函数的耗时,就可以明确知道其实函数 A 和 B 才是造成卡顿的主要原因;
那能否利用 Android Runtime 函数调用的回调事件,做一个自定义的 Traceview++ 呢?
ftrace 所有性能埋点数据都会通过 trace_marker 文件写入内核缓冲区,Profilo 通过
PLT Hook 拦截了写入操作,选择部分关心的事件做分析。这样所有 systrace 的探针我们
都可以拿到,例如四大组件生命周期、锁等待时间、类校验、GC 时间等。
//1. build.gradle下配置它的依赖
api 'com.github.markzhai:blockcanary-android:1.5.0'
// 仅在debug包启用BlockCanary进行卡顿监控和提示的话,可以这么用
debugApi 'com.github.markzhai:blockcanary-android:1.5.0'
//2. Application的onCreate方法中开启卡顿监控
BlockCanary.install(this, new AppBlockCanaryContext()).start();
//3.继承BlockCanaryContext类去实现自己的监控配置上下文类
/**
* @Author: LiuJinYang
* @CreateDate: 2020/12/9
*/
public class AppBlockCanaryContext extends BlockCanaryContext {
// 实现各种上下文,包括应用标识符,用户uid,网络类型,卡顿判断阙值,Log保存位置等等
/**
* 提供应用的标识符
*
* @return 标识符能够在安装的时候被指定,建议为 version + flavor.
*/
@Override
public String provideQualifier() {
return "unknown";
}
/**
* 提供用户uid,以便在上报时能够将对应的
* 用户信息上报至服务器
*
* @return user id
*/
@Override
public String provideUid() {
return "uid";
}
/**
* 提供当前的网络类型
*
* @return {@link String} like 2G, 3G, 4G, wifi, etc.
*/
@Override
public String provideNetworkType() {
return "unknown";
}
/**
* 配置监控的时间区间,超过这个时间区间 ,BlockCanary将会停止, use
* with {@code BlockCanary}'s isMonitorDurationEnd
*
* @return monitor last duration (in hour)
*/
@Override
public int provideMonitorDuration() {
return -1;
}
/**
* 指定判定为卡顿的阈值threshold (in millis),
* 你可以根据不同设备的性能去指定不同的阈值
*
* @return threshold in mills
*/
@Override
public int provideBlockThreshold() {
return 1000;
}
/**
* 设置线程堆栈dump的间隔, 当阻塞发生的时候使用, BlockCanary 将会根据
* 当前的循环周期在主线程去dump堆栈信息
*
* 由于依赖于Looper的实现机制, 真实的dump周期
* 将会比设定的dump间隔要长(尤其是当CPU很繁忙的时候).
*
*
* @return dump interval (in millis)
*/
@Override
public int provideDumpInterval() {
return provideBlockThreshold();
}
/**
* 保存log的路径, 比如 "/blockcanary/", 如果权限允许的话,
* 会保存在本地sd卡中
*
* @return path of log files
*/
@Override
public String providePath() {
return "/blockcanary/";
}
/**
* 是否需要通知去通知用户发生阻塞
*
* @return true if need, else if not need.
*/
@Override
public boolean displayNotification() {
return true;
}
/**
* 用于将多个文件压缩为一个.zip文件
*
* @param src files before compress
* @param dest files compressed
* @return true if compression is successful
*/
@Override
public boolean zip(File[] src, File dest) {
return false;
}
/**
* 用于将已经被压缩好的.zip log文件上传至
* APM后台
*
* @param zippedFile zipped file
*/
@Override
public void upload(File zippedFile) {
throw new UnsupportedOperationException();
}
/**
* 用于设定包名, 默认使用进程名,
*
* @return null if simply concern only package with process name.
*/
@Override
public List concernPackages() {
return null;
}
/**
* 使用 @{code concernPackages}方法指定过滤的堆栈信息
*
* @return true if filter, false it not.
*/
@Override
public boolean filterNonConcernStack() {
return false;
}
/**
* 指定一个白名单, 在白名单的条目将不会出现在展示阻塞信息的UI中
*
* @return return null if you don't need white-list filter.
*/
@Override
public List provideWhiteList() {
LinkedList whiteList = new LinkedList<>();
whiteList.add("org.chromium");
return whiteList;
}
/**
* 使用白名单的时候,是否去删除堆栈在白名单中的文件
*
* @return true if delete, false it not.
*/
@Override
public boolean deleteFilesInWhiteList() {
return true;
}
/**
* 阻塞拦截器, 我们可以指定发生阻塞时应该做的工作
*/
@Override
public void onBlock(Context context, BlockInfo blockInfo) {
}
}
其他监控
// 监听界面是否存在绘制行为
getWindow().getDecorView().getViewTreeObserver().addOnDrawListener
//WAITING、TIME_WAITING 和 BLOCKED 都是需要特别注意的状态;
//BLOCKED 是指线程正在等待获取锁,对应的是下面代码中的情况一;
//WAITING 是指线程正在等待其他线程的“唤醒动作”,对应的是代码中的情况二;
synchronized (object) { // 情况一:在这里卡住 --> BLOCKED
doSomething();
object.wait(); // 情况二:在这里卡住 --> WAITING
}
//不过当一个线程进入 WAITING 状态时,它不仅会释放 CPU 资源,还会将持有的 object 锁也同时释放。
"BackgroundHandler" RUNNABLE
at android.content.res.AssetManager.list
at com.sample.business.init.listZipFiles
//通过查看AssetManager.list的确发现是使用了同一个 synchronized 锁,而 list 函数需要遍历整个目录,耗时会比较久
public String[] list(String path) throws IOException {
synchronized (this) {
ensureValidLocked();
return nativeList(mObject, path);
}
}
//另外一方面,“BackgroundHandler”线程属于低优先级后台线程,这也是我们前面文章提到的不良现象,也就是主线程等待低优先级的后台线程
// 线程名称; 优先级; 线程id; 线程状态
"main" prio=5 tid=1 Suspended
// 线程组; 线程suspend计数; 线程debug suspend计数;
| group="main" sCount=1 dsCount=0 obj=0x74746000 self=0xf4827400
// 线程native id; 进程优先级; 调度者优先级;
| sysTid=28661 nice=-4 cgrp=default sched=0/0 handle=0xf72cbbec
// native线程状态; 调度者状态; 用户时间utime; 系统时间stime; 调度的CPU
| state=D schedstat=( 3137222937 94427228 5819 ) utm=218 stm=95 core=2 HZ=100
// stack相关信息
| stack=0xff717000-0xff719000 stackSize=8MB
上面的 ANR 日志中“main”线程的状态是 Suspended,Java 线程中的 6 种状态中并不存在 Suspended 状态啊?
事实上,Suspended 代表的是 Native 线程状态。怎么理解呢?在 Android 里面 Java 线程的运行都委托于一个
Linux 标准线程 pthread 来运行,而 Android 里运行的线程可以分成两种,一种是 Attach 到虚拟机的,一种是
没有 Attach 到虚拟机的,在虚拟机管理的线程都是托管的线程,所以本质上 Java 线程的状态其实是 Native 线程
的一种映射。不同的 Android 版本 Native 线程的状态不太一样,例如 Android 9.0 就定义了 27 种线程状态,
它能更加明确地区分线程当前所处的情况。
// 堆栈相关信息
at android.content.res.AssetManager.open(AssetManager.java:311)
- waiting to lock <0x41ddc798> (android.content.res.AssetManager) held by tid=66 (BackgroundHandler)
at android.content.res.AssetManager.open(AssetManager.java:289)
//1. build.gradle下配置它的依赖
implementation 'com.github.anrwatchdog:anrwatchdog:1.4.0'
//2. Application的onCreate方法中初始化ANR-WatchDog
new ANRWatchDog().start();
//3.源码:ANRWatchDog实际上是继承了Thread类,也就是它是一个线程,对于线程来说,最重要的就是其run方法
private static final int DEFAULT_ANR_TIMEOUT = 5000;
private volatile long _tick = 0;
private volatile boolean _reported = false;
private final Runnable _ticker = new Runnable() {
@Override public void run() {
_tick = 0;
_reported = false;
}
};
@Override
public void run() {
// 1、首先,将线程命名为|ANR-WatchDog|。
setName("|ANR-WatchDog|");
// 2、接着,声明了一个默认的超时间隔时间,默认的值为5000ms。
long interval = _timeoutInterval;
// 3、然后,在while循环中通过_uiHandler去post一个_ticker Runnable。
while (!isInterrupted()) {
// 3.1 这里的_tick默认是0,所以needPost即为true。
boolean needPost = _tick == 0;
// 这里的_tick加上了默认的5000ms
_tick += interval;
if (needPost) {
_uiHandler.post(_ticker);
}
// 接下来,线程会sleep一段时间,默认值为5000ms。
try {
Thread.sleep(interval);
} catch (InterruptedException e) {
_interruptionListener.onInterrupted(e);
return ;
}
// 4、如果主线程没有处理Runnable,即_tick的值没有被赋值为0,则说明发生了ANR,第二个_reported标志位是为了避免重复报道已经处理过的ANR。
if (_tick != 0 && !_reported) {
//noinspection ConstantConditions
if (!_ignoreDebugger && (Debug.isDebuggerConnected() || Debug.waitingForDebugger())) {
Log.w("ANRWatchdog", "An ANR was detected but ignored because the debugger is connected (you can prevent this with setIgnoreDebugger(true))");
_reported = true;
continue ;
}
interval = _anrInterceptor.intercept(_tick);
if (interval > 0) {
continue;
}
final ANRError error;
if (_namePrefix != null) {
error = ANRError.New(_tick, _namePrefix, _logThreadsWithoutStackTrace);
} else {
// 5、如果没有主动给ANR_Watchdog设置线程名,则会默认会使用ANRError的NewMainOnly方法去处理ANR。
error = ANRError.NewMainOnly(_tick);
}
// 6、最后会通过ANRListener调用它的onAppNotResponding方法,其默认的处理会直接抛出当前的ANRError,导致程序崩溃。 _anrListener.onAppNotResponding(error);
interval = _timeoutInterval;
_reported = true;
}
}
}
//但是在Java层去获取所有线程堆栈以及各种信息非常耗时,对于卡顿场景不一定合适,它可能会进一步加剧用户的卡顿。
如果是对性能要求比较高的应用,可以通过Hook Native层的方式去获得所有线程的堆栈信息,参考上面“方案3:Hook实现”
// 1、对IPC操作开始监控
adb shell am trace-ipc start
// 2、结束IPC操作的监控,同时,将监控到的信息存放到指定的文件
adb shell am trace-ipc stop -dump-file /data/local/tmp/ipc-trace.txt
// 3、将监控到的ipc-trace导出到电脑查看
adb pull /data/local/tmp/ipc-trace.txt
//通过PackageManager去拿到我们应用的一些信息,或者去拿到设备的DeviceId这样的信息以及AMS相关的信息等,最终会调用到android.os.BinderProxy
//在项目中的Application的onCreate方法中使用ARTHook对android.os.BinderProxy类的transact方法进行Hook
try {
DexposedBridge.findAndHookMethod(Class.forName("android.os.BinderProxy"), "transact",
int.class, Parcel.class, Parcel.class, int.class, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
LogHelper.i( "BinderProxy beforeHookedMethod " + param.thisObject.getClass().getSimpleName()
+ "\n" + Log.getStackTraceString(new Throwable()));
super.beforeHookedMethod(param);
}
});
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
//1. 在根目录的 build.gradle 添加:
dependencies {
classpath 'me.ele:lancet-plugin:1.0.6'
}
//2. 在 app 目录的'build.gradle' 添加
apply plugin: 'me.ele.lancet'
dependencies {
provided 'me.ele:lancet-base:1.0.6'
}
//3. 基础API使用
/**
* @Author: LiuJinYang
* @CreateDate: 2020/12/10
*/
public class LancetUtil {
//@Proxy 指定了将要被织入代码目标方法i, 织入方式为Proxy(将使用新的方法替换代码里存在的原有的目标方法)
@Proxy("i")
//TargetClass指定了将要被织入代码目标类 android.util.Log
@TargetClass("android.util.Log")
public static int anyName(String tag, String msg){
msg = "LJY_LOG: "+msg ;
//Origin.call() 代表了 Log.i() 这个目标方法
return (int) Origin.call();
}
}
//4. 统计界面耗时
/**
* @Author: LiuJinYang
* @CreateDate: 2020/12/10
*/
public class LancetUtil {
public static ActivityRecord sActivityRecord;
static {
sActivityRecord = new ActivityRecord();
}
@Insert(value = "onCreate",mayCreateSuper = true)
@TargetClass(value = "android.support.v7.app.AppCompatActivity",scope = Scope.ALL)
protected void onCreate(Bundle savedInstanceState) {
sActivityRecord.mOnCreateTime = System.currentTimeMillis();
// 调用当前Hook类方法中原先的逻辑
Origin.callVoid();
}
@Insert(value = "onWindowFocusChanged",mayCreateSuper = true)
@TargetClass(value = "android.support.v7.app.AppCompatActivity",scope = Scope.ALL)
public void onWindowFocusChanged(boolean hasFocus) {
sActivityRecord.mOnWindowsFocusChangedTime = System.currentTimeMillis();
LjyLogUtil.i(getClass().getCanonicalName() + " onWindowFocusChanged cost "+(sActivityRecord.mOnWindowsFocusChangedTime - sActivityRecord.mOnCreateTime));
Origin.callVoid();
}
public static class ActivityRecord {
/**
* 避免没有仅执行onResume就去统计界面打开速度的情况,如息屏、亮屏等等
*/
public boolean isNewCreate;
public long mOnCreateTime;
public long mOnWindowsFocusChangedTime;
}
}