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

来源:互联网 发布:淘宝布局管理怎么做好 编辑:程序博客网 时间:2024/06/05 04:42

实现

现在就剩下写入到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

0 0
原创粉丝点击