1. 背景

最近做的需求需要频繁使用3G/4G网络,可惜公司给的测试卡流量只有几百M,播几个视频流量就耗光了,测试起来非常不方便。于是就想没有什么工具或者软件可以在wifi环境下模拟3G/4G网络,在网上找半天,结果无功而返。

既然没有软件能做到,那只能从代码层面下手了。代码中判断当前手机网络类型的代码比较简单:

1
2
3
4
5
6
7
8
9
10
11
public static boolean isMobileNetwork(Context context) {
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
if (cm == null) {
return false;
}
NetworkInfo info = cm.getActiveNetworkInfo();
if (info == null || !info.isAvailable()) {
return false;
}
return ConnectivityManager.TYPE_MOBILE == info.getType();
}

如果能让wifi环境下也返回是3G/4G网络就可以达到目的了,但是直接修改上述代码是不行的,因为判断当前是否是3G/4G网络的地方很多,一一修改比较耗时间,关键是项目中导入的很多SDK中也有网络类型的判断,SDK中的代码我们没法修改。

既然不能直接修改网络判断的代码,那能不能通过修改ConnectivityManager类(以下简称CM)的getActiveNetworkInfo方法的返回值,让wifi环境下getActiveNetworkInfo方法也返回3G/4G网络的NetworkInfo呢?

听起来有些似乎有点困难。让我们先看看源代码,一步一步分析。

2. 分析源码,寻找切入点

从源码看起来CM类使用了单例模式,其getActiveNetworkInfo方法实现如下:

1
2
3
4
5
6
7
public NetworkInfo getActiveNetworkInfo() {
try {
return mService.getActiveNetworkInfo();
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}

其中mService对象是IConnectivityManager接口(以下简称ICM)的一个实例,这个接口是一个aidl接口,不属于Android SDK的一部分,源码在framework中。如果能自己创建一个ICM对象替换调CM类中原本的mService属性,就可以实现修改getActiveNetworkInfo方法返回值的效果了。

3. 使用动态代理替换原有的mService对象

ICM对AndroidSDK不可见,我们不能直接创建一个类并实现了ICM接口。另外这个接口声明了50个左右的方法,即使能创建实现了ICM接口的实例,要重写这50个左右的方法也不那么容易。那有没有其他简便的方法创建ICM实例呢?有,使用动态代理。

关于什么是动态代理,动态代理可以做什么,可以参考这篇文章,这里不再介绍(其实是技术太菜,说不清楚)。下面是使用动态代理替换CM中原有的mService属性的过程:

  1. 使用反射获取原有的mService对象:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 先获取ConnectivityManager对象
    ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
    // 获取ConnectivityManager的mService对象
    Field[] fs = ConnectivityManager.class.getDeclaredFields();
    Object mService = null;
    if (fs != null) {
    for (Field f : fs) {
    if ("mService".equals(f.getName())) {
    f.setAccessible(true);
    mService = f.get(cm);
    }
    }
    }
  2. 使用动态代理创建ICM对象,并修改 getActiveNetworkInfo 的返回值为自己准备好的networkInfo,其他方法仍然调用mService的相应方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    final Object mFinalService = mService;
    // 使用动态代理创建 IConnectivityManager 的实例
    Object mProxyService = Proxy.newProxyInstance(ConnectivityManager.class.getClassLoader(),
    // 需要实现的接口
    new Class[]{Class.forName("android.net.IConnectivityManager")},
    // 方法调用处理器
    new InvocationHandler() {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    // 拦截 getActiveNetworkInfo, 返回自己准备好的networkInfo
    if ("getActiveNetworkInfo".equals(method.getName())) {
    if (SettingsPreferences.getHookNetwork(context)) {
    // 该networkInfo是自己手动创建的3g模式的NetworkInfo。
    return networkInfo;
    }
    }
    // 不拦截其他方法,
    return method.invoke(mFinalService, args);
    }
    });
  3. 将原来的mService对象替换为上一步中创建的mProxyService对象:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 将自己创建的代理对象`mProxyService`替换调原来的`mService`对象。
    if (fs != null) {
    for (Field f : fs) {
    if ("mService".equals(f.getName())) {
    f.setAccessible(true);
    f.set(cm, mProxyService);
    }
    }
    }

