Android 关于提高性能方面(卡顿)

主要有以下三方面:

 1.业务/功能

 2.符合逻辑的交互

 3.优秀的性能

Android 系统作为以移动设备为主的操作系统,硬件配置是有一定的限制的,虽然配置现在越来越高级,但仍然无法与 PC 相比,在 CPU 和内存上使用不合理或者耗费资源多时,就会碰到内存不足导致的稳定性问题、CPU 消耗太多导致的卡顿问题等。

具体的性能问题总结:

户体验的性能问题:

 1.流畅

 2.稳定

 3.省电、省流量

 4.安装包小

 快:使用时避免出现卡顿,响应速度快,减少用户等待的时间,满足用户期望。

 稳:减低 crash 率和 ANR 率,不要在用户使用过程中崩溃和无响应。

 省:节省流量和耗电,减少用户使用成本,避免使用时导致手机发烫。

 小:安装包小可以降低用户的安装成本。


要想达到这4个目标,具体实现是在右边框里的问题:卡顿、内存使用不合理、代码质量差、代码逻辑乱、安装包过大,这些问题也是在开发过程中碰到最多的问题,在实现业务需求同时,也需要考虑到这点,多花时间去思考,如何避免功能完成后再来做优化,不然的话等功能实现后带来的维护成本会增加。

卡顿上的优化:
UI 绘制、应用启动、页面跳转、事件响应

这4种卡顿场景的根本原因可以分为两大类:

 1.界面绘制。主要原因是绘制的层级深、页面复杂、刷新不合理,由于这些原因导致卡顿的场景更多出现在 UI 和启动后的初始界面以及跳转到页面的绘制上。

 2.数据处理。导致这种卡顿场景的原因是数据处理量太大,一般分为三种情况,一是数据在处理 UI 线程,二是数据处理占用 CPU 高,导致主线程拿不到时间片,三是内存增加导致 GC 频繁,从而引起卡顿。

造成卡顿的根本原因:

根据Android 系统显示原理可以看到,影响绘制的根本原因有以下两个方面:

 1.绘制任务太重,绘制一帧内容耗时太长。

 2.主线程太忙,根据系统传递过来的 VSYNC 信号来时还没准备好数据导致丢帧。

绘制耗时太长,有一些工具可以帮助我们定位问题。主线程太忙则需要注意了,主线程关键职责是处理用户交互,在屏幕上绘制像素,并进行加载显示相关的数据,所以特别需要避免任何主线程的事情,这样应用程序才能保持对用户操作的即时响应。总结起来,主线程主要做以下几个方面工作:

  • 1.UI 生命周期控制

  • 2.系统事件处理

  • 3.消息处理

  • 4.界面布局

  • 5.界面绘制

  • 6.界面刷新

除此之外,应该尽量避免将其他处理放在主线程中,特别复杂的数据计算和网络请求等。

解决卡顿的优化:

1,布局优化

布局是否合理主要影响的是页面测量时间的多少,我们知道一个页面的显示测量和绘制过程都是通过递归来完成的,多叉树遍历的时间与树的高度h有关,其时间复杂度 O(h),如果层级太深,每增加一层则会增加更多的页面显示时间,所以布局的合理性就显得很重要。

那布局优化有哪些方法呢,主要通过减少层级、减少测量和绘制时间、提高复用性三个方面入手。

总结如下:

  • 1.减少层级。合理使用 RelativeLayout 和 LinerLayout,合理使用Merge。

  • 2.提高显示速度。使用 ViewStub,它是一个看不见的、不占布局位置、占用资源非常小的视图对象。

  • 3.布局复用。可以通过<include> 标签来提高复用。

  • 4.尽可能少用wrap_content。wrap_content 会增加布局 measure 时计算成本,在已知宽高为固定值时,不用wrap_content 。

  • 5.删除控件中无用的属性。

2,避免过度绘制

过度绘制是指在屏幕上的某个像素在同一帧的时间内被绘制了多次。在多层次重叠的 UI 结构中,如果不可见的 UI 也在做绘制的操作,就会导致某些像素区域被绘制了多次,从而浪费了多余的 CPU 以及 GPU 资源。

如何避免过度绘制呢,如下:

  • 1.布局上的优化。移除 XML 中非必须的背景,移除 Window 默认的背景、按需显示占位背景图片

  • 2.自定义View优化。使用 canvas.clipRect()来帮助系统识别那些可见的区域,只有在这个区域内才会被绘制。

3,启动优化

通过对启动速度的监控,发现影响启动速度的问题所在,优化启动逻辑,提高应用的启动速度。启动主要完成三件事:UI 布局、绘制和数据准备。因此启动速度优化就是需要优化这三个过程:

  • 1.UI 布局。应用一般都有闪屏页,优化闪屏页的 UI 布局,可以通过 Profile GPU Rendering 检测丢帧情况。

  • 2.启动加载逻辑优化。可以采用分布加载、异步加载、延期加载策略来提高应用启动速度。

  • 3.数据准备。数据初始化分析,加载数据可以考虑用线程初始化等策略。

4,合理的刷新机制

在应用开发过程中,因为数据的变化,需要刷新页面来展示新的数据,但频繁刷新会增加资源开销,并且可能导致卡顿发生,因此,需要一个合理的刷新机制来提高整体的 UI 流畅度。合理的刷新需要注意以下几点:

  • 1.尽量减少刷新次数。

  • 2.尽量避免后台有高的 CPU 线程运行。

  • 3.缩小刷新区域。

5,其他

在实现动画效果时,需要根据不同场景选择合适的动画框架来实现。有些情况下,可以用硬件加速方式来提供流畅度。

1、ANR分析与实战

一、ANR介绍与实战

首先,咱们再来回顾一下ANR的几种常见的类型,以下所示:linux

  • 一、KeyDispatchTimeout:按键事件在5s的时间内没有处理完成。
  • 二、BroadcastTimeout:广播接收器在前台10s,后台60s的时间内没有响应完成。
  • 三、ServiceTimeout:服务在前台20s,后台200s的时间内没有处理完成。

具体的时间定义咱们能够在AMS(ActivityManagerService)中找到:android

// How long we allow a receiver to run before giving up on it.
static final int BROADCAST_FG_TIMEOUT = 10*1000;
static final int BROADCAST_BG_TIMEOUT = 60*1000;

// How long we wait until we timeout on key dispatching.
static final int KEY_DISPATCHING_TIMEOUT = 5*1000;
复制代码

接下来,咱们来看一下ANR的执行流程。git

ANR执行流程

  • 一、首先,咱们的应用发生了ANR。
  • 二、而后,咱们的进程就会接收到异常终止信息,并开始写入进程ANR信息,也就是当时应用的场景信息,它包含了应用全部的堆栈信息、CPU、IO等使用的状况。
  • 三、最后,会弹出一个ANR提示框,看你是要选择继续等待仍是退出应用,须要注意这个ANR提示框不必定会弹出,根据不一样ROM,它的表现状况也不一样。由于有些手机厂商它会默认去掉这个提示框,以免带来很差的用户体验。

分析完ANR的执行流程以后,咱们来分析下怎样去解决ANR,究竟哪里能够做为咱们的一个突破点。github

