preface

The whole article is expanded around the knowledge points in Tinker ResourceidTask.

  1. aaptaapt2Differences (operating environment and operating results);
  2. resourcesidThe fixed;
  3. forPUBLICThe tag;

Aapt runs gradle:2.2.0 and gradle-wrapper:3.4.1

Aapt2 run gradle:3.3.2 and gradle-Wrapper :5.6.2

The Android-aapt-sample project is my own experimental sample. There are two branches, AAPT and AAPT2, corresponding to their implementation respectively.

AAPT overview

Since Android Studio 3.0, Google has enabled AAPT2 as a compiler for resource compilation by default. Aapt2 is available to support incremental compilation of resources. To disable AAPT2, set Android. enableAapt2=false in gradle.properties.

resources

Android is compatible with various natural quite a lot of work has been done on different devices, such as screen size, internationalization, keyboard, pixel density and so on, we can use for a particular variety of specific scenarios compatible without resources changes one line of code, suppose we adapted for a variety of different scenarios of different resources, How can I use these resources quickly? Android provides us with R class to specify the index (ID) of a resource, and then we only need to tell the system to use the corresponding resource in different business scenarios. As for the specific file in the specified resource, it is decided by the system according to the configuration of the developer.

In this scenario, assuming that the given ID is the x value, when the current business needs to use the resource, the state of the phone is the Y value. With (x,y), the specific path of the resource file can be quickly located in a table. This table is resources.arsc, which is compiled from AAPT.

Binary resources (such as images) do not need to be compiled, but the act of “compiling” is used to generate resources. Arsc is the table above, and binary XML file is used to make the system read better. AssetManager will find the corresponding file in this table and read it when we call the R related ID.

Gradle calls these aapT2 commands when compiling resources, and the parameters passed are described in this document, but the call details are hidden from the developer.

Aapt2 mainly have two steps, one is compile and the other is link.

Create an empty project: write only two XML, androidmanifest.xml and activity_main.xml.

Compile

 mkdir compiled
 aapt2 compile src/main/res/layout/activity_main.xml -o compiled/
Copy the code

In the compiled folder, there is a file called layout_activity_main.xml. Flat, which is aapT2 specific and not available in AAPT (aAPT copies source files), and which aapT2 use for incremental compilation. If we have a lot of files, we need to compile them one by one. We can also use the -dir parameter, but this parameter does not have the effect of incremental compilation. That is, AAPT2 recompile all the files in the directory even if only one resource changes when passing the entire directory.

Link

Link is a bit more work than compile, where the input is multiple flat files and Androidmanifest.xml, external resources, and the output is apK and R.Java containing only resources. The command is as follows:

aapt2 link -o out.apk \
-I $ANDROID_HOME/platforms/android-28/android.jar \
compiled/layout_activity_main.xml.flat \
--java src/main/java \
--manifest src/main/AndroidManifest.xml
Copy the code
  • The second line-IimportExternal resources, mainly hereandroidSome properties defined under the namespace that we normally use@android:xxxIt’s all here jarIn fact, we can also provide their own resources for others to link;
  • The third line is inputflatFile, if there are multiple, directly in the back can be spliced;
  • In the fourth row aR.javaGenerated directories;
  • The fifth line is specifiedAndroidManifest.xml;

After Link is completed, out.apk and r.java are generated, and out.apk contains a resources.arsc file. Those with only resource files can use the suffix. Ap_.

View compiled resources

In addition to using Android Studio to check resources. Arsc, you can also directly use aapT2 dump APK information to check the ID and status of resources:

aapt2 dump out.apk
Copy the code

The output is as follows:

Binary APK
Package name=com.geminiwen.hello id=7f
  type layout id=01 entryCount=1
    resource 0x7f010000 layout/activity_main
      () (file) res/layout/activity_main.xml type=XML
Copy the code

You can see that the ID of layout/ Activity_main is 0x7F010000.

Resource sharing

