最近在研究插件化开发,插件化开发的基础就是hook技术,现在市面上存在的各种插件化框架,其基础原理都是使用hook技术
1.什么是hook技术(网上查到了一个解释,觉得挺好贴在这里):
Hook 英文翻译过来就是「钩子」的意思,那我们在什么时候使用这个「钩子」呢?在 Android 操作系统中系统维护着自己的一套事件分发机制。应用程序,包括应用触发事件和后台逻辑处理,也是根据事件流程一步步地向下执行。而「钩子」的意思,就是在事件传送到终点前截获并监控事件的传输,像个钩子钩上事件一样,并且能够在钩上事件时,处理一些自己特定的事件,换句话说hook就是在程序的执行过程中拦截程序运行的本来流程,然后再其流程中添加我们自己的代码逻辑,其实在很多优秀的开源框架中,都是运用了类似的技术来实现,比如我们常用的retrofit,其实在将我们定义的请求接口转化成不同的serviceMethod时,就是使用到了动态代理,动态代理就是hook技术常用的技巧之一,又比如流行的卡顿监听工具BlockCanary,其原理就是在handler的dispatchMessage执行前后添加相应的处理逻辑,其实这就是hook技术的实现;Hook 的这个本领,使它能够将自身的代码插入被勾住(Hook)的程序中,成为目标进程的一个部分。API Hook 技术是一种用于改变 API 执行结果的技术,能够将系统的 API 函数执行重定向。在 Android 系统中使用了沙箱机制,普通用户程序的进程空间都是独立的,程序的运行互不干扰。这就使我们希望通过一个程序改变其他程序的某些行为的想法不能直接实现,但是 Hook 的出现给我们开拓了解决此类问题的道路。当然,根据 Hook 对象与 Hook 后处理的事件方式不同,Hook 还分为不同的种类,比如消息 Hook、API Hook 等。
首先我们得找到被Hook的对象,我称之为Hook点;什么样的对象比较好Hook呢?自然是容易找到的对象。什么样的对象容易找到?静态变量和单例;在一个进程之内,静态变量和单例变量是相对不容易发生变化的,因此非常容易定位,而普通的对象则要么无法标志,要么容易改变。我们根据这个原则找到所谓的Hook点
2.hook的实现方式(通过set方法的方式实现,有可以使用的系统api)
如果我们可以调用某些类的共有方法来改变属性的值,从而可以在系统的原有的操作流程中添加我们新的处理逻辑;如:现在我们有这样一个需求,监听ui卡顿,我们知道android系统是通过消息机制进行UI更新,事件分发的,如果在主线程handler的dispatchMessage方法进行了耗时操作,就会发生UI卡顿,那么我们监听ui卡顿,其实只需要监听dispatchMessage执行时间就可以了,通过分析系统源码我们知道,dispatchMessage是在Looper类中调研(关于hander的原理,在这里不做阐述),我们可以简单的看看Looper.loop()方法执行的是什么操作呢,只看关键代码:
public static void loop() {
....................................................................
final Printer logging = me.mLogging;
if (logging !=null) {
logging.println(">>>>> Dispatching to " + msg.target +" " +
msg.callback +": " + msg.what);
}
....................................................................
try {
msg.target.dispatchMessage(msg);
dispatchEnd = needEndTime ? SystemClock.uptimeMillis() :0;
}finally {
if (traceTag !=0) {
Trace.traceEnd(traceTag);
}
}
....................................................................
if (logging !=null) {
logging.println("<<<<< Finished to " + msg.target +" " + msg.callback);
}
中间的代码我们不去关心,我们只知道在dispatchMessage方法的执行前后都会调用,两次logging.println方法我们只需要在计算两次打印的时间间隔是否大于某一个阀值来确定是否存在ui卡顿(这也是BlockCanary的核心原理)实现如下:
private void initHandlerCheck(){
Looper.getMainLooper().setMessageLogging(new Printer() {
@Override
public void println(String x) {
if (x.startsWith(START)) {
LogMonitor.getInstance().startMonitor();
}
if (x.startsWith(END)) {
LogMonitor.getInstance().removeMonitor();
}}});}
LogMonitor实现类如下:
public class LogMonitor {
private static LogMonitorsInstance =new LogMonitor();
private HandlerThreadmHandlerThread =new HandlerThread("log");
private HandlermHandler;
public static final StringTAG ="LogMonitor";
private LogMonitor() {
mHandlerThread.start();
mHandler =new Handler(mHandlerThread.getLooper());
}
private static RunnablemRunnable =new Runnable() {
@Override
public void run() {
StringBuilder sb =new StringBuilder();
StackTraceElement[] stackTrace = Looper.getMainLooper().getThread().getStackTrace();
for (StackTraceElement s : stackTrace) {
sb.append(s.toString() +"\n");
}
Log.e(TAG, sb.toString());
}
};
public static LogMonitorgetInstance() {
return sInstance;
}
public void startMonitor() {
mHandler.postDelayed(mRunnable, 1000);
}
public void removeMonitor() {
mHandler.removeCallbacks(mRunnable);
}
}
通过以上的代码,我们就可以把出现了耗时超过1000毫秒的操作,出现错误的地方打印在控制台上,这也是hook的一种实现形式;
2.hook的实现方式(通过反射的方式实现,静态代理)
比如我们有一个这样的需求,对于页面的所有按钮,我们想实现相同的业务处理流程,假如我们希望用户激活某种权限后按钮才能够点击, 否则就给出相应的提示,实现这个需求有很多种方式,如(1)在每个button的点击事件设置的地方都加上相应的判断(这肯定是不合适的,业务多的情况容易加漏,也是容易出错的),(2)所有的点击事件继承一个公用的点击事件类,但是,如果你并不是在项目的最开始介入,而是中途被分配来维护这个项目,这种方式可能也会涉及到大量的代码修改,那么还有没有其他的实现方式呢,hook的思想给我们提供了实现思路,通过分析view的源码(button也是一个view)我们知道所有的点击事件都是存储在ListenerInfo中,源码如下:
/**
* Register a callback to be invoked when this view is clicked. If this view is not
* clickable, it becomes clickable.
* @param l The callback that will run
* @see #setClickable(boolean)
*/
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
那问题就简单了,我们只需要找到一种方式,改变原来的mOnClickListener的值,就完美的达到了,我们的目的,那如果做到呢,反射就隆重登场了,如下代码:
public static void hookOnClickListener(View view)throws Exception {
// 反射得到 ListenerInfo 对象
Method getListenerInfo = View.class.getDeclaredMethod("getListenerInfo");
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);
//用 Hook代理类 替换原始的 OnClickListener
View.OnClickListener hookedOnClickListener =new HookedClickListenerProxy(originOnClickListener,false);
mOnClickListener.set(listenerInfo, hookedOnClickListener);
}
HookedClickListenerProxy,就是我们用来hook原来的view点击事件的对象,代码如下:
public class HookedClickListenerProxyimplements View.OnClickListener {
public boolean isActive =false;
private View.OnClickListenerorigin;
public HookedClickListenerProxy(View.OnClickListener origin,boolean isActive) {
this.origin = origin;
this.isActive = isActive;
}
@Override
public void onClick(View v) {
if(!isActive){
Toast.makeText(v.getContext(), "这个按钮还不能够点击", Toast.LENGTH_SHORT).show(); }
if (origin !=null) {
origin.onClick(v); }}}
4.hook的实现方式(通过动态代理的方式)
假如你收到了这样的一个需求,由于好多时候,用户都会复制我们客户端内部的一些文案去分享,我们这个时候想在复制的内容前面,默认添加上“=====斑马信用,天天向上====”
这个文案以扩大我们产品的影响力(纯粹为了演示瞎想的需求哈)如何实现呢,我们依然可以利用hook思想来实现。我们知道粘贴板服务是由ClipBoardService控制的,这个时候你依然
需要去分析它的源码(这块的源码花费了很长时间去理解),我们的目的依然是替换掉粘贴板服务的操作(我们肯定不能替换掉粘贴板服务,因为那是属于另外的一个进程),这个时候,
我们需要分析ServiceManager这个类大概的源码如下:
public final class ServiceManager {
private static final StringTAG ="ServiceManager";
private static IServiceManagersServiceManager;
private static HashMapsCache =new HashMap();
private static IServiceManagergetIServiceManager() {
if (sServiceManager !=null) {
return sServiceManager;
}
// Find the service manager
sServiceManager = ServiceManagerNative.asInterface(BinderInternal.getContextObject());
return sServiceManager;
}
/**
* Returns a reference to a service with the given name.
*
* @param name the name of the service to get
* @return a reference to the service, or <code>null</code> if the service doesn't exist
*/
public static IBindergetService(String name) {
try {
IBinder service =sCache.get(name);
if (service !=null) {
return service;
}else {
return getIServiceManager().getService(name);
}
}catch (RemoteException e) {
Log.e(TAG, "error in getService", e);
}
return null;
}
}
通过分析ServiceManager的代码,我们知道如果要替换ClipBoardService操作IBinder对象,我们只要替换掉存储在sCache这个hashmap中的值就可以了,所有我们可以这个样完成;
public static void setService(String serviceName, IBinder service) {
if (c_ServiceManager ==null) {
return;
}
if (sCacheService ==null) {
try {
Field sCache =c_ServiceManager.getDeclaredField("sCache");
sCache.setAccessible(true);
sCacheService = (Map) sCache.get(null);
}catch (Exception e) {
e.printStackTrace();
}
}
sCacheService.remove(serviceName);
sCacheService.put(serviceName, service);
}
这样就完成了ClipBoardService对于的IBinder对象的替换,但是,我们替换了这个对象,并不能完成我们的需求,第二步我们需要hook住这个service,这里我们就可以使用到动态代理了,
动态代理大家肯定很熟悉,在这里就不讨论了,具体的代码如下:
public class ClipboardHook {
private static final StringTAG = ClipboardHook.class.getSimpleName();
public static void hookService() {
IBinder clipboardService = ServiceManager.getService(Context.CLIPBOARD_SERVICE);
String IClipboard ="android.content.IClipboard";
if (clipboardService !=null) {
IBinder hookClipboardService =
(IBinder) Proxy.newProxyInstance(clipboardService.getClass().getClassLoader(),
clipboardService.getClass().getInterfaces(),
new ServiceHook(clipboardService, IClipboard, true, new ClipboardHookHandler()));
ServiceManager.setService(Context.CLIPBOARD_SERVICE, hookClipboardService);
}else {
Log.e(TAG, "ClipboardService hook failed!");
}
}
public static class ClipboardHookHandlerimplements InvocationHandler {
@Override
public Objectinvoke(Object proxy, Method method, Object[] args)throws Throwable {
String methodName = method.getName();
int argsLength = args.length;
//每次从本应用复制的文本,前面都加上分享的出处,点击复制时会调用setPrimaryClip设置数据
//所以在这里hook住这个方法
if ("setPrimaryClip".equals(methodName)) {
if (argsLength >=2 && args[0]instanceof ClipData) {
ClipData data = (ClipData) args[0];
String text = data.getItemAt(0).getText().toString();
text ="=====斑马信用,天天向上====" + text;
args[0] = ClipData.newPlainText(data.getDescription().getLabel(), text);
}
}
return method.invoke(proxy, args);
}
}
}
这样就实现对ClipBoardService复制操作的hook,当然上面的代码中ServiceHook类的作用在这里就不展开谈了,如果大家感兴趣可以看看源码,大致的作用是这样的:
通过分析ServiceManager源码我们知道asInterface最终是调用queryLocalInterface这个方法返回给Service 的调用方一个IBinder对象。所以 queryLocalInterface 方法的最后返回的对象
是会被外部直接调用的对象,所以我们需要hook住这个方法产生一个被代理的IBinder对象,而对象后续的所有操作都执行都会回调到上面的ClipboardHook 的invoke方法中,而我们只是
改变setPrimaryClip(复制操作执行的函数)这一个方法的操作,我们要偷偷的再这里添加上“=====斑马信用,天天向上====”这句话。
(4)结论
在这里只是大概用几个例子描述下,我所知道的大概几种java层的完成hook的方式,其实还存在nativie层的hook,这块目前还没有研究到,hook是一种思想,是一种实现方式
也是安卓面向切面(AOP)编程的一种体现,但是它却是插件化的基础,现在市面上开源的插件化框架很多,但是,他们的基础原理都基于hook技术,只是不同的插件框架
hook的地方不一样,hook点的多少不一样。
demo源码:hook.zip