在上面咱们说过,当应用发生ANR时,会写入当时发生ANR的场景信息到文件中,那么,咱们可不能够经过这个文件来判断是否发生了ANR呢?算法

关于根据ANR log进行ANR问题的排查与解决的方式笔者已经在深刻探索Android稳定性优化的第三节ANR优化中讲解过了,这里就很少赘述了。shell

线上ANR监控方式

深刻探索Android稳定性优化的第三节ANR优化中我说到了使用FileObserver能够监听 /data/anr/traces.txt的变化,利用它能够实现线上ANR的监控,可是它有一个致命的缺点,就是高版本ROM须要root权限,解决方案是只能经过海外Google Play服务、国内Hardcoder的方式去规避。可是,这在国内显然是不现实的,那么,有没有更好的实现方式呢?json

那就是ANR-WatchDog,下面我就来详细地介绍一下它。c#

ANR-WatchDog项目地址

ANR-WatchDog是一种非侵入式的ANR监控组件,能够用于线上ANR的监控,接下来,咱们就使用ANR-WatchDog来监控ANR。

首先,在咱们项目的app/build.gradle中添加以下依赖:

implementation 'com.github.anrwatchdog:anrwatchdog:1.4.0'
复制代码

而后,在应用的Application的onCreate方法中添加以下代码启动ANR-WatchDog:

new ANRWatchDog().start();
复制代码

能够看到,它的初始化方式很是地简单,同时,它内部的实现也很是简单,整个库只有两个类,一个是ANRWatchDog,另外一个是ANRError。

接下来咱们来看一下ANRWatchDog的实现方式。

/**
* A watchdog timer thread that detects when the UI thread has frozen.
*/
public class ANRWatchDog extends Thread {
复制代码

能够看到,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() {
    // 一、首先,将线程命名为|ANR-WatchDog|。
    setName("|ANR-WatchDog|");

    // 二、接着,声明了一个默认的超时间隔时间,默认的值为5000ms。
    long interval = _timeoutInterval;
    // 三、而后,在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 ;
        }

        // 四、若是主线程没有处理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 {
                // 五、若是没有主动给ANR_Watchdog设置线程名,则会默认会使用ANRError的NewMainOnly方法去处理ANR。
                error = ANRError.NewMainOnly(_tick);
            }
           
           // 六、最后会经过ANRListener调用它的onAppNotResponding方法,其默认的处理会直接抛出当前的ANRError,致使程序崩溃。 _anrListener.onAppNotResponding(error);
            interval = _timeoutInterval;
            _reported = true;
        }
    }
}
复制代码

首先,在注释1处,咱们将线程命名为了|ANR-WatchDog|。接着,在注释2处,声明了一个默认的超时间隔时间,默认的值为5000ms。而后,注释3处,在while循环中经过_uiHandler去post一个_ticker Runnable。注意这里的_tick默认是0,因此needPost即为true。接下来,线程会sleep一段时间,默认值为5000ms。在注释4处,若是主线程没有处理Runnable,即_tick的值没有被赋值为0,则说明发生了ANR,第二个_reported标志位是为了不重复报道已经处理过的ANR。若是发生了ANR,就会调用接下来的代码,开始会处理debug的状况,而后,咱们看到注释5处,若是没有主动给ANR_Watchdog设置线程名,则会默认会使用ANRError的NewMainOnly方法去处理ANR。ANRError的NewMainOnly方法以下所示:

/**
 * The minimum duration, in ms, for which the main thread has been blocked. May be more.
 */
public final long duration;

static ANRError NewMainOnly(long duration) {
    // 一、获取主线程的堆栈信息
    final Thread mainThread = Looper.getMainLooper().getThread();
    final StackTraceElement[] mainStackTrace = mainThread.getStackTrace();

    // 二、返回一个包含主线程名、主线程堆栈信息以及发生ANR的最小时间值的实例。
    return new ANRError(new $(getThreadTitle(mainThread), mainStackTrace).new _Thread(null), duration);
}
复制代码