这样就完成了修改getActiveNetworkInfo方法的返回值的目标,同时还能控制什么情况下返回自己定义的NetworkInfo (上述代码中只有当SettingsPreferences.getHookNetwork(context)为true的情况下才返回自己创建的networkInfo,否则仍然返回系统原本的networkInfo)。

通过上述方式是不是能做到让代码中所有调用getActiveNetworkInfo方法的地方都返回自己创建的NetworkInfo呢?

实践中发现:CM类看起来使用了单例模式,但事实上,在Api level 19 及以上的系统使用不同的context调用getSystemService方法返回的是不同的CM对象。

也就是说在代码中调用 getSystemService方法时用的context和上述代码中context不一样,那么获取到的NetworkInfo对象仍然可能是系统本身的NetworkInfo而不是我们自己创建的NetworkInfo

4. 替换IConnectivityManager对象的创建过程

既然直接替换mService属性不完全可行,那能不能尝试找到mService被创建的地方,然后替换掉mService的创建过程,让所有给mService赋值的地方都返回我们自己创建的ICM对象呢?

这个过程就好比现在小区内有好几家超市都在卖矿泉水,你想让这些超市都卖你自己生产的矿泉水,现在通过动态代理的方式你可以做到让其中一家超市卖你生产的矿泉水了。接下来你想让所有超市都卖你生产的矿泉水,如果能找到这些超市在哪家供应商进的货,然后通过某种方式,把供应商的货都替换为你生产的矿泉水那就ok了。

来看看mService是何时以及怎么被创建的。

首先,CM是通过Context对象的getSystemService方法获取的。我们看看getSystemService方法是怎么实现的(Context的实现在ContextImpl里面):

1
2
3
4
public Object getSystemService(String name) {
ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
return fetcher != null ? fetcher.getService(ctx) : null;
}

从上面的代码可以看出来,所有的service对象都保存在一张map(SYSTEM_SERVICE_FETCHERS)中,该map的初始化过程如下:

1
2
3
4
5
6
7
8
registerService(Context.CONNECTIVITY_SERVICE, ConnectivityManager.class,
new StaticApplicationContextServiceFetcher<ConnectivityManager>() {
@Override
public ConnectivityManager createService(Context context) {
IBinder b = ServiceManager.getService(Context.CONNECTIVITY_SERVICE);
IConnectivityManager service = IConnectivityManager.Stub.asInterface(b);
return new ConnectivityManager(context, service);
}});

从上述代码看来,ICM的创建依赖于ServiceManager.getService方法返回的IBinder对象,使用这个IBinder对象再调用IConnectivityManager.Stub类的静态方法asInterface就可以将其转为本地接口ICM对象(这也使用Binder进行跨进程方法调用的基本流程):

1
2
3
4
5
6
7
8
9
10
public static android.net.IConnectivityManager asInterface(android.os.IBinder obj) {
if ((obj == null)) {
return null;
}
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (((iin != null) && (iin instanceof android.net.IConnectivityManager))) {
return ((android.net.IConnectivityManager) iin);
}
return new android.net.IConnectivityManager.Stub.Proxy(obj);
}

总结一下: CMgetActiveNetworkInfo方法直接调用了其成员变量mServicegetActiveNetworkInfo方法,而mService是一个ICM的实例,它是在CM对象创建时被赋值的(构造函数中)。CM对象的创建位于ContextImpl类中,其创建时会首先调用ServiceManager.getService方法获取到一个IBinder对象,并通过IConnectivityManager.Stub的静态方法asInterface将这个IBinder对象转为ICM对象,最终以这个ICM对象作为参数创建了CM对象。

找到CM以及ICM创建的地方后,目标就很明确了:只要自己创建一个IBinder对象替换掉ServiceManager.getService方法返回的IBinder对象,然后修改这个IBinderqueryLocalInterface方法让它始终返回同一个ICM对象就可以了。

要想替换掉ServiceManager.getService的返回值,我们先看看这个方法的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static IBinder getService(String name) {
try {
IBinder service = sCache.get(name);
if (service != null) {
return service;
} else {
return getIServiceManager().getService(name);
}
} catch (RemoteException e) {
Log.e(TAG, "error in getService", e);
}
return null;
}

