VirtualApp 框架解析---安装篇

VirtualApp是如何来安装apk,与系统原生通过AppManagerService服务来进行安装会有哪些不同之处呢?
首先,从添加apk的函数入手,来掌握整个安装的脉络。

public void addApp(AppInfoLite info) {
    ...
    InstalledAppInfo installedAppInfo = VirtualCore.get().getInstalledAppInfo(info.packageName, 0);
            addResult.justEnableHidden = installedAppInfo != null;
            if (addResult.justEnableHidden) {
                int[] userIds = installedAppInfo.getInstalledUsers();
                int nextUserId = userIds.length;
                /*
                  Input : userIds = {0, 1, 3}
                  Output: nextUserId = 2
                 */
                for (int i = 0; i < userIds.length; i++) {
                    if (userIds[i] != i) {
                        nextUserId = i;
                        break;
                    }
                }
                addResult.userId = nextUserId;
                if (VUserManager.get().getUserInfo(nextUserId) == null) {
                    // user not exist, create it automatically.
                    ...
                }
                boolean success = VirtualCore.get().installPackageAsUser(nextUserId, info.packageName);
                ...
            } else {
                InstallResult res = mRepo.addVirtualApp(info);
                ...
            }
    ...
}

从核心代码来看,整个过程清晰并不复杂,主要的步骤分别是

  1. 根据AppInfoLitepackageName调用getInstalledAppInfo方法去获取已经安装的apk信息
  2. 通过justEnableHidden判断该apk是否是第一次安装(是否有用户已经完成过安装)
  3. 如果已经安装过,则通过现有的用户Id数组创建一个新的userId,同时如果该userId的用户不存在,还需通过VUserManager.get().createUser进行用户创建,最后调用VirtualCore.get().installPackageAsUser进行安装
  4. 如果没有安装过,则直接调用mRepo.addVirtualApp其实就是VirtualCore.get().installPackage进行安装

对上述过程分析后,发现主要的关键点分别是传入AppInfoLite调用getInstalledAppInfo获取apk信息、首次安装调用installPackage以及非首次安装调用installPackageAsUser方法。
其中,AppInfoLite是一个序列化的类对象,是AppInfo的简化版。

public class AppInfoLite implements Parcelable {
    ...
    public String packageName;
    public String path;
    public boolean fastOpen;
    ...
}

public class AppInfo {
    public String packageName;
    public String path;
    public boolean fastOpen;
    public Drawable icon;
    public CharSequence name;
    public int cloneCount;
}

构造数据基本是通过PackageManager来获取,整个构造过程也比较简单,需要注意的就是info.cloneCount,是指通过VirtualApp进行安装过的次数。

List<PackageInfo> pkgList = context.getPackageManager().getInstalledPackages(0)
PackageManager pm = context.getPackageManager();
List<AppInfo> list = new ArrayList<>(pkgList.size());
String hostPkg = VirtualCore.get().getHostPkg();
for (PackageInfo pkg : pkgList) {
    // ignore the host package
    if (hostPkg.equals(pkg.packageName)) {
        continue;
    }
    // ignore the System package
    if (isSystemApplication(pkg)) {
        continue;
    }
    ApplicationInfo ai = pkg.applicationInfo;
    String path = ai.publicSourceDir != null ? ai.publicSourceDir : ai.sourceDir;
    if (path == null) {
        continue;
    }
    AppInfo info = new AppInfo();
    info.packageName = pkg.packageName;
    info.fastOpen = fastOpen;
    info.path = path;
    info.icon = ai.loadIcon(pm);
    info.name = ai.loadLabel(pm);
    InstalledAppInfo installedAppInfo = VirtualCore.get().getInstalledAppInfo(pkg.packageName, 0);
    if (installedAppInfo != null) {
        info.cloneCount = installedAppInfo.getInstalledUsers().length;
    }
    list.add(info);
}

那接下来看下getInstalledAppInfoinstallPackage以及installPackageAsUser,这三个都属于VAppManagerService类的方法。

getInstalledAppInfo

public InstalledAppInfo getInstalledAppInfo(String packageName, int flags) {
        synchronized (PackageCacheManager.class) {
            if (packageName != null) {
                PackageSetting setting = PackageCacheManager.getSetting(packageName);
                if (setting != null) {
                    return setting.getAppInfo();
                }
            }
            return null;
        }
}

从代码来看,就是从PackageCacheManager中获取InstalledAppInfo对象,而PackageCacheManager是在首次安装才会构造对应的app数据并进行存储,所以安装过程中也会根据这个方法获取的对象是否为空来判断是否为首次安装。

installPackageAsUser

这个方法呢,是在非首次安装的时候进行调用,所以处理的事情不多。

if (VUserManagerService.get().exists(userId)) {
    PackageSetting ps = PackageCacheManager.getSetting(packageName);
    if (ps != null) {
        if (!ps.isInstalled(userId)) {
            ps.setInstalled(userId, true);
            notifyAppInstalled(ps, userId);
            ...
        }
    }
}

