转载链接:https://www.zhihu.com/question/46229570/answer/124043146
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

跟据

@黑魔法师

的回复,搜索到了这篇文章:vysor原理以及Android同屏方案
原文内容如下:

vysor原理以及Android同屏方案
2016-07-02

vysor是一个免root实现电脑控制手机的chrome插件,目前也有几款类似的通过电脑控制手机的软件,不过都需要root权限,并且流畅度并不高。vysor没有多余的功能,流畅度也很高,刚接触到这款插件时我惊讶于它的流畅度以及免root,就一直对它的实现原理很感兴趣。这款插件我用了大半年,最近在升级后我发现它居然开始收费了,终生版需要39.99美元,不过经过简单的分析后我很轻松的破解了它的pro版,在分析的过程中发现它的原理并不复杂,所以就打算自己也实现一个类似的软件。

vysor原理以及Android同屏方案截屏常见的方案

在介绍vysor的原理前我先简单介绍一下目前公开的截屏方案。

  • View.getDrawingCache()

这是最常见的应用内截屏方法,这个函数的原理就是通过view的Cache来获取一个bitmap对象,然后保存成图片文件,这种截屏方式非常的简单,但是局限行也很明显,首先它只能截取应用内部的界面,甚至连状态栏都不能截取到。其次是对某些view的兼容性也不好,比如webview内的内容也无法截取。

  • 读取/dev/graphics/fb0

因为Android是基于linux内核,所以我们也能在android中找到framebuffer这个设备,我们可以通过读取/dev/graphics/fb0这个帧缓存文件中的数据来获取屏幕上的内容,但是这个文件是system权限的,所以只有通过root才能读取到其中的内容,并且直接通过framebuffer读取出来的画面还需要转换成rgb才能正常显示。下面是通过adb读取这个文件内容的效果。

  • 反射调用SurfaceControl.screenshot()/Surface.screenshot()

SurfaceControl.screenshot()(低版本是Surface.screenshot())是系统内部提供的截屏函数,但是这个函数是@hide的,所以无法直接调用,需要反射调用。我尝试反射调用这个函数,但是函数返回的是null,后面发现SurfaceControl这个类也是隐藏的,所以从用户代码中无法获取这个类。也有一些方法能够调用到这个函数,比如重新编译一套sdk,或者在源码环境下编译apk,但是这种方案兼容性太差,只能在特定ROM下成功运行。

  • screencap -p xxx.png/screenshot xxx.png

这两个是在shell下调用的命令,通过adb shell可以直接截图,但是在代码里调用则需要系统权限,所以无法调用。可以看到要实现类似vysor的同步操作,可以使用这两个命令来截取屏幕然后传到电脑显示,但是我自己实现后发现这种方式非常的卡,因为这两个命令不能压缩图片,所以导致获取和生成图片的时间非常长。

  • MediaProjection,VirtualDisplay (>=5.0)

在5.0以后,google开放了截屏的接口,可以通过”虚拟屏幕”来录制和截取屏幕,不过因为这种方式会弹出确认对话框,并且只在5.0上有效,所以我没有对这种方案做深入的研究。

可以看到,上述方案中并没有解决方案能够做到兼容性和效率都非常完美,但是我在接触到vysor后发现它不但画面清晰,流畅,而且不需要root。那么它是用了什么黑科技呢?下面我们反编译它的代码来研究一下它的实现机制。

vysor原理以及Android同屏方案vysor原理

反编译vysor的apk后可以发现它的代码并不多,通过分析后我发现它的核心代码在Main这个类中。

首先来看Main函数的main方法,这个方法比较长,这里直接贴出源码。

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的实现代码。

