阅读 2126

Android 开发太难了,这异常竟然捕获不到?

本文已授权个人公众号「鸿洋」原创发布。

背景

之前写的博客大家吐槽没有具体场景,这篇文章我们说一下背景。

有一天小张正在开心的和大家划着水,忽然运营同学嗷的一声拉了个群,告诉我线上出现高频聚集的崩溃反馈。

小张作为稳定性指标的负责同学,心理暗想:

不可能呀,稳定性波动报警极其敏感,这个没触发报警,大概率是用户反馈并不是Crash,只是描述像崩溃,被运营错误归类了吧。

打开反馈List,竟然还有个贴心的用户给发了个视频,非常明确的可以看出:

在点击某个Button跳转到下个Activity后,App果断退出。

这个时候小张心理有点慌了...

看起来确实是闪退,不过没事查下崩溃日志,先看看情况。

没想到...该用户没有崩溃相关日志。

这会疑惑更大了:

没有崩溃日志?这个运行场景都已经App彻底跑起来了,不管是Java/Native都应该有比较全的崩溃信息的呀。

正在困惑之际,一位同学伸出了援助之手:

我这里复现了!

在复现场景下,经过详细分析,最终定位是跳转Activity时,携带数据过多导致,小张跟领导简单汇报了一下,没想到被领导一顿反问:

  1. 为什么这个崩溃没有被捕获到?
  2. 如果捕获不到?后续怎么规避这个问题?

以上背景纯属虚构,单纯为了引出博客的内容,请勿对号入座。

从Demo复现开始

本文测试设备系统版本:Android-29

对于Java崩溃捕获,Android提供了捕获机制,即:

Thread.setDefaultUncaughtExceptionHandler
复制代码

那么我们先写个Java Crash捕获的代码:

Thread.UncaughtExceptionHandler defaultUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();

Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
    @Override
    public void uncaughtException(@NonNull Thread t, @NonNull Throwable e) {
    	 Log.e("zhy-crash-catch", "当前进程id:" + Process.myPid())
        Log.e("zhy-crash-catch", Log.getStackTraceString(e));
        if (defaultUncaughtExceptionHandler != null) {
            defaultUncaughtExceptionHandler.uncaughtException(t, e);
        }
    }
});

复制代码

可以放到Application#onCreate中,确保我们一会操作App的时候,运行了就可以。

然后编写我们我们第一个Activity,这个Activity内部极其简单,只有一个按钮点击跳转到目标Activity:

public class EntranceActivity extends AppCompatActivity {

    private Button mBtnGoAct;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_entrance);

        mBtnGoAct = findViewById(R.id.btn_go_act_01);
        mBtnGoAct.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(EntranceActivity.this, DataTransactActivity.class);
                startActivity(intent);
            }
        });
    }
}
复制代码

运行一下:

到这里小张的准备工作就完事了。

既然是Activity启动时携带数据过大,即binder在做跨进程传输时,最大可以携带多少数据。

带着这个问题,小张先Google搜索一下,毕竟一会还要汇报,话术还是要准备的。

找到了一个还比较靠谱的说法:

Binder 事务缓冲区具有一个有限的固定大小,当前为 1MB。你可别以为传递 1MB 以下的数据就安全了,这里的 1MB 空间并不是当前操作独享的,而是由当前进程所共享。也就是说 Intent 在 Activity 间传输数据,本身也不适合传递太大的数据。

大概意思就是,binder传输的时候,数据数据的最大限制大概为1M。

这个回答也基本符合小张平时的认知,于是乎,改了下代码,给我们的Intent加入了1M的数据:

Intent intent = new Intent(EntranceActivity.this, DataTransactActivity.class);
intent.putExtra("large_data", new byte[1 * 1024 * 0124]);
startActivity(intent);
复制代码

运行一下:

