vysor的實現原理是什麼?

Vysor 這是一款被大家稱作神器的工具,在chrome安裝一個插件無需root就能連接android,實現同步手機操作和投影顯示。

android屏幕共享和遠程協助這類功能的應用都是什麼實現原理?


跟據 @黑魔法師的回復,搜索到了這篇文章: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手機進行控制,下面是截圖。


用系統API創建一個VirtualDisplay來實時獲取屏幕錄像,,通過反射拿到SurfaceControl類,用MediaCodec createInputSurface()方法用於接收圖像,然後用h264編碼 發到瀏覽器上。手機與瀏覽器用Websocket建立連接,


能否使用客戶端以無線的方式去連接手機和截屏?


vysor只能進行 屏幕截屏,遠程控制沒遇見到比較細的原理講解.


推薦閱讀:

如何看待谷歌要求OEM廠家保留並不得修改安卓6.0的Doze省電模式?
請教手機製造廠商——手機屏幕顯示的顏色深淺(黑白)與耗電量的關係是怎樣的?
Android上有哪些比較好的開源 UI 組件?
如何看待工信部要求 App 備案的傳聞?
iOS 不開源怎麼深入學習 iOS 開發?

TAG:Android開發 | Android | 移動開發 | 遠程協助 |