Android性能测试手段和工具

关于性能监控和测试是安卓开发中技术进阶的重要内容,对于基建开发更是必备技能。所谓工欲善其事必先利其器,首先应该掌握安卓官方的提供的各种工具的使用,进一步的,通过探究其原理来开发我们自己的性能检测工具。

测试指标和测试方法

指标工具或方法备注
启动时间adb am-
内存占用Memory Profiler
卡顿分析SysTrace,Looper监听,dumpsys
UI布局LayoutInspector
ANRANR-WatchDog
网速检测
耗电情况Battery Historian

另外也有一些线上/线下的性能检测工具:

UI性能

当一个xml布局首次加载到activity/fragment中时,它需要经历:

IO读取
xml解析
反射生成View对象
绘制渲染

由于class对象的缓存机制,性能瓶颈一般出现在首次加载的时候。我们应当通过重构xml的手段尽量减少xml布局的嵌套深度【1】,但是这种手段的优化效果是非常有限的,此时如果需要彻底优化,就要考虑使用AsyncLayoutInflator【2】来异步加载布局,真正彻底解决主线程卡顿问题。

使用异步加载布局并没有真正解决布局加载耗时的问题,我们回顾这个卡顿的根源还是在与这个xml加载的机制必然涉及到IO和反射,如果使用纯代码编写布局就没有这个问题。目前就有极客想到利用注解处理器将xml直接在编译器转为Java代码,X2C【3】就是这样开源项目。不过这也不是一个完美的解决方案,比如当xml属性没有对应的setXXX方法时(ProgressBar、SeekBar等)生成的Java代码就会有问题。

网络优化

  • 优化DNS:使用HttpDNS
  • 连接复用:开启keep-alive,默认开启
  • 数据压缩:开启gzip压缩
  • 资源分级:根据网络状况下载不同清晰度图片

网络请求策略:

  1. 无网络则不发起网络请求;
  2. 优先使用WiFi;
  3. 统计日志等非重要非重要网络请求等到WiFi环境再发送;

电量优化

  • 定位服务、消息推送服务等共性服务复用;
  • 前台不申请WakeLock或使用带超时的方法申请,使用FLAG_KEEP_SCREEN_ON保持屏幕常亮;

SysTrace

sysTrace是老版的用于系统跟踪的CLI工具,基于python 2.7实现,。其生成的报告是个HTML文件,用浏览器打开其界面大致如下:

Systrace HTML报告截图,其中显示了与某个应用之间5秒的交互情况

在这里插入图片描述

所谓“系统跟踪”就是抓取和记录某个时间段内设备的活动情况(包括但不限于CPU使用率、线程活动、磁盘活动等),并生成跟踪文件(Perfetto或Systrace格式,根据具体的工具),用这些文件可以生成系统报告(html或其他展示形式);

生成报告

CLI命令生成

python systrace.py -a com.example.myapp -b 16384 \
-o my_systrace_report.html sched freq idle am wm gfx view binder_driver hal \
dalvik camera input res
  • -a:指定进程名称,通常就是包名;
  • -b:指定缓冲区大小;
  • -o:指定输出报告的文件路径;
  • -t:跟踪设备活动N秒。如果您未指定此选项,systrace 会提示您在命令行中按 Enter 键结束跟踪;
  • -e:指定设备序列号;
  • categories:指定要跟踪的信息,包括schedfreqidleamwmgfx渲染图形的系统进程、viewbinder_driverhaldalvikcamerainputres等,可通过systrace -l查看已连接设备可用的服务列表;
  1. 经实际测试systrace.py仅支持python 2.7无法在python 3.x中使用,这点需要注意;
  2. 出了实时捕获还支持通过指定trace文件生成HTML报告;

android.os.Debug生成

可用于生成.trace文件,用于在SystraceCPU Profiler中导入分析。使用示例如下:

public class XXXActivity extends BaseActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        // 生成的.trace文件位于:sdcard/Android/data/package_name/files/XXXActivity_1111.trace
        Debug.startMethodTracing("XXXActivity_"+ System.currentTimeMillis());
        ...
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        Debug.stopMethodTracing();
    }
}

然后用adb pull到开发机器导入到Profiler或则直接用AS的Device File Explorer双击打开:
在这里插入图片描述
需要注意的是:

  1. 启用剖析功能后,应用的运行速度会减慢;
  2. 同名trace文件会相互覆盖,因此文件名要唯一;

读懂报告

Systrace报告的官网介绍中可以了解到我们通常关注Frames中为黄色的帧,通过点击对应的黄色圆点可以查看调用栈,从而定位问题点。

自定义事件

android.os.Trace作为SDK提供的辅助API工具,可以帮助我们实现自定义事件的统计。根据官网介绍,使用示例如下:

  • 演示Java层新增定义自定义事件
list_item.setOnItemClickListener((parent, view, position, id) -> {
    // 追踪列表项点击事件的耗时情况
    Trace.beginSection("OnListItemClick");
    try {
        ......
    } finally {
        // 为确保Trace.endSection执行,建议放到finally块中
        Trace.endSection();
    }
});
  • 演示native层新增定义自定义事件
//1. 定义在游戏内捕获自定义事件所用 ATrace 函数的函数指针
#include <android/trace.h>
#include <dlfcn.h>

void *(*ATrace_beginSection) (const char* sectionName);
void *(*ATrace_endSection) (void);

typedef void *(*fp_ATrace_beginSection) (const char* sectionName);
typedef void *(*fp_ATrace_endSection) (void);

//2. 在运行时加载 ATrace 符号,如以下代码段所示。通常在对象构造函数中执行此过程
// Retrieve a handle to libandroid.
void *lib = dlopen("libandroid.so", RTLD_NOW || RTLD_LOCAL);

