Github地址:VirtualApp
簡(jiǎn)介
VirtualApp是一款運(yùn)行于Android系統(tǒng)的沙盒產(chǎn)品亏拉,可以理解為輕量級(jí)的“Android虛擬機(jī)”确憨。其產(chǎn)品形態(tài)為高可擴(kuò)展谱姓,可定制的集成SDK,您可以基于VA或者使用VA定制開發(fā)各種看似不可能完成的項(xiàng)目蜒蕾。VA目前被廣泛應(yīng)用于插件化開發(fā)及汉、無(wú)感知熱更新沮趣、云控自動(dòng)化、多開坷随、手游租號(hào)房铭、手游手柄免激活驻龟、區(qū)塊鏈、移動(dòng)辦公安全缸匪、軍隊(duì)政府保密翁狐、手機(jī)模擬信息、腳本自動(dòng)化凌蔬、自動(dòng)化測(cè)試等技術(shù)領(lǐng)域露懒。
VirtualApp可以創(chuàng)建一個(gè)虛擬空間,你可以在虛擬空間內(nèi)任意的安裝砂心、啟動(dòng)和卸載APK隐锭,這一切都與外部隔離,如同一個(gè)沙盒计贰,APK無(wú)需在外部安裝。
VirtualApp的特有能力
克隆能力
可以克隆外部系統(tǒng)中已經(jīng)安裝的App蒂窒,并在內(nèi)部運(yùn)行躁倒,互不干擾。典型應(yīng)用場(chǎng)景為App雙開洒琢。免安裝能力
除了克隆已安裝App之外秧秉,VA可以直接在內(nèi)部安裝(外部無(wú)感知)apk,并在內(nèi)部直接運(yùn)行衰抑。典型應(yīng)用場(chǎng)景為插件化象迎,獨(dú)立應(yīng)用市場(chǎng)等。多開能力
VA不僅可以“雙開”呛踊,獨(dú)特的多用戶模式支持用戶在內(nèi)部無(wú)限多開同一個(gè)App砾淌。內(nèi)外隔離能力
VA是一個(gè)標(biāo)準(zhǔn)的沙盒,或者說(shuō)“虛擬機(jī)”谭网,提供了一整套內(nèi)部與外部的隔離機(jī)制汪厨,包括但不限于(文件隔離/組件隔離/進(jìn)程通訊隔離),簡(jiǎn)單的說(shuō)VA內(nèi)部就是一個(gè)“完全獨(dú)立的空間”愉择。在此基礎(chǔ)之上劫乱,稍作定制即可實(shí)現(xiàn)一部手機(jī)上的“虛擬手機(jī)”。當(dāng)然您也可以發(fā)揮想象锥涕,定制成應(yīng)用于數(shù)據(jù)加密衷戈,數(shù)據(jù)隔離,隱私保護(hù)层坠,企業(yè)管理的應(yīng)用系統(tǒng)殖妇。對(duì)于內(nèi)部App的完全控制能力
VA對(duì)于內(nèi)部的App具有完全的監(jiān)控和控制能力,這點(diǎn)在未Root的外部環(huán)境中是絕對(duì)無(wú)法實(shí)現(xiàn)的窿春。
運(yùn)行機(jī)制
首先拉一,我們來(lái)看一下它在開啟APP后的進(jìn)程信息采盒。
USER PID PPID VSZ RSS WCHAN ADDR S NAME
u0_a645 435 501 2125236 285608 0 0 S com.duowan.kiwi:yyPushService
u0_a645 24705 501 1875532 22620 0 0 S io.virtualapp
u0_a645 24761 501 1831868 22728 0 0 S io.virtualapp:x
u0_a645 26243 501 2770772 147752 0 0 S com.duowan.kiwi
可以看到,所有被ViralApp打開的應(yīng)用蔚润,都和VirtalApp屬于同一個(gè)uid:u0_a645磅氨。其中,VirtualApp本身有兩個(gè)進(jìn)程:io.virtualapp 和 io.virtualapp:x
- io.virtualapp 就是可見的交互界面嫡纠,同時(shí)也負(fù)責(zé)APK包的管理和安裝烦租。
- io.virtualapp:x 作為一個(gè)單獨(dú)的服務(wù)進(jìn)程,虛擬了一些系統(tǒng)服務(wù)除盏。
以這里安裝的虎牙直播為例叉橱,查看一下它的進(jìn)程的內(nèi)存空間,可以看到相關(guān)路徑全都被映射到了/data/data/io.virtualapp/virtual下面者蠕。
7f8a8000-7f8a9000 rw-p 00095000 b3:1c 261396 /data/data/io.virtualapp/virtual/data/app/com.duowan.kiwi/lib/libtrustdevice.so
8393a000-83980000 r--s 00000000 b3:1c 263573 /data/data/io.virtualapp/virtual/data/user/0/com.duowan.kiwi/app_tbs/core_share/libresources.so
840fe000-840ff000 rw-p 00027000 b3:1c 261344 /data/data/io.virtualapp/virtual/data/app/com.duowan.kiwi/lib/libjscexecutor.so
86905000-8691e000 rw-p 0256f000 b3:1c 263574 /data/data/io.virtualapp/virtual/data/user/0/com.duowan.kiwi/app_tbs/core_share/libmttwebview.so
86c10000-86c13000 r-xp 00000000 b3:1c 263579 /data/data/io.virtualapp/virtual/data/user/0/com.duowan.kiwi/app_tbs/core_share/libqb_keystore.so
86d3e000-86d56000 r-xp 00000000 b3:1c 261313 /data/data/io.virtualapp/virtual/data/app/com.duowan.kiwi/lib/libsecurityenv.so
8704d000-87050000 r-xp 00000000 b3:1c 263567 /data/data/io.virtualapp/virtual/data/user/0/com.duowan.kiwi/app_tbs/core_share/libmttwebview_plat_support.so
87375000-87421000 r-xp 00000000 b3:1c 261301 /data/data/io.virtualapp/virtual/data/app/com.duowan.kiwi/lib/libgnustl_shared.so
87531000-87c29000 r-xp 00000000 b3:1c 261321 /data/data/io.virtualapp/virtual/data/app/com.duowan.kiwi/lib/libjsc.so
88386000-883a1000 r--p 00000000 b3:1c 263621 /data/data/io.virtualapp/virtual/data/user/0/com.duowan.kiwi/app_tbs/core_share/asr_base_dex.dex
89e4f000-8a6cb000 r--p 00000000 b3:1c 263600 /data/data/io.virtualapp/virtual/data/user/0/com.duowan.kiwi/app_tbs/core_share/tbs_jars_fusion_dex.dex
可見窃祝,這里面對(duì)路徑做過(guò)了重新映射。
注入邏輯
要想實(shí)現(xiàn)對(duì)一個(gè)APP的虛擬化踱侣,就是不直接把APP安裝進(jìn)系統(tǒng)粪小,同時(shí)又要提供APP運(yùn)行過(guò)程中所需的一切,從而可以讓它誤以為自己是運(yùn)行在正常系統(tǒng)中抡句。這里就需要實(shí)現(xiàn)系統(tǒng)服務(wù)的虛擬化和相關(guān)路徑的虛擬化探膊。
其中,系統(tǒng)服務(wù)的虛擬化主要靠注入大量framework組件來(lái)實(shí)現(xiàn)的待榔。
@VirtualApp/lib/src/main/java/com/lody/virtual/client/core/InvocationStubManager.java
private void injectInternal() throws Throwable {
if (VirtualCore.get().isMainProcess()) {
return;
}
if (VirtualCore.get().isServerProcess()) {
addInjector(new ActivityManagerStub());
addInjector(new PackageManagerStub());
return;
}
if (VirtualCore.get().isVAppProcess()) {
addInjector(new LibCoreStub());
addInjector(new ActivityManagerStub());
addInjector(new PackageManagerStub());
addInjector(HCallbackStub.getDefault());
addInjector(new ISmsStub());
addInjector(new ISubStub());
addInjector(new DropBoxManagerStub());
addInjector(new NotificationManagerStub());
addInjector(new LocationManagerStub());
addInjector(new WindowManagerStub());
addInjector(new ClipBoardStub());
addInjector(new MountServiceStub());
addInjector(new BackupManagerStub());
addInjector(new TelephonyStub());
addInjector(new TelephonyRegistryStub());
addInjector(new PhoneSubInfoStub());
addInjector(new PowerManagerStub());
addInjector(new AppWidgetManagerStub());
addInjector(new AccountManagerStub());
addInjector(new AudioManagerStub());
addInjector(new SearchManagerStub());
addInjector(new ContentServiceStub());
addInjector(new ConnectivityStub());
if (Build.VERSION.SDK_INT >= JELLY_BEAN_MR2) {
addInjector(new VibratorStub());
addInjector(new WifiManagerStub());
addInjector(new BluetoothStub());
addInjector(new ContextHubServiceStub());
}
if (Build.VERSION.SDK_INT >= JELLY_BEAN_MR1) {
addInjector(new UserManagerStub());
}
if (Build.VERSION.SDK_INT >= JELLY_BEAN_MR1) {
addInjector(new DisplayStub());
}
if (Build.VERSION.SDK_INT >= LOLLIPOP) {
addInjector(new PersistentDataBlockServiceStub());
addInjector(new InputMethodManagerStub());
addInjector(new MmsStub());
addInjector(new SessionManagerStub());
addInjector(new JobServiceStub());
addInjector(new RestrictionStub());
}
if (Build.VERSION.SDK_INT >= KITKAT) {
addInjector(new AlarmManagerStub());
addInjector(new AppOpsManagerStub());
addInjector(new MediaRouterServiceStub());
}
if (Build.VERSION.SDK_INT >= LOLLIPOP_MR1) {
addInjector(new GraphicsStatsStub());
}
if (Build.VERSION.SDK_INT >= M) {
addInjector(new NetworkManagementStub());
}
if (Build.VERSION.SDK_INT >= N) {
addInjector(new WifiScannerStub());
addInjector(new ShortcutServiceStub());
}
}
}
這個(gè)注入過(guò)程是發(fā)生在io.virtualapp.VApp.attachBaseContext中逞壁,因此,每次啟動(dòng)一個(gè)子進(jìn)程都會(huì)執(zhí)行到這里锐锣,這會(huì)區(qū)分是isMainProcess(io.virtualapp)或者isServerProcess(io.virtualapp:x)或者isVAppProcess(被安裝APP)來(lái)進(jìn)行不同的注入腌闯,可以看到,注入最多的還是在被安裝APP的進(jìn)程中刺下。
可以看到绑嘹,之前在injectInternal 中addInjector的所有Stub都會(huì)調(diào)用它的inject方法。
VirtualApp/lib/src/main/java/com/lody/virtual/client/core/InvocationStubManager.java
void injectAll() throws Throwable {
for (IInjector injector : mInjectors.values()) {
injector.inject();
}
// XXX: Lazy inject the Instrumentation,
addInjector(AppInstrumentation.getDefault());
}
由此實(shí)現(xiàn)對(duì)各個(gè)系統(tǒng)類的替換橘茉。
而在底層工腋,VirtualApp還實(shí)現(xiàn)了對(duì)原本路徑的替換,在java層傳入需要重定向的所有路徑畅卓。
private void startIOUniformer() {
ApplicationInfo info = mBoundApplication.appInfo;
int userId = VUserHandle.myUserId();
String wifiMacAddressFile = deviceInfo.getWifiFile(userId).getPath();
NativeEngine.redirectDirectory("/sys/class/net/wlan0/address", wifiMacAddressFile);
NativeEngine.redirectDirectory("/sys/class/net/eth0/address", wifiMacAddressFile);
NativeEngine.redirectDirectory("/sys/class/net/wifi/address", wifiMacAddressFile);
NativeEngine.redirectDirectory("/data/data/" + info.packageName, info.dataDir);
NativeEngine.redirectDirectory("/data/user/0/" + info.packageName, info.dataDir);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
NativeEngine.redirectDirectory("/data/user_de/0/" + info.packageName, info.dataDir);
}
String libPath = new File(VEnvironment.getDataAppPackageDirectory(info.packageName), "lib").getAbsolutePath();
String userLibPath = new File(VEnvironment.getUserSystemDirectory(userId), "lib").getAbsolutePath();
NativeEngine.redirectDirectory(userLibPath, libPath);
NativeEngine.redirectDirectory("/data/data/" + info.packageName + "/lib/", libPath);
NativeEngine.redirectDirectory("/data/user/0/" + info.packageName + "/lib/", libPath);
NativeEngine.readOnly(VEnvironment.getDataAppDirectory().getPath());
VirtualStorageManager vsManager = VirtualStorageManager.get();
String vsPath = vsManager.getVirtualStorage(info.packageName, userId);
boolean enable = vsManager.isVirtualStorageEnable(info.packageName, userId);
if (enable && vsPath != null) {
File vsDirectory = new File(vsPath);
if (vsDirectory.exists() || vsDirectory.mkdirs()) {
HashSet<String> mountPoints = getMountPoints();
for (String mountPoint : mountPoints) {
NativeEngine.redirectDirectory(mountPoint, vsPath);
}
}
}
NativeEngine.hook();
}
這些路徑最終會(huì)添加進(jìn)JNI層的一個(gè)映射表中
void IOUniformer::redirect(const char *orig_path, const char *new_path) {
LOGI("Start Java_nativeRedirect : from %s to %s", orig_path, new_path);
add_pair(orig_path, new_path);
}
static void add_pair(const char *_orig_path, const char *_new_path) {
std::string origPath = std::string(_orig_path);
std::string newPath = std::string(_new_path);
IORedirectMap.insert(std::pair<std::string, std::string>(origPath, newPath));
if (endWith(origPath, '/')) {
RootIORedirectMap.insert(
std::pair<std::string, std::string>(
origPath.substr(0, origPath.length() - 1),
newPath.substr(0, newPath.length() - 1))
);
}
}
然后擅腰,會(huì)hook所有的c庫(kù)函數(shù),這些函數(shù)在調(diào)用的時(shí)候翁潘,就會(huì)替換路徑為新路徑趁冈。由于hook的是libc的函數(shù),java層和虛擬機(jī)的文件訪問(wèn)最終也會(huì)調(diào)用到這里,從而受到影響渗勘。
void IOUniformer::startUniformer(int api_level, int preview_api_level) {
gVars.hooked_process = true;
HOOK_SYMBOL(RTLD_DEFAULT, vfork);
HOOK_SYMBOL(RTLD_DEFAULT, kill);
HOOK_SYMBOL(RTLD_DEFAULT, __getcwd);
HOOK_SYMBOL(RTLD_DEFAULT, truncate);
HOOK_SYMBOL(RTLD_DEFAULT, __statfs64);
HOOK_SYMBOL(RTLD_DEFAULT, execve);
HOOK_SYMBOL(RTLD_DEFAULT, __open);
if ((api_level < 25) || (api_level == 25 && preview_api_level == 0)) {
HOOK_SYMBOL(RTLD_DEFAULT, utimes);
HOOK_SYMBOL(RTLD_DEFAULT, mkdir);
HOOK_SYMBOL(RTLD_DEFAULT, chmod);
HOOK_SYMBOL(RTLD_DEFAULT, lstat);
HOOK_SYMBOL(RTLD_DEFAULT, link);
HOOK_SYMBOL(RTLD_DEFAULT, symlink);
HOOK_SYMBOL(RTLD_DEFAULT, mknod);
HOOK_SYMBOL(RTLD_DEFAULT, rmdir);
HOOK_SYMBOL(RTLD_DEFAULT, chown);
HOOK_SYMBOL(RTLD_DEFAULT, rename);
HOOK_SYMBOL(RTLD_DEFAULT, stat);
HOOK_SYMBOL(RTLD_DEFAULT, chdir);
HOOK_SYMBOL(RTLD_DEFAULT, access);
HOOK_SYMBOL(RTLD_DEFAULT, readlink);
HOOK_SYMBOL(RTLD_DEFAULT, unlink);
}
HOOK_SYMBOL(RTLD_DEFAULT, fstatat);
HOOK_SYMBOL(RTLD_DEFAULT, fchmodat);
HOOK_SYMBOL(RTLD_DEFAULT, symlinkat);
HOOK_SYMBOL(RTLD_DEFAULT, readlinkat);
HOOK_SYMBOL(RTLD_DEFAULT, unlinkat);
HOOK_SYMBOL(RTLD_DEFAULT, linkat);
HOOK_SYMBOL(RTLD_DEFAULT, utimensat);
HOOK_SYMBOL(RTLD_DEFAULT, __openat);
HOOK_SYMBOL(RTLD_DEFAULT, faccessat);
HOOK_SYMBOL(RTLD_DEFAULT, mkdirat);
HOOK_SYMBOL(RTLD_DEFAULT, renameat);
HOOK_SYMBOL(RTLD_DEFAULT, fchownat);
HOOK_SYMBOL(RTLD_DEFAULT, mknodat);
// hook_dlopen(api_level);
#if defined(__i386__) || defined(__x86_64__)
// Do nothing
#else
GodinHook::NativeHook::hookAllRegistered();
#endif
}
以chmod函數(shù)為例
// int chmod(const char *path, mode_t mode);
HOOK_DEF(int, chmod, const char *pathname, mode_t mode) {
const char *redirect_path = match_redirected_path(pathname);
if (isReadOnlyPath(redirect_path)) {
return -1;
}
int ret = syscall(__NR_chmod, redirect_path, mode);
FREE(redirect_path, pathname);
return ret;
}
可以看到沐绒,它會(huì)把原先的pathname,通過(guò)match_redirected_path找到映射后的新路徑旺坠,然后用syscall來(lái)調(diào)用它乔遮,這樣就實(shí)現(xiàn)了所有路徑的重定向。
運(yùn)行時(shí)結(jié)構(gòu)
VA 參照原生系統(tǒng) framework 仿造了一套 framework service取刃,還有配套在 client 端的 framework 庫(kù)蹋肮。
-
系統(tǒng)原生的 framework 運(yùn)作方式
簡(jiǎn)單來(lái)說(shuō),我們平時(shí)所用到的 app 運(yùn)行空間中的 framework api 最終會(huì)通過(guò) Binder 遠(yuǎn)程調(diào)用到 framework service 空間的遠(yuǎn)程服務(wù)璧疗。
而遠(yuǎn)程服務(wù)類似 AMS 中的 Recoder 中會(huì)持有 app 空間的 Ibinder token 句柄坯辩,通過(guò) token 也可以讓 framework service 遠(yuǎn)程調(diào)用到 app 空間。
-
VA 環(huán)境下framework 運(yùn)作方式
而在 VA 環(huán)境下崩侠,情況其實(shí)也是類似漆魔,只不過(guò)在 framework service 和 client app 之間還有另外一個(gè) VA 實(shí)現(xiàn)的 VAService,VAService 仿造了 framework service 的一些功能却音。
因?yàn)樵?VA 中運(yùn)行的 Client App 都是沒有(也不能注冊(cè))在 framework service 的有送,注冊(cè)的只有 VA 預(yù)先注冊(cè)在 Menifest 中的 Stub 而已。所以 frameservice 是無(wú)法像普通 App 一樣管理 VA Client App 的會(huì)話的僧家。
這就要依靠 VA 仿造的另外一套 VAService 完成對(duì) VA 中 Client App 的會(huì)話管理了。
VA初始化
先看一下代碼:
VirtualCore.startup
public void startup(Context context) throws Throwable {
if (!isStartUp) {
// 確保 MainThread
if (Looper.myLooper() != Looper.getMainLooper()) {
throw new IllegalStateException("VirtualCore.startup() must called in main thread.");
}
VASettings.STUB_CP_AUTHORITY = context.getPackageName() + "." + VASettings.STUB_DEF_AUTHORITY;
ServiceManagerNative.SERVICE_CP_AUTH = context.getPackageName() + "." + ServiceManagerNative.SERVICE_DEF_AUTH;
this.context = context;
// 獲取 ActivityThread 實(shí)例
mainThread = ActivityThread.currentActivityThread.call();
unHookPackageManager = context.getPackageManager();
hostPkgInfo = unHookPackageManager.getPackageInfo(context.getPackageName(), PackageManager.GET_PROVIDERS);
detectProcessType();
// hook 系統(tǒng)類
InvocationStubManager invocationStubManager = InvocationStubManager.getInstance();
invocationStubManager.init();
invocationStubManager.injectAll();
// 修復(fù)權(quán)限管理
ContextFixer.fixContext(context);
isStartUp = true;
if (initLock != null) {
initLock.open();
initLock = null;
}
}
}
InvocationStubManager.injectInternal
主要完成對(duì) Java 層 framework 的 Hook裸删,將其定位到 VA 偽造 VA framework 上去八拱。
private void injectInternal() throws Throwable {
// VA 自身的 App 進(jìn)程不需要 Hook
if (VirtualCore.get().isMainProcess()) {
return;
}
// VAService 需要 Hook AMS 和 PMS
if (VirtualCore.get().isServerProcess()) {
addInjector(new ActivityManagerStub());
addInjector(new PackageManagerStub());
return;
}
// Client APP 需要 Hook 整個(gè) framework,來(lái)使其調(diào)用到 VA framework
if (VirtualCore.get().isVAppProcess()) {
addInjector(new LibCoreStub());
addInjector(new ActivityManagerStub());
addInjector(new PackageManagerStub());
addInjector(HCallbackStub.getDefault());
addInjector(new ISmsStub());
addInjector(new ISubStub());
addInjector(new DropBoxManagerStub());
.....................
}
}
Client App 的安裝
VirtualCore.installPackage
public InstallResult installPackage(String apkPath, int flags) {
try {
// 調(diào)用遠(yuǎn)程 VAService
return getService().installPackage(apkPath, flags);
} catch (RemoteException e) {
return VirtualRuntime.crash(e);
}
}
最終調(diào)用 VAServcie 中的 VAppManagerService.installPackage
public synchronized InstallResult installPackage(String path, int flags, boolean notify) {
long installTime = System.currentTimeMillis();
if (path == null) {
return InstallResult.makeFailure("path = NULL");
}
// 是否 OPT 優(yōu)化(dex -> binary)
boolean skipDexOpt = (flags & InstallStrategy.SKIP_DEX_OPT) != 0;
// apk path
File packageFile = new File(path);
if (!packageFile.exists() || !packageFile.isFile()) {
return InstallResult.makeFailure("Package File is not exist.");
}
VPackage pkg = null;
try {
// 進(jìn)入解析包結(jié)構(gòu)涯塔,該結(jié)構(gòu)是可序列化的肌稻,為了持久化在磁盤上
pkg = PackageParserEx.parsePackage(packageFile);
} catch (Throwable e) {
e.printStackTrace();
}
if (pkg == null || pkg.packageName == null) {
return InstallResult.makeFailure("Unable to parse the package.");
}
InstallResult res = new InstallResult();
res.packageName = pkg.packageName;
// PackageCache holds all packages, try to check if we need to update.
VPackage existOne = PackageCacheManager.get(pkg.packageName);
PackageSetting existSetting = existOne != null ? (PackageSetting) existOne.mExtras : null;
if (existOne != null) {
if ((flags & InstallStrategy.IGNORE_NEW_VERSION) != 0) {
res.isUpdate = true;
return res;
}
if (!canUpdate(existOne, pkg, flags)) {
return InstallResult.makeFailure("Not allowed to update the package.");
}
res.isUpdate = true;
}
// 獲得 app 安裝文件夾
File appDir = VEnvironment.getDataAppPackageDirectory(pkg.packageName);
// so 文件夾
File libDir = new File(appDir, "lib");
if (res.isUpdate) {
FileUtils.deleteDir(libDir);
VEnvironment.getOdexFile(pkg.packageName).delete();
VActivityManagerService.get().killAppByPkg(pkg.packageName, VUserHandle.USER_ALL);
}
if (!libDir.exists() && !libDir.mkdirs()) {
return InstallResult.makeFailure("Unable to create lib dir.");
}
// 是否基于系統(tǒng)的 apk 加載,前提是安裝過(guò)的 apk 并且 dependSystem 開關(guān)打開
boolean dependSystem = (flags & InstallStrategy.DEPEND_SYSTEM_IF_EXIST) != 0
&& VirtualCore.get().isOutsideInstalled(pkg.packageName);
if (existSetting != null && existSetting.dependSystem) {
dependSystem = false;
}
// 復(fù)制 so 到 sandbox lib
NativeLibraryHelperCompat.copyNativeBinaries(new File(path), libDir);
// 如果不基于系統(tǒng)匕荸,一些必要的拷貝工作
if (!dependSystem) {
File privatePackageFile = new File(appDir, "base.apk");
File parentFolder = privatePackageFile.getParentFile();
if (!parentFolder.exists() && !parentFolder.mkdirs()) {
VLog.w(TAG, "Warning: unable to create folder : " + privatePackageFile.getPath());
} else if (privatePackageFile.exists() && !privatePackageFile.delete()) {
VLog.w(TAG, "Warning: unable to delete file : " + privatePackageFile.getPath());
}
try {
FileUtils.copyFile(packageFile, privatePackageFile);
} catch (IOException e) {
privatePackageFile.delete();
return InstallResult.makeFailure("Unable to copy the package file.");
}
packageFile = privatePackageFile;
}
if (existOne != null) {
PackageCacheManager.remove(pkg.packageName);
}
// 給上可執(zhí)行權(quán)限爹谭,5.0 之后在 SD 卡上執(zhí)行 bin 需要可執(zhí)行權(quán)限
chmodPackageDictionary(packageFile);
// PackageSetting 的一些配置,后面會(huì)序列化在磁盤上
PackageSetting ps;
if (existSetting != null) {
ps = existSetting;
} else {
ps = new PackageSetting();
}
ps.skipDexOpt = skipDexOpt;
ps.dependSystem = dependSystem;
ps.apkPath = packageFile.getPath();
ps.libPath = libDir.getPath();
ps.packageName = pkg.packageName;
ps.appId = VUserHandle.getAppId(mUidSystem.getOrCreateUid(pkg));
if (res.isUpdate) {
ps.lastUpdateTime = installTime;
} else {
ps.firstInstallTime = installTime;
ps.lastUpdateTime = installTime;
for (int userId : VUserManagerService.get().getUserIds()) {
boolean installed = userId == 0;
ps.setUserState(userId, false/*launched*/, false/*hidden*/, installed);
}
}
//保存 VPackage Cache 到 Disk
PackageParserEx.savePackageCache(pkg);
//保存到 RamCache
PackageCacheManager.put(pkg, ps);
mPersistenceLayer.save();
BroadcastSystem.get().startApp(pkg);
//發(fā)送通知 安裝完成
if (notify) {
notifyAppInstalled(ps, -1);
}
res.isSuccess = true;
return res;
}
APk 的安裝主要完成以下幾件事情:
- 解析 menifest 拿到 apk 內(nèi)部信息榛搔,包括組件信息诺凡,權(quán)限信息等。并將這些信息序列化到磁盤和內(nèi)存中践惑,以備打開時(shí)調(diào)用腹泌。
- 準(zhǔn)備 App 在 VA 沙箱環(huán)境中的私有空間,并且復(fù)制一些必要的 apk 和 so libs尔觉。
- 最后通知前臺(tái)安裝完成凉袱。
VPackage
public class VPackage implements Parcelable {
public static final Creator<VPackage> CREATOR = new Creator<VPackage>() {
@Override
public VPackage createFromParcel(Parcel source) {
return new VPackage(source);
}
@Override
public VPackage[] newArray(int size) {
return new VPackage[size];
}
};
public ArrayList<ActivityComponent> activities;
public ArrayList<ActivityComponent> receivers;
public ArrayList<ProviderComponent> providers;
public ArrayList<ServiceComponent> services;
public ArrayList<InstrumentationComponent> instrumentation;
public ArrayList<PermissionComponent> permissions;
public ArrayList<PermissionGroupComponent> permissionGroups;
public ArrayList<String> requestedPermissions;
public ArrayList<String> protectedBroadcasts;
public ApplicationInfo applicationInfo;
public Signature[] mSignatures;
public Bundle mAppMetaData;
public String packageName;
public int mPreferredOrder;
public String mVersionName;
public String mSharedUserId;
public ArrayList<String> usesLibraries;
public int mVersionCode;
public int mSharedUserLabel;
// Applications hardware preferences
public ArrayList<ConfigurationInfo> configPreferences = null;
// Applications requested features
public ArrayList<FeatureInfo> reqFeatures = null;
public Object mExtras;
..........................
可以看到 VPackage 幾乎保存了 apk 中所有的關(guān)鍵信息,尤其是組件的數(shù)據(jù)結(jié)構(gòu)會(huì)在 app 在 VA 中運(yùn)行的時(shí)候給 VAMS,VPMS 這些 VAService 提供 apk 的組件信息专甩。
Client App 啟動(dòng)
首先要了解的是 Android App 是組件化的钟鸵,Apk 其實(shí)是 N 多個(gè)組件的集合,以及一些資源文件和 Assert涤躲,App 的啟動(dòng)有多種情況棺耍,只要在一個(gè)新的進(jìn)程中調(diào)起了 apk 中任何一個(gè)組件,App 將被初始化篓叶,Application 將被初始化烈掠。
Activity 啟動(dòng)
Hook startActivity(重定位 Intent 到 StubActivity)
首先在 Client App 中,startActivity 方法必須被 Hook 掉缸托,不然 Client App 調(diào)用 startActivity 就直指外部 Activity 去了左敌。
這部分的原理其實(shí)與 DroidPlugin 大同小異,由于插件(Client App)中的 Activity 是沒有在 AMS 中注冊(cè)的俐镐,AMS 自然無(wú)法找到我們的插件 Activity矫限。
Hook 的目的是我們拿到用戶的 Intent,把他替換成指向 VA 在 Menifest 中站好坑的 StubActivity 的 Intent佩抹,然后將原 Intent 當(dāng)作 data 打包進(jìn)新 Intent 以便以后流程再次進(jìn)入 VA 時(shí)恢復(fù)叼风。
Hook 的方法就是用我們動(dòng)態(tài)代理生成的代理類對(duì)象替換系統(tǒng)原來(lái)的 ActiityManagerNative.geDefault 對(duì)象。
public void inject() throws Throwable {
if (BuildCompat.isOreo()) {
//Android Oreo(8.X)
Object singleton = ActivityManagerOreo.IActivityManagerSingleton.get();
Singleton.mInstance.set(singleton, getInvocationStub().getProxyInterface());
} else {
if (ActivityManagerNative.gDefault.type() == IActivityManager.TYPE) {
ActivityManagerNative.gDefault.set(getInvocationStub().getProxyInterface());
} else if (ActivityManagerNative.gDefault.type() == Singleton.TYPE) {
Object gDefault = ActivityManagerNative.gDefault.get();
Singleton.mInstance.set(gDefault, getInvocationStub().getProxyInterface());
}
}
BinderInvocationStub hookAMBinder = new BinderInvocationStub(getInvocationStub().getBaseInterface());
hookAMBinder.copyMethodProxies(getInvocationStub());
ServiceManager.sCache.get().put(Context.ACTIVITY_SERVICE, hookAMBinder);
}
好了棍苹,下面只要調(diào)用到 startActivity 就會(huì)被 Hook 到 call无宿。
這個(gè)函數(shù)需要注意以下幾點(diǎn):
- VA 有意將安裝和卸載 APP 的請(qǐng)求重定向到了卸載 VA 內(nèi)部 APK 的邏輯。
- resolveActivityInfo 調(diào)用到了 VPM 的 resolveIntent枢里,最終會(huì)遠(yuǎn)程調(diào)用到 VPMS 的 resolveIntent孽鸡,然后 VPMS 就會(huì)去查詢 VPackage 找到目標(biāo) Activity 并將信息附加在 ResolveInfo 中返回 VPM。
- 最后也是最重要的一點(diǎn)栏豺,startActivity 會(huì)調(diào)用到 VAM.startActivity,同樣最終會(huì)遠(yuǎn)程調(diào)用到 VAMS 的 startActivity彬碱。
static class StartActivity extends MethodProxy {
private static final String SCHEME_FILE = "file";
private static final String SCHEME_PACKAGE = "package";
@Override
public String getMethodName() {
return "startActivity";
}
@Override
public Object call(Object who, Method method, Object... args) throws Throwable {
int intentIndex = ArrayUtils.indexOfObject(args, Intent.class, 1);
if (intentIndex < 0) {
return ActivityManagerCompat.START_INTENT_NOT_RESOLVED;
}
int resultToIndex = ArrayUtils.indexOfObject(args, IBinder.class, 2);
String resolvedType = (String) args[intentIndex + 1];
Intent intent = (Intent) args[intentIndex];
intent.setDataAndType(intent.getData(), resolvedType);
IBinder resultTo = resultToIndex >= 0 ? (IBinder) args[resultToIndex] : null;
int userId = VUserHandle.myUserId();
if (ComponentUtils.isStubComponent(intent)) {
return method.invoke(who, args);
}
// 請(qǐng)求安裝和卸載界面
if (Intent.ACTION_INSTALL_PACKAGE.equals(intent.getAction())
|| (Intent.ACTION_VIEW.equals(intent.getAction())
&& "application/vnd.android.package-archive".equals(intent.getType()))) {
if (handleInstallRequest(intent)) {
return 0;
}
} else if ((Intent.ACTION_UNINSTALL_PACKAGE.equals(intent.getAction())
|| Intent.ACTION_DELETE.equals(intent.getAction()))
&& "package".equals(intent.getScheme())) {
if (handleUninstallRequest(intent)) {
return 0;
}
}
String resultWho = null;
int requestCode = 0;
Bundle options = ArrayUtils.getFirst(args, Bundle.class);
if (resultTo != null) {
resultWho = (String) args[resultToIndex + 1];
requestCode = (int) args[resultToIndex + 2];
}
// chooser 調(diào)用選擇界面
if (ChooserActivity.check(intent)) {
intent.setComponent(new ComponentName(getHostContext(), ChooserActivity.class));
intent.putExtra(Constants.EXTRA_USER_HANDLE, userId);
intent.putExtra(ChooserActivity.EXTRA_DATA, options);
intent.putExtra(ChooserActivity.EXTRA_WHO, resultWho);
intent.putExtra(ChooserActivity.EXTRA_REQUEST_CODE, requestCode);
return method.invoke(who, args);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
args[intentIndex - 1] = getHostPkg();
}
//解析 ActivityInfo
ActivityInfo activityInfo = VirtualCore.get().resolveActivityInfo(intent, userId);
if (activityInfo == null) {
VLog.e("VActivityManager", "Unable to resolve activityInfo : " + intent);
if (intent.getPackage() != null && isAppPkg(intent.getPackage())) {
return ActivityManagerCompat.START_INTENT_NOT_RESOLVED;
}
return method.invoke(who, args);
}
// 調(diào)用遠(yuǎn)程 VAMS.startActivity
int res = VActivityManager.get().startActivity(intent, activityInfo, resultTo, options, resultWho, requestCode, VUserHandle.myUserId());
if (res != 0 && resultTo != null && requestCode > 0) {
VActivityManager.get().sendActivityResult(resultTo, resultWho, requestCode);
}
// 處理 Activity 切換動(dòng)畫,因?yàn)榇藭r(shí)動(dòng)畫還是 Host 的 Stub Activity 默認(rèn)動(dòng)畫奥洼,需要覆蓋成子程序包的動(dòng)畫
if (resultTo != null) {
ActivityClientRecord r = VActivityManager.get().getActivityRecord(resultTo);
if (r != null && r.activity != null) {
try {
TypedValue out = new TypedValue();
Resources.Theme theme = r.activity.getResources().newTheme();
theme.applyStyle(activityInfo.getThemeResource(), true);
if (theme.resolveAttribute(android.R.attr.windowAnimationStyle, out, true)) {
TypedArray array = theme.obtainStyledAttributes(out.data,
new int[]{
android.R.attr.activityOpenEnterAnimation,
android.R.attr.activityOpenExitAnimation
});
r.activity.overridePendingTransition(array.getResourceId(0, 0), array.getResourceId(1, 0));
array.recycle();
}
} catch (Throwable e) {
// Ignore
}
}
}
return res;
}
private boolean handleInstallRequest(Intent intent) {
IAppRequestListener listener = VirtualCore.get().getAppRequestListener();
if (listener != null) {
Uri packageUri = intent.getData();
if (SCHEME_FILE.equals(packageUri.getScheme())) {
File sourceFile = new File(packageUri.getPath());
try {
listener.onRequestInstall(sourceFile.getPath());
return true;
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
return false;
}
private boolean handleUninstallRequest(Intent intent) {
IAppRequestListener listener = VirtualCore.get().getAppRequestListener();
if (listener != null) {
Uri packageUri = intent.getData();
if (SCHEME_PACKAGE.equals(packageUri.getScheme())) {
String pkg = packageUri.getSchemeSpecificPart();
try {
listener.onRequestUninstall(pkg);
return true;
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
return false;
}
}
邏輯最終走到 VAMS 后巷疼,VAMS 調(diào)用 ActivityStack.startActivityLocked
// 參考 framework 的實(shí)現(xiàn)
int startActivityLocked(int userId, Intent intent, ActivityInfo info, IBinder resultTo, Bundle options,
String resultWho, int requestCode) {
optimizeTasksLocked();
Intent destIntent;
ActivityRecord sourceRecord = findActivityByToken(userId, resultTo);
TaskRecord sourceTask = sourceRecord != null ? sourceRecord.task : null;
// 忽略一大堆對(duì) Flag 的處理
.............................
String affinity = ComponentUtils.getTaskAffinity(info);
// 根據(jù) Flag 尋找合適的 Task
TaskRecord reuseTask = null;
switch (reuseTarget) {
case AFFINITY:
reuseTask = findTaskByAffinityLocked(userId, affinity);
break;
case DOCUMENT:
reuseTask = findTaskByIntentLocked(userId, intent);
break;
case CURRENT:
reuseTask = sourceTask;
break;
default:
break;
}
boolean taskMarked = false;
if (reuseTask == null) {
startActivityInNewTaskLocked(userId, intent, info, options);
} else {
boolean delivered = false;
mAM.moveTaskToFront(reuseTask.taskId, 0);
boolean startTaskToFront = !clearTask && !clearTop && ComponentUtils.isSameIntent(intent, reuseTask.taskRoot);
if (clearTarget.deliverIntent || singleTop) {
taskMarked = markTaskByClearTarget(reuseTask, clearTarget, intent.getComponent());
ActivityRecord topRecord = topActivityInTask(reuseTask);
if (clearTop && !singleTop && topRecord != null && taskMarked) {
topRecord.marked = true;
}
// Target activity is on top
if (topRecord != null && !topRecord.marked && topRecord.component.equals(intent.getComponent())) {
deliverNewIntentLocked(sourceRecord, topRecord, intent);
delivered = true;
}
}
if (taskMarked) {
synchronized (mHistory) {
scheduleFinishMarkedActivityLocked();
}
}
if (!startTaskToFront) {
if (!delivered) {
destIntent = startActivityProcess(userId, sourceRecord, intent, info);
if (destIntent != null) {
startActivityFromSourceTask(reuseTask, destIntent, info, resultWho, requestCode, options);
}
}
}
}
return 0;
}
然后 call 到了 startActivityProcess ,這就是真正替換 Intent 的地方
private Intent startActivityProcess(int userId, ActivityRecord sourceRecord, Intent intent, ActivityInfo info) {
intent = new Intent(intent);
// 獲得 Activity 對(duì)應(yīng)的 ProcessRecorder灵奖,如果沒有則表示這是 Process 第一個(gè)打開的組件嚼沿,需要初始化 Application
ProcessRecord targetApp = mService.startProcessIfNeedLocked(info.processName, userId, info.packageName);
if (targetApp == null) {
return null;
}
Intent targetIntent = new Intent();
// 根據(jù) Client App 的 PID 獲取 StubActivity
String stubActivityPath = fetchStubActivity(targetApp.vpid, info);
Log.e("gy", "map activity:" + intent.getComponent().getClassName() + " -> " + stubActivityPath);
targetIntent.setClassName(VirtualCore.get().getHostPkg(), stubActivityPath);
ComponentName component = intent.getComponent();
if (component == null) {
component = ComponentUtils.toComponentName(info);
}
targetIntent.setType(component.flattenToString());
StubActivityRecord saveInstance = new StubActivityRecord(intent, info,
sourceRecord != null ? sourceRecord.component : null, userId);
saveInstance.saveToIntent(targetIntent);
return targetIntent;
}
fetchStubActivity 會(huì)根據(jù)相同的進(jìn)程 id 在 VA 的 Menifest 中找到那個(gè)提前占坑的 StubActivity
private String fetchStubActivity(int vpid, ActivityInfo targetInfo) {
boolean isFloating = false;
boolean isTranslucent = false;
boolean showWallpaper = false;
try {
int[] R_Styleable_Window = R_Hide.styleable.Window.get();
int R_Styleable_Window_windowIsTranslucent = R_Hide.styleable.Window_windowIsTranslucent.get();
int R_Styleable_Window_windowIsFloating = R_Hide.styleable.Window_windowIsFloating.get();
int R_Styleable_Window_windowShowWallpaper = R_Hide.styleable.Window_windowShowWallpaper.get();
AttributeCache.Entry ent = AttributeCache.instance().get(targetInfo.packageName, targetInfo.theme,
R_Styleable_Window);
if (ent != null && ent.array != null) {
showWallpaper = ent.array.getBoolean(R_Styleable_Window_windowShowWallpaper, false);
isTranslucent = ent.array.getBoolean(R_Styleable_Window_windowIsTranslucent, false);
isFloating = ent.array.getBoolean(R_Styleable_Window_windowIsFloating, false);
}
} catch (Throwable e) {
e.printStackTrace();
}
boolean isDialogStyle = isFloating || isTranslucent || showWallpaper;
// 根據(jù)在 Menifest 中注冊(cè)的 pid
if (isDialogStyle) {
return VASettings.getStubDialogName(vpid);
} else {
return VASettings.getStubActivityName(vpid);
}
}
這里需要特別注意,VA 占坑的方式和 DroidPlugin 有些小不同瓷患,VA 沒有為每個(gè) Process 注冊(cè)多個(gè) Activity伏尼,也沒有為不同的啟動(dòng)方式注冊(cè)多個(gè) Activity,這里確實(shí)是有改進(jìn)的尉尾。
這里根本原因是因?yàn)?VA 對(duì) VAMS 實(shí)現(xiàn)的更為完整爆阶,實(shí)現(xiàn)了原版 AMS 的基本功能,包括完整的 Recorder 管理,Task Stack 管理等辨图,這樣的話 StubActivity 的唯一作用便是攜帶 Client App 真正的 Intent 交給 VAMS 處理班套。這套機(jī)制衍生到其他的組件也是一樣的。
最終, VAMS 調(diào)用原生 AM 的 startActivity 向真正的 AMS 發(fā)送替換成 StubActivity 的偽造 Intent故河。
private void startActivityFromSourceTask(TaskRecord task, Intent intent, ActivityInfo info, String resultWho,
int requestCode, Bundle options) {
ActivityRecord top = task.activities.isEmpty() ? null : task.activities.get(task.activities.size() - 1);
if (top != null) {
if (startActivityProcess(task.userId, top, intent, info) != null) {
realStartActivityLocked(top.token, intent, resultWho, requestCode, options);
}
}
}
private void realStartActivityLocked(IBinder resultTo, Intent intent, String resultWho, int requestCode,
Bundle options) {
Class<?>[] types = mirror.android.app.IActivityManager.startActivity.paramList();
Object[] args = new Object[types.length];
if (types[0] == IApplicationThread.TYPE) {
args[0] = ActivityThread.getApplicationThread.call(VirtualCore.mainThread());
}
int intentIndex = ArrayUtils.protoIndexOf(types, Intent.class);
int resultToIndex = ArrayUtils.protoIndexOf(types, IBinder.class, 2);
int optionsIndex = ArrayUtils.protoIndexOf(types, Bundle.class);
int resolvedTypeIndex = intentIndex + 1;
int resultWhoIndex = resultToIndex + 1;
int requestCodeIndex = resultToIndex + 2;
args[intentIndex] = intent;
args[resultToIndex] = resultTo;
args[resultWhoIndex] = resultWho;
args[requestCodeIndex] = requestCode;
if (optionsIndex != -1) {
args[optionsIndex] = options;
}
args[resolvedTypeIndex] = intent.getType();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
args[intentIndex - 1] = VirtualCore.get().getHostPkg();
}
ClassUtils.fixArgs(types, args);
mirror.android.app.IActivityManager.startActivity.call(ActivityManagerNative.getDefault.call(),
(Object[]) args);
}
恢復(fù)原 Intent 重定向到原 Activity
當(dāng) AMS 收到偽裝的 Intent 后吱韭,就會(huì)找到 StubActivity,這時(shí)流程回到 VA 里的主線程中的消息隊(duì)列中鱼的。
Hook 過(guò)程就是用我們自己的 Handler 替換 android.os.Handler.mCallback 因?yàn)橹骶€程在這里分發(fā)一些操作理盆。
public void inject() throws Throwable {
otherCallback = getHCallback();
mirror.android.os.Handler.mCallback.set(getH(), this);
}
handlerMessage 判斷是 LAUNCH_ACTIVITY Action 后直接調(diào)用了 handlerLaunchActivity 方法,和原版其實(shí)很像凑阶。
private boolean handleLaunchActivity(Message msg) {
Object r = msg.obj;
Intent stubIntent = ActivityThread.ActivityClientRecord.intent.get(r);
// 獲取原版 Intent 信息
StubActivityRecord saveInstance = new StubActivityRecord(stubIntent);
if (saveInstance.intent == null) {
return true;
}
// 原版 Intent
Intent intent = saveInstance.intent;
ComponentName caller = saveInstance.caller;
IBinder token = ActivityThread.ActivityClientRecord.token.get(r);
ActivityInfo info = saveInstance.info;
// 如果 token 還沒初始化猿规,代表 App 剛剛啟動(dòng)第一個(gè)組件
if (VClientImpl.get().getToken() == null) {
VActivityManager.get().processRestarted(info.packageName, info.processName, saveInstance.userId);
getH().sendMessageAtFrontOfQueue(Message.obtain(msg));
return false;
}
// AppBindData 為空,則 App 信息不明
if (!VClientImpl.get().isBound()) {
// 初始化并綁定 Application
VClientImpl.get().bindApplication(info.packageName, info.processName);
getH().sendMessageAtFrontOfQueue(Message.obtain(msg));
return false;
}
// 獲取 TaskId
int taskId = IActivityManager.getTaskForActivity.call(
ActivityManagerNative.getDefault.call(),
token,
false
);
// 1.將 ActivityRecorder 加入 mActivities 2.通知服務(wù)端 VAMS Activity 創(chuàng)建完成
VActivityManager.get().onActivityCreate(ComponentUtils.toComponentName(info), caller, token, info, intent, ComponentUtils.getTaskAffinity(info), taskId, info.launchMode, info.flags);
ClassLoader appClassLoader = VClientImpl.get().getClassLoader(info.applicationInfo);
intent.setExtrasClassLoader(appClassLoader);
// 將 Host Stub Activity Intent 替換為原版 Intent
ActivityThread.ActivityClientRecord.intent.set(r, intent);
// 同上
ActivityThread.ActivityClientRecord.activityInfo.set(r, info);
return true;
}
最后成功從 StubActivity Intent 還原出來(lái)的原版 Intent 被繼續(xù)交給原生的 AM
// 將 Host Stub Activity Intent 替換為原版 Intent
ActivityThread.ActivityClientRecord.intent.set(r, intent);
// 同上
ActivityThread.ActivityClientRecord.activityInfo.set(r, info);
最后一個(gè) Hook 點(diǎn)在 Instrumentation.callActivityOnCreate:
因?yàn)?AMS 實(shí)際上啟動(dòng)的是 StubActivity 的關(guān)系宙橱,真正的 Activity 的一些信息還不是其真正的信息姨俩,比如主題之類的,所以需要在這個(gè)時(shí)機(jī)修復(fù)一下师郑,選擇這個(gè)時(shí)間修復(fù)的原因也是因?yàn)?Activity 已經(jīng)被 new 出來(lái)了环葵,而且資源已經(jīng)準(zhǔn)備完畢。
public void callActivityOnCreate(Activity activity, Bundle icicle) {
VirtualCore.get().getComponentDelegate().beforeActivityCreate(activity);
IBinder token = mirror.android.app.Activity.mToken.get(activity);
ActivityClientRecord r = VActivityManager.get().getActivityRecord(token);
// 替換 Activity 對(duì)象
if (r != null) {
r.activity = activity;
}
ContextFixer.fixContext(activity);
ActivityFixer.fixActivity(activity);
ActivityInfo info = null;
if (r != null) {
info = r.info;
}
// 設(shè)置主題和屏幕縱橫控制
if (info != null) {
if (info.theme != 0) {
activity.setTheme(info.theme);
}
if (activity.getRequestedOrientation() == ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
&& info.screenOrientation != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
activity.setRequestedOrientation(info.screenOrientation);
}
}
super.callActivityOnCreate(activity, icicle);
VirtualCore.get().getComponentDelegate().afterActivityCreate(activity);
}
引用(如有侵權(quán)宝冕,即刻刪除):
Android虛擬化引擎VirtualApp探究
Android 雙開沙箱 VirtualApp 源碼分析