gradle打包_Android Gradle(4)gradle插件V2签名多风味打包

享学课堂诚邀作者:周周
转载请声明出处!

正文大纲

1. gradle是什么 2. groovy语言的特性以及它和java的关系 3. 为什么你的apk打包这么慢 4. 如何利用gradle编程解决工作中的实际问题 5. gradle的高级用法(gradle多渠道快速打包插件)

5. gradle的高级用法(gradle多渠道快速打包插件)

本节小目录

  • productFlavors多渠道打包的优势与缺陷
  • android中签名的那些梗
  • 如何在V1签名校验下写入渠道信息
  • 如何在V2/V3签名校验下写入渠道信息
  • 如何发布gradle插件到mavenLocal

如何在V2/V3签名校验下写入渠道信息

Demo地址:

https://github.com/18598925736/GradleStudy1023/tree/v2v3Sign(请进入 v2v3Sign 分支)

理论基础

阅读本节需要 有扎实的字节码操作的概念基础(不然容易走神懵逼)
RandomAccessFile.java , ByteBuffer.java 有基本的了解.

V2/V3 本质上区别不大,android7.0 优先选择V2签名校验,9.0则优先选择V3签名校验。
相对于V1 ,它保护的并不是签名之前原本apk中存在的文件,而是整个apk的字节码,任何针对apk的篡改(包括上一节中我们在META-INF中塞入的 新文件),都会导致字节码的变化.
一个apk文件,其实本身就是一个zip格式的压缩包,一个zip包所包含的3个部分是
( ZIP内容区块中央目录区块中央目录的结尾区块):

bd40a4f391e32a0b338843acb4cec8c8.png

f3b54bce448aa1ae5fcea2b7d69c6174.png

签名之后多出来的部分,就是签名区块,安装apk的时候,就是对这一块进行校验,如果有人在拿到apk之后,对第1,3,4区块进行了修改,那么就会导致签名校验不通过,安装失败。但是! 如果我们在第2 签名区块中新增一些东西,并不会导致1,3,4区块的变化,从而不会导致校验失败。这也就是 V2/V3签名 下仍然可以写入渠道信息的原因!
那么既然要在这一块字节区块中写入我们自己的数据,我们必须要去了解这一块的内容都有什么含义。

7dc0c83211d96b62336618d7a8b46e0f.png

解读一下上图,Apk签名区块中的大部分数据我们不能去修改,因为 这些数据是用来校验apk的合法性的,一旦篡改,apk不合法,但是,我们在上图蓝色区域 id-Value这一块字节中插入一个id-value,不去影响其他的字节,则不算篡改。

那么,确定V2/V3签名下的 渠道信息注入方案的大方向:

1. 拿到一个apk,确定当前apk经过了V2/V3签名 2. 拷贝原apk,并且重命名为带有渠道信息的apk 3. 在拷贝之后的apk文件的Apk签名区块中加入我们自己的id-value 4. 在安装该apk之后,同样用读取字节码的形式,找到我们自己插入的id-value并解析它

其中最难的是第一点,我们如何确定 一个apk使用了V2/V3签名??以下内容假设已经启用了V2/V3签名

解决这个问题,必须了解 apk的第四部分: EndOfCentralDirectory 的内容.

0a482ff36d8783db9a7662e59eba8023.png

这一部分数据的格式是EOCD格式:其字节数据的内容如下图:

52bed34c892ed981ae6cd964b6642af8.png

当然,上图中的大部分内容暂时对我们没有用,有用的只有一个:

fdb0bced8086a81fc091792788edfb2a.png

它其实表示的是 核心目录,也就是 3 CentralDirectory

0e47666c0ce60f394718b4f192c93bfb.png

相当于与整个apk的字节数据的开始的偏移位置, 我们找到了第三部分的起始位置,也就找到了第二部分

b80010047ad59c8a503a6e89222f181b.png

的结束位置.
然后,我们回顾一下第二部分2ApkSigningBlockapk签名区块的内容,它的结尾部分是一个16个字节长度的 magic 用于标记数据格式的,他是一个 写死的固定值
我们从apk的字节数据中,找到了核心目录 CentralDirectory的起始位置, 假如是200 , 我们往前推16个字节,再读16个字节的长度的数据,然后跟magic的值(这个值是zip格式定义的写死的值)进行对比。如果相同,那么说明存在 2ApkSigningBlockapk签名区块,也就说明当前apk使用了V2/V3签名。

第2步的文件拷贝,用FileOutputStream就行了。