能够看到,在注释1处,首先获了主线程的堆栈信息,而后返回了一个包含主线程名、主线程堆栈信息以及发生ANR的最小时间值的实例。(咱们能够改造其源码在此时添加更多的卡顿现场信息,如CPU 使用率和调度信息、内存相关信息、I/O 和网络相关的信息等等

接下来,咱们再回到ANRWatchDog的run方法中的注释6处,最后这里会经过ANRListener调用它的onAppNotResponding方法,其默认的处理会直接抛出当前的ANRError,致使程序崩溃。对应的代码以下所示:

private static final ANRListener DEFAULT_ANR_LISTENER = new ANRListener() {
    @Override public void onAppNotResponding(ANRError error) {
        throw error;
    }
};
复制代码

了解了ANRWatchDog的实现原理以后,咱们试一试它的效果如何。首先,咱们给MainActivity中的悬浮按钮添加主线程休眠10s的代码,以下所示:

@OnClick({R.id.main_floating_action_btn})
void onClick(View view) {
    switch (view.getId()) {
        case R.id.main_floating_action_btn:
            try {
                // 对应项目中的第170行
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            jumpToTheTop();
            break;
        default:
            break;
    }
}
复制代码

而后,咱们从新安装运行项目,点击悬浮按钮,发如今10s内都不能触发屏幕点击和触摸事件,而且在10s以后,应用直接发生了崩溃。接着,咱们在Logcat过滤栏中输入fatal关键字,找出致命的错误,log以下所示:

2020-01-18 09:55:53.459 29924-29969/? E/AndroidRuntime: FATAL EXCEPTION: |ANR-WatchDog|
Process: json.chao.com.wanandroid, PID: 29924
com.github.anrwatchdog.ANRError: Application Not Responding for at least 5000 ms.
Caused by: com.github.anrwatchdog.ANRError$$$_Thread: main (state = TIMED_WAITING)
    at java.lang.Thread.sleep(Native Method)
    at java.lang.Thread.sleep(Thread.java:373)
    at java.lang.Thread.sleep(Thread.java:314)
    // 1
    at json.chao.com.wanandroid.ui.main.activity.MainActivity.onClick(MainActivity.java:170)
    at json.chao.com.wanandroid.ui.main.activity.MainActivity_ViewBinding$1.doClick(MainActivity_ViewBinding.java:45)
    at butterknife.internal.DebouncingOnClickListener.onClick(DebouncingOnClickListener.java:22)
    at android.view.View.performClick(View.java:6311)
    at android.view.View$PerformClick.run(View.java:24833)
    at android.os.Handler.handleCallback(Handler.java:794)
    at android.os.Handler.dispatchMessage(Handler.java:99)
    at android.os.Looper.loop(Looper.java:173)
    at android.app.ActivityThread.main(ActivityThread.java:6653)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:821)
 Caused by: com.github.anrwatchdog.ANRError$$$_Thread: AndroidFileLogger./storage/emulated/0/Android/data/json.chao.com.wanandroid/log/ (state = RUNNABLE)
复制代码

能够看到,发生崩溃的线程正是|ANR-WatchDog|。咱们重点关注注释1,这里发生崩溃的位置是在MainActivity的onClick方法,对应的行数为170行,从前可知,这里正是线程休眠的地方。

接下来,咱们来分析一下ANR-WatchDog的实现原理。

二、ANR-WatchDog原理

  • 首先,咱们调用了ANR-WatchDog的start方法,而后这个线程就会开始工做。
  • 而后,咱们经过主线程的Handler post一个消息将主线程的某个值进行一个加值的操做
  • post完成以后呢,咱们这个线程就sleep一段时间。
  • 在sleep以后呢,它就会来检测咱们这个值有没有被修改,若是这个值被修改了,那就说明咱们在主线程中执行了这个message,即代表主线程没有发生卡顿,不然,则说明主线程发生了卡顿
  • 最后,ANR-WatchDog就会判断发生了ANR,抛出一个异常给咱们。

最后,ANR-WatchDog的工做流程简图以下所示:

image

 

上面咱们最后说到,若是检测到主线程发生了卡顿,则会抛出一个ANR异常,这将会致使应用崩溃,显然不能将这种方案带到线上,那么,有什么方式可以自定义最后发生卡顿时的处理过程吗?

其实ANR-WatchDog自身就实现了一个咱们自身也能够去实现的ANRListener,经过它,咱们就能够对ANR事件去作一个自定义的处理,好比将堆栈信息压缩后保存到本地,并在适当的时间上传到APM后台。

三、小结

ANR-WatchDog是一种非侵入式的ANR监控方案,它可以弥补咱们在高版本中没有权限去读取traces.txt文件的问题,须要注意的是,在线上这两种方案咱们须要结合使用。

在以前,咱们还讲到了AndroidPerformanceMonitor,那么它和ANR-WatchDog有什么区别呢?

对于AndroidPerformanceMonitor来讲,它是监控咱们主线程中每个message的执行,它会在主线程的每个message的先后打印一个时间戳,而后,咱们就能够据此计算每个message的具体执行时间,可是咱们须要注意的是一个message的执行时间一般是很是短暂的,也就是很难达到ANR这个级别。而后咱们来看看ANR-WatchDog的原理,它是无论应用是如何执行的,它只会看最终的结果,即sleep 5s以后,我就看主线程的这个值有没有被更改。若是说被改过,就说明没有发生ANR,不然,就代表发生了ANR

根据这两个库的原理,咱们即可以判断出它们分别的适用场景,对于AndroidPerformanceMonitor来讲,它适合监控卡顿,由于每个message它执行的时间并不长。对于ANR-WatchDog来讲,它更加适合于ANR监控的补充

此外,虽然ANR-WatchDog解决了在高版本系统没有权限读取 /data/anr/traces.txt 文件的问题,可是在Java层去获取全部线程堆栈以及各类信息很是耗时,对于卡顿场景不必定合适,它可能会进一步加重用户的卡顿。若是是对性能要求比较高的应用,能够经过Hook Native层的方式去得到全部线程的堆栈信息,具体为以下两个步骤:

经过这种方式就大体模拟了系统打印 ANR 日志的流程,可是因为采用的是Hook方式,因此可能会产生一些异常甚至崩溃的状况,这个时候就须要经过 fork 子进程方式去避免这种问题,并且使用 子进程去获取堆栈信息的方式能够作到彻底不卡住咱们主进程。

可是须要注意的是,fork 进程会致使进程号发生改变,此时须要经过指定 /proc/[父进程 id]的方式从新获取应用主进程的堆栈信息

经过 Native Hook 的 方式咱们实现了一套“无损”获取全部 Java 线程堆栈与详细信息的卡顿监控体系。为了下降上报数据量,建议只有主线程的 Java 线程状态是 WAITING、TIME_WAITING 或者 BLOCKED 的时候,才去使用这套方案

2、卡顿单点问题检测方案

除了自动化的卡顿与ANR监控以外,咱们还须要进行卡顿单点问题的检测,由于上述两种检测方案的并不能知足全部场景的检测要求,这里我举一个小栗子:

好比我有不少的message要执行,可是每个message的执行时间
都不到卡顿的阈值,那自动化卡顿检测方案也就不可以检测出卡
顿,可是对用户来讲,用户就以为你的App就是有些卡顿。
复制代码

除此以外,为了创建体系化的监控解决方案,咱们就必须在上线以前将问题尽量地暴露出来

一、IPC单点问题检测方案

常见的单点问题有主线程IPC、DB操做等等,这里我就拿主线程IPC来讲,由于IPC实际上是一个很耗时的操做,可是在实际开发过程当中,咱们可能对IPC操做没有足够的重视,因此,咱们常常在主程序中去作频繁IPC操做,因此说,这种耗时它可能并不到你设定卡顿的一个阈值,接下来,咱们看一下,对于IPC问题,咱们应该去监测哪些指标。

  • 一、IPC调用类型:如PackageManager、TelephoneManager的调用。
  • 二、每个的调用次数与耗时。
  • 三、IPC的调用堆栈(代表哪行代码调用的)、发生线程。

常规方案

常规方案就是在IPC的先后加上埋点。可是,这种方式不够优雅,并且,在日常开发过程当中咱们常常忘记某个埋点的真正用处,同时它的维护成本也很是大

接下来,咱们讲解一下IPC问题监测的技巧。

IPC问题监测技巧

在线下,咱们能够经过adb命令的方式来进行监测,以下所示:

// 一、首先,对IPC操做开始进行监控
adb shell am trace-ipc start
// 二、而后,结束IPC操做的监控,同时,将监控到的信息存放到指定的文件当中
adb shell am trace-ipc stop -dump-file /data/local/tmp/ipc-trace.txt
// 三、最后,将监控到的ipc-trace导出到电脑查看
adb pull /data/local/tmp/ipc-trace.txt
复制代码

而后,这里咱们介绍一种优雅的实现方案,看过深刻探索Android布局优化(上)的同窗可能知道这里的实现方案无非就是ARTHook或AspectJ这两种方案,这里咱们须要去监控IPC操做,那么,咱们应该选用哪一种方式会更好一些呢?(利用epic实现ARTHook)

要回答这个问题,就须要咱们对ARTHook和AspectJ这二者的思想有足够的认识,对应ARTHook来讲,其实咱们能够用它来去Hook系统的一些方法,由于对于系统代码来讲,咱们没法对它进行更改,可是咱们能够Hook住它的一个方法,在它的方法体里面去加上本身的一些代码。可是,对于AspectJ来讲,它只能针对于那些非系统方法,也就是咱们App本身的源码,或者是咱们所引用到的一些jar、aar包。由于AspectJ其实是往咱们的具体方法里面插入相对应的代码,因此说,他不可以针对于咱们的系统方法去作操做,在这里,咱们就须要采用ARTHook的方式去进行IPC操做的监控

在使用ARTHook去监控IPC操做以前,咱们首先思考一下,哪些操做是IPC操做呢?

好比说,咱们经过PackageManager去拿到咱们应用的一些信息,或者去拿到设备的DeviceId这样的信息以及AMS相关的信息等等,这些其实都涉及到了IPC的操做,而这些操做都会经过固定的方式进行IPC,并最终会调用到android.os.BinderProxy,接下来,咱们来看看它的transact方法,以下所示:

public boolean transact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
复制代码

这里咱们仅仅关注transact方法的参数便可,第一个参数是一个行动编码,为int类型,它是在FIRST_CALL_TRANSACTION与LAST_CALL_TRANSACTION之间的某个值,第2、三个参数都是Parcel类型的参数,用于获取和回复相应的数据,第四个参数为一个int类型的标记值,为0表示一个正常的IPC调用,不然代表是一个单向的IPC调用。而后,咱们在项目中的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();
    }
