Android性能优化-实践篇:Frida监测Binder调用

一、前言

在目前的卡顿优化中,主线程的Binder调用优化是一个绕不过的问题,实际上,在目前的工具上,例如使用Perfetto,可以看到binder_transaction,针对某个特定的binder_transaction,也可以找到对端,但是如果想要知道对端的name、code甚至是堆栈,就是一件非常困难的事情。

二、eBPF+golang监控Binder调用

此前,在看雪上面看到一篇文章,[原创]btrace:binder_transaction+eBPF+Golang实现通用的Android APP动态行为追踪工具,此文通过eBPF+golang实现了对Binder调用的监测,但是只能拿到调用的name+function,如下图所示。

这是一个可以直接push到Android系统执行的工具,但是在使用此工具的时候,发现了一些问题

  • 1、不同手机上面的Code和Function映射不一致,需要多次编译
  • 2、只有部分应用可以正常映射,例如抖音、微信,别的应用一概不行。

第二个问题经过debug发现是由于拿到的data.ptr.buffer是内核态地址,但是此处不可能是内核态地址,而是用户态的,对于内部的原理不清楚,但是通过mask高位就可以正常读取了,猜测是内核做了一次转换。

修改如下:

c++ 复制代码
bpf_probe_read_user(binder_transaction_event->chunk_data, probe_read_size, (void *)((data.ptr.buffer + i * CHUNK_SIZE) & 0x0000FFFFFFFFFFFF));

编译后重新push,测试可以使用。

具体的原理可以参考原文,大致的原理如下:

  • 1、通过eBPF挂载binder_transaction
  • 2、解析Parcel数据,拿到其中的ServerName和Code
  • 3、通过映射拿到Code对应的Function
  • 4、以可读的形式输出

eBPF也是非常强大的工具,但是偏底层,这里我们不再赘述,以后有机会再做分享。

三、Frida+Binder

对于性能优化来讲,我们想要的几个信息是:

  • 1、拿到ServerName+Function
  • 2、拿到耗时
  • 3、拿到堆栈

目前前面的工具已经可以满足第一点,但是后面的不好拿到,那么Frida能做到吗?答案是肯定的。

3.1 分析

首先,观察aidl调用,可以看到第一个调用的方法是writeInterfaceToken,这个方法完成了对ServerName的写入,所以我们可以通过这个拿到ServerName,将ServerName与Parcel做映射。

基本上所有的Java层Binder调用都要经过BinderProxy,所以我们可以Hook BinderProxy的transact方法拿到Code、Parcel,通过从映射里面取数据,我们直接拿到ServerName,这里还有Code信息。

实测解析会出现一些问题,但是目前我们是在Java层进行Hook,所以我们直接hook writeInterfaceToken,在这里直接保存Parcel对ServerName的映射。

然后,Frida可以直接无视私有类,直接通过ClassLoader加载出BinderTransactionNameResolver类,直接进行方法转换,拿到FunctionName。

最后,我们打印出方法前后的方法耗时,以及堆栈信息。

至此,我们就拿到了所有信息

3.2 实现

这里我直接贴一下实现:

JavaScript 复制代码
Java.perform(() => {
    const BinderProxy = Java.use("android.os.BinderProxy");
    const Parcel = Java.use("android.os.Parcel");
    const ResolverClass = Java.use("com.android.internal.os.BinderTransactionNameResolver");
    const binderResolver = ResolverClass.$new();
    const Thread = Java.use("java.lang.Thread");
    var parcelMap = new Map();
    Parcel.writeInterfaceToken.implementation = function (name) {
        parcelMap[this] = name;
        return this.writeInterfaceToken(name);
    }
    BinderProxy.transact.overload('int', 'android.os.Parcel', 'android.os.Parcel', 'int').implementation = function (code, data, reply, flags) {
        let log = "\n=== [Binder Call Detected] ===\n";
        log += "→ Code :", code;
        const thread = Thread.currentThread();
        log += "→ Thread:" + thread.getName() + "\n";
        try {
            var name = parcelMap[data];
            log += "→ InterfaceToken:" + name + "\n";
            const stubClassName = name + "$Stub";
            let stubClass = null;
            try {
                stubClass = Java.use(stubClassName);
                const methodName = binderResolver.getMethodName(stubClass.class, code);
                log += "→ Method Name:" + methodName + "\n";
            } catch (e) {
                console.warn("⚠️ Stub class not found or getMethodName failed:", stubClassName);
            }
            const Throwable = Java.use("java.lang.Throwable");
            const stack = Throwable.$new().getStackTrace();
            var startTime = Date.now();
            var ret = this.transact(code, data, reply, flags);
            log += `Binder call cost : ${Date.now() - startTime}ms`;
            if (thread.getName() == "main") {
                log += "→ Java Stack:\n";
                for (let i = 0; i < stack.length; i++) {
                    log += "    at " + stack[i].toString() + "\n";
                }
            }
            console.log(log);
            return ret;
        } catch (e) {
            console.error("❌ Error parsing Parcel manually:", e);
        }
        return this.transact(code, data, reply, flags);
    };
});

结果如下:

四、总结

到此,我们拿到了检测Binder调用的所有信息,对于卡顿优化,这是个很有用的输入,可以检测到我们应用是否存在异常的在主线程的Binder调用。

但是Frida只是用于线下检测,如果要用线上监听的话,需要考虑别的方式实现。

参考

1、[原创]btrace:binder_transaction+eBPF+Golang实现通用的Android APP动态行为追踪工具

相关推荐
宋智孝的小迷弟1 小时前
抽丝剥茧带你掌握 Kotlin Flow(一):协程时代的异步数据流处理“神器”
android·面试·app
newki18 小时前
学习笔记,关于NDK/JNI的简介与实战
android·c++·app
Jiaberrr4 天前
uniapp 安卓 APP 后台持续运行(保活)的尝试办法
android·前端·javascript·uni-app·app·保活
iOS阿玮4 天前
社交的本质是价值交换,请不要浪费别人的时间。
uni-app·app·apple
Xy9104 天前
AppTrace技术全景:开发者视角下的工具链与实践经验
app
iOS阿玮5 天前
苹果2024透明报告看似更加严格的背后是利好!
uni-app·app·apple
yxc_inspire5 天前
基于Qt的app开发第十三天
c++·qt·app·tcp·面向对象
iOS阿玮6 天前
苹果审核被拒4.1-Copycats过审技巧实操
uni-app·app·apple
宋智孝的小迷弟7 天前
Android 异步数据流:Kotlin Flow 为何成为新一代“利器”?LiveData 又有何局限?
android·面试·app