Welcome to follow my public numberEfficient Android Development“Focuses on Android project efficiency and development experience, covering topics such as infrastructure, Kotlin Multiplatform, Gradle construction and optimization, etc. At the same time, we also talk about overseas work and life, and push the latest Podcast of” Two-part radio “.

“Build North” is a series of articles exploring Android building. It covers Gradle, Android Gradle Plugin, Kotlin Script, and other tools, as well as related architecture applications. To find the problem to solve the problem as the starting point, transfer new knowledge to improve production efficiency as the foothold.

Issue review

The debuggable attribute in build.gradle is invalid. You can only write the debuggable attribute in the application node of androidmanifest.xml manually. Remove this property before publishing, and add it after publishing. I recently spent two or three weekends researching why.

By default, readers of this article already know this:

The criterion for determining whether an App can be debugged is whether the application node in androidmanifest.xml has the debuggable attribute and its value is true. (Please refer to official documentation)

Problem analysis & source code reproduction

0x01

Start with the debuggable property in buildType. Now that we know that debuggable actually works on the Manifest property, look for manifest-related processes. Process {variant}Manifest, Process {variant}Resources, package{variant} and other tasks in Android Gradle Plugin Nothing unusual was found. But they did learn something about their intermediate products:

  • process{variant}ManifestAndroidManifest.xmlIntermediate is located in the/app/build/intermediates/manifests, is divided into instant – run and other variant (debug, release, etc.) and other folders;
  • process{variant}ResourcesAndroidManifest.xmlLocated in the/app/build/intermediates/res/resources – debug. Ap_ compressed package (in fact at first you don’t look at this task, is a product in the middle of the other task, it found that this task is actuallyAndroidManifest.xmlThe product);
  • package{variant}AndroidManifest.xmlThis is actually the file in the last APK package;

Other than that, I don’t see any direct writing of debuggable to androidmanifest.xml (except merge).

0x02

Compare a new project with a problem project and find:

  • New Projects:process{variant}ManifestIs not found in the product ofdebuggableAttributes, but inprocess{variant}ResourcesThis property (true in debug state) is present in the product of, indicating that the internal action occurred at this step;
  • Problem engineering: in all tasksAndroidManifest.xmlNone of the productsdebuggableThe value of the;

Process {variant}Resources (aAPT) {variant}Resources (aAPT)


// AaptV1.java 