复制代码

从新安装应用,便可看到以下的Log信息:

2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ WanAndroidApp$1.beforeHookedMethod  (WanAndroidApp.java:160)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │    LogHelper.i  (LogHelper.java:37)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ [WanAndroidApp.java | 160 | beforeHookedMethod] BinderProxy beforeHookedMethod BinderProxy
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ java.lang.Throwable
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at json.chao.com.wanandroid.app.WanAndroidApp$1.beforeHookedMethod(WanAndroidApp.java:160)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at com.taobao.android.dexposed.DexposedBridge.handleHookedArtMethod(DexposedBridge.java:237)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at me.weishu.epic.art.entry.Entry64.onHookBoolean(Entry64.java:72)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at me.weishu.epic.art.entry.Entry64.referenceBridge(Entry64.java:237)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at me.weishu.epic.art.entry.Entry64.booleanBridge(Entry64.java:86)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.os.ServiceManagerProxy.getService(ServiceManagerNative.java:123)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.os.ServiceManager.getService(ServiceManager.java:56)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.os.ServiceManager.getServiceOrThrow(ServiceManager.java:71)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.app.UiModeManager.<init>(UiModeManager.java:127)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.app.SystemServiceRegistry$42.createService(SystemServiceRegistry.java:511)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.app.SystemServiceRegistry$42.createService(SystemServiceRegistry.java:509)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.app.SystemServiceRegistry$CachedServiceFetcher.getService(SystemServiceRegistry.java:970)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.app.SystemServiceRegistry.getSystemService(SystemServiceRegistry.java:920)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.app.ContextImpl.getSystemService(ContextImpl.java:1677)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.view.ContextThemeWrapper.getSystemService(ContextThemeWrapper.java:171)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.app.Activity.getSystemService(Activity.java:6003)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.support.v7.app.AppCompatDelegateImplV23.<init>(AppCompatDelegateImplV23.java:33)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.support.v7.app.AppCompatDelegateImplN.<init>(AppCompatDelegateImplN.java:31)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.support.v7.app.AppCompatDelegate.create(AppCompatDelegate.java:198)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.support.v7.app.AppCompatDelegate.create(AppCompatDelegate.java:183)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.support.v7.app.AppCompatActivity.getDelegate(AppCompatActivity.java:519)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.support.v7.app.AppCompatActivity.onCreate(AppCompatActivity.java:70)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at me.yokeyword.fragmentation.SupportActivity.onCreate(SupportActivity.java:38)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at json.chao.com.wanandroid.base.activity.AbstractSimpleActivity.onCreate(AbstractSimpleActivity.java:29)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at json.chao.com.wanandroid.base.activity.BaseActivity.onCreate(BaseActivity.java:37)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.app.Activity.performCreate(Activity.java:7098)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.app.Activity.performCreate(Activity.java:7089)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1215)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2770)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2895)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.app.ActivityThread.-wrap11(Unknown Source:0)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1616)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.os.Handler.dispatchMessage(Handler.java:106)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.os.Looper.loop(Looper.java:173)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.app.ActivityThread.main(ActivityThread.java:6653)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at java.lang.reflect.Method.invoke(Native Method)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:821)
复制代码

能够看出,这里弹出了应用中某一个IPC调用的全部堆栈信息。在这里,具体是在AbstractSimpleActivity的onCreate方法中调用了ServiceManager的getService方法,它是一个IPC调用的方法。这样,应用的IPC调用咱们就能很方便地捕获到了。

你们能够看到,经过这种方式咱们能够很方便地拿到应用中全部的IPC操做,并能够得到到IPC调用的类型、调用耗时、发生次数、调用的堆栈等等一系列信息。固然,除了IPC调用的问题以外,还有IO、DB、View绘制等一系列单点问题须要去创建与之对应的检测方案。

二、卡顿问题检测方案

对于卡顿问题检测方案的建设,主要是利用ARTHook去完善线下的检测工具,尽量地去Hook相对应的操做,以暴露、分析问题。这样,才能更好地实现卡顿的体系化解决方案。

3、如何实现界面秒开?

界面的打开速度对用户体验来讲是相当重要的,那么如何实现界面秒开呢?

其实界面秒开就是一个小的启动优化,其优化的思想能够借鉴启动速度优化与布局优化的一些实现思路

一、界面秒开实现

首先,咱们能够经过Systrace来观察CPU的运行情况,好比有没有跑满CPU;而后,咱们在启动优化中学习到的优雅异步以及优雅延迟初始化等等一些方案;其次,针对于咱们的界面布局,咱们可使用异步Inflate、X2C、其它的绘制优化措施等等;最后,咱们可使用预加载的方式去提早获取页面的数据,以免网络或磁盘IO速度的影响,或者也能够将获取数据的方法放到onCreate方法的第一行

那么咱们如何去衡量界面的打开速度呢?

一般,咱们是经过界面秒开率去统计页面的打开速度的,具体就是计算onCreate到onWindowFocusChanged的时间。固然,在某些特定的场景下,把onWindowFocusChanged做为页面打开的结束点并非特别的精确,那咱们能够去实现一个特定的接口来适配咱们的Activity或Fragment,咱们能够把那个接口方法做为页面打开的结束点

那么,除了以上说到的一些界面秒开的实现方式以外,尚未更好的方式呢?

那就是Lancet。

二、Lancet

Lancet是一个轻量级的Android AOP框架,它具备以下优点:

  • 一、编译速度快,支持增量编译。
  • 二、API简单,没有任何多余代码插入apk。(这一点对应包体积优化时相当重要的)

而后,我来简单地讲解下Lancet的用法。Lancet自身提供了一些注解用于Hook,以下所示:

  • @Prxoy:一般是用于对系统API调用的Hook。
  • @Insert:常常用于操做App或者是Library当中的一些类。

接下来,咱们就是使用Lancet来进行一下实战演练。

首先,咱们须要在项目根目录的 build.gradle 添加以下依赖:

dependencies{
    classpath 'me.ele:lancet-plugin:1.0.5'
}
复制代码

而后,在 app 目录的'build.gradle' 添加:

apply plugin: 'me.ele.lancet'

dependencies {
    compileOnly 'me.ele:lancet-base:1.0.5'
}
复制代码

接下来,咱们就可使用Lancet了,这里咱们须要先新建一个类去进行专门的Hook操做,以下所示:

public class ActivityHooker {

    @Proxy("i")
    @TargetClass("android.util.Log")
    public static int i(String tag, String msg) {
        msg = msg + "JsonChao";
        return (int) Origin.call();
    }
}
复制代码

