vysor是一个免root实现电脑控制手机的chrome插件,目前也有几款相似的经过电脑控制手机的软件,不过都须要root权限,而且流畅度并不高。vysor没有多余的功能,流畅度也很高,刚接触到这款插件时我惊讶于它的流畅度以及免root,就一直对它的实现原理很感兴趣。这款插件我用了大半年,最近在升级后我发现它竟然开始收费了,终生版须要39.99美圆,不过通过简单的分析后我很轻松的破解了它的pro版,在分析的过程当中发现它的原理并不复杂,因此就打算本身也实现一个相似的软件。javascript
截屏常见的方案
在介绍vysor的原理前我先简单介绍一下目前公开的截屏方案。java
这是最多见的应用内截屏方法,这个函数的原理就是经过view的Cache来获取一个bitmap对象,而后保存成图片文件,这种截屏方式很是的简单,可是局限行也很明显,首先它只能截取应用内部的界面,甚至连状态栏都不能截取到。其次是对某些view的兼容性也很差,好比webview内的内容也没法截取。linux
由于Android是基于linux内核,因此咱们也能在android中找到framebuffer这个设备,咱们能够经过读取/dev/graphics/fb0这个帧缓存文件中的数据来获取屏幕上的内容,可是这个文件是system权限的,因此只有经过root才能读取到其中的内容,而且直接经过framebuffer读取出来的画面还须要转换成rgb才能正常显示。下面是经过adb读取这个文件内容的效果。android

- 反射调用SurfaceControl.screenshot()/Surface.screenshot()
SurfaceControl.screenshot()(低版本是Surface.screenshot())是系统内部提供的截屏函数,可是这个函数是@hide的,因此没法直接调用,须要反射调用。我尝试反射调用这个函数,可是函数返回的是null,后面发现SurfaceControl这个类也是隐藏的,因此从用户代码中没法获取这个类。也有一些方法可以调用到这个函数,好比从新编译一套sdk,或者在源码环境下编译apk,可是这种方案兼容性太差,只能在特定ROM下成功运行。web
- screencap -p xxx.png/screenshot xxx.png
这两个是在shell下调用的命令,经过adb shell能够直接截图,可是在代码里调用则须要系统权限,因此没法调用。能够看到要实现相似vysor的同步操做,可使用这两个命令来截取屏幕而后传到电脑显示,可是我本身实现后发现这种方式很是的卡,由于这两个命令不能压缩图片,因此致使获取和生成图片的时间很是长。chrome
- MediaProjection,VirtualDisplay (>=5.0)
在5.0之后,google开放了截屏的接口,能够经过”虚拟屏幕”来录制和截取屏幕,不过由于这种方式会弹出确认对话框,而且只在5.0上有效,因此我没有对这种方案作深刻的研究。shell
能够看到,上述方案中并无解决方案可以作到兼容性和效率都很是完美,可是我在接触到vysor后发现它不但画面清晰,流畅,并且不须要root。那么它是用了什么黑科技呢?下面咱们反编译它的代码来研究一下它的实现机制。json
vysor原理
反编译vysor的apk后能够发现它的代码并很少,经过分析后我发现它的核心代码在Main这个类中。windows

