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

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

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


    atlas.png
    atlas.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.jpeg
WechatIMG88.jpeg

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

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

推荐阅读更多精彩内容

  • 最近几年移动开发业界兴起了「 插件化技术 」的旋风,各个大厂都推出了自己的插件化框架,各种开源框架都评价自身功能优...
    斜杠时光阅读 3,910评论 1 36
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,566评论 25 707
  • 引言 如果App引用的库太多,方法数超过65536后无法编译。这是因为单个dex里面不能有超过65536个方法。为...
    喜欢丶下雨天阅读 9,680评论 2 20
  • 动态加载技术 介绍 在程序运行的时候,加载一些程序自身原本不存在的可执行文件并运行这些文件里的代码逻辑。 动态调用...
    冰点k阅读 3,765评论 1 11
  • 上个世纪七八十年代,在我国农村有一种现象,现在听来会觉得匪夷所思,但在当时却是习以为常的,黎慧、黎智以及苏敏正是这...
    梓博两三事阅读 953评论 11 12