再来解决第3步:

既然已经确定了apk使用了V2/V3签名,那么 我们拷贝了一份apk文件之后,应该如何去向其中加入我们自己的渠道信息呢?
其实在解决第一个问题的时候,我们已经找到了 2ApkSigningBlock签名区块的结束位置,那么,按照签名区块的内容图,我们就可以反推出签名区块在整个apk中的起始位置:

7dc0c83211d96b62336618d7a8b46e0f.png

问:?如何反推?
答:得到 2ApkSigningBlock签名区块的结束位置 之后比如是200,那么减去magic所占16字节,就是 184,再减去8,得到176, 那么176->184就是第二个sizeOfBlock,我们读出他的值,发现是50,这说明,签名区块的总长度除去第一个sizeOfBlock的结果是50,所以,最终得出签名区块的起始位置是: 200 - 50 - 8 = 142 . 从142开始,我们就可以从apk的字节数据中写入,或者读出我们所需的数据。

Demo解读

如果读到这里你依然没有懵逼,那么请继续往下:下面先给出zip四个区的完整图示,请对照着看Demo源码

976d6f3e509b89c8ef40e6d3a40acef4.png

注:我们先不要让gradle插件的东西掺和进来,直接用java的main函数来进行 渠道信息的注入与提取.

48d0c7ea85decbc966ead8f37e08890e.png

98e4fd6cd17e21b53750d56515b85b5b.png

关键步骤1:把apk解析成我们自定义的Apk类的对象

ApkParse.parse(baseApk)

先做这一步,是因为,baseApk文件的字节码内容,我们后面用得着,所以不如先把字节码内容分段保存下来.

961d76daf344fd1d37374cb432a9ee48.png

主要保留了两段,一个是 EndOfCentralDirectory,它是apk文件的最后一段中央目录结尾区,二是 ApkSigningBlock ,apk文件的第二段"apk签名块".ApkParse.parse(baseApk) 方法的内容,在这里重点解读:

public static Apk parser(File file) throws Exception {
        Apk apk = new Apk(file);
        RandomAccessFile apkFile = new RandomAccessFile(file, "r");
        //查找eocd数据
        ByteBuffer eocdBuffer = findEocd(apkFile, Constants.EOCD_COMMENT_OFFSET);
        //先假设注释的长度是0,则整个eocd区的长度就是22,此时去头部寻找核心目录结束标记,此时刚好找到,那么说明eocd区的大小确实是0
        if (null == eocdBuffer) {//但是如果查找失败,那么就假设注释的长度是short的最大值,short2字节,16位,一个16进制的数表示4位,所以需要4个16进制的数,最大值则是0xffff
            eocdBuffer = findEocd(apkFile, Constants.EOCD_COMMENT_OFFSET + Constants
                    .EOCD_COMMENT_MAX_LEN);
        }
        if (null == eocdBuffer) {
            apkFile.close();
            throw new Exception(file.getPath() + " 不是一个标准的apk文件");
        }
        apk.setEocd(new EndOfCentralDirectory(eocdBuffer));
        int cdOffset = apk.getEocd().getCdOffset();//核心目录相对于整个apk的偏移位置

        //查找v2签名块
        apk.setApkSigningBlock(findV2SignBlock(apkFile, cdOffset));
        if (!apk.isV2()) {
            apk.setV1(isV1(file));
        }
        if (!apk.isV1() && !apk.isV2()) {
            apkFile.close();
            throw new Exception(file.getPath() + " 没有签名");
        }
        apkFile.close();
        return apk;
    }

解读最好对照之前我贴的那个图去读代码

  1. 拿到一个apk的File,我们使用RandomAccessFile将它封装,然后我们就能够访问这个文件的任意位置
  2. 采用倒序查找的方式,先找到最后一个区 eocd(EndOfCentralDirectory)相对于整个apk字节码的偏移位置,获得它的所有byte数据(这里由于comment的长度不是固定的,所以只能用假设法来推断,具体如何假设,请看代码)
  3. 上一步中获得的eocd区的所有数据,其中包含了 cd区(CentralDirectory)的开始位置
  4. 利用上一步中获得的cd区(CentralDirectory)的开始位置,再利用假设法,验证当前apk是否存在V2才有的(asb)ApkSigningBlock区,如果存在,就说明这是V2签名的包,我们把asb区的byte数据也保存起来。

关键步骤2:注入渠道信息,生成渠道包

ApkBuilder.generateChannel(channel, apk, file);

