0
点赞
收藏
分享

微信扫一扫

深入探索Android卡顿优化(上)

就是耍帅 2022-04-21 阅读 125

此外,如果你需要分析Native 函数的调用,请使用Android 5.0 新增的[Simpleperf](()性能分析工具,它利用了 CPU 的性能监控单元(PMU)提供的硬件 perf 事件。使用 Simpleperf 可以看到所有的 Native 代码的耗时,对一些 Android 系统库的调用,在分析问题时有比较大的帮助,例如分析加载 dex、verify class 的耗时等等。此外,在 Android Studio 3.2 中的 Profiler 也直接支持了 Simpleper(SampleNative性能分析工具 (API Level 26+)),这更加方便了native代码的调试。

3、StrictMode

StrictMode是Android 2.3引入的一个工具类,它被称为严苛模式,是Android提供的一种运行时检测机制,可以用来帮助开发人员用来检测代码中一些不规范的问题。对于我们的项目当中,可能会成千上万行代码,如果我们用肉眼Review,这样不仅效率非常低效,而且比较容易出问题。使用StrictMode之后,系统会自动检测出来在主线程中的一些异常情况,并按照我们的配置给出相应的反应

StrictMode这个工具是非常强大的,但是我们可能因为对它不熟悉而忽略掉它。StrictMode主要用来检测两大问题:

1、线程策略

线程策略的检测内容,是一些自定义的耗时调用、磁盘读取操作以及网络请求等

2、虚拟机策略

虚拟机策略的检测内容如下:

  • Activity泄漏
  • Sqlite对象泄漏
  • 检测实例数量

StrictMode实战

如果要在应用中使用StrictMode,只需要在Applicaitoin的onCreate方法中对StrictMode进行统一配置,代码如下所示:

private void initStrictMode() { Android开源项目《ali1024.coding.net/public/P7/Android/git》
// 1、设置Debug标志位,仅仅在线下环境才使用StrictMode
if (DEV_MODE) {
// 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()
// 给NewsItem对象的实例数量限制为1
.setClassInstanceLimit(NewsItem.class, 1)
.detectLeakedClosableObjects() //API等级11
.penaltyLog()
.build());
}
}

最后,在日志输出栏中注意使用“StrictMode”关键字过滤出对应的log即可。

4、Profilo

Profilo是一个用于收集应用程序生产版本的性能跟踪的Android库。

对于Profilo来说,它集成了atrace功能,ftrace 所有的性能埋点数据都会通过 trace_marker 文件写入到内核缓冲区,Profilo 使用了 PLT Hook 拦截了写入操作,以选择部分关心的事件去做特定的分析。这样所有的 systrace 的探针我们都可以拿到,例如四大组件生命周期、锁等待时间、类校验、GC 时间等等。不过大部分的 atrace 事件都比较笼统,从事件“B|pid|activityStart”,我们无法明确知道该事件具体是由哪个 Activity 来创建的。

此外,使用Profilo还能够快速获取Java堆栈。由于获取堆栈需要暂停主线程的运行,所以profilo通过间隔发送 SIGPROF 信号这样一种类似 Native 崩溃捕捉的方式去快速获取 Java 堆栈

Profilo能够低耗时地快速获取Java堆栈的具体实现原理为当Signal Handler 捕获到信号后,它就会获取到当前正在执行的 Thread,通过 Thread 对象就可以拿到当前线程的 ManagedStack,ManagedStack 是一个单链表,它保存了当前的 ShadowFrame 或者 QuickFrame 栈指针,先依次遍历 ManagedStack 链表,然后遍历其内部的 ShadowFrame 或者 QuickFrame 还原一个可读的调用栈,从而 unwind 出当前的 Java 堆栈。关于ManagedStack与ShadowFrame、QuickFrame三者的关系如下图所示:

image

Profilo通过这种方式,就可以实现线程同步运行的同时,我们还可以去帮它做检查,并且耗时基本可以忽略不计。但是目前 Profilo 快速获取堆栈的功能不支持 Android 8.0 和 Android 9.0,并且它内部使用了Hook等大量的黑科技手段,鉴于稳定性问题,建议采取抽样部分用户的方式来开启该功能。

