聊聊Android 热修复Nuwa有哪些坑

 

聊聊Android 热修复Nuwa有哪些坑

标签: AndroidNuwaClassLoadeHotpatch
  4889人阅读  评论(14)  收藏  举报

原创地址:http://blog.csdn.net/sbsujjbcy/article/details/51028027

前面写了两篇关于Nuwa的文章

然后我说了Nuwa有坑,有人就问Nuwa到底有哪些坑,这篇文章对自己在Nuwa上走过的坑做一个总结,如果你遇到了其他坑,欢迎留言,我会统一加到文章中去。当然有些也不算是Nuwa的坑,算是ClassLoader这种方式进行热修复暴露出来的问题吧。

坑一、混淆有哪些坑

  • excludeClass没有参考混淆产物mapping.txt,导致无法exclude掉一些不需要处理的类

在不混淆的情况下,Nuwa在这一方面是没有什么问题的,但是一旦混淆了,有些类你不想让他注入字节码,它却注入了,这是为什么呢,原因是Nuwa处理的是混淆后的jar,混淆后的jar包名和类名发生了变化,你再使用配置进去的excludeClass是无法主动不进行字节码注入处理的,除非你加进去的是混淆后的类名,但是在没混淆前,我们是根本不知道混淆后的类名的,有人说,我可以先混淆一遍,混淆完了查看一下mapping文件,找到对应的混淆后的类名,加到excludeClass中去,可以是可以,难道你不觉得蛋疼吗,而且这样也很有可能出现差错。那么有没有更好的方法呢?当然有。

混淆后在outputs目录下会产生一个mapping.txt文件,我们能不能解析这个文件,将混淆后的类还原为原来的类名呢,这个文件的大致内容就像下面这样。

android.support.graphics.drawable.AnimatedVectorDrawableCompat$1 -> android.support.a.a.c:
    android.support.graphics.drawable.AnimatedVectorDrawableCompat this$0 -> a
    629:629:void <init>(android.support.graphics.drawable.AnimatedVectorDrawableCompat) -> <init>
    632:633:void invalidateDrawable(android.graphics.drawable.Drawable) -> invalidateDrawable
    637:638:void scheduleDrawable(android.graphics.drawable.Drawable,java.lang.Runnable,long) -> scheduleDrawable
    642:643:void unscheduleDrawable(android.graphics.drawable.Drawable,java.lang.Runnable) -> unscheduleDrawable
android.support.graphics.drawable.AnimatedVectorDrawableCompat$AnimatedVectorDrawableCompatState -> android.support.a.a.d:
    int mChangingConfigurations -> a
    android.support.graphics.drawable.VectorDrawableCompat mVectorDrawable -> b
    java.util.ArrayList mAnimators -> c
    android.support.v4.util.ArrayMap mTargetNameMap -> d
    473:503:void <init>(android.content.Context,android.support.graphics.drawable.AnimatedVectorDrawableCompat$AnimatedVectorDrawableCompatState,android.graphics.drawable.Drawable$Callback,android.content.res.Resources) -> <init>
    507:507:android.graphics.drawable.Drawable newDrawable() -> newDrawable
    512:512:android.graphics.drawable.Drawable newDrawable(android.content.res.Resources) -> newDrawable
    517:517:int getChangingConfigurations() -> getChangingConfigurations
android.support.graphics.drawable.AnimatedVectorDrawableCompat$AnimatedVectorDrawableDelegateState -> android.support.a.a.e:
    android.graphics.drawable.Drawable$ConstantState mDelegateState -> a
    424:426:void <init>(android.graphics.drawable.Drawable$ConstantState) -> <init>
    430:434:android.graphics.drawable.Drawable newDrawable() -> newDrawable
    439:443:android.graphics.drawable.Drawable newDrawable(android.content.res.Resources) -> newDrawable
    448:452:android.graphics.drawable.Drawable newDrawable(android.content.res.Resources,android.content.res.Resources$Theme) -> newDrawable
    457:457:boolean canApplyTheme() -> canApplyTheme
    462:462:int getChangingConfigurations() -> getChangingConfigurations

   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

仔细观察一下,还是挺有规律的,第一行是原始类名对应的混淆类名,中间用->分割,之后是原始变量名对应的混淆后的变量名,还是用->分割,但是开头缩进了四个空格。最后是方法的混淆,最前面是方法的行数,使用:分割,两个数字分割后再跟一个:,后面就是原始方法名对应的混淆方法名,也是使用->分割。方法和变量都是有类型的。你是不是想到怎么解析了,没错,正则表达式,别急着写代码,在写代码之前我们先看看有没有造好的轮子可以用用,在github上搜一下proguard,没结果。。。再换个关键字,retrace,为什么是retrace呢,因为proguard自带了一个脚本叫retrace,可以从混淆后的异常信息还原为原始类的异常信息。结果出来了,在code中选择java,第一页的最后一条就是。这里我把这个仓库fork到自己的仓库中去了,见地址https://github.com/lizhangqu/retrace

