ReplaceMethod(对调用的方法进行替换的工具)

ReplaceMethod: 在代码编译阶段,根据收集的配置信息,利用ASM对字节码进行替换,以达到对调用的方法进行替换的工具 (您不需要学习怎么写gradle插件,不需要学习ASM非常复杂的语法)

为什么要做这个工具

  1. 治理项目中的线程问题
    背景:由于我做的项目历史非常的悠久并且非常的庞大复杂,项目中的线程没有一个统一的管理方式并且野线程(没有名字的线程)到处飞。
    于是想着使用ASM在编译过程中对所有new Thread的地方进行替换到某个方法中,在这个方法中来统一处理 统一管理
  2. 在做 轻量级LayoutInspector工具 时候, 需要定位view被inflate的位置信息(哪个类的哪个方法哪行)以及view的点击事件的位置信息。想到的办法也是同上面(利用ASM在编译过程中替换字节码来实现)

于是我就想不能每次遇到 对方法替换的时候 就要写重复的写gradle插件,并且在写ASM相关的替换代码,那我为啥不写一个这样的工具呢(并且ASM相关的api真的很复杂),并且这个工具是可以做很多事情的。

用它能做什么

下面列举了几个例子:

  1. 对view.SetOnclickListener方法进行替换(以及对其他的点击事件进行替换)。比如:

    代码中的所有的view.setOnClickListener方法最终被下面的方法替换

    public static void setOnClickListener(View view, View.OnClickListener clickListener, Object[] objects) {
         view.setOnClickListener(new ClickListenerWrapper(clickListener,objects));
     }
    
     public static class ClickListenerWrapper implements View.OnClickListener {
         private View.OnClickListener listener;
         private Object[] params;
    
         public ClickListenerWrapper(View.OnClickListener listener,Object[] objects) {
             this.listener = listener;
             params = objects;
         }
    
         @Override
         public void onClick(View v) {
             String className = params[0]+"";
             String classSimpleName = className.substring(className.lastIndexOf(".") + 1);
             Log.i(TAG, "click info: (" + classSimpleName + ".java:" + params[3] + ")" + " or (" + classSimpleName + ".kt:" + params[3] + ")"+"   view:"+v+"  clickListener:"+listener);
             if (listener != null) {
                 listener.onClick(v);
             }
         }
     }
    

    上面代码最终的效果是:在view被点击的时候,会打印当前setOnClickListener的具体代码处(类名,方法,行数)

  2. 对各种new Thread() 进行治理,如下:

    把代码中的甚至是第三方库中的所有new Thread()的代码统一都转入下面的方法中生成线程

    public static Thread createThread() {
          //使用统一的创建线程的方法重新生成Thread,
    }
    
  3. 排查修复隐私问题(众所周知现在隐私问题国家管控的非常严格),可以对涉及隐私的方法调用进行替换, 如下例子:

获取手机mac的代码如下:

WifiManager wm = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
WifiInfo wi = wm.getConnectionInfo();
 wi.getMacAddress()

可以对项目中所有的wi.getMacAddress方法替换到下面方法中,在这个方法中就可以写自己的逻辑了:

public static String getMacAddress(WifiInfo wi) {
      // 增加自己的逻辑,如添加log信息
      String result = wi.getMacAddress()
      return result;
}
  1. 对第三方jar或aar的方法进行替换,比如对第三方jar中的Log进行拦截,把需要的关键的log信息存储下来,或者修复第三方的bug
  2. 大家还可以根据自己的需要来做其他更有趣的事情

实现原理

在说实现原理之前,先看下使用ReplaceMethod替换方法的例子

对Activity的setContentView(int)方法进行替换例子

原先代码

替换后的class
最终调用的方法

上面例子展示了对Activity的setContentView(int)方法替换,

  1. 会在当前的类中生成一个私有的静态的方法(当前的类实例及setContentView的参数作为参数)
  2. 判断当前类是否是Activity的子类,是的话则调用ReplaceMethodDemo.setContentView(Activity, int) 方法, 至此setContentView(int)方法被替换为ReplaceMethodDemo.setContentView(Activity, int)
  3. 若当前类不是Activity的子类,则还是执行之前的逻辑
原理

看了上面的替换结果,我想大家冒出来的第一个问题是:替换不应该是 xxx.invokeAMethod -----> 被替换为yyy.invokeAMethod 这么简单吗? 为啥要生成私有的静态方法这个啰嗦的不在?关于这个问题在下面给与答复。

方法替换的本质就是: xxx.invokeAMethod -----> yyy.invokeAMethod

围绕本质 ,实现主要做三件事情:

1 收集替换信息
收集替换信息主要是在**.gradle文件中进行配置(为啥没有采用在txt文本文件中进行配置的主要原因是,配置项目确实很多,在文本文件中配置起来非常麻烦,出现错误难以定位问题),具体的配置介绍会在后面介绍
收集需要替换的方法信息,在编译过程中通过ASM,查找到替换的方法后,插入替换者的信息字节码。

2 定位替换方法
利用ASM,根据收集到的信息,去定位具体的方法,定位的时候主要对比:方法的owner(所属类),方法是静态的还是实例,方法的名称,方法描述符。在定位确定的方法,比如:静态方法调用 StaticClass.invokeStaticMethod() (并且StaticClass的父类中没有定义invokeStaticMethod这个方法)的时候,是非常的简单的,在定位调用的是父类的静态/非静态方法是不能正确定位的,如上面例子 对Activity类的setContentView(int)方法替换,在ChildActivity(Activity子类)调用setContentView(int)方法,这时候的owner是ChildActivity ,它和Activity肯定是不一样的,这时候就会导致定位不到,但是ChildActivity中调用的setContentView(int)确实是需要替换的方法,那因此针对这种情况需要特殊处理,处理方法如下:

  1. 在当前类中生成一个私有的静态方法,它的参数有(当前类作为第一个参数,替换方法的参数。静态/实例方法的参数是不一样的),它的返回值与替换方法保持一致
  2. 在该静态方法中加入一些判断逻辑,判断第一个参数是否是替换方法owner的子类,是的话进行替换,不是则保存原先逻辑
  3. 把调用替换方法的地方替换为调用 生成的私有静态方法

