Android插件化(六): OpenAtlasの改写aapt以防止资源ID冲突
转 https://www.300168.com/yidong/show-2791.html
引言
Android应用程序的编译中,负责资源打包的是aapt,如果不对打包后的资源ID进行控制,就会导致插件中的资源ID冲突。所以,我们需要改写aapt的源码,以达到通过某种方式传递资源ID的Package ID,通过aapt打包时获取到这个Package ID并且应用才插件资源的命名中。
1.改造aapt的目的:防止资源冲突
我们前面知道了插件中的类的加载是通过DelegateClassLoader来进行的,那么插件中资源的加载呢?
其实也是通过DelegateResources进行的,只不过DelegateResources的更新是发生在每个插件安装完成后。在BundleLifecycleHandler的bundleChanged()方法中,监听到BundleEvent.LOADED便开始加载,loaded()方法如下:
//bundle是BundleImpl对象,该对象中的bundleDir是类似"/data/data/cn.edu.zafu.atlasdemo/files/storage/com.lizhangqu.test"这样的 private void loaded(Bundle bundle) { long currentTimeMillis = System.currentTimeMillis(); BundleImpl bundleImpl = (BundleImpl) bundle; try { DelegateResources.newDelegateResources( RuntimeVariables.androidApplication, RuntimeVariables.delegateResources, bundleImpl.getArchive().getArchiveFile().getAbsolutePath()); } catch (Throwable e) { log.error("Could not load resource in bundle " + bundleImpl.getLocation(), e); } if (DelegateComponent.getPackage(bundle.getLocation()) == null) { //注意:会在这里解析出PackageLite对象,其实就是解析Manifest文件中的内容,Application和4大组件等都会解析出来 PackageLite parse = PackageLite.parse(bundleImpl.getArchive() .getArchiveFile()); log.info("Bundle installation info " + bundle.getLocation() + ":" + parse.components); DelegateComponent.putPackage(bundle.getLocation(), parse); } log.info("loaded() spend " + (System.currentTimeMillis() - currentTimeMillis) + " milliseconds"); }
显然是调用newDelegateResources()方法:
//加载宿主中的资源时,newPath为空;加载插件中的资源时,newPath类似"data/data/cn.edu.zafu.atlasdemo/lib/libcom_lizhangqu_test.so"; public static void newDelegateResources(Application application, Resources resources, String newPath) throws Exception { if (Thread.currentThread().getId() == Looper.getMainLooper().getThread().getId()) { newDelegateResourcesInternal(application, resources, newPath); return; } synchronized (lock) { new Handler(Looper.getMainLooper()).post(new DelegateResourcesGetter(application, resources, newPath)); lock.wait(); } }
再进入到DelegateResources.newDelegateResourcesInternal()方法中:
/********将资源加入宿主程序中,最早调用这里的是从FrameworkLifecycleHandler的frameworkEvent()中开始,是将宿主中的资源加入到宿主的AsssetManager中.newPath是类似"/data/data/cn.edu.zafu.atlasdemo/lib/libcom_lizhangqu_test.so" * @param newPath 新插件的路径 * ******/ private static void newDelegateResourcesInternal(Application application, Resources resources, String newPath) throws Exception { AssetManager assetManager; if (ignoreOpt || VERSION.SDK_INT <= 20 || assetPathsHistory == null) { Set<String> generateNewAssetPaths = generateNewAssetPaths(application, newPath);//generateNewAssetPaths中的对象类似["/data/app/cn.edu.zafu.atlasdemo-1.apk","/data/data/cn.edu.zafu.atlasdemo/lib/libcom_lizhangqu_test.so"] if(generateNewAssetPaths.size()>2){ //generateNewAssetPaths.size()>2时,其为["/data/app/cn.edu.zafu.atlasdemo-1.apk","/data/data/cn.edu.zafu.atlasdemo/lib/libcom_lizhangqu_test.so","/data/ddata/cn.edu.zafu.atlasdemo/lib/libcom_lizhangqu_zxing.so"] Log.d(TAG,"generateNewAssetPaths.size()>2"); } if (generateNewAssetPaths != null) { Resources delegateResources; //这个新建的assetManager即新的delegateResources的AssetManager对象,利用它生成delegateResources assetManager = AssetManager.class.newInstance(); for (String assetPath : generateNewAssetPaths) { //但assetPath为"/data/app/cn.edu.zafu.atlasdemo-1.apk"时,即为宿主apk路径时,assetManager.addAssetPath()结果为0表示执行失败,当执行失败时,会在尝试3次 try {//一般OpentAtlasHacks.AssetManager_addAssetPath.invoke(assetManager,assetPath)能够执行成功,所以不需要更多尝试 if (Integer.parseInt(OpenAtlasHacks.AssetManager_addAssetPath.invoke(assetManager, assetPath).toString()) == 0) { for (int i = 0; i < 3; i++) { //再尝试3次 if (Integer.parseInt(OpenAtlasHacks.AssetManager_addAssetPath.invoke(assetManager, assetPath).toString()) != 0) { break; } if (i == 3) { //如果尝试3次之后仍然失败,则打出log OpenAtlasMonitor.getInstance().trace(Integer.valueOf(-1), assetPath, "", "Add asset path failed"); } } } } catch (NumberFormatException e) { e.printStackTrace(); } } if (resources == null || !resources.getClass().getName().equals("android.content.res.MiuiResources")) {//如果是翔米UI需要使用MiuiResources delegateResources = new DelegateResources(assetManager, resources); } else { //MiuiResources的话需要特殊处理 Constructor<?> declaredConstructor = Class.forName("android.content.res.MiuiResources").getDeclaredConstructor(AssetManager.class, DisplayMetrics.class, Configuration.class); declaredConstructor.setAccessible(true);//新建MiuiResources作为delegateResources,并且其中的一个assetManager为刚刚建立的assetManager,包含了宿主和当前插件中的资源,所以通过delegateResources既可以引用插件中的资源,也可以引用宿主中的资源 delegateResources = (Resources) declaredConstructor.newInstance(assetManager, resources.getDisplayMetrics(), resources.getConfiguration()); } RuntimeVariables.delegateResources = delegateResources; //application是类似BootApp对象这样的宿主Application,delegateResources //AndroidHack.injectResources()中利用delegateResources替换LoadedApk中的mResources AndroidHack.injectResources(application, delegateResources); assetPathsHistory = generateNewAssetPaths; if (log.isDebugEnabled()) { StringBuffer stringBuffer = new StringBuffer(); stringBuffer.append("newDelegateResources ["); for (String append : generateNewAssetPaths) { stringBuffer.append(append).append(","); } stringBuffer.append("]"); if (newPath != null) { stringBuffer.append("Add new path:" + newPath); } log.debug(stringBuffer.toString()); return; } return; } return; } assetManager = application.getAssets(); if (!TextUtils.isEmpty(newPath) && !assetPathsHistory.contains(newPath)) { OpenAtlasHacks.AssetManager_addAssetPath.invoke(assetManager, newPath); assetPathsHistory.add(newPath); } }
这个方法主要做了以下事情:
- 新建一个AssetManager对象
- 通过反射调用AssetManager的addAssetPath将当前插件的资源路径添加进去,如果添加失败则尝试3次,3次之后还是失败则给出log
- 如果添加成功,则根据该AssetManager对象生成delegateResources这个DelegateResources或Resources对象,其中对于Miui做了兼容
- 将最新的delegateResources对象赋予RuntimVariables.delegateResources,并且将当前的插件路径赋值给assetPathsHistory
总结起来可以发现:OpenAtlas中管理资源的方式是每安装一个插件,就新建一个AssetManager,并且将之前的资源和插件中的资源都加入到唯这个AssetManager对象的的管理中,之后利用这个AssetManager对象生成新的delegate Resources对象,再利用反射将这个对象注入到LoadedApk中。这样统一管理的好处是一些基础资源(如主题,logo等)可以由宿主提供即可,减小插件包的大小。
但是,这样的话,由于不隔离,如果两个插件的资源ID相同(但是却对应不同的资源),就会造成资源ID的冲突。
首先了解一下Android应用程序的编译和打包过程。用一张图概括如下:
从图中可以清楚地看到Android的编译过程:先是利用aapt编译Manifest,Resources和Assets资源,生成R文件和打包好的资源文件,之后利用javac将java源码编译成字节码,利用NDK将native源码编译成.so库,之后利用dx将所有的字节码(jar包和之前编译的字节码)编译成dex文件(如果有混淆的话需要加上混淆规则),最后利用apkbuilder将dex文件、so库和打包好的资源文件一起编译成apk,如果需要签名的话,再利用签名程序(jarsigner)进行签名。
其中aapt称为Android Asset Package Tool,它的作用是将XML资源文件从文本格式编译成二进制格式,并且会执行以下两个额外的操作:
- 赋予每个非assets资源一个ID值,这些ID值以常量的形式定义在一个R.java文件中
- 生成一个resources.arsc文件,用来描述那些具有ID值的资源的配置信息,它的内容就相当于是一个资源索引表
有了资源ID以及资源索引表之后,Android资源管理框架就可以迅速将根据设备当前配置信息来定位最匹配的资源了。
如果大家对Android应用程序的编译和打包过程不熟悉,可以看老罗的这篇博客 Android应用程序资源的编译和打包过程分析 .
我们在编译一个Android应用程序的资源的时候,至少会涉及到两个包,其中一个是被引用的系统资源包,另外一个就跟当前正在编译的应用程序资源包。每个包都可以定义自己的资源,同时它也可以引用其他包的资源。
那么,一个包是通过什么方式来引用其它包的资源的呢?这就是我们熟悉的资源ID了。资源ID是一个4字节的无符号整数,其中,最高字节表示Package ID,次高字节表示Type ID,最低两字节表示Entry ID。
Package ID相当于是一个命名空间,限定资源的来源。Android系统当前定义了两个资源命令空间,其中一个系统资源命令空间,它的Package ID等于0x01,另外一个是应用程序资源命令空间,它的Package ID等于0x7f。所有位于[0x01, 0x7f]之间的Package ID都是合法的,而在这个范围之外的都是非法的Package ID。前面提到的系统资源包package-export.apk的Package ID就等于0x01,而我们在应用程序中定义的资源的Package ID的值都等于0x7f,这一点可以通过生成的R.java文件来验证。
Type ID是指资源的类型ID。资源的类型有animator、anim、color、drawable、layout、menu、raw、string和xml等等若干种,每一种都会被赋予一个ID。
Entry ID是指每一个资源在其所属的资源类型中所出现的次序。注意,不同类型的资源的Entry ID有可能是相同的,但是由于它们的类型不同,我们仍然可以通过其资源ID来区别开来。
显然,要使各插件的资源ID不冲突,可以通过控制各个插件的Package ID来达到,即使用0x02-0x7E之间的id,如下是一种插件id的架构:
那么如何达到控制package id的目的呢?
当然要通过修改aapt的源码来达到。
2.aapt的改写
aapt的源码在/frameworks/base/tools/aapt下,这里以Android API 22的appt源码为例进行分析。先看Main.cpp中的main()函数:
/* * Parse args. */ int main(int argc, char* const argv[]) { isUpdatePkgId=0; char *prog = argv[0]; Bundle bundle; bool wantUsage = false; int result = 1; // pessimistically assume an error. int tolerance = 0; /* default to compression */ bundle.setCompressionMethod(ZipEntry::kCompressDeflated); if (argc < 2) { wantUsage = true; goto bail; } if (argv[1][0] == 'v') bundle.setCommand(kCommandVersion); else if (argv[1][0] == 'd') bundle.setCommand(kCommandDump); else if (argv[1][0] == 'l') bundle.setCommand(kCommandList); else if (argv[1][0] == 'a') bundle.setCommand(kCommandAdd); else if (argv[1][0] == 'r') bundle.setCommand(kCommandRemove); else if (argv[1][0] == 'p') bundle.setCommand(kCommandPackage); else if (argv[1][0] == 'c') bundle.setCommand(kCommandCrunch); else if (argv[1][0] == 's') bundle.setCommand(kCommandSingleCrunch); else if (argv[1][0] == 'm') bundle.setCommand(kCommandDaemon); else { fprintf(stderr, "ERROR: Unknown command '%s' ", argv[1]); wantUsage = true; goto bail; } argc -= 2; argv += 2; /* * Pull out flags. We support "-fv" and "-f -v". */ while (argc && argv[0][0] == '-') { /* flag(s) found */ const char* cp = argv[0] +1; while (*cp != ' ') { switch (*cp) { case 'v': bundle.setVerbose(true); break; case 'a': bundle.setAndroidList(true); break; ... default: fprintf(stderr, "ERROR: Unknown flag '-%c' ", *cp); wantUsage = true; goto bail; } cp++; } argc--; argv++; } /* * We're past the flags. The rest all goes straight in. */ bundle.setFileSpec(argv, argc); result = handleCommand(&bundle); bail: if (wantUsage) { usage(); result = 2; } //printf("--> returning %d ", result); return result; }
这里省略了main()中对于aapt参数的处理,直接进入handleCommand()函数中:
/* * Dispatch the command. */ int handleCommand(Bundle* bundle) { //printf("--- command %d (verbose=%d force=%d): ", // bundle->getCommand(), bundle->getVerbose(), bundle->getForce()); //for (int i = 0; i < bundle->getFileSpecCount(); i++) // printf(" %d: '%s' ", i, bundle->getFileSpecEntry(i)); switch (bundle->getCommand()) { case kCommandVersion: return doVersion(bundle); case kCommandList: return doList(bundle); case kCommandDump: return doDump(bundle); case kCommandAdd: return doAdd(bundle); case kCommandRemove: return doRemove(bundle); case kCommandPackage: return doPackage(bundle); case kCommandCrunch: return doCrunch(bundle); case kCommandSingleCrunch: return doSingleCrunch(bundle); case kCommandDaemon: return runInDaemonMode(bundle); default: fprintf(stderr, "%s: requested command not yet supported ", gProgName); return 1; } }
显然,handleCommand()是用于分发命令的,我们的是打包资源的命令,所以是调用doPackage(bundle);其中doPackage()方法如下:
/* * Package up an asset directory and associated application files. */ int doPackage(Bundle* bundle) { const char* outputAPKFile; int retVal = 1; status_t err; sp<AaptAssets> assets; int N; FILE* fp; String8 dependencyFile; sp<ApkBuilder> builder; // -c en_XA or/and ar_XB means do pseudolocalization sp<WeakResourceFilter> configFilter = new WeakResourceFilter(); err = configFilter->parse(bundle->getConfigurations()); if (err != NO_ERROR) { goto bail; } if (configFilter->containsPseudo()) { bundle->setPseudolocalize(bundle->getPseudolocalize() | PSEUDO_ACCENTED); } if (configFilter->containsPseudoBidi()) { bundle->setPseudolocalize(bundle->getPseudolocalize() | PSEUDO_BIDI); } N = bundle->getFileSpecCount(); if (N < 1 && bundle->getResourceSourceDirs().size() == 0 && bundle->getJarFiles().size() == 0 && bundle->getAndroidManifestFile() == NULL && bundle->getAssetSourceDirs().size() == 0) { fprintf(stderr, "ERROR: no input files "); goto bail; } outputAPKFile = bundle->getOutputAPKFile(); // Make sure the filenames provided exist and are of the appropriate type. if (outputAPKFile) { FileType type; type = getFileType(outputAPKFile); if (type != kFileTypeNonexistent && type != kFileTypeRegular) { fprintf(stderr, "ERROR: output file '%s' exists but is not regular file ", outputAPKFile); goto bail; } } // Load the assets. assets = new AaptAssets(); // Set up the resource gathering in assets if we're going to generate // dependency files. Every time we encounter a resource while slurping // the tree, we'll add it to these stores so we have full resource paths // to write to a dependency file. if (bundle->getGenDependencies()) { sp<FilePathStore> resPathStore = new FilePathStore; assets->setFullResPaths(resPathStore); sp<FilePathStore> assetPathStore = new FilePathStore; assets->setFullAssetPaths(assetPathStore); } err = assets->slurpFromArgs(bundle); if (err < 0) { goto bail; } if (bundle->getVerbose()) { assets->print(String8()); } // Create the ApkBuilder, which will collect the compiled files // to write to the final APK (or sets of APKs if we are building // a Split APK. builder = new ApkBuilder(configFilter); // If we are generating a Split APK, find out which configurations to split on. if (bundle->getSplitConfigurations().size() > 0) { const Vector<String8>& splitStrs = bundle->getSplitConfigurations(); const size_t numSplits = splitStrs.size(); for (size_t i = 0; i < numSplits; i++) { std::set<ConfigDescription> configs; if (!AaptConfig::parseCommaSeparatedList(splitStrs[i], &configs)) { fprintf(stderr, "ERROR: failed to parse split configuration '%s' ", splitStrs[i].string()); goto bail; } err = builder->createSplitForConfigs(configs); if (err != NO_ERROR) { goto bail; } } } // If they asked for any fileAs that need to be compiled, do so. if (bundle->getResourceSourceDirs().size() || bundle->getAndroidManifestFile()) { err = buildResources(bundle, assets, builder); if (err != 0) { goto bail; } } ... }
这里省略了编译资源之后输出R.java文件等代码,可以看出编译资源的代码是buildResources():
status_t buildResources(Bundle* bundle, const sp<AaptAssets>& assets, sp<ApkBuilder>& builder) { // First, look for a package file to parse. This is required to // be able to generate the resource information. sp<AaptGroup> androidManifestFile = assets->getFiles().valueFor(String8("AndroidManifest.xml")); if (androidManifestFile == NULL) { fprintf(stderr, "ERROR: No AndroidManifest.xml file found. "); return UNKNOWN_ERROR; } status_t err = parsePackage(bundle, assets, androidManifestFile); if (err != NO_ERROR) { return err; } NOISY(printf("Creating resources for package %s ", assets->getPackage().string())); ResourceTable::PackageType packageType = ResourceTable::App; if (bundle->getBuildSharedLibrary()) { packageType = ResourceTable::SharedLibrary; } else if (bundle->getExtending()) { packageType = ResourceTable::System; } else if (!bundle->getFeatureOfPackage().isEmpty()) { packageType = ResourceTable::AppFeature; } ResourceTable table(bundle, String16(assets->getPackage()), packageType); err = table.addIncludedResources(bundle, assets); if (err != NO_ERROR) { return err; } ... return err; }
这里省略了很多无关的代码,其中的PackageType就是与Package ID有关的,它的定义如下:
enum PackageType{ App, System, SharedLibrary, AppFeature }
显然,分为普通应用类型,系统类型,共享库类型和AppFeature类型。其中ResourceTable类的构造函数如下:
ResourceTable::ResourceTable(Bundle* bundle, const String16& assetsPackage, ResourceTable::PackageType type) : mAssetsPackage(assetsPackage) , mPackageType(type) , mTypeIdOffset(0) , mNumLocal(0) , mBundle(bundle) { ssize_t packageId = -1; switch (mPackageType) { case App: case AppFeature: packageId = 0x7f; break; case System: packageId = 0x01; break; case SharedLibrary: packageId = 0x00; break; default: assert(0); break; } sp<Package> package = new Package(mAssetsPackage, packageId); mPackages.add(assetsPackage, package); mOrderedPackages.add(package); // Every resource table always has one first entry, the bag attributes. const SourcePos unknown(String8("????"), 0); getType(mAssetsPackage, String16("attr"), unknown); }
在这里就可以很明显的看出PackageType与packageId的关系.
看到这里,就可以知道,需要控制插件的packageId,就需要修改ResourceTable的构造函数,在其中传入对应插件的packageId。
这里有两个思路:第一种是将在Bundle中增加字段,将这个参数放在bundle中(因为bundle是从main()函数中一路传递下来的);第二种是通过全局变量来引用。
而bunnyblue采用的是第二种方法(其实这种方法不优美).
bunnyblue具体的实现方法是:在插件的build.gradle中的versionName中同时声明versionName和packageId,如下:
defaultConfig { applicationId "com.lizhangqu.test" minSdkVersion 10 targetSdkVersion 22 versionCode 1 versionName "1.00x20" }
到了aapt中在进行处理,分离为"1.0"这个versionName和0x20这个插件的packageId.
具体是如何分离的呢?
在Main.cpp的main()方法中,有这么一行:
int main(int argc, char* const argv[]){ ... case 'M': argc--; argv++; if (!argc) { fprintf(stderr, "ERROR: No argument supplied for '-M' option "); wantUsage = true; goto bail; } convertPath(argv[0]); bundle.setAndroidManifestFile(argv[0]); hack_getVersionName(&bundle); break; ... }
注意其中的hack_getVersionName(&bundle);该方法在Resourcehack.cpp中,如下:
void hack_getVersionName(Bundle* bundle){ // return ; fprintf(stderr, "hack version ibundle->getAndroidManifestFile()X%s, ",bundle->getAndroidManifestFile()); String8 srcFile(bundle->getAndroidManifestFile()); fprintf(stderr, "hack version ibundle->getAndroidManifestFile() %s , %s 1 ",srcFile.getPathLeaf().string(), srcFile.getPathDir().string()); AaptFile *mAaptFile=new AaptFile(srcFile.getPathLeaf(), AaptGroupEntry(), srcFile.getPathDir()); fprintf(stderr, "hack version ibundle->getAndroidManifestFile()2 "); const sp<AaptFile> manifestFile(mAaptFile); fprintf(stderr, "hack version ibundle->getAndroidManifestFile()3 "); String8 manifestPath(bundle->getAndroidManifestFile()); fprintf(stderr, "hack version dump info ..get default versionName%s ",manifestPath.string()); fprintf(stderr, "hack version ibundle->getAndroidManifestFile()4 "); // Generate final compiled manifest file. //manifestFile->clearData(); sp<XMLNode> root = XMLNode::parse(bundle->getAndroidManifestFile()); fprintf(stderr, "hack version ibundle->getAndroidManifestFile()6 "); if (root == NULL) { if(!access(bundle->getAndroidManifestFile(),0)){}else{ fprintf(stderr, "no found 7 "); } fprintf(stderr, "no node 7 "); return ; } hack_massageManifest(root); // root = root->searchElement(String16(), String16("manifest")); // // const XMLNode::attribute_entry* attrlocal = root->getAttribute( // String16(RESOURCES_ANDROID_NAMESPACE), String16("versionName")); // if (attrlocal != NULL) { // fprintf(stderr, "hack version dump info ..get default versionName%s ",strdup(String8(attrlocal->string).string())); // char * versionNameMisc=strdup(String8(attrlocal->string).string());//bunny // if(strlen(versionNameMisc)>5){ // char resOffset[64]={0}; // strncpy(resOffset,versionNameMisc+strlen(versionNameMisc)-4,4); // if(resOffset[0]=='0'&&resOffset[1]=='x'){ // pkgIdOffset=strtol(resOffset,NULL,16); // } // fprintf(stderr, "hack version is ok,found new version packageID %s ",resOffset); // }else{ // fprintf(stderr, "hack version is failed,versionName should endwith 0xXX "); // // } // // // // } //delete(mAaptFile); fprintf(stderr, "hack version ibundle->getAndroidManifestFile()7 "); }
显然,由于bundle.gradle中的versionName会写入到Manifest文件中,所以这里通过解析Manifest文件来获取插件的packageId,在hack_messageManifest()中:
void hack_massageManifest( sp<XMLNode> root) { root = root->searchElement(String16(), String16("manifest")); const XMLNode::attribute_entry* attrlocal = root->getAttribute( String16(RESOURCES_ANDROID_NAMESPACE), String16("versionName")); if (attrlocal != NULL) { fprintf(stderr, "hack version dump info ..get default versionName%s ",strdup(String8(attrlocal->string).string())); char * versionNameMisc=strdup(String8(attrlocal->string).string());//bunny if(strlen(versionNameMisc)>5){ char resOffset[64]={0}; strncpy(resOffset,versionNameMisc+strlen(versionNameMisc)-4,4); if(resOffset[0]=='0'&&resOffset[1]=='x'){ pkgIdOffset=strtol(resOffset,NULL,16); isUpdatePkgId=1; } fprintf(stderr, "hack version is ok,found new version packageID %s ",resOffset); }else{ fprintf(stderr, "hack version is failed,versionName should endwith 0xXX "); } } // if (!addTagAttribute(root, RESOURCES_ANDROID_NAMESPACE, "versionName", // "0x7f", errorOnFailedInsert, true)) { // return UNKNOWN_ERROR; // } else { // const XMLNode::attribute_entry* attr = root->getAttribute( // String16(RESOURCES_ANDROID_NAMESPACE), String16("versionName")); // if (attr != NULL) { // fprintf(stderr, "hack version dump info... %s ",strdup(String8(attr->string).string())); // bundle->setVersionName(strdup(String8(attr->string).string())); // } // } }
显然,在这里分离出了插件的packageId并赋值给了全局变量pkgIdOffset,而这个pkgIdOffset是在Main.cpp中定义的:
int pkgIdOffset=0x7f;
显然,默认值为0x7f;而pkgIdOffset的使用当然是在ResourceTable的构造函数中:
ResourceTable::ResourceTable(Bundle* bundle, const String16& assetsPackage, ResourceTable::PackageType type) : mAssetsPackage(assetsPackage) , mPackageType(type) , mTypeIdOffset(0) , mNumLocal(0) , mBundle(bundle) { ssize_t packageId = -1; switch (mPackageType) { case App: case AppFeature: packageId = pkgIdOffset; break; case System: packageId = 0x01; break; case SharedLibrary: packageId = 0x00; break; default: assert(0); break; } sp<Package> package = new Package(mAssetsPackage, packageId); mPackages.add(assetsPackage, package); mOrderedPackages.add(package); // Every resource table always has one first entry, the bag attributes. const SourcePos unknown(String8("????"), 0); getType(mAssetsPackage, String16("attr"), unknown); }
显然,对于mPackageType为App和AppFeature的,packageId=pkgIdOffset.这样就获取到了我们写在插件项目的build.gradle中的packageId值。
不过,我自己觉得最好的处理方案是在Manifest中增加一个packageId的attr,之后在aapt的main()中解析出这个结果,并且放入bundle的字段中,最终在ResoureTable中对于App和AppFeature,去bundle中的该字段作为packageId.
改造后的源码可以在 OpenAtlasExtension 看到。