|
| 1 | +# ReplaceMethod(对调用的方法进行替换的工具) |
| 2 | + |
| 3 | +### 用它能做什么 |
| 4 | + |
| 5 | +**ReplaceMethod**: 在编译阶段利用ASM对方法进行替换的工具。 **用它可以对调用的方法进行替换**,比如下面的一些例子: |
| 6 | + |
| 7 | +1. 对view.SetOnclickListener方法进行替换。比如: |
| 8 | + |
| 9 | + 代码中的所有的**view.setOnClickListener**方法最终被下面的方法替换 |
| 10 | + |
| 11 | + ``` |
| 12 | + public static void setOnClickListener(View view, View.OnClickListener clickListener, Object[] objects) { |
| 13 | + view.setOnClickListener(new ClickListenerWrapper(clickListener,objects)); |
| 14 | + } |
| 15 | + |
| 16 | + public static class ClickListenerWrapper implements View.OnClickListener { |
| 17 | + private View.OnClickListener listener; |
| 18 | + private Object[] params; |
| 19 | + |
| 20 | + public ClickListenerWrapper(View.OnClickListener listener,Object[] objects) { |
| 21 | + this.listener = listener; |
| 22 | + params = objects; |
| 23 | + } |
| 24 | + |
| 25 | + @Override |
| 26 | + public void onClick(View v) { |
| 27 | + String className = params[0]+""; |
| 28 | + String classSimpleName = className.substring(className.lastIndexOf(".") + 1); |
| 29 | + Log.i(TAG, "click info: (" + classSimpleName + ".java:" + params[3] + ")" + " or (" + classSimpleName + ".kt:" + params[3] + ")"+" view:"+v+" clickListener:"+listener); |
| 30 | + if (listener != null) { |
| 31 | + listener.onClick(v); |
| 32 | + } |
| 33 | + } |
| 34 | + } |
| 35 | + ``` |
| 36 | + |
| 37 | + 上面代码最终的效果是:在view被点击的时候,会打印当前setOnClickListener的具体代码处(类名,方法,行数) |
| 38 | +2. 对各种new Thread() 进行治理,如下: |
| 39 | + |
| 40 | + 把代码中的甚至是第三方库中的所有new Thread()的代码统一都转入下面的方法中生成线程 |
| 41 | + |
| 42 | + ``` |
| 43 | + public static Thread createThread() { |
| 44 | + //使用统一的创建线程的方法重新生成Thread, |
| 45 | + } |
| 46 | + ``` |
| 47 | +3. 排查隐私问题,可以对涉及隐私的方法调用进行替换, 如下: |
| 48 | + |
| 49 | + 获取手机mac的代码如下: |
| 50 | + |
| 51 | + ``` |
| 52 | + WifiManager wm = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE); |
| 53 | + WifiInfo wi = wm.getConnectionInfo(); |
| 54 | + wi.getMacAddress() |
| 55 | + ``` |
| 56 | + |
| 57 | + 可以对**wi.getMacAddress**方法进行替换到下面方法中: |
| 58 | + |
| 59 | + ``` |
| 60 | + public static String getMacAddress(WifiInfo wi) { |
| 61 | + // 增加自己的逻辑,如添加log信息 |
| 62 | + String result = wi.getMacAddress() |
| 63 | + return result; |
| 64 | + } |
| 65 | + ``` |
| 66 | +4. 对第三方jar或aar的方法进行替换,比如对第三方jar中的Log进行拦截,把需要的关键的log信息存储下来,或者修复第三方的bug |
| 67 | +5. 大家还可以根据自己的需要来做其他更有趣的事情 |
| 68 | + |
| 69 | +### 实现原理 |
| 70 | + |
| 71 | +在说实现原理之前,先看下使用**ReplaceMethod**替换方法的例子 |
| 72 | + |
| 73 | +**对Activity的setContentView(int)方法进行替换**例子 |
| 74 | + |
| 75 | + |
| 76 | + |
| 77 | + |
| 78 | + |
| 79 | + |
| 80 | + |
| 81 | +上面例子展示了对Activity的setContentView(int)方法替换, |
| 82 | + |
| 83 | +1. 会在当前的类中生成一个私有的静态的方法(当前的类实例及setContentView的参数作为参数) |
| 84 | +2. 判断当前类是否是Activity的子类,是的话则调用**ReplaceMethodDemo.setContentView(Activity, int)** 方法, 至此setContentView(int)方法被替换为ReplaceMethodDemo.setContentView(Activity, int) |
| 85 | +3. 若当前类不是Activity的子类,则还是执行之前的逻辑 |
| 86 | + |
| 87 | +##### 原理 |
| 88 | + |
| 89 | +看了上面的替换结果,我想大家冒出来的第一个问题是:替换不应该是 xxx.invokeAMethod -----> 被替换为yyy.invokeAMethod 这么简单吗? 为啥要生成私有的静态方法这个啰嗦的不在?关于这个问题在下面给与答复。 |
| 90 | + |
| 91 | +**方法替换的本质**就是: xxx.invokeAMethod -----> yyy.invokeAMethod |
| 92 | + |
| 93 | +围绕本质 ,实现主要做三件事情: |
| 94 | + |
| 95 | +**1 收集替换信息** |
| 96 | +收集替换信息主要是在**.gradle文件中进行配置(为啥没有采用在txt文本文件中进行配置的主要原因是,配置项目确实很多,在文本文件中配置起来非常麻烦,出现错误难以定位问题),具体的配置介绍会在后面介绍 |
| 97 | +收集需要替换的方法信息,在编译过程中通过ASM,查找到替换的方法后,插入替换者的信息字节码。 |
| 98 | + |
| 99 | +**2 定位替换方法** |
| 100 | +利用ASM,根据收集到的信息,去定位具体的方法,定位的时候主要对比:**方法的owner(所属类),方法是静态的还是实例,方法的名称,方法描述符**。在定位**确定的方法**,比如:静态方法调用 **StaticClass.invokeStaticMethod()** (并且StaticClass的父类中没有定义invokeStaticMethod这个方法)的时候,是非常的简单的,在定位**调用的是父类的静态/非静态方法**是不能正确定位的,如上面例子 对**Activity**类的setContentView(int)方法替换,在ChildActivity(Activity子类)调用setContentView(int)方法,这时候的**owner**是ChildActivity ,它和Activity肯定是不一样的,这时候就会导致定位不到,但是ChildActivity中调用的setContentView(int)确实是需要替换的方法,那因此针对这种情况需要特殊处理,处理方法如下: |
| 101 | + |
| 102 | +1. 在当前类中生成一个私有的静态方法,它的参数有(**当前类作为第一个参数**,替换方法的参数。静态/实例方法的参数是不一样的),它的返回值与替换方法保持一致 |
| 103 | +2. 在该静态方法中加入一些判断逻辑,判断第一个参数是否是替换方法owner的子类,是的话进行替换,不是则保存原先逻辑 |
| 104 | +3. 把调用替换方法的地方替换为调用 生成的私有静态方法 |
| 105 | + |
| 106 | +这样,就可以在运行期间来进行检测定位逻辑和替换逻辑了 |
| 107 | + |
| 108 | +**3 替换** |
| 109 | +定位到替换方法后,利用ASM插入对应的字节码,主要分为几种情况: |
| 110 | + |
| 111 | +1. 对于**确定的方法** 的方法,直接替换为目标类的方法名(目标类指yyy),替换后的效果:xxx.invokeAMethod -----> yyy.invokeAMethod |
| 112 | +2. 对于new对象的方法,直接替换为目标类的静态方法(目标类指yyy,静态方法的参数与构造函数参数一致,返回值为new对象对应的类),替换后的效果: new MyClass( int ,int ) -------> yyy.createMyClass( int, int) |
| 113 | +3. 对于调用的是父类的静态/非静态方法,利用ASM在当前类中插入 私有静态方法(见 **2 定位替换方法**),替换后效果:xxx.invokeAMethod -----> 当前类.generateStaticMethod ------> yyy.invokeAMethod |
| 114 | + |
| 115 | +### 接入 |
| 116 | + |
| 117 | +**1.工程的gradle文件** |
| 118 | + |
| 119 | +``` |
| 120 | +buildscript { |
| 121 | + repositories { |
| 122 | + maven { |
| 123 | + url "https://pkgs.d.xiaomi.net:443/artifactory/maven-release-virtual/" |
| 124 | + } |
| 125 | + |
| 126 | + } |
| 127 | + dependencies { |
| 128 | + |
| 129 | + classpath "com.mi.tools:replacemetohd:1.0.3" |
| 130 | + } |
| 131 | +} |
| 132 | +``` |
| 133 | + |
| 134 | +**2.app的gradle文件** |
| 135 | + |
| 136 | +``` |
| 137 | +apply plugin: 'ReplaceMethodPlugin' |
| 138 | +
|
| 139 | +
|
| 140 | +replaceMethod{ |
| 141 | + open true |
| 142 | + openLog true |
| 143 | + logFilters "IInterfaceTest" |
| 144 | + replaceByMethods{ |
| 145 | + register { |
| 146 | + replace { |
| 147 | + invokeType "ins" |
| 148 | + className "android.view.LayoutInflater" |
| 149 | + methodName "inflate" |
| 150 | + desc "(int,android.view.ViewGroup)android.view.View" |
| 151 | + } |
| 152 | + by { |
| 153 | + className = "com.mi.replacemethod.ReplaceMethodDemo" |
| 154 | + methodName = "inflate" |
| 155 | + addExtraParams = true |
| 156 | + } |
| 157 | + } |
| 158 | + } |
| 159 | +} |
| 160 | +``` |
| 161 | + |
| 162 | +在**replaceMethod**中进行配置,可以参考代码中的例子。 |
| 163 | + |
| 164 | +**配置项介绍** |
| 165 | +**open**: true:替换功能打开, false:替换功能关闭 |
| 166 | +**openLog** : true: 编译过程中的log打开, 否则关闭 (日志建议不要打开,否则影响编译速度) |
| 167 | +**logFilters**: 配合openLog使用,只有在openLog为true的情况下才有效。不配置则会把所有日志打印出来,配置后只显示配置的日志,可以配置多个,用","分割,如: logFilters "IInterfaceTest","Main", "AA" |
| 168 | +**replaceByMethods**:注册多个替换方法 |
| 169 | + |
| 170 | +**register**: 注册一对**replace** **by**。 可以这样理解register:**replace**中的方法被**by**方法替换 |
| 171 | + |
| 172 | +**replace**: 配置需要替换的方法,它的属性有: |
| 173 | + |
| 174 | +1. **invokeType**,代表方法类型:静态的,实例,构造方法,它的值有:**static**(静态方法),**ins**(非私有实例方法),**new**(构造方法) |
| 175 | +2. **className**,方法所属的类名, 配置内部类时候必须使用"\\\$", 如 |
| 176 | + |
| 177 | + ``` |
| 178 | + className "(android.view.View\$OnClickListener)" |
| 179 | + ``` |
| 180 | +3. **methodName**,方法的名称 |
| 181 | +4. **desc**,方法描述符配置,格式: (paramType, paramType2,...) returnType。 |
| 182 | + paramType: 基本数据类型直接用基本数据类型,否则使用类的全路径,多个param直接用 **","** 分割 |
| 183 | + returnType: 同上,代表返回类型,返回类型为void,可以不用配置 |
| 184 | + 若方法没有参数并且返回类型为void,则可不用配置该项 |
| 185 | +5. **releaseEnable**: 在buildType为release的时候 替换功能 是否有用。 true:代表该条替换在release进行替换,默认值false |
| 186 | +6. **ignoreOverideStaticMethod**:针对**子类中调用父类定义的非私有静态方法**情况,默认值为false,如下面的例子: |
| 187 | + |
| 188 | + ``` |
| 189 | + register { |
| 190 | + replace { |
| 191 | + invokeType "static" |
| 192 | + className "android.view.View" |
| 193 | + methodName "inflate" |
| 194 | + desc "(android.content.Context,int,android.view.ViewGroup)android.view.View" |
| 195 | + ignoreOverideStaticMethod true |
| 196 | + } |
| 197 | + by { |
| 198 | + className = "com.mi.replacemethod.ReplaceMethodDemo" |
| 199 | + methodName = "inflate" |
| 200 | + addExtraParams true |
| 201 | + } |
| 202 | + } |
| 203 | + ``` |
| 204 | + |
| 205 | + 对View的inflate方法替换,若该值为true,则会忽略子类中重新定义的相同的inflate方法,而直接进行替换。 |
| 206 | +7. **replacePackages**:对哪些package进行替换,不配置则对所有的包进行替换,可以配置多个,如:replacePackages "com.mi","com.niu" |
| 207 | + |
| 208 | +**by**: 配置替换**replace**的方法信息, 它的属性有: |
| 209 | + |
| 210 | +1. **className**: 类名, 配置内部类时候必须使用"\\\$", 如 |
| 211 | + |
| 212 | + ``` |
| 213 | + className "(android.view.View\$OnClickListener)" |
| 214 | + ``` |
| 215 | +2. **methodName**:方法名,必须是**public类型的静态方法**, 若与**replace**的方法同名,则可不用配置 |
| 216 | +3. **addExtraParams**: 是否需要额外数据, true需要,**若配置为true,则在方法的参数中必须有一个Object[] 类型的参数,并且必须是最后一个参数,否则在运行过程中会奔溃**, **Object[]** 信息有:object[0] 调用**replace方法**的类全路径名称, object[1] 调用**replace方法**的方法名称, object[2] 调用**replace方法**的方法描述符合,object[3] 调用**replace方法**的行信息 |
| 217 | + |
| 218 | +### 还未实现功能 |
| 219 | + |
| 220 | +**不能对私有方法进行替换** |
| 221 | +**不能对在子类中调用父类定义的静态方法进行替换**,比如:对在View子类的静态方法中调用View的inflate方法进行替换 |
| 222 | + |
| 223 | +``` |
| 224 | +public class MyView extends View{ |
| 225 | + |
| 226 | + private static void init(Context context, int resource, ViewGroup root){ |
| 227 | + inflate(contet, resource, root); |
| 228 | + } |
| 229 | +
|
| 230 | +} |
| 231 | +``` |
0 commit comments