一个v2签名的apk拥有4个区域,所以,我们在生成渠道包的时候,也要按照次序把这4个区依次写进去

第一区 : coze(ContentOfZipEntries)

c0aab584af1dd823ef27aa94c3fef5ed.png

先前我们的出了ApkSigningBlock的全部数据,进而得到它的容量大小,先前我们还得到了cd(CentralDirectory)的开始位置,这两个值相减,得出第一区的coze_len长度,由于第一区并未更改任何数据,所以,从源apk中直接写入即可。

第二区:asb(ApkSigningBlock)

808250305ee7259ea3814c00d6e76c43.png

之前我们解析apk文件的时候,保存了整个第二区的byte数据,现在由于我们要注入渠道信息,就可以按照如下步骤:1. 由于我们要插入新的id-value块,整个签名块肯定会增大,先计算出增大之后整个签名块的大小(要考虑到之前就存在渠道信息的情况)2. 构建一个新的ByteBuffer,先写入新签名块的大小写一个long值到sizeOfBlock中,然后是原本就存在的id-value块原样写入3. 再写入新的渠道信息id-value块4. 然后写入第二个sizeOfBlock,注意,值和第一个sizeOfBlock一样5. 最后写入 固定值magic

第三区:cd(CentralDirectory)

b643ac5f3dcfe104e08ab24eefd8bc8e.png

前面获得了cd区的size和offset,那么现在,我就可以从原apk文件中读出原本的数据,直接写入即可。

第四区:Eocd(EndOfCentralDirectory)

a735ab4de3e3261e884cc2af714fb073.png

第四区的数据,大部分都原样写入即可,但是有个特例,

fec1384c44522067a1de3eeda770ecfd.png

由于我们之前在第二区中插入了渠道信息id-value块,所以导致 cd(centralDirectory)区往后移动了。所以,这个值应该要变。
用新的eocd区的大小,加上原来第一区coze的大小,即可newEocd.putInt(newV2Block.capacity() + coze_len);

关键步骤3: 渠道信息的获取

经过了前面两步,我们发现渠道信息已经成功注入,并且生成了渠道包。现在,我们要在app内部编码实现渠道信息的读取。

2a0dbe232795a784782d73c3d9ed66fb.png

280923461b556c5d828cb72f2ba4a19c.png

257484fe4c1f6780d232ebd51c1cde79.png
  1. 仍然是将渠道apk包,解析为我们的Apk类的对象apk
  2. 从apk 中拿到 第二区 asb(ApkSigningBlock)的 id-value对,再从中找到我们的渠道信息

Ok,关键代码解析完毕,我们来看看Main的运行结果:

de9f83eb88985083f29b18d7df0f3b02.png

1043f1bab463475baa3f85cf11ec4179.png


Main方法只是用来测试我们的V2渠道注入方式是否正确。最终我们需要在gradle插件中使用。

7d7a1b42b388c0b5cfd1a056e3cc1fa4.png

最终gradle插件运行效果.

33e2c4b8184830ad29d811fc688f79cf.png

生成了16个渠道包。几乎是瞬间完成。经过测试,可以正常安装以及读取渠道信息。

总结

  1. 字节码的操作,相对于 V1签名的压缩文件IO流操作,难度几乎是上了好几个台阶,要求熟悉 RandomAccessFile,ByteBuffer 的使用,以及 熟悉apk即zip文件格式的各个细节,稍有不慎,字节码写入错误,就会导致apk签名验证失败。所以必须细心。探索的过程中,我写入字节码好几次都导致了签名验证失败,后来发现有这么一个工具Fairdell HexCmp2 ,可以对文件的字节码进行对比,对于字节码的读写也算是有帮助。
  2. 关于zip文件格式的思考,一个apk文件也就是zip格式,谷歌可以在它的第一和第二区之间插入一个签名块,其实我们也可以在某个位置插入我们自己的信息,比如,最后一区(EndOfCentralDirectory)的comment字段,这个字段没有特别的含义,只是zip文件的注释而已,只要我们改了这个注释之后,把comment-length这个字段也改了,那么,整个apk仍然是合法的,不会导致签名验证失败。这也就让我们对渠道信息的注入多了一种方式,把渠道信息写到comment中去,然后app从comment中去读渠道信息, 其实这种做法,在上面的demo中已经有了体现,有兴趣的读者可以去找找。

如何发布gradle插件到mavenLocal

本文详解如何在androidStudio中,搭建以及使用maven本地仓库

搭建

从无到有

1. 创建一个javaLibrary module名字自取.