@Override
@NonNull
protected ProcessInfoBuilder makePackageProcessBuilder(@NonNull AaptPackageConfig config)
        throws AaptException {
    ProcessInfoBuilder builder = new ProcessInfoBuilder();

    /* * AaptPackageProcessBuilder had this code below, but nothing was ever added to * mEnvironment. */
    //builder.addEnvironments(mEnvironment);

    builder.setExecutable(getAaptExecutablePath());
    builder.addArgs("package"); . (Omit some code that doesn't matter)// The debuggable property of buildType is injected here
    if (config.isDebuggable()) {
        builder.addArgs("--debug-mode"); }...return builder;
}
Copy the code

As you can see, the debug parameter is added to the input parameter of AAPT, which is the external process of calling AAPT. The Android Gradle Plugin is not in control.

0x03

Find the Android SDK source code, compile your own AAPT (see here for details), and don’t forget to add some logs in key places:

  • First import the file, find the corresponding read--debug-mode[main.cpp], is temporarily stored in a bundle with the correct argument true:
if (strcmp(cp, "-debug-mode") = =0) {
    fprintf(stderr, "AAPTDEBUG: set debug == true");
    bundle.setDebugMode(true);
}
Copy the code
  • Again, findbundle->getDebugMode()And found that this was the core of it, reallydebuggableIs written toAndroidManifest.xml; But there’s an error hereIf the application node cannot be obtained, it is null[Resource. CPP] :
status_t massageManifest(Bundle* bundle, sp<XMLNode> root)
    {
    root = root->searchElement(String16(), String16("manifest"));
    if (root == NULL) {
        fprintf(stderr, "No <manifest> tag.\n");
        return UNKNOWN_ERROR;
    }
    
    // ...
    
    fprintf(stderr, "AAPTDEBUG: bundle->getDebugMode()");
    if (bundle->getDebugMode()) {
        fprintf(stderr, "AAPTDEBUG: bundle->getDebugMode() - true");
        sp<XMLNode> application = root->getChildElement(String16(), String16("application"));
        if (application == NULL) {
            fprintf(stderr, "AAPTDEBUG: application == NULL"); // Print out this line of log
        }
        if(application ! =NULL) {
            fprintf(stderr, "AAPTDEBUG: application ! = NULL");
            if (!addTagAttribute(application, RESOURCES_ANDROID_NAMESPACE, "debuggable"."true",
                        errorOnFailedInsert)) {
                    fprintf(stderr, "AAPTDEBUG: error on insert");
                returnUNKNOWN_ERROR; }}}// ...
}
Copy the code
  • Take a look atgetChildElement()[xmlNode.cpp] : [xmlNode.cpp] : [xmlNode.cpp] : [xmlNode.cpp] : [xmlNode.cpp] : [xmlNode.cpp] : [xmlNode.cpp] :
sp<XMLNode> XMLNode::getChildElement(const String16& tagNamespace, const String16& tagName)
    {
    printf("AAPTDEBUG: root -> %s\n".String8(mElementName).string());
    for (size_t i=0; i<mChildren.size(a); i++) { sp<XMLNode> child = mChildren.itemAt(i);
        if (child->getType() == XMLNode::TYPE_ELEMENT) {
            printf("AAPTDEBUG: getChildElement-> %s\n".String8(child->mElementName).string());
        }
    
        if (child->getType() == XMLNode::TYPE_ELEMENT
                && child->mNamespaceUri == tagNamespace
                && child->mElementName == tagName) {
            returnchild; }}return NULL;
}
Copy the code

  • Then the final question is, why not manifest level application node child nodes, check the compiled binary manifest (. / app/build/intermediates/res/resources – the debug. Ap_, To avoid sensitive information, use the DebuggableTest project as an example) :

  • Application’s direct parent is namespace!

0x04

See all kinds of dependent libraries Manifest (build – cache/exploded – aar), check the Manifest combine log (app/build/outputs/logs/Manifest – merger – {variant} – report). The Manifest Precheck plugin Seal can cause this problem in two ways:

  1. Dependencies on the library itself to declare namespace not only in the manifest node, for example, in the application node declared android namespace, can refer to my pub this DebuggableTest project;
  2. Before the Manifest merge (i.eprocess{variant}ManifestBefore) if there is an exampleSealThis cleaning of the Manifest that depends on the library,And it just so happens that you dogroovy.utilXML libraries do XML parsing and output, so congratulations you, the library has a chance to various nodes may cause you to duplicate the namespace, such as USES – SDK, applicationHowever, I have not seriously investigated under what circumstances such a problem might occur. Currently, only a small number of samples in the experiment have such a problem, so I have not found their common points for the time being.

The solution

  • Using a custom AAPT, a single line of code solves this problem: Call sp

    XMLNode::getChildElement(const string16&tagnamespace, Const string16&tagName) instead of another built-in method sp

    XMLNode::searchElement(const string16&tagnamespace, Const string16&tagName), because the implementation of searchElement is to loop through all the deep children:

    // Resource.cpp
    
     status_t massageManifest(Bundle* bundle, sp<XMLNode> root)
     {
         root = root->searchElement(String16(), String16("manifest"));
         if (root == NULL) {
             fprintf(stderr, "No <manifest> tag.\n");
             returnUNKNOWN_ERROR; }...fprintf(stderr, "AAPTDEBUG: bundle->getDebugMode()");
         if (bundle->getDebugMode()) {
             fprintf(stderr, "AAPTDEBUG: bundle->getDebugMode() - true");
             // change the lookup method!
             sp<XMLNode> application = root->searchElement(String16(), String16("application")); . }... }// XMLNode.cpp
    
     sp<XMLNode> XMLNode::searchElement(const String16& tagNamespace, const String16& tagName)
     {
     
         if (getType() == XMLNode::TYPE_ELEMENT
                 && mNamespaceUri == tagNamespace
                 && mElementName == tagName) {
             return this;
         }
         // Recursive implementations avoid the above problems
         for (size_t i=0; i<mChildren.size(a); i++) { sp<XMLNode> found = mChildren.itemAt(i)->searchElement(tagNamespace, tagName);
             if(found ! =NULL) {
                 returnfound; }}return NULL;
     }
    Copy the code
  • Or use the new Seal plug-in V1.1.0, which adds a configuration item called xmlnsSweep, which cleans its artifacts XMLNS after executing process{variant}Manifest. To avoid unnecessary namespace declarations outside the MANIFEST node, refer to the README of the Seal repository for detailed instructions. There is no guarantee that all dependent libraries will not fail in Precheck.

conclusion

This is probably the most troublesome problem I have studied this year. The link is long and the pits are constantly large and small. Some of them are dug by Google and some by Groovy.

Dang! Update issue address: issuetracker.google.com/issues/6607… !

(Note, the project was based on AAPT1 when this article was written. Currently, the Seal Plugin has been upgraded to version 3.0 and migrated to AAPT2, supporting Android Gradle Plugin 4.2.x alpha. Please refer to the latest instructions in the warehouse.)

Welcome to comment like, and follow my public accountEfficient Android Development.