// Access the native tracing functions.
if (lib != NULL) {
    // Use dlsym() to prevent crashes on devices running Android 5.1
    // (API level 22) or lower.
    ATrace_beginSection = reinterpret_cast<fp_ATrace_beginSection>(
        dlsym(lib, "ATrace_beginSection"));
    ATrace_endSEction = reinterpret_cast<fp_ATrace_endSection>(
        dlsym(lib, "ATrace_endSection"));
}
    

//3. 在自定义事件的开头和结尾分别调用 ATrace_beginSection() 和 ATrace_endSection()
#include <android/trace.h>

char *customEventName = new char[32];
sprintf(customEventName, "User tapped %s button", buttonName);

ATrace_beginSection(customEventName);
// Your app or game's response to the button being pressed.
ATrace_endSection();

然后运行APP,点击操作列表项,同事启用AS的CPU Profiler对过程进行录制,结束录制后将在分析报告中看到对应的自定义事件:
自定义跟踪事件

System Tracing

一款运行在Android 9(API 级别 28)或更高版本的终端工具,其功能与systrace类似,用于直接在手机上获取跟踪数据,生成.perfetto-trace[新系统,用Perfetto打开]/.ctrace[老系统,用Systrace生成报告]文件。
具体用法参考:捕获设备上的系统跟踪记录

TraceView

一款搭配Android Device Monitor的图形化日志查看工具,可以打开Systrace生成的.trace。截图如下:

LayoutInspector

AS自带的布局检测工具,可以查看当前运行的activity布局的详细树状结构。

Profiler

CPU Profiler

AS CPU Profiler官方使用说明
在这里插入图片描述

在这里插入图片描述

Memory Profiler

未完待续…

Network Profiler

未完待续…

Energy Profiler

未完待续…

代码检测

Looper监听

为什么主线程的代码最终都会在Looper.loop()中执行?

当APP启动后,其实主线程ActivityThread多数时间是阻塞在loop的queue.next()中的nativePollOnce()方法里,得益于Linux pipe/epoll机制,主线不会像我们日常多线程开发时使用独占锁导致的线程阻塞那样占用CPU资源,此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生,通过往pipe管道写端写入数据来唤醒主线程工作。
主线程休眠后被谁唤醒呢?答案是同个进程中的其他线程发过来的消息。比如:当我们启动一个activity时,实际是执行了如下的IPC通讯过程:

而线程4(ApplcaitionThread)接收到消息后,便通过ActivityThread.H实例向主线程发送消息,进而在主线程(ActivityThread)中触发ActivityThread.H.handleMessage()方法,从执行AMS的对应三大组件生命周期方法。

至此可以已经了解,我们编写的代码逻辑,最终都被转化为MessageQueue中一个个的Message,并在msg.target.dispatchMessage(msg)中被执行。所以要检测卡顿就很明显了,只要根据此方法执行的耗时时长就可以判断是否发生了卡顿。而正好,Looper#mLogging在dispatchMessage执行前后打印了日志消息,这便允许我们通过检测日志前后打印的内容来判断方法执行完毕与否。

  1. 上图中线程1、2、3、4是指Binder线程池中的线程,因为Binder方法都是运行在这个线程池中的,所以尽管ApplcaitionThreadAcitvityThread运行都运行在APP进程,却是不同的线程,所以还是需要借助Handler进行通讯;
  2. Handler是同一进程内不同线程间通讯的桥梁,这一点虽然是常识,还是要重点强调下。
  3. Looper.loop()方法中,同样执行于dispatchMessage前后的还有Trace#traceBegin/Trace#traceEnd方法,这也正是Systrace这个工具的实现原理。

代码实现示例

private void check() {
    Looper.getMainLooper().setMessageLogging(new Printer() {
        private final String START = ">>>>> Dispatching to";
        private final String END = "<<<<< Finished to";

        @Override
        public void println(String s) {
            if (s.startsWith(START)) {
                mCheckTask.start();
            } else if (s.startsWith(END)) {
                mCheckTask.end();
            }
        }
    });
}

private class CheckTask {
    private HandlerThread mHandlerThread = new HandlerThread("卡顿检测");
    private Handler mHandler;

    private final int THREAD_HOLD = 1000;

    public CheckTask() {
        mHandlerThread.start();
        mHandler = new Handler(mHandlerThread.getLooper());
    }

    private Runnable mRunnable = new Runnable() {
        @Override
        public void run() {
            log();
        }
    };

    public void start() {
        mHandler.postDelayed(mRunnable, THREAD_HOLD);
    }

    public void end() {
        mHandler.removeCallbacks(mRunnable);
    }
}


/**
* 输出当前异常或及错误堆栈信息。
*/
private void log() {
    StringBuilder sb = new StringBuilder();
    StackTraceElement[] stackTrace = Looper.getMainLooper().getThread().getStackTrace();
    for (StackTraceElement s : stackTrace) {
        sb.append(s + "\n");
    }

    Log.w(TAG, sb.toString());
}

相关开源库

Choreographer

Android SDK提供的工具类,用于实时获取当前屏幕绘制的FPS数据。

相关开源库

其实谷歌官网对于UI绘制帧率的检测也有相关方法的介绍:测试界面性能,使用的是adb dumpsys工具,感兴趣的可以看看。

LayoutInflaterCompat.setFactory2

LayoutInflater提供的hook入口,该方法可用于统计每个View的加载耗时。

Debug

android.os.Debug这个工具类提供了很多调试和性能检测相关的功能,比如:

  • startMethodTracing/stopMethodTracing
    在用户方法前后分别调用这两个方法,用于生成.trace文件。

  • dumpHprofData(String fileName)
    保存内存快照到本地文件,此文件可能体积非常大,不可在生产环境使用此方法。

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值