首先来看Main函数的main方法,这个方法比较长,这里直接贴出源码。浏览器
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 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
|
public static void main(String[] args) throws Exception { if (args.length > 0) { commandLinePassword = args[0]; Log.i(LOGTAG, "Received command line password: " + commandLinePassword); } Looper.prepare(); looper = Looper.myLooper(); AsyncServer server = new AsyncServer(); AsyncHttpServer httpServer = new AsyncHttpServer() { protected boolean onRequest(AsyncHttpServerRequest request, AsyncHttpServerResponse response) { Log.i(Main.LOGTAG, request.getHeaders().toString()); return super.onRequest(request, response); } }; String str = "getInstance"; Object[] objArr = new Object[0]; InputManager im = (InputManager) InputManager.class.getDeclaredMethod(r20, new Class[0]).invoke(null, objArr); str = "obtain"; MotionEvent.class.getDeclaredMethod(r20, new Class[0]).setAccessible(true); str = "injectInputEvent"; Method injectInputEventMethod = InputManager.class.getMethod(r20, new Class[]{InputEvent.class, Integer.TYPE}); KeyCharacterMap kcm = KeyCharacterMap.load(-1); Class cls = Class.forName("android.os.ServiceManager"); Method getServiceMethod = cls.getDeclaredMethod("getService", new Class[]{String.class}); IClipboard clipboard = IClipboard.Stub.asInterface((IBinder) getServiceMethod.invoke(null, new Object[]{"clipboard"})); clipboard.addPrimaryClipChangedListener(new AnonymousClass3(clipboard), null); IPowerManager pm = IPowerManager.Stub.asInterface((IBinder) getServiceMethod.invoke(null, new Object[]{"power"})); IWindowManager wm = IWindowManager.Stub.asInterface((IBinder) getServiceMethod.invoke(null, new Object[]{"window"})); IRotationWatcher watcher = new Stub() { public void onRotationChanged(int rotation) throws RemoteException { if (Main.webSocket != null) { Point displaySize = SurfaceControlVirtualDisplayFactory.getCurrentDisplaySize(); JSONObject json = new JSONObject(); try { json.put("type", "displaySize"); json.put("screenWidth", displaySize.x); json.put("screenHeight", displaySize.y); json.put("nav", Main.hasNavBar()); Main.webSocket.send(json.toString()); } catch (JSONException e) { } } } }; wm.watchRotation(watcher); httpServer.get("/screenshot.jpg", new AnonymousClass5(wm)); httpServer.websocket("/input", "mirror-protocol", new AnonymousClass6(watcher, im, injectInputEventMethod, pm, wm, kcm)); httpServer.get("/h264", new AnonymousClass7(im, injectInputEventMethod, pm, wm)); Log.i(LOGTAG, "Server starting"); AsyncServerSocket rawSocket = server.listen(null, 53517, new AnonymousClass8(wm)); if (httpServer.listen(server, 53516) == null || rawSocket == null) { System.out.println("No server socket?"); Log.e(LOGTAG, "No server socket?"); throw new AssertionError("No server socket?"); } System.out.println("Started"); Log.i(LOGTAG, "Waiting for exit"); Looper.loop(); Log.i(LOGTAG, "Looper done"); server.stop(); if (current != null) { current.stop(); current = null; } Log.i(LOGTAG, "Done!"); System.exit(0); }
|
这个软件koushikdutta是由开发的,这个团队之前发布过一个很是流行的开源网络库:async。在这个项目中也用到了这个开源库。main函数主要是新建了一个httpserver而后开放了几个接口,经过screenshot.jpg获取截图,经过socket input接口来发送点击信息,经过h264这个接口来获取实时的屏幕视频流。每个接口都有对应的响应函数,这里咱们主要研究截图,因此就看screenshot这个接口。h264这个接口传输的是实时的视频流,因此就流畅性来讲应该会更好,它也是经过virtualdisplay来实现的有兴趣的读者能够自行研究。
接下来咱们来看screenshot对应的响应函数AnonymousClass5的实现代码。
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
|
* renamed from: com.koushikdutta.vysor.Main.5 */ static class AnonymousClass5 implements HttpServerRequestCallback { final |
这个类传入了一个wm类,这个类是用来监听屏幕旋转的,这里不用管它。另外在vysor开始运行时,会随机生成一个验证码,只有验证经过才能进行链接,因此这里有一个验证的过程,这里也不过管。能够看到这个类定义的响应函数的代码很是简单,就是经过EncoderFeeder.screenshot()函数来过去截图的bitmap,而后返回给请求端。那么EncoderFeeder.screenshot这个函数是怎样实现截图的呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
public static Bitmap screenshot(IWindowManager wm) throws Exception { String surfaceClassName; Point size = SurfaceControlVirtualDisplayFactory.getCurrentDisplaySize(false); if (VERSION.SDK_INT <= 17) { surfaceClassName = "android.view.Surface"; } else { surfaceClassName = "android.view.SurfaceControl"; } Bitmap b = (Bitmap) Class.forName(surfaceClassName).getDeclaredMethod("screenshot", new Class[]{Integer.TYPE, Integer.TYPE}).invoke(null, new Object[]{Integer.valueOf(size.x), Integer.valueOf(size.y)}); int rotation = wm.getRotation(); if (rotation == 0) { return b; } Matrix m = new Matrix(); if (rotation == 1) { m.postRotate(-90.0f); } else if (rotation == 2) { m.postRotate(-180.0f); } else if (rotation == 3) { m.postRotate(-270.0f); } return Bitmap.createBitmap(b, 0, 0, size.x, size.y, m, false); }
|
这里的截图的核心代码也是反射调用Surface/SurfaceControl的screenshot方法。可是咱们前面已经了解到,这个类只有在系统权限下才能获取到,那么vysor又是怎么调用到这个函数的呢?咱们能够确认的是vysor不是经过重编译sdk和使用系统签名来完成的,由于那样只能对特定的rom适用。
当时看到这里的代码后我也很是困惑,vysor是怎么调用到这个类的。我注意到了vysor的核心代码不是在某个Activity或者Service中而是在一个Main类中,按照通常的逻辑来讲,这种实时传屏应该是放在Service中不断截屏而后发给服务端,因此我决定再看下它的服务端的代码。
vysor的服务端是一个chrome插件,用javascript写成的,因此找到源码比java更加简单。虽然js通过混淆,可是很容易的能够经过一些工具来解密。而后就是分析它的代码了,终于被我找到了关键的代码。
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
|
function y(e, t, n) { m(e, "Connecting...");
function o(o) { var i = Math.round(Math.random() * (1 << 30)).toString(16); var r = "echo -n " + i + " > /data/local/tmp/vysor.pwd ; chmod 600 /data/local/tmp/vysor.pwd"; Adb.shell({ command: "ls -l /system/bin/app_process*", serialno: e }, function(s) { var c = "/system/bin/app_process"; if (s && s.indexOf("app_process32") != -1) { c += "32" } Adb.sendClientCommand({ command: 'shell:sh -c "CLASSPATH=' + o + " " + c + " /system/bin com.koushikdutta.vysor.Main " + i + '"', serialno: e }, function(o) { Adb.shell({ serialno: e, command: 'sh -c "' + r + '"' }, function(e) { Socket.eat(o); n(t, i) }) }) }) }
|
能够看到上面的代码是调用了adb shell命令来启动com.koushikdutta.vysor.Main类,而且上面获取了app_process这个程序。相信对android熟悉读者已经明白它的原理了。我简单解释一下。咱们已经知道Surface/SurfaceControl这两个类是须要具备相应权限的程序才能调用到,用户进程没法获取到。adb shell能够调用screencap或者screenshot来截取屏幕,那就说明adb shell具备截屏的权限。Surface/SurfaceControl和screenshot/screencap它们内部的实现机制应该是相同的,因此也就是说adb shell是具备截屏权限的也就是可以调用到Surface/SurfaceControl。那么咱们怎么经过adb shell来调用到这两个类呢,答案就是app_process。app_process能够直接运行一个普通的java类,详细的资料你们能够在网上找到。也就是说咱们经过adb shell运行app_process,而后经过app_process来运行一个java类,在java类中就能够访问到Surface/SurfaceControl这两个类,是否是很巧妙?
理论有了,下面咱们来经过代码验证。这里咱们能够直接使用vysor的代码。由于是测试用因此我没有添加其余功能。
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 34 35 36 37 38 39 40 41 42 43
|
public class Main {
static Looper looper;
public static void main(String[] args) {
AsyncHttpServer httpServer = new AsyncHttpServer() { protected boolean onRequest(AsyncHttpServerRequest request, AsyncHttpServerResponse response) { return super.onRequest(request, response); } };
Looper.prepare(); looper = Looper.myLooper(); System.out.println("Andcast Main Entry!"); AsyncServer server = new AsyncServer(); httpServer.get("/screenshot.jpg", new AnonymousClass5()); httpServer.listen(server, 53516);
Looper.loop();
}
|
编译成apk而后安装后,咱们使用adb shell来运行这个类,主要方法以下,首先导出classpath,不然会提示找不到类。
1
|
export CLASSPATH=/data/app/com.zke1e.andcast-1/base.apk
|
而后调用app_process来启动这个类。
1
|
exec app_process /system/bin com.zke1e.andcast.Main '$@'
|
能够看到类已经成功运行了,正在监听请求。

而后使用adb forward转发端口。
1
|
adb forward tcp:53516 tcp:53516
|
最后在浏览器里访问,就能够获取截图了。

固然只有简单的截图功能是不够,咱们须要可以流畅实时的传输android的屏幕,而且可以在电脑上控制,通过两天的编写,我使用java实现了相似vysor的功能。从流畅度和清晰度上都和vysor差很少,后续还会考虑加入文件传输和声音传输等功能。最近计划编写一个java版的android反编译集成环境,相似android killer。由于android killer只能在windows上使用,而linux下没有相似的方面的软件。到时这个同步软件能够做为插件和反编译套件集成。最后放一张截图。

更新
通过一段时间的研究,最后实现了将传输的截图改为了h264码流,提升的流畅度和稳定性,而后将接受端放在了浏览器中,实现了能够在浏览器中对android手机进行控制,下面是截图。