上述的方法就是对android.util.Log的i方法进行Hook,并在全部的msg后面加上"JsonChao"字符串,注意这里的i方法咱们须要从android.util.Log里面将它的i方法复制过来,确保方法名和对应的参数信息一致;而后,方法上面的@TargetClass与@Proxy分别是指定对应的全路径类名与方法名;最后,咱们须要经过Lancet提供的Origin类去调用它的call方法来实现返回原来的调用信息。完成以后,咱们从新运行项目,会出现以下log信息:

2020-01-23 13:13:34.124 7277-7277/json.chao.com.wanandroid I/MultiDex: VM with version 2.1.0 has multidex supportJsonChao
2020-01-23 13:13:34.124 7277-7277/json.chao.com.wanandroid I/MultiDex: Installing applicationJsonChao
复制代码

能够看到,log后面都加上了咱们预先添加的字符串,说明Hook成功了。下面,咱们就能够用Lancet来统计一下项目界面的秒开率了,代码以下所示:

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();
    LogHelper.i(getClass().getCanonicalName() + " onWindowFocusChanged cost "+(sActivityRecord.mOnWindowsFocusChangedTime - sActivityRecord.mOnCreateTime));
    Origin.callVoid();
}
复制代码

上面,咱们经过@TargetClass和@Insert两个注解实现Hook了android.support.v7.app.AppCompatActivity的onCreate与onWindowFocusChanged方法。咱们注意到,这里@Insert注解能够指定两个参数,其源码以下所示:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Insert {
    String value();

    boolean mayCreateSuper() default false;
}
复制代码

第二个参数mayCreateSuper设定为true则代表若是没有重写父类的方法,则会默认去重写这个方法。对应到咱们ActivityHooker里面实现的@Insert注解方法就是若是当前的Activity没有重写父类的onCreate和 onWindowFocusChanged方法,则此时默认会去重写父类的这个方法,以避免因某些Activity不存在该方法而Hook失败的状况

而后,咱们注意到@TargetClass也能够指定两个参数,其源码以下所示:

@Retention(RetentionPolicy.RUNTIME)
@java.lang.annotation.Target({ElementType.TYPE, ElementType.METHOD})
public @interface TargetClass {
    String value();

    Scope scope() default Scope.SELF;
}
复制代码

第二个参数scope指定的值是一个枚举,可选的值以下所示:

public enum Scope {

    SELF,
    DIRECT,
    ALL,
    LEAF
}
复制代码

对于Scope.SELF,它表明仅匹配目标value所指定的一个匹配类;对于DIRECT,它表明匹配value所指定的类的一个直接子类;若是是Scope.ALL,它就代表会去匹配value所指定的类的全部子类,而咱们上面指定的value值为android.support.v7.app.AppCompatActivity,由于scope指定为了Scope.ALL,则说明会去匹配AppCompatActivity的全部子类。而最后的Scope.LEAF 表明匹配 value 指定类的最终子类,由于java是单继承,因此继承关系是树形结构,因此这里表明了指定类为顶点的继承树的全部叶子节点。

最后,咱们设定了一个ActivityRecord类去记录onCreate与onWindowFocusChanged的时间戳,以下所示:

public class ActivityRecord {

    /**
    * 避免没有仅执行onResume就去统计界面打开速度的状况,如息屏、亮屏等等
    */
    public boolean isNewCreate;

    public long mOnCreateTime;
    public long mOnWindowsFocusChangedTime;
}
复制代码

经过sActivityRecord.mOnWindowsFocusChangedTime - sActivityRecord.mOnCreateTime获得的时间即为界面的打开速度,最后,从新运行项目,会获得以下log信息:

2020-01-23 14:12:16.406 15098-15098/json.chao.com.wanandroid I/WanAndroid-LOG: │ [null | 57 | json_chao_com_wanandroid_aop_ActivityHooker_onWindowFocusChanged] json.chao.com.wanandroid.ui.main.activity.SplashActivity onWindowFocusChanged cost 257
2020-01-23 14:12:18.930 15098-15098/json.chao.com.wanandroid I/WanAndroid-LOG: │ [null | 57 | json_chao_com_wanandroid_aop_ActivityHooker_onWindowFocusChanged] json.chao.com.wanandroid.ui.main.activity.MainActivity onWindowFocusChanged cost 608
复制代码

从上面的log信息,咱们就能够知道 SplashActivity 和 MainActivity 的界面打开速度分别是257ms和608ms。

最后,咱们来看下界面秒开的监控纬度。

三、界面秒开监控纬度

对于界面秒开的监控纬度,主要分为如下三个方面:

  • 整体耗时
  • 生命周期耗时
  • 生命周期间隔耗时

首先,咱们会监控界面打开的总体耗时,也就是onCreate到onWindowFocusChanged这个方法的耗时;固然,若是咱们是在一个特殊的界面,咱们须要更精确的知道界面打开的一个时间,这个咱们能够用自定义的接口去实现。其次,咱们也须要去监控生命周期的一个耗时,如onCreate、onStart、onResume等等。最后,咱们也须要去作生命周期间隔的耗时监控,这点常常被咱们所忽略,好比onCreate的结束到onStart开始的这一段时间,也是有时间损耗的,咱们能够监控它是否是在一个合理的范围以内。经过这三个方面的监控纬度,咱们就可以很是细粒度地去检测页面秒开各个方面的状况

4、优雅监控耗时盲区

尽管咱们在应用中监控了不少的耗时区间,可是仍是有一些耗时区间咱们尚未捕捉到,如onResume到列表展现的间隔时间,这些时间在咱们的统计过程当中很容易被忽视,这里咱们举一个小栗子:

咱们在Activity的生命周期中post了一个message,那这个message极可能其中
执行了一段耗时操做,那你知道这个message它的具体执行时间吗?这个message其实
颇有可能在列表展现以前就执行了,若是这个message耗时1s,那么列表的展现
时间就会延迟1s,若是是200ms,那么咱们设定的自动化卡顿检测就没法
发现它,那么列表的展现时间就会延迟200ms。
复制代码

其实这种场景很是常见,接下来,咱们就在项目中来进行实战演练。

首先,咱们在MainActivity的onCreate中加上post消息的一段代码,其中模拟了延迟1000ms的耗时操做,代码以下所示:

// 如下代码是为了演示Msg致使的主线程卡顿
    new Handler().post(() -> {
        LogHelper.i("Msg 执行");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
复制代码

接着,咱们在RecyclerView对应的Adapter中将列表展现的时间打印出来,以下所示:

if (helper.getLayoutPosition() == 1 && !mHasRecorded) {
        mHasRecorded = true;
        helper.getView(R.id.item_search_pager_group).getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
            @Override
            public boolean onPreDraw() {
                helper.getView(R.id.item_search_pager_group).getViewTreeObserver().removeOnPreDrawListener(this);
                LogHelper.i("FeedShow");
                return true;
            }
        });
    }
复制代码

最后,咱们从新运行下项目,看看二者的执行时间,log信息以下:

