前言:
最近在学习到张绍文讲到的一段关于解决崩溃的话是这样说的:
“ Hook 解决。这里分为 Java Hook 和 Native Hook。我以我最近解决的一个系统崩溃为例,我们发现线上出现一个
Toast的相关系统崩溃,它只出现在Android7.0的系统中,看起来是在Toast显示的时候窗口的token已
经无效了。这有可能出现在Toast需要显示时,窗口已经销毁了。
android.view.WindowManager$BadTokenException:
at android.view.ViewRootImpl.setView(ViewRootImpl.java)
at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java4)
at android.widget.Toast$TN.handleShow(Toast.java)
为什么Android8.0的系统不会有这个问题?在查看Android8.0的源码后我们发现有以下修改:
try {
mWM.addView(mView, mParams);
trySendAccessibilityEvent();
} catch (WindowManager.BadTokenException e) {
/* ignore */
}
考虑再三我们决定参考Android8.0的做法,直接catch住这个异常。我这的关键在于寻找Hook点,
这个安全算是相对简单的。Toast里面有一个变量叫mTN,它的类型为handler,我们只需要代理它
可以实现捕获。”
从上面这一段说让我发现作为一名Android开发工程师,学习Hook技术的重要性,于是我开始了我的Hook学习之旅。
前置技能
1.java反射
熟练掌握类Class,方法Method,成员Field的使用方法
源码内部,很多类和方法都是@hide的,外部直接无法访问,所以只能通过反射,去创建源码中的类,方法,或者成员.
2.阅读安卓源码的能力
hook的切入点都在源码内部,不能阅读源码,不能理清源码逻辑,则不用谈hook.其实使用 androidStudio来阅读源码有个坑,有时候会看到源码里面 "一片飘红",看似是有什么东西没有引用进来,其实是因为有部分源码没有对开发者开放,解决起来很麻烦,所以,推荐从安卓官网下载整套源码,然后使用 SourceInsight 查看源码。如果不需要跳来跳去的话,直接用 安卓源码网站 一步到位
hook通用思路
无论多么复杂的源码,我们想要干涉其中的一些执行流程,最终的杀招只有一个: “偷梁换柱”.
而 “偷梁换柱”的思路,通常都是一个套路:
1. 根据需求确定 要hook的对象
2. 寻找要hook的对象的持有者,拿到要hook的对象
(持有:B类 的成员变量里有 一个是A的对象,那么B就是A的持有者,如下)
class B{
A a;
}
class A{}
3. 定义“要hook的对象”的代理类,并且创建该类的对象
4. 使用上一步创建出来的对象,替换掉要hook的对象
案例实战
这是一个最简单的案例:我们自己的代码里面,给一个view设置了点击事件,现在要求在不改动这个点击事件的情况下,添加额外的点击事件逻辑.
View v = findViewById(R.id.tv);
v.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this, "别点啦,再点我咬你了...", Toast.LENGTH_SHORT).show();
}
});
这是view的点击事件,toast了一段话,现在要求,不允许改动这个OnClickListener,要在toast之前添加日志打印 Log.d(...).
乍一看,无从下手.看hook如何解决.
按照上面的思路来:
第一步:根据需求确定 要hook的对象;
我们的目的是在OnClickListener中,插入自己的逻辑.所以,确定要hook的,是v.setOnClickListener()方法的实参。
第二步:寻找要hook的对象的持有者,拿到要hook的对象
进入v.setOnClickListener源码:发现我们创建的OnClickListener对象被赋值给了getListenerInfo().mOnClickListener
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
继续索引:getListenerInfo() 是个什么玩意?继续追查:
ListenerInfo getListenerInfo() {
if (mListenerInfo != null) {
return mListenerInfo;
}
mListenerInfo = new ListenerInfo();
return mListenerInfo;
}
结果发现这个其实是一个伪单例,一个View对象中只存在一个ListenerInfo对象.
进入ListenerInfo内部:发现OnClickListener对象 被ListenerInfo所持有.
static class ListenerInfo {
...
public OnClickListener mOnClickListener;
...
}
到这里为止,完成第二步,找到了点击事件的实际持有者:ListenerInfo .
第三步:定义“要hook的对象”的代理类,并且创建该类的对象
我们要hook的是View.OnClickListener对象,所以,创建一个类 实现View.OnClickListener接口.
package com.example.hookdemo;
import android.util.Log;
import android.view.View;
public class OnClickListenerProxy implements View.OnClickListener {
private View.OnClickListener mOriginListener;
public OnClickListenerProxy(View.OnClickListener listener) {
this.mOriginListener = listener;
}
@Override
public void onClick(View v) {
if (mOriginListener != null) {
mOriginListener.onClick(v);
}
String name = (String) v.getTag(v.getId()); //接收参数
Log.d("LOGCAT", name + "被hooked!");
}
}
然后,new出它的对象待用。
ProxyOnClickListener proxyOnClickListener = new ProxyOnClickListener(onClickListenerInstance);
第四步:使用上一步创建出来的对象,替换掉要hook的对象,达成 偷梁换柱的最终目的.
利用反射,将我们创建的代理点击事件对象,传给这个view
field.set(mListenerInfo, proxyOnClickListener);
这里,贴出最终代码:
package com.example.hookdemo;
import android.util.Log;
import android.view.View;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class HookUtil {
public static void hookOnClickListener(View view) {
try {
//第一步:得到View的getListenerInfo方法
Method getListenerInfo = View.class.getDeclaredMethod("getListenerInfo");
//修改getListenerInfo为可访问(View中的getListenerInfo不是public)
getListenerInfo.setAccessible(true);
Object listenerInfo = getListenerInfo.invoke(view);
//第二步:得到原始的OnClickListener对象
Class<?> listenerInfoClz = Class.forName("android.view.View$ListenerInfo");
Field mOnClickListener = listenerInfoClz.getDeclaredField("mOnClickListener");
mOnClickListener.setAccessible(true);
View.OnClickListener originOnClickListener = (View.OnClickListener) mOnClickListener.get(listenerInfo);
//第三步:创建代理类,代理原始类
View.OnClickListener hookedOnClickListener = new OnClickListenerProxy(originOnClickListener);
// 或者用动态代理实现上面代码完成上面两步,直接生成代理类,需要多传一个Context参数
// Object proxyOnClickListener = Proxy.newProxyInstance(context.getClass().getClassLoader(), new Class[]{View.OnClickListener.class}, new InvocationHandler() {
// @Override
// public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// Log.d("HookSetOnClickListener", "点击事件被hook到了");//加入自己的逻辑
// return method.invoke(onClickListenerInstance, args);//执行被代理的对象的逻辑
// }
// });
mOnClickListener.set(listenerInfo, hookedOnClickListener);
} catch (Exception e) {
Log.d("LOGCAT", "hook clickListener failed!", e);
}
}
}
最后 一步,调用。设置钩子,替代原有逻辑:
package com.example.hookdemo;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
private Button btnHook;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btnHook = findViewById(R.id.btnHook);
btnHook.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this, "点击事件", Toast.LENGTH_LONG).show();
}
});
btnHook.setTag(btnHook.getId(), "按钮1"); //练习带参数,也可以不带。
HookUtil.hookOnClickListener(btnHook);
}
}
自此,一个最简单的Hook实例就完成了。
参考:https://www.jianshu.com/p/74c12164ffca?tdsourcetag=s_pcqq_aiomsg
本文详细介绍了Hook技术在Android开发中的应用,通过解决Toast显示导致的系统崩溃问题,展示了Hook技术的重要性和其实现步骤。文章深入讲解了Java反射、阅读安卓源码的能力以及Hook通用思路,并提供了一个具体的Hook实战案例。
1534

被折叠的 条评论
为什么被折叠?



