前言
前面讲了一些monkey作者的设计思想(有的我没写完,还没发布,惨),这篇来点实际的,monkey程序是如何发起一个触摸事件的呢?
本例子中假设使用的命令是:
adb shell monkey -p com.android.camera1000
表示向相机app发出1000个事件,所有事件都是随机的,其中会包括touch事件,那么这些touch事件是怎么构造的呢?
执行流程
一、入口函数
public static void main(String[] args) {
//…………省略……
int resultCode = (new Monkey()).run(args);
//……………省略……
}
main函数是java程序的入口函数,monkey作为一个标准的java程序,当然它也最先调用main函数,在main函数的内部,创建了Monkey对象,并调用了run()方法,参数args表示命令行参数数组,同时也传了进去
二、Monkey的run()方法
private int run(String[] args) {
//………………省略…………
mEventSource = new MonkeySourceRandom(mRandom, mMainApps,
mThrottle, mRandomizeThrottle, mPermissionTargetSystem);
//………………省略…………
crashedAtCycle = runMonkeyCycles();
//…………………省略………
}
Monkey中的run()方法挺长的,有200多行,该方法完整的分析,可以看我之前的文章,这里我们最需要知道的是两个
1、创建事件来源对象
这里创建的是:MonkeySourceRandom对象
2、调用runMonkeyCycles()方法
调用runMonkeyCycles()方法后,主线程会在该方法内循环执行,当中有我们的touch事件
三、Monkey的runMonkeyCycles()方法
private int runMonkeyCycles() {
//…………省略……
while (!systemCrashed && cycleCounter < mCount) {
//………………省略……
if (shouldAbort) {
//………………省略……
return eventCounter;
}
//…………………省略……
MonkeyEvent ev = mEventSource.getNextEvent();
if (ev != null) {
int injectCode = ev.injectEvent(mWm, mAm, mVerbose);
//……………………省略……
}
//……………………省略……
}
runMonkeyCycles()方法也挺长的,截取了今天我们关注的业务逻辑
1、循环获取事件
通过while循环,一直获取事件
2、循环结束条件
第一是:systemCrashed 表示系统发生故障,monkey不再获取新的事件
第二是:cycleCount 超过预设的次数,就是我们在命令行参数传入的事件总数
第三是:shouldAbort 标志位,跟我们命令行传入的命令有关,比如anr、native crash、默认不开启的,这个标志位也会终止事件循环
3、反复获取事件
MonkeyEvent ev = mEventSource.getNextEvent();
通过之前的文章可知,MonkeyEvent是所有事件类的父类,而这里的mEventSource指向的是MonkeySourceRandom对象,调用它的getNextEvent()方法,这个方法会返回一个Event对象,我们要看到的touch事件,就会在这个方法中返回,我们接下来就去这个方法中看看什么时候会返回touch事件
4、开始注入事件
int injectCode = ev.injectEvent(mWm, mAm, mVerbose);
每个Event对象会有injectEvent()方法,同时会wms、ams、以及一个debug参数mVerbose传入进去,具体的事件执行,就是在各个事件对象的injectEvent()方法中进行的,稍后我们会看到touch事件的injectEvent是如何发出一个touch事件的!
四、MonkeySourceRandom的getNextEvent()方法
public MonkeyEvent getNextEvent() {
if (mQ.isEmpty()) { //当双向链表中没有元素时,说明没有可用的事件
generateEvents(); //构造事件,可能构造一个,也可能构造多个,构造的事件对象会添加到m
}
mEventCount++; //MonkeySourceRandom对象持有的事件数量增加1,表示已经提取出的事件数量
MonkeyEvent e = mQ.getFirst(); //获取双向链表中的第一个元素,赋值给局部变量e保存
mQ.removeFirst(); //删除掉该事件(第一个元素)
return e; //向调用者返回MonkeyEvent对象
}
getNextEvent()方法不长,但是信息量不错,mQ是个双向链表,用于保存每个事件对象,两种情况
1、mQ中没有事件
mQ.isEmpty()位true,表示没有事件,此时会调用genrateEvents()方法构造事件,我们的touch事件就是在这个方法里构建,接下来就去这个
2、如果mQ中有事件
取出来链表中保存的第一个事件对象
最后总会返回一个事件对象e
五、MonkeySourceRandom的generateEvents()方法
private void generateEvents() {
float cls = mRandom.nextFloat();
int lastKey = 0;
if (cls < mFactors[FACTOR_TOUCH]) {
generatePointerEvent(mRandom, GESTURE_TAP); //构造Pointer事件(点击事件)
return;
}
// ………………省略…………
}
这是根据命令行中的比例,构造各类事件的方法,touch事件是最先判断的,它的默认比例(当我们不指定比例,monkey会有默认touch事件比例)
1、随机数
mRandom.nextFloat()会产生一个float型的随机数,mFactors是个数组,它存储着每个事件的比例,touch事件对应的数组下标是FACTOR_TOUCH
2、构造一个点事件
通过调用generatePointerEvent()方法来构造touch事件
六、MonkeySourceRandom的generatePointerEvent()方法
private void generatePointerEvent(Random random, int gesture) {
//………………省略……
mQ.addLast(new MonkeyTouchEvent(MotionEvent.ACTION_DOWN) //此处传入的按下的动作
.setDownTime(downAt) //记录按下的时间戳
.addPointer(0, p1.x, p1.y) //id都传0……,把获取到的x坐标与y坐标也传进去
.setIntermediateNote(false)); //false表示这不是一个过渡事件
//向双向链表中添加事件,添加一个元素,即MonkeyTouchEvent对象
//………………省略……
mQ.addLast(new MonkeyTouchEvent(MotionEvent.ACTION_UP)
.setDownTime(downAt) //为啥还记录的按下的时间?
.addPointer(0, p1.x, p1.y)
.setIntermediateNote(false)); //最后再添加一个ACTION_UP事件,如果是点事件,则至少添加了两个元素对象到mQ中,一个ACTION_DOWN、一个ACTION_UP、并且不是过渡事件
}
touch事件,会向mQ中传入两个元素,一个down表示按下、另一个是up表示抬起,组合在一起就是touch事件
mQ中有了事件,事件就可以被获取到了,接下来事件的injectEvent()方法会被调用了
由于mQ中实际持有的是MonkeyTouchEvent对象,所以它的injectEvent()方法会被调用
七、MonkeyTouchEvent的injectEvent()方法
如果你是第一次进入MonkeyTouchEvent,你会发现它根本没有injectEvent()方法,这个时候我们要向上查找,去它的父类MonkeyMotionEvent中在找找,这里要注意了
八、MonkeyMotionEvent的injectEvent()方法
public int injectEvent(IWindowManager iwm, IActivityManager iam, int verbose) {
//…………省略……
try {
if (!InputManager.getInstance().injectInputEvent(me, //走到这里才是真的向手机注入事件,通过InputManager的injectInputEvent注入事件,传入MotionEvent对象
InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT)) { //依赖InputManagerService系统服务注入事件,看来还得死磕Android的系统服务
return MonkeyEvent.INJECT_FAIL; //只要IMS返回的是失败,则证明注入失败,看来这里也是同步方法,Monkey主线程会等待执行完……
}
} finally {
me.recycle(); //将缓存的MotionEvent对象回收掉,牛逼!
}
return MonkeyEvent.INJECT_SUCCESS; //走到这里说明注入事件成功,系统封装好了,靠你来执行了
}
我们只关注与事件注入相关的内容,重点来了,injectEvent()方法中并没有使用传入的wms系统服务、也么有使用ams系统服务,而是另外使用了一个ims系统服务
InputManager对象,这个InputManger对象是用来操作InputManagerService系统服务
调用ims系统服务的injectInputEvent()方法,发出touch事件
总结
1、monkey作者熟悉Android框架,它巧妙的使用了系统预留的系统服务,InputMangerService发出了touch事件
2、InputMangerService预留的injectEvent()方法可以发出touch事件,只要熟悉这个方法的使用,我们自己也能写个monkey程序