[Profilo项目地址](()

前面我们说过,Profilo最终也使用了ftrace,而Systrace主要也是根据Linux的ftrace机制来实现的,而ftrace的作用是帮助我们了解 Linux 内核的运行时行为,以便进行故障调试或性能分析。ftrace的整体架构如下所示:

image

由上图可知,Ftrace 有两大组成部分,一个是 framework,另外就是一系列的 tracer 。每个 tracer 用于完成不同的功能,并且它们统一由 framework 管理。 ftrace 的 trace 信息保存在 ring buffer 中,由 framework 负责管理。 Framework 利用 debugfs 系统在 /debugfs 下建立 tracing 目录,并提供了一系列的控制文件。

下面,我这里给出使用 PLTHook 技术来获取 Atrace 日志的一个项目。

1、使用profilo的PLTHook来hook libc.so的 write 与 __write_chk 方法

[使用 PLTHook 技术来获取 Atrace 的日志-项目地址](()

运行项目后,我们点击按钮开启Atrace日志,然后就可以在Logcat中看到如下的native层日志信息:

2020-02-05 10:58:00.873 13052-13052/com.dodola.atrace I/HOOOOOOOOK: =install systrace hoook====
2020-02-05 10:58:00.879 13052-13052/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|inflate
2020-02-05 10:58:00.880 13052-13052/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|LinearLayout
2020-02-05 10:58:00.881 13052-13052/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.882 13052-13052/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|TextView
2020-02-05 10:58:00.884 13052-13052/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.885 13052-13052/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.888 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|notifyFramePending
2020-02-05 10:58:00.888 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.889 13052-13052/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|Choreographer#doFrame
2020-02-05 10:58:00.889 13052-13052/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|input
2020-02-05 10:58:00.889 13052-13052/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.889 13052-13052/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|traversal
2020-02-05 10:58:00.889 13052-13052/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|draw
2020-02-05 10:58:00.890 13052-13052/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|Record View#draw()
2020-02-05 10:58:00.891 13052-13052/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.891 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|DrawFrame
2020-02-05 10:58:00.891 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|syncFrameState
2020-02-05 10:58:00.891 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|prepareTree
2020-02-05 10:58:00.891 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.891 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.891 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|query
2020-02-05 10:58:00.891 13052-13052/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.891 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.891 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|query
2020-02-05 10:58:00.891 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.892 13052-13052/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.892 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|query
2020-02-05 10:58:00.892 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.892 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|query
2020-02-05 10:58:00.892 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.892 13052-13052/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.892 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|query
2020-02-05 10:58:00.892 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.892 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|query
2020-02-05 10:58:00.892 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.892 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|setBuffersDimensions
2020-02-05 10:58:00.892 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.892 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|dequeueBuffer
2020-02-05 10:58:00.894 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|importBuffer
2020-02-05 10:58:00.894 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|HIDL::IMapper::importBuffer::passthrough
2020-02-05 10:58:00.894 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.894 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.894 13052-13058/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|Compiling
2020-02-05 10:58:00.894 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.894 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|query

需要注意的是,日志中的B代表begin,也就是对应时间开始的时间,而E代表End,即对应事件结束的时间,并且,B|事件和E|事件是成对出现的,这样我们就可以通过该事件的结束时间减去对应的开始时间来获得每个事件使用的时间。例如,上述log中我们可以看出TextView的draw方法显示使用了3ms。

此外,在下面这个项目里展示了如何使用 PLTHook 技术来获取线程创建的堆栈。

2、使用PLTHook技术来获取线程创建的堆栈

[使用 PLTHook 技术来获取线程创建的堆栈-项目地址](()

运行项目后,我们点击开启 Thread Hook按钮,然后点击新建 Thread按钮。最后可以在Logcat 中看到Thread创建的堆栈信息:

2020-02-05 13:47:59.006 20159-20159/com.dodola.thread E/HOOOOOOOOK: stack:com.dodola.thread.ThreadHook.getStack(ThreadHook.java:16)
com.dodola.thread.MainActivity 2. o n C l i c k ( M a i n A c t i v i t y . j a v a : 40 ) a n d r o i d . v i e w . V i e w . p e r f o r m C l i c k ( V i e w . j a v a : 6311 ) a n d r o i d . v i e w . V i e w 2.onClick(MainActivity.java:40) android.view.View.performClick(View.java:6311) android.view.View 2.onClick(MainActivity.java:40)android.view.View.performClick(View.java:6311)android.view.ViewPerformClick.run(View.java:24833)
android.os.Handler.handleCallback(Handler.java:794)
android.os.Handler.dispatchMessage(Handler.java:99)
android.os.Looper.loop(Looper.java:173)
android.app.ActivityThread.main(ActivityThread.java:6653)
java.lang.reflect.Method.invoke(Native Method)
com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547)
com.android.internal.os.ZygoteInit.main(ZygoteInit.java:821)
2020-02-05 13:47:59.007 20159-20339/com.dodola.thread E/HOOOOOOOOK: thread name:Thread-2
2020-02-05 13:47:59.008 20159-20339/com.dodola.thread E/HOOOOOOOOK: thread id:1057
2020-02-05 13:47:59.009 20159-20339/com.dodola.thread E/HOOOOOOOOK: stack:com.dodola.thread.ThreadHook.getStack(ThreadHook.java:16)
com.dodola.thread.MainActivity$2$1.run(MainActivity.java:38)
2020-02-05 13:47:59.011 20159-20340/com.dodola.thread E/HOOOOOOOOK: inner thread name:Thread-3
2020-02-05 13:47:59.012 20159-20340/com.dodola.thread E/HOOOOOOOOK: inner thread id:1058

由于Profilo与PLT Hook涉及了大量的C/C++、NDK开发的知识,限于篇幅,所以这部分不做详细讲解,如对NDK开发感兴趣的同学可以期待下我后面的[Awesome-Android-NDK系列文章]((),等性能优化系列文章更新完毕之后,就会开始去系统地学习NDK相关的开发知识,敬请期待。

二、自动化卡顿检测方案及优化

1、为什么需要自动化卡顿检测方案?

主要有以下两点原因:

  • 1、Cpu Profiler、Systrace等系统工具仅适合线下针对性分析。
  • 2、线上及测试环境需要自动化的卡顿检方案来定位卡顿,同时,更重要的是,它能记录卡顿发生时的场景。

2、卡顿检测方案原理

它的原理源于Android的消息处理机制,一个线程不管有多少Handler,它只会有一个Looper存在,主线程执行的任何代码都会通过Looper.loop()方法执行。而在Looper函数中,它有一个mLogging对象,这个对象在每个message处理前后都会被调用。主线程发生了卡顿,那一定是在dispatchMessage()方法中执行了耗时操作。那么,我们就可以通过这个mLogging对象对dispatchMessage()进行监控

卡顿检测方案的具体实现步骤

首先,我们看下Looper用于执行消息循环的loop()方法,关键代码如下所示:

/**

  • Run the message queue in this thread. Be sure to call
  • {@link #quit()} to end the loop.
    */
    public static void loop() {

for (;😉 {
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}

// This must be in a local variable, in case a UI event sets the logger
final Printer logging = me.mLogging;
if (logging != null) {
// 1
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}

try {
// 2
msg.target.dispatchMessage(msg);
dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
} finally {
if (traceTag != 0) {
Trace.traceEnd(traceTag);
}
}

if (logging != null) {
// 3
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}

在Looper的loop()方法中,在其执行每一个消息(注释2处)的前后都由logging进行了一次打印输出。可以看到,在执行消息前是输出的">>>>> Dispatching to “,在执行消息后是输出的”<<<<< Finished to ",它们打印的日志是不一样的,我们就可以由此来判断消息执行的前后时间点。

所以,具体的实现可以归纳为如下步骤:

  • 1、首先,我们需要使用Looper.getMainLooper().setMessageLogging()去设置我们自己的Printer实现类去打印输出logging。这样,在每个message执行的之前和之后都会调用我们设置的这个Printer实现类。
  • 2、如果我们匹配到">>>>> Dispatching to "之后,我们就可以执行一行代码:也就是在指定的时间阈值之后,我们在子线程去执行一个任务,这个任务就是去获取当前主线程的堆栈信息以及当前的一些场景信息,比如:内存大小、电脑、网络状态等。
  • 3、如果在指定的阈值之内匹配到了"<<<<< Finished to ",那么说明message就被执行完成了,则表明此时没有产生我们认为的卡顿效果,那我们就可以将这个子线程任务取消掉。

3、AndroidPerformanceMonitor

它是一个非侵入式的性能监控组件,可以通过通知的形式弹出卡顿信息。它的原理就是我们刚刚讲述到的卡顿监控的实现原理。

接下我们通过一个简单的示例来讲解一下它的使用。

首先,我们需要在moudle的build.gradle下配置它的依赖,如下所示:

// release:项目中实现了线上监控体系的时候去使用
api ‘com.github.markzhai:blockcanary-android:1.5.0’

// 仅在debug包启用BlockCanary进行卡顿监控和提示的话,可以这么用
debugApi ‘com.github.markzhai:blockcanary-android:1.5.0’
releaseApi ‘com.github.markzhai:blockcanary-no-op:1.5.0’

其次,在Application的onCreate方法中开启卡顿监控:

// 注意在主进程初始化调用
BlockCanary.install(this, new AppBlockCanaryContext()).start();

最后,继承BlockCanaryContext类去实现自己的监控配置上下文类:

public class AppBlockCanaryContext extends BlockCanaryContext {
// 实现各种上下文,包括应用标识符,用户uid,网络类型,卡顿判断阙值,Log保存位置等等

/**

  • 提供应用的标识符
  • @return 标识符能够在安装的时候被指定,建议为 version + flavor.
    */
    public String provideQualifier() {
    return “unknown”;
    }

/**

  • 提供用户uid,以便在上报时能够将对应的
  • 用户信息上报至服务器
  • @return user id
    */
    public String provideUid() {
    return “uid”;
    }

/**

  • 提供当前的网络类型
  • @return {@link String} like 2G, 3G, 4G, wifi, etc.
    */
    public String provideNetworkType() {
    return “unknown”;
    }

/**

  • 配置监控的时间区间,超过这个时间区间 ,BlockCanary将会停止, use
  • with {@code BlockCanary}'s isMonitorDurationEnd
  • @return monitor last duration (in hour)
    */
    public int provideMonitorDuration() {
    return -1;
    }

/**

  • 指定判定为卡顿的阈值threshold (in millis),
  • 你可以根据不同设备的性能去指定不同的阈值
  • @return threshold in mills
    */
    public int provideBlockThreshold() {
    return 1000;
    }

/**

  • 设置线程堆栈dump的间隔, 当阻塞发生的时候使用, BlockCanary 将会根据
  • 当前的循环周期在主线程去dump堆栈信息
  • 由于依赖于Looper的实现机制, 真实的dump周期
  • 将会比设定的dump间隔要长(尤其是当CPU很繁忙的时候).
  • @return dump interval (in millis)
    */
    public int provideDumpInterval() {
    return provideBlockThreshold();
    }

/**

  • 保存log的路径, 比如 “/blockcanary/”, 如果权限允许的话,
  • 会保存在本地sd卡中
  • @return path of log files
    */
    public String providePath() {
    return “/blockcanary/”;
    }

/**

  • 是否需要通知去通知用户发生阻塞
  • @return true if need, else if not need.
    */
    public boolean displayNotification() {
    return true;
    }

/**

  • 用于将多个文件压缩为一个.zip文件
  • @param src files before compress
  • @param dest files compressed
  • @return true if compression is successful
    */
    public boolean zip(File[] src, File dest) {
    return false;
    }

/**

  • 用于将已经被压缩好的.zip log文件上传至
  • APM后台
  • @param zippedFile zipped file
    */
    public void upload(File zippedFile) {
    throw new UnsupportedOperationException();
    }

/**

  • 用于设定包名, 默认使用进程名,
  • @return null if simply concern only package with process name.
    */
    public List concernPackages() {
    return null;
    }

/**

  • 使用 @{code concernPackages}方法指定过滤的堆栈信息
  • @return true if filter, false it not.
    */
    public boolean filterNonConcernStack() {
    return false;
    }

/**

  • 指定一个白名单, 在白名单的条目将不会出现在展示阻塞信息的UI中
  • @return return null if you don’t need white-list filter.
    */
    public List provideWhiteList() {
    LinkedList whiteList = new LinkedList<>();
    whiteList.add(“org.chromium”);
    return whiteList;
    }

/**

  • 使用白名单的时候,是否去删除堆栈在白名单中的文件
  • @return true if delete, false it not.
    */
    public boolean deleteFilesInWhiteList() {
    return true;
    }

/**

  • 阻塞拦截器, 我们可以指定发生阻塞时应该做的工作
    */
    public void onBlock(Context context, BlockInfo blockInfo) {

}
}

可以看到,在上述配置中,我们指定了卡顿的阈值为1000ms。接下来,我们可以测试一下BlockCanary监测卡顿时的效果,这里我在Activity的onCreate方法中添加如下代码使线程休眠3s:

try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}

然后,我们运行项目,打开App,即可看到类似LeakCanary界面那样的卡顿信息堆栈。

除了发生卡顿时BlockCanary提供的图形界面可供开发和测试人员直接查看卡顿原因之外。其最大的作用还是在线上环境或者自动化monkey测试的环节进行大范围的log采集与分析,对于分析的纬度,可以从以下两个纬度来进行:

  • 卡顿时间。
  • 根据同堆栈出现的卡顿次数来进行排序和归类。

BlockCanary的优势如下

  • 非侵入式。
  • 方便精准,能够定位到代码的某一行代码。

那么这种自动检测卡顿的方案有什么问题吗?

在卡顿的周期之内,应用确实发生了卡顿,但是获取到的卡顿信息可能会不准确,和我们的OOM一样,也就是最后的堆栈信息仅仅只是一个表象,并不是真正发生问题时的一个堆栈。下面,我们先看下如下的一个示意图:

image

假设主线程在T1到T2的时间段内发生了卡顿,卡顿检测方案获取卡顿时的堆栈信息是T2时刻,但是实际上发生卡顿的时刻可能是在这段时间区域内另一个耗时过长的函数,那么可能在我们捕获卡顿的时刻时,真正的卡顿时机已经执行完成了,所以在T2时刻捕获到的一个卡顿信息并不能够反映卡顿的现场,也就是最后呈现出来的堆栈信息仅仅只是一个表象,并不是真正问题的藏身之处。

那么,我们如何对这种情况进行优化呢?

我们可以获取卡顿周期内的多个堆栈,而不仅仅是最后一个,这样的话,如果发生了卡顿,我们就可以根据这些堆栈信息来清晰地还原整个卡顿现场。因为我们有卡顿现场的多个堆栈信息,我们完全知道卡顿时究竟发生了什么,到底哪些函数它的调用时间比较长。接下来,我们看看下面的卡顿检测优化流程图:

image

根据图中,可以梳理出优化后的具体实现步骤为:

  • 1、首先,我们会通过startMonitor方法对这个过程进行监控。
  • 2、接着,我们就开始高频采集堆栈信息。如果发生了卡顿,我们就会调用endMonitor方法
  • 3、然后,将之前我们采集的多 《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》开源 个堆栈信息记录到文件中。
  • 4、最后,在合适的时机上报给我们的服务器。

通过上述的优化,我们就可以知道在整个卡顿周期之内,究竟是哪些方法在执行,哪些方法比较耗时。

但是这种海量卡顿堆栈的处理又存在着另一个问题,那就是高频卡顿上报量太大,服务器压力较大,这里我们来分析下如何减少服务端对堆栈信息的处理量

在出现卡顿的情况下,我们采集到了多个堆栈,大概率的情况下,可能会存在多个重复的堆栈,而这个重复的堆栈信息才是我们应该关注的地方。我们可以对一个卡顿下的堆栈进行能hash排重,找出重复的堆栈。这样,服务器需要处理的数据量就会大大减少,同时也过滤出了我们需要重点关注的对象。对于开发人员来说,就能更快地找到卡顿的原因。

4、小结

在本节中,我们学习了自动化卡顿检测的原理,然后,我们使用这种方案进行了实战,最后,我还介绍了这种方案的问题和它的优化思路。

三、总结

在本篇文章中,我们主要对卡顿优化分析方法与工具 、自动化卡顿检测方案及优化相关的知识进行了全面且深入地讲解,这里再简单总结一下本篇文章涉及的两大主题:

  • 1、卡顿优化分析方法与工具:背景介绍、卡顿分析方法之使用shell命令分析CPU耗时、卡顿优化工具。
  • 2、自动化卡顿检测方案及优化:卡顿检测方案原理、AndroidPerformanceMonitor实战及其优化。

下篇,笔者将带领大家更加深入地去学习卡顿优化的相关知识,敬请期待~

参考链接:

1、[国内Top团队大牛带你玩转Android性能分析与优化 第6章 卡顿优化](()


,我们学习了自动化卡顿检测的原理,然后,我们使用这种方案进行了实战,最后,我还介绍了这种方案的问题和它的优化思路。

三、总结

在本篇文章中,我们主要对卡顿优化分析方法与工具 、自动化卡顿检测方案及优化相关的知识进行了全面且深入地讲解,这里再简单总结一下本篇文章涉及的两大主题:

  • 1、卡顿优化分析方法与工具:背景介绍、卡顿分析方法之使用shell命令分析CPU耗时、卡顿优化工具。
  • 2、自动化卡顿检测方案及优化:卡顿检测方案原理、AndroidPerformanceMonitor实战及其优化。

下篇,笔者将带领大家更加深入地去学习卡顿优化的相关知识,敬请期待~

参考链接:

1、[国内Top团队大牛带你玩转Android性能分析与优化 第6章 卡顿优化](()

[外链图片转存中…(img-EPvOP9om-1650450662171)]

举报

相关推荐

0 条评论