美文网首页产品初入门程序员Android开发
Android插件混淆解决方法以及MultiDex的配置

Android插件混淆解决方法以及MultiDex的配置

作者: 最近是狗爷 | 来源:发表于2016-12-30 17:25 被阅读2400次

    最近在公司做的工作都是插件化相关,所以看了很多插件化的框架。整个插件化的方案现在是比较成熟的,怎样处理ClassLoader,怎么替换Activity生命周期,怎么去处理Receiver和Service,几个主流的框架基本上都是大同小异。我们团队选用了AndroidPluginFramework这个框架,具体的BenchMark其实在很多框架下面都可以看到。如何选取还是取决于自身需求,在插件化这块其实主流的需求一般是两种:

    1. 完全独立的插件。就是给一个APK和宿主没有关系,宿主可以不安装的情况下调起这个APK,让用户无感知。
    2. 非独立插件。这个其实是大部分公司的需求,就是随着公司业务的发展,客户端承载的业务越来越多,这个时候无论是从团队合作的角度还是动态化的角度,都希望各个业务之间解耦,发布能更加独立和动态。这种模式下,一般会抽出一个公共库,给各个组件提供基本功能,比如手淘还未开源的Atlas的结构。
      借用一张架构图,公共库抽象出中间件那个模块,提供给各个组件基本的能力


      atlas.pngatlas.png

    AndroidPluginFramework这个框架是支持这两种场景的,但我们实际业务场景是第二种,非独立插件。公共库中包含里基本的网络,缓存,以及UI框架。当然独立插件最后出来也是独立的APK。基本背景介绍完了,接下来开始讲讲本文的主题吧,关于插件化时代码混淆的问题。这个问题应该很容易想到,我们公共库中提供出去给插件使用的类应该只在宿主中有一份,宿主打包的时候把公共库打包到宿主的APK中,插件只应该在编译过程中用到,gradle中以provide的方式依赖这些代码,比如我们工程中mobilebase是公共依赖库

     provide files(project(':mobilebase').getBuildDir().absolutePath + '/intermediates/bundles/release/classes.jar')
        provide files(project(':mobilebase').getBuildDir().absolutePath + '/outputs/rClasses.jar')
    

    当然就会遇到一个问题是宿主在打release包的时候,会混淆mobilebase类,此时插件是不知道混淆的规则的,所以当插件想去调用公共库时就会ClassNotFound或者method不对。如何解决这个问题,有两种思路

    1. 完全不混淆mobilebase,keep住mobilebase中的所有东西。这个方案适用于你的公共库够薄的情况,比如你各个组件之间公用的东西很少,那适用这个方案。
    2. 使用相同的混淆规则。这个其实听上去相对合理一点的方案,宿主和插件使用相同的混淆的规则,理所当然能解决上面的问题。

    我们其实公共库里的东西还是有点多的,所以准备用第二种方案。
    Proguard在开启混淆时,会在app的 ****/build/outpust/mapping**** 目录下生成四个文件

    dump.txt
    说明 APK 中所有类文件的内部结构。
    mapping.txt
    提供原始与混淆过的类、方法和字段名称之间的转换。
    seeds.txt
    列出未进行混淆的类和成员。
    usage.txt
    列出从 APK 移除的代码。
    

    其中mapping文件是混淆的规则,故我们只需要把这个文件用到插件的混淆配置中即可。所以拷贝这个文件到插件的目录,在插件的proguard-rules中添加

    -applymapping mapping.txt
    

    表示复用mappting,但由于混淆规则中的很多类插件是没有的,所以会有很多的Warning,所以我们配置一下ignore掉这些w,最终插件的混淆配置如下

    -optimizationpasses 5
    -dontusemixedcaseclassnames
    -dontskipnonpubliclibraryclasses
    -dontpreverify
    -verbose
    -optimizations !code/simplification/arithmetic,!field/*,!class/merging/*
    -ignorewarnings
    -printseeds
    
    -applymapping mapping.txt
    

    OK,到这儿本以为能混淆的配置可以了,但运行时发现,插件中调用到公共库的地方并没有被正常混淆,还是找不到混淆后的方法。调研了发现provide的包gradle不会去混淆。。。

    接下来就得去折腾multidex了

    首先现在的问题是,我们不能把公共库打包到插件的apk中,但是以provide方式依赖又会出现无法混淆的问题。看了下AndroidPluginFramework官方提供的混淆建议(作者表示也没有试过)

     具体方法:
            1、开启混淆编译宿主,保留mapping文件
            2、将插件的build.gradle文件中的provided配置换成compile, 因为provided方式提供的包不会被混淆
            3、在插件的混淆配置中apply编译宿主时产生的mapping文件。
            4、接着在插件编译脚本中开启multdex编译。并配置multdex的mainlist,使得原先所有provided的包的class被打入到副dex中。
               这样插件编译完成后,会有2个dex,1个是插件自己需要的代码,1个是原先provided后来改成了compile的那些包。
            5、再将这个原provided的包形成的dex,也就是副dex从apk中删除,再对插件apk重新签名。
    

    简单来说就是先全部混淆,再利用multidex把之前provide的类全部打到第二个dex中,再删除第二个dex,再重新签名得到混淆后的插件APK。

    那就去试用一下multidex这个官方的拆包的库,至于这个multidex的原理以及大量的坑网上都能搜到很多的分析文章,美团也有很多技术分析文章
    我说说我在实施这个过程中的坑吧,就是如何实现把指定的class打包到Class2.dex中。因为我们需要把插件的类打到主dex,其余provide的类打包到第二个dex中。
    这个问题网上最多的答案是在你插件的build.gradle中插入如下脚本

    afterEvaluate {
        tasks.matching {
            it.name.startsWith('dex')
        }.each { dx ->
            def listFile = project.rootDir.absolutePath + '/plugintest/maindexlist.txt'
            println "root dir:" + project.rootDir.absolutePath
            println "dex task found:" + dx.name
            if (dx.additionalParameters == null) {
                dx.additionalParameters = []
            }
            dx.additionalParameters += '--multi-dex'
            dx.additionalParameters += '--main-dex-list=' + listFile
            dx.additionalParameters += '--minimal-main-dex'
            dx.additionalParameters += '--set-max-idx-number=20000'
        }
    }
    

    这段脚本的意思是当你插件的gradle的task graph扫描完成的时候,在dexXXX的任务中插入几个参数,

    1. --main-dex-list= 这个是一个txt文件指明你想哪些类打包到主dex
    2. --minimal-main-dex 最小化主dex,保证主dex中只有上面参数指定的类
    3. --set-max-idx-number 每个dex中最多的方法数(不太确定,大概是这个意思,默认值65535)

    网上的答案大部分是这段脚本,但你发现********并不会生效********。因为dexXXXDebug这个任务只在gradle1.5以下才有,之后就被隐藏了。
    我现在版本是2.2应该怎么配置?

    android{
    dexOptions {
            additionalParameters += '--main-dex-list=maindexlist.txt'
            additionalParameters += '--minimal-main-dex'
            additionalParameters += '--set-max-idx-number=20000'
        }
    }
    

    在Android配置中添加这段即可,当然gradle1.5之后开始提供更多的第三方接口,所以也可以尝试使用
    https://github.com/ceabie/DexKnifePlugin 这个分包插件来完成。
    配置上以上混淆配置和multidex后,再打包插件,发现主dex中并没有我们预料的那些类,反而少了很多,反编译来看,貌似我们配置到maindexlist.txt中的类被混淆了。

    网上查到原来maindexlist.txt中需要配置混淆之后的类名,这个就坑了,实用性大减,分包实在混淆之后,所以流程上来说确实要配置混淆之后的类名。为了简化这个过程,我最终选择keep住插件中的所有类,只会混淆公共依赖类。

    那插件中的类怎么才能全部写到maindexlist中,当然写个脚本扫描一下代码目录即可

    #!/bin/bash
    SPATH=`pwd`'/src/main/java'
    
    function walk()
    {
      for file in `ls $1`
      do
        local path=$1"/"$file
        if [ -d $path ]
         then
          echo "DIR $path"
          walk $path
        else
          a=${path#*/plugintest/src/main/java/}
          echo ${a/.java/.class}>>maindexlist.txt
        fi
      done
    }
     
    echo $SPATH
    walk $SPATH
    
    

    上面得脚本放到插件根目录下,打包前跑一遍便会自动生成maindexlist.txt.
    完成上述之后即可正确混淆,分包也正确,混淆规则和宿主一致。

    截下来就是对于打出来的插件包,删除class2.dex并且重新打包签名

    这也应该是由脚本来完成的工作,由于对于jar命令暂时发现只有update这个操作,所以比较low的方式创建了一个叫class2.dex的空文件用于覆盖打包后apk中的class2.dex。
    脚本如下

    #!/bin/bash
    
    KEYSTORE_NAME=your key file
    KEYSTORE_ALIAS=your key alias
    KEYSTORE_STOREPASS=your key store password
    KEYSTORE_KEYPASS=your key password
    
    INPUT_APK=./build/outputs/apk/plugintest-release.apk
    CLASS2=classes2.dex
    META_INF=./META-INF
    
    UNSIGNED=./build/outputs/apk/plugintest-release.apk
    SIGNED=./build/outputs/apk/plugintest-release_resign.apk
    OPT=./build/outputs/apk/plugintest-release_resign_align.apk
    
    
    jar -uf $UNSIGNED $CLASS2
    jar -uf $UNSIGNED $META_INF
    echo Replace OK!
    
    jarsigner -sigalg MD5withRSA -digestalg SHA1 -keystore $KEYSTORE_NAME -storepass $KEYSTORE_STOREPASS -keypass $KEYSTORE_KEYPASS -signedjar $SIGNED $UNSIGNED $KEYSTORE_ALIAS
    echo Signe OK!
    
    rm -r $OPT
    zipalign 4 $SIGNED $OPT
    echo Zipalign ok!
    
    #rm -r $UNSIGNED
    #rm -r $SIGNED
    echo Operate OK!
    
    

    注意一点是必须要从之前的apk中拷贝出META_INF下面的几个文件,才能完成正常的重新签名,否则插件lib在校验签名时会报失败:


    WechatIMG88.jpegWechatIMG88.jpeg

    以上就是整个插件打包和混淆的过程,由于刚接触插件化不久,如果有更合理的混淆方案,请告知一下,搞这块还是挺蛋疼的,记录一下!

    相关文章

      网友评论

      本文标题: Android插件混淆解决方法以及MultiDex的配置

      本文链接:https://www.haomeiwen.com/subject/pqgmvttx.html