private void notifyAppInstalled(PackageSetting setting, int userId) {
    final String pkg = setting.packageName;
    int N = mRemoteCallbackList.beginBroadcast();
    while (N-- > 0) {
        try {
            if (userId == -1) {
                sendInstalledBroadcast(pkg);
                mRemoteCallbackList.getBroadcastItem(N).onPackageInstalled(pkg);
                mRemoteCallbackList.getBroadcastItem(N).onPackageInstalledAsUser(0, pkg);
            } else {
                mRemoteCallbackList.getBroadcastItem(N).onPackageInstalledAsUser(userId, pkg);
            }
            ...
        }
    }
        ...
}
...

从代码流程来看,主要干了两件事情,设置对应的PackageSetting安装标志位为已安装,同时通过调用notifyAppInstalled方法来通知其他进程广播已安装消息。

installPackage

而这个方法和installPackageAsUser从字面来看都是安装,但是过程全大不相同,此方法是在首次安装的时候调用,可以理解为是最为核心的方法,涉及处理的流程也较多,接下来顺着几个主要流程来解读下代码。

public synchronized InstallResult installPackage(String path, int flags, boolean notify) {
        ...
        /*
        ** 通过反射调用android.content.pm.PackageParser中的parsePackage方法对apk进行解析,
        ** 获取其中的activity、service、receivers、providers、permissions等信息并赋值给VPackage对象
        */
        VPackage pkg = null;
        try {
            pkg = PackageParserEx.parsePackage(packageFile);
        } catch (Throwable e) {
            e.printStackTrace();
        }
        ...
        /*
        ** 检查是否是更新安装,如果是更新安装,杀死app所在的进程并进行信息更新
        */
        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;
            }
            ...
        }
        File appDir = VEnvironment.getDataAppPackageDirectory(pkg.packageName);
        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);
        }
        ...
        /*
        ** 反射调用com.android.internal.content.NativeLibraryHelper中的copyNativeBinaries方法,
        ** 拷贝对应的so到app对应的私有lib目录下。
        */   
        NativeLibraryHelperCompat.copyNativeBinaries(new File(path), libDir);
        /*
        ** 拷贝apk文件到对应的app私有目录下,并修改起读写权限。
        */  
        if (!dependSystem) {
            ...
            try {
                FileUtils.copyFile(packageFile, privatePackageFile);
            } catch (IOException e) {
                privatePackageFile.delete();
                return InstallResult.makeFailure("Unable to copy the package file.");
            }
            packageFile = privatePackageFile;
        }
        ...
        chmodPackageDictionary(packageFile);
        /*
        ** 构造对应的PackageSetting对象,存储相关信息并缓存到PackageCacheManager。
        */  
        PackageSetting ps;
        ...
        ps.dependSystem = dependSystem;
        ps.apkPath = packageFile.getPath();
        ps.libPath = libDir.getPath();
        ps.packageName = pkg.packageName;
        ps.appId = VUserHandle.getAppId(mUidSystem.getOrCreateUid(pkg));
        ...
        PackageParserEx.savePackageCache(pkg);
        PackageCacheManager.put(pkg, ps);
        ...
        /*
        ** 如果是art虚拟机(5.0以上系统),则先通过dex2oat命令进行dex优化,如果失败则通过DexFile.loadDex方法调用进行dex优化。
        */ 
        if (!dependSystem) {
            boolean runDexOpt = false;
            if (VirtualRuntime.isArt()) {
                try {
                    ArtDexOptimizer.interpretDex2Oat(ps.apkPath, VEnvironment.getOdexFile(ps.packageName).getPath());
                } catch (IOException e) {
                    e.printStackTrace();
                    runDexOpt = true;
                }
            } else {
                runDexOpt = true;
            }
            if (runDexOpt) {
                try {
                    DexFile.loadDex(ps.apkPath, VEnvironment.getOdexFile(ps.packageName).getPath(), 0).close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        /**
        ** 读取apk中manifest配置的静态注册广播信息,以动态注册的形式注册到VirtuapApp进程中,注意不是apk对应的进程;并且广播其他进程已安装信息。
        */
        BroadcastSystem.get().startApp(pkg);
        if (notify) {
            notifyAppInstalled(ps, -1);
        }
        res.isSuccess = true;
        return res;
    }

通过解读核心代码,可以得知主要存在7个步骤(已对应在代码的注释中)。

  1. 通过反射调用android.content.pm.PackageParser中的parsePackage方法对apk进行解析,获取其中的activity、service、receivers、providers、permissions、signatures等信息并赋值给VPackage对象。
private static VPackage buildPackageCache(PackageParser.Package p) {
        VPackage cache = new VPackage();
        cache.activities = new ArrayList<>(p.activities.size());
        cache.services = new ArrayList<>(p.services.size());
        cache.receivers = new ArrayList<>(p.receivers.size());
        cache.providers = new ArrayList<>(p.providers.size());
        cache.instrumentation = new ArrayList<>(p.instrumentation.size());
        cache.permissions = new ArrayList<>(p.permissions.size());
        cache.permissionGroups = new ArrayList<>(p.permissionGroups.size());

        for (PackageParser.Activity activity : p.activities) {
            cache.activities.add(new VPackage.ActivityComponent(activity));
        }
        for (PackageParser.Service service : p.services) {
            cache.services.add(new VPackage.ServiceComponent(service));
        }
        for (PackageParser.Activity receiver : p.receivers) {
            cache.receivers.add(new VPackage.ActivityComponent(receiver));
        }
        for (PackageParser.Provider provider : p.providers) {
            cache.providers.add(new VPackage.ProviderComponent(provider));
        }
        for (PackageParser.Instrumentation instrumentation : p.instrumentation) {
            cache.instrumentation.add(new VPackage.InstrumentationComponent(instrumentation));
        }
        cache.requestedPermissions = new ArrayList<>(p.requestedPermissions.size());
        cache.requestedPermissions.addAll(p.requestedPermissions);
        if (mirror.android.content.pm.PackageParser.Package.protectedBroadcasts != null) {
            List<String> protectedBroadcasts = mirror.android.content.pm.PackageParser.Package.protectedBroadcasts.get(p);
            if (protectedBroadcasts != null) {
                cache.protectedBroadcasts = new ArrayList<>(protectedBroadcasts);
                cache.protectedBroadcasts.addAll(protectedBroadcasts);
            }
        }
        cache.applicationInfo = p.applicationInfo;
        cache.mSignatures = p.mSignatures;
        cache.mAppMetaData = p.mAppMetaData;
        cache.packageName = p.packageName;
        cache.mPreferredOrder = p.mPreferredOrder;
        cache.mVersionName = p.mVersionName;
        cache.mSharedUserId = p.mSharedUserId;
        cache.mSharedUserLabel = p.mSharedUserLabel;
        cache.usesLibraries = p.usesLibraries;
        cache.mVersionCode = p.mVersionCode;
        cache.mAppMetaData = p.mAppMetaData;
        cache.configPreferences = p.configPreferences;
        cache.reqFeatures = p.reqFeatures;
        addOwner(cache);
        return cache;
}
  1. 检查是否是更新安装,如果是则杀死app所在的进程并进行信息更新。
  2. 反射调用com.android.internal.content.NativeLibraryHelper中的copyNativeBinaries方法,拷贝对应的so到app对应的私有lib目录下。
private static int copyNativeBinariesBeforeL(File apkFile, File sharedLibraryDir) {
    try {
        return Reflect.on(NativeLibraryHelper.TYPE).call("copyNativeBinariesIfNeededLI", apkFile, sharedLibraryDir)
                    .get();
    } catch (Throwable e) {
        e.printStackTrace();
    }
    return -1;
}
  1. 拷贝apk文件到对应的app私有目录下,并修改其文件目录读写权限。
  2. 构造对应的PackageSetting对象,存储相关信息并缓存到PackageCacheManager中。
  3. 如果是art虚拟机,则通过dex2oat命令或者DexFile.loadDex方法调用的方式进行dex优化。
  4. 读取apk中AndroidMinifest文件配置的静态注册广播信息,动态注册到VirtuapApp进程中,同时广播其他进程已安装通知。
public void startApp(VPackage p) {
        PackageSetting setting = (PackageSetting) p.mExtras;
        for (VPackage.ActivityComponent receiver : p.receivers) {
            ActivityInfo info = receiver.info;
            List<BroadcastReceiver> receivers = mReceivers.get(p.packageName);
            if (receivers == null) {
                receivers = new ArrayList<>();
                mReceivers.put(p.packageName, receivers);
            }
            String componentAction = String.format("_VA_%s_%s", info.packageName, info.name);
            IntentFilter componentFilter = new IntentFilter(componentAction);
            BroadcastReceiver r = new StaticBroadcastReceiver(setting.appId, info, componentFilter);
            mContext.registerReceiver(r, componentFilter, null, mScheduler);
            receivers.add(r);
            for (VPackage.ActivityIntentInfo ci : receiver.intents) {
                IntentFilter cloneFilter = new IntentFilter(ci.filter);
                SpecialComponentList.protectIntentFilter(cloneFilter);
                r = new StaticBroadcastReceiver(setting.appId, info, cloneFilter);
                mContext.registerReceiver(r, cloneFilter, null, mScheduler);
                receivers.add(r);
            }
        }
}

尾语

VirtualApp整个安装过程大概如此,那到底和系统原生安装主要存在什么差异呢?

根据上述过程,可以认知的几个不同之处在于:

  1. 系统安装是拷贝apk到/data/app/包名目录下,而va是拷贝到自己定义的私有目录下。
  2. 首次安装的时候要自行进行dex优化。
  3. 需要将AndroidMinifest文件中静态注册的广播信息解析出来并且在va进程中进行动态注册。

好了,VirtualApp的安装过程就先到此为止。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容