当然不能完完全全的直接用,其实我们用得到的就三个类,一个是ClassMapping.java,一个是MethodMapping.java,还有一个是Retrace.java,至于如何改造,靠你自己了,源码都摆在你面前了你还不会改造?改造后的结果就是传入混淆后的全类名,返回原始的全类目,这样跟excludeClass进行对比就能正确处理了。

  • 没有被修改的类被却被打进了patch,为什么?

我们修改了一个复杂一点的类,准备打patch了,发现被打进patch的类怎样不是一个,还包含了一大堆其他的类,为什么呢?打修复包时利用正式包的mapping,修复bug,修改了原先的类,改变的类会改变,但有些类没有改变也会因为混淆的关系产生变化(混淆会剔除一些无用的方法,打修复包时那些无用的方法可能会加上),这就造成了有些类没有修改,但也会出现在修复包中。当然这种情况出现的概率还是挺大的,但是出现的类的数量就不一定了,有多了一个的,也有多了一坨的。。。。怎样解决。。。无解,多就多了呗。。。最多也就是patch包大小变大了。只能尽量避免这种情况的发生,比如打修复包的时候不要修改原有的缩进,一不小心手贱重新进行格式化,可能原来的代码没有格式化,你这么一格式化,整个类都发生了变化,包括这个类的内部类,这样patch的类的数量就会爆增。所以打patch的时候应该尽可能的减少代码的改动。

坑二、Application直接引用的类无法打Patch

  • 为什么会出现这种现象?

    出现这种现象的原因是Application类我们没有引用hack.apk,为什么不引用呢,因为在加载Application类之前我们还没加载hack.apk,引用了就会报找不到类的异常,于是这个类不能打,并且在加载hack.apk前用的类都不能引用hack.apk。于是就导致了Application类被打上了那个标记进行了校验。然后Application直接引用的类就无法打patch了,一打patch就会报那个异常Class ref in pre-verified class resolved to unexpected implementation

  • 如何解决这个问题?

    直接引用的类不能打patch,但是间接引用的可以打呀,把直接引用的类改成间接引用就ok了,怎么做呢?新建一个中间类,比如PatchUtil,里面有一个init方法,入参是Application,把原来在Application中的逻辑全都转移到PatchUtil中去,然后Application引用PatchUtil类进行调用,最终将一大推直接引用的类变成了间接引用,同时PatchUtil变成了直接引用的类,于是原来一大坨不能打patch的类变成了一个类不能打patch,还是值得的。

坑三、字节码注入的坑

  • 注入失败的原因是什么(混淆和私有构造函数)?

    如果代码不混淆,字节码注入是没问题的,所以这个原因还是混淆导致的,混淆之后,很多类没有了< init >,或者< init > 变成了 < clinit >,为什么会这样呢,我估计是剔除了无用方法导致的。还有一个特殊的情况就是私有构造函数,比如单例的情况下就存在只有一个私有构造函数,私有构造函数字节码中没有 < init >,甚至更绝,也没有 < clinit >,这种情况是百分百注入不进去的,而Nuwa的逻辑是判断name是不是等于< init >并且在构造函数的末尾。但是实际测试情况是绝大多数的类混淆之后字节码中都没有< init >或者< clinit >。

  • 如何去解决注入失败的问题?

    能不能插一个成员变量呢,实际测试结果是不能。。。具体原因我也不清楚。那么没有构造函数就给它插一个构造函数,但是却又不能显示的插一个构造函数,因为这种情况也可能是有问题的,比如原来就有一个私有构造函数,你再插一个公有构造函数,肯定是有问题的,于是就演变成了给它插一段静态初始化的代码就可以了,在这段代码中直接引用Hack.class。就像这样子