2021-08-29 11:06:40.656 22143-22143/com.example.zhanghongyang.kotlinlearn E/zhy-crash-catch: 当前进程id:4678
021-08-29 10:44:33.187 4678-4678/com.example.zhanghongyang.kotlinlearn E/zhy-crash-catch: java.lang.RuntimeException: Failure from system
        at android.app.Instrumentation.execStartActivity(Instrumentation.java:1711)
        at android.app.Activity.startActivityForResult(Activity.java:5173)
        at androidx.fragment.app.FragmentActivity.startActivityForResult(FragmentActivity.java:676)
        at android.app.Activity.startActivityForResult(Activity.java:5131)
        at androidx.fragment.app.FragmentActivity.startActivityForResult(FragmentActivity.java:663)
        at android.app.Activity.startActivity(Activity.java:5502)
        at android.app.Activity.startActivity(Activity.java:5470)
        at com.blog04.EntranceActivity$1.onClick(EntranceActivity.java:29)
        at android.view.View.performClick(View.java:7125)
        at android.view.View.performClickInternal(View.java:7102)
        at android.view.View.access$3400(View.java:801)
        at android.view.View$PerformClick.run(View.java:27301)
        at android.os.Handler.handleCallback(Handler.java:883)
        at android.os.Handler.dispatchMessage(Handler.java:100)
        at android.os.Looper.loop(Looper.java:214)
        at android.app.ActivityThread.main(ActivityThread.java:7319)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:934)
     Caused by: android.os.TransactionTooLargeException: data parcel size 1049052 bytes
        at android.os.BinderProxy.transactNative(Native Method)
        at android.os.BinderProxy.transact(BinderProxy.java:510)
        at android.app.IActivityTaskManager$Stub$Proxy.startActivity(IActivityTaskManager.java:3823)
   		  at android.app.Instrumentation.execStartActivity(Instrumentation.java:1705)
        at android.app.Activity.startActivityForResult(Activity.java:5173) 
复制代码

意料之中发生了崩溃,因为虽然我们写了1M的bytes,但是总还要带一些别的信息跨进程传输,所以整体肯定是突破1M了,导致发生了崩溃。

不过这个时候小张确开心不起来,因为这里虽然发生了崩溃,但是异常明显被捕获到了

解释不了:

为什么线上用户崩溃没有捕获到崩溃日志呢?

正当小张苦恼之际,一旁的羊叔把头伸了过来,看了眼代码,微微一笑,你把传输数据改小点试试。

嗯?改小点?

行吧,活马当死马医...

小张把数据传输从1M改成了800k,然后点击了运行:

Intent intent = new Intent(EntranceActivity.this, DataTransactActivity.class);
intent.putExtra("large_data", new byte[800 * 1024]);
startActivity(intent);
复制代码

大家觉得,这次运行结果是什么?

崩溃?正常启动?

看效果:

我擦嘞,应用还是发生了“闪退”...

不过,注意看图:

我们的Thread.UncaughtExceptionHandler没有捕获到异常。

小张暗自yeah了一声,终于复现了线上用户的行为。

但是两个疑惑铺面而来:

1. 不是说好的binder传输是1M的限制吗?难道是错的 ?

2. 为什么捕获不到呢?

虽然有疑惑,但这个时候小张没有停下,把800kb改成了700,600,500,400一直尝试。

发现,崩溃的数据量大概是500kb(不崩溃)-600kb(崩溃)之间。

疑惑重重

虽然复现了问题,但是疑惑一个都没解决,重新思考一下:

1. 不是说能够传输1M么,这怎么600kb就不行了,真理和实践差距就这么大吗?

2. 就算传输限制为500多kb,为什么捕获不到该闪退呢?

回到1M以上

查下数据量在1M以上时,捕获到的崩溃

