hook 技术又叫做钩子函数,在系统没有调用该函数之前,钩子程序先捕捉该消息,钩子函数先得到控制权,这时钩子函数即可以加工处理(改变)该函数的执行行为,还可以强制结束消息的传递。简单来说,就是把系统的程序拉出来,变成我们自己执行的代码片段。
实现hook我们必须要知道java的反射和动态代理。
Java反射机制详解_贺兰猪的博客-CSDN博Java动态代理_贺兰猪的博客-CSDN博客Java反射机制详解_贺兰猪的博客-CSDN博客
tips:这一小段源码层面我们主要针对于Android7.x。
相信Android朋友们平时开发的时候应该都遇到过token失效。
按照正常的流程,是不会出现这种异常。但是由于在某些情况下, Android
进程某个 UI 线程的某个消息阻塞。导致 TN
的 show
方法 post
出来 0 (显示) 消息位于该消息之后,迟迟没有执行。这时候,NotificationManager
的超时检测结束,删除了 WMS
服务中的 token
记录。也就是如图所示,删除 token
发生在 Android
进程 show
方法之前。这就导致了我们上面的异常。
整个toast显示原理及分析大家可以完整的看下QQ音乐技术团队的分析,(上图来源也是那)[Android] Toast问题深度剖析(一) - 腾讯云开发者社区-腾讯云
用sleep的方式并没有复现出来这个token is valid,但阅读Toast源代码后可以用另一种方式来复现这个BadTokenException:
val mw = getSystemService(WINDOW_SERVICE) as WindowManager
val tv = TextView(this)
tv.layoutParams = WindowManager.LayoutParams(1, 1)
tv.text = "模拟toast悬浮窗"
val params = WindowManager.LayoutParams()
params.type = WindowManager.LayoutParams.TYPE_TOAST
mw.addView(tv, params)
Toast.makeText(this, "xxxx", Toast.LENGTH_SHORT).show()
因为type==TYPE_TOAST的类型的toast不能重复添加,所以这样也会报一个BadTokenException,接下来我们就要通过这个demo,用hook的解决方案来解决这个异常。
阅读源码我们发现,在Android 7.0 Toast.java handleShow方法:
和在Android 8.0 Toast.java上:
对比发现在Android 8.0中,在WindowManager进行addView的时候8.0进行了一层try catch保护,而在7.0上并没有。那么我们就可以参考8.0的方法,直接catch住这个异常。
寻找hook点
尽量hook静态变量和单例对象
尽量hook public的对象和方法
查看调用链,mHandler中发了一个消息,在handlerMessage中处理这个toast显示的消息后调用handlerShow,mHandler是TN类中的变量,Toast 里面有一个变量mTN(TN类)。所以就很简单了,我们hook点就定位这个mTN,然后反射替换TN的内部成员变量mHandler,对handleMessage方法添加try-catch做到保护即可。
public class HookToastUtil {
private static Field sField_N;
private static Field sField_TN_Handler;
private static Toast mToast;
private HookToastUtil() {
}
public static void show(Context context, CharSequence message, int duration) {
if (mToast == null) {
mToast = Toast.makeText(context.getApplicationContext(), message, duration);
hook(mToast);
} else {
mToast.setDuration(duration);
mToast.setText(message);
mToast.show();
}
}
public static void show(Context context, @StringRes int resId, int duration) {
if (mToast == null) {
mToast = Toast.makeText(context.getApplicationContext(), resId, duration);
hook(mToast);
} else {
mToast.setDuration(duration);
mToast.setText(context.getString(resId));
}
mToast.show();
}
private static void hook(Toast toast) {
if (Build.VERSION.SDK_INT != Build.VERSION_CODES.N_MR1) {
return;
}
try {
Class> cls = Class.forName("android.widget.Toast");
sField_N = cls.getDeclaredField("mTN");
sField_N.setAccessible(true);
sField_TN_Handler = sField_N.getType().getDeclaredField("mHandler");
sField_TN_Handler.setAccessible(true);
Object tn = sField_N.get(toast);
Handler handler = (Handler) sField_TN_Handler.get(tn);
sField_TN_Handler.set(tn, new ReplaceHandler(handler));
} catch (Exception e) {
e.printStackTrace();
}
}
private static class ReplaceHandler extends Handler {
private Handler tnHandler;
public ReplaceHandler(Handler handler) {
this.tnHandler = handler;
}
@Override
public void handleMessage(Message msg) {
try{
tnHandler.handleMessage(msg);
}catch (Exception e){
e.printStackTrace();
}
}
}
}
调用HookToastUtil.show(this,"111",1000) 替换之前的Toast.makeText就不会报错了