<code class="hljs avrasm has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: "Source Code Pro", monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">static{
    System<span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.out</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.println</span>(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">com</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.package</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.Hack</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.class</span>)<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">;</span>
}</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li></ul>

至于这段代码怎么插。。。我表示用asm插我真的不会插,所以我把Nuwa插字节码的那段代码从使用asm插字节码替换成了用javassist插,至于怎么插,见后文。

  • 如何修改注入的字节码使其找不到类也不会报错

Nuwa原来的字节码注入是在构造函数中注入一段这样的代码

<code class="hljs avrasm has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: "Source Code Pro", monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">System<span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.out</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.println</span>(Hack<span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.class</span>)<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">;</span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li></ul>

这段代码有什么问题呢,仔细用脑子想一下,万一有些类在加载Hack.class之前就使用了,并且我们一不小心给他注入了这段代码,那么程序运行就会立马crash,于是,我们想能不能不让这段代码执行呢,答案是可能的,通过一个if语句,让它永远进不去这个if语句就可以了,下面是一种方式,当然你完全可以使用其他类似的代码

<code class="hljs avrasm has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: "Source Code Pro", monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">if(Boolean<span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.FALSE</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.booleanValue</span>()){
    System<span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.out</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.println</span>(Hack<span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.class</span>)
}</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li></ul>

这样这段代码就永远不会被执行,即使提取使用了某个不应该使用的类,程序也不会crash,最多是控制台输出一条log,说这个引用的类找不到。而实际测试结果是,即使报了这个log,也还是能打patch的。

坑四、不支持gradle 1.5以上

  • 如何解决? 
    • Hook方式解决 
      hook的方式和1.2.3是一样的,只不过hook 1.5的gradle比1.2.3的处理要简单许多,具体实现可以见这个实现 AndHotFix hook的task的名字叫transfromClassesWidthDexForRelease或者transfromClassesWidthDexForDebug,在这个task之前执行我们的注入操作就可以了。
    • 使用transform api解决 
      除了hook,还可以使用gradle1.5的新的api来解决,也就是transform接口,具体实现可以参考我前面的一篇文章 Android 热修复使用Gradle Plugin1.5改造Nuwa插件,这种方式有一个缺点,我们处理的类是没有被混淆前的类,处理完后打patch的时候需要在代码中根据配置文件以及mapping文件进行一次代码级别的混淆操作,当然这个操作也是全自动的,用代码来进行混淆即可,缺点是一个类的内部类都会被打进patch。所以也不是特别合适,反倒hook的方式更加灵活。

坑五、patch包没有进行签名校验

  • 如何防止patch包被非法篡改? 
    patch包在app端的校验是必须的,因此校验的前提是对patch进行签名,如何签名呢?可以参考携程的打包脚本 https://github.com/CtripMobile/DynamicAPK,里面有对apk进行sign和zipalign的脚本,拿来稍微修改一下就可以使用了。下面是我修改后的脚本
<code class="hljs cs has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: "Source Code Pro", monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">static</span> <span class="hljs-title" style="box-sizing: border-box;">signedApk</span>(Logger logger, def variant, File apkFile) {
    <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">if</span> (!apkFile.exists())
        <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span>;

    def signingConfigs = variant.getSigningConfig()
    <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">if</span> (signingConfigs == <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">null</span>) {
        logger.error <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"no need to sign"</span>
        <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span>;
    }


    def args = [JavaEnvUtils.getJdkExecutable(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'jarsigner'</span>),
                <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'-verbose'</span>,
                <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'-sigalg'</span>, <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'MD5withRSA'</span>,
                <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'-digestalg'</span>, <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'SHA1'</span>,
                <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'-keystore'</span>, signingConfigs.storeFile,
                <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'-keypass'</span>, signingConfigs.keyPassword,
                <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'-storepass'</span>, signingConfigs.storePassword,
                apkFile.absolutePath,
                signingConfigs.keyAlias]

    def proc = args.execute()
}


