版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.jianshu.com/p/a47bcf62109c
一,背景
这是个沙雕操作,原因是:在小米手机的部分机型上,弹Toast时会在吐司内容前面带上app名称,如下:
此时产品经理发话了:为了统一风格,在小米手机上去掉Toast前的应用名。
网上有以下解决方案,比如:先给toast
的message
设置为空,然后再设置需要提示的message
,如下:
Toast toast = Toast.makeText(context, “”, Toast.LENGTH_LONG);
toast.setText(message);
toast.show();
但这些都不能从根本上解决问题,于是Hook Toast的方案诞生了。
二,分析
首先分析一下Toast的创建过程.
Toast的简单使用如下:
Toast.makeText(this,"abc",Toast.LENGTH_LONG).show();
1,构造toast
通过makeText()
构造一个Toast,具体代码如下:
public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
@NonNull CharSequence text, @Duration int duration) {
if (Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM)) {
Toast result = new Toast(context, looper);
result.mText = text;
result.mDuration = duration;
return result;
} else {
Toast result = new Toast(context, looper);
View v = ToastPresenter.getTextToastView(context, text);
result.mNextView = v;
result.mDuration = duration;
return result;
}
}
makeText()
中也就是设置了时长以及要显示的文本或自定义布局,对Hook没有什么帮助。
2,展示toast
接着看下Toast的show()
:
public void show() {
...
INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;
final int displayId = mContext.getDisplayId();
try {
if (Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM)) {
if (mNextView != null) {
// It's a custom toast
service.enqueueToast(pkg, mToken, tn, mDuration, displayId);
} else {
// It's a text toast
ITransientNotificationCallback callback =
new CallbackBinder(mCallbacks, mHandler);
service.enqueueTextToast(pkg, mToken, mText, mDuration, displayId, callback);
}
} else {
// 展示toast
service.enqueueToast(pkg, mToken, tn, mDuration, displayId);
}
} catch (RemoteException e) {
// Empty
}
}
代码很简单,主要是通过service
的enqueueToast()
和enqueueTextToast()
两种方式显示toast。
service
是一个INotificationManager
类型的对象,INotificationManager
是一个接口,这就为动态代理提供了可能。
service
是在每次show()
时通过getService()
获取,那就来看看getService()
:
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
private static INotificationManager sService;
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
static private INotificationManager getService() {
if (sService != null) {
return sService;
}
sService = INotificationManager.Stub.asInterface(
ServiceManager.getService(Context.NOTIFICATION_SERVICE));
return sService;
}
getService()
最终返回的是sService
,是一个懒汉式单例,因此可以通过反射获取到其实例。
3,小结
sService
是一个单例,可以反射获取到其实例。
sService
实现了INotificationManager
接口,因此可以动态代理。
因此可以通过Hook来干预Toast的展示。
三,撸码
理清了上面的过程,实现就很简单了,直接撸码:
1,获取sService
的Field
Class<Toast> toastClass = Toast.class;
Field sServiceField = toastClass.getDeclaredField("sService");
sServiceField.setAccessible(true);
2,动态代理替换
Object proxy = Proxy.newProxyInstance(Thread.class.getClassLoader(), new Class[]{INotificationManager.class}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return null;
}
});
// 用代理对象给sService赋值
sServiceField.set(null, proxy);
3,获取sService
原始对象
因为动态代理不能影响被代理对象的原有流程,因此需要在第二步的InvocationHandler()
的invoke()
中需要执行原有的逻辑,这就需要获取sService
的原始对象。
前面已经获取到了sService
的Field,它是静态的,那直接通过sServiceField.get(null)
获取不就可以了?然而并不能获取到,这是因为整个Hook操作是在应用初始化时,整个应用还没有执行过Toast.show()的操作,因此sService
还没有初始化(因为它是一个懒汉单例)。
既然不能直接获取,那就通过反射调用一下:
Method getServiceMethod = toastClass.getDeclaredMethod("getService", null);
getServiceMethod.setAccessible(true);
Object service = getServiceMethod.invoke(null);
接着完善一下第二步代码:
Object proxy = Proxy.newProxyInstance(Thread.class.getClassLoader(), new Class[]{INotificationManager.class}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return method.invoke(service, args);
}
});
到此,已经实现了对Toast的代理,Toast可以按照原始逻辑正常执行,但还没有加入额外逻辑。
4,添加Hook逻辑
在InvocationHandler
的invoke()
方法中添加额外逻辑:
Object proxy = Proxy.newProxyInstance(Thread.class.getClassLoader(), new Class[]{INotificationManager.class}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 判断enqueueToast()方法时执行操作
if (method.getName().equals("enqueueToast")) {
Log.e("hook", method.getName());
getContent(args[1]);
}
return method.invoke(service, args);
}
});
args数组的第二个是TN类型的对象,其中有一个LinearLayout
类型的mNextView
对象,mNextView
中有一个TextView
类型的childView,这个childView就是展示toast文本的那个TextView
,可以直接获取其文本内容,也可以对其赋值,因此代码如下:
private static void getContent(Object arg) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
// 获取TN的class
Class<?> tnClass = Class.forName(Toast.class.getName() + "$TN");
// 获取mNextView的Field
Field mNextViewField = tnClass.getDeclaredField("mNextView");
mNextViewField.setAccessible(true);
// 获取mNextView实例
LinearLayout mNextView = (LinearLayout) mNextViewField.get(arg);
// 获取textview
TextView childView = (TextView) mNextView.getChildAt(0);
// 获取文本内容
CharSequence text = childView.getText();
// 替换文本并赋值
childView.setText(text.toString().replace("HookToast:", ""));
Log.e("hook", "content: " + childView.getText());
}
最后看一下效果:
四,总结
这个一个沙雕操作,实际应用中这种需求也比较少见。通过Hook的方式可以统一控制,而且没有侵入性。大佬勿喷!!!