这样,就可以在运行期间来进行检测定位逻辑和替换逻辑了

3 替换
定位到替换方法后,利用ASM插入对应的字节码,主要分为几种情况:

  1. 对于确定的方法 的方法,直接替换为目标类的方法名(目标类指yyy),替换后的效果:xxx.invokeAMethod -----> yyy.invokeAMethod
  2. 对于new对象的方法,直接替换为目标类的静态方法(目标类指yyy,静态方法的参数与构造函数参数一致,返回值为new对象对应的类),替换后的效果: new MyClass( int ,int ) -------> yyy.createMyClass( int, int)
  3. 对于调用的是父类的静态/非静态方法,利用ASM在当前类中插入 私有静态方法(见 2 定位替换方法),替换后效果:xxx.invokeAMethod -----> 当前类.generateStaticMethod ------> yyy.invokeAMethod

接入(参考代码中的例子)

1.工程的gradle文件

buildscript {
    repositories {
                maven { url 'https://jitpack.io' }
       
    }
    dependencies {
        classpath "com.github.niuxiaowei:ReplaceMethod:1.0.0"
    }
}
allprojects {
    repositories {
        maven { url 'https://jitpack.io' }
    }
}

2.app的gradle文件

apply plugin: 'ReplaceMethodPlugin'


replaceMethod{
    open true
    openLog true
    logFilters "IInterfaceTest"
    replaceByMethods{
        register {
            replace {
                invokeType "ins"
                className "android.view.LayoutInflater"
                methodName "inflate"
                desc "(int,android.view.ViewGroup)android.view.View"
            }
            by {
                className = "com.mi.replacemethod.ReplaceMethodDemo"
                methodName = "inflate"
                addExtraParams = true
            }
        }
   }
}

replaceMethod中进行配置,可以参考代码中的例子。

配置项介绍
open: true:替换功能打开, false:替换功能关闭
openLog : true: 编译过程中的log打开, 否则关闭 (日志建议不要打开,否则影响编译速度)
logFilters: 配合openLog使用,只有在openLog为true的情况下才有效。不配置则会把所有日志打印出来,配置后只显示配置的日志,可以配置多个,用","分割,如: logFilters "IInterfaceTest","Main", "AA"
replaceByMethods:注册多个替换方法

register: 注册一对replace by。 可以这样理解register:replace中的方法被by方法替换

replace: 配置需要替换的方法,它的属性有:

  1. invokeType,代表方法类型:静态的,实例,构造方法,它的值有:static(静态方法),ins(非私有实例方法),new(构造方法)

  2. className,方法所属的类名, 配置内部类时候必须使用"\$", 如

    className "(android.view.View\$OnClickListener)"
    
  3. methodName,方法的名称

  4. desc,方法描述符配置,格式: (paramType, paramType2,...) returnType。
    paramType: 基本数据类型直接用基本数据类型,否则使用类的全路径,多个param直接用 "," 分割
    returnType: 同上,代表返回类型,返回类型为void,可以不用配置
    若方法没有参数并且返回类型为void,则可不用配置该项

  5. releaseEnable: 在buildType为release的时候 替换功能 是否有用。 true:代表该条替换在release进行替换,默认值false

  6. ignoreOverideStaticMethod:针对子类中调用父类定义的非私有静态方法情况,默认值为false,如下面的例子:

    register {
             replace {
                 invokeType "static"
                 className "android.view.View"
                 methodName "inflate"
                 desc "(android.content.Context,int,android.view.ViewGroup)android.view.View"
                 ignoreOverideStaticMethod true
             }
             by {
                 className = "com.mi.replacemethod.ReplaceMethodDemo"
                 methodName = "inflate"
                 addExtraParams  true
             }
         }
    

    对View的inflate方法替换,若该值为true,则会忽略子类中重新定义的相同的inflate方法,而直接进行替换。

  7. replacePackages:对哪些package进行替换,不配置则对所有的包进行替换,可以配置多个,如:replacePackages "com.mi","com.niu"

by: 配置替换replace的方法信息, 它的属性有:

  1. className: 类名, 配置内部类时候必须使用"\$", 如

    className "(android.view.View\$OnClickListener)"
    
  2. methodName:方法名,必须是public类型的静态方法, 若与replace的方法同名,则可不用配置

  3. addExtraParams: 是否需要额外数据, true需要,若配置为true,则在方法的参数中必须有一个Object[] 类型的参数,并且必须是最后一个参数,否则在运行过程中会奔溃, Object[] 信息有:object[0] 调用replace方法的类全路径名称, object[1] 调用replace方法的方法名称, object[2] 调用replace方法的方法描述符合,object[3] 调用replace方法的行信息

还未实现功能

不能对私有方法进行替换
不能对在子类中调用父类定义的静态方法进行替换,比如:对在View子类的静态方法中调用View的inflate方法进行替换

public class MyView extends View{
  
      private static void init(Context context, int resource, ViewGroup root){
               inflate(contet, resource,  root);
     }

}

代码地址

代码地址:https://github.com/niuxiaowei/ReplaceMethod.git

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容