2021-08-29 11:06:40.656 22143-22143/com.example.zhanghongyang.kotlinlearn E/zhy-crash-catch: 当前进程id: 22141
2021-08-29 10:56:59.788 22141-22141/com.example.zhanghongyang.kotlinlearn E/zhy-crash-catch: java.lang.RuntimeException: Failure from system
        at android.app.Instrumentation.execStartActivity(Instrumentation.java:1711)
        at android.app.Activity.startActivityForResult(Activity.java:5173)
        at androidx.fragment.app.FragmentActivity.startActivityForResult(FragmentActivity.java:676)
        at android.app.Activity.startActivityForResult(Activity.java:5131)
        at androidx.fragment.app.FragmentActivity.startActivityForResult(FragmentActivity.java:663)
        at android.app.Activity.startActivity(Activity.java:5502)
        at android.app.Activity.startActivity(Activity.java:5470)
        at com.blog04.EntranceActivity$1.onClick(EntranceActivity.java:29)
        at android.view.View.performClick(View.java:7125)
        at android.view.View.performClickInternal(View.java:7102)
        at android.view.View.access$3400(View.java:801)
        at android.view.View$PerformClick.run(View.java:27301)
        at android.os.Handler.handleCallback(Handler.java:883)
        at android.os.Handler.dispatchMessage(Handler.java:100)
        at android.os.Looper.loop(Looper.java:214)
        at android.app.ActivityThread.main(ActivityThread.java:7319)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:934)
     Caused by: android.os.TransactionTooLargeException: data parcel size 1049052 bytes
        at android.os.BinderProxy.transactNative(Native Method)
        at android.os.BinderProxy.transact(BinderProxy.java:510)
        at android.app.IActivityTaskManager$Stub$Proxy.startActivity(IActivityTaskManager.java:3823)

复制代码

可以最核心的代码是:

at android.os.BinderProxy.transact(BinderProxy.java:510)
at android.app.IActivityTaskManager$Stub$Proxy.startActivity
复制代码

就是这次跨进程调用,出现数据量过大的情况,我们仔细分析这个对象android.app.IActivityTaskManager$Stub$Proxy

如果大家熟知aidl,那么肯定能猜测出来:

  1. IActivityTaskManager这是个aidl文件;
  2. android.app.IActivityTaskManager$Stub 对象是运行在system进程的,有时我们称之为binder服务端;
  3. android.app.IActivityTaskManager$Stub$Proxy 这个Proxy对象是运行在我们app进程的,有时我们称之为binder代理;

Proxy对象通过transact方法调用到Stub对象的onTransact方法。

仔细看堆栈是调用transact的时候,也就是说这个异常确实发生在App进程侧,所以说这个崩溃能捕获也是正常的。

那么我们改成600k之后,为何就捕获不到了呢?

600k崩溃的分析

首先应用发生了“闪退”,但我们没有捕获到。

但是两次运行的代码路径肯定是一致的对吧。

也就是说,600k的时候:

android.app.IActivityTaskManager$Stub$Proxy.startActivity
复制代码

这个执行时没有发生崩溃的,那么也就是说我们的数据都给到system进程了,然后轮到system进程处理了。

难道说:

难度system进程处理的时候发生了问题?

另外还有个上图奇怪的现象:

大家仔细看那个图几遍,你会发现这个应用的“闪退”,竟然还有个退出动画...

这像是收到了什么指令,被kill的感觉,而不是闪退

再探600k

上面都是猜想,虽然我们的tag:zhy-crash-catch没有捕获到异常。

这个时候,我们可以放开过滤,既然是system进程处理遇到什么问题,kill我们,那么系统应该有一些日志打印出来:

transact_04.gif

果然有一个堆栈:

2021-08-29 11:22:32.628 2287-4464/system_process E/JavaBinder: !!! FAILED BINDER TRANSACTION !!!  (parcel size = 618984)
2021-08-29 11:22:32.629 2287-4464/system_process E/ActivityTaskManager: Second failure launching com.example.zhanghongyang.kotlinlearn/com.blog04.DataTransactActivity, giving up
    android.os.TransactionTooLargeException: data parcel size 618984 bytes
        at android.os.BinderProxy.transactNative(Native Method)
        at android.os.BinderProxy.transact(BinderProxy.java:510)
        at android.app.IApplicationThread$Stub$Proxy.scheduleTransaction(IApplicationThread.java:2448)
        at android.app.servertransaction.ClientTransaction.schedule(ClientTransaction.java:135)
        at com.android.server.wm.ClientLifecycleManager.scheduleTransaction(ClientLifecycleManager.java:47)
        at com.android.server.wm.ActivityStackSupervisor.realStartActivityLocked(ActivityStackSupervisor.java:853)
        at com.android.server.wm.RootActivityContainer.attachApplication(RootActivityContainer.java:783)
        at com.android.server.wm.ActivityTaskManagerService$LocalService.attachApplication(ActivityTaskManagerService.java:6827)
        at com.android.server.am.ActivityManagerService.attachApplicationLocked(ActivityManagerService.java:4953)
        at com.android.server.am.ActivityManagerService.attachApplication(ActivityManagerService.java:5030)
        at android.app.IActivityManager$Stub.onTransact(IActivityManager.java:2003)
        at com.android.server.am.ActivityManagerService.onTransact(ActivityManagerService.java:2628)
        at android.os.Binder.execTransactInternal(Binder.java:1021)
        at android.os.Binder.execTransact(Binder.java:994)
复制代码

注意信息:

TransactionTooLargeException: data parcel size 618984 bytes

还真是600k都超过了限制,此外大家可以发现,这次打出崩溃堆栈的进程是:

2287(进程id)-4464(线程id)

我们可以打印下当前设备的2287是什么进程:

zhanghongyang$ adb shell ps  | grep 2287
system        2287  1979 2203952 259552 ep_poll             0 S system_server
复制代码

可以看到是我们的system进程。

其次,从堆栈上看,还有个Second failure launching,已经是第二次尝试了?无所谓了,我们直接看最核心的几行堆栈:

com.android.server.am.ActivityManagerService.attachApplication(ActivityManagerService.java:5030)
        at android.app.IActivityManager$Stub.onTransact(IActivityManager.java:2003)
        at com.android.server.am.ActivityManagerService.onTransact(ActivityManagerService.java:2628)
        at android.os.Binder.execTransactInternal(Binder.java:1021)
        at android.os.Binder.execTransact(Binder.java:994)
复制代码

可以看到,这次运行崩溃时的对象是IActivityManager$Stub,并且是onTransact方法,也就说是,这个堆栈确实是system进程发生的。

这个时候我们已经能基本猜出原因了:

system进程在收到我们的数据,在执行某些操作后,发生了数据过大的问题

那么具体是什么操作发生数据过大呢?一般这些都是跨进程传输的时候才检测吧?

莫急,我们再往上看几行堆栈:

at android.os.BinderProxy.transactNative(Native Method)
        at android.os.BinderProxy.transact(BinderProxy.java:510)
        at android.app.IApplicationThread$Stub$Proxy.scheduleTransaction(IApplicationThread.java:2448)
        at android.app.servertransaction.ClientTransaction.schedule(ClientTransaction.java:135)
复制代码

看到一个android.app.IApplicationThread,这个大家熟悉吗?

当我们app进程和system进程发生通信的时候,system进程会通过applicationThread对象和app进程通信。

不过这个时候app进程时binder服务端,也就是android.app.IApplicationThread$Stub,而system进程是Proxy,所以可以看出,system进程在尝试执行:

android.app.IApplicationThread$Stub$Proxy.scheduleTransaction
复制代码

发生了数据过大的问题。

也就是说:

system进程在通过ApplicationThread对象,和app进程通信的时候发现数据过大,从而抛出了异常。

但是system进程时何等的重要,肯定是不能崩溃的,所以system进程捕获了异常,但是也不能不通知app进程呀,不然app进程岂不是没有反应了,于是乎直接kill掉了app进程。

