Android 安装应用-浏览阶段
应用安装的浏览阶段主要是由PackageManagerService类中的scanPackageNewLI()实现的,看一下它的代码:
// TODO: scanPackageNewLI() and scanPackageOnly() should be merged. But, first, commiting// the results / removing app data needs to be moved up a level to the callers of this// method. Also, we need to solve the problem of potentially creating a new shared user// setting. That can probably be done later and patch things up after the fact.@GuardedBy({"mInstallLock", "mLock"})private ScanResult scanPackageNewLI(@NonNull ParsedPackage parsedPackage,final @ParseFlags int parseFlags, @ScanFlags int scanFlags, long currentTime,@Nullable UserHandle user, String cpuAbiOverride) throws PackageManagerException {final String renamedPkgName = mSettings.getRenamedPackageLPr(parsedPackage.getRealPackage());final String realPkgName = getRealPackageName(parsedPackage, renamedPkgName);if (realPkgName != null) {ensurePackageRenamed(parsedPackage, renamedPkgName);}final PackageSetting originalPkgSetting = getOriginalPackageLocked(parsedPackage,renamedPkgName);final PackageSetting pkgSetting = mSettings.getPackageLPr(parsedPackage.getPackageName());final PackageSetting disabledPkgSetting =mSettings.getDisabledSystemPkgLPr(parsedPackage.getPackageName());if (mTransferredPackages.contains(parsedPackage.getPackageName())) {Slog.w(TAG, "Package " + parsedPackage.getPackageName()+ " was transferred to another, but its .apk remains");}scanFlags = adjustScanFlags(scanFlags, pkgSetting, disabledPkgSetting, user, parsedPackage);synchronized (mLock) {boolean isUpdatedSystemApp;if (pkgSetting != null) {isUpdatedSystemApp = pkgSetting.getPkgState().isUpdatedSystemApp();} else {isUpdatedSystemApp = disabledPkgSetting != null;}applyPolicy(parsedPackage, parseFlags, scanFlags, mPlatformPackage, isUpdatedSystemApp);assertPackageIsValid(parsedPackage, parseFlags, scanFlags);SharedUserSetting sharedUserSetting = null;if (parsedPackage.getSharedUserId() != null) {// SIDE EFFECTS; may potentially allocate a new shared usersharedUserSetting = mSettings.getSharedUserLPw(parsedPackage.getSharedUserId(),0 /*pkgFlags*/, 0 /*pkgPrivateFlags*/, true /*create*/);if (DEBUG_PACKAGE_SCANNING) {if ((parseFlags & ParsingPackageUtils.PARSE_CHATTY) != 0) {Log.d(TAG, "Shared UserID " + parsedPackage.getSharedUserId()+ " (uid=" + sharedUserSetting.userId + "):"+ " packages=" + sharedUserSetting.packages);}}}String platformPackageName = mPlatformPackage == null? null : mPlatformPackage.getPackageName();final ScanRequest request = new ScanRequest(parsedPackage, sharedUserSetting,pkgSetting == null ? null : pkgSetting.pkg, pkgSetting, disabledPkgSetting,originalPkgSetting, realPkgName, parseFlags, scanFlags,Objects.equals(parsedPackage.getPackageName(), platformPackageName), user,cpuAbiOverride);return scanPackageOnlyLI(request, mInjector, mFactoryTest, currentTime);}}
参数parsedPackage是解析安装文件得到的解析包对象。parsedPackage.getRealPackage()是在解析安装文件时,配置文件存在application同级别original-package标签 的属性 "name"的值,并且它的值和包名不一致时,会设置该值。在这里是找是否存在更改包名的情况,包名的更改关系维护在mSettings中的mRenamedPackages中,所以如果存在包名更改,renamedPkgName就是改名之前的包名。
getRealPackageName(parsedPackage, renamedPkgName)是来判断改名之前的名字,是否存在parsedPackage对象的OriginalPackages中。如果存在,就返回parsedPackage.getRealPackage()。
如果realPkgName != null不为null,说明存在改名字的情况,所以调用ensurePackageRenamed(parsedPackage, renamedPkgName)来将解析包的包名改为之前的包名。
getOriginalPackageLocked(parsedPackage, renamedPkgName)是得到它原来的PackageSetting对象,但是像上面改名之前的包名在parsedPackage对象的OriginalPackages中的,则会返回null。因为它的名字已经是现在解析包的包名了。如果renamedPkgName不在parsedPackage对象的OriginalPackages中,并且parsedPackage对象的OriginalPackages中包名存在PackageSetting对象,但是看它的条件得旧包是系统包,并且在mPackages中不存在对应的值的情况下,才能返回它的PackageSetting对象。所以在这里我们也知道,包名修改是为了系统应用的功能,普通APP没法使用。在这里originalPkgSetting大部分情况下,为null。
pkgSetting则是维护在mSettings中根据包名得到的PackageSetting对象。disabledPkgSetting是禁止的系统包的PackageSetting对象。
mTransferredPackages中保存的是改过应用的包名。
接下来就是调用adjustScanFlags()方法调整浏览标识。
接着通过判断pkgSetting里面的状态和disabledPkgSetting != null来设置变量isUpdatedSystemApp的值。
applyPolicy()方法,是用来根据标识来设置解析包对象的一些状态。
assertPackageIsValid(parsedPackage, parseFlags, scanFlags)是进行一些判断,如果有些状态不符合要求,会报PackageManagerException异常。
sharedUserSetting是解析包中配置的“android:sharedUserId”标签的共享用户的SharedUserSetting对象。
platformPackageName是平台包名,它是由mPlatformPackage得出的值。mPlatformPackage是应用包名为"android"的安装包对象,如果存在,它不为null;如果不存在,它为null。
下面会将上面说的变量封装到ScanRequest对象request中去。最后就是调用scanPackageOnlyLI(request, mInjector, mFactoryTest, currentTime),并将它的结果返回。下面看一下它的代码:
scanPackageOnlyLI(request, mInjector, mFactoryTest, currentTime)的实现
分段一
它的代码也很长,所以分段阅读,第一段:
@GuardedBy("mInstallLock")@VisibleForTesting@NonNullstatic ScanResult scanPackageOnlyLI(@NonNull ScanRequest request,Injector injector,boolean isUnderFactoryTest, long currentTime)throws PackageManagerException {final PackageAbiHelper packageAbiHelper = injector.getAbiHelper();final UserManagerInternal userManager = injector.getUserManagerInternal();ParsedPackage parsedPackage = request.parsedPackage;PackageSetting pkgSetting = request.pkgSetting;final PackageSetting disabledPkgSetting = request.disabledPkgSetting;final PackageSetting originalPkgSetting = request.originalPkgSetting;final @ParseFlags int parseFlags = request.parseFlags;final @ScanFlags int scanFlags = request.scanFlags;final String realPkgName = request.realPkgName;final SharedUserSetting sharedUserSetting = request.sharedUserSetting;final UserHandle user = request.user;final boolean isPlatformPackage = request.isPlatformPackage;List<String> changedAbiCodePath = null;if (DEBUG_PACKAGE_SCANNING) {if ((parseFlags & ParsingPackageUtils.PARSE_CHATTY) != 0) {Log.d(TAG, "Scanning package " + parsedPackage.getPackageName());}}// Initialize package source and resource directoriesfinal File destCodeFile = new File(parsedPackage.getPath());// We keep references to the derived CPU Abis from settings in oder to reuse// them in the case where we're not upgrading or booting for the first time.String primaryCpuAbiFromSettings = null;String secondaryCpuAbiFromSettings = null;boolean needToDeriveAbi = (scanFlags & SCAN_FIRST_BOOT_OR_UPGRADE) != 0;if (!needToDeriveAbi) {if (pkgSetting != null) {// TODO(b/154610922): if it is not first boot or upgrade, we should directly use// API info from existing package setting. However, stub packages currently do not// preserve ABI info, thus the special condition check here. Remove the special// check after we fix the stub generation.if (pkgSetting.pkg != null && pkgSetting.pkg.isStub()) {needToDeriveAbi = true;} else {primaryCpuAbiFromSettings = pkgSetting.primaryCpuAbiString;secondaryCpuAbiFromSettings = pkgSetting.secondaryCpuAbiString;}} else {// Re-scanning a system package after uninstalling updates; need to derive ABIneedToDeriveAbi = true;}}
开始是给局部变量赋值。packageAbiHelper实际是PackageAbiHelperImpl类型。parsedPackage是解析的包实例,pkgSetting是旧包的PackageSetting对象。disabledPkgSetting是禁止的系统包的PackageSetting对象(包名相同)、originalPkgSetting是更改包名之前的原来包名对应的PackageSetting对象、realPkgName是如果存在更改包名现在的包名、user目前用户,像例子中也就是System用户(userId为0)。
parsedPackage.getPath()是解析包的路径。解析Apk文件时,分为解析的是Apk还是目录。如果解析的直接是Apk,则parsedPackage.getPath()为Apk的完整路径名;如果是目录,则parsedPackage.getPath()为Apk文件的目录。
primaryCpuAbiFromSettings是主要的cpu架构的abi,secondaryCpuAbiFromSettings是次要的cpu架构的abi。在升级系统或第一次启动的情况下,abi是需要从解析包里面取的。所以scanFlags 里面有SCAN_FIRST_BOOT_OR_UPGRADE标识时,会将变量needToDeriveAbi 设置为true,代表abi需要从解析包parsedPackage取得。
所以needToDeriveAbi为false情况,如果pkgSetting 不为null,则直接从pkgSetting 取得abi。但是如果包设置pkgSetting对应的解析包pkg.isStub()的情况,那stub包是没有保留ABI信息的,所以也需要从当前解析的包信息里面取得。如果pkgSetting 为null,那也只能从解析包信息里面取得ABI。
分段二
继续看scanPackageOnlyLI()的第二段代码:
if (pkgSetting != null && pkgSetting.sharedUser != sharedUserSetting) {PackageManagerService.reportSettingsProblem(Log.WARN,"Package " + parsedPackage.getPackageName() + " shared user changed from "+ (pkgSetting.sharedUser != null? pkgSetting.sharedUser.name : "<nothing>")+ " to "+ (sharedUserSetting != null ? sharedUserSetting.name : "<nothing>")+ "; replacing with new");pkgSetting = null;}String[] usesStaticLibraries = null;if (!parsedPackage.getUsesStaticLibraries().isEmpty()) {usesStaticLibraries = new String[parsedPackage.getUsesStaticLibraries().size()];parsedPackage.getUsesStaticLibraries().toArray(usesStaticLibraries);}final UUID newDomainSetId = injector.getDomainVerificationManagerInternal().generateNewId();// TODO(b/135203078): Remove appInfoFlag usage in favor of individually assigned booleans// to avoid adding something that's unsupported due to lack of state, since it's called// with null.final boolean createNewPackage = (pkgSetting == null);if (createNewPackage) {final boolean instantApp = (scanFlags & SCAN_AS_INSTANT_APP) != 0;final boolean virtualPreload = (scanFlags & SCAN_AS_VIRTUAL_PRELOAD) != 0;// Flags contain system values stored in the server variant of AndroidPackage,// and so the server-side PackageInfoUtils is still called, even without a// PackageSetting to pass in.int pkgFlags = PackageInfoUtils.appInfoFlags(parsedPackage, null);int pkgPrivateFlags = PackageInfoUtils.appInfoPrivateFlags(parsedPackage, null);// REMOVE SharedUserSetting from method; update in a separate callpkgSetting = Settings.createNewSetting(parsedPackage.getPackageName(),originalPkgSetting, disabledPkgSetting, realPkgName, sharedUserSetting,destCodeFile, parsedPackage.getNativeLibraryRootDir(),AndroidPackageUtils.getRawPrimaryCpuAbi(parsedPackage),AndroidPackageUtils.getRawSecondaryCpuAbi(parsedPackage),parsedPackage.getVersionCode(), pkgFlags, pkgPrivateFlags, user,true /*allowInstall*/, instantApp, virtualPreload,UserManagerService.getInstance(), usesStaticLibraries,parsedPackage.getUsesStaticLibrariesVersions(), parsedPackage.getMimeGroups(),newDomainSetId);} else {// make a deep copy to avoid modifying any existing system state.pkgSetting = new PackageSetting(pkgSetting);pkgSetting.pkg = parsedPackage;// REMOVE SharedUserSetting from method; update in a separate call.//// TODO(narayan): This update is bogus. nativeLibraryDir & primaryCpuAbi,// secondaryCpuAbi are not known at this point so we always update them// to null here, only to reset them at a later point.Settings.updatePackageSetting(pkgSetting, disabledPkgSetting, sharedUserSetting,destCodeFile, parsedPackage.getNativeLibraryDir(),AndroidPackageUtils.getPrimaryCpuAbi(parsedPackage, pkgSetting),AndroidPackageUtils.getSecondaryCpuAbi(parsedPackage, pkgSetting),PackageInfoUtils.appInfoFlags(parsedPackage, pkgSetting),PackageInfoUtils.appInfoPrivateFlags(parsedPackage, pkgSetting),UserManagerService.getInstance(),usesStaticLibraries, parsedPackage.getUsesStaticLibrariesVersions(),parsedPackage.getMimeGroups(), newDomainSetId);}
如果pkgSetting存在共享用户,但是和sharedUserSetting不同,则会将pkgSetting = null。sharedUserSetting是来自解析包,如果不同,认为共享用户发生了变化,这个时候,将pkgSetting置为null,后面将建一个新的PackageSetting对象。
parsedPackage.getUsesStaticLibraries()来自解析Apk文件AndroidManifest.xml时,"application"标签包裹内"uses-static-library"标签的属性值。这里将它们取出来放到usesStaticLibraries变量内。
injector.getDomainVerificationManagerInternal()得到的是DomainVerificationService对象,调用它的generateNewId()生成一个UUID值newDomainSetId。
如果pkgSetting == null,将createNewPackage置为true,代表需要新建一个PackageSetting对象。scanFlags如果存在SCAN_AS_INSTANT_APP或SCAN_AS_VIRTUAL_PRELOAD标识,会将变量instantApp和virtualPreload设置为true。这俩属性在创建新PackageSetting对象时,使用。然后从解析包对象parsedPackage中得到标识pkgFlags、私有标识pkgPrivateFlags。再接着就是调用Settings.createNewSetting()创建一个新的PackageSetting对象。
如果它不为null,则会通过深copy新建一个PackageSetting对象。并且将解析包指定给它。接着又调用Settings.updatePackageSetting()来更新pkgSetting的对应值。
分段三
继续看scanPackageOnlyLI()的第三段代码:
if (createNewPackage && originalPkgSetting != null) {// This is the initial transition from the original package, so,// fix up the new package's name now. We must do this after looking// up the package under its new name, so getPackageLP takes care of// fiddling things correctly.parsedPackage.setPackageName(originalPkgSetting.name);// File a report about this.String msg = "New package " + pkgSetting.realName+ " renamed to replace old package " + pkgSetting.name;reportSettingsProblem(Log.WARN, msg);}final int userId = (user == null ? UserHandle.USER_SYSTEM : user.getIdentifier());// for existing packages, change the install state; but, only if it's explicitly specifiedif (!createNewPackage) {final boolean instantApp = (scanFlags & SCAN_AS_INSTANT_APP) != 0;final boolean fullApp = (scanFlags & SCAN_AS_FULL_APP) != 0;setInstantAppForUser(injector, pkgSetting, userId, instantApp, fullApp);}// TODO(patb): see if we can do away with disabled check here.if (disabledPkgSetting != null|| (0 != (scanFlags & SCAN_NEW_INSTALL)&& pkgSetting != null && pkgSetting.isSystem())) {pkgSetting.getPkgState().setUpdatedSystemApp(true);}parsedPackage.setSeInfo(SELinuxMMAC.getSeInfo(parsedPackage, sharedUserSetting,injector.getCompatibility())).setSeInfoUser(SELinuxUtil.assignSeinfoUser(pkgSetting.readUserState(userId == UserHandle.USER_ALL ? UserHandle.USER_SYSTEM : userId)));if (parsedPackage.isSystem()) {configurePackageComponents(parsedPackage);}
如果是新创建包,并且originalPkgSetting不为null,则将解析包的包名改为originalPkgSetting的包名。这可能发生在改包名的首次安装时。
得到userId,默认为UserHandle.USER_SYSTEM。
如果不是新创建包,会根据SCAN_AS_INSTANT_APP和SCAN_AS_FULL_APP标识,设置pkgSetting里面对应用户的状态。
如果参数disabledPkgSetting不为null,或者scanFlags 存在SCAN_NEW_INSTALL标识,并且pkgSetting是系统包的情况下,会将pkgSetting的成员变量PackageStateUnserialized对象 pkgState的updatedSystemApp设置为true。什么时候会设置SCAN_NEW_INSTALL标识呢?它是在用户手动安装一个Apk的时候,会设置该标识。
如果解析包parsedPackage是系统的,调用configurePackageComponents()来设置解析包里组件的enabled属性。
分段四
继续看scanPackageOnlyLI()的第四段代码:
final String cpuAbiOverride = deriveAbiOverride(request.cpuAbiOverride);final boolean isUpdatedSystemApp = pkgSetting.getPkgState().isUpdatedSystemApp();final File appLib32InstallDir = getAppLib32InstallDir();if ((scanFlags & SCAN_NEW_INSTALL) == 0) {if (needToDeriveAbi) {Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "derivePackageAbi");final Pair<PackageAbiHelper.Abis, PackageAbiHelper.NativeLibraryPaths> derivedAbi =packageAbiHelper.derivePackageAbi(parsedPackage, isUpdatedSystemApp,cpuAbiOverride, appLib32InstallDir);derivedAbi.first.applyTo(parsedPackage);derivedAbi.second.applyTo(parsedPackage);Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);// Some system apps still use directory structure for native libraries// in which case we might end up not detecting abi solely based on apk// structure. Try to detect abi based on directory structure.String pkgRawPrimaryCpuAbi = AndroidPackageUtils.getRawPrimaryCpuAbi(parsedPackage);if (parsedPackage.isSystem() && !isUpdatedSystemApp&& pkgRawPrimaryCpuAbi == null) {final PackageAbiHelper.Abis abis = packageAbiHelper.getBundledAppAbis(parsedPackage);abis.applyTo(parsedPackage);abis.applyTo(pkgSetting);final PackageAbiHelper.NativeLibraryPaths nativeLibraryPaths =packageAbiHelper.deriveNativeLibraryPaths(parsedPackage,isUpdatedSystemApp, appLib32InstallDir);nativeLibraryPaths.applyTo(parsedPackage);}} else {// This is not a first boot or an upgrade, don't bother deriving the// ABI during the scan. Instead, trust the value that was stored in the// package setting.parsedPackage.setPrimaryCpuAbi(primaryCpuAbiFromSettings).setSecondaryCpuAbi(secondaryCpuAbiFromSettings);final PackageAbiHelper.NativeLibraryPaths nativeLibraryPaths =packageAbiHelper.deriveNativeLibraryPaths(parsedPackage,isUpdatedSystemApp, appLib32InstallDir);nativeLibraryPaths.applyTo(parsedPackage);if (DEBUG_ABI_SELECTION) {Slog.i(TAG, "Using ABIS and native lib paths from settings : " +parsedPackage.getPackageName() + " " +AndroidPackageUtils.getRawPrimaryCpuAbi(parsedPackage)+ ", "+ AndroidPackageUtils.getRawSecondaryCpuAbi(parsedPackage));}}} else {if ((scanFlags & SCAN_MOVE) != 0) {// We haven't run dex-opt for this move (since we've moved the compiled output too)// but we already have this packages package info in the PackageSetting. We just// use that and derive the native library path based on the new codepath.parsedPackage.setPrimaryCpuAbi(pkgSetting.primaryCpuAbiString).setSecondaryCpuAbi(pkgSetting.secondaryCpuAbiString);}// Set native library paths again. For moves, the path will be updated based on the// ABIs we've determined above. For non-moves, the path will be updated based on the// ABIs we determined during compilation, but the path will depend on the final// package path (after the rename away from the stage path).final PackageAbiHelper.NativeLibraryPaths nativeLibraryPaths =packageAbiHelper.deriveNativeLibraryPaths(parsedPackage, isUpdatedSystemApp,appLib32InstallDir);nativeLibraryPaths.applyTo(parsedPackage);}
cpuAbiOverride是从request.cpuAbiOverride里面得到的Abi,isUpdatedSystemApp则是代表是系统升级App。
文件appLib32InstallDir的路径为"/data/app-lib"。
下面要从scanFlags存不存在SCAN_NEW_INSTALL标识两种情况去讨论,SCAN_NEW_INSTALL是在用户手动安装APP的时候,会设置。
1、scanFlags不存在SCAN_NEW_INSTALL标识,并且需要从解析包里得到Abi值
会调用packageAbiHelper.derivePackageAbi()函数得到Pair对象值。packageAbiHelper.derivePackageAbi()其实主要是将本地库文件提取出来,并且确定解析包的主要、次要CPU ABI。可以进入这篇文件里面了解一下,Android 提取出Apk的本地库。
然后将Pair对象值应用到解析包。Pair的first是Abis对象,Pair的second是PackageAbiHelper.NativeLibraryPaths。看一下它俩的applyTo()方法:
类Abis
public void applyTo(ParsedPackage pkg) {pkg.setPrimaryCpuAbi(primary).setSecondaryCpuAbi(secondary);}
类NativeLibraryPathspublic void applyTo(ParsedPackage pkg) {pkg.setNativeLibraryRootDir(nativeLibraryRootDir).setNativeLibraryRootRequiresIsa(nativeLibraryRootRequiresIsa).setNativeLibraryDir(nativeLibraryDir).setSecondaryNativeLibraryDir(secondaryNativeLibraryDir);}
可以看到,解析包对象主要设置了它的primaryCpuAbi、secondaryCpuAbi、nativeLibraryRootDir、nativeLibraryRootRequiresIsa、nativeLibraryDir、secondaryNativeLibraryDir。
如果设置完之后,parsedPackage的主CpuAbi依然为null,这个时候,会调用packageAbiHelper.getBundledAppAbis()通过Apk文件所在的目录结构结合系统支持的ABI来得到Abi。得到结果之后,应用到解析包和包设置对象中。最后packageAbiHelper.deriveNativeLibraryPaths()(该方法也在这篇文章有介绍)得到本地库的路径然后设置到解析包中。
2、scanFlags不存在SCAN_NEW_INSTALL标识,并且不需要从解析包里得到Abi值。
在这种情况下,解析包直接取primaryCpuAbiFromSettings、secondaryCpuAbiFromSettings的值,从前面我们知道,它俩的值是从包设置对象里面取得的。接着调用packageAbiHelper.deriveNativeLibraryPaths()得到本地库的路径然后设置到解析包中。
3、scanFlags存在SCAN_NEW_INSTALL标识
代表着有可能用户手动安装APP的。SCAN_MOVE代表是要移动一个安装包,这个时候,将parsedPackage的Abi设置成包设置里的。接着也是调用packageAbiHelper.deriveNativeLibraryPaths()得到本地库的路径然后设置到解析包中。 Android 安装应用-准备阶段 里面已经执行过这些步骤了,为什么在这里又执行了一遍呢?Android 安装应用-准备阶段 里面执行过之后,紧接着就进行了更改文件目录名的操作,所以这些个路径其实已经发生改变了,在这里又执行了一遍,就将路径名改对了。
分段五
继续看scanPackageOnlyLI()的第五段代码:
// This is a special case for the "system" package, where the ABI is// dictated by the zygote configuration (and init.rc). We should keep track// of this ABI so that we can deal with "normal" applications that run under// the same UID correctly.if (isPlatformPackage) {parsedPackage.setPrimaryCpuAbi(VMRuntime.getRuntime().is64Bit() ?Build.SUPPORTED_64_BIT_ABIS[0] : Build.SUPPORTED_32_BIT_ABIS[0]);}// If there's a mismatch between the abi-override in the package setting// and the abiOverride specified for the install. Warn about this because we// would've already compiled the app without taking the package setting into// account.if ((scanFlags & SCAN_NO_DEX) == 0 && (scanFlags & SCAN_NEW_INSTALL) != 0) {if (cpuAbiOverride == null) {Slog.w(TAG, "Ignoring persisted ABI override " + cpuAbiOverride +" for package " + parsedPackage.getPackageName());}}pkgSetting.primaryCpuAbiString = AndroidPackageUtils.getRawPrimaryCpuAbi(parsedPackage);pkgSetting.secondaryCpuAbiString = AndroidPackageUtils.getRawSecondaryCpuAbi(parsedPackage);pkgSetting.cpuAbiOverrideString = cpuAbiOverride;if (DEBUG_ABI_SELECTION) {Slog.d(TAG, "Resolved nativeLibraryRoot for " + parsedPackage.getPackageName()+ " to root=" + parsedPackage.getNativeLibraryRootDir()+ ", to dir=" + parsedPackage.getNativeLibraryDir()+ ", isa=" + parsedPackage.isNativeLibraryRootRequiresIsa());}// Push the derived path down into PackageSettings so we know what to// clean up at uninstall time.pkgSetting.legacyNativeLibraryPathString = parsedPackage.getNativeLibraryRootDir();if (DEBUG_ABI_SELECTION) {Log.d(TAG, "Abis for package[" + parsedPackage.getPackageName() + "] are"+ " primary=" + pkgSetting.primaryCpuAbiString+ " secondary=" + pkgSetting.primaryCpuAbiString+ " abiOverride=" + pkgSetting.cpuAbiOverrideString);}if ((scanFlags & SCAN_BOOTING) == 0 && pkgSetting.sharedUser != null) {// We don't do this here during boot because we can do it all// at once after scanning all existing packages.//// We also do this *before* we perform dexopt on this package, so that// we can avoid redundant dexopts, and also to make sure we've got the// code and package path correct.changedAbiCodePath = applyAdjustedAbiToSharedUser(pkgSetting.sharedUser, parsedPackage,packageAbiHelper.getAdjustedAbiForSharedUser(pkgSetting.sharedUser.packages, parsedPackage));}parsedPackage.setFactoryTest(isUnderFactoryTest && parsedPackage.getRequestedPermissions().contains(android.Manifest.permission.FACTORY_TEST));if (parsedPackage.isSystem()) {pkgSetting.setIsOrphaned(true);}
如果是平台包,根据虚拟机是不是64位,将平台包的主Cpu ABI设置为系统支持的64位或32位的数组的第一序列的值。
设置SCAN_NO_DEX是为了跳过dexopt。
接着设置了包设置对象pkgSetting的主要、次要ABI为解析包里的主要次要ABI。并将pkgSetting.cpuAbiOverrideString = cpuAbiOverride。
接着会将解析包的本地库文件的根目录,设置到包设置对象的legacyNativeLibraryPathString属性中。
在不是启动的过程(SCAN_BOOTING代表系统启动)中,并且包设置对象pkgSetting的共享用户不为null的情况下,先调用packageAbiHelper.getAdjustedAbiForSharedUser(pkgSetting.sharedUser.packages, parsedPackage) 得到共享用户下包需要调整的ABI,接着调用applyAdjustedAbiToSharedUser()会找到共享用户下的包们主要CPU ABI为null的包,将包的getPath()放入changedAbiCodePath中。
接着设置解析包的FACTORY_TEST标识。它是由系统处于的工程测试模式打开 和解析包中权限包含android.Manifest.permission.FACTORY_TEST共同确定的。
如果解析包是系统的,会设置包设置对象setIsOrphaned(true)。
得到共享用户下包需要调整的ABI
packageAbiHelper是PackageAbiHelperImpl实例,先看一下PackageAbiHelperImpl的getAdjustedAbiForSharedUser():
/*** Adjusts ABIs for a set of packages belonging to a shared user so that they all match.* i.e, so that all packages can be run inside a single process if required.** Optionally, callers can pass in a parsed package via {@code newPackage} in which case* this function will either try and make the ABI for all packages in* {@code packagesForUser} match {@code scannedPackage} or will update the ABI of* {@code scannedPackage} to match the ABI selected for {@code packagesForUser}. This* variant is used when installing or updating a package that belongs to a shared user.** NOTE: We currently only match for the primary CPU abi string. Matching the secondary* adds unnecessary complexity.*/@Override@Nullablepublic String getAdjustedAbiForSharedUser(Set<PackageSetting> packagesForUser, AndroidPackage scannedPackage) {String requiredInstructionSet = null;if (scannedPackage != null) {String pkgRawPrimaryCpuAbi = AndroidPackageUtils.getRawPrimaryCpuAbi(scannedPackage);if (pkgRawPrimaryCpuAbi != null) {requiredInstructionSet = VMRuntime.getInstructionSet(pkgRawPrimaryCpuAbi);}}PackageSetting requirer = null;for (PackageSetting ps : packagesForUser) {// If packagesForUser contains scannedPackage, we skip it. This will happen// when scannedPackage is an update of an existing package. Without this check,// we will never be able to change the ABI of any package belonging to a shared// user, even if it's compatible with other packages.if (scannedPackage != null && scannedPackage.getPackageName().equals(ps.name)) {continue;}if (ps.primaryCpuAbiString == null) {continue;}final String instructionSet =VMRuntime.getInstructionSet(ps.primaryCpuAbiString);if (requiredInstructionSet != null && !requiredInstructionSet.equals(instructionSet)) {// We have a mismatch between instruction sets (say arm vs arm64) warn about// this but there's not much we can do.String errorMessage = "Instruction set mismatch, "+ ((requirer == null) ? "[caller]" : requirer)+ " requires " + requiredInstructionSet + " whereas " + ps+ " requires " + instructionSet;Slog.w(PackageManagerService.TAG, errorMessage);}if (requiredInstructionSet == null) {requiredInstructionSet = instructionSet;requirer = ps;}}if (requiredInstructionSet == null) {return null;}final String adjustedAbi;if (requirer != null) {// requirer != null implies that either scannedPackage was null or that// scannedPackage did not require an ABI, in which case we have to adjust// scannedPackage to match the ABI of the set (which is the same as// requirer's ABI)adjustedAbi = requirer.primaryCpuAbiString;} else {// requirer == null implies that we're updating all ABIs in the set to// match scannedPackage.adjustedAbi = AndroidPackageUtils.getRawPrimaryCpuAbi(scannedPackage);}return adjustedAbi;}
先取到解析包的主要ABI对应的指令集requiredInstructionSet。
接着循环包设置对象集合packagesForUser,如果解析包不为null,并且包名和当前循环的包名相等,就直接跳出,进行下次循环。
如果循环的包设置的主要ABI为null,就进行下次循环。
接下来,如果解析包的指令集为null,找到第一个主要ABI不为null的包设置对象,在解析包的主要指令为null的情况下,将解析包的指令设置给requiredInstructionSet变量,将包对象设置为requirer。
接着判断requirer不为null,其实就是取值为循环包设置集合里面第一个ABI不为null的包设置对象的ABI。
如果requirer为null,取值就是解析包的主要ABI。
其实它这个方法,主要就是解析包的主要ABI存在,就取解析包的主要ABI;如果不存在,就去包设置集合里面寻找第一个主要ABI不为null的包设置的主要ABI。
得到共享用户中包需要修改ABI的包的路径
/*** Applies the adjusted ABI calculated by* {@link PackageAbiHelper#getAdjustedAbiForSharedUser(Set, AndroidPackage)} to all* relevant packages and settings.* @param sharedUserSetting The {@code SharedUserSetting} to adjust* @param scannedPackage the package being scanned or null* @param adjustedAbi the adjusted ABI calculated by {@link PackageAbiHelper}* @return the list of code paths that belong to packages that had their ABIs adjusted.*/private static List<String> applyAdjustedAbiToSharedUser(SharedUserSetting sharedUserSetting,ParsedPackage scannedPackage, String adjustedAbi) {if (scannedPackage != null) {scannedPackage.setPrimaryCpuAbi(adjustedAbi);}List<String> changedAbiCodePath = null;for (PackageSetting ps : sharedUserSetting.packages) {if (scannedPackage == null || !scannedPackage.getPackageName().equals(ps.name)) {if (ps.primaryCpuAbiString != null) {continue;}ps.primaryCpuAbiString = adjustedAbi;if (ps.pkg != null) {if (!TextUtils.equals(adjustedAbi,AndroidPackageUtils.getRawPrimaryCpuAbi(ps.pkg))) {if (DEBUG_ABI_SELECTION) {Slog.i(TAG,"Adjusting ABI for " + ps.name + " to " + adjustedAbi+ " (scannedPackage="+ (scannedPackage != null ? scannedPackage : "null")+ ")");}if (changedAbiCodePath == null) {changedAbiCodePath = new ArrayList<>();}changedAbiCodePath.add(ps.getPathString());}}}}return changedAbiCodePath;}
解析包不为null,直接设置解析包的主要ABI。
接着就是在循环中寻找,包名和解析包的包名不同的情况下,包设置的主要ABI为null的包设置对象,并且会将包设置对象的主要ABI设置为调整的ABI adjustedAbi。然后检查包设置对象的解析包对象的ABI和调整的ABI不同,就会将包设置对象的getPathString()添加到结果集合中。
分段六
继续看scanPackageOnlyLI()的最后一段代码:
// Take care of first install / last update times.final long scanFileTime = getLastModifiedTime(parsedPackage);if (currentTime != 0) {if (pkgSetting.firstInstallTime == 0) {pkgSetting.firstInstallTime = pkgSetting.lastUpdateTime = currentTime;} else if ((scanFlags & SCAN_UPDATE_TIME) != 0) {pkgSetting.lastUpdateTime = currentTime;}} else if (pkgSetting.firstInstallTime == 0) {// We need *something*. Take time time stamp of the file.pkgSetting.firstInstallTime = pkgSetting.lastUpdateTime = scanFileTime;} else if ((parseFlags & ParsingPackageUtils.PARSE_IS_SYSTEM_DIR) != 0) {if (scanFileTime != pkgSetting.timeStamp) {// A package on the system image has changed; consider this// to be an update.pkgSetting.lastUpdateTime = scanFileTime;}}pkgSetting.setTimeStamp(scanFileTime);// TODO(b/135203078): Remove, move to constructorpkgSetting.pkg = parsedPackage;pkgSetting.pkgFlags = PackageInfoUtils.appInfoFlags(parsedPackage, pkgSetting);pkgSetting.pkgPrivateFlags =PackageInfoUtils.appInfoPrivateFlags(parsedPackage, pkgSetting);if (parsedPackage.getLongVersionCode() != pkgSetting.versionCode) {pkgSetting.versionCode = parsedPackage.getLongVersionCode();}// Update volume if neededfinal String volumeUuid = parsedPackage.getVolumeUuid();if (!Objects.equals(volumeUuid, pkgSetting.volumeUuid)) {Slog.i(PackageManagerService.TAG,"Update" + (pkgSetting.isSystem() ? " system" : "")+ " package " + parsedPackage.getPackageName()+ " volume from " + pkgSetting.volumeUuid+ " to " + volumeUuid);pkgSetting.volumeUuid = volumeUuid;}SharedLibraryInfo staticSharedLibraryInfo = null;if (!TextUtils.isEmpty(parsedPackage.getStaticSharedLibName())) {staticSharedLibraryInfo =AndroidPackageUtils.createSharedLibraryForStatic(parsedPackage);}List<SharedLibraryInfo> dynamicSharedLibraryInfos = null;if (!ArrayUtils.isEmpty(parsedPackage.getLibraryNames())) {dynamicSharedLibraryInfos = new ArrayList<>(parsedPackage.getLibraryNames().size());for (String name : parsedPackage.getLibraryNames()) {dynamicSharedLibraryInfos.add(AndroidPackageUtils.createSharedLibraryForDynamic(parsedPackage, name));}}return new ScanResult(request, true, pkgSetting, changedAbiCodePath,!createNewPackage /* existingSettingCopied */, staticSharedLibraryInfo,dynamicSharedLibraryInfos);}
处理包设置对象的firstInstallTime、lastUpdateTime。
这里注意这一点,在这里将包设置对象的pkg设置为解析包对象parsedPackage。
将包设置对象和解析包对象的Flags合并赋值给包设置对象的pkgFlags。
将解析包对象的PrivateFlags合并赋值给包设置对象的pkgPrivateFlags。
如果解析包对象的版本和包设置对象的版本不同,将包设置对象的版本改为解析包对象的版本。
如果解析包对象的版本和包设置对象的volumeUuid不同,将pkgSetting.volumeUuid = volumeUuid。
如果解析包对象里面如果配置了静态库,则生成静态库对象staticSharedLibraryInfo。
如果解析包对象里面如果配置了动态库,则生成动态库对象添加到dynamicSharedLibraryInfos。
最后将相关信息封装到ScanResult对象返回。封装的内容包括生成的ScanRequest对象,成功结果true、新生成的PackageSetting对象pkgSetting、共享用户中其他修改了ABI的文件路径、是否是深拷贝存在的PackageSetting对象、静态分享库信息、动态分享库信息。
总结
根据标识调整浏览标识,设置解析包的状态,也会检查一些状态。
将相关内容封装到ScanRequest对象中,包括解析包对象、共享用户对象、旧安装包对象、现在使用PackageSetting对象、禁止的系统PackageSetting对象、旧PackageSetting对象(改包名之前)、真包名、解析标识、浏览标识、是否是系统平台包、用户、CPU ABI参数。
创建了一个新的PackageSetting对象(可能是直接new,也可能深copy现在使用的PackageSetting对象)。
如果是系统第一次启动或者系统更新之后第一次启动时,需要去安装包里得到主要CPU ABI和次要CPU ABI,本地库解析的路径,提取本地库文件(能否提取需要判断)。如果不是,它的主要CPU ABI和次要CPU ABI取值是在生成PackageSetting对象时定的,但它会重新确定本地库解析的路径。
最后将生成的ScanRequest对象,成功结果true、新生成的PackageSetting对象pkgSetting、共享用户中其他修改了ABI的文件路径、是否是深拷贝存在的PackageSetting对象、静态分享库信息、动态分享库信息,封装成ScanResult对象返回。
相关文章:
Android 安装应用-浏览阶段
应用安装的浏览阶段主要是由PackageManagerService类中的scanPackageNewLI()实现的,看一下它的代码: // TODO: scanPackageNewLI() and scanPackageOnly() should be merged. But, first, commiting// the results / removing app data needs to be move…...
JavaEE 初阶(10)——多线程8之“单例模式”
目录 一. 设计模式 二. 单例模式 2.1 饿汉模式 2.2 懒汉模式 a. 加锁synchronized b. 双重if判定 c. volatile关键字(双重检查锁定) 一. 设计模式 设计模式是在软件工程中解决常见问题的经典解决方案。针对一些特定场景给出的一些比较好的解决…...
Javascript常见设计模式
JS设计模式学习【待吸收】-CSDN博客 JavaScript 中的设计模式是用来解决常见问题的最佳实践方案。这些模式有助于创建可重用、易于理解和维护的代码。下面列出了一些常见的 JavaScript 设计模式及其代码示例。 1. 单例模式(Singleton) 单例模式确保一…...
JavaFX布局-SplitPane
JavaFX布局-SplitPane 常用属性orientationpaddingdividerPositionsdisable 实现方式Java实现fxml实现 一个拆分至少两个区域的容器支持水平、垂直布局可以拖动区域的大小初始化大小通过比例设置[0,1] 常用属性 orientation 排列方式,Orientation.VERTICAL、Orien…...
2.MySQL库的操作
创建数据库 创建数据库的代码: CREATE DATABASE [IF NOT EXISTS] db_name [create_specification [,create_specification] ...];create_specification:[DEFAULT] CHARACTER SET charset_name[DEFAULT] COLLATE collation_name 说明: 大写的表示关键…...
如何学习计算机
不要只盯着计算机语言学习,你现在已经学习了C语言和Java,暑假又规划学习Python,最后你掌握的就是计算机语言包而已。 2. 建议你找一门想要深挖的语言,沿着这个方向继续往后学习知识就行。计算机语言是学不完的,而未来就…...
Spring MVC 快速入门指南及实战演示
1、SpringMVC简介 1.1 背景 Servlet属于web层开发技术,技术特点: 1. 每个请求都需要创建一个Servlet进行处理 2. 创建Servlet存在重复操作 3. 代码灵活性低,开发效率低 是否有技术方案可以解决以上问题? 1.2 SpringMVC概述 Sp…...
在线测评系统(未完结)
文章目录 注意!!!1、多模块开发(后端)(1).Maven依赖(2)swagger配置 2、判题机开发(1)docker 前言:大二刚开始接手了本学院的oj,并管理了一段时间,后来老师给我…...
Python 爬虫项目实战(一):破解网易云 VIP 免费下载付费歌曲
前言 网络爬虫(Web Crawler),也称为网页蜘蛛(Web Spider)或网页机器人(Web Bot),是一种按照既定规则自动浏览网络并提取信息的程序。爬虫的主要用途包括数据采集、网络索引、内容抓…...
PTA 6-7 统计某类完全平方数
6-7 统计某类完全平方数(20分) 本题要求实现一个函数,判断任一给定整数N是否满足条件:它是完全平方数,又至少有两位数字相同,如144、676等。 函数接口定义: int IsTheNumber ( const int N );…...
PyFilesystem2 - Python 操作文件系统
文章目录 一、关于 PyFilesystem2二、安装三、快速使用四、指南为什么要使用 PyFilesystem ?打开文件系统树打印关闭目录信息子目录处理文件遍历 WalkingGlobbing移动和复制 五、概念路径系统路径沙盒错误 六、资源信息信息对象命名空间基本命名空间细节命名空间访问…...
Bug小记:关于servlet后端渲染界面时出现的问题小记1P
问题1: 问题描述: int delete(Integer Sno);后端在该方法调用时传入参数 req.getParameter("Sno")报错参数应该为Integer类型问题分析:后端通过请求获取到的前端数据都是字符串类型,需要手动转换参数类型 解决方法&a…...
智慧水务项目(二)django(drf)+angular 18 创建通用model,并对orm常用字段进行说明
一、说明 上一篇文章建立一个最简单的项目,现在我们建立一个公共模型,抽取公共字段,以便于后续模块继承,过程之中会对orm常用字段进行说明,用到的介绍一下 二、创建一个db.py 目录如下图 1、代码 from importlib im…...
<数据集>人员摔倒识别数据集<目标检测>
数据集格式:VOCYOLO格式 图片数量:8605张 标注数量(xml文件个数):8605 标注数量(txt文件个数):8605 标注类别数:1 标注类别名称:[fall] 序号类别名称图片数框数1fall860512275 使用标注工具…...
npm install 报错 ‘proxy‘ config is set properly. See: ‘npm help config‘
解决 参考链接:npm install 报错 ‘proxy‘ config is set properly. See: ‘npm help config‘-阿里云开发者社区 (aliyun.com)...
爬虫问题---ChromeDriver的安装和使用
一、安装 1.查看chrome的版本 在浏览器里面输入 chrome://version/ 回车查看浏览器版本 Chrome的版本要和ChromeDriver的版本对应,否则会出现版本问题。 2.ChromeDriver的版本选择 114之前的版本:https://chromedriver.storage.googleapis.com/index.ht…...
Spring的配置类分为Full和Lite两种模式
Spring的配置类分为Full和Lite两种模式 首先查看 Configuration 注解的源码, 如下所示: Target({ElementType.TYPE}) Retention(RetentionPolicy.RUNTIME) Documented Component public interface Configuration {AliasFor(annotation Component.class)String value() defau…...
探索Perl的代码生成艺术:利用编译器后端释放潜能
探索Perl的代码生成艺术:利用编译器后端释放潜能 Perl,作为一种解释型语言,通常不通过编译器后端直接生成机器代码。然而,通过一些高级技术,Perl 程序员可以利用编译器后端来生成代码,从而提高性能或实现特…...
21 B端产品经理之技术常识(1)
产品经理需要掌握一些基本的技术知识。 了解公司前端与后端 前端 前端开发:创建WEB页面或APP等前端界面呈现给用户的过程,即前端负责用户界面交互。 前端技能: HTML:一种标记语言,能够实现Web页面并在浏览器中显示。…...
数据结构基础详解(C语言):单链表_定义_初始化_插入_删除_查找_建立操作_纯c语言代码注释讲解
单链表理论知识详解 文章目录 单链表理论知识详解1.单链表的定义2.单链表的初始化3.单链表的插入和删除3.1 单链表的插入3.1.1 按位序插入3.1.2 在指定结点的前后插入一.后插操作二.前插操作 4.单链表的删除4.1 按位序删除4.2 指定结点的删除 5.单链表的查找5.1 按位序查找5.2 …...
【智能时代的创新工具】LangChain快速入门指南:轻松掌握语言模型的集成与运用
一、LangChain:连接语言模型与现实世界的桥梁 1.1 LangChain的定义与重要性 LangChain是一个开源的Python库,它旨在为开发人员提供一种简便的方式来集成和运用语言模型。它不仅仅是一个简单的API调用工具,而是一个具有丰富功能的框架&#x…...
文献阅读:细胞分辨率全脑图谱的交互式框架
文献介绍 文献题目: An interactive framework for whole-brain maps at cellular resolution 研究团队: Daniel Frth(瑞典卡罗林斯卡学院)、Konstantinos Meletis(瑞典卡罗林斯卡学院) 发表时间ÿ…...
YAML基础语言深度解析
引言 YAML(YAML Aint Markup Language,即YAML不是一种标记语言)是一种直观、易于阅读的数据序列化格式,常用于配置文件、数据交换和程序间的通信。其设计目标是易于人类阅读和编写,同时也便于机器解析和生成。在本文中…...
xcode使用
1. 界面 1.1. Build Settings,Build Phases和Build Rules三个设置项 Build Settings(编译设置): 每个选项由标题(Title)和定义(Definition)组成。这里主要定义了Xcode在编译项目时的一些具体配置 Build Phases(编译资源):用于指定编译过程中项目所链接的原文件,依赖对象,库…...
OV2640引脚的定义(OV2640 FPC模组规格书(接口线序))
OV2640是一款由Omni Vision公司生产的1/4寸CMOS UXGA(1632x1222)图像传感器。这款传感器以其小巧的体积、低工作电压和强大的功能而著称,它集成了单片UXGA摄像头和影像处理器,能够通过SCCB总线控制输出各种分辨率的8/10位影像数据…...
CTFSHOW 萌新 web10 解题思路和方法(passthru执行命令)
点击题目链接,分析页面代码。发现代码中过滤了system、exec 函数,这意味着我们不能通过system(cmd命令)、exec(cmd命令)的方式运行命令。 在命令执行中,常用的命令执行函数有: system(cmd_code);exec(cmd_…...
深入Java数据库连接和JDBC
引言 Java数据库连接(JDBC)是Java语言中用于执行SQL语句的标准API。通过JDBC,开发者可以方便地与关系型数据库进行交互。然而,直接使用JDBC API面临着数据库连接管理复杂、性能瓶颈等问题。数据库连接池作为一种解决方案,可以有效地管理数据库连接,提高应用程序的性能。…...
灰狼优化算法(GWO)与长短期记忆网络(LSTM)结合的预测模型(GWO-LSTM)及其Python和MATLAB实现
#### 一、背景 在现代数据科学和人工智能领域,预测模型的准确性和效率是研究者和工程师不断追求的目标,尤其是在时间序列预测、金融市场分析、气象预测等领域。长短期记忆(LSTM)网络是一种解决传统递归神经网络(RNN&a…...
电路板热仿真覆铜率,功率,结温,热阻率信息计算获取方法总结
🏡《电子元器件学习目录》 目录 1,概述2,覆铜率3,功率4,器件尺寸5,结温6,热阻1,概述 电路板热仿真操作是一个复杂且细致的过程,旨在评估和优化电路板内部的热分布及温度变化,以确保电子元件的可靠性和性能。本文简述在进行电路板的热仿真时,元器件热信息的计算方法…...
C#中多线程编程中的同步、异步、串行、并行及并发及死锁
在C#中,多线程编程是一个强大的功能,它允许程序同时执行多个任务。然而,这也带来了复杂性,特别是在处理同步、异步、串行、并行、并发以及死锁等问题时。下面我将详细解释这些概念,并给出一些C#中的示例和注意事项。 …...
做公司网站的服务费入什么费用/网站优化比较好的公司
作者:深邃暗黑范特西 链接:https://www.zhihu.com/question/272156840/answer/367180861 来源:知乎 董可人的那个功夫,低时延的核心在于易经筋模块。进程间通过共享内存(mmap)通信。共享内存上面的数据结构比较简洁,就…...
手机怎么做弹幕小视频网站/站外推广渠道
UIView 继承于UIResponder 所遵守的协议有 NSCoding 、UIAppearance、 UIAppearanceContainer UIDynamicItem、 NSObject 从继承的类我们就可以看出 UIView 这个类可以响应手势 那么我们就从它的属性开始这一旅程吧 UIView 之属性篇 Pro…...
住房和城乡建设部标准定额司网站/引流平台有哪些
Picasso.with(MyApp.getContext()).load("http://avatar.csdn.net/9/3/9/1_mp624183768.jpg").into( viewHolder.ivMingxiImg);依赖 compile com.squareup.picasso:picasso:2.5.2...
宝塔做网站安全吗/网站建设定制
点击链接PTA-Python-AC全解汇总 题目: 本题要求编写程序,计算序列 2/13/25/38/5… 的前N项之和。注意该序列从第2项起,每一项的分子是前一项分子与分母的和,分母是前一项的分子。 输入格式: 输入在一行中给出一个正整数N。 输出…...
做社交网站要注册哪类商标/百度商家入驻
转自:http://www.cnblogs.com/ubosm/p/5444919.html 使用vs2015编译ffmpeg的一个小项时,出现了__imp__fprintf和__imp____iob_func 的错误,google了一下,有的人 建议下载SDL源码重新编译一下,当然这个方案非常不科学。…...
专业做pc+手机网站/短视频营销的优势
内容整理自花利忠教授的课件 文章目录组件对象模型COM基本概念使用接口定义类查询接口(QueryInterface)COM总结组件对象模型COM 基本概念 对象(Object): 系统中用来描述客观事物的一个实体,构成系统的一个基本单位。由类实例化产生 类(Class)…...