Android.jar is just a compilation stump, and when it comes time to actually execute, the Android OS provides a runtime library (framework.jar). Android.jar is a lot like an APK, except it has a class file, and then an Androidmanifest.xml and resources.arsc. This means that we can also use aapT2 dump on it and run the following command:

aapt2 dump $ANDROID_HOME/platforms/android-28/android.jar > test.out
Copy the code

We get a lot of output like this:

resource 0x010a0000 anim/fade_in PUBLIC () (file) res/anim/fade_in.xml type=XML resource 0x010a0001 anim/fade_out PUBLIC  () (file) res/anim/fade_out.xml type=XML resource 0x010a0002 anim/slide_in_left PUBLIC () (file) res/anim/slide_in_left.xml type=XML resource 0x010a0003 anim/slide_out_right PUBLIC () (file) res/anim/slide_out_right.xml type=XMLCopy the code

It has some PUBLIC fields. Resources in an APK file, if marked with this tag, can be referenced by other APKs using @ package name: type/name, for example: @android:color/red.

If we want to provide our resources, we will first mark our resources as PUBLIC and then reference your package name in XML, such as @com.gemini. App :color/red to reference your defined color/red.

For those interested in how AAPT2 generate PUBLIC, read on.

Ids. Summary of XML

Ids. XML: provides unique resource ids for application-related resources. Object = findViewById(R.id_name); Object = findViewById(R.id_name); The id_name.

These values can be referenced in the code with Android.r.ID. If ID is defined in ids. XML, @id/price_edit can be defined in layout as follows; otherwise, @+ ID /price_edit can be defined in layout.

advantages

  1. Naming is convenient, we can put some specific control first life good name, when using direct referenceidCan, save a naming link.
  2. Optimize compilation efficiency:
    • addidWill be in afterR.javaGenerated;
    • useids.xmlUnified management, one-time compilation can be used many times. But the use of"@+id/btn_next"Form each time the file is saved(Ctrl+s)After R.j avaWill be retested if theidIf it does not exist, it needs to be addedid. Therefore, the compilation efficiency decreases.

Ids.xml file contents:


      
<resources>
    <item name="forecast_list" type="id"/>
<! -- <item name="app_name" type="string" />-->
</resources>
Copy the code

If you are curious, there is a line of annotated code above. If you open the comment, you will find that the compilation will report an error:

Execution failed for task ':app:mergeDebugResources'.
> [string/app_name] /Users/tanzx/AndroidStudioProjects/AaptDemo/app/src/main/res/values/strings.xml	[string/app_name] /Users/tanzx/AndroidStudioProjects/AaptDemo/app/src/main/res/values/ids.xml: Error: Duplicate resources
Copy the code

App_name is already declared in value.

Public. Summary of XML

Official website: Select resources to be made public.

All resources in the library are public by default. To make all resources implicitly private, you must define at least one specific property as public. Resources include all files, such as images, in your project’s RES/directory. To prevent users of a library from accessing resources intended for internal use only, you should use this automatic private identification mechanism by declaring one or more public resources. Alternatively, you can make all resources private by adding an empty tag, which does not make any resources public, but makes everything (all resources) private.

By making properties implicitly private, you not only prevent users of your library from getting code completion suggestions from internal library resources, but you can also rename or remove private resources without breaking the library’s clients. Private resources are filtered from code completion, and Lint will warn you when you try to reference private resources.

When you build the library, the Android Gradle plug-in takes the public resource definition and extracts it into a public.txt file, which is then packaged into an AAR file.

The measured results are just not back to the code automatically incomplete, the compiler reported red. If lint is checked, compile without warning ~!

The file RES/value/public.xml is used to assign fixed resource ids to Android resources.

stackoverfloew:What is the use of the res/values/public.xml file on Android?

The contents of the public.xml file:


      
<resources>
    <public name="forecast_list" id="0x7f040001" type="id" />
    <public name="app_name" id="0x7f070002" type="string" />
    <public name="string3" id="0x7f070003" type="string" />