到这里,我们基本弄清楚这次闪退没有堆栈的原因了,因为实际上不是crash,而是system进程在跟我们交互过程中发生了异常,然后把我们kill掉了。

其实我们可以拿着异常堆栈,一个个类去看哪里进行了catch,抛出,例如,我们打开ActivityStackSupervisor#realStartActivityLocked可以看到:

transact_05.png

注意哈:这里的堆栈因为被system进程捕获到了,所以我们看到的异常堆栈,只能说system进程确实遇到了问题,但并一定不是最后的现场。

第一个问题搞清楚了,回到第二个问题:

为什么600kb的数据量发生了崩溃?App进程和system进程交互的时候调用IActivityTaskManager$Stub$Proxy.startActivity时可以的,为啥回来调用IApplicationThread$Stub$Proxy.scheduleTransaction就不行了呢?

oneway的出现

注意我们是android29的版本,下面的描述会跟系统版本相关。

因为最终崩溃是发生在BinderProxy.transactNative(Native Method)这个环节,得分析到cpp的代码,我只能说告辞!

但是其实我们也能猜,因为两次崩溃最终调用的都是BinderProxy.transactNative,也就说可能限制的逻辑并不在其内部,所以1M,600k的限制跟外部的调用相关,从崩溃链上分析,其实不同的就是:

去:IActivityTaskManager\$Stub\$Proxy.startActivity
回:IApplicationThread\$Stub\$Proxy.scheduleTransaction
复制代码

这两个方法类还是aidl生成的,也就是说模板代码,那能有什么不同的地方。

不对?生成的,模板代码?

也就是说,如果这个模板代码中想有不同,那么aidl中方法上必有不同。

那我们找下:

startActivity:

interface IActivityTaskManager {
    int startActivity(in IApplicationThread caller, in String callingPackage,
            in String callingFeatureId, in Intent intent, in String resolvedType,
            in IBinder resultTo, in String resultWho, int requestCode,
            int flags, in ProfilerInfo profilerInfo, in Bundle options);
复制代码

scheduleTransaction:

oneway interface IApplicationThread {
 	void scheduleTransaction(in ClientTransaction transaction);
}
复制代码

方法上并没有什么神奇之处,难道是根据参数判断的限制?

我们看下BinderProxy.tranactNative:

public native boolean transactNative(int code, Parcel data, Parcel reply,
            int flags) throws RemoteException;
复制代码

实在是不像...

再看一眼两个方法的声明,忽然发现:

咦...IApplicationThread前面多了个关键词oneway。

好了,不再继续往下分析了,再往下得翻我看不懂的代码了,我们直接公布答案看了:

其实问题就出在oneway上:

在binder驱动中,除了记录Binder事务的缓冲区空间,还会记录一个异步事务(ONE_WAY)的空间,这个空间为前者的1/2。

transact_06.png

图片来源于小缘在wanandroid中的每日一问的回答:

wanandroid.com/wenda/show/…

关于oneway详细的分析也可以参考小缘的回答,小缘是强呀。

到这里,我们基本心里的困惑基本都理清了。

我们还是来总结下:

  1. 为什么这样的“闪退”我们的UncaughtExceptionHandler捕获不到?

因为实际上问题发生在system进程在尝试通过ApplicationThread对象跟app进程通信的时候发生了错误,这个错误被system进程捕获打印,然后kill掉了我们app;

