结合友盟统计的多渠道快速打包

来源:互联网 发布:大数据团队功能 编辑:程序博客网 时间:2024/06/05 06:42


又是周二啦,由于大家的积极投稿,所以今天还是推送投稿文章,这里感谢作者们的大力支持。


多渠道打包我之前参考过美团,感兴趣的朋友可以看看:

http://tech.meituan.com/mt-apk-packaging.html


但是本篇来自 何以诚 的投稿,另辟蹊径,从zip属性特点出发,实现了多渠道打包(文中的使用的zip属性我也不知道,借此我也学习了一下)。


何以诚 的博客地址:

http://blog.csdn.net/u013022222


前言


随着业务的增长,传统的多渠道打包方式已经不符合需求。比如,我们需要在360, 豌豆荚等平台发布新版本,就必须对每一个应用商店编译一份apk,然后发布。可是如果我们要发十来个应用商店呢?是不是还要再编译一次?然而我们只是改变了友盟的渠道号,就必须再打包一次,这对时间显然是种巨大浪费。所以我们必须寻找突破,最好是在原来的基础之上,仅仅需要一点点的修改,就能够做到快速多渠道打包。


一次偶然的机会(在地铁上,晕厥了一会儿),我想到:友盟对渠道的判断无非就是如下的代码:




这里有的朋友可能会感到困惑,我提一下,在打包应用时,如果我们是要发布到QQ的应用宝,通常是在 AndroidManifest.xml 中修改如下的代码:


<meta-data android:value="QQ" android:name="UMENG_CHANNEL"/>


只要将 value 设置为QQ,只要安装了此应用的人,都会被认为是通过QQ应用宝安装的应用。


好了,科普完就讲正事。我们看看上面的代码,首先是 MainActivity.this.getPackageManager() 获得 PackageManager,然后通过调用 getApplicationInfo 获得 AndroidManifest.xml 中的 meta-data


这里有个关键点,就是获得 PackageManager 服务。阅读过我之前文章的人都知道,我们是 有办法Hook系统服务,来修改它的行为的。但是为了通俗易懂,我们还是通过源码来分析具体的做法。


源码分析


我们看下具体的代码:




这里不多讲了,显然是要到 ContextImpl 中去查看具体的实现,至于原因,我在之前插件化系列的文章中已经提及了,读者自行查阅。


ContextImpl.java :




这里是通过 ActivityThread 获得 IPackageManager,看到以 I 开头就知道,它的类型肯定是 接口类型,那么我们就有可能通过动态代理拦截 getApplicationInfo方法,修改它的返回值,从而达到欺骗友盟的目的,然他误以为我们修改的UMENG_CHANNEL值就是从 AndroidManifest.xml 中读取的。


我们找到 ActivityThread 中去查看:




卧槽,太顺了,看到 sPackManager 就想到了:我们可以通过hook它,然后注入我们动态代理生成的对象,来达到欺骗友盟的目的。


碰到的问题


那么问题来了,我们如何获得相应的渠道号,然后欺骗友盟呢?这显然是不能在代码里面写死的,因为这样就得每打包一个渠道就要编译一次。


解决方案


1:每个APK其实是一个zip文件,而在zip文件的说明里面有这样一段,参考文献:


https://en.wikipedia.org/wiki/Zip_%28file_format%29




在apk的末尾有一个 注释字段“它并不算是apk文件的一部分”,通俗的话来说就是:如果我们修改这个字段的值,并不会影响整个apk的签名,也就是不必再打包也能够直接安装。从图上看20offset开始,有两个字节用于确定 comment 的长度,我们先计算出要写入 comment 的内容长度(我们的渠道号),然后写到apk文件后面不就行了吗。


为了易于理解,我截两个图: 




在这张图里面,是原始的apk, 我们可以看到末尾两个字节 是 0x00 0x00也就代表我们的注释是空的。


下面一张图是我在写入注释之后的apk:



 

可以看到 从 12:EC00h0x7 0x8 位置标志我们的注释字段有8个字节长,数一下后面的内容正好就是八个字节。


2:但是我们的应用如何读apk呢,毕竟它只是个安装包啊。其实很简单,我们每个安装过的应用最后都会在 /data/app/…. 这个路径下,获得它的方式很简单:




值得注意的是,我们只有读取权限哦,但这已经足够了。


实现


现在就剩下写入到apk注释字段的内容设计了。我是这么做的:


注释字段内容 = magic_number + 渠道号 + 注释字段长度


magic_number 用于确定是否是我们自己的渠道号注释方式,最后的文件的末尾存放我们整个注释的大小,这样可以方便计算偏移,使用随机读取的时候可以很容易的读取到 comment 的内容。


好了我们看下具体的实现 :




我认为注释已经足够清楚,现在我们开始实现如何在Android设备中欺骗友盟,替换成我们在注释中写入的渠道号。


替换渠道号方法回顾


我们之前分析:我们看到在 ActivityThread 中是通过一个静态域存放 IPackageManager 的,这很符合我们的hook规则,如果你还是不懂请参阅我之前的博客:


http://blog.csdn.net/u013022222/article/details/51111814




之后拦截 getApplicationInfo 方法,修改它的返回值内容,使得当客户端调用appInfo.meta.get(“UMENG_CHANNEL”) 的时候永远都是我们替换的渠道号。我们下面便开始一步步实现我们的需求。


获得ActivityThread


首先这个类是hide的,所以只能通过反射拿到它的clazz,我们看下源码分析: 




可以看到它是个静态对象,不过如果你是老乘客的话,应该在这里轻车熟路了,因为这个分析我做了不只是一遍。(不过它也只能是静态的啊,毕竟在Android里面一个进程只对应这一个ActivityThread) 


拿到它还是很容易的,不过这毕竟是个私有域,名字会变化的概率比较高,我们找下有没有可以返回它的共有方法,这样变动的可能性很小,很高兴这里是有的:




所以我们可以拿到 ActivityThread 了:




替换IPackageManager


剩下的事情就是拿到 sPackageManger,替换成我们的代理类,这个代理类拦截 getApplicationInfo方法,修改它的返回值,使得友盟都是拿到的我们修改的值:




代理类替换渠道号


现在就只剩下代理类的实现了,不懂的还是看我上面的文章链接,我在之前的几篇博文中已经都写出来了。




获取渠道号


上面的代码还有一处我是没有注释的,那就是获得 channel 的方法。要知道,在我们安装一个apk之后,系统都会在 /data/app/... 保留一份拷贝,所以理所当然的我们可以读到那个apk文件:




之后就是读取文件末尾两个字节的 comment 大小:


RandomAccessFile randomAccessFile = new RandomAccessFile(apk, "r");randomAccessFile.seek(randomAccessFile.length() - 2);
short offset = (short) randomAccessFile.read();


然后验证 magic number


randomAccessFile.seek(randomAccessFile.length() - offset);
int magic = randomAccessFile.readInt();

if (magic != 0x52560b0b) {
   return "known";}


验证通过的话,那就放心的读渠道就行了:


byte[] flavor = new byte[offset - 2 - 4];randomAccessFile.read(flavor);
return new String(flavor);


使用


因为 Hook了系统服务,所以还是 越早Hook越好,我们在重载 Application 的方法:


public class BaseApplication extends Application {
   @Override    protected void attachBaseContext(Context base) {
       super.attachBaseContext(base);
       try {
           YetWYCore.init(this);        } catch (Exception e) {}    }}


效果图: 








点击最后 阅读原文 查看源码。