* renamed from: com.koushikdutta.vysor.Main.5 */static class AnonymousClass5 implements HttpServerRequestCallback {final /* synthetic */ IWindowManager val$wm;AnonymousClass5(IWindowManager iWindowManager) {this.val$wm = iWindowManager;}public void onRequest(AsyncHttpServerRequest request, AsyncHttpServerResponse response) {if (Main.checkPassword(request.getQuery().getString("password"))) {Log.i(Main.LOGTAG, "screenshot authentication success");try {Bitmap bitmap = EncoderFeeder.screenshot(this.val$wm);ByteArrayOutputStream bout = new ByteArrayOutputStream();bitmap.compress(CompressFormat.JPEG, 100, bout);bout.flush();response.send("image/jpeg", bout.toByteArray());return;} catch (Exception e) {response.code(500);response.send(e.toString());return;}}Log.i(Main.LOGTAG, "screenshot authentication failed");response.code(401);response.send("Not Authorized.");}}

这个类传入了一个wm类,这个类是用来监听屏幕旋转的,这里不用管它。另外在vysor开始运行时,会随机生成一个验证码,只有验证通过才能进行连接,所以这里有一个验证的过程,这里也不过管。可以看到这个类定义的响应函数的代码非常简单,就是通过EncoderFeeder.screenshot()函数来过去截图的bitmap,然后返回给请求端。那么EncoderFeeder.screenshot这个函数是怎样实现截图的呢?

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经过混淆,但是很容易的可以通过一些工具来解密。然后就是分析它的代码了,终于被我找到了关键的代码。

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的代码。因为是测试用所以我没有添加其他功能。

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();}/* renamed from: com.koushikdutta.vysor.Main.5 */static class AnonymousClass5 implements HttpServerRequestCallback {public void onRequest(AsyncHttpServerRequest request, AsyncHttpServerResponse response) {try {Bitmap bitmap = ScreenShotFb.screenshot();ByteArrayOutputStream bout = new ByteArrayOutputStream();bitmap.compress(Bitmap.CompressFormat.JPEG, 100, bout);bout.flush();response.send("image/jpeg", bout.toByteArray());return;} catch (Exception e) {response.code(500);response.send(e.toString());return;}}}
}

编译成apk然后安装后,我们使用adb shell来运行这个类,主要方法如下,首先导出classpath,否则会提示找不到类。

export CLASSPATH=/data/app/com.zke1e.andcast-1/base.apk

然后调用app_process来启动这个类。

exec app_process /system/bin com.zke1e.andcast.Main '$@'

可以看到类已经成功运行了,正在监听请求。

然后使用adb forward转发端口。

adb forward tcp:53516 tcp:53516

最后在浏览器里访问,就可以获取截图了。

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

vysor原理以及Android同屏方案更新

经过一段时间的研究,最后实现了将传输的截图改成了h264码流,提高的流畅度和稳定性,然后将接受端放在了浏览器中,实现了可以在浏览器中对android手机进行控制,下面是截图。

编辑于 2016-09-28
126 14 条评论

分享

收藏 感谢 收起

黑魔法师
Android程序员

7 人赞同了该回答

用系统API创建一个VirtualDisplay来实时获取屏幕录像,,通过反射拿到SurfaceControl类,用MediaCodec createInputSurface()方法用于接收图像,然后用h264编码 发到浏览器上。手机与浏览器用Websocket建立连接, 

