个推SDK动态配置初始化,实现多环境切换

本文介绍了一种通过HookPackageManagerService在个推SDK初始化前动态配置环境参数的方法,解决了不同开发环境下频繁打包的问题,提高了开发效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、引入背景

目前的项目有好几个网络环境。以前在不同开发环境之间切换需要在gradle里面配置flavor,选定环境以后再打包。切换环境需要重新打包Apk,效率低下。于是想实现大一统,可以打包完apk以后动态切换环境(主要是服务器地址和一些SDK的配置数据,比如个推)。基本构想是采用先选网络环境再登录的形式。把配置项写到本地,登录之前先选择环境,然后再登录。发现服务器地址的配置比较容易,但是由于某些三方SDK的初始化都是放在Application下,并且不受人为干预,所以配置比如个推的APPID(项目中不同环境配置的个推的参数不同,所以个推也需要切换环境)那些meta-data就不容易实现。经过思考和尝试,于是有了这一套通过Hook PMS来在个推SDK初始化之前动态配置meta-data的方案。

二、思路
1. 个推SDK初始化流程

读取Manifest下的meta-data标签获取PUSH_APPID、PUSH_APPKEY、PUSH_APPSECRET然后初始化。读取过程如下:

ApplicationInfo info = getPackageManager().getApplicationInfo(getPackageName(), PackageManager.GET_META_DATA);
String push_appid = (String) info.metaData.get("PUSH_APPID");
2.干预初始化流程

SDK初始化由自己管理,想要干涉的话也要阅读SDK源码,利用反射去搞。但是源码混淆,阅读困难。还有一个切入点就是在SDK调用上面的系统Api获取数据之前替换为自己想要的值。也就是在SDK 调用info.metaData.get(“PUSH_APPID”);之前调用info.metaData.putString(“PUSH_APPID”,“xxxxxxxxxxx”)填充自己想要的值。

3.Hook的前提
  1. 为什么要Hook PackageManagerService?上面调用了getPackageManager,为什么不是hook PackageManager。因为系统服务都是基于C/S的,真正的实现类都是在服务端。还有就是系统的所有服务都是非常好的Hook切入点。一是有静态变量来保持对服务的引用,二是基于Binder机制通信导致有接口。

  2. 为什么要选静态?因为静态变量(方法)是类变量(方法),不依赖于任何对象。所以利用反射获取类变量和调用类方法都很方便,不依赖具体对象。只需要传入null即可:

    Feild.get(null);
    Method.invoke(null);
    
  3. 为什么要接口?碍于动态代理的实现原理,代理对象和被代理对象必须实现同一个接口。然后CGlib在安卓上无法使用。所以无接口,不代理。Aidl就是基于Binder机制,一说Aidl就明白了。作为服务端和客户端都会用到同一套接口,服务端负责实现,客户端负责调用。

    Object proxy = Proxy.newProxyInstance(classLoader, interfaces,  invocationHandlerImpl);
    // classLoader 用来装载类。一般传入被代理对象的类加载器。
    // interfaces 代理对象和被代理对象共同实现的接口。
    // invocationHandlerImpl InvocationHandler的实现类,也就是在这里完成对info.metaData.putString("PUSH_APPID","xxxxxxxxxxx")的调用。
    
4.寻找Hook点

Hook有一条原则,寻找framework层那些不易变动的代码,或者变动少的代码。这样可以减轻因为framework机制修改带来的适配工作。既然要劫持info.metaData,所以还必须要跟踪源码来定位Hook点。这里其实也基本定了,就是Hook PMS。其实这里也没那么复杂,总的来说就是用自己的PackageManager代理对象来替换Binder返回的packeManager。因为只要替换了packageManager就能干预getApplicationInfo的执行,也就能在applicationInfo返回之前调用info.metaData.putString(“PUSH_APPID”,“xxxxxxxxxxx”)。

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals("getApplicationInfo") && (Integer) args[1] == PackageManager.GET_META_DATA) {   
    ApplicationInfo info = (ApplicationInfo) method.invoke(packageManager, args);           info.metaData.putString("PUSH_APPID","xxxxxxxxxxx");            
	return info;
	} 
    return method.invoke(this.packageManager, args);
}

有了以上思路以后,就可以开始寻找Hook点了。

4.1 ContextImpl
context.getPackageManager().getApplicationInfo(getPackageName(), PackageManager.GET_META_DATA);

由于源头是context,所以先要跟踪context的实现类ContextImpl。

@Override
public PackageManager getPackageManager() {
    //第一次进来 mPackageManager == null
    if (mPackageManager != null) {
        return mPackageManager;
    }
	
    IPackageManager pm = ActivityThread.getPackageManager();//1
    if (pm != null) {
        // Doesn't matter if we make more than one instance.
        return (mPackageManager = new ApplicationPackageManager(this, pm));//2
    }

    return null;
}

从注释1处看到,会先去调用ActivityThread.getPackageManager()来获取IPackageManager。也就是在这里明确了PackageManagerService肯定实现了IpackageManager借口,这个接口后面动态代理要用。接着进去ActivityThread类看看。

