最近在调研各种 hotfix 以及动态加载的技术,在这些技术层出不穷的时候,身为测试,为了拿到我们想要的信息,需要掌握些什么?
相信经常看文档(尤其是官方文档)的同学一定会有一个体会,文档总是由某一个 topic 出发,然后在介绍这个 topic 的时候包含了各种各样的引用、链接,如果有某一个工具可以一次性打开所有这些深层嵌套的链接,那它们的样子一定会像是一个图(因为还有循环的嵌套),每个节点都有多条有向边。那如果想要搞清楚这个主题说的是什么,我们是要对这个图进行怎样的遍历呢?深度优先还是广度优先?我建议先广度,再深度。如果现在这个 topic 就是 hotfix ,那从广度上我觉得它应该至少包含以下系列:
今天就来说下 hook
参考资料:
http://weishu.me/2016/01/28/understand-plugin-framework-overview/
https://www.ibm.com/developerworks/cn/java/j-lo-classloader/
http://blog.csdn.net/shenyunsese/article/details/11737179
关于 hook 的关键字,非下面几个莫属:
不细说了,网上资料一大把,概括如下:
AOP,就跟 OOP(Object Oriented Programming,面向对象)一样,它俩都是一种 编程思想,但是面向对象目的模块化,封装,即一个对象完成设计出来,是为了完成它分内的事,但如果各个 module 都只做自己的事,突然有一个类似性能监控的需求下来,它要作用于所有的模块,怎么办?
就像上图,logging 应该是所有模块都要兼备的功能。可能有人会说,可以把 logging 设计为一个 interface,现有的所有 module 实现该 interface 就好了。那如果之前系统已经有一千个 module 怎么办?难道要手动去改一千个类?又或者之后系统需要扩展,再加两千个类,每次新写一个类都得实现这个 interface?
这个 logging,又或者具有 logging 属性的其他类似 module,它就是一个 aspect,需要贯穿所有目标 module,在不考虑现在量级和以后扩展量级的基础上怎么实现?这大概就是 AOP 想要描述的事情,也是它和 OOP 的区别所在,AOP 分下面两种
Proxy(代理),这个不用说了,大家都很熟悉了,代理就相当于站在原始 client 和原始 server 之间的一个中间人,原始的输入和输出全部都经过它,它可以帮助原 client 处理事情,那么它有以下这些特点:
这里的静态和动态指的是代理 instance 的生成时机,和『透明代理,反向代理,动态代理』这些不是一码事
例子来源于http://weishu.me/2016/01/28/understand-plugin-framework-proxy-hook/, 略有改动
首先得有一个接口:
public interface Shopping {
Object[] doShopping(long money);
}
原始的实现如下,自己拿钱去购物:
public class ShoppingImpl implements Shopping {
@Override
public Object[] doShopping(long money) {
System.out.println("逛淘宝 ,逛商场,买买买!!");
System.out.println(String.format("花了%s块钱", money));
return new Object[] { "鞋子", "衣服", "零食" };
}
}
代理的实现如下:
public class ProxyShopping implements Shopping {
Shopping base;
ProxyShopping(Shopping base) {
this.base = base;
}
@Override
public Object[] doShopping(long money) {
if (money < 100) {
return null
} else {
// 先黑点钱(修改输入参数)
long readCost = (long) (money * 0.5);
System.out.println(String.format("花了%s块钱", readCost));
// 帮忙买东西
Object[] things = base.doShopping(readCost);
// 偷梁换柱(修改返回值)
if (things != null && things.length > 1) {
things[0] = "被掉包的东西!!";
}
return things;
}
}
这个栗子很棒,总结一下:
等等,上面的第 2 点和第 3 点,怎么感觉和装饰器(decorator)这么像,实际上,本来 Proxy 和 Decorator 就是比较类似的两种设计模式,但它俩的区别如下,比较微妙:
下篇讲
不涉及到动态的创建代理对象的劫持行为,我称它为静态 hook,下面给大家再举两个栗子:
相信很多 app 都有这么一种功能,打开 app 第 n 次,弹出一个框让用户给个好评;又或者,进行了某个操作 n 次,触发某个 dialog。像这种弹框在 UI 自动化测试中是比较难处理的,因为无法确定它出现的时机,那我们是否可以打出测试包时 hook 掉它,让它永远也不可能为 n
一般这些值都存储在 app 的 SharedPreference 中,来看下怎么在程序中获取到关联的 SharedPreference:
PreferenceManager.getDefaultSharedPreferences(context);
SharedPreference 就是一个键值对,里面提供了很多方法,可以根据某个键 get 或 set 其对应的 value,比如:
getDefaultSharedPreferences(context).getString(key, defaultValue)
getDefaultSharedPreferences(context).edit().putString(key, value).apply()
为了加深 Reflection 的理解,我们可以定义如下的方法:
public final class HookHelper {
public static void hookprefs(Context ctx)
{
try {
final String PREF_LAUNCH_TIMES = "pref_launch_times";
Class<?> prefsmanager = Class.forName("android.preference.PreferenceManager");
Method getprefs = prefsmanager.getDeclaredMethod("getDefaultSharedPreferences", Context.class);
SharedPreferences prefs = (SharedPreferences) getprefs.invoke(null, ctx);
int rawLaunchTimes = prefs.getInt(PREF_LAUNCH_TIMES, 0);
Log.d("hooked launchTimes", "old launch time is " + rawLaunchTimes);
// 修改启动次数永远为3
prefs.edit().putInt(PREF_LAUNCH_TIMES, 3).apply(); // 不要忘了 apply
}catch (Exception e) {
throw new RuntimeException("Hook Failed", e);
}
}
ok,然后在 application 的 onCreate() 方法里面,加上一句:
HookHelper.hookprefs(this)
这样启动次数永远都是 3,可以在 adb logcat 中验证
一个例子说服力不强,那下面我们来 hook 一个 activity 的启动,我们的 MainActivity 代码如下,很简单,就一个按钮,点击之后 startActivity,跳转到 XXX 主页
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
try {
// 在这里进行Hook
HookHelper.attachContext(this);
} catch (Exception e) {
e.printStackTrace();
}
Button tv = new Button(this);
tv.setText("测试界面");
setContentView(tv);
tv.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("http://www.XXX.com"));
startActivity(intent);
}
});
}
}
为了达到 hook 的效果,我们必须看下 startActivity 的源码,从源头上下手,发现调用链如下:
@Override
public void startActivity(Intent intent) {
this.startActivity(intent, null);
}
然后
@Override
public void startActivity(Intent intent, @Nullable Bundle options) {
if (options != null) {
startActivityForResult(intent, -1, options);
} else {
// Note we want to go through this call for compatibility with
// applications that may have overridden the method.
startActivityForResult(intent, -1);
}
}
然后,走到startActivityForResult
,真正执行的是:
Instrumentation.ActivityResult ar =
mInstrumentation.execStartActivity(
this, mMainThread.getApplicationThread(), mToken, this,
intent, requestCode, options);
即调用了 mInstrumentation
的 execStartActivity
方法,这个mInstrumentation
是 Activity 类里一个 Instrumentation 型的私有变量,如果我们能 hook 掉这个变量,将它替换成我们自定义的某个实例,而这个实例改写了 execStartActivity 方法,是不是就达到目的呢?
我们在 onCreate 里设置了一段 hook,来看看它的实现:
public class HookHelper {
public static void attachContext(Activity obj) throws Exception{
// 先获取到当前的ActivityThread对象
Class<?> currentActivity = Class.forName("android.app.Activity");
Field instrumentationField = currentActivity.getDeclaredField("mInstrumentation");
instrumentationField.setAccessible(true);
Instrumentation mInstrumentation = (Instrumentation) instrumentationField.get(obj);
Instrumentation evilInstrumentation = new EvilInstrumentation(mInstrumentation);
instrumentationField.set(obj, evilInstrumentation);
}
}
ok,确实是给mInstrumentation
赋了值,值为我们自定义的 EvilInstrumentation,这个类的构造函数是接受了一个旧的 Instrumentation 变量(请结合 Proxy 的总结),
来看下实现:
public class EvilInstrumentation extends Instrumentation {
private static final String TAG = "EvilInstrumentation";
// ActivityThread中原始的对象, 保存起来
Instrumentation mBase;
public EvilInstrumentation(Instrumentation base) {
mBase = base;
}
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
// Hook之前, 输出原始信息!
long startTime = 0L;
Log.e(TAG, "\nhi baby,you are hooked!, 原始参数如下: \n" + "who = [" + who + "], " +
"\ncontextThread = [" + contextThread + "], \ntoken = [" + token + "], " +
"\ntarget = [" + target + "], \nintent = [" + intent +
"], \nrequestCode = [" + requestCode + "], \noptions = [" + options + "]");
// 开始调用原始的方法, 也可决定是否调用
// 由于这个方法是隐藏的,因此需要使用反射调用;首先找到这个方法
try {
intent.setData(Uri.parse("http://www.douban.com"));
startTime = System.currentTimeMillis();
Log.d(TAG,"before exec : " + startTime);
Method execStartActivity = Instrumentation.class.getDeclaredMethod(
"execStartActivity",
Context.class, IBinder.class, IBinder.class, Activity.class,
Intent.class, int.class, Bundle.class);
execStartActivity.setAccessible(true);
return (ActivityResult) execStartActivity.invoke(mBase, who,
contextThread, token, target, intent, requestCode, options);
} catch (Exception e) {
// 某该死的rom修改了 需要手动适配
throw new RuntimeException("do not support!!! pls adapt it");
} finally
{
long afterTime = System.currentTimeMillis();
long totalTime = afterTime - startTime;
Log.d(TAG,"after exec :" + afterTime);
Log.d(TAG,"total exec :" + totalTime);
}
}
}
使用这种这个 EvilInstrumentation 就是一个代理,它来做一些 hook 的事:
build 看下效果,果然成功啦!日志也打出来了!
如果我们把这一段放在项目中所有 Activity 的基类中,比如 BaseActivity,加入一些有 logging 属性的监控代码,那么是不是 AOP 的一种体现呢?
目前这样写虽然目的性达到了,但是对于代码的侵入性还是很强的,从 Activity 的成员变量下手,是能 hold 住所有的 activity,但其他的组件我们有办法兼顾吗?
下篇讲
知道为什么 hook 后跳转的是豆瓣主页吗,因为我们在招 iOS 的测试开发同学,详情见:https://jobs.douban.com/#position-cskfgcs,欢迎私