前言
我们都知道 Android Studio 里内置了 Profiler
tool 供大家对 App 在 Memory、CPU、Network、Power 等角度进行 dump 和分析。
但如果一个内存相关的 bug 是运行时发生的,而且很难复现,那么后期就很难准确定位发生时的具体状况。
值得兴奋的是 Android 15 将直面这个痛点:引入了 ProfilingManager
API,允许 app 对 Memory 进行动态的、随时随地的 dump。
生成的文件默认存在本地,也可以通过网络传递到 offboard,方便开发者事后回溯。
API 说明
ProfilingManager
主要提供了 3 个方法。
registerForAllProfilingResults
注册 profiling 请求的回调和执行的线程,需要如下两个参数:
Parameters | Descriptions |
---|---|
executor | Executor : 回调执行的线程池实例,不可为空 |
listener | Consumer : 携带 profiling 结果的 listener 实例,不可为空 |
unregisterForAllProfilingResults
注销 profiling 请求的回调,如果没有指定 listener 参数的话,将移除所有 callback。
Parameters | Descriptions |
---|---|
listener | Consumer : 待移除的回调,null 的话移除所有 |
requestProfiling
请求进行一次 profiling 操作,有非常详细的参数可供设置:
Parameters | Descriptions |
---|---|
profilingType | int : profiling 操作的类型,主要包括 dump Java 堆的 PROFILING_TYPE_JAVA_HEAP_DUMP ,dump 堆的 PROFILING_TYPE_HEAP_PROFILE , dump 栈的 PROFILING_TYPE_STACK_SAMPLING 和 dump 系统 trace 的PROFILING_TYPE_SYSTEM_TRACE ,参数不能为空。 |
parameters | Bundle : 携带请求额外的相关参数, 如果包含了未定义的参数类型,请求会失败,在 callback 当中以 ERROR_FAILED_INVALID_REQUEST 结果进行返回,参数可为空。 |
tag | String : 回来识别 dump 输出的 tag 标签,其中的前 20 个字符将会以小写的形式拼接到 dump 文件名中,参数可为空 |
cancellationSignal | CancellationSignal : 支持请求侧用来取消 dump 的 cancellation 实例,如果 dump 结果已出来的话,会被返回。参数可为空,此时将执行系统默认的超时时间,之后结束 dump。 |
executor | Executor : 回调执行的线程池实例,参数可为空。但如果没有其他 executor 注册的话,该请求会被无视。 |
listener | Consumer : 监听操作结果的实例,registerForAllProfilingResults() 注册的 callback 同样也会被回调,参数可为空。但如果没有其他 listener 注册的话,该请求会被无视。 |
需要说明的是:
- 很多时候,并不推荐直接使用该 API 进行 dump,相反可以采用 androidx 中封装好的高层级接口进行请求。该接口内部会依据可用选项和简化的参数进行正确地请求。
- 并非所有情况都会得到响应
- 需要同时考虑 result 的监听和执行它的线程池两个参数,要么都设置,要么都不设置交给 registerForAllProfilingResults() 一起设置
ProfilingResult
在上述提到的 registerForAllProfilingResults() 里会回调 ProfilingResult
参数过来,它用来封装单次请求 profiling 操作的结果。
主要提供了这几个方法来获得信息:
- getErrorCode():获取 profiling 请求失败的原因,如果成功的话,值为 ERROR_NONE 即 0,其他的还有 ERROR_FAILED_RATE_LIMIT_SYSTEM 等值
- getErrorMessage():获取额外的失败信息
- getTag():请求时传入的参数 tag,方便回溯
- getResultFilePath():获取 profiling 结果文件的路径
实战
留意一下,需要将 Android 15 的 SDK 升级到 revision 3 才可以看到该 API。
class ProfilingActivity : AppCompatActivity() {
private val singleThreadExecutor = Executors.newSingleThreadExecutor()
private val profilingResultConsumer = Consumer<ProfilingResult> {
Log.d(TAG_PROFILING, "accept profilingResult:${it.printProfilingResult()}")
}
private val profilingManager by lazy {
getSystemService(ProfilingManager::class.java)
}
override fun onCreate(savedInstanceState: Bundle?) {
...
binding.dump.setOnClickListener {
Log.d(TAG_PROFILING, "button dump tapped")
profilingManager.registerForAllProfilingResults(
singleThreadExecutor,
profilingResultConsumer
)
}
binding.request.setOnClickListener {
Log.d(TAG_PROFILING, "button request tapped")
profilingManager.requestProfiling(
// ProfilingManager.PROFILING_TYPE_SYSTEM_TRACE,
// ProfilingManager.PROFILING_TYPE_JAVA_HEAP_DUMP,
// ProfilingManager.PROFILING_TYPE_STACK_SAMPLING,
ProfilingManager.PROFILING_TYPE_HEAP_PROFILE,
null,
"TEST_FOR_PROFILING_MANAGER",
null,
singleThreadExecutor,
profilingResultConsumer
)
}
binding.stop.setOnClickListener {
Log.d(TAG_PROFILING, "button stop tapped")
profilingManager.unregisterForAllProfilingResults(profilingResultConsumer)
}
}
}
可以看到我们在代码里设置的 tag 为 “TEST_FOR_PROFILING_MANAGER” 。
我们点击 “register dump profile” button 开始注册回调,然后点击 “request dump profile” button 开始请求。
看下 Profiling 的 log 输出。
点击完 request 之后需要等待一段时间(系统默认的 dump 超时为 120s 左右)才会看到 dump 结果。
注意,不要重复点击,否则会收到如下错误的 callback。其中 3 对应的是 ERROR_FAILED_PROFILING_IN_PROGRESS,表示仍在 dump 中,该重复请求被拒绝。
ProfilingResult{errorCode:3 errorMessage:null resultFilePath:null tag:TEST_FOR_PROFILING_MANAGER}
下面我们以如下 4 种 dump 类型看下具体的 ProfilingResult
输出内容。
PROFILING_TYPE_JAVA_HEAP_DUMP
04-21 19:37:32.220 7184 7270 D Profiling: accept profilingResult:ProfilingResult{errorCode:0 errorMessage:null resultFilePath:/data/user/0/com.ellison.osvdemo/files/profiling/profile_testforprofilingmana_2024-04-21-19-37-26.perfetto-java-heap-dump tag:TEST_FOR_PROFILING_MANAGER}
可以看到成功输出了 Java heap 的 dump 文件,并且咱们的 tag 被拼接到了文件名中,而且将_删除并限制了 20 的长度。
所以咱们设置的 tag 最好不要包含字母以外的字符,并且不要过长,不然不方便定位 tag。
PROFILING_TYPE_HEAP_PROFILE
04-21 19:47:16.810 7474 7552 D Profiling: accept profilingResult:ProfilingResult{errorCode:0 errorMessage:null resultFilePath:/data/user/0/com.ellison.osvdemo/files/profiling/profile_testforprofilingmana_2024-04-21-19-45-11.perfetto-heap-profile tag:TEST_FOR_PROFILING_MANAGER}
和上面的 Java dump 一样,成功输出了 heap 全量的 dump 文件。
PROFILING_TYPE_STACK_SAMPLING
04-21 19:53:01.043 7704 7790 D Profiling: accept profilingResult:ProfilingResult{errorCode:0 errorMessage:null resultFilePath:/data/user/0/com.ellison.osvdemo/files/profiling/profile_testforprofilingmana_2024-04-21-19-51-55.perfetto-stack-sample tag:TEST_FOR_PROFILING_MANAGER}
dump stack 的记录也是一样,不再赘述。
PROFILING_TYPE_SYSTEM_TRACE
04-21 15:43:33.926 4730 4775 D Profiling: accept profilingResult:ProfilingResult{errorCode:7 errorMessage:Trace is not supported until redaction lands resultFilePath:null tag:null}
和前面 3 中 type 不同,这种 dump 系统 trace 的请求总是会直接失败。
ProfilingResult 结果对应的 errorCode 为 7,常量名称为 ERROR_FAILED_INVALID_REQUEST,表示请求失败了:这是一个不合法的 ProfilingRequest。
The request failed due to invalid ProfilingRequest.
单从这个 error 定义来看,压根不知道问题出在哪。
在 API 章节里我们提到过,如果调用 requestProfiling() 时传递的 Bundle 参数包含了不支持的 key-value,会造成 ERROR_FAILED_INVALID_REQUEST 错误。
但可以看到:实际上咱们的 DEMO 代码里啥 bundle 都没携带,所以应该不是这个原因。
好在,ProfilingRequest 结果还打携带了 errorMessage,其内容为:Trace is not supported until redaction lands。
笔者尝试依据该 message 找到蛛丝马迹,但无论在 Android 官网,还是在 Android 源码里,抑或是在 AOSP issue 的首页上,都没找到合相关的记录。
我猜测是 PROFILING_TYPE_SYSTEM_TRACE 需要某个 DOC 里没说明的 Bundle 参数,造成了失败。当然也有可能是 beta 版阶段的系统 bug。
后续再看。
perfetto 支持
以 PROFILING_TYPE_JAVA_HEAP_DUMP 的文件为例,从 data 目录里 pull 之后,在 “https://link.zhihu.com/?target=https%3A//ui.perfetto.dev/”) 中打开,可以进行分析。
)