<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">static</span> <span class="hljs-title" style="box-sizing: border-box;">zipalign</span>(Project project, File apkFile) {
    <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">if</span> (apkFile.exists()) {
        def sdkDir
        Properties properties = <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">new</span> Properties()
        File localProps = project.rootProject.file(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"local.properties"</span>)
        <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">if</span> (localProps.exists()) {
            properties.load(localProps.newDataInputStream())
            sdkDir = properties.getProperty(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"sdk.dir"</span>)
        } <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">else</span> {
            sdkDir = System.getenv(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"ANDROID_HOME"</span>)
        }
        <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">if</span> (sdkDir) {
            def cmdExt = Os.isFamily(Os.FAMILY_WINDOWS) ? <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'.exe'</span> : <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">''</span>
            File dest = <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">new</span> File(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"${apkFile.absolutePath}.zipalign"</span>);
            def argv = []
            argv << <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'-f'</span>    <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">//overwrite existing outfile.zip</span>
            <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// argv << '-z'    //recompress using Zopfli</span>
            argv << <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'-v'</span>    <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">//verbose output</span>
            argv << <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'4'</span>     <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">//alignment in bytes, e.g. '4' provides 32-bit alignment</span>
            argv << apkFile.absolutePath

            argv << dest.absolutePath  <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">//output</span>

            project.exec {
                commandLine <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"${sdkDir}/build-tools/${project.android.buildToolsVersion}/zipalign${cmdExt}"</span>
                args argv
            }

            <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">if</span> (apkFile.exists()) {
                apkFile.delete()
            }
            dest.renameTo(apkFile)
        } <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">else</span> {
            <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">throw</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">new</span> InvalidUserDataException(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'$ANDROID_HOME is not defined'</span>)
        }
    }
}</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li><li style="box-sizing: border-box; padding: 0px 5px;">16</li><li style="box-sizing: border-box; padding: 0px 5px;">17</li><li style="box-sizing: border-box; padding: 0px 5px;">18</li><li style="box-sizing: border-box; padding: 0px 5px;">19</li><li style="box-sizing: border-box; padding: 0px 5px;">20</li><li style="box-sizing: border-box; padding: 0px 5px;">21</li><li style="box-sizing: border-box; padding: 0px 5px;">22</li><li style="box-sizing: border-box; padding: 0px 5px;">23</li><li style="box-sizing: border-box; padding: 0px 5px;">24</li><li style="box-sizing: border-box; padding: 0px 5px;">25</li><li style="box-sizing: border-box; padding: 0px 5px;">26</li><li style="box-sizing: border-box; padding: 0px 5px;">27</li><li style="box-sizing: border-box; padding: 0px 5px;">28</li><li style="box-sizing: border-box; padding: 0px 5px;">29</li><li style="box-sizing: border-box; padding: 0px 5px;">30</li><li style="box-sizing: border-box; padding: 0px 5px;">31</li><li style="box-sizing: border-box; padding: 0px 5px;">32</li><li style="box-sizing: border-box; padding: 0px 5px;">33</li><li style="box-sizing: border-box; padding: 0px 5px;">34</li><li style="box-sizing: border-box; padding: 0px 5px;">35</li><li style="box-sizing: border-box; padding: 0px 5px;">36</li><li style="box-sizing: border-box; padding: 0px 5px;">37</li><li style="box-sizing: border-box; padding: 0px 5px;">38</li><li style="box-sizing: border-box; padding: 0px 5px;">39</li><li style="box-sizing: border-box; padding: 0px 5px;">40</li><li style="box-sizing: border-box; padding: 0px 5px;">41</li><li style="box-sizing: border-box; padding: 0px 5px;">42</li><li style="box-sizing: border-box; padding: 0px 5px;">43</li><li style="box-sizing: border-box; padding: 0px 5px;">44</li><li style="box-sizing: border-box; padding: 0px 5px;">45</li><li style="box-sizing: border-box; padding: 0px 5px;">46</li><li style="box-sizing: border-box; padding: 0px 5px;">47</li><li style="box-sizing: border-box; padding: 0px 5px;">48</li><li style="box-sizing: border-box; padding: 0px 5px;">49</li><li style="box-sizing: border-box; padding: 0px 5px;">50</li><li style="box-sizing: border-box; padding: 0px 5px;">51</li><li style="box-sizing: border-box; padding: 0px 5px;">52</li><li style="box-sizing: border-box; padding: 0px 5px;">53</li><li style="box-sizing: border-box; padding: 0px 5px;">54</li><li style="box-sizing: border-box; padding: 0px 5px;">55</li><li style="box-sizing: border-box; padding: 0px 5px;">56</li><li style="box-sizing: border-box; padding: 0px 5px;">57</li><li style="box-sizing: border-box; padding: 0px 5px;">58</li><li style="box-sizing: border-box; padding: 0px 5px;">59</li><li style="box-sizing: border-box; padding: 0px 5px;">60</li><li style="box-sizing: border-box; padding: 0px 5px;">61</li><li style="box-sizing: border-box; padding: 0px 5px;">62</li></ul>

然后客户端需要做的就是根据这个patch的前面和当前app的签名进行校验即可。

坑六、ASM字节码注入的维护成本高

这个不能算是Nuwa的坑,只不过Nuwa使用了ASM来进行注入字节码,ASM的可读性实在是太差,对于不懂字节码的人来说有一定的难度,所以必须提高代码的可读性,降低维护成本。

  • 如何降低维护成本

替换asm为javassist,相对asm来说,javassist在性能上可能差一点,但是在可读性上,那绝对是对开发人员友好的,因为写的就是java代码。下面我们来演示一下注入之前说的那段代码

