一、引入背景
目前的项目有好几个网络环境。以前在不同开发环境之间切换需要在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的前提
-
为什么要Hook PackageManagerService?上面调用了getPackageManager,为什么不是hook PackageManager。因为系统服务都是基于C/S的,真正的实现类都是在服务端。还有就是系统的所有服务都是非常好的Hook切入点。一是有静态变量来保持对服务的引用,二是基于Binder机制通信导致有接口。
-
为什么要选静态?因为静态变量(方法)是类变量(方法),不依赖于任何对象。所以利用反射获取类变量和调用类方法都很方便,不依赖具体对象。只需要传入null即可:
Feild.get(null); Method.invoke(null);
-
为什么要接口?碍于动态代理的实现原理,代理对象和被代理对象必须实现同一个接口。然后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,方案应用至今有一两个月了,目前稳健运行。如果有更好的方案,求分享。
五、注意点
-
退出应用的时候,由于只会杀死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());