需求背景

前段时间组长让我调研一下一个技术需求, 需要在项目中每个调用

Log.x(TAG, msg);

的地方判断msg中是否有指定的字段来替换为我们自定义的方法来实现日志上报。

需求调研

首先想到了hook技术, Hook 又叫“钩子”,它可以在事件传送的过程中截获并监控事件的传输,将自身的代码与系统方法进行融入。这样当这些方法被调用时,也就可以执行我们自己的代码,这也是面向切面编程的思想(AOP)。

1. Hook 必须掌握的知识

  • 反射

如果你对反射还不是很熟悉的话,建议看一下这篇博客 Java 反射机制详解。

  • java 的动态代理

动态代理是指在运行时动态生成代理类,不需要我们像静态代理那个去手动写一个个的代理类, 且委托类和代理类需要实现相同的接口。在 java 中,我们可以使用 InvocationHandler 实现动态代理,有兴趣的,可以查看这篇博客 java 代理模式详解。

这里介绍下代理

在某些情况下,我们不希望或是不能直接访问对象 A,而是通过访问一个中介对象 B,由 B 去访问 A 达成目的,这种方式我们就称为代理。
这里对象 A 所属类我们称为委托类,也称为被代理类,对象 B 所属类称为代理类。
代理优点有:

  • 隐藏委托类的实现
  • 解耦,不改变委托类代码情况下做一些额外处理,比如添加初始判断及其他公共操作

根据程序运行前代理类是否已经存在,可以将代理分为静态代理和动态代理。

静态代理

代理类在程序运行前已经存在的代理方式称为静态代理, 静态代理中代理类和委托类也常常继承同一父类或实现同一接口。

1. 1 动态代理

代理类在程序运行前不存在、运行时由程序动态生成的代理方式称为动态代理。这种代理方式的一大好处是可以方便对代理类的函数做统一或特殊处理,如记录所有函数执行时间、所有函数执行前添加验证判断、对某个特殊函数进行特殊操作,而不用像静态代理方式那样需要修改每个函数

这里例举一个权限认证的例子:在app中一部分页面是不用登录就可以使用, 另一个部分页面是需要登录之后才可以访问的情形下, 如果这些需要登录的页面都编写是否登录的逻辑来决定是否跳转到登录页面还是正常跳转, 如果页面比较多,我们的登录逻辑需要修改的话, 这些逻辑都要修改一遍, 在这里我们可以使用动态代理的技术来实现 。

实现动态代理包括三步:

(1). 新建委托接口类;

public interface IAuthorityFilter {

   boolean isLogin();
}

(2). 实现InvocationHandler接口,这是负责连接代理类和委托类的中间类必须实现的接口;

public class AuthorityInvocationHandler implements InvocationHandler {

    private Object mTarget;
    private Context mContext;

    public AuthorityInvocationHandler() {}

    public AuthorityInvocationHandler(Object target, Context context) {
      mTarget = target;
      mContext = context;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object obj = null;
        // long start = System.currentTimeMillis();
        if (LoginUtils.hasLogin()) {
          obj = method.invoke(mTarget, args)
        } else {
          Intent intent = new Intent(mContext, LoginActivity.class);
          mContext.startActivity(intent);
        }
        // 日志记录或者方法的耗时统计, 一般会使用插桩来实现
        // System.out.println(method.getName() + " cost time is:" + (System.currentTimeMillis() - start));
        return obj;
    }
}

(3). 通过Proxy类新建代理类对象。

public class MainActivity extends Activity implements IAuthorityFilter {

 private IAuthorityFilter mAuthorityFilter;

 @Override 
 protected void onCreate(@Nullable Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   //第一个参数的class类对象一定要是第二个参数的实现类 
   mAuthorityFilter = (IAuthorityFilter) (Proxy.newProxyInstance(Operate.class.getClassLoader()/**类加载器*/, new Class[] {IAuthorityFilter.class}/**委托类*/, new AuthorityInvocationHandler(new this, this)/**回调处理*/);

 }

 public void jumpDetailActivity(View view) {
   mAuthorityFilter.isLogin();
 }

 @Override
 public boolean isLogin() {
   // TODO 登录后跳转的页面 
 }
}

优缺点
优点
1)良好的扩展性。修改被代理角色并不影响调用者使用代理,对于调用者,被代理角色是透明的。

2)隔离,降低耦合度。代理角色协调调用者和被代理角色,被代理角色只需实现本身关心的业务,非自己本职的业务通过代理处理和隔离。

缺点
1)增加了代理接口类,实现需要经过代理,因此请求速度会变慢。

 

Hook 选择的关键点

Hook 的选择点:尽量静态变量和单例,因为一旦创建对象,它们不容易变化,非常容易定位。

Hook 过程:

寻找 Hook 点,原则是尽量静态变量或者单例对象,尽量 Hook public 的对象和方法。
选择合适的代理方式,如果是接口可以用动态代理。
偷梁换柱——用代理对象替换原始对象。
Android 的 API 版本比较多,方法和类可能不一样,所以要做好 API 的兼容工作。

具体的hook过程可以参考这几篇文章:

https://www.jianshu.com/p/c431ad21f071


cglib

通过调研后发现cglib是java的代理库不能直接用在android上

Cglib原理是针对目标类生成一个子类,覆盖其中的所有方法,所以目标类和方法不能声明为final类型。

字节码处理

这里需要使用的就是自定义Gradle插件, 使用javassits或者AMS来修改字节码来达到上述的效果。具体请移步。