開篇廢話
線上用戶遇到一個問題滑潘,就是會經(jīng)常彈出一個Toast胰挑,但是這個Toast的文案在端上和后臺都沒有找到,只能懷疑是第三方SDK彈出的竿奏,但是又不能一個一個問,問了也不一定幫你好好查腥放,所以只能自食其力泛啸。
遇到的問題
如何Hook Toast,線上用戶是Android 13秃症。
開始解決
所以有兩條路候址,一條是Hook所有調(diào)用Toast的地方,一條是通過Xposed框架解決种柑,Xposed成本太高岗仑,所以采用Hook調(diào)用Toast的地方,方案采用站在巨人的肩膀上的第三方庫me.ele:lancet-plugin
聚请。這個第三方庫荠雕,屬于在編譯期,動態(tài)生成代碼良漱,所以我們可以在所有調(diào)用Toast的方法前后添加我們的代碼,輸出堆棧信息欢际。
添加第三方庫
在Porject的build.gradle中添加hook插件母市。
buildscript {
dependencies {
classpath 'com.bytedance.tools.lancet:lancet-plugin-asm6:1.0.0' //看情況添加,是為了解決asm6問題
classpath 'me.ele:lancet-plugin:1.0.6' //hook框架损趋,必須添加
}
}
在Module的build.gradle頂部中添加引用插件患久。
apply plugin: 'com.glazero.android.spi'
在Module的build.gradle中導(dǎo)包hook工程。
dependencies {
implementation 'me.ele:lancet-base:1.0.6'
}
找到Hook點
我們先來看一下Toast的源碼,Toast源碼還是比較簡單的蒋失,我這里列舉一些關(guān)鍵代碼返帕,方便我們進行觀察。
public class Toast {
@Nullable
private View mNextView;
@Nullable
private CharSequence mText;
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) {
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;
}
}
public Toast(Context context) {
this(context, null);
}
public Toast(@NonNull Context context, @Nullable Looper looper) {
}
public void show() {
}
public void setText(@StringRes int resId) {
setText(mContext.getText(resId));
}
public void setText(CharSequence s) {
if (Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM)) {
if (mNextView != null) {
throw new IllegalStateException(
"Text provided for custom toast, remove previous setView() calls if you "
+ "want a text toast instead.");
}
mText = s;
} else {
if (mNextView == null) {
throw new RuntimeException("This Toast was not created with Toast.makeText()");
}
TextView tv = mNextView.findViewById(com.android.internal.R.id.message);
if (tv == null) {
throw new RuntimeException("This Toast was not created with Toast.makeText()");
}
tv.setText(s);
}
}
@Deprecated
public void setView(View view) {
mNextView = view;
}
}
可以看出篙挽,我們需要注意的關(guān)鍵點是荆萤,用戶在makeText()、setText()铣卡、setView()方法链韭、這些方法設(shè)置了彈出的Toast的內(nèi)容。重點來了煮落,我們要對這些方法敞峭,用第三方庫,進行Hook蝉仇。
Hook具體方法
先放出實現(xiàn)類旋讹,大家觀察一下。
public class ToastsLancet {
private static final String TAG = "HookToast";
@TargetClass("android.widget.Toast")
@Proxy("show")
public void show() {
Log.i(TAG, "Toast方法被調(diào)用----------> showToast()");
showStackTraceLog();
Origin.callVoid();
}
@TargetClass("android.widget.Toast")
@Proxy("makeText")
public static Toast makeText(Context context, CharSequence text, int duration) {
Log.i(TAG, "Toast方法被調(diào)用----------> makeText(text)");
Log.i(TAG, "text = " + text);
showStackTraceLog();
return (Toast) Origin.call();
}
@TargetClass("android.widget.Toast")
@Proxy("makeText")
public static Toast makeText(Context context, int res, int duration) {
Log.i(TAG, "Toast方法被調(diào)用----------> makeText(res)");
Log.i(TAG, "text = " + Application.getString(res));
showStackTraceLog();
return (Toast) Origin.call();
}
@TargetClass("android.widget.Toast")
@Proxy("setText")
public void setText(CharSequence text) {
Log.i(TAG, "Toast方法被調(diào)用----------> setText(text)");
Log.i(TAG, "text = " + text);
showStackTraceLog();
Origin.callVoid();
}
@TargetClass("android.widget.Toast")
@Proxy("setText")
public void setText(int res) {
Log.i(TAG, "Toast方法被調(diào)用----------> setText(res)");
Log.i(TAG, "text = " + Application.getString(res));
showStackTraceLog();
Origin.callVoid();
}
@TargetClass("android.widget.Toast")
@Proxy("setView")
public void setView(View view) {
List<TextView> textViewList = FindViewHelper.findTextView(view);
for (TextView textView : textViewList) {
Log.i(TAG, "Toast方法被調(diào)用----------> setView(view)");
Log.i(TAG, "text = " + textView.getText().toString());
showStackTraceLog();
}
Origin.callVoid();
}
private static void showStackTraceLog() {
//打印堆棧信息
Log.i(TAG, Log.getStackTraceString(new Throwable()));
}
}
Application.getString()方法轿衔,大家可以替換成自己的方法沉迹,去獲取到具體的Toast內(nèi)容,然后進行打印內(nèi)容和堆棧信息呀枢。
@TargetClass
代表Hook的類名胚股,@Proxy
代表是的Hook的方法名,Origin.callVoid();
代表調(diào)用原來的代碼裙秋,并且無返回值琅拌,如果不調(diào)用則不會顯示Toast的了,Origin.call()
是有返回值的調(diào)用方法摘刑。
在setView()方法之后进宝,我們需要通過遍歷View的方式,找到TextView枷恕,再拿到Toast的內(nèi)容党晋,但是如果調(diào)用的地方是先進行setView(),再進行TextView.setText()徐块,那么現(xiàn)在是拿不到的未玻,只能通過相同方法去Hook TextView.setText()方法了,就不展開了胡控。
找到所有TextView的方法我也列出來扳剿。
public class FindViewHelper {
public static List<TextView> findTextView(View view) {
if (view == null) {
return new ArrayList<>();
}
List<TextView> visited = new ArrayList<>();
List<View> unvisited = new ArrayList<>();
unvisited.add(view);
while (!unvisited.isEmpty()) {
View child = unvisited.remove(0);
if (child instanceof TextView) {
visited.add((TextView) child);
}
if (!(child instanceof ViewGroup)){
continue;
}
ViewGroup group = (ViewGroup) child;
final int childCount = group.getChildCount();
for (int i=0; i < childCount; i++) {
unvisited.add(group.getChildAt(i));
}
}
return visited;
}
}
寫在最后
這里只提供一種解決問題的思路,除了這種方案昼激,還可以通過上面提到的Xposed框架解決庇绽,這種方案是可以直接Hook到系統(tǒng)源碼的锡搜,只不過我現(xiàn)在了解的在自己工程中使用,最高支持到Android 11瞧掺,具體可以參考github-epic耕餐,不過作者主要精力在Xposed框架太極上,所以這里的文檔都沒有更新辟狈,類名方法名有小調(diào)整肠缔。