vysor 原理以及 Android 同屏方案

   2016-10-13 0
核心提示:vysor是一个免root实现电脑控制手机的chrome插件,目前也有几款类似的通过电脑控制手机的软件,不过都需要root权限,并且流畅度并不高。vysor没有多余的功能,流畅度也很高,刚接触到这款插件时我惊讶于它的流畅度以及免root,就一直对它的实现原理很感兴趣。

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

截屏常见的方案

在介绍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原理

反编译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 onRotation Changed(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 Assertion Error("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下没有类似的方面的软件。到时这个同步软件可以作为插件和反编译套件集成。最后放一张截图。

 
标签: Bitmap 安卓开发
反对 0举报 0 评论 0
 

免责声明:本文仅代表作者个人观点,与乐学笔记(本网)无关。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
    本网站有部分内容均转载自其它媒体,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责,若因作品内容、知识产权、版权和其他问题,请及时提供相关证明等材料并与我们留言联系,本网站将在规定时间内给予删除等相关处理.

  • 如何进行网络视频截图/获取视频的缩略图
    如何进行网络视频截图/获取视频的缩略图
    小编导读:获取视频的缩略图,截图正在播放的视频某一帧,是在音视频开发中,常遇到的问题。本文是主要用于点播中截图视频,同时还可以获取点播视频的缩略图进行显示,留下一个问题,如下图所示, 如果要获取直播中节目视频缩略图,该怎么做呢?(ps:直播是直
  • 不得不知道的图片加载框架之Glide
    不得不知道的图片加载框架之Glide
    简介在泰国举行的谷歌开发者论坛上,谷歌为我们介绍了一个名叫 Glide 的图片加载库,作者是bumptech。这个库被广泛的运用在google的开源项目中,包括2014年google I/O大会上发布的官方app。特点(1)使用简单(2)可配置度高,自适应程度高(3)支持常见图片
  • 一个实用方便的图片控件SImageView
    一个实用方便的图片控件SImageView
    控件介绍 这是一个简单到 sImageView.setImageUrls(http://img3.cache.netease.com/ent/2009/4/17/20090417104402666a4.jpg); 设置一个网址即可显示图片的控件相对 ImageView 功能的扩展的控件, 但是没有继承 ImageView 直接继承的 View . 比如 QQ群组头像 ,
  • Android开发艺术探索学习笔记(三)—Android性能优化之Bitmap导致的内存溢出
    Android开发艺术探索学习笔记(三)—Android性能
    原本计划是按照章节顺序学习《Android开发艺术探索》这本书的,Android性能优化这部分也是本书的最后一章。但是周末的时候,友盟线下反馈的公司项目的一个错误让我不得不提前学习这一块的知识。先看看线下反馈的错误吧:java.lang.OutOfMemoryError:应用程序
  • Android图形图像使用总结
    Android图形图像使用总结
    一.图形特效(一)特效的实现方式在Android中,提供了3种方式实现特效,setXXX方法,postXXX和preXXX()方法。1.setXXX方法用于直接设置Matrix的值,每使用一次setXXX()方法,整个的Matrix都会变掉。2.postXXX方法用于采用后乘的方式为Matrix设置值,可以连续多次
  • AsyncTask 工作原理(上)
    AsyncTask 是一种轻量级是异步任务类,它可以在线程池中执行后台任何,将执行的进度和最终结果传递给主线程,并在主线程中更新UI。AsyncTask 是一个抽象类,其构造函数//Params:传入doInBackground 中的参数类型//Progress: 后台执行进度的类型,传入onProgre
  • 简单实现 Android 图片三级缓存机制
    简单实现 Android 图片三级缓存机制
    用户在使用我们的APP时,通常会重复浏览一些图片,这时如果每一次浏览都需要通过网络获取图片,那么将会非常流量。为了节省用户流量,提高图片加载效率,我们通常使用图片三级缓存策略,即通过网络、本地、内存三级缓存图片,来减少不必要的网络交互,避免浪
  • Picasso 解析 (1)- 一张图片是如何加载出来的
    前言Picasso是JakeWharton大神在github上的一个开源图片加载框架,使用起来极其方便,甚至只需要一行代码就可以搞定图片加载:Picasso.with(context).load(http://i.imgur.com/DvpvklR.png).into(imageView);具体如何使用该框架我就不在这里赘述了,大家可以
  • 自定义圆形图片、可控位置圆角图片
    自定义圆形图片、可控位置圆角图片
    一.原理1.下面的Xfermode子类可以改变这种行为:AvoidXfermode 指定了一个颜色和容差,强制Paint避免在它上面绘图(或者只在它上面绘图)。PixelXorXfermode 当覆盖已有的颜色时,应用一个简单的像素XOR操作。PorterDuffXfermode 这是一个非常强大的转换模式,
  • Android的oom详解
    Android的oom详解
    oom的定义OOM(out of memory)即内存泄露。一个程序中,已经不需要使用某个对象,但是因为仍然有引用指向它垃圾回收器就无法回收它,当该对象占用的内存无法被回收时,就容易造成内存泄露。Android的oom原因1.资源对象没关闭造成的内存泄露,try catch final
    09-12 BitmapOOM
点击排行