vysor技术实现原理(投屏不包括鼠标控制)相关推荐

  1. 使用Vysor将安卓手机投屏到PC上

    偶然发现了一个很实用的投屏软件Vysor,Vysor是一个谷歌浏览器的插件,可以将手机的屏幕投屏到电脑上,也可以从电脑上来控制屏幕.下面来介绍一下安装的过程,是以Windows10家庭版和华为手机来做 ...

  2. scrcpy投屏_安卓投屏利器——PC一键控制多台手机

    点击关注,我们共同每天进步一点点! 之前给大家介绍了投屏开源工具scrcpy(Scrcpy投屏,在电脑上流畅操控你的手机!),今天要介绍的投屏工具是在scrcpy的基础上进行了二次开发,使用更加友好. ...

  3. android scrcpy 源代码分析,Scrcpy投屏原理浅析-设备控制篇

    起初我真的想过自己单独写一套来着,后来发现 Scrcpy与vysor是都是投屏中比较优秀的项目了,非侵入性,不需要设备单独 scrcpy启动阶段 它到底是怎么做到执行scrcpy命令,在较短的时间内就 ...

  4. java 控制 区域投屏,Scrcpy投屏原理浅析-设备控制篇

    起初我真的想过自己单独写一套来着,后来发现时间真的不够,所以有了对scrcpy源码的浅析,服务端我就用scrcpy现有的了,客户端scrcpy采用ffmpeg+sdl2.0进行了跨平台的播放,我准备用 ...

  5. iphone屏幕录制_iphone投屏到电脑详细教程

    Windows操作系统投屏方法: 注意:只要是Windows操作系统,不论Windows 7还是Windows 10,不论品牌机还是组装机,台式机笔记本,联想,惠普,戴尔,华硕等等都适用! 方法1:无 ...

  6. 爱奇艺发布新款电视果,AI 语音控制,4G、WiFi 双模式投屏!

    6 月 21 日,爱奇艺电视果 4G 新品发布会在京东集团总部举行,爱奇艺发布了全球首款 4G 人工智能投屏智能硬件--电视果 4G.电视果负责人谭涛表示,电视果 4G 率先将极速 4G 网络与人工智 ...

  7. 安卓miracast花屏_EMUI 10.1 Cast+无线投屏 让你开启“0”花屏新体验

    在家用手机看电影或玩游戏,相对局限的屏幕尺寸,限制了视觉感知,不论是观影还是玩游戏,体验都受到不同程度的影响.而EMUI 10.1带来的Cast+无线投屏功能,可将手机屏幕的画面稳定流畅地投送到大屏终 ...

  8. iphone怎么投屏到三星电视?这样操作即可实现

    不知道为啥,身边好多人用起了苹果手机(难道没有打工人的压力了?),后来,因为工作需要,我自己也入手了一部iphone,看看与在用的安卓手机有什么不同.其实使用效果差不了多少,安卓.ios系统的手机各有 ...

  9. iPhone销量下降,这两种方法可以把Android和iPhone手机投屏到电脑上!

    随着现在科技的快速发展,智能产品不断的创新,各种各样的黑科技技术逐步的呈现在我们的眼前,而今天小编要和大家讨论的就是手机投屏到电脑上这一技术,就目前而言,大家对投影技术应该非常的熟悉了,不管是演讲还是 ...

最新文章

  1. java 连接池技术_java数据库连接池技术原理(浅析)
  2. 公钥加密以及混合加密
  3. 【POJ - 1698】Alice's Chance(网络流最大流,建图)
  4. java io流操作_【Java基础】IO流操作
  5. Maven系列2--pom.xml 配置详解
  6. HTML5商城开发四 多图或多商品的水平滚动展示
  7. 盒子背景颜色(HTML、CSS)
  8. KVC在定义Model类中的妙用
  9. 机器学习基础算法14-波士顿房价预测-ElasticNet模型
  10. Eclipse开发环境设置(Maven+Spring MVC+Flex)
  11. Android 第三方应用接入微信平台(2)
  12. matlab产生年月日的时间序列,求助:在MATLAB里如何输入时间序列中的时间
  13. 445端口不通经验总结
  14. Web前端开发实验(导航栏、购物页面)
  15. SpringBoot整合高德地图 地理编码\逆地理编码
  16. 开源SIP服务器OpenSIPS简介
  17. 国内大多数网站的密码在 post 传输过程中都是明文的,这正常吗?
  18. 再看《英雄本色》:世上已无英雄?
  19. Docker容器的数据卷(volumes)
  20. HDU 5804 Price List(水~)

热门文章

  1. 准备了 185 万养老金
  2. CCS编写F28335定时器0测试程序报错unresolved symbols remain error解决方法
  3. 用emWin的2D绘图函数画一个带圆形端点的环形进度条
  4. 8.19! 今天我有18生日,点击阅读或顶部 尾随幸运的一天!生日知识!↓——【Badboy】...
  5. revit卸载/完美解决安装失败/如何彻底卸载清除干净revit各种残留注册表和文件的方法
  6. vue ie浏览器兼容
  7. 研究生如何进行文献检索和文献阅读
  8. java 余弦定理_文本相似度计算之余弦定理
  9. LS-DYNA (动力分析程序)
  10. docker安装nginx镜像