2020-01-23 15:21:55.076 19091-19091/json.chao.com.wanandroid I/WanAndroid-LOG: │ [MainActivity.java | 108 | lambda$initEventAndData$1$MainActivity] Msg 执行
2020-01-23 15:21:56.264 19091-19091/json.chao.com.wanandroid I/WanAndroid-LOG: │ [null | 57 | json_chao_com_wanandroid_aop_ActivityHooker_onWindowFocusChanged] json.chao.com.wanandroid.ui.main.activity.MainActivity onWindowFocusChanged cost 1585
2020-01-23 15:21:57.207 19091-19091/json.chao.com.wanandroid I/WanAndroid-LOG: │ ArticleListAdapter$1.onPreDraw  (ArticleListAdapter.java:93)
2020-01-23 15:21:57.208 19091-19091/json.chao.com.wanandroid I/WanAndroid-LOG: │    LogHelper.i  (LogHelper.java:37)
2020-01-23 15:21:57.208 19091-19091/json.chao.com.wanandroid I/WanAndroid-LOG: ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
2020-01-23 15:21:57.208 19091-19091/json.chao.com.wanandroid I/WanAndroid-LOG: │ [ArticleListAdapter.java | 93 | onPreDraw] FeedShow
复制代码

从log信息中能够看到,MAinActivity的onWindowFocusChanged方法延迟了1000ms才被调用,与此同时,列表页时延迟了1000ms才展现出来。也就是说,post的这个message消息是执行在界面、列表展现以前的。由于任何一个开发都有可能在某一个生命周期或者是某一个阶段以及一些第三方的SDK里面,回去作一些handler post的相关操做,这样,他的handler post的message的执行,颇有可能在咱们的界面或列表展现以前就被执行,因此说,出现这种耗时的盲区是很是广泛的,并且也很差排查,下面,咱们分析下耗时盲区存在的难点。

一、耗时盲区监控难点

首先,咱们能够经过细化监控的方式去获取耗时的一些盲区,可是咱们殊不知道在这个盲区中它执行了什么操做。其次,对于线上的一些耗时盲区,咱们是没法进行排查的。

这里,咱们先来看看如何创建耗时盲区监控的线下方案。

二、耗时盲区监控线下方案

这里咱们直接使用TraceView去检测便可,由于它可以清晰地记录线程在具体的时间内到底作了什么操做,特别适合一段时间内的盲区监控。

而后,咱们来看下如何创建耗时盲区监控的线上方案。

三、耗时盲区监控线上方案

咱们知道主线程的全部方法都是经过message来执行的,还记得在以前咱们学习了一个库:AndroidPerformanceMonitor,咱们是否能够经过这个mLogging来作盲区检测呢?经过这个mLogging确实能够知道咱们主线程发生的message,可是经过mLogging没法获取具体的调用栈信息,由于它所获取的调用栈信息都是系统回调回来的,它并不知道当前的message是被谁抛出来的,因此说,这个方案并不够完美。

那么,咱们是否能够经过AOP的方式去切Handler方法呢?好比sendMessage、sendMessageDeleayd方法等等,这样咱们就能够知道发生message的一个堆栈,可是这种方案也存在着一个问题,就是它不清楚准确的执行时间,咱们切了这个handler的方法,仅仅只知道它具体是在哪一个地方被发的和它所对应的堆栈信息,可是没法获取准确的执行时间。若是咱们想知道在onResume到列表展现之间执行了哪些message,那么经过AOP的方式也没法实现。

那么,最终的耗时盲区监控的一个线上方案就是使用一个统一的Handler,定制了它的两个方法,一个是sendMessageAtTime,另一个是dispatchMessage方法。由于对于发送message,无论调用哪一个方法最终都会调用到一个是sendMessageAtTime这个方法,而处理message呢,它最终会调用dispatchMessage方法。而后,咱们须要定制一个gradle插件,来实现自动化的接入咱们定制好的handler,经过这种方式,咱们就能在编译期间去动态地替换全部使用Handler的父类为咱们定制好的这个handler。这样,在整个项目中,全部的sendMessage和handleMessage都会通过咱们的回调方法。接下来,咱们来进行一下实战演练。

首先,我这里给出定制好的全局Handler类,以下所示:

public class GlobalHandler extends Handler {

    private long mStartTime = System.currentTimeMillis();

    public GlobalHandler() {
        super(Looper.myLooper(), null);
    }

    public GlobalHandler(Callback callback) {
        super(Looper.myLooper(), callback);
    }

    public GlobalHandler(Looper looper, Callback callback) {
        super(looper, callback);
    }

    public GlobalHandler(Looper looper) {
        super(looper);
    }

    @Override
    public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
        boolean send = super.sendMessageAtTime(msg, uptimeMillis);
        // 1
        if (send) {
            GetDetailHandlerHelper.getMsgDetail().put(msg, Log.getStackTraceString(new Throwable()).replace("java.lang.Throwable", ""));
        }
        return send;
    }

    @Override
    public void dispatchMessage(Message msg) {
        mStartTime = System.currentTimeMillis();
        super.dispatchMessage(msg);

        if (GetDetailHandlerHelper.getMsgDetail().containsKey(msg)
            && Looper.myLooper() == Looper.getMainLooper()) {
            JSONObject jsonObject = new JSONObject();
            try {
                // 2
                jsonObject.put("Msg_Cost", System.currentTimeMillis() - mStartTime);
                jsonObject.put("MsgTrace", msg.getTarget() + " " + GetDetailHandlerHelper.getMsgDetail().get(msg));

                // 3
                LogHelper.i("MsgDetail " + jsonObject.toString());
                GetDetailHandlerHelper.getMsgDetail().remove(msg);
            } catch (Exception e) {
            }
        }
    }
}
复制代码

上面的GlobalHandler将会是咱们项目中全部Handler的一个父类。在注释1处,咱们在sendMessageAtTime这个方法里面判断若是message发送成功,将会把当前message对象对应的调用栈信息都保存到一个ConcurrentHashMap中,GetDetailHandlerHelper类的代码以下所示:

public class GetDetailHandlerHelper {

    private static ConcurrentHashMap<Message, String> sMsgDetail = new ConcurrentHashMap<>();

    public static ConcurrentHashMap<Message, String> getMsgDetail() {
        return sMsgDetail;
    }
}
复制代码

这样,咱们就可以知道这个message它是被谁发送过来的。而后,在dispatchMessage方法里面,咱们能够计算拿到其处理消息的一个耗时,并在注释2处将这个耗时保存到一个jsonObject对象中,同时,咱们也能够经过GetDetailHandlerHelper类的ConcurrentHashMap对象拿到这个message对应的堆栈信息,并在注释3处将它们输出到log控制台上。固然,若是是线上监控,则会把这些信息保存到本地,而后选择合适的时间去上传。最后,咱们还能够在方法体里面作一个判断,咱们设置一个阈值,好比阈值为20ms,超过了20ms就把这些保存好的信息上报到APM后台。

在前面的实战演练中,咱们使用了handler post的方式去发送一个消息,经过gradle插件将全部handler的父类替换为咱们定制好的GlobalHandler以后,咱们就能够优雅地去监控应用中的耗时盲区了。

对于实现全局替换handler的gradle插件,除了使用AspectJ实现以外,这里推荐一个已有的项目:DroidAssist

而后,从新运行项目,关键的log信息以下所示:

MsgDetail {"Msg_Cost":1001,"MsgTrace":"Handler (com.json.chao.com.wanandroid.performance.handler.GlobalHandler) {b0d4d48} \n\tat 
com.json.chao.com.wanandroid.performance.handler.GlobalHandler.sendMessageAtTime(GlobalHandler.java:36)\n\tat
json.chao.com.wanandroid.ui.main.activity.MainActivity.initEventAndData$__twin__(MainActivity.java:107)\n\tat"
复制代码

从以上信息咱们不只能够知道message执行的时间,还能够从对应的堆栈信息中获得发送message的位置,这里的位置是MainActivity的107行,也就是new Handler().post()这一行代码。使用这种方式咱们就能够知道在列表展现以前到底执行了哪些自定义的message,咱们一眼就能够知道哪些message实际上是不符合咱们预期的,好比说message的执行时间过长,或者说这个message其实能够延后执行,这个咱们均可以根据实际的项目和业务需求进行相应地修改

四、耗时盲区监控方案总结

耗时盲区监控是咱们卡顿监控中不可或缺的一个环节,也是卡顿监控全面性的一个重要保障。而须要注意的是,TraceView仅仅适用于线下的一个场景,同时对于TraceView来讲,它能够用于监控咱们系统的message。而最后介绍的动态替换的方式实际上是适合于线上的,同时,它仅仅监控应用自身的一个message。

5、卡顿优化技巧总结

一、卡顿优化实践经验

若是应用出现了卡顿现象,那么能够考虑如下方式进行优化:

  • 首先,对于耗时的操做,咱们能够考虑异步或延迟初始化的方式,这样能够解决大多数的问题。可是,你们必定要注意代码的优雅性。
  • 对于布局加载优化,能够采用AsyncLayoutInflater或者是X2C的方式来优化主线程IO以及反射致使的消耗,同时,须要注意,对于重绘问题,要给与必定的重视。
  • 此外,内存问题也可能会致使应用界面的卡顿,咱们能够经过下降内存占用的方式来减小GC的次数以及时间,而GC的次数和时间咱们能够经过log查看。

而后,咱们来看看卡顿优化的工具建设。

二、卡顿优化工具建设

工具建设这块常常容易被你们所忽视,可是它的收益却很是大,也是卡顿优化的一个重点。首先,对于系统工具而言,咱们要有一个认识,同时必定要学会使用它,这里咱们再回顾一下。

  • 对于Systrace来讲,咱们能够很方便地看出来它的CPU使用状况。另外,它的开销也比较小。
  • 对于TraceView来讲,咱们能够很方便地看出来每个线程它在特定的时间内作了什么操做,可是TraceView它的开销相对比较大,有时候可能会被带偏优化方向。
  • 同时,须要注意,StrictMode也是一个很是强大的工具。

而后,咱们介绍了自动化工具建设以及优化方案。咱们介绍了两个工具,AndroidPerformanceMonitor以及ANR-WatchDog。同时针对于AndroidPerformanceMonitor的问题,咱们采用了高频采集,以找出重复率高的堆栈这样一种方式进行优化,在学习的过程当中,咱们不只须要学会怎样去使用工具,更要去理解它们的实现原理以及各自的使用场景。

同时,咱们对于卡顿优化工具的建设也作了细化,对于单点问题,好比说IPC监控,咱们经过Hook的手段来作到尽早的发现问题。对于耗时盲区的监控,咱们在线上采用的是替换Handler的方式来监控全部子线程message执行的耗时以及调用堆栈

最后,咱们来看一下卡顿监控的指标。咱们会计算应用总体的卡顿率,ANR率、界面秒开率以及交换时间、生命周期时间等等。在上报ANR信息的同时,咱们也须要上报环境和场景信息,这样不只方便咱们在不一样版本之间进行横向对比,同时,也能够结合咱们的报警平台在第一时间感知到异常

6、常见卡顿问题解决方案总结

一、CPU资源争抢引起的卡顿问题如何解决?

此时,咱们的应用不只应该控制好核心功能的CPU消耗,也须要尽可能减小非核心需求的CPU消耗。

二、要注意Android Java中提供的哪些低效的API?

好比List.removeall方法,它内部会遍历一次须要过滤的消息列表,在已经存在循环列表的状况下会形成CPU资源的冗余使用,此时应该去优化相关的算法,避免使用List.removeall这个方法。

三、如何减小图形处理的CPU消耗?

这个时候咱们须要使用神器renderscript来图形处理的相关运算,将CPU转换到GPU。

四、硬件加速长中文字体渲染时形成的卡顿如何解决?

此时只能关闭文本TextView的硬件加速,以下所示:

textView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
复制代码

当开启了硬件加速进行长中文字体的渲染时,首先会调用ViewRootImpl.draw()方法,最后会调用GLES20Canvas.nDrawDisplayList()方法开始经过JNI调整到Native层。在这个方法里,会继续调用OpenGLRenderer.drawDisplayList()方法,它经过调用DisplayList的replay方法,以回放前面录制的DisplayList执行绘制操做

DisplayList的replay方法会遍历DisplayList中保存的每个操做。其中渲染字体的操做名是DrawText,当遍历到一个DrawText操做时,会调用OpenGLRender::drawText方法区渲染字体。最终,会在OpenGLRender::drawText方法里去调用Font::render()方法渲染字体,而在这个方法中有一个很关键的操做,即获取字体缓存。咱们都知道每个中文的编码都是不一样的,所以中文的缓存效果很是不理想,可是对于英文而言,只须要缓存26个字母就能够了。在Android 4.1.2版本以前对文本的Buffer设置太小,因此状况比较严重,若是你的应用在其它版本的渲染性能尚可,就能够仅仅把Android 4.0.x的硬件加速关闭,代码以下所示:

// AndroidManifest中
<Applicaiton
        ...
        android:hardwareAccelerated="@bool/hardware_acceleration">
        
// value-v1四、value-v15中设置相应的Bool
值便可
<bool name="hardware_acceleration">false</bool>
复制代码

此外,硬件渲染还有一些其它的问题在使用时须要注意,具体为以下所示:

  • 一、在软件渲染的状况下,若是须要重绘某个父View的全部子View,只须要调用这个Parent View的invalidate()方法便可,但若是开启了硬件加速,这么作是行不通的,须要遍历整个子View并调用invalidate()。
  • 二、在软件渲染的状况下,会经常使用Bitmap重用的方式来节省内存,可是若是开启了硬件加速,这将会无效。
  • 三、当开启硬件加速的UI在前台运行时,须要耗费额外的内存。当硬件加速的UI切换到后台时,上述额外内存有可能不会释放,这大多存在于Android 4.1.2版本中。
  • 四、长或宽大于2048像素的Bitmap没法绘制,显示为一片透明。缘由是OpenGL的材质大小上限为2048 * 2048,所以对于超过2048像素的Bitmap,须要将其切割成2048 * 2048之内的图片块,最后在显示的时候拼起来。
  • 五、当UI中存在过渡绘制时,可能会发生花屏,通常来讲绘制少于5层不会出现花屏现象,若是有大块红色区域就要十分当心了。
  • 六、须要注意,关于LAYER_TYPE_SOFTWARE,虽然不管在App打开硬件加速或没有打开硬件加速的时候,都会经过软件绘制Bitmap做为离屏缓存,但区别在于打开硬件加速的时候,Bitmap最终还会经过硬件加速方式drawDisplayList去渲染这个Bitmap。

7、卡顿优化的常见问题

一、你是怎么作卡顿优化的?

从项目的初期到壮大期,最后再到成熟期,每个阶段都针对卡顿优化作了不一样的处理。各个阶段所作的事情以下所示:

  • 一、系统工具定位、解决
  • 二、自动化卡顿方案及优化
  • 三、线上监控及线下监测工具的建设

我作卡顿优化也是经历了一些阶段,最初咱们的项目当中的一些模块出现了卡顿以后,我是经过系统工具进行了定位,我使用了Systrace,而后看了卡顿周期内的CPU情况,同时结合代码,对这个模块进行了重构,将部分代码进行了异步和延迟,在项目初期就是这样解决了问题。

可是呢,随着咱们项目的扩大,线下卡顿的问题也愈来愈多,同时,在线上,也有卡顿的反馈,可是线上的反馈卡顿,咱们在线下难以复现,因而咱们开始寻找自动化的卡顿监测方案,其思路是来自于Android的消息处理机制,主线程执行任何代码都会回到Looper.loop方法当中,而这个方法中有一个mLogging对象,它会在每一个message的执行先后都会被调用,咱们就是利用这个先后处理的时机来作到的自动化监测方案的。同时,在这个阶段,咱们也完善了线上ANR的上报,咱们采起的方式就是监控ANR的信息,同时结合了ANR-WatchDog,做为高版本没有文件权限的一个补充方案。

在作完这个卡顿检测方案以后呢,咱们还作了线上监控及线下检测工具的建设,最终实现了一整套完善,多维度的解决方案。

二、你是怎么样自动化的获取卡顿信息?

咱们的思路是来自于Android的消息处理机制,主线程执行任何代码它都会走到Looper.loop方法当中,而这个函数当中有一个mLogging对象,它会在每一个message处理先后都会被调用,而主线程发生了卡顿,那就必定会在dispatchMessage方法中执行了耗时的代码,那咱们在这个message执行以前呢,咱们能够在子线程当中去postDelayed一个任务,这个Delayed的时间就是咱们设定的阈值,若是主线程的messaege在这个阈值以内完成了,那就取消掉这个子线程当中的任务,若是主线程的message在阈值以内没有被完成,那子线程当中的任务就会被执行,它会获取到当前主线程执行的一个堆栈,那咱们就能够知道哪里发生了卡顿。

通过实践,咱们发现这种方案获取的堆栈信息它不必定是准确的,由于获取到的堆栈信息它极可能是主线程最终执行的一个位置,而真正耗时的地方其实已经执行完成了,因而呢,咱们就对这个方案作了一些优化,咱们采起了高频采集的方案,也就是在一个周期内咱们会屡次采集主线程的堆栈信息,若是发生了卡顿,那咱们就将这些卡顿信息压缩以后上报给APM后台,而后找出重复的堆栈信息,这些重复发生的堆栈大几率就是卡顿发生的一个位置,这样就提升了获取卡顿信息的一个准确性。

三、卡顿的一整套解决方案是怎么作的?

首先,针对卡顿,咱们采用了线上、线下工具相结合的方式,线下工具咱们须要尽量早地去暴露问题,而针对于线上工具呢,咱们侧重于监控的全面性、自动化以及异常感知的灵敏度。

同时呢,卡顿问题还有不少的难题。好比说有的代码呢,它不到你卡顿的一个阈值,可是执行过多,或者它错误地执行了不少次,它也会致使用户感官上的一个卡顿,因此咱们在线下经过AOP的方式对常见的耗时代码进行了Hook,而后对一段时间内获取到的数据进行分析,咱们就能够知道这些耗时的代码发生的时机和次数以及耗时状况。而后,看它是否是知足咱们的一个预期,不知足预期的话,咱们就能够直接到线下进行修改。同时,卡顿监控它还有不少容易被忽略的一个盲区,好比说生命周期的一个间隔,那对于这种特定的问题呢,咱们就采用了编译时注解的方式修改了项目当中全部Handler的父类,对于其中的两个方法进行了监控,咱们就能够知道主线程message的执行时间以及它们的调用堆栈。

对于线上卡顿,咱们除了计算App的卡顿率、ANR率等常规指标以外呢,咱们还计算了页面的秒开率、生命周期的执行时间等等。并且,在卡顿发生的时刻,咱们也尽量多地保存下来了当前的一个场景信息,这为咱们以后解决或者复现这个卡顿留下了依据。

8、总结

恭喜你,若是你看到了这里,你会发现要作好应用的卡顿优化的确不是一件简单的事,它须要你有成体系的知识构建基底。最后,咱们再来回顾一下面对卡顿优化,咱们已经探索的如下九大主题:

  • 一、卡顿优化分析方法与工具:背景介绍、卡顿分析方法之使用shell命令分析CPU耗时、卡顿优化工具。
  • 二、自动化卡顿检测方案及优化:卡顿检测方案原理、AndroidPerformanceMonitor实战及其优化。
  • 三、ANR分析与实战:ANR执行流程、线上ANR监控方式、ANR-WatchDog原理。
  • 四、卡顿单点问题检测方案:IPC单点问题检测方案、卡顿问题检测方案。
  • 五、如何实现界面秒开?:界面秒开实现、Lancet、界面秒开监控纬度。
  • 六、优雅监控耗时盲区:耗时盲区监控难点以及线上与线下的监控方案。
  • 七、卡顿优化技巧总结:卡顿优化实践经验、卡顿优化工具建设。
  • 8︎、常见卡顿问题解决方案总结
  • 九、卡顿优化的常见问题

相信看到这里,你必定收获满满,可是要记住,方案再好,也只有本身动手去实践,才能真正地掌握它。只有重视实践,充分运用感性认知潜能,在项目中磨炼本身,才是正确的学习之道。在实践中,在某些关键动做上刻意练习,也会取得事半功倍的效果。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Android系统性能优化对于解决卡顿、提升稳定性和优化续航方面起着重要的作用。 首先,在解决卡顿问题上,发人员需要关注应用程序的UI线程。为了确保应用程序的流畅运行,可以采用以下优化措施:优化布局文件,减少层级嵌套;使用异步加载图片,避免在主线程中进行网络请求等耗时操作;合理利用缓存机制,避免重复加载数据。此外,还可以针对卡顿问题进行性能分析,通过工具查找耗时操作,并进行相应的优化。 其次,在提高系统稳定性方面发人员需要考虑异常崩溃的处理和内存管理。异常崩溃处理可通过捕获并记录崩溃异常来及时解决问题和改进代码。内存管理方面,应避免内存泄漏和过度分配内存,使用系统提供的工具来进行内存管理和优化。 最后,在续航优化上,需要考虑电源管理和资源使用的合理分配。通过使用省电模式、灵活控制后台任务和限制应用程序在后台运行等方式,最大程度地延长设备的电池寿命。另外,合理管理资源,避免过度使用CPU、网络和图形渲染等资源,有助于降低能耗并优化系统续航。 总之,Android系统性能优化是一个综合性的工作,需要发人员关注卡顿问题、提升稳定性和优化续航方面的问题。通过合理使用工具和采取相应的优化措施,可以实现系统性能的有效提升。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值