- 最近發(fā)現(xiàn)在小米高系統(tǒng)版本的手機(jī)上,Toast的內(nèi)容會(huì)自帶應(yīng)用名稱的前綴鼻吮;百度一下育苟,發(fā)現(xiàn)的確不少這些反饋(萬(wàn)惡的小米系統(tǒng)開(kāi)發(fā)...),看了幾篇解決這個(gè)問(wèn)題的文章椎木,基本如下:
Toast toast = Toast.makeText(context, “”, Toast.LENGTH_LONG);
toast.setText(message);
toast.show();
- 但是如果我們的項(xiàng)目中违柏,有幾十個(gè)地方用到了Toast笨腥,那就要在幾十個(gè)地方都去修改,這樣太麻煩了勇垛,能不能在一個(gè)地方做處理脖母,其他地方都不用修改呢。
先查看Toast類的源碼:
1闲孤、makeText()
public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
return makeText(context, null, text, duration);//調(diào)用makeText的重載方法谆级,Looper傳入為null
}
public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
@NonNull CharSequence text, @Duration int duration) {
//調(diào)用Toast的構(gòu)造方法,先創(chuàng)建一個(gè)Toast的實(shí)例
Toast result = new Toast(context, looper);
//填充id為transient_notification的layout頁(yè)面讼积,獲取id為message的TextView肥照,設(shè)置內(nèi)容為text
LayoutInflater inflate = (LayoutInflater)
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
tv.setText(text);
//把Toast中的mNextView變量賦值為v,這句很重要勤众,后面hook的時(shí)候會(huì)用到
result.mNextView = v;
result.mDuration = duration;
return result;
}
makeText()第一步就是創(chuàng)建一個(gè)Toast實(shí)例舆绎。
1.1 Toast的構(gòu)造方法
public Toast(Context context) {
this(context, null);
}
//Toast的構(gòu)造方法就是初始化mTN變量(mTN在show()方法中會(huì)用到),配置Toast的layout參數(shù)
public Toast(@NonNull Context context, @Nullable Looper looper) {
mContext = context;
mTN = new TN(context.getPackageName(), looper);
mTN.mY = context.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.toast_y_offset);
mTN.mGravity = context.getResources().getInteger(
com.android.internal.R.integer.config_toastDefaultGravity);
}
private static class TN extends ITransientNotification.Stub {
//TN的構(gòu)造方法就是配置Toast的layout參數(shù)
TN(String packageName, @Nullable Looper looper) {
// XXX This should be changed to use a Dialog, with a Theme.Toast
// defined that sets up the layout params appropriately.
final WindowManager.LayoutParams params = mParams;
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
params.width = WindowManager.LayoutParams.WRAP_CONTENT;
params.format = PixelFormat.TRANSLUCENT;
params.windowAnimations = com.android.internal.R.style.Animation_Toast;
params.type = WindowManager.LayoutParams.TYPE_TOAST;
params.setTitle("Toast");
params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
mPackageName = packageName;
if (looper == null) {
// Use Looper.myLooper() if looper is not specified.
looper = Looper.myLooper();
if (looper == null) {
throw new RuntimeException(
"Can't toast on a thread that has not called Looper.prepare()");
}
}
mHandler = new Handler(looper, null) {
...
};
}
}
分析:
- makeText()方法首先創(chuàng)建一個(gè)Toast的實(shí)例们颜。
- Toast的構(gòu)造方法中會(huì)配置好Toast展示所需要的layout參數(shù)
- 創(chuàng)建好Toast后吕朵,會(huì)填充id為transient_notification的layout布局,實(shí)例成View實(shí)例窥突,這個(gè)View也就是我們能看到的Toast努溃,layout中包含了一個(gè)id為message的TextView,給TextView設(shè)置內(nèi)容為傳遞進(jìn)來(lái)的text阻问。
- 最后再把mNextView變量賦值為上一步填充形成的View梧税;這個(gè)mNextView最后調(diào)用show()方法時(shí)會(huì)用到。
2称近、show()
public void show() {
if (mNextView == null) {
throw new RuntimeException("setView must have been called");
}
//通過(guò)getService()方法獲取INotificationManager 的實(shí)例
INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
TN tn = mTN;
//把makeText方法中實(shí)例的view賦值給tn的mNextView變量
tn.mNextView = mNextView;
try {
//調(diào)用INotificationManager的enqueueToast的方法
//參數(shù)tn的mNextView為View布局第队,包含了TextView的子控件
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
// Empty
}
}
//獲取INotificationManager 的實(shí)例,非空判斷確保了sService為單例
static private INotificationManager getService() {
if (sService != null) {
return sService;
}
sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
return sService;
}
- 首先先通過(guò)調(diào)用 getService()獲取INotificationManager的實(shí)例刨秆;
- getService()方法返回sService的單例實(shí)例凳谦;
- 通過(guò)調(diào)用INotificationManager的實(shí)例service的enqueueToast方法來(lái)展示toast。
小結(jié):展示一個(gè)Toast坛善,主要經(jīng)過(guò)4個(gè)步驟:
1晾蜘、創(chuàng)建一個(gè)Toast實(shí)例,同時(shí)創(chuàng)建Toast的內(nèi)部類TN的實(shí)例眠屎,配置Toast展示時(shí)需要的layout參數(shù)剔交。
2、填充Toast展示所需要的layout布局改衩,實(shí)例化為View類型岖常,然后把view中的TextView控件設(shè)置我們傳入的text內(nèi)容文字。
3葫督、把上一步實(shí)例出來(lái)的view竭鞍,設(shè)置為給TN類的mNextView 變量板惑。
4、通過(guò)getService()方法獲取INotificationManager實(shí)例偎快,調(diào)用INotificationManager的enqueueToast方法冯乘。
3、hook消息內(nèi)容
- 先找到需要hook的對(duì)象(最好是個(gè)單例對(duì)象晒夹,這樣可以實(shí)現(xiàn)無(wú)侵入修改)裆馒。
- 然后找到hook對(duì)象的持有者(在這里也就是找到Toast類中指向這個(gè)被hook的對(duì)象的全局變量)。
- 創(chuàng)建hook對(duì)象的代理類丐怯,并新建這個(gè)代理類的實(shí)例喷好。
- 用代理類的實(shí)例替換原先需要hook的對(duì)象。
3.1读跷、先確定要hook的對(duì)象
final TN mTN;
public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
return makeText(context, null, text, duration);
}
public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
@NonNull CharSequence text, @Duration int duration) {
Toast result = new Toast(context, looper);
LayoutInflater inflate = (LayoutInflater)
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
tv.setText(text);
result.mNextView = v;
result.mDuration = duration;
return result;
}
public void show() {
if (mNextView == null) {
throw new RuntimeException("setView must have been called");
}
INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;
try {
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
// Empty
}
}
private static INotificationManager sService;
static private INotificationManager getService() {
if (sService != null) {
return sService;
}
sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
return sService;
}
我們的目的是修改text的內(nèi)容梗搅,而text是設(shè)置給TextView控件,也就是我們最終需要hook TextView對(duì)象效览,或者h(yuǎn)ook TextView對(duì)象的持有者或間接持有者(通過(guò)TextView對(duì)象的持有者獲取到TextView无切,來(lái)實(shí)現(xiàn)對(duì)TextView內(nèi)容的修改)。
- 首先TextView是局部變量钦铺,沒(méi)辦法hook订雾。
- 而TextView的持有者View被全局變量mNextView所持有,這是一個(gè)可以hook的切入點(diǎn)矛洞。
- 從show()方法中可以看到,mTN的mNextView變量也指向了View烫映,所以mTN也是TextView的持有者沼本,這也是一個(gè)可以hook的切入點(diǎn)。
- show()方法中锭沟,tn對(duì)象作為參數(shù)傳入了enqueueToast方法中抽兆,也就是service對(duì)象間接持有了tn對(duì)象,service間接持有TextView對(duì)象族淮,service也是一個(gè)可以hook的切入點(diǎn)辫红。
從上面四點(diǎn),我們可以找到三個(gè)可以hook的切入點(diǎn)祝辣,而最佳的hook的對(duì)象service贴妻,因?yàn)閟ervice對(duì)象持有者是sService變量,sService是個(gè)單例蝙斜,多個(gè)的Toast對(duì)象中我們都只需要替換一次名惩。
3.2創(chuàng)建hook對(duì)象的代理類
public class ToastProxy implements InvocationHandler {
private static final String TAG = "ToastProxy";
private Object mProxyObject;
private Context mContext;
public ToastProxy( Context mContext, Object mProxyObject) {
this.mContext = mContext;
this.mProxyObject = mProxyObject;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Log.i(TAG, "invoke: method == " + method.getName());
//對(duì)Toast類中的INotificationManager實(shí)例sService執(zhí)行enqueueToast方法時(shí),進(jìn)行攔截操作
if (method.getName().equals("enqueueToast")){
if (args != null && args.length > 0) {
try{
//獲取tn對(duì)象
Object tn = args[1];
//獲取mNextView變量孕荠,也就是View, 對(duì)應(yīng)的是LinearLayout對(duì)象
Field mNextView = tn.getClass().getDeclaredField("mNextView");
mNextView.setAccessible(true);
LinearLayout linearLayout = (LinearLayout) mNextView.get(args[1]);
//從對(duì)應(yīng)的是LinearLayout對(duì)象中獲取TextView對(duì)象
if (linearLayout.getChildAt(0) instanceof TextView){
TextView textView = (TextView) linearLayout.getChildAt(0);
String msgOfToast = textView.getText().toString();//這個(gè)就是Toast的內(nèi)容
String appName = mContext.getString(R.string.app_name);
if (msgOfToast.contains(appName)){
String content = msgOfToast.substring(appName.length() + 1);
textView.setText(content);
}
}
}catch (NoSuchFieldException e){
e.printStackTrace();
}
}
}
return method.invoke(mProxyObject, args);
}
}
判斷方法名娩鹉,攔截enqueueToast方法的邏輯攻谁,獲取到TextView對(duì)象,修改文字內(nèi)容弯予。
3.3戚宦、替換需要hook的對(duì)象
public class ToastUtil {
public static void hookToast(Context ctx){
Looper.prepare();
Toast toast = new Toast(ctx);
try {
Method getService = toast.getClass().getDeclaredMethod("getService");
getService.setAccessible(true);
//實(shí)例化INotificationManager
Object sService = getService.invoke(toast);
ToastProxy toastProxy = new ToastProxy(ctx, sService);
//創(chuàng)建hook對(duì)象的代理類實(shí)例
Object serviceProxy = Proxy.newProxyInstance(toast.getClass().getClassLoader(), sService.getClass().getInterfaces(), toastProxy);
Field sServiceField = toast.getClass().getDeclaredField("sService");
sServiceField.setAccessible(true);
//替換Toast類中已經(jīng)初始化的單例對(duì)象sService
sServiceField.set(toast, serviceProxy);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
}
創(chuàng)建Toast實(shí)例,同時(shí)實(shí)例化INotificationManager的單例sService锈嫩;再創(chuàng)建hook的對(duì)象service的代理類實(shí)例阁苞,替換sService所指向的對(duì)象。然后在我們業(yè)務(wù)代碼使用Toast前祠挫,執(zhí)行hookToast方法即可那槽。
總結(jié):hook可以幫我們?cè)谀承┨囟ǖ那腥朦c(diǎn)中無(wú)侵入式的完成一些代碼邏輯的修改;可以在不改變?cè)械拇a業(yè)務(wù)等舔,插入一些特定的代碼業(yè)務(wù)骚灸。而實(shí)現(xiàn)hook,只需要我們根據(jù)需求慌植,從源代碼中找到最佳可以hook的對(duì)象甚牲,通過(guò)反射等代碼即可實(shí)現(xiàn)。