Android Hook 解决诡异Toast
开篇废话
线上用户遇到一个问题,就是会经常弹出一个Toast,但是这个Toast的文案在端上和后台都没有找到,只能怀疑是第三方SDK弹出的,但是又不能一个一个问,问了也不一定帮你好好查,所以只能自食其力。
遇到的问题
如何Hook Toast,线上用户是Android 13。
开始解决
所以有两条路,一条是Hook所有调用Toast的地方,一条是通过Xposed框架解决,Xposed成本太高,所以采用Hook调用Toast的地方,方案采用站在巨人的肩膀上的第三方库me.ele:lancet-plugin
。这个第三方库,属于在编译期,动态生成代码,所以我们可以在所有调用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中导包hook工程。
dependencies {
implementation 'me.ele:lancet-base:1.0.6'
}
找到Hook点
我们先来看一下Toast的源码,Toast源码还是比较简单的,我这里列举一些关键代码,方便我们进行观察。
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;
}
}
可以看出,我们需要注意的关键点是,用户在makeText()、setText()、setView()方法、这些方法设置了弹出的Toast的内容。重点来了,我们要对这些方法,用第三方库,进行Hook。
Hook具体方法
先放出实现类,大家观察一下。
public class ToastsLancet {
private static final String TAG = "HookToast";
@TargetClass("android.widget.Toast")
@Proxy("show")
public void show() {
Log.i(TAG, "Toast方法被调用----------> showToast()");
showStackTraceLog();
Origin.callVoid();
}
@TargetClass("android.widget.Toast")
@Proxy("makeText")
public static Toast makeText(Context context, CharSequence text, int duration) {
Log.i(TAG, "Toast方法被调用----------> 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方法被调用----------> 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方法被调用----------> 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方法被调用----------> 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方法被调用----------> 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内容,然后进行打印内容和堆栈信息。
@TargetClass
代表Hook的类名,@Proxy
代表是的Hook的方法名,Origin.callVoid();
代表调用原来的代码,并且无返回值,如果不调用则不会显示Toast的了,Origin.call()
是有返回值的调用方法。
在setView()方法之后,我们需要通过遍历View的方式,找到TextView,再拿到Toast的内容,但是如果调用的地方是先进行setView(),再进行TextView.setText(),那么现在是拿不到的,只能通过相同方法去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到系统源码的,只不过我现在了解的在自己工程中使用,最高支持到Android 11,具体可以参考github-epic,不过作者主要精力在Xposed框架太极上,所以这里的文档都没有更新,类名方法名有小调整。