<code class="hljs avrasm has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: "Source Code Pro", monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">if(Boolean<span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.FALSE</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.booleanValue</span>()){
    System<span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.out</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.println</span>(Hack<span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.class</span>)
}</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li></ul>
<code class="hljs avrasm has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: "Source Code Pro", monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">ClassPool classPool = ClassPool<span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.getDefault</span>()<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">;</span>
//这里动态生成Hack类,插入到classpatch中,因为javassist生成字节码需要依赖这个类,这里采用动态生成
CtClass hackClass = classPool<span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.makeClass</span>(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"com.lizhangqu.hack.Hack"</span>)
byte[] hackBytes = hackClass<span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.toBytecode</span>()
hackClass<span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.defrost</span>()
classPool<span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.insertClassPath</span>(new ByteArrayClassPath(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"com.lizhangqu.hack.Hack"</span>, hackBytes))</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li></ul>

Nuwa原来注入字节码的函数原型是这样的

<code class="hljs cs has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: "Source Code Pro", monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">private</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">static</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">byte</span>[] <span class="hljs-title" style="box-sizing: border-box;">referHackWhenInit</span>(InputStream inputStream) {
}</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li></ul>

入参是InputStream,返回值是字节码的byte数组,我们不改变函数原型,编写这个注入函数

<code class="hljs cs has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: "Source Code Pro", monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">private</span>
    <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">static</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">byte</span>[] <span class="hljs-title" style="box-sizing: border-box;">referHackByJavassistWhenInit</span>(ClassPool classPool, InputStream inputStream) {
        CtClass clazz = classPool.makeClass(inputStream)
        CtConstructor ctConstructor = clazz.makeClassInitializer()
        ctConstructor.insertAfter(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"if(Boolean.FALSE.booleanValue()){System.out.println(com.weidian.hack.Hack.class);}"</span>)
        def bytes = clazz.toBytecode()
        clazz.defrost()
        <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> bytes
    }</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li></ul>

入参多了一个ClassPool参数,这个参数就是前面的那个ClassPool,里面包含了Hack这个类。这里面的关键是makeClassInitializer函数,这个函数的作用就是生成一段静态初始化的代码,如果不存在的话会新建一个,存在的话就返回,然后我们在这个最后面插入一段字节码,即

<code class="hljs avrasm has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: "Source Code Pro", monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">if(Boolean<span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.FALSE</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.booleanValue</span>()){System<span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.out</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.println</span>(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">com</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.lizhangqu</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.hack</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.Hack</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.class</span>)<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">;}</span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li></ul>

插入完成后转换成字节数组,记得调用defrost方法进行解冻,否则会有异常。最终生产的代码就是这样的。

<code class="hljs avrasm has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: "Source Code Pro", monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">static{
    if(Boolean<span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.FALSE</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.booleanValue</span>(){
        System<span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.out</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.println</span>(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">com</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.lizhangqu</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.hack</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.Hack</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.class</span>)<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">;</span>
    }
}</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li></ul>

坑七、Android各版本的兼容性如何

  • 在Android5.0与6.0上兼容性表现得如何? 
    实际情况下,我测了三个系统版本,即4.4,5.0,6.0,实际测试结果怎么样呢,三个系统版本打patch都是没有问题的,唯一需要特殊处理的系统可能是6.0,为什么是6.0呢,因为6.0多了一个运行时权限申请。

  • Android 6.0 动态权限申请的坑

为什么这是个坑呢,因为测试的时候我是把patch放到sdcard根目录进行测试的,这种情况下,对应6.0的系统来说,读写sdcard除了需要在manifest文件中进行声明之外,还需要动态申请权限,因为对于用户来说,读写sdcard属于危险权限,需要用户主动授权,所以6.0的系统,如果你的patch在sdcard,你可能需要加入类似这样的申请权限的代码

<code class="hljs avrasm has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: "Source Code Pro", monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">int permission = ContextCompat<span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.checkSelfPermission</span>(getApplicationContext(), Manifest<span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.permission</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.WRITE</span>_EXTERNAL_STORAGE)<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">;</span>
if (permission != PackageManager<span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.PERMISSION</span>_GRANTED) {
    Log<span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.e</span>(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"TAG"</span>, <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"未授权"</span>)<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">;</span>
    ActivityCompat<span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.requestPermissions</span>(this,
            new String[]{Manifest<span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.permission</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.WRITE</span>_EXTERNAL_STORAGE},
            <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">100</span>)<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">;</span>
}
</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li></ul>

之后只要用户授权了,就能正常的打patch了。

以上就是最近我遇到的一些坑的简述以及简单的给出了解决思路,如果你遇到了其他坑,欢迎留言。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值