4.2 ActivityThread
public static IPackageManager getPackageManager() {
        if (sPackageManager != null) {
            //Slog.v("PackageManager", "returning cur default = " + sPackageManager);
            return sPackageManager;
        }
        IBinder b = ServiceManager.getService("package");
        //Slog.v("PackageManager", "default service binder = " + b);
        sPackageManager = IPackageManager.Stub.asInterface(b);
        //Slog.v("PackageManager", "default service = " + sPackageManager);
        return sPackageManager;
}

可以看到ActivityThread会持有一个PackageManager对象的静态引用。如果此时sPackageManager还未初始化,则会从ServiceManager里面去取。这里也就是Aidl跨进程通信。总而言之主要把这个地方的sPackageManager给替换掉,基本就达到了目的。但是看4.1中的注释2处,还用ApplicationManager对getPackageManager的返回值进行了一次包装,所以在那里去替换也是可以的。但是为了稳定性和应对多变性,应该在出现的地方尽可能都去替换。此处应该是要都替换的,因为上面说了// Doesn’t matter if we make more than one instance.这是一个新的实例对象。

public static ActivityThread currentActivityThread() {
    return sCurrentActivityThread;
}

可以看到ActivityThread有如下静态方法获取自己的实例对象。所以可以从这里入手拿到ActivityThred。拿到sCurrentActivityThread,然后再拿到sCurrentActivityThread的sPackageManager字段,最后利用动态代理替换掉sPackageManager。

4.3 AppicationPackageInfo

除了ActivityThread,还在替换掉ApplicationPackageInfo。这里相对就比较简单了,因为PackageManager是暴露出来的一个方法,所以可以直接拿到PackageManager。然后再替换掉里面的mPM对象即可。

@Override
public ApplicationInfo getApplicationInfo(String packageName, int flags)
        throws NameNotFoundException {
    return getApplicationInfoAsUser(packageName, flags, mContext.getUserId());
}

@Override
public ApplicationInfo getApplicationInfoAsUser(String packageName, int flags, int userId)
        throws NameNotFoundException {
    try {
        ApplicationInfo ai = mPM.getApplicationInfo(packageName, flags, userId);
        if (ai != null) {
            // This is a temporary hack. Callers must use
            // createPackageContext(packageName).getApplicationInfo() to
            // get the right paths.
            return maybeAdjustApplicationInfo(ai);
        }
    } catch (RemoteException e) {
        throw e.rethrowFromSystemServer();
    }

    throw new NameNotFoundException(packageName);
}
三、完整Hook代码
//获取ActivityThread的字节码对象
Class activityThreadClass = Class.forName("android.app.ActivityThread");
//获取ActivityThread的静态方法currentActivityThread
Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
//调用currentActivityThread获取到ActivityThread的静态类对象
Object currentActivityThread = currentActivityThreadMethod.invoke(null);
//获取currentActivityThread对象的私有静态成员sPackageManager,sPackageManager也就是被代理对象
Field sPackageManagerField = activityThreadClass.getDeclaredField("sPackageManager");
sPackageManagerField.setAccessible(true);
Object sPackageManager = sPackageManagerField.get(currentActivityThread);
//动态代理,用代理对象proxy替换sPackageManager
Class iPackageManagerInterface = Class.forName("android.content.pm.IPackageManager");
Object proxy = Proxy.newProxyInstance(iPackageManagerInterface.getClassLoader(), new Class[]{iPackageManagerInterface}, new PMInvocationHandler(sPackageManager, this.metaData, this.enabled));
sPackageManagerField.set(currentActivityThread, proxy);
//不用context.getApplicationContext。在Application里面实例化,传进来就是applicationContext。
//这里获取到的是ApplicationPackageManager,是对PackageManager的一个包装。
PackageManager pm = context.getPackageManager();
//用proxy替换ApplicationPackageManager的成员变量mPM,mPM是被代理对象
Field pmField = pm.getClass().getDeclaredField("mPM");
pmField.setAccessible(true);
pmField.set(pm, proxy);
四、总结

Hook系统Api是一项长期而艰苦的任务。谷歌为了隐藏一些接口引入了hide注解,但是没有防住反射。后面又引入了UnsupportedAppUsage来对抗反射。不同的国产系统,定制程度又不一样,所以很难保证100%的兼容性和稳定性。已知在华为P10上必须早Application的attachBaseContext方法里面初始化才能生效,在onCreate里面不行,小米8和安卓Q虚拟机在onCreate里面初始化也没问题。ps,方案应用至今有一两个月了,目前稳健运行。如果有更好的方案,求分享。

五、注意点
  1. 退出应用的时候,由于只会杀死app进程,个推进程不会死,所以会导致更改了环境以后个推SDK初始化不生效,还是上次的环境。所以需要杀死个推进程,让它重启。

    //杀死个推进程,进程在初始化个推的类中获取
    android.os.Process.killProcess(GTPid);
    //重启app进程
    Intent intent = new Intent(activity, MainActivity.class);
    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    activity.startActivity(intent);
    android.os.Process.killProcess(android.os.Process.myPid());
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值