title: 記 Scrcpy 框架使用記錄
date: 2019-03-03
背景
最近使用 vysor 尼啡。發(fā)現(xiàn)直接把手機當成模擬器操作確實是方便到不行侄榴。 但是魅族 16th plus 在 vysor 失效了。同時vysor 通知太過干擾蜈漓「榱希基于以上兩點切換到開源框架 scrcpy: Display and control your Android device
原理
主要步驟如下:
- 通過
adb push
一個scrcpy-server.jar
到手機上凉驻。
注: scrcpy-server.jar 是雖然是一個 zip 文件。 但是其實是一個apk艳丛。 - PC 端通過
adb reverse
反向代理手機端口匣掸。用來接收手機端發(fā)送過來的數(shù)據(jù)。 -
adb shell CLASSPATH=/data/local/tmp/scrcpy-server.jar app_process /com.genymobile.scrcpy.Server com.genymobile.scrcpy.Server 0 8000000 false - false
使用 app_process 運行 scrcpy-server.jar 的代碼氮双。
scrcpy-server.jar 主要做三件事情:
1碰酝,開啟 LocalSocket 和PC連接。 相應 PC 端傳遞過來的操作戴差。
2送爸,源源不斷的將屏幕畫面輸出到PC,使用Mediacodec 編碼暖释。 PC 通過ffmpeg 解碼播放袭厂。
3,使用 adb 來提高 scrcpy-server.jar 的運行權(quán)限
注: 模擬 input 事件使用 android.hardware.input.IInputManager.injectInputEvent 方法球匕。
安裝
mac 環(huán)境下使用 brew install scrcpy
纹磺,通過漫長的等待完成安裝。同時設置adb 環(huán)境變量亮曹。這里不具體展開橄杨。運行 scrcpy
命令
scrcpy
問題
魅族16 th 出現(xiàn)了錯誤:
通過scrcpy 的issue 發(fā)現(xiàn)這是一個已知的問題 #365 startsWith() on null object at ScreenEncoder.java:158
同樣發(fā)生在魅族 16th 的機型上。 可以確定是因為魔改 Android 源碼導致照卦。 可以相信這個 BUG 將會存在很長一段時間式矫。 那么我們嘗試自己動手一下。
分析
這是一個空指針異常窄瘟, 空指針是最常見也是最簡單的一個bug衷佃。 我們需要拿到系統(tǒng)的代碼進行分析。
獲取 android.media.MediaCodec 源碼
獲取系統(tǒng)的代碼
adb pull /system/framework/arm/boot-framework.oat
使用 baksmali 反編譯oat:
java -jar baksmali-2.2.6.jar deodex /boot-framework.oat
得到系統(tǒng)代碼的 smali 蹄葱。 找到 MediaCodec 的 smali 的1918行
注:反編譯方式 在 8.1 上 baksmali 會失敗氏义, 可以嘗試 vdexExtractor 從 vdex 獲取 dex 文件。在對dex 進行反解得到代碼图云。
.method private configure(Landroid/media/MediaFormat;Landroid/view/Surface;Landroid/media/MediaCrypto;Landroid/os/IHwBinder;I)V
.registers 19
.param p1, "format" # Landroid/media/MediaFormat;
.param p2, "surface" # Landroid/view/Surface;
.param p3, "crypto" # Landroid/media/MediaCrypto;
.param p4, "descramblerBinder" # Landroid/os/IHwBinder;
.param p5, "flags" # I
...
.line 1918
.local v2, "values":[Ljava/lang/Object;
invoke-static {}, Landroid/app/ActivityThread;->currentPackageName()Ljava/lang/String;
move-result-object v0
const-string/jumbo v3, "com.tencent.mm"
invoke-virtual {v0, v3}, Ljava/lang/String;->startsWith(Ljava/lang/String;)Z
move-result v0
if-eqz v0, :cond_23
...
}
這里使用 ActivityThread.currentPackageName() 獲取包名與 微信的包名做比較惯悠。
通過分析可以發(fā)現(xiàn) ActivityThread.currentPackageName() 返回為 null 導致的空指針異常。
問題原因
ActivityThread.currentPackageName() 為空竣况?
正確情況 APP 啟動的流程如下:AMS 調(diào)用
Process.start("android.app.ActivityThread", app.processName, uid, uid...)
通知 zygote 進程 fork 出一個新的進程同時執(zhí)行 android.app.ActivityThread.main(String[] args)
方法克婶。
main方法會初始化 ActivityThread 和初始化主線程Looper。同時調(diào)用 ActivityThread.attach()方法,會將 binder 類型 ApplicationThread 對象傳遞給 ActivityMangerService 情萤。ASM 獲取到 ApplicationThread 以后會查詢對應的信息(包括 PackageName)鸭蛙, 會通過 ApplicationThread 以 IPC 的方式將信息回調(diào)給 ActivityThread,bindApplication 方法. app bindApplication 方法以后才知道自己的包名。在至此 ActivityThread.currentPackageName() 不為空筋岛。
App 運行在 fork zygote 的子進程中娶视。
而 Scrcpy 是通過 app_process 啟動非 zygote 的 Runtime 進程中。
解決方案:
public static String currentPackageName() {
ActivityThread am = currentActivityThread();
return (am != null && am.mBoundApplication != null)
? am.mBoundApplication.appInfo.packageName : null;
}
最初方式是使用 xposed hook 該方法睁宰。 對空值進行防護肪获。這樣就不用管 ActivityThread 后續(xù)可能的魔改。
但是Xposed 默認不對非 zygote 進行進行攔截柒傻。
isXposedLoaded = xposed::initialize(zygote, startSystemServer, className, argc, argv)
/** Initialize Xposed (unless it is disabled). */
bool initialize(bool zygote, bool startSystemServer, const char* className, int argc, char* const argv[]) {
#if !defined(XPOSED_ENABLE_FOR_TOOLS)
if (!zygote)
return false;
#endif
if (isMinimalFramework()) {
ALOGI("Not loading Xposed for minimal framework (encrypted device)");
return false;
}
求其次使用最簡單的方案: 反射
只要 currentActivityThread 孝赫,mBoundApplication ,appInfo红符,packageName 不為空青柄,ActivityThread.currentPackageName() 返回不為空。
try {
Class<?> ActivityThreadClass = Class.forName("android.app.ActivityThread");
Method currentPackageName = ActivityThreadClass.getMethod("currentPackageName");
currentPackageName.setAccessible(true);
Field sCurrentActivityThread = ActivityThreadClass.getDeclaredField("sCurrentActivityThread");
Field mBoundApplication = ActivityThreadClass.getDeclaredField("mBoundApplication");
sCurrentActivityThread.setAccessible(true);
mBoundApplication.setAccessible(true);
Constructor<?> constructor = ActivityThreadClass.getDeclaredConstructor();
constructor.setAccessible(true);
// sCurrentActivityThread.set(null, UnsafeAllocator.create().newInstance(ActivityThreadClass));
sCurrentActivityThread.set(null, constructor.newInstance());
Object sCurrentActivityThreadObject = sCurrentActivityThread.get(null);
Class<?> AppBindDataClass = Class.forName("android.app.ActivityThread$AppBindData");
Field appInfo = AppBindDataClass.getDeclaredField("appInfo");
appInfo.setAccessible(true);
Constructor<?> constructor1 = AppBindDataClass.getDeclaredConstructor();
constructor1.setAccessible(true);
Object AppBindDataObject = constructor1.newInstance();
ApplicationInfo applicationInfo = new ApplicationInfo();
applicationInfo.packageName = "com.dim";
appInfo.set(AppBindDataObject, applicationInfo);
mBoundApplication.setAccessible(true);
mBoundApplication.set(sCurrentActivityThreadObject, AppBindDataObject);
} catch (Throwable throwable) {
}
我們需要將代碼插入到 com.genymobile.scrcpy.Server 的
main
方法中违孝。 將編譯好的 apk 重新命令 scrcpy-server.jar
替換目錄 /usr/local/Cellar/scrcpy/1.7/share/scrcpy/scrcpy-server.jar:
這是mac 下的地方刹前。其他可能會有不同。
同時還需要做的事情是
初始化 Looper 因為 ActivityThread 中的 H 是個Handler雌桑。 Handler 的初始化需要Looper 環(huán)境喇喉。
至此對于 魅族16 th 在 Scrcpy 的改造完成。
總結(jié)
這篇主要介紹了 Scrcpy 框架的主要原理校坑。 以及如何反編譯系統(tǒng)的代碼拣技。