上述代码中, sCacheServiceManager的一个静态成员变量(Map类型),我们可以自己创建一个IBinder对象(跟之前一样,使用动态代理创建),然后通过Mapput方法将sCache里面的内容替换,从而达到瞒天过海的目的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 首先获取ServiceManager class
Class<?> serviceManager = Class.forName("android.os.ServiceManager");
Method getService = serviceManager.getDeclaredMethod("getService", String.class);
// 调用ServiceManag的"getService"方法获取原始的 IBinder 对象
final IBinder rawBinder = (IBinder) getService.invoke(null, Context.CONNECTIVITY_SERVICE);
// 使用动态代理伪造一个新的 IBinder对象
IBinder hookedBinder = (IBinder) newProxyInstance(serviceManager.getClassLoader(),
new Class<?>[] {IBinder.class}, new InvocationHandler()
{
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
{
// ....
}
});
// 把伪造的 IBinder 对象放进ServiceManager的cache里面
Field cacheField = serviceManager.getDeclaredField("sCache");
cacheField.setAccessible(true);
@SuppressWarnings("unchecked")
Map<String, IBinder> cache = (Map<String, IBinder>) cacheField.get(null);
cache.put(Context.CONNECTIVITY_SERVICE, hookedBinder);

替换掉ServiceManager.getService的返回值以后,接着我们需要修改我们刚刚创建的IBinder对象的queryLocalInterface方法,让它始终返回同一个ICM对象:

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
new InvocationHandler() {
private Object mIConnectivityManager = null;
/**
* 获取原本的IConnectivityManager对象
*/
private Object getBaseManager(@NonNull IBinder binder, Class<?> stubCls) {
try {
Method asInterfaceMethod = stubCls.getDeclaredMethod("asInterface", IBinder.class);
return asInterfaceMethod.invoke(null, binder);
} catch (Exception e) {
LogUtils.error("wentaoli hook => createIConnectivityManager error: " + e, e);
}
return null;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (!"queryLocalInterface".equals(method.getName())) {
return method.invoke(baseBinder, args);
}
if (mIConnectivityManager != null) {
return mIConnectivityManager;
}

// 替换掉 queryLocalInterface 方法的返回值
Object base = getBaseManager(baseBinder, Class.forName("android.net.IConnectivityManager$Stub"));
mIConnectivityManager = Proxy.newProxyInstance(proxy.getClass().getClassLoader(),
// asInterface 的时候会检测是否是特定类型的接口然后进行强制转换, 因此这里的动态代理生成的类型信息的类型必须是正确的
new Class[]{IInterface.class, Class.forName("android.net.IConnectivityManager")},
new BinderHookHandler(context, base));
return mIConnectivityManager;
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private static class BinderHookHandler implements InvocationHandler {
Context context;
// 原始的IConnectivityManager对象 (也是IInterface)
Object base;
BinderHookHandler(Context context, Object base) {
this.context = context.getApplicationContext();
this.base = base;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("getActiveNetworkInfo".equals(method.getName())) {
if (SettingsPreferences.getHookNetwork(context)) {
return networkInfo;
}
}
return method.invoke(base, args);
}
}

大功告成。

5. 模拟发送网络变化广播

经过上面操作,已经可以做到在wifi环境下模拟移动网络了,但还有一些可以完善的地方,比如说网络的切换。

考虑如下情况:当在应用中通过手动点击某个按钮将上述SettingsPreferences.getHookNetwork(context)的值从false变为true,这时候getActiveNetworkInfo方法返回的NetworkInfo就从原来的wifi变成了自定义的3g/4g网络,这事实上就相当于发生了一次网络状态的变化。按照正常的流程,如果手机发生网络状态变化系统会发送相应的广播,同时app中动态或者静态注册的广播接收器会收到相应的广播。但是现在我们只是模拟网络变化,系统自然不会帮忙发送网络状态变化广播,那怎么去模拟这个过程呢?

网络状态变化广播是一个敏感的广播,需要系统级权限才能发送,普通应用是不允许发送的。既然不能直接发送这个广播,那能不能通过获取到当前应用中所注册的广播接收器,然后直接调用这些广播接收器的onReceive方法来模拟广播接收过程呢?让我们来分析下。

首先来看静态注册的广播接收器。

5.1. 静态注册的广播接收器

要获取当前app中的注册了哪些静态广播接收器是比较简单的,就跟获取app中声明了哪些Activity是一样的逻辑:

1
2
3
Intent i = new Intent(ConnectivityManager.CONNECTIVITY_ACTION);
i.setPackage(context.getPackageName());
List<ResolveInfo> list = context.getPackageManager().queryBroadcastReceivers(i, PackageManager.MATCH_ALL);

通过上述代码拿到能接收网络状态变化的静态广播接收器后(当然并不是BroadcastReceiver实例对象,仅仅是一些BroadcastReceiver描述信息),只需遍历这些接收器信息,然后通过Class.newInstance方法创建相应的BroadcastReceiver实例,再调用其onReceive方法就行了,代码如下:

1
2
3
4
5
6
7
8
9
for (ResolveInfo resolveInfo : list) {
try {
Class<?> clazz = Class.forName(resolveInfo.activityInfo.name);
BroadcastReceiver receiver = (BroadcastReceiver) clazz.newInstance();
receiver.onReceive(context, intent);
} catch (Exception e) {
LogUtils.error("wentaoli hook static receiver error " + e, e);
}
}

这样就完成了模拟静态注册的广播接收器收到广播的场景。

下面再看动态注册的广播接收器

5.2. 动态注册的广播接收器

要获得应用中所有动态注册的广播接收器并不容易。通过对广播注册与接收的源代码的分析得知(关于广播的注册接收过程可以参考这篇博客),广播的接收过程最终会走到LoadedApk的一个内部类 ReceiverDispatcherperformReceive方法中,而LoadedApk的一个成员变量mReceivers则保存了当前app中所有动态注册的广播接收器:

1
private final HashMap<Context, HashMap<BroadcastReceiver, LoadedApk.ReceiverDispatcher>> mReceivers = new HashMap<Context, HashMap<BroadcastReceiver, LoadedApk.ReceiverDispatcher>>();

只要能拿到LoadedApk实例的mReceviers属性值,则可以获取到所有动态注册的广播。要获取LoadedApkmReceivers属性值,首先得拿到LoadedApk的实例。事实上,ActivityThreadmPackages属性就持有LoadedApk的实例(看过四大组件启动流程源代码的同学应该对ActivityThread类很熟悉),接着获取LoadedApk以及LoadedApkmReceivers属性就都不是问题了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 先获取到当前应用的ActivityThread对象
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
currentActivityThreadMethod.setAccessible(true);
Object currentActivityThread = currentActivityThreadMethod.invoke(null);
// 获取mPackages属性
Field f = activityThreadClass.getDeclaredField("mPackages");
f.setAccessible(true);
Map map = (Map) f.get(currentActivityThread);
// 获取LoadedApk实例
Object loadedApkRef = map.get(context.getPackageName());
if (loadedApkRef instanceof WeakReference) {
Object loadedApk = ((WeakReference) loadedApkRef).get();
// 获取LoadedApk实例的mReceivers属性值
field = loadedApk.getClass().getDeclaredField("mReceivers");
field.setAccessible(true);
Map mReceiversMap = (Map) field.get(loadedApk);
// 通过mReceivers属性获取所有动态注册的广播
....
}

在获取到所有动态注册的广播接收器后,只要知道哪些广播接收器可以接收网络状态变化广播,然后再直接调用这些广播接收器的onReceive方法就好了。但事实上这并不容易,具体怎么判断哪些广播接收器可以接收网络状态变化广播,就不再说明了(其实是懒),有兴趣的可以到这里查看代码。

到此为止就完成了对发送网络状态变化广播的模拟,也就完成了Wifi下模拟3g/4g网络的整个流程的模拟。

6. 其他

  1. 关于动态代理:
    动态代理是AOP的基础。像 DroidPlugin 之类插件框架以动态代理作为基础的。

  2. 关于兼容性:
    文中的代码中使用了较多的反射调用以及系统隐藏的api,由于公司没有那么多手机,没有测试代码的兼容性。

参考文章:

http://weishu.me/2016/04/12/understand-plugin-framework-receiver/
http://weishu.me/2016/01/28/understand-plugin-framework-proxy-hook/
http://blog.csdn.net/u013263323/article/details/76014494
http://blog.csdn.net/Luoshengyang/article/details/6744448