版權(quán)聲明:本文為博主原創(chuàng)文章痊臭,遵循 CC 4.0 BY-SA 版權(quán)協(xié)議踩萎,轉(zhuǎn)載請附上原文出處鏈接和本聲明停局。
本文鏈接:http://www.reibang.com/p/a47bcf62109c
一,背景
這是個(gè)沙雕操作香府,原因是:在小米手機(jī)的部分機(jī)型上董栽,彈Toast時(shí)會在吐司內(nèi)容前面帶上app名稱,如下:
此時(shí)產(chǎn)品經(jīng)理發(fā)話了:為了統(tǒng)一風(fēng)格企孩,在小米手機(jī)上去掉Toast前的應(yīng)用名锭碳。
網(wǎng)上有以下解決方案,比如:先給toast
的message
設(shè)置為空勿璃,然后再設(shè)置需要提示的message
擒抛,如下:
Toast toast = Toast.makeText(context, “”, Toast.LENGTH_LONG);
toast.setText(message);
toast.show();
但這些都不能從根本上解決問題,于是Hook Toast的方案誕生了补疑。
二闻葵,分析
首先分析一下Toast的創(chuàng)建過程.
Toast的簡單使用如下:
Toast.makeText(this,"abc",Toast.LENGTH_LONG).show();
1,構(gòu)造toast
通過makeText()
構(gòu)造一個(gè)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()
中也就是設(shè)置了時(shí)長以及要顯示的文本或自定義布局,對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
是一個(gè)INotificationManager
類型的對象嬉橙,INotificationManager
是一個(gè)接口早直,這就為動態(tài)代理提供了可能。
service
是在每次show()
時(shí)通過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
霞扬,是一個(gè)懶漢式單例,因此可以通過反射獲取到其實(shí)例枫振。
3喻圃,小結(jié)
sService
是一個(gè)單例,可以反射獲取到其實(shí)例粪滤。
sService
實(shí)現(xiàn)了INotificationManager
接口斧拍,因此可以動態(tài)代理。
因此可以通過Hook來干預(yù)Toast的展示杖小。
三肆汹,擼碼
理清了上面的過程,實(shí)現(xiàn)就很簡單了予权,直接擼碼:
1昂勉,獲取sService
的Field
Class<Toast> toastClass = Toast.class;
Field sServiceField = toastClass.getDeclaredField("sService");
sServiceField.setAccessible(true);
2,動態(tài)代理替換
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
原始對象
因?yàn)閯討B(tài)代理不能影響被代理對象的原有流程岗照,因此需要在第二步的InvocationHandler()
的invoke()
中需要執(zhí)行原有的邏輯,這就需要獲取sService
的原始對象斧账。
前面已經(jīng)獲取到了sService
的Field谴返,它是靜態(tài)的,那直接通過sServiceField.get(null)
獲取不就可以了咧织?然而并不能獲取到嗓袱,這是因?yàn)檎麄€(gè)Hook操作是在應(yīng)用初始化時(shí),整個(gè)應(yīng)用還沒有執(zhí)行過Toast.show()的操作习绢,因此sService
還沒有初始化(因?yàn)樗且粋€(gè)懶漢單例)渠抹。
既然不能直接獲取,那就通過反射調(diào)用一下:
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);
}
});
到此闪萄,已經(jīng)實(shí)現(xiàn)了對Toast的代理梧却,Toast可以按照原始邏輯正常執(zhí)行,但還沒有加入額外邏輯败去。
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()方法時(shí)執(zhí)行操作
if (method.getName().equals("enqueueToast")) {
Log.e("hook", method.getName());
getContent(args[1]);
}
return method.invoke(service, args);
}
});
args數(shù)組的第二個(gè)是TN類型的對象,其中有一個(gè)LinearLayout
類型的mNextView
對象圆裕,mNextView
中有一個(gè)TextView
類型的childView广鳍,這個(gè)childView就是展示toast文本的那個(gè)TextView
荆几,可以直接獲取其文本內(nèi)容,也可以對其賦值赊时,因此代碼如下:
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實(shí)例
LinearLayout mNextView = (LinearLayout) mNextViewField.get(arg);
// 獲取textview
TextView childView = (TextView) mNextView.getChildAt(0);
// 獲取文本內(nèi)容
CharSequence text = childView.getText();
// 替換文本并賦值
childView.setText(text.toString().replace("HookToast:", ""));
Log.e("hook", "content: " + childView.getText());
}
最后看一下效果:
四吨铸,總結(jié)
這個(gè)一個(gè)沙雕操作,實(shí)際應(yīng)用中這種需求也比較少見祖秒。通過Hook的方式可以統(tǒng)一控制诞吱,而且沒有侵入性。大佬勿噴=叻臁7课!