结合友盟统计的多渠道快速打包
2017-02-04 10:12
651 查看
实现
现在就剩下写入到apk注释字段的内容设计了。我是这么做的: 注释字段内容 = magic_number + 渠道号 + 注释字段长度
magic_number用于确定是否是我们自己的渠道号注释方式,最后的文件的末尾存放我们整个注释的大小,这样可以方便计算偏移,使用随机读取的时候可以很容易的读取到comment的内容。
好了我们看下具体的实现:
import java.io.*; import java.util.ArrayList; import java.util.List; public class Main { public static void main(String[] args) throws IOException { //原始apk的存放位置 这里有个坑 就是不能用已经渠道化的apk 也就是加入了某个渠道的apk File apk = new File("/Users/chan/Documents/开源代码/ChanWeather/app/app-release.apk"); FileInputStream is = new FileInputStream(apk); ByteArrayOutputStream os = new ByteArrayOutputStream(); int length = -1; //我们把文件的内容读出来 byte[] cache = new byte[256]; while ((length = is.read(cache)) != -1) { os.write(cache, 0, length); } byte[] copy = os.toByteArray(); //你要加入的渠道 List<String> flavors = new ArrayList<>(); flavors.add("QQ"); flavors.add("360Store"); flavors.add("WanDouJia"); flavors.add("ywy"); //写在comment的头部 //内容其实很随意 取你喜欢的名字就行 我这里用的是我gf的谐音 byte[] magic = {0x52, 0x56, 0x0b, 0x0b}; for (String flavor : flavors) { //渠道的长度 byte[] content = flavor.getBytes(); //渠道加上魔数的长度等于注释的长度 short commentLength = (short) (content.length + magic.length); //末尾在存放整个的大小 方便之后文件指针的读取 所以真正的渠道号要再多两个字节 commentLength += 2; //要用小端模式存放 for (int i = 0; i < 2; ++i) { copy[copy.length - 2 + i] = (byte) (commentLength % 0xff); commentLength >>= 8; } //目的位置 apk = new File("/Users/chan/Documents/开源代码/ChanWeather/app/app-{what}-release.apk".replace ("{what}", flavor)); FileOutputStream fileOutputStream = new FileOutputStream(apk); //先是存放的原始内容 fileOutputStream.write(copy); //存放的是魔数 fileOutputStream.write(magic); //写入内容 fileOutputStream.write(content); //再把长度信息添加到末尾 for (int i = 0; i < 2; ++i) { fileOutputStream.write(copy[copy.length - 2 + i]); } fileOutputStream.flush(); fileOutputStream.close(); } } /** * 测试用 * * @param file * @throws IOException */ private static void read(String file) throws IOException { File apk = new File(file); RandomAccessFile randomAccessFile = new RandomAccessFile(apk, "r"); randomAccessFile.seek(randomAccessFile.length() - 2); short offset = (short) randomAccessFile.read(); randomAccessFile.seek(randomAccessFile.length() - offset); int magic = randomAccessFile.readInt(); if (magic != 0x52560b0b) { System.out.println("魔数不对"); } byte[] flavor = new byte[offset - 2 - 4]; randomAccessFile.read(flavor); String content = new String(flavor); System.out.println(content); } }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
我认为注释已经足够清楚,现在我们开始实现如何在android设备中欺骗友盟,替换成我们在注释中写入的渠道号
替换渠道号方法回顾
我们之前分析:我们看到在ActivityThread中是通过一个静态域存放IPackageManager的,这很符合我们的hook规则,如果你还是不懂请参阅以往的博客 之后拦截 getApplicationInfo 方法,修改它的返回值内容,使得当客户端调用appInfo.meta.get(“UMENG_CHANNEL”)的时候永远都是我们替换的渠道号。我们下面便开始一步步实现我们的需求。
获得ActivityThread
首先这个类是hide的,所以只能通过反射拿到它的clazz,我们看下源码分析: 可以看到它是个静态对象,不过如果你是老乘客的话,应该在这里轻车熟路了,因为这个分析我做了不只是一遍。(不过它也只能是静态的啊,毕竟在android里面一个进程只对应这一个ActivityThread)
拿到它还是很容易的,不过这毕竟是个私有域,名字会变化的概率比较高,我们找下有没有可以返回它的共有方法,这样变动的可能性很小,很高兴这里是有的:
所以我们可以拿到ActivityThread了
//获取ActivityThread实例 Class<?> activityThreadClazz = Class.forName("android.app.ActivityThread", false, context.getClassLoader()); Method currentActivityThreadMethod = activityThreadClazz.getDeclaredMethod("currentActivityThread"); Object activityThreadObject = currentActivityThreadMethod.invoke(null);1
2
3
4
替换IPackageManager
剩下的事情就是拿到sPackageManger,替换成我们的代理类,这个代理类拦截getApplicationInfo方法,修改它的返回值,使得友盟都是拿到的我们修改的值//获得原始的IPackageManager Method getPackageManagerMethod = activityThreadClazz.getDeclaredMethod("getPackageManager"); Object packageManager = getPackageManagerMethod.invoke(activityThreadObject); //生成我们的代理类 Class<?> iPackageManagerClazz = Class.forName("android.content.pm.IPackageManager", false, context.getClassLoader()); Object proxy = Proxy.newProxyInstance(context.getClassLoader(), new Class[] {iPackageManagerClazz}, new PackageManagerProxy(context, packageManager)); //把原先的IPackageManager替换掉 Field packageManagerField = activityThreadClazz.getDeclaredField("sPackageManager"); packageManagerField.setAccessible(true); packageManagerField.set(activityThreadObject, proxy);1
2
3
4
5
6
7
8
9
10
11
12
13
实现代理类替换渠道号
现在就只剩下代理类的实现了,不懂的还是看我上面的文章链接,我在之前的几篇博文中已经都写出来了。import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.os.Bundle; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; /** * Created by chan on 16/7/25. */ public class PackageManagerProxy implements InvocationHandler { private Object mPackageManager; private Context mContext; public PackageManagerProxy(Context context, Object packageManager) { mContext = context; mPackageManager = packageManager; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //拦截getApplicationInfo方法 if ("getApplicationInfo".equals(method.getName())) { return invokeGetApplicationInfo(method, args); } //其它的方法就让它自己过去吧 return method.invoke(mPackageManager, args); } private Object invokeGetApplicationInfo(Method method, Object[] args) throws InvocationTargetException, IllegalAccessException, PackageManager.NameNotFoundException, IOException { //获得他的第二个参数值 //不懂的看函数签名吧 /** * Retrieve all of the information we know about a particular * package/application. * * <p>Throws {@link NameNotFoundException} if an application with the given * package name cannot be found on the system. * * @param packageName The full name (i.e. com.google.apps.contacts) of an * application. * @param flags Additional option flags. Use any combination of * {@link #GET_META_DATA}, {@link #GET_SHARED_LIBRARY_FILES}, * {@link #GET_UNINSTALLED_PACKAGES} to modify the data returned. * * @return {@link ApplicationInfo} Returns ApplicationInfo object containing * information about the package. * If flag GET_UNINSTALLED_PACKAGES is set and if the package is not * found in the list of installed applications, * the application information is retrieved from the * list of uninstalled applications(which includes * installed applications as well as applications * with data directory ie applications which had been * deleted with {@code DONT_DELETE_DATA} flag set). * * @see #GET_META_DATA * @see #GET_SHARED_LIBRARY_FILES * @see #GET_UNINSTALLED_PACKAGES */ //ApplicationInfo getApplicationInfo(String packageName, int flags) int mask = (int) args[1]; Object result = method.invoke(mPackageManager, args); if (mask == PackageManager.GET_META_DATA) { ApplicationInfo applicationInfo = (ApplicationInfo) result; if (applicationInfo.metaData == null) { applicationInfo.metaData = new Bundle(); } //把UMENG_CHANNEL这个key都是替换成我们自己的 applicationInfo.metaData.putString("UMENG_CHANNEL", getChannel()); } return result; } private String getChannel() { try { ApplicationInfo appInfo = mContext.getPackageManager() .getApplicationInfo(mContext.getPackageName(), 0); File apk = new File(appInfo.sourceDir); RandomAccessFile randomAccessFile = new RandomAccessFile(apk, "r"); randomAccessFile.seek(randomAccessFile.length() - 2); short offset = (short) randomAccessFile.read(); 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); } catch (Exception e) { return "unknown"; } } }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
获取渠道号
上面的代码还有一处我是没有注释的,那就是获得channel的方法。要知道,在我们安装一个apk之后,系统都会在/data/app/。。。保留一份拷贝,所以理所当然的我们可以读到那个apk文件:ApplicationInfo appInfo = mContext.getPackageManager() .getApplicationInfo(mContext.getPackageName(), 0); File apk = new File(appInfo.sourceDir);1
2
3
4
之后就是读取文件末尾两个字节的comment大小
RandomAccessFile randomAccessFile = new RandomAccessFile(apk, "r"); randomAccessFile.seek(randomAccessFile.length() - 2); short offset = (short) randomAccessFile.read();1
2
3
然后验证magic number:
randomAccessFile.seek(randomAccessFile.length() - offset); int magic = randomAccessFile.readInt(); if (magic != 0x52560b0b) { return "known"; }1
2
3
4
5
6
7
验证通过的话,那就放心的读渠道就行了
byte[] flavor = new byte[offset - 2 - 4]; randomAccessFile.read(flavor); return new String(flavor);1
2
3
使用
因为Hook了系统服务,所以还是越早Hook越好,我们在重载Application的方法:public class BaseApplication extends Application { @Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); try { YetWYCore.init(this); } catch (Exception e) {} } }1
2
3
4
5
6
7
8
9
10
效果图:
转载自:http://blog.csdn.net/u013022222
相关文章推荐
- 结合友盟统计的多渠道快速打包
- 结合友盟统计的多渠道快速打包
- 结合友盟统计的多渠道快速打包
- 美团多渠道打包工具walle及结合python实现界面化快速打包
- 基于Walle的多渠道快速打包自动脚本
- gradle多渠道打包及友盟统计-eclipse版本
- 腾讯 VasDolly 接入(快速多渠道打包)
- 多渠道打包-友盟统计
- gradle多渠道打包及友盟统计-eclipse版本
- Android packer-ng-plugin 多渠道快速打包
- 多渠道打包(友盟统计)
- Xcode结合iTunes快速打包制作ipa
- Android Studio Gradle实践之多渠道自动化打包(Android快速多渠道打包)
- 5分钟搞定Android多渠道打包(基于友盟统计)
- 基于Walle的多渠道快速打包自动脚本
- 多渠道打包---友盟统计(下载量、Bug....)
- 友盟统计多渠道打包页面访问路径
- gradle多渠道打包及友盟统计-eclipse版本
- gradle多渠道打包及友盟统计-eclipse版本
- android多渠道打包——集成友盟统计