9d26f6b950c3f65a3aa7179a0803e532.png

f2f48613415914aa1183e643e91e5ae6.png

2. 找到这个module的build.gradle文件

7f8ce1994c73e720c7b6497e2c1a82b4.png

此时,右侧Gradle栏目中,zzsyc的Tasks是长这样:

63e65f6fe1d2e7b16ec8c7611da0cda9.png

接下来我们进行处理:

c2f7f714ac0550a5f01081ccaec33098.png

在配置publishing闭包的时候

publishing {//然后进行发布配置
    publications {
        zzsycChannelPlugin(MavenPublication) {//zzsycChannelPlugin 是发布配置命令的名字
            from components.java // 这个命令来自java
            groupId = 'com.zhou.plugins' //插件包名,在此插件被引用的时候必须写成一摸一样
            artifactId = 'channelPlugin' //插件名,在此插件被引用的时候必须写成一摸一样
            version = '1.2'//插件版本号
        }
    }
}

要特别注意3个参数:

groupId 这是maven的群组ID, artifactId 是插件名,version是插件版本号,三者完全匹配才能准确定位到一个插件, 定义插件的时候可以 随便写,但是 使用插件的时候,必须 一毛一样,否则编译失败,说找不到插件。

3. 新建java类,编辑插件逻辑

7327f18a6ba7f1b699287c20c72e8414.png

83443fec243f207ef48350bb78830ecf.png

e6ef533d56940ae2af3d75ecfbf330a8.png

最最简单的插件,也必须有两个类,一个 impelement Plugin<Project>,一个 extends DefaultTask

4. 创建插件配置文件

accc0cbf05fea694a6f25a992e91a07e.png

这里要特别注意:

3bdd7e4ece29230213413412845c1e7f.png

这里有3级目录, resources / META-INF / gradle-plugins ,然后才是文件名:hank.zhou.plugin.properties
这里的目录,必须创建成一摸一样,否则,会有未知错误。

5. 发布插件到mavenLocal

如果上述过程都完成,而且没有报编译错误,那么就可以发布插件到mavenLocal:

a7125cb27a9f2fa57afd9bf281fff9da.png

注:如果刚才的java代码中存在中文,那么,你有可能会看到:

b5e98af742f44f8d79a2ff4a4e3477aa.png

此时,只需要将文件的编码格式从utf-8改为GBK即可:

4797aa03bb24388d164c5047ab6fab65.png

如果顺利,就会发布成功:

fc1b6db4613c67923774406f64d10e9c.png
我们到本地的maven仓库目录下去找找看:

411c6b35838001d94c3a14c926de3e14.png

发布成功!

使用本地gradle插件

使用的时候,两个步骤:

1. 在全局build.gradle 文件中,加入两个部分:

8c7bad6faabe948607c255238843ecb1.png

注:这里classpath 'com.zhou.plugins:channelPlugin:1.2'也是分为3个部分,一个groupId,冒号后面接,artifactId,冒号,接版本号.

2. 在app module的build.gradle中, 引用此插件

59559b44a451b2782032be2a77c725c7.png

注意,上图中,apply plugin: 'hank.zhou.plugin' 是新增的代码。其中 hank.zhou.plugin 是和 hank.zhou,plugin.properties 的文件名是一摸一样的.
gradle同步成功之后,就能看到我们自定义的Task:

b40bdcd8f595731ed669b6bc6c1d383a.png

如果你还没有搭建maven本地仓库的话

1. 先去官网下载maven最新版:

f8a191717f96022e9e58245eee6a4792.png

2. 下载好之后,解压到本地,并且进入bin目录

4fc9c41a5e6d51aa7f72b8323a889884.png

3. 在此处打开cmd,输入mvn help:system 回车

1aaa72959f0838e2f955427a0fcf9d48.png

772f7d011ef281a9b20be5fffa9d7643.png

直到:

d262af479f2157122e44124437c038c3.png

才算搭建成功!之后就可以在用户目录下找到.m2 文件夹,从而找到自己发布的插件.

98e9874f7761236d815f613995b42053.png

结语

文中我提供了完整可运行的Demo,配上详尽的理论基础和demo解读,大部分人阅读起来应该难度不大,但是如果项目跑不通,或者跑的过程中出现了乱码,或者gradle插件编译不成功的问题, 问了度娘也没有解决的话,请直接联系本人本人水平有限,此Demo来自某位高人,本人解读的不到位的地方,望各位海涵,欢迎多多留言评论。
既然来了,点个关注再走呗~
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值