Skip to content

Commit bc0711b

Browse files
author
niuxiaowei
committed
replacemethod替换方法工具
0 parents  commit bc0711b

File tree

80 files changed

+3538
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

80 files changed

+3538
-0
lines changed

.gitignore

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
*.iml
2+
.gradle
3+
/local.properties
4+
/.idea/*
5+
.idea/
6+
.DS_Store
7+
/build
8+
/captures
9+
.externalNativeBuild
10+
.cxx
11+
local.properties
12+
*.iml
13+
14+

README.md

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
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+
![原先代码](/pictures/1.png)
76+
77+
![替换后的class](/pictures/2.png)
78+
79+
![最终调用的方法](/pictures/3.png)
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+
```

app/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/build

0 commit comments

Comments
 (0)