当前位置: 首页 > news >正文

Android 安装应用-准备阶段

  安装应用的准备阶段是在PackageManagerService类中的preparePackageLI(InstallArgs args, PackageInstalledInfo res),代码有些长,分段阅读。

分段一

  分段一:

    @GuardedBy("mInstallLock")private PrepareResult preparePackageLI(InstallArgs args, PackageInstalledInfo res)throws PrepareFailure {final int installFlags = args.installFlags;final File tmpPackageFile = new File(args.getCodePath());final boolean onExternal = args.volumeUuid != null;final boolean instantApp = ((installFlags & PackageManager.INSTALL_INSTANT_APP) != 0);final boolean fullApp = ((installFlags & PackageManager.INSTALL_FULL_APP) != 0);final boolean virtualPreload =((installFlags & PackageManager.INSTALL_VIRTUAL_PRELOAD) != 0);final boolean isRollback = args.installReason == PackageManager.INSTALL_REASON_ROLLBACK;@ScanFlags int scanFlags = SCAN_NEW_INSTALL | SCAN_UPDATE_SIGNATURE;if (args.move != null) {// moving a complete application; perform an initial scan on the new install locationscanFlags |= SCAN_INITIAL;}if ((installFlags & PackageManager.INSTALL_DONT_KILL_APP) != 0) {scanFlags |= SCAN_DONT_KILL_APP;}if (instantApp) {scanFlags |= SCAN_AS_INSTANT_APP;}if (fullApp) {scanFlags |= SCAN_AS_FULL_APP;}if (virtualPreload) {scanFlags |= SCAN_AS_VIRTUAL_PRELOAD;}if (DEBUG_INSTALL) Slog.d(TAG, "installPackageLI: path=" + tmpPackageFile);// Validity checkif (instantApp && onExternal) {Slog.i(TAG, "Incompatible ephemeral install; external=" + onExternal);throw new PrepareFailure(PackageManager.INSTALL_FAILED_SESSION_INVALID);}// Retrieve PackageSettings and parse package@ParseFlags final int parseFlags = mDefParseFlags | ParsingPackageUtils.PARSE_CHATTY| ParsingPackageUtils.PARSE_ENFORCE_CODE| (onExternal ? ParsingPackageUtils.PARSE_EXTERNAL_STORAGE : 0);Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "parsePackage");final ParsedPackage parsedPackage;try (PackageParser2 pp = mInjector.getPreparingPackageParser()) {parsedPackage = pp.parsePackage(tmpPackageFile, parseFlags, false);AndroidPackageUtils.validatePackageDexMetadata(parsedPackage);} catch (PackageParserException e) {throw new PrepareFailure("Failed parse during installPackageLI", e);} finally {Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);}// Instant apps have several additional install-time checks.if (instantApp) {if (parsedPackage.getTargetSdkVersion() < Build.VERSION_CODES.O) {Slog.w(TAG, "Instant app package " + parsedPackage.getPackageName()+ " does not target at least O");throw new PrepareFailure(INSTALL_FAILED_SESSION_INVALID,"Instant app package must target at least O");}if (parsedPackage.getSharedUserId() != null) {Slog.w(TAG, "Instant app package " + parsedPackage.getPackageName()+ " may not declare sharedUserId.");throw new PrepareFailure(INSTALL_FAILED_SESSION_INVALID,"Instant app package may not declare a sharedUserId");}}if (parsedPackage.isStaticSharedLibrary()) {// Static shared libraries have synthetic package namesrenameStaticSharedLibraryPackage(parsedPackage);// No static shared libs on external storageif (onExternal) {Slog.i(TAG, "Static shared libs can only be installed on internal storage.");throw new PrepareFailure(INSTALL_FAILED_INVALID_INSTALL_LOCATION,"Packages declaring static-shared libs cannot be updated");}}String pkgName = res.name = parsedPackage.getPackageName();if (parsedPackage.isTestOnly()) {if ((installFlags & PackageManager.INSTALL_ALLOW_TEST) == 0) {throw new PrepareFailure(INSTALL_FAILED_TEST_ONLY, "installPackageLI");}}try {// either use what we've been given or parse directly from the APKif (args.signingDetails != PackageParser.SigningDetails.UNKNOWN) {parsedPackage.setSigningDetails(args.signingDetails);} else {parsedPackage.setSigningDetails(ParsingPackageUtils.getSigningDetails(parsedPackage, false /* skipVerify */));}} catch (PackageParserException e) {throw new PrepareFailure("Failed collect during installPackageLI", e);}if (instantApp && parsedPackage.getSigningDetails().signatureSchemeVersion< SignatureSchemeVersion.SIGNING_BLOCK_V2) {Slog.w(TAG, "Instant app package " + parsedPackage.getPackageName()+ " is not signed with at least APK Signature Scheme v2");throw new PrepareFailure(INSTALL_FAILED_SESSION_INVALID,"Instant app package must be signed with APK Signature Scheme v2 or greater");}        

  先设置一些变量。installFlags为安装表示,tmpPackageFile为安装apk文件目录,onExternal是安装在外部空间,instantApp是代表INSTANT_APP。在这安装的例子中,args是FileInstallArgs类对象,它的.getCodePath()的值是来自InstallParams对象的成员origin的成员file,而构造InstallParams对象时,参数就是安装apk文件的目录。scanFlags开始时,就有SCAN_NEW_INSTALL和SCAN_UPDATE_SIGNATURE标识,它的值是在浏览那个步骤,需要使用,它还根据状态,是否增加几个标识。
  在INSTANT_APP并且是装在外部的情况下,会扔出PrepareFailure异常。
  下面是要解析包,pp是PackageParser2类对象,tmpPackageFile是目录,解析出来的parsedPackage实际类型是PackageImpl。
  AndroidPackageUtils.validatePackageDexMetadata(parsedPackage)是如果存在安装包的dex元数据文件,是去做验证。
  如果是INSTANT_APP,目标sdk不能低于Build.VERSION_CODES.O。并且不能声明sharedUserId属性值。不然会报异常。
  如果解析包是静态分享库,需要将它的包名改成静态分享库的名字格式,它的格式为packageName + “_” + libraryVersion。并且静态分享库不能放在外部存储中。
  接着会为参数res.name赋值为解析包的包名。
  如果解析包是配置了android:testOnly=“true”,但是安装标识里面没有INSTALL_ALLOW_TEST,也会报PrepareFailure异常。
  如果参数args.signingDetails不为PackageParser.SigningDetails.UNKNOWN,说明它已经有签名信息了。所以直接将它设置在解析包对象parsedPackage中。如果没有,这时需要调用ParsingPackageUtils.getSigningDetails()得到对应的签名信息,然后设置到解析包对象parsedPackage中。
  如果是INSTANT_APP,并且签名方式版本比SIGNING_BLOCK_V2小,则会报PrepareFailure异常。

分段二

  分段二:

        boolean systemApp = false;boolean replace = false;synchronized (mLock) {// Check if installing already existing packageif ((installFlags & PackageManager.INSTALL_REPLACE_EXISTING) != 0) {String oldName = mSettings.getRenamedPackageLPr(pkgName);if (parsedPackage.getOriginalPackages().contains(oldName)&& mPackages.containsKey(oldName)) {// This package is derived from an original package,// and this device has been updating from that original// name.  We must continue using the original name, so// rename the new package here.parsedPackage.setPackageName(oldName);pkgName = parsedPackage.getPackageName();replace = true;if (DEBUG_INSTALL) {Slog.d(TAG, "Replacing existing renamed package: oldName="+ oldName + " pkgName=" + pkgName);}} else if (mPackages.containsKey(pkgName)) {// This package, under its official name, already exists// on the device; we should replace it.replace = true;if (DEBUG_INSTALL) Slog.d(TAG, "Replace existing pacakge: " + pkgName);}if (replace) {// Prevent apps opting out from runtime permissionsAndroidPackage oldPackage = mPackages.get(pkgName);final int oldTargetSdk = oldPackage.getTargetSdkVersion();final int newTargetSdk = parsedPackage.getTargetSdkVersion();if (oldTargetSdk > Build.VERSION_CODES.LOLLIPOP_MR1&& newTargetSdk <= Build.VERSION_CODES.LOLLIPOP_MR1) {throw new PrepareFailure(PackageManager.INSTALL_FAILED_PERMISSION_MODEL_DOWNGRADE,"Package " + parsedPackage.getPackageName()+ " new target SDK " + newTargetSdk+ " doesn't support runtime permissions but the old"+ " target SDK " + oldTargetSdk + " does.");}// Prevent persistent apps from being updatedif (oldPackage.isPersistent()&& ((installFlags & PackageManager.INSTALL_STAGED) == 0)) {throw new PrepareFailure(PackageManager.INSTALL_FAILED_INVALID_APK,"Package " + oldPackage.getPackageName() + " is a persistent app. "+ "Persistent apps are not updateable.");}}}PackageSetting ps = mSettings.getPackageLPr(pkgName);if (ps != null) {if (DEBUG_INSTALL) Slog.d(TAG, "Existing package: " + ps);// Static shared libs have same package with different versions where// we internally use a synthetic package name to allow multiple versions// of the same package, therefore we need to compare signatures against// the package setting for the latest library version.PackageSetting signatureCheckPs = ps;if (parsedPackage.isStaticSharedLibrary()) {SharedLibraryInfo libraryInfo = getLatestSharedLibraVersionLPr(parsedPackage);if (libraryInfo != null) {signatureCheckPs = mSettings.getPackageLPr(libraryInfo.getPackageName());}}// Quick validity check that we're signed correctly if updating;// we'll check this again later when scanning, but we want to// bail early here before tripping over redefined permissions.final KeySetManagerService ksms = mSettings.getKeySetManagerService();if (ksms.shouldCheckUpgradeKeySetLocked(signatureCheckPs, scanFlags)) {if (!ksms.checkUpgradeKeySetLocked(signatureCheckPs, parsedPackage)) {throw new PrepareFailure(INSTALL_FAILED_UPDATE_INCOMPATIBLE, "Package "+ parsedPackage.getPackageName() + " upgrade keys do not match the "+ "previously installed version");}} else {try {final boolean compareCompat = isCompatSignatureUpdateNeeded(parsedPackage);final boolean compareRecover = isRecoverSignatureUpdateNeeded(parsedPackage);// We don't care about disabledPkgSetting on install for now.final boolean compatMatch = verifySignatures(signatureCheckPs, null,parsedPackage.getSigningDetails(), compareCompat, compareRecover,isRollback);// The new KeySets will be re-added later in the scanning process.if (compatMatch) {synchronized (mLock) {ksms.removeAppKeySetDataLPw(parsedPackage.getPackageName());}}} catch (PackageManagerException e) {throw new PrepareFailure(e.error, e.getMessage());}}if (ps.pkg != null) {systemApp = ps.pkg.isSystem();}res.origUsers = ps.queryInstalledUsers(mUserManager.getUserIds(), true);}final int numGroups = ArrayUtils.size(parsedPackage.getPermissionGroups());for (int groupNum = 0; groupNum < numGroups; groupNum++) {final ParsedPermissionGroup group =parsedPackage.getPermissionGroups().get(groupNum);final PermissionGroupInfo sourceGroup = getPermissionGroupInfo(group.getName(), 0);if (sourceGroup != null&& cannotInstallWithBadPermissionGroups(parsedPackage)) {final String sourcePackageName = sourceGroup.packageName;if ((replace || !parsedPackage.getPackageName().equals(sourcePackageName))&& !doesSignatureMatchForPermissions(sourcePackageName, parsedPackage,scanFlags)) {EventLog.writeEvent(0x534e4554, "146211400", -1,parsedPackage.getPackageName());throw new PrepareFailure(INSTALL_FAILED_DUPLICATE_PERMISSION_GROUP,"Package "+ parsedPackage.getPackageName()+ " attempting to redeclare permission group "+ group.getName() + " already owned by "+ sourcePackageName);}}}// TODO: Move logic for checking permission compatibility into PermissionManagerServicefinal int N = ArrayUtils.size(parsedPackage.getPermissions());for (int i = N - 1; i >= 0; i--) {final ParsedPermission perm = parsedPackage.getPermissions().get(i);final Permission bp = mPermissionManager.getPermissionTEMP(perm.getName());// Don't allow anyone but the system to define ephemeral permissions.if ((perm.getProtectionLevel() & PermissionInfo.PROTECTION_FLAG_INSTANT) != 0&& !systemApp) {Slog.w(TAG, "Non-System package " + parsedPackage.getPackageName()+ " attempting to delcare ephemeral permission "+ perm.getName() + "; Removing ephemeral.");perm.setProtectionLevel(perm.getProtectionLevel() & ~PermissionInfo.PROTECTION_FLAG_INSTANT);}// Check whether the newly-scanned package wants to define an already-defined permif (bp != null) {final String sourcePackageName = bp.getPackageName();if (!doesSignatureMatchForPermissions(sourcePackageName, parsedPackage,scanFlags)) {// If the owning package is the system itself, we log but allow// install to proceed; we fail the install on all other permission// redefinitions.if (!sourcePackageName.equals("android")) {throw new PrepareFailure(INSTALL_FAILED_DUPLICATE_PERMISSION, "Package "+ parsedPackage.getPackageName()+ " attempting to redeclare permission "+ perm.getName() + " already owned by "+ sourcePackageName).conflictsWithExistingPermission(perm.getName(),sourcePackageName);} else {Slog.w(TAG, "Package " + parsedPackage.getPackageName()+ " attempting to redeclare system permission "+ perm.getName() + "; ignoring new declaration");parsedPackage.removePermission(i);}} else if (!PLATFORM_PACKAGE_NAME.equals(parsedPackage.getPackageName())) {// Prevent apps to change protection level to dangerous from any other// type as this would allow a privilege escalation where an app adds a// normal/signature permission in other app's group and later redefines// it as dangerous leading to the group auto-grant.if ((perm.getProtectionLevel() & PermissionInfo.PROTECTION_MASK_BASE)== PermissionInfo.PROTECTION_DANGEROUS) {if (bp != null && !bp.isRuntime()) {Slog.w(TAG, "Package " + parsedPackage.getPackageName()+ " trying to change a non-runtime permission "+ perm.getName()+ " to runtime; keeping old protection level");perm.setProtectionLevel(bp.getProtectionLevel());}}}}if (perm.getGroup() != null&& cannotInstallWithBadPermissionGroups(parsedPackage)) {boolean isPermGroupDefinedByPackage = false;for (int groupNum = 0; groupNum < numGroups; groupNum++) {if (parsedPackage.getPermissionGroups().get(groupNum).getName().equals(perm.getGroup())) {isPermGroupDefinedByPackage = true;break;}}if (!isPermGroupDefinedByPackage) {final PermissionGroupInfo sourceGroup =getPermissionGroupInfo(perm.getGroup(), 0);if (sourceGroup == null) {EventLog.writeEvent(0x534e4554, "146211400", -1,parsedPackage.getPackageName());throw new PrepareFailure(INSTALL_FAILED_BAD_PERMISSION_GROUP,"Package "+ parsedPackage.getPackageName()+ " attempting to declare permission "+ perm.getName() + " in non-existing group "+ perm.getGroup());} else {String groupSourcePackageName = sourceGroup.packageName;if (!PLATFORM_PACKAGE_NAME.equals(groupSourcePackageName)&& !doesSignatureMatchForPermissions(groupSourcePackageName,parsedPackage, scanFlags)) {EventLog.writeEvent(0x534e4554, "146211400", -1,parsedPackage.getPackageName());throw new PrepareFailure(INSTALL_FAILED_BAD_PERMISSION_GROUP,"Package "+ parsedPackage.getPackageName()+ " attempting to declare permission "+ perm.getName() + " in group "+ perm.getGroup() + " owned by package "+ groupSourcePackageName+ " with incompatible certificate");}}}}}}

  下面要进入一段同步锁的代码段,变量systemApp代表是系统APP,replace代表是要替换安装。
  我们在文章Android安装过程二 系统进程中PackageInstallerSession对象的创建 中代码分段二 中可以看到,会将PackageManager.INSTALL_REPLACE_EXISTING添加到SessionParams 对象中成员变量installFlags中,而我们这里FileInstallArgs对象成员变量installFlags的值就是来自SessionParams 对象中成员变量installFlags。所以,这里会进入这个if语句中。
  先看一下,应用包名变更的一种情况。如果包名变更,需要在配置文件(Manifest文件)中,配置application同级别original-package标签 的属性 "name"的值。变更之后,再更新安装时,会将包名变更的关系保存在PackageManagerService对象成员mSettings(Settings类对象)的成员变量mRenamedPackages中,它是WatchedArrayMap类型,key是变更之后包名,值为变更之前的包名。
  所以在这里,它先检查是否符合包名变更的情况,如果是,将解析包对象的包名设置为它的原来的包名。并且将临时变量pkgName设置为原来的包名,replace = true,代表它是替换更新。
  如果不是包名变更,但是正常包更新安装的情况,就是mPackages.containsKey(pkgName)为true的情况。mPackages中包括所有安装成功应用的包信息。如果是这种情况,将replace = true,代表它是替换更新。
  其他的就不是替换安装的情况,replace的值还是为false。
  接下来就是处理替换安装的情况下,如果旧安装包的目标sdk版本大于Build.VERSION_CODES.LOLLIPOP_MR1,但是新包的目标sdk版本小于等于它,则报异常。如果旧包时持久包,并且安装标识没有PackageManager.INSTALL_STAGED,则也会报异常。
  下面是从成员mSettings中通过包名取得PackageSetting对象,在安装成功之后,每个应用在mSettings中也会通过包名维持PackageSetting对象。如果它存在,下面主要会是去检查签名匹配。
  如果解析包是静态分享库,它会通过名字存在多个版本,这里是去找一个最近版本的库信息。
  之后拿到KeySetManagerService对象ksms。它里面维护这签名公钥和对应的key的关系。
  ksms.shouldCheckUpgradeKeySetLocked(signatureCheckPs, scanFlags)这种是对于APK里面Manifest文件中配置了"key-sets"标签的应用来说的。一般的应用不会配置它。
  其他的会走else这种情况,它需要验证签名。变量compareCompat代表它会兼容签名升级之前和之后的格式。变量compareRecover代表它会考虑签名有可能会编码格式稍有错误的情况。这俩都代表一种回退的验证,如果正常验证没有通过,满足这俩条件,会进行回退验证。像compareCompat为true的情况下,这里需要理解一下PackageParser.SigningDetails这个类,它有一个成员变量Signature[] signatures,以前旧版本,如果多个证书,都会存在它里面。而现在,它们也会存在Signature类的成员变量Certificate[] mCertificateChain中,所以像这种情况,需要将mCertificateChain中的证书都取出来和之前的signatures进行比较。
  这里调用verifySignatures()方法,是得验证通过的(和返回值compatMatch为true不完全一样),如果没通过,会扔出PackageManagerException异常。
  如果compatMatch为true,需要删除该表对应的公钥相关的信息。
  如果ps.pkg != null,会检查它是否是系统app。
  res.origUsers则是已经安装的用户id。
  接下来会检查解析包的PermissionGroup,它来自Manifest中的"permission-group"标签(和uses-permission同级)。如果某个PermissionGroup在被其他的应用声明过,或者该应用自己本身升级使用。如果它们的证书不相互符合,会报PrepareFailure异常。
  下面是用来检查解析包的Permission,它来自Manifest中的"permission"标签(和uses-permission同级)。
  如果声明的权限等级有PermissionInfo.PROTECTION_FLAG_INSTANT,但是它不是系统APP,需要将该等级去除。
  如果该权限已经声明过(可能是应用本身声明也可能是其他应用),如果证书验证不能通过,加入原来声明权限的包名不是系统自己,会报异常。如果是系统自己声明的权限,需要将解析包的权限去除。
  权限声明过,并且证书验证通过,但是解析包不是平台系统包名,如果它声明的权限是危险级,并且该权限之前不是运行时,需要将解析包的保护等级设置成它之前的等级。
  下面接着处理的是,如果权限所属的权限组不是该解析包声明的权限组会做什么。isPermGroupDefinedByPackage为true,则说明它的权限组为解析包声明的,反之,则不是。
  在它不是解析包声明的权限组的情况下。如果找不到权限组,则报PrepareFailure异常,是说该权限没有对应的权限组。如果存在权限组,但是权限组所属的应用包不为系统,并且它和解析包的证书验证不通过的情况下,也会报不一致签名证书的PrepareFailure异常。

分段三

  分段三:

        if (systemApp) {if (onExternal) {// Abort update; system app can't be replaced with app on sdcardthrow new PrepareFailure(INSTALL_FAILED_INVALID_INSTALL_LOCATION,"Cannot install updates to system apps on sdcard");} else if (instantApp) {// Abort update; system app can't be replaced with an instant appthrow new PrepareFailure(INSTALL_FAILED_SESSION_INVALID,"Cannot update a system app with an instant app");}}if (args.move != null) {// We did an in-place move, so dex is ready to rollscanFlags |= SCAN_NO_DEX;scanFlags |= SCAN_MOVE;synchronized (mLock) {final PackageSetting ps = mSettings.getPackageLPr(pkgName);if (ps == null) {res.setError(INSTALL_FAILED_INTERNAL_ERROR,"Missing settings for moved package " + pkgName);}// We moved the entire application as-is, so bring over the// previously derived ABI information.parsedPackage.setPrimaryCpuAbi(ps.primaryCpuAbiString).setSecondaryCpuAbi(ps.secondaryCpuAbiString);}} else {// Enable SCAN_NO_DEX flag to skip dexopt at a later stagescanFlags |= SCAN_NO_DEX;try {PackageSetting pkgSetting;synchronized (mLock) {pkgSetting = mSettings.getPackageLPr(pkgName);}boolean isUpdatedSystemAppFromExistingSetting = pkgSetting != null&& pkgSetting.getPkgState().isUpdatedSystemApp();final String abiOverride = deriveAbiOverride(args.abiOverride);AndroidPackage oldPackage = mPackages.get(pkgName);boolean isUpdatedSystemAppInferred = oldPackage != null && oldPackage.isSystem();final Pair<PackageAbiHelper.Abis, PackageAbiHelper.NativeLibraryPaths>derivedAbi = mInjector.getAbiHelper().derivePackageAbi(parsedPackage,isUpdatedSystemAppFromExistingSetting || isUpdatedSystemAppInferred,abiOverride, mAppLib32InstallDir);derivedAbi.first.applyTo(parsedPackage);derivedAbi.second.applyTo(parsedPackage);} catch (PackageManagerException pme) {Slog.e(TAG, "Error deriving application ABI", pme);throw new PrepareFailure(INSTALL_FAILED_INTERNAL_ERROR,"Error deriving application ABI: " + pme.getMessage());}}if (!args.doRename(res.returnCode, parsedPackage)) {throw new PrepareFailure(INSTALL_FAILED_INSUFFICIENT_STORAGE, "Failed rename");}try {setUpFsVerityIfPossible(parsedPackage);} catch (InstallerException | IOException | DigestException | NoSuchAlgorithmException e) {throw new PrepareFailure(INSTALL_FAILED_INTERNAL_ERROR,"Failed to set up verity: " + e);}final PackageFreezer freezer =freezePackageForInstall(pkgName, installFlags, "installPackageLI");boolean shouldCloseFreezerBeforeReturn = true;

  如果是系统app,不能更新到外部空间,不能使用instant app方式更新。
  args.move != null代表是需要移动应用包,这里先不说。
  接着将scanFlags添加上SCAN_NO_DEX标识。
  再接下来就是处理本地库和确定使用的指令集。mInjector.getAbiHelper()在这里是PackageAbiHelperImpl对象,调用它的derivePackageAbi()方法,就是确定使用的指令abi,还会将对应的so包文件拷贝出来,放到对应的文件夹中。这块参看 Android 提取出Apk的本地库 该篇文章。使用的指令abi就在结果derivedAbi的first中,复制出来的so文件所在文件信息在derivedAbi.second中,所以将它们设置到解析包对象parsedPackage中。看下对应的类的apply方法如下:

    final class Abis {public void applyTo(ParsedPackage pkg) {pkg.setPrimaryCpuAbi(primary).setSecondaryCpuAbi(secondary);}   }…………final class NativeLibraryPaths {public void applyTo(ParsedPackage pkg) {pkg.setNativeLibraryRootDir(nativeLibraryRootDir).setNativeLibraryRootRequiresIsa(nativeLibraryRootRequiresIsa).setNativeLibraryDir(nativeLibraryDir).setSecondaryNativeLibraryDir(secondaryNativeLibraryDir);}}             

  可见这里是将对应值设置到pkg对象(实际是PackageImpl对象)的primaryCpuAbi、secondaryCpuAbi、nativeLibraryRootDir、nativeLibraryRootRequiresIsa、nativeLibraryDir、secondaryNativeLibraryDir中。
  接下来调用args.doRename(res.returnCode, parsedPackage),这里args是FileInstallArgs对象,所以会调用FileInstallArgs类的doRename(int status, ParsedPackage parsedPackage)方法,这里是重命名安装文件的目录。
  接下来就是处理安装文件,如果可能,将它enable fs-verity。
  下面会调用freezePackageForInstall(pkgName, installFlags, “installPackageLI”)创建一个PackageFreezer。如果installFlags没有INSTALL_DONT_KILL_APP标识,并且之前已经安装过该应用,在创建PackageFreezer时,会将该应用杀掉,以防止它继续运行,导致混乱。
  shouldCloseFreezerBeforeReturn变量是说该方法执行完返回时,是否应该关闭刚才创建的PackageFreezer对象。
  下面先看看重命名安装文件的目录,也即FileInstallArgs类的doRename()方法,再看看文件使能fs-verity,也即setUpFsVerityIfPossible(parsedPackage)方法。

重命名安装文件的目录

    class FileInstallArgs extends InstallArgs {……boolean doRename(int status, ParsedPackage parsedPackage) {if (status != PackageManager.INSTALL_SUCCEEDED) {cleanUp();return false;}final File targetDir = resolveTargetDir();final File beforeCodeFile = codeFile;final File afterCodeFile = getNextCodePath(targetDir, parsedPackage.getPackageName());if (DEBUG_INSTALL) Slog.d(TAG, "Renaming " + beforeCodeFile + " to " + afterCodeFile);final boolean onIncremental = mIncrementalManager != null&& isIncrementalPath(beforeCodeFile.getAbsolutePath());try {makeDirRecursive(afterCodeFile.getParentFile(), 0775);if (onIncremental) {// Just link files here. The stage dir will be removed when the installation// session is completed.mIncrementalManager.linkCodePath(beforeCodeFile, afterCodeFile);} else {Os.rename(beforeCodeFile.getAbsolutePath(), afterCodeFile.getAbsolutePath());}} catch (IOException | ErrnoException e) {Slog.w(TAG, "Failed to rename", e);return false;}if (!onIncremental && !SELinux.restoreconRecursive(afterCodeFile)) {Slog.w(TAG, "Failed to restorecon");return false;}// Reflect the rename internallycodeFile = afterCodeFile;// Reflect the rename in scanned detailstry {parsedPackage.setCodePath(afterCodeFile.getCanonicalPath());} catch (IOException e) {Slog.e(TAG, "Failed to get path: " + afterCodeFile, e);return false;}parsedPackage.setBaseCodePath(FileUtils.rewriteAfterRename(beforeCodeFile,afterCodeFile, parsedPackage.getBaseApkPath()));parsedPackage.setSplitCodePaths(FileUtils.rewriteAfterRename(beforeCodeFile,afterCodeFile, parsedPackage.getSplitCodePaths()));return true;} ……}           	

  如果当前结果已经是失败了,就调用cleanUp()执行清理。
  紧接着是调用resolveTargetDir()得到目标目录,看一下它的实现,

        private File resolveTargetDir() {boolean isStagedInstall = (installFlags & INSTALL_STAGED) != 0;if (isStagedInstall) {return Environment.getDataAppDirectory(null);} else {return codeFile.getParentFile();}}

  如果是staged安装,则目录为“/data/app”。如果是正常安装,得到成员变量codeFile的父目录。我们知道我们这里codeFile也是安装文件的目录,它里面包含安装文件。像我们的例子中如果是内置存储位置,codeFile为"/data/app/vmdl" + sessionId + “.tmp”,里面有安装文件为“base.apk”。所以这里得到的目标目录为"/data/app/“。
  回到doRename()中,将beforeCodeFile为之前的安装目录。
  再通过getNextCodePath()得到之后安装文件的目录文件,赋值给afterCodeFile。它的格式为 targetDir/~~[randomStrA]/[packageName]-[randomStrB],其中randomStrA、randomStrB都是随机数。
  接着会创建新生成目录。之后,会调用Os.rename(beforeCodeFile.getAbsolutePath(), afterCodeFile.getAbsolutePath())将之前的安装目录重命名为新生成的目录。像我们的例子即将”/data/app/vmdl" + sessionId + “.tmp”目录重命名为 /data/app/~~[randomStrA]/[packageName]-[randomStrB]。
  之后,会将codeFile指向新生成的目录。
  再之后,会将解析包对象的path设置为新生成的目录。
  下面也是设置mBaseApkPath为新生成的目录+安装包的名字。splitCodePaths为新生成的目录+其他安装包的名字。

文件设置fs-verity

    /*** Set up fs-verity for the given package if possible.  This requires a feature flag of system* property to be enabled only if the kernel supports fs-verity.** <p>When the feature flag is set to legacy mode, only APK is supported (with some experimental* kernel patches). In normal mode, all file format can be supported.*/private void setUpFsVerityIfPossible(AndroidPackage pkg) throws InstallerException,PrepareFailure, IOException, DigestException, NoSuchAlgorithmException {final boolean standardMode = PackageManagerServiceUtils.isApkVerityEnabled();final boolean legacyMode = PackageManagerServiceUtils.isLegacyApkVerityEnabled();if (!standardMode && !legacyMode) {return;}if (isIncrementalPath(pkg.getPath()) && IncrementalManager.getVersion()< IncrementalManager.MIN_VERSION_TO_SUPPORT_FSVERITY) {return;}// Collect files we care for fs-verity setup.ArrayMap<String, String> fsverityCandidates = new ArrayMap<>();if (legacyMode) {synchronized (mLock) {final PackageSetting ps = mSettings.getPackageLPr(pkg.getPackageName());if (ps != null && ps.isPrivileged()) {fsverityCandidates.put(pkg.getBaseApkPath(), null);if (pkg.getSplitCodePaths() != null) {for (String splitPath : pkg.getSplitCodePaths()) {fsverityCandidates.put(splitPath, null);}}}}} else {// NB: These files will become only accessible if the signing key is loaded in kernel's// .fs-verity keyring.fsverityCandidates.put(pkg.getBaseApkPath(),VerityUtils.getFsveritySignatureFilePath(pkg.getBaseApkPath()));final String dmPath = DexMetadataHelper.buildDexMetadataPathForApk(pkg.getBaseApkPath());if (new File(dmPath).exists()) {fsverityCandidates.put(dmPath, VerityUtils.getFsveritySignatureFilePath(dmPath));}if (pkg.getSplitCodePaths() != null) {for (String path : pkg.getSplitCodePaths()) {fsverityCandidates.put(path, VerityUtils.getFsveritySignatureFilePath(path));final String splitDmPath = DexMetadataHelper.buildDexMetadataPathForApk(path);if (new File(splitDmPath).exists()) {fsverityCandidates.put(splitDmPath,VerityUtils.getFsveritySignatureFilePath(splitDmPath));}}}}for (Map.Entry<String, String> entry : fsverityCandidates.entrySet()) {final String filePath = entry.getKey();final String signaturePath = entry.getValue();if (!legacyMode) {// fs-verity is optional for now.  Only set up if signature is provided.if (new File(signaturePath).exists() && !VerityUtils.hasFsverity(filePath)) {try {VerityUtils.setUpFsverity(filePath, signaturePath);} catch (IOException e) {throw new PrepareFailure(PackageManager.INSTALL_FAILED_BAD_SIGNATURE,"Failed to enable fs-verity: " + e);}}continue;}// In legacy mode, fs-verity can only be enabled by process with CAP_SYS_ADMIN.final VerityUtils.SetupResult result = VerityUtils.generateApkVeritySetupData(filePath);if (result.isOk()) {if (Build.IS_DEBUGGABLE) Slog.i(TAG, "Enabling verity to " + filePath);final FileDescriptor fd = result.getUnownedFileDescriptor();try {final byte[] rootHash = VerityUtils.generateApkVerityRootHash(filePath);try {// A file may already have fs-verity, e.g. when reused during a split// install. If the measurement succeeds, no need to attempt to set up.mInstaller.assertFsverityRootHashMatches(filePath, rootHash);} catch (InstallerException e) {mInstaller.installApkVerity(filePath, fd, result.getContentSize());mInstaller.assertFsverityRootHashMatches(filePath, rootHash);}} finally {IoUtils.closeQuietly(fd);}} else if (result.isFailed()) {throw new PrepareFailure(PackageManager.INSTALL_FAILED_BAD_SIGNATURE,"Failed to generate verity");}}}

  变量standardMode、legacyMode代表两种模式:标准模式、遗留模式。看一下标准模式、遗留模式的条件:

    /** Returns true if standard APK Verity is enabled. */static boolean isApkVerityEnabled() {return Build.VERSION.DEVICE_INITIAL_SDK_INT >= Build.VERSION_CODES.R|| SystemProperties.getInt("ro.apk_verity.mode", FSVERITY_DISABLED)== FSVERITY_ENABLED;}static boolean isLegacyApkVerityEnabled() {return SystemProperties.getInt("ro.apk_verity.mode", FSVERITY_DISABLED) == FSVERITY_LEGACY;}

  可见和系统SDK版本和系统属性"ro.apk_verity.mode"的值有关。如果SDK版本大于等于R时或者"ro.apk_verity.mode"的值为FSVERITY_ENABLED时,就为标准模式。如果"ro.apk_verity.mode"的值为FSVERITY_LEGACY时,为遗留模式。所谓标准模式,就是以后新的都使用这种模式,而遗留模式则是之前实现的,现在需要考虑兼容的。
  如果这两种模式都不满足,就结束执行。
  下面就要收集哪些文件需要建立fs-verity。fsverityCandidates就是收集的文件集合。
  如果在遗留模式下,当前解析包的应用是特权应用,会将它的基本包和分包安装文件收集到fsverityCandidates中的key。fsverityCandidates的value值是文件对应使用的证书文件,之前是不用的,所以设置为null。

标准模式fsverity

  如果是在标准模式下,会将解析包的基本安装文件路径和证书文件路径放到fsverityCandidates中。证书文件是安装文件名+".fsv_sig"的文件,它们在一个文件夹中。
  如果对应安装文件的dex元数据文件(它的名字是安装文件名字的“.apk”替换成“.dm”)存在,也会将它放到fsverityCandidates集合中。
  同样如果存在分包,也和基础安装文件是同样的处理。
  再往下就是循环fsverityCandidates集合中的文件,进行设置fsverity。
  可以看到,如果是在标准模式下,fsverity也不是必定进行的,也得提供证书的情况下,才会调用VerityUtils.setUpFsverity(filePath, signaturePath)设置fsverity。并且使用的这个公钥需要在.fs-verity内核密钥环中。

遗留模式fsverity

  如果是在遗留模式下,看一下如何设置fsverity。
  VerityUtils.generateApkVeritySetupData(filePath)方法,它会生成设置fsverity的数据,并且它里面会进行一些验证。如果数据生成成功了,它生成设置fsverity的数据会放在一个共享内存中,result.getUnownedFileDescriptor()得到文件描述符即指向共享内存。
  VerityUtils.generateApkVerityRootHash(filePath)是生成摘要hash。它是用来判断文件是否已经设置fsverity。mInstaller.assertFsverityRootHashMatches(filePath, rootHash)就是为了验证文件是否已经设置fsverity,因为有的文件可能已经设置过fsverity,所以如果验证通过,接着就关闭共享内存。如果文件没有设置fsverity,则会调用mInstaller.installApkVerity(filePath, fd, result.getContentSize())是执行设置fsverity,设置之后,再调用一遍mInstaller.assertFsverityRootHashMatches(filePath, rootHash)执行验证。
  如果生成设置fsverity的数据的方法返回失败了,会扔出PrepareFailure异常。
  下面来看看生成设置fsverity的数据,然后再看看生成验证摘要数据,之后看它如何设置fsverity。

生成设置fsverity的数据

  它的实现是VerityUtils.generateApkVeritySetupData(filePath),看一下它的代码:

    /*** Generates legacy Merkle tree and fs-verity metadata with Signing Block skipped.** @deprecated This is only used for previous fs-verity implementation, and should never be used*             on new devices.* @return {@code SetupResult} that contains the result code, and when success, the*         {@code FileDescriptor} to read all the data from.*/@Deprecatedpublic static SetupResult generateApkVeritySetupData(@NonNull String apkPath) {if (DEBUG) {Slog.d(TAG, "Trying to install legacy apk verity to " + apkPath);}SharedMemory shm = null;try {final byte[] signedVerityHash = ApkSignatureVerifier.getVerityRootHash(apkPath);if (signedVerityHash == null) {if (DEBUG) {Slog.d(TAG, "Skip verity tree generation since there is no signed root hash");}return SetupResult.skipped();}Pair<SharedMemory, Integer> result =generateFsVerityIntoSharedMemory(apkPath, signedVerityHash);shm = result.first;int contentSize = result.second;FileDescriptor rfd = shm.getFileDescriptor();if (rfd == null || !rfd.valid()) {return SetupResult.failed();}return SetupResult.ok(Os.dup(rfd), contentSize);} catch (IOException | SecurityException | DigestException | NoSuchAlgorithmException| SignatureNotFoundException | ErrnoException e) {Slog.e(TAG, "Failed to set up apk verity: ", e);return SetupResult.failed();} finally {if (shm != null) {shm.close();}}}

  ApkSignatureVerifier.getVerityRootHash(apkPath)主要就是从Apk文件中的签名分块中找到验证的根hash。这些内容可以参考一下该篇博客 Android APK文件的签名V2查找、验证。如果没有该验证摘要,就返回SetupResult.skipped(),代表跳过。
  接着是调用generateFsVerityIntoSharedMemory(apkPath, signedVerityHash)来生成建立FsVerity的数据并将它们放入共享内存中。他返回的结果是一个Pair对象result,result.first是一个共享内存对象,result.second则代表其中数据的长度。如果没有问题,会返回SetupResult.ok(Os.dup(rfd), contentSize)。其中rfd是共享内存的文件描述符,contentSize是数据的长度。
  看一下generateFsVerityIntoSharedMemory(apkPath, signedVerityHash)方法:

    /*** Returns a pair of {@code SharedMemory} and {@code Integer}. The {@code SharedMemory} contains* Merkle tree and fsverity headers for the given apk, in the form that can immediately be used* for fsverity setup. The data is aligned to the beginning of {@code SharedMemory}, and has* length equals to the returned {@code Integer}.*/private static Pair<SharedMemory, Integer> generateFsVerityIntoSharedMemory(String apkPath,@NonNull byte[] expectedRootHash)throws IOException, DigestException, NoSuchAlgorithmException,SignatureNotFoundException {TrackedShmBufferFactory shmBufferFactory = new TrackedShmBufferFactory();byte[] generatedRootHash =ApkSignatureVerifier.generateApkVerity(apkPath, shmBufferFactory);// We only generate Merkle tree once here, so it's important to make sure the root hash// matches the signed one in the apk.if (!Arrays.equals(expectedRootHash, generatedRootHash)) {throw new SecurityException("verity hash mismatch: "+ bytesToString(generatedRootHash) + " != " + bytesToString(expectedRootHash));}int contentSize = shmBufferFactory.getBufferLimit();SharedMemory shm = shmBufferFactory.releaseSharedMemory();if (shm == null) {throw new IllegalStateException("Failed to generate verity tree into shared memory");}if (!shm.setProtect(OsConstants.PROT_READ)) {throw new SecurityException("Failed to set up shared memory correctly");}return Pair.create(shm, contentSize);}

  TrackedShmBufferFactory是用来创建共享内存的类。ApkSignatureVerifier.generateApkVerity(apkPath, shmBufferFactory)是用来生成Merkle树根的摘要值,同时生成设置fsverity的数据也在刚生成的参数shmBufferFactory中。
  接着拿生成Merkle树根的摘要值和参数中的摘要值expectedRootHash相比,如果不相等,会报异常。
  shmBufferFactory.getBufferLimit()是设置fsverity的数据的长度,shmBufferFactory.releaseSharedMemory()是得到共享内存对象shm 。最后是构造成Pair对象返回。
  再看一下ApkSignatureVerifier.generateApkVerity(apkPath, shmBufferFactory)的实现:

    public static byte[] generateApkVerity(String apkPath, ByteBufferFactory bufferFactory)throws IOException, SignatureNotFoundException, SecurityException, DigestException,NoSuchAlgorithmException {// first try v3try {return ApkSignatureSchemeV3Verifier.generateApkVerity(apkPath, bufferFactory);} catch (SignatureNotFoundException e) {// try older version}return ApkSignatureSchemeV2Verifier.generateApkVerity(apkPath, bufferFactory);}

  它还是先按照V3签名的方式处理,如果没找到V3签名就会去按照V2签名的方式生成根的hash。这里就按照V2签名的方式来说,它的实现在ApkSignatureSchemeV2Verifier类中:

    static byte[] generateApkVerity(String apkPath, ByteBufferFactory bufferFactory)throws IOException, SignatureNotFoundException, SecurityException, DigestException,NoSuchAlgorithmException {try (RandomAccessFile apk = new RandomAccessFile(apkPath, "r")) {SignatureInfo signatureInfo = findSignature(apk);return VerityBuilder.generateApkVerity(apkPath, bufferFactory, signatureInfo);}}

  这里是找到签名的信息对象signatureInfo,然后调用VerityBuilder类的generateApkVerity(apkPath, bufferFactory, signatureInfo)方法

    /*** Generates the apk-verity header and hash tree to be used by kernel for the given apk. This* method does not check whether the root hash exists in the Signing Block or not.** <p>The output is stored in the {@link ByteBuffer} created by the given {@link* ByteBufferFactory}.** @return the root hash of the generated hash tree.*/@NonNullstatic byte[] generateApkVerity(@NonNull String apkPath,@NonNull ByteBufferFactory bufferFactory, @NonNull SignatureInfo signatureInfo)throws IOException, SignatureNotFoundException, SecurityException, DigestException,NoSuchAlgorithmException {try (RandomAccessFile apk = new RandomAccessFile(apkPath, "r")) {VerityResult result = generateVerityTreeInternal(apk, bufferFactory, signatureInfo);ByteBuffer footer = slice(result.verityData, result.merkleTreeSize,result.verityData.limit());generateApkVerityFooter(apk, signatureInfo, footer);// Put the reverse offset to apk-verity header at the end.footer.putInt(footer.position() + 4);result.verityData.limit(result.merkleTreeSize + footer.position());return result.rootHash;}}

  generateVerityTreeInternal(apk, bufferFactory, signatureInfo)是通过构建merkle树来计算出来树根的摘要值。它还会计算出来merkle树的大小和对应的数据。这块参考一下该篇博客 Android APK文件完整性验证 。
  接下来会处理设置fsverity的所需数据的尾部数据footer。它是通过generateApkVerityFooter(apk, signatureInfo, footer)实现的,接着会在尾部数据添加上数据的位置footer.position() + 4。
  VerityResult类型result是上面构造merkle树方法计算出来的结果,result.verityData即是设置fsverity的所需数据。它在这里会将它的数据所在的位置设置为result.merkleTreeSize + footer.position()。result.merkleTreeSize即为merkle树的大小,footer.position()为数据尾部数据大小。
  result.rootHash即为计算出来树根的摘要值,将它返回。

fsverity的所需数据的尾部数据的生成

  咱们看下尾部数据footer的生成,

    static void generateApkVerityFooter(@NonNull RandomAccessFile apk,@NonNull SignatureInfo signatureInfo, @NonNull ByteBuffer footerOutput)throws IOException {footerOutput.order(ByteOrder.LITTLE_ENDIAN);generateApkVerityHeader(footerOutput, apk.length(), DEFAULT_SALT);long signingBlockSize =signatureInfo.centralDirOffset - signatureInfo.apkSigningBlockOffset;generateApkVerityExtensions(footerOutput, signatureInfo.apkSigningBlockOffset,signingBlockSize, signatureInfo.eocdOffset);}

  先是调用generateApkVerityHeader(footerOutput, apk.length(), DEFAULT_SALT)生成尾部数据的头数据,之后调用generateApkVerityExtensions(footerOutput, signatureInfo.apkSigningBlockOffset, signingBlockSize, signatureInfo.eocdOffset)生成尾部数据的扩展数据。signingBlockSize 是签名块的大小。
   先看下头数据的生成:

    private static ByteBuffer generateApkVerityHeader(ByteBuffer buffer, long fileSize,byte[] salt) {if (salt.length != 8) {throw new IllegalArgumentException("salt is not 8 bytes long");}// TODO(b/30972906): update the reference when there is a better one in public.buffer.put("TrueBrew".getBytes());  // magicbuffer.put((byte) 1);               // major versionbuffer.put((byte) 0);               // minor versionbuffer.put((byte) 12);              // log2(block-size): log2(4096)buffer.put((byte) 7);               // log2(leaves-per-node): log2(4096 / 32)buffer.putShort((short) 1);         // meta algorithm, SHA256 == 1buffer.putShort((short) 1);         // data algorithm, SHA256 == 1buffer.putInt(0);                   // flagsbuffer.putInt(0);                   // reservedbuffer.putLong(fileSize);           // original file sizebuffer.put((byte) 2);               // authenticated extension countbuffer.put((byte) 0);               // unauthenticated extension countbuffer.put(salt);                   // salt (8 bytes)skip(buffer, 22);                   // reservedreturn buffer;}

  可以根据注释看一下每个字段的意思,总共64个字节的数据。魔数为"TrueBrew",相关版本、对应算法、文件大小、盐等数据。这里的盐是8个数值为0的byte数组。
   先看下扩展数据的生成:

    private static ByteBuffer generateApkVerityExtensions(ByteBuffer buffer,long signingBlockOffset, long signingBlockSize, long eocdOffset) {// Snapshot of the experimental fs-verity structs (different from upstream).//// struct fsverity_extension_elide {//   __le64 offset;//   __le64 length;// }//// struct fsverity_extension_patch {//   __le64 offset;//   u8 databytes[];// };final int kSizeOfFsverityExtensionHeader = 8;final int kExtensionSizeAlignment = 8;{// struct fsverity_extension #1final int kSizeOfFsverityElidedExtension = 16;// First field is total size of extension, padded to 64-bit alignmentbuffer.putInt(kSizeOfFsverityExtensionHeader + kSizeOfFsverityElidedExtension);buffer.putShort((short) 1);  // ID of elide extensionskip(buffer, 2);             // reserved// struct fsverity_extension_elidebuffer.putLong(signingBlockOffset);buffer.putLong(signingBlockSize);}{// struct fsverity_extension #2final int kTotalSize = kSizeOfFsverityExtensionHeader+ 8 // offset size+ ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_SIZE;buffer.putInt(kTotalSize);   // Total size of extension, padded to 64-bit alignmentbuffer.putShort((short) 2);  // ID of patch extensionskip(buffer, 2);             // reserved// struct fsverity_extension_patchbuffer.putLong(eocdOffset + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET);  // offsetbuffer.putInt(Math.toIntExact(signingBlockOffset));  // databytes// The extension needs to be 0-padded at the end, since the length may not be multiple// of 8.int kPadding = kExtensionSizeAlignment - kTotalSize % kExtensionSizeAlignment;if (kPadding == kExtensionSizeAlignment) {kPadding = 0;}skip(buffer, kPadding);      // padding}return buffer;}

  看这注释,它这数据是按照fsverity_extension_elide、fsverity_extension_patch结构来进行填充的。
  其中方法参数signingBlockOffset是签名块的偏移值、signingBlockSize是签名块的大小、eocdOffset是中央目录尾部的数据在apk文件中的偏移量。
  这样我们就看完了fsverity的所需数据。前面是Merkle树的数据,后面是尾部数据,尾部数据包括头和扩展数据。头数据是一些魔数为"TrueBrew",相关版本、对应算法、文件大小、盐等数据。扩展数据是和fsverity_extension_elide、fsverity_extension_patch结构相关的数据。

设置文件fsverity

  设置文件fsverity是Installer对象的installApkVerity(String filePath, FileDescriptor verityInput, int contentSize)实现的,Installer对象又通过AIDL与installd进程进行交互。它最终会调用到InstalldNativeService.cpp文件中的installApkVerity(const std::string& filePath, android::base::unique_fd verityInputAshmem, int32_t contentSize)方法:

binder::Status InstalldNativeService::installApkVerity(const std::string& filePath,android::base::unique_fd verityInputAshmem, int32_t contentSize) {ENFORCE_UID(AID_SYSTEM);CHECK_ARGUMENT_PATH(filePath);std::lock_guard<std::recursive_mutex> lock(mLock);if (!android::base::GetBoolProperty(kPropApkVerityMode, false)) {return ok();}
#ifndef NDEBUGASSERT_PAGE_SIZE_4K();
#endif// TODO: also check fsverity support in the current file system if compiled with DEBUG.// TODO: change ashmem to some temporary file to support huge apk.if (!ashmem_valid(verityInputAshmem.get())) {return error("FD is not an ashmem");}// 1. Seek to the next page boundary beyond the end of the file.::android::base::unique_fd wfd(open(filePath.c_str(), O_WRONLY));if (wfd.get() < 0) {return error("Failed to open " + filePath);}struct stat st;if (fstat(wfd.get(), &st) < 0) {return error("Failed to stat " + filePath);}// fsverity starts from the block boundary.off_t padding = kVerityPageSize - st.st_size % kVerityPageSize;if (padding == kVerityPageSize) {padding = 0;}if (lseek(wfd.get(), st.st_size + padding, SEEK_SET) < 0) {return error("Failed to lseek " + filePath);}// 2. Write everything in the ashmem to the file.  Note that allocated//    ashmem size is multiple of page size, which is different from the//    actual content size.int shmSize = ashmem_get_size_region(verityInputAshmem.get());if (shmSize < 0) {return error("Failed to get ashmem size: " + std::to_string(shmSize));}if (contentSize < 0) {return error("Invalid content size: " + std::to_string(contentSize));}if (contentSize > shmSize) {return error("Content size overflow: " + std::to_string(contentSize) + " > " +std::to_string(shmSize));}auto data = std::unique_ptr<void, std::function<void (void *)>>(mmap(nullptr, contentSize, PROT_READ, MAP_SHARED, verityInputAshmem.get(), 0),[contentSize] (void* ptr) {if (ptr != MAP_FAILED) {munmap(ptr, contentSize);}});if (data.get() == MAP_FAILED) {return error("Failed to mmap the ashmem");}char* cursor = reinterpret_cast<char*>(data.get());int remaining = contentSize;while (remaining > 0) {int ret = TEMP_FAILURE_RETRY(write(wfd.get(), cursor, remaining));if (ret < 0) {return error("Failed to write to " + filePath + " (" + std::to_string(remaining) ++ "/" + std::to_string(contentSize) + ")");}cursor += ret;remaining -= ret;}wfd.reset();// 3. Enable fsverity (needs readonly fd. Once it's done, the file becomes immutable.::android::base::unique_fd rfd(open(filePath.c_str(), O_RDONLY));if (ioctl(rfd.get(), FS_IOC_ENABLE_VERITY, nullptr) < 0) {return error("Failed to enable fsverity on " + filePath);}return ok();
}

  参数verityInputAshmem是共享内存的文件描述符,contentSize是设置fsverity数据的长度,filePath是文件的路径。
  先打开文件filePath,得到wfd文件描述符。然后调用fstat,得到文件信息,如果文件长度不是kVerityPageSize(4KB)的整数倍,需要得到填充数据的数量padding。
  接着调用lseek,将文件设置到文件长度+padding的位置。
  下面shmSize是共享内存的大小,如果数据大小contentSize大于共享内存大小会报溢出错误。
  接着data是通过mmap映射到共享内存的数据。
  然后通过while循环将数据写入文件的文件长度+padding的位置之后。
  再打开文件,通过ioctl调用FS_IOC_ENABLE_VERITY命令,将文件使能fsverity。
  这样我们就将遗留模式的文件使能fsverity说完了。

  下面我们接着返回到preparePackageLI(InstallArgs args, PackageInstalledInfo res)方法中,继续看代码

分段四

  分段四:

        try {final AndroidPackage existingPackage;String renamedPackage = null;boolean sysPkg = false;int targetScanFlags = scanFlags;int targetParseFlags = parseFlags;final PackageSetting ps;final PackageSetting disabledPs;if (replace) {if (parsedPackage.isStaticSharedLibrary()) {// Static libs have a synthetic package name containing the version// and cannot be updated as an update would get a new package name,// unless this is installed from adb which is useful for development.AndroidPackage existingPkg = mPackages.get(parsedPackage.getPackageName());if (existingPkg != null&& (installFlags & PackageManager.INSTALL_FROM_ADB) == 0) {throw new PrepareFailure(INSTALL_FAILED_DUPLICATE_PACKAGE,"Packages declaring "+ "static-shared libs cannot be updated");}}final boolean isInstantApp = (scanFlags & SCAN_AS_INSTANT_APP) != 0;final AndroidPackage oldPackage;final String pkgName11 = parsedPackage.getPackageName();final int[] allUsers;final int[] installedUsers;final int[] uninstalledUsers;synchronized (mLock) {oldPackage = mPackages.get(pkgName11);existingPackage = oldPackage;if (DEBUG_INSTALL) {Slog.d(TAG,"replacePackageLI: new=" + parsedPackage + ", old=" + oldPackage);}ps = mSettings.getPackageLPr(pkgName11);disabledPs = mSettings.getDisabledSystemPkgLPr(ps);// verify signatures are validfinal KeySetManagerService ksms = mSettings.getKeySetManagerService();if (ksms.shouldCheckUpgradeKeySetLocked(ps, scanFlags)) {if (!ksms.checkUpgradeKeySetLocked(ps, parsedPackage)) {throw new PrepareFailure(INSTALL_FAILED_UPDATE_INCOMPATIBLE,"New package not signed by keys specified by upgrade-keysets: "+ pkgName11);}} else {SigningDetails parsedPkgSigningDetails = parsedPackage.getSigningDetails();SigningDetails oldPkgSigningDetails = oldPackage.getSigningDetails();// default to original signature matchingif (!parsedPkgSigningDetails.checkCapability(oldPkgSigningDetails,SigningDetails.CertCapabilities.INSTALLED_DATA)&& !oldPkgSigningDetails.checkCapability(parsedPkgSigningDetails,SigningDetails.CertCapabilities.ROLLBACK)) {// Allow the update to proceed if this is a rollback and the parsed// package's current signing key is the current signer or in the lineage// of the old package; this allows a rollback to a previously installed// version after an app's signing key has been rotated without requiring// the rollback capability on the previous signing key.if (!isRollback || !oldPkgSigningDetails.hasAncestorOrSelf(parsedPkgSigningDetails)) {throw new PrepareFailure(INSTALL_FAILED_UPDATE_INCOMPATIBLE,"New package has a different signature: " + pkgName11);}}}// don't allow a system upgrade unless the upgrade hash matchesif (oldPackage.getRestrictUpdateHash() != null && oldPackage.isSystem()) {final byte[] digestBytes;try {final MessageDigest digest = MessageDigest.getInstance("SHA-512");updateDigest(digest, new File(parsedPackage.getBaseApkPath()));if (!ArrayUtils.isEmpty(parsedPackage.getSplitCodePaths())) {for (String path : parsedPackage.getSplitCodePaths()) {updateDigest(digest, new File(path));}}digestBytes = digest.digest();} catch (NoSuchAlgorithmException | IOException e) {throw new PrepareFailure(INSTALL_FAILED_INVALID_APK,"Could not compute hash: " + pkgName11);}if (!Arrays.equals(oldPackage.getRestrictUpdateHash(), digestBytes)) {throw new PrepareFailure(INSTALL_FAILED_INVALID_APK,"New package fails restrict-update check: " + pkgName11);}// retain upgrade restrictionparsedPackage.setRestrictUpdateHash(oldPackage.getRestrictUpdateHash());}// Check for shared user id changesString invalidPackageName = null;if (!Objects.equals(oldPackage.getSharedUserId(),parsedPackage.getSharedUserId())) {invalidPackageName = parsedPackage.getPackageName();}if (invalidPackageName != null) {throw new PrepareFailure(INSTALL_FAILED_SHARED_USER_INCOMPATIBLE,"Package " + invalidPackageName + " tried to change user "+ oldPackage.getSharedUserId());}// In case of rollback, remember per-user/profile install stateallUsers = mUserManager.getUserIds();installedUsers = ps.queryInstalledUsers(allUsers, true);uninstalledUsers = ps.queryInstalledUsers(allUsers, false);// don't allow an upgrade from full to ephemeralif (isInstantApp) {if (args.user == null || args.user.getIdentifier() == UserHandle.USER_ALL) {for (int currentUser : allUsers) {if (!ps.getInstantApp(currentUser)) {// can't downgrade from full to instantSlog.w(TAG,"Can't replace full app with instant app: " + pkgName11+ " for user: " + currentUser);throw new PrepareFailure(PackageManager.INSTALL_FAILED_SESSION_INVALID);}}} else if (!ps.getInstantApp(args.user.getIdentifier())) {// can't downgrade from full to instantSlog.w(TAG, "Can't replace full app with instant app: " + pkgName11+ " for user: " + args.user.getIdentifier());throw new PrepareFailure(PackageManager.INSTALL_FAILED_SESSION_INVALID);}}}// Update what is removedres.removedInfo = new PackageRemovedInfo(this);res.removedInfo.uid = oldPackage.getUid();res.removedInfo.removedPackage = oldPackage.getPackageName();res.removedInfo.installerPackageName = ps.installSource.installerPackageName;res.removedInfo.isStaticSharedLib = parsedPackage.getStaticSharedLibName() != null;res.removedInfo.isUpdate = true;res.removedInfo.origUsers = installedUsers;res.removedInfo.installReasons = new SparseArray<>(installedUsers.length);for (int i = 0; i < installedUsers.length; i++) {final int userId = installedUsers[i];res.removedInfo.installReasons.put(userId, ps.getInstallReason(userId));}res.removedInfo.uninstallReasons = new SparseArray<>(uninstalledUsers.length);for (int i = 0; i < uninstalledUsers.length; i++) {final int userId = uninstalledUsers[i];res.removedInfo.uninstallReasons.put(userId, ps.getUninstallReason(userId));}sysPkg = oldPackage.isSystem();if (sysPkg) {// Set the system/privileged/oem/vendor/product flags as neededfinal boolean privileged = oldPackage.isPrivileged();final boolean oem = oldPackage.isOem();final boolean vendor = oldPackage.isVendor();final boolean product = oldPackage.isProduct();final boolean odm = oldPackage.isOdm();final boolean systemExt = oldPackage.isSystemExt();final @ParseFlags int systemParseFlags = parseFlags;final @ScanFlags int systemScanFlags = scanFlags| SCAN_AS_SYSTEM| (privileged ? SCAN_AS_PRIVILEGED : 0)| (oem ? SCAN_AS_OEM : 0)| (vendor ? SCAN_AS_VENDOR : 0)| (product ? SCAN_AS_PRODUCT : 0)| (odm ? SCAN_AS_ODM : 0)| (systemExt ? SCAN_AS_SYSTEM_EXT : 0);if (DEBUG_INSTALL) {Slog.d(TAG, "replaceSystemPackageLI: new=" + parsedPackage+ ", old=" + oldPackage);}res.setReturnCode(PackageManager.INSTALL_SUCCEEDED);targetParseFlags = systemParseFlags;targetScanFlags = systemScanFlags;} else { // non system replacereplace = true;if (DEBUG_INSTALL) {Slog.d(TAG,"replaceNonSystemPackageLI: new=" + parsedPackage + ", old="+ oldPackage);}}} else { // new package install

  如果replace为true,代表是一个更新安装。下面这段代码都是处理更新安装的情况。
  如果安装包是一个静态分享库,它的名字会包含版本号,如果允许更新会生成一个新的包名,这是不允许,除非是使用adb安装形式,是可以的。
  ps是旧的PackageSetting对象,代码分段二中说过了KeySetManagerService对象,ksms.shouldCheckUpgradeKeySetLocked(signatureCheckPs, scanFlags)这种是对于APK里面Manifest文件中配置了"key-sets"标签的应用来说的。一般的应用不会配置它。
  进入到else分支后,是检查签名验证的。如果parsedPkgSigningDetails.checkCapability(oldPkgSigningDetails, SigningDetails.CertCapabilities.INSTALLED_DATA)和oldPkgSigningDetails.checkCapability(parsedPkgSigningDetails, SigningDetails.CertCapabilities.ROLLBACK)都不满足的情况下,isRollback为true并且oldPkgSigningDetails.hasAncestorOrSelf( parsedPkgSigningDetails)为true才会不报PrepareFailure异常。
  系统包一般不允许更新,除非更新文件的hash摘要也旧包的相同。而新文件的hash摘要是使用文件的内容进行"SHA-512"摘要算法得到的。
  如果解析包和旧包的SharedUserId不同,会报INSTALL_FAILED_SHARED_USER_INCOMPATIBLE PrepareFailure异常。
  installedUsers变量是旧包的安装用户,uninstalledUsers是旧包的卸载用户。
  如果现在是InstantApp安装,而之前不是,则会报INSTALL_FAILED_SESSION_INVALID PrepareFailure异常。它不允许从完全安装向instant安装降级。
   接着会更新删除信息,它是一个PackageRemovedInfo对象。它包含uid,删除包名、安装者的包名、静态共享库是否、安装用户、还有安装用户的原因、卸载用户的原因。
   再接下来,如果是系统包更新,会根据旧包的状态,来更新浏览标识,这些标识包括SCAN_AS_PRIVILEGED、SCAN_AS_OEM、SCAN_AS_VENDOR、SCAN_AS_PRODUCT、SCAN_AS_ODM、SCAN_AS_SYSTEM_EXT。
  接着会设置res的返回code为PackageManager.INSTALL_SUCCEEDED。
  接着会更新变量targetParseFlags、targetScanFlags的值。
  如果不是系统更新则不进行处理了。

分段五

  分段五:

            } else { // new package installps = null;disabledPs = null;replace = false;existingPackage = null;// Remember this for later, in case we need to rollback this installString pkgName1 = parsedPackage.getPackageName();if (DEBUG_INSTALL) Slog.d(TAG, "installNewPackageLI: " + parsedPackage);// TODO(patb): MOVE TO RECONCILEsynchronized (mLock) {renamedPackage = mSettings.getRenamedPackageLPr(pkgName1);if (renamedPackage != null) {// A package with the same name is already installed, though// it has been renamed to an older name.  The package we// are trying to install should be installed as an update to// the existing one, but that has not been requested, so bail.throw new PrepareFailure(INSTALL_FAILED_ALREADY_EXISTS,"Attempt to re-install " + pkgName1+ " without first uninstalling package running as "+ renamedPackage);}if (mPackages.containsKey(pkgName1)) {// Don't allow installation over an existing package with the same name.throw new PrepareFailure(INSTALL_FAILED_ALREADY_EXISTS,"Attempt to re-install " + pkgName1+ " without first uninstalling.");}}}// we're passing the freezer back to be closed in a later phase of installshouldCloseFreezerBeforeReturn = false;return new PrepareResult(replace, targetScanFlags, targetParseFlags,existingPackage, parsedPackage, replace /* clearCodeCache */, sysPkg,ps, disabledPs);} finally {res.freezer = freezer;if (shouldCloseFreezerBeforeReturn) {freezer.close();}}}

  这个else里面的处理是针对初次安装的应用来说的。
  可见ps、disabledPs、existingPackage都设置为null,因为现在还没有旧包。replace自然为false。
  接着是处理,包改过名字,但是不是按照正常的改包的步骤来做的(配置文件(Manifest文件)中,配置application同级别original-package标签 的属性 "name"的值),这时renamedPackage != null,所以会报异常INSTALL_FAILED_ALREADY_EXISTS异常。如果mPackages.containsKey(pkgName1)为true,也代表不是首次安装,也会报异常。
  shouldCloseFreezerBeforeReturn = false,说明关闭freezer在之后的某个安装阶段执行。 如果 shouldCloseFreezerBeforeReturn 为true,代表需要关闭freezer,直接执行freezer.close()关闭它。
  最后将是否替换包、浏览标识、解析标识、存在的旧解析包、新的安装解析包、清除代码缓存、是否系统包、旧的PackSetting对象,替换的系统PackSetting对象封装到PrepareResult对象中,返回它。

总结

  准备阶段做的事情,咱们在这里做一下总结:
  1、解析安装包,并且这里是以Cluster方式解析的。
  2、如果是更新安装,会检查签名。还会检查安装文件中声明的权限和权限组。
  3、解析包的so包复制到对应目录中,并将使用的ABI和对应的so包路径设置到解析包对象中。
  4、修改解析包的安装路径和安装位置。
  5、如果可能,会将安装文件设置FsVerity
  6、如果更新安装,还会检查签名,这次比上次严格。
  7、如果更新安装,会设置删除包的信息。
  8、最后将信息封装到PrepareResult对象中返回。

相关文章:

Android 安装应用-准备阶段

安装应用的准备阶段是在PackageManagerService类中的preparePackageLI(InstallArgs args, PackageInstalledInfo res)&#xff0c;代码有些长&#xff0c;分段阅读。 分段一 分段一&#xff1a; GuardedBy("mInstallLock")private PrepareResult preparePackageLI(I…...

【JKI SMO】框架讲解(九)

本节内容将演示如何向SMO框架添加启动画面。 1.打开LabVIEW新建一个空白项目&#xff0c;并保存。 2.找到工具&#xff0c;打开SMO Editor。 3.新建一个SMO&#xff0c;选择SMO.UI.Splash。 4. 打开LabVIEW项目&#xff0c;可以看到项目里多了一个SystemSplash类。 打开Process…...

Linux通过Docker安装Microsoft Office+RDP远程控制

之前根据B站教程《在linux上安装微软office》&#xff1a;在linux上安装微软office_哔哩哔哩_bilibili 写过一篇使用KVM虚拟机安装Microsoft OfficeRDP远程控制的文章&#xff0c;根据B站的教程安装后&#xff0c;发现有远程控制延迟的问题&#xff0c;比如拖动Office窗口时会…...

利用Qt实现调用文字大模型的API,文心一言、通义千问、豆包、GPT、Gemini、Claude。

利用Qt实现调用文字大模型的API&#xff0c;文心一言、通义千问、豆包、GPT、Gemini、Claude。 下载地址: AI.xyz 1 Qt实现语言大模型API调用 视频——Qt实现语言大模型API调用 嘿&#xff0c;大家好&#xff01;分享一个最近做的小项目 “AI.xyz” 基于Qt实现调用各家大模型…...

借助医疗保健专用的 LLM提高诊断支持与准确性

概述 最近的研究表明&#xff0c;大规模语言模型在医疗人工智能应用中非常有效。它们在诊断和临床支持系统中的有效性尤为明显&#xff0c;在这些系统中&#xff0c;它们已被证明能为各种医疗询问提供高度准确的答案&#xff08;例如&#xff0c;医生在诊断过程中需要用到语言…...

微前端(qiankun)

微前端 特点&#xff1a;独立开发、独立部署&#xff0c;独立运行&#xff0c;增量升级 解决的问题&#xff1a;日常开发过程中&#xff0c;可能有很多老项目需要迭代&#xff0c;但是可能新的一些可能需要使用的依赖或者新的一些框架&#xff0c;老项目已经不满足&#xff0c;…...

速通c++(周二)

前言 Hello&#xff0c;大家好啊&#xff0c;我是文宇&#xff0c;不是文字&#xff0c;是文宇哦。 今天是速通c第二期。 运算符 c里的运算符种类有很多&#xff0c;因为这个教程是入门教程&#xff0c;所以只介绍其中我们会用到的几种。 算数运算 c中的算数运算有九个&a…...

拓扑未来物联网平台简介

拓扑未来物联网平台是基于Thingsboard二次开发的面向产业互联和智慧生活应用的物联网PaaS平台&#xff0c;支持适配各种网络环境和协议类型&#xff0c;可实现各种传感器和智能硬件的快速接入。有效降低物联网应用开发和部署成本&#xff0c;满足物联网领域设备连接、智能化改造…...

软件测试经理工作日常随记【7】-接口+UI自动化(多端集成测试)

软件测试经理工作日常随记【7】-UI自动化&#xff08;多端集成测试&#xff09; 自动化测试前篇在此 前言 今天开这篇的契机是&#xff0c;最近刚好是运维开发频繁更新证书的&#xff0c;每次更新都在0点&#xff0c;每次一更新都要走一次冒烟流程。为了不让我的美容觉被阉割…...

软考:软件设计师 — 9.数据流图

九. 数据流图 数据流图是下午场考试中第一个题目&#xff0c;分值 15 分。通常会考察实体名、存储名、加工名的补充&#xff0c;以及找到缺失的数据流并改正等。 1. 数据平衡原则 数据流的分析依赖于数据平衡原则。 父图与子图之间的平衡 父图与子图之间平衡是指任何一张 …...

收银系统源码-门店折扣活动应该怎么做

系统概况&#xff1a; 专门为零售行业的连锁店量身打造的收银系统&#xff0c;适用于常规超市、生鲜超市、水果店、便利店、零食专卖店、服装店、母婴用品、农贸市场等类型的门店使用。同时线上线下数据打通&#xff0c;线下收银的数据与小程序私域商城中的数据完全同步&#…...

Python数值计算(12)——线性插值

1. 概述 插值是根据已知的数据序列&#xff08;可以理解为你坐标中一系列离散的点&#xff09;&#xff0c;找到其中的规律&#xff0c;然后根据找到的这个规律&#xff0c;来对其中尚未有数据记录的点进行数值估计的方法。最简单直观的一种插值方式是线性插值&#xff0c;它是…...

TypeScript(switch判断)

1.switch 语法用法 switch是对某个表达式的值做出判断。然后决定程序执行哪一段代码 case语句中指定的每个值必须具有与表达式兼容的类型 语法switch(表达式){ case 值1&#xff1a; ​ 执行语句块1 break; case 值2&#xff1a; ​ 执行语句块3 break; dfault: //如…...

血细胞自动检测与分类系统:深度学习与UI界面的结合

一、项目概述 项目背景 在医学实验室中&#xff0c;血细胞的检测和分类是诊断和研究的重要环节。传统方法依赖于人工显微镜检查&#xff0c;费时且容易出现误差。通过深度学习技术&#xff0c;特别是目标检测模型YOLO&#xff0c;可以实现自动化、快速且准确的血细胞检测和分…...

鸿蒙Flex布局

效果&#xff1a; 代码&#xff1a; 换行代码参数设置&#xff1a; wrap:FlexWrap.Wrap Entry Component struct FlexCase {State message: string Hello World;build() {Flex({direction:FlexDirection.Row,justifyContent:FlexAlign.SpaceAround,alignItems:ItemAlign.Cen…...

开发自己的 Web 框架

开发自己的 Web 框架 开发Web服务器主体程序开发Web框架程序使用模板来展示响应内容开发框架的路由列表功能采用装饰器的方式添加路由电影列表页面的开发案例 接收web服务器的动态资源请求&#xff0c;给web服务器提供处理动态资源请求的服务。根据请求资源路径的后缀名进行判断…...

用于自动驾驶的基于立体视觉的语义 3D 对象和自我运动跟踪

Stereo Vision-based Semantic 3D Object and Ego-motion Tracking for Autonomous Driving 论文 摘要&#xff1a; 我们提出了一种基于立体视觉的方法&#xff0c;用于在动态自动驾驶场景中跟踪相机自我运动和 3D 语义对象。我们建议使用易于标记的 2D 检测和离散视点分类以及…...

Spring@Autowired注解

Autowired顾名思义&#xff0c;就是自动装配&#xff0c;其作用是为了消除代码Java代码里面的getter/setter与bean属性中的property。当然&#xff0c;getter看个人需求&#xff0c;如果私有属性需要对外提供的话&#xff0c;应当予以保留。 因此&#xff0c;引入Autowired注解…...

32.x86游戏实战-使用物品call

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 本次游戏没法给 内容参考于&#xff1a;微尘网络安全 工具下载&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1rEEJnt85npn7N38Ai0_F2Q?pwd6tw3 提…...

Prometheus+Alertmanager+邮件告警

参考node_exporter-CSDN博客&#xff0c;球球不要断更&#xff01;&#xff01;&#xff01;&#xff01; 大致流程 1.部署promethus 可以写一个自定义的 systemd 服务启动文档&#xff0c;详情见自定义的 systemd 服务启动方式-CSDN博客 [rootlocalhost system]# sudo tee /e…...

upload-labs漏洞靶场~文件上传漏洞

寻找测试网站的文件上传的模块&#xff0c;常见&#xff1a;头像上传&#xff0c;修改上传&#xff0c;文件编辑器中文件上传&#xff0c;图片上传、媒体上传等&#xff0c;通过抓包上传恶意的文件进行测试&#xff0c;上传后缀名 asp php aspx 等的动态语言脚本&#xff0c;查…...

PostgreSQL 高阶函数详解:全面深入的功能与实用示例

PostgreSQL 高阶函数详解 PostgreSQL 是一款功能强大的开源关系数据库管理系统&#xff0c;以其丰富的功能和高扩展性著称。在数据处理和分析方面&#xff0c;PostgreSQL 提供了一系列高阶函数&#xff0c;可以极大地简化和优化各种复杂操作。本文将详细介绍 PostgreSQL 的高阶…...

Redis——集合 SET

目录 1. 添加元素 SADD 2. 查看元素 SMEMBERS 3. 判断元素是否存在该集合 SISMEMBER 4. 删除元素 SREM 集合 SET 是一种无序集合&#xff1b;因此其与列表有以下区别&#xff1a; &#xff08;1&#xff09;列表是有序的&#xff0c;集合是无序的&#xff1b; &#xff0…...

openEuler安装docker

1.下载地址 搜索docker 寻找docker-ce 复制地址 2.配置仓库 [rootlocalhost yum.repos.d]# pwd /etc/yum.repos.d [rootlocalhost yum.repos.d]# vim docker-ce.repo [docker-ce] namedocker baseurlhttps://mirrors.aliyun.com/docker-ce/linux/rhel/9/x86_64/stable/ gpgche…...

每天一个数据分析题(四百六十五)- 缺失值

某连续型变量的数据集存在缺失值&#xff0c;可以采用哪种方法处理&#xff1f; A. 插值法填补 B. EM算法填补 C. 随机森林填补 D. 以上均不对 数据分析认证考试介绍&#xff1a;点击进入 题目来源于CDA模拟题库 点击此处获取答案 数据分析专项练习题库 内容涵盖Pytho…...

干货 | 变频器的详细介绍

变频器简述 变频器是电机控制领域中常见的一种设备&#xff0c;也称变频调节器&#xff0c;是一种将固定频率的交流电转换为可调频率的交流电的电力电子设备&#xff0c;用于控制交流电机的转速和输出功率。变频器通过调节输出电源的电压和频率&#xff0c;从而控制电动机的转速…...

Linux线程2

线程相关函数 线程分离--pthread_detach&#xff08;后面会详细讲&#xff09; 函数原型&#xff1a;int pthread_datach(pthread_t thread); 调用该函数之后不需要 pthread_join 子线程会自动回收自己的PCB 杀死&#xff08;取消&#xff09;线程--pthread_cancel 取…...

乱弹篇(40)人类追求长寿

不要认为只有中国的老龄化才严重&#xff0c;实际上全球都面临老龄化&#xff0c;其中日本最为严重。 这是随着人类生活和医学水平的不断提高&#xff0c;寿命才会比过去数十年有了大幅度的提升。据资料显示&#xff0c;目前全球平均预期寿命估计为73岁。与百年之前相比&#…...

技术详解:互联网医院系统源码与医保购药APP的整合开发策略

本篇文章&#xff0c;小编将从系统架构、数据安全、用户体验和技术实现等方面详细探讨互联网医院系统与医保购药APP的整合开发策略。 一、系统架构 1.模块化设计 互联网医院系统与医保购药APP的整合需要采用模块化设计。 2.微服务架构 每个功能模块作为一个独立的微服务&am…...

N4 - Pytorch实现中文文本分类

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 目录 任务描述步骤环境设置数据准备模型设计模型训练模型效果展示 总结与心得体会 任务描述 在上周的任务中&#xff0c;我们使用torchtext下载了托管的英文的…...

做管理培训的网站有什么/免费外网加速器

Android的ImageView无法直接加载Gif图片&#xff0c;如果需要在自己的代码中加载一个gif图片(这很常见&#xff0c;比如下载过程中的loading以示正在下载的转动的圆球)&#xff0c;则无法直接用ImageView。鉴于此&#xff0c;Android社区开发者为解决此问题贡献了很多解决方案&…...

重庆网站建设与网络推广/怎样制作一个网站

Java几种常见的四舍五入的方法 题目要CSS布局HTML小编今天和大家分享编写一个四舍五入的函数,要CSS布局HTML小编今天和大家分享可以保留到小数点后面的任意一位。 java四舍五入的函数:Math.round 语法: Math.round(x); 参数: x 为一数值。 解释: 方法。返回对参数x四舍五入…...

安徽省工程建设信息网网站/好口碑关键词优化

对于图片的格式&#xff0c;我们常用的和熟悉的大部分为jpg格式&#xff0c;但jpg格式的图片有时候很难满足我们的需求&#xff0c;这时&#xff0c;我们需要让图片保存成png的格式来满足我们的需求。.Png格式是图像文件存储格式&#xff0c;在网页设计中已经不是一个陌生的名词…...

4秒网站建设/腾讯企点怎么注册

使用帮助在任何命令模式下&#xff0c;只需输入“?”&#xff0c;即显示该命令模式下所有可用到的命令及其用途。另外&#xff0c;还可以在一个命令和参数后面加“&#xff1f;”&#xff0c;以寻求相关的帮助。例如&#xff0c;我们想看一下在Privileged Exec模式下哪些命令可…...

网站设计 导航条/西安网站seo诊断

Prometheus是继Kubernetes后第2个正式加入CNCF基金会的项目&#xff0c;容器和云原生领域事实的监控标准解决方案。在这次分享将从Prometheus的基础说起&#xff0c;学习和了解Prometheus强大的数据处理能力&#xff0c;了解如何使用Prometheus进行白盒和黑盒监控&#xff0c;以…...

网站左边logo图标怎么做/苏州网站维护

一、jQuery概述 宗旨: Write Less, Do More. 基础知识&#xff1a; 1.符号$代替document.getElementById()函数 2.使用CssXpath来查询页面元素 3.适当的混用jQuery、Dom和JavaScript能够提升程序的执行效率。 如&#xff1a;Offset、App…...