  1. 不是说binder传输数据量大概为1M吗?为什么600k就会失败?

其实是oneway机制导致的,被oneway修饰的方法(修饰类,则方法均为oneway),为异步事务,而异步事务的空间只有binder事务缓冲区空间的一半,我们一直认知的1M是binder事务缓冲空间大小,那么oneway修饰的话,大约只有512k。

那么我又有一个问题了:

也就是说在29的版本上,当我们在启动Activity的时候,不要说携带1M数据了,只要是超过512kb就会有问题,而这个问题在线上是捕获不到的,怎么办呢?

分析如何监控

目前我们发现其实512k就是一个危险的跨进程传输数据量了,那么我们最好的方式就是提前拦截上报。

什么意思呢?

我们可以hook拦截startActivity的方法,在发现数据量超过512k,又或者到达一个我们觉得危险的区间的时候,我们就上报搞个报警:

[error]你这里携带数据量已经快到风险值512k了,需要修改下

然后再让相关同学去优化。

hook点在哪呢?因为我们要获得传输数据量的大小,所以参数携带数据量可量化是非常关键的。

我们可以盯着最初的崩溃时的堆栈来看下:

androidx.fragment.app.FragmentActivity.startActivityForResult(FragmentActivity.java:663)
        at android.app.Activity.startActivity(Activity.java:5502)
        at android.app.Activity.startActivity(Activity.java:5470)
        at com.blog04.EntranceActivity$1.onClick(EntranceActivity.java:29)
        at android.view.View.performClick(View.java:7125)
        at android.view.View.performClickInternal(View.java:7102)
        at android.view.View.access$3400(View.java:801)
        at android.view.View$PerformClick.run(View.java:27301)
        at android.os.Handler.handleCallback(Handler.java:883)
        at android.os.Handler.dispatchMessage(Handler.java:100)
        at android.os.Looper.loop(Looper.java:214)
        at android.app.ActivityThread.main(ActivityThread.java:7319)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:934)
     Caused by: android.os.TransactionTooLargeException: data parcel size 1049052 bytes
        at android.os.BinderProxy.transactNative(Native Method)
        at android.os.BinderProxy.transact(BinderProxy.java:510)
        at android.app.IActivityTaskManager$Stub$Proxy.startActivity(IActivityTaskManager.java:3823)
复制代码

想要获得到传输数据的大小,那么还是transact方法合适,因为Parcel有个dataSize方法:

android.os.BinderProxy.transact(BinderProxy.java:510)
复制代码

这个路径最合适。

如果你点开BinderProxy这个类,会发现其实源码中也有一个检测大小的逻辑,默认是关闭的:

public boolean transact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
        Binder.checkParcel(this, code, data, "Unreasonably large binder buffer");

        if (mWarnOnBlocking && ((flags & FLAG_ONEWAY) == 0)) {
            // For now, avoid spamming the log by disabling after we've logged
            // about this interface at least once
            mWarnOnBlocking = false;
            Log.w(Binder.TAG, "Outgoing transactions from this process must be FLAG_ONEWAY",
                    new Throwable());
        }

        try {
            return transactNative(code, data, reply, flags);
        } finally {
            if (tracingEnabled) {
                Trace.traceEnd(Trace.TRACE_TAG_ALWAYS);
            }
        }
    }
复制代码

checkParcel:

static void checkParcel(IBinder obj, int code, Parcel parcel, String msg) {
    if (CHECK_PARCEL_SIZE && parcel.dataSize() >= 800*1024) {
        // Trying to send > 800k, this is way too much
        StringBuilder sb = new StringBuilder();
        sb.append(msg);
        sb.append(": on ");
        sb.append(obj);
        sb.append(" calling ");
        sb.append(code);
        sb.append(" size ");
        sb.append(parcel.dataSize());
        sb.append(" (data: ");
        parcel.setDataPosition(0);
        sb.append(parcel.readInt());
        sb.append(", ");
        sb.append(parcel.readInt());
        sb.append(", ");
        sb.append(parcel.readInt());
        sb.append(")");
        Slog.wtfStack(TAG, sb.toString());
    }
}
复制代码

好了,不说了,开干。

准备hook android.app.IActivityTaskManager中的相关方法。

开始Hook IActivityTaskManager

关于hook这块,这篇文章我先简单的描述下,因为我后面还会写一篇重点就是如何hook各种系统的服务的博客,那篇博客会从aidl文件开始分析,引申出一个通信的hook系统服务方案。

既然要hookIActivityTaskManager,那么首先我们得了解下整个调用过程,再次阅读前面崩溃的堆栈:

Caused by: android.os.TransactionTooLargeException: data parcel size 1049052 bytes
at android.os.BinderProxy.transactNative(Native Method)
at android.os.BinderProxy.transact(BinderProxy.java:510)
at android.app.IActivityTaskManager$Stub$Proxy.startActivity(IActivityTaskManager.java:3823)
at android.app.Instrumentation.execStartActivity(Instrumentation.java:1705)
at android.app.Activity.startActivityForResult(Activity.java:5173) 
复制代码

注意:如果你看很久前的博客,会发现startActivity相关在ActivityManager中,但是新版本在IActivityTaskManager中,所以一定要注意测试设备版本,本文测试设备Android-29.

那么我们看下Instrumentation.execStartActivity的代码:

transact_10.png

可以看到直接调用的ActivityTaskManager.getService().startActivity,跟过去:

transact_11.png

可以看到ActivityTaskManager对象实际是由下面两行代码产生的:

final IBinder b = ServiceManager.getService(Context.ACTIVITY_TASK_SERVICE);
return IActivityTaskManager.Stub.asInterface(b);
复制代码

再看一眼ServiceManager.getService:

transact_12.png

可以看到有个sCache,是个静态的ArrayMap对象:

/**
 * Cache for the "well known" services, such as WM and AM.
 */
@UnsupportedAppUsage
private static Map<String, IBinder> sCache = new ArrayMap<String, IBinder>();

复制代码

也就说我们依赖反射可以做到换掉IActivityTaskManager对象的IBinder对象。

而IBinder对象即包含我们需要监控的transact方法。

那么问题简单了,hook流程为:

  1. 首先我们通过ServiceManager中的sCache获取到原来activity_task对应的IBinder;
  2. 因为IBinder是个接口,然后我们通过动态代理创造一个IBinder的代理对象IBinderProxy;
  3. 再把IBinderProxy放到ServiceManager的sCache中;
  4. 最后在Application#attachBaseContext中调用;

那源码不复杂:

public static void hook() {
    try {
        // 1. 首先我们通过ServiceManager中的sCache获取到原来activity_task对应的IBinder;
        Class serviceManager = Class.forName("android.os.ServiceManager");
        Method getServiceMethod = serviceManager.getMethod("getService", String.class);

        Field sCacheField = serviceManager.getDeclaredField("sCache");
        sCacheField.setAccessible(true);
        Map<String, IBinder> sCache = (Map<String, IBinder>) sCacheField.get(null);
        Map<String, IBinder> sNewCache;

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            sNewCache = new ArrayMap<>();
            sNewCache.putAll(sCache);
        } else {
            sNewCache = new HashMap<>(sCache);
        }

        IBinder activityTaskRemoteBinder = (IBinder) getServiceMethod.invoke(null, "activity_task");

		  // 步骤2,步骤3
        sNewCache.put("activity_task", (IBinder) Proxy.newProxyInstance(serviceManager.getClassLoader(),
                new Class[]{IBinder.class},
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        Log.d("zhy", "activity_task method = " + method.getName() + ", args = " + Arrays.toString(args));
                        // TODO 在这里检查
                        return method.invoke(activityTaskRemoteBinder, args);
                    }
                }));
        sCacheField.set(null, sNewCache);
        Log.d("zhy", "hook success");
    } catch (Exception e) {
        e.printStackTrace();
    }
}

复制代码

然后我们在invoke方法中,关注transact方法,那么现在问题来了,怎么获得跨进程传输参数的大小呢?

我们看一眼,IBinder#transact方法:

public boolean transact(int code, @NonNull Parcel data, @Nullable Parcel reply, int flags)
        throws RemoteException;
复制代码

注意其第二个参数Parcel data,其实我们传递的参数最终会序列化为Parcel对象,所以我们只要知道这个data的大小就可以了。

而Parcel刚好有个方法:dataSize()

/**
 * Returns the total amount of data contained in the parcel.
 */
public final int dataSize() {
    return nativeDataSize(mNativePtr);
}
复制代码

那检测的代码就简单了:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    Log.d("zhy", "activity_task method = " + method.getName() + ", args = " + Arrays.toString(args));
    if ("transact".equals(method.getName())) {
        if (args != null && args.length > 1) {
            Object arg = args[1];
            if (arg instanceof Parcel) {
                Parcel parcelArg = (Parcel) arg;
                int dataSize = parcelArg.dataSize();
                if (dataSize > 300 * 1024) {
                		// TODO 报警
                    Log.e("zhy", Log.getStackTraceString(new RuntimeException("[error]TransactionTooLargeException: parcel size exceed 300Kb:" + dataSize)));
                    if (BuildConfig.DEBUG) {
                        if (dataSize > 512 * 1024) {
                            throw new RuntimeException("[error]TransactionTooLargeException: parcel size exceed 300Kb:" + dataSize);
                        }
                    }
                }
            }
        }
    }
    return method.invoke(activityTaskRemoteBinder, args);
}
复制代码

在transact方法中:

  1. 如果传递数据超过300k,我们可以在控制台打印error或者弹出toast提醒,又或者上报到服务端产生一个报警;
  2. 如果在DEBUG状态,传递超过512k,则我们可以直接崩溃,注意这个崩溃我们是可以捕获到的,也就是崩溃后台可以看到日志;

我们测试一哈:

先测试超过300k:

transact_13.gif

可以看到我们成功捕获到了这次传输,应用处于正常运行状态:

2021-09-05 21:40:33.062 13903-13903/com.example.zhanghongyang.kotlinlearn E/zhy: java.lang.RuntimeException: 
    [error]TransactionTooLargeException: parcel size exceed 300Kb:308700
        at com.blog04.Hooker$1.invoke(Hooker.java:55)
        at java.lang.reflect.Proxy.invoke(Proxy.java:1006)
        at $Proxy0.transact(Unknown Source)
        at android.app.IActivityTaskManager$Stub$Proxy.startActivity(IActivityTaskManager.java:3823)
        at android.app.Instrumentation.execStartActivity(Instrumentation.java:1705)
复制代码

在测试一个超过512k的:

transact_14.gif

统一被我们监控到了,并且触发了我们手动抛出的异常,触发崩溃成功被defaultUncaughtExceptionHandler捕获。

2021-09-05 21:43:15.037 13901-13901/com.example.zhanghongyang.kotlinlearn E/zhy-crash-catch: 当前进程id:13901
2021-09-05 21:43:15.038 13901-13901/com.example.zhanghongyang.kotlinlearn E/zhy-crash-catch: java.lang.RuntimeException: 
    [error]TransactionTooLargeException:parcel size exceed 300Kb:525788
        at com.blog04.Hooker$1.invoke(Hooker.java:58)
        at java.lang.reflect.Proxy.invoke(Proxy.java:1006)
        at $Proxy0.transact(Unknown Source)
        at android.app.IActivityTaskManager$Stub$Proxy.startActivity(IActivityTaskManager.java:3823)
复制代码

假设你没有写hook这段代码,那么513k的时候就会触发app被直接kill,且捕获不到异常信息。

注意掉hook代码后:

transact_15.gif

ok,到此从遇到一个无法捕获的异常,到分析,再到我们最后的监控机制就完成了。

大家通过这篇文章可以学习到:

  1. 遇到这类奇怪的问题,可以仔细分析,详细推敲,猜测与验证;
  2. binder在通信过程中,并不是携带1M以下的数据就安全了;
  3. binder由于数据量过大导致的崩溃,Java异常捕获机制捕获不到;
  4. 针对这类捕获不到的问题,我们自己提前拦截,做异常报警,提前发现问题。

告辞,下篇见!

你可以添加微信公众号:鸿洋,这样可以第一时间接收文章。

文章分类
Android
文章标签