</resources>
Copy the code

Fixed resource ID

The fixation of resource ids is extremely important in hot fixes and plugins. During patch construction, ensure that the resource ID of the patch package is consistent with that of the base package. In plug-in, if the plug-in needs to reference the host resource, the host resource ID needs to be fixed. Therefore, resource ID fixation is particularly important in these two scenarios.

In Android Gradle Plugin 3.0.0, aapT2 is enabled by default, and the original aAPT resource fixing method, public. XML, will also be disabled. We must find a new way to fix resources instead of simply disabling AAPT2. Therefore, this paper discusses how AAPT and AAPT2 fix resource IDS respectively.

aaptforidThe fixed

Project environment configuration (PS: Just to make fun of aAPT has been replaced by AAPT2, there is almost no relevant information about AAPT, it is too difficult to build environment ~!)

Com. Android. Tools. Build: gradle: 2.2.0

distributionUrl=https\://services.gradle.org/distributions/gradle-3.4.1-all.zip

compileSdkVersion 24

BuildToolsVersion '24.0.0'

Generate the corresponding files in the value file according to the contents and file names of ids. XML and public. XML above.

Direct compilation results

By looking directly at the contents of the compiled R file, we can see that the resource IDS we want to set are not generated as expected.

To the public. The XML file copy to build/intermediates/res/merged the corresponding directory

afterEvaluate {
    for (variant in android.applicationVariants) {
        def scope = variant.getVariantData().getScope()
        String mergeTaskName = scope.getMergeResourcesTask().name
        def mergeTask = tasks.getByName(mergeTaskName)
        mergeTask.doLast {
            copy {
                int i=0
                from(android.sourceSets.main.res.srcDirs) {
                    include 'values/public.xml'
                    rename 'public.xml', (i++ == 0? "public.xml": "public_${i}.xml")
                }
                into(mergeTask.outputDir)
            }
        }
    }
}
Copy the code

This time we can directly see the resource ID generated as we need it.

Why is that?

  1. Public. XML can be compiled directly in the source res directory.

  2. The 1.3+ version of Android Gradle plugin ignores public. XML when the mergeResource task is executed, so there is no public. So you need to insert public. XML into the RES directory in the build directory after the merge at compile time through a script. This is possible because Aapt supports public. XML itself, but gradle filters public. XML when merging resources.

aapt2foridThe fixed

Copying the public. XML file to this directory will cause a compilation error when aapT2 compile the resources into binary format and discover that the resources of the merge have been precompiled, resulting in flat files.

But in the link stage of AAPT2, we look at the relevant link options:

options instructions
--emit-ids path Generates a file at the given path that contains the name of the resource type and a list of its ID mappings. It is suitable for the--stable-idsUse in combination.
--stable-ids outputfilename.ext Used by--emit-idsGenerated file that contains the name of the resource type and a list of ids assigned to it. This option keeps assigned ids stable even if you remove or add new resources when linking.

— ememit -ids and — stables -ids can be used to fix ids.

android {
  aaptOptions {
        File publicTxtFile = project.rootProject.file('public.txt')
        // If the public file exists, apply it; if it does not exist, generate it
        if (publicTxtFile.exists()) {
            project.logger.error "${publicTxtFile} exists, apply it."
            // Add aapT2 -- stables are applied
            aaptOptions.additionalParameters("--stable-ids"."${publicTxtFile}")}else {
            project.logger.error "${publicTxtFile} not exists, generate it."
            // aapT2 add --emit- IDS parameter generated
            aaptOptions.additionalParameters("--emit-ids"."${publicTxtFile}")}}}Copy the code
  1. First compile, pass first--emit-idsGenerated at the root of the projectpublic.txt;
  2. thenpublic.txtOn the insideidChange to what you want to fixid;
  3. Compile again, pass--stable-idsAnd the root directorypublic.txtFor resourceidThe fixed;

–emit-ids compilation result

Modify the contents of the public. TXT file to compile again

R.t xt turn public. TXT

Our normal packaging generated intermediate is the build/intermediates/symbols/debug/R.t xt, need to convert it to the public. TXT.

R.xt format (int type name id) or (int[] styleable name {id,id, XXXX})

TXT format (applicationId:type/name = id)

Therefore, styleable types in r.tuck files need to be filtered out during conversion.

android {
    aaptOptions {
        File rFile = project.rootProject.file('R.txt')
        List<String> sortedLines = new ArrayList<>()
        // Read line by line
        rFile.eachLine {line ->
            //rLines.add(line)
            String[] test = line.split("")
            String type = test[1]
            String name = test[2]
            String idValue = test[3]
            if ("styleable"! = type) { sortedLines.add("${applicationId}:${type}/${name} = ${idValue}")
            }
        }
        Collections.sort(sortedLines)
        File publicTxtFile = project.rootProject.file('public.txt')
        if(! publicTxtFile.exists()) { publicTxtFile.createNewFile() sortedLines? .each { publicTxtFile.append("${it}\n")}}}}Copy the code

PUBLIC tag

In the AAPT overview section, we explained that resources in an APK file can be referenced by other APKs if they are tagged with a PUBLIC flag, using @ package name: type/name, e.g., @ Android :color/red.

Reading the sections “AAPT for ID fixation” and “AAPT2 for ID fixation” above, we know that the methods of AAPT and AAPT2 for ID fixation are not the same.

In fact, if we use aapt2 dump the build/intermediates/res/resources – debug. Ap_ command to view information generated resources.

Aapt uses public. XML for fixed resource information with public tags:

Second, the above method of AAPT2 id fixing does not have the PUBLIC mark as shown in the figure below.

This is because of the difference between AAPT and AAPT2. Aapt2’s public. TXT is not equal to AAPT’s public.

Review of thinking

review

  1. aaptFor resourceidFixed andPUBLICPrice tag, is willpublic.xmlCopied to the${mergeResourceTask.outputDir};
  2. aapt2Compared to theaapt, optimized for incremental compilation.AAPT2The file is parsed and an extension is generated.flatThe intermediate binary file of.

thinking

Can use aapt2 themselves to the public. The XML for the public. Arsc. Flat, and copy it to the like aapt operation ${mergeResourceTask. OutputDir};

Hands-on practice

android {
    / / will be public. TXT into public. XML, and the public. The XML aapt2 compile copies the result to ${ergeResourceTask. OutputDir}
  // Most of the following code is copied from tinker's source code
  applicationVariants.all { def variant ->
      def mergeResourceTask = project.tasks.findByName("merge${variant.getName().capitalize()}Resources")
      if (mergeResourceTask) {
          mergeResourceTask.doLast {
              // Target conversion file, note that the parent directory of public. XML must have values directory, otherwise AAPT2 will report an illegal file path
              File publicXmlFile = new File(project.buildDir, "intermediates/res/public/${variant.getDirName()}/values/public.xml")
              // Convert the public.txt file to publicXml, with the last parameter true identifying the fixed resource ID
              convertPublicTxtToPublicXml(project.rootProject.file('public.txt'), publicXmlFile, false)
              def variantData = variant.getMetaClass().getProperty(variant, 'variantData')
              def variantScope = variantData.getScope()
              def globalScope = variantScope.getGlobalScope()
              def androidBuilder = globalScope.getAndroidBuilder()
              def targetInfo = androidBuilder.getTargetInfo()
              def mBuildToolInfo = targetInfo.getBuildTools()
              Map<BuildToolInfo.PathId, String> mPaths = mBuildToolInfo.getMetaClass().getProperty(mBuildToolInfo, "mPaths") as Map<BuildToolInfo.PathId, String>
                Public. / / by aapt2 compile command generated arsc. The flat and output to ${mergeResourceTask. OutputDir}
              project.exec(new Action<ExecSpec>() {
                  @Override
                  void execute(ExecSpec execSpec) {
                      execSpec.executable "${mPaths.get(BuildToolInfo.PathId.AAPT2)}"
                      execSpec.args("compile")
                      execSpec.args("--legacy")
                      execSpec.args("-o")
                      execSpec.args("${mergeResourceTask.outputDir}")
                      execSpec.args("${publicXmlFile}")}})}}}}Copy the code

Convert the public. TXT file to a public. XML file.

  • public.txtinstyleableType resource,public.xmlDoes not exist, so if encountered during conversionstyleableType, to be ignored;
  • vectorIf there is an internal vector resource, it also needs to be ignoredaapt2In, its name is$The primary resource name is followed by the __ number increment index. These resources are not referenced externally, but need to be fixedidYou do not need to addPUBLICMark, and$Sign inpublic.xmlIs illegal, so ignore it.
  • Due to theaapt2Has the resourcesidThe fixed way, so the conversion process can be directly discardedid, simple declaration can be (PS: here throughwithIdWhether the parameter control needs to be fixedid);
  • aapt2Compilation ofpublic.xmlThe parent directory of the file must bevaluesFolder, otherwise the compilation process will report an illegal path;
/** * convert publicTxt to publicXml * copy tinker:com.tencent.tinker.build.gradle.task.TinkerResourceIdTask#convertPublicTxtToPublicXml */
@SuppressWarnings("GrMethodMayBeStatic")
void convertPublicTxtToPublicXml(File publicTxtFile, File publicXmlFile, boolean withId) {
    if (publicTxtFile == null || publicXmlFile == null| |! publicTxtFile.exists() || ! publicTxtFile.isFile()) {throw new GradleException("publicTxtFile ${publicTxtFile} is not exist or not a file")
    }

    GFileUtils.deleteQuietly(publicXmlFile)
    GFileUtils.mkdirs(publicXmlFile.getParentFile())
    GFileUtils.touch(publicXmlFile)

    project.logger.info "convert publicTxtFile ${publicTxtFile} to publicXmlFile ${publicXmlFile}"

    publicXmlFile.append("<! -- AUTO-GENERATED FILE. DO NOT MODIFY -->")
    publicXmlFile.append("\n")
    publicXmlFile.append("<resources>")
    publicXmlFile.append("\n")
    Pattern linePattern = Pattern.compile(". *? : (. *?) / (. *?) \\s+=\\s+(.*?) ")

    publicTxtFile.eachLine {def line ->
        Matcher matcher = linePattern.matcher(line)
        if (matcher.matches() && matcher.groupCount() == 3) {
            String resType = matcher.group(1)
            String resName = matcher.group(2)
            if (resName.startsWith('$')) {
                project.logger.info "ignore to public res ${resName} because it's a nested resource"
            } else if (resType.equalsIgnoreCase("styleable")) {
                project.logger.info "ignore to public res ${resName} because it's a styleable resource"
            } else {
                if (withId) {
                    publicXmlFile.append("\t<public type=\"${resType}\" name=\"${resName}\" id=\"${matcher.group(3)}\" />\n")}else {
                    publicXmlFile.append("\t<public type=\"${resType}\" name=\"${resName}\" />\n")
                }

            }
        }
    }
    publicXmlFile.append("</resources>")}Copy the code

Through the above process of thinking and hands-on practice, we not only solved the problem of AAPT2’s PUBLIC labeling, but also found a new method for AAPT2’s ID fixation.

Possible errors:

no signature of method com.android.build.gradle.internal.variant.applicationvariantdata.getscope() is applicable for argument types: () values: []
Copy the code

The solution is to change gradle versions to gradle:3.3.2 and gradle-wrapper:5.6.2. Tinker does not support the latest gradle versions either.

Reference:

Github:tinker

Android public. XML usage

Android – Gradle notes

The resource ID of AAPT2 adaptation is fixed

The article here is all about the end, if there are other need to exchange can leave a message oh ~! ~!