从ParseError at [row,col]:[1,1]讲如何优雅调试第三方插件

531 阅读4分钟

Android Gradle 优雅调试第三方插件

前言:上一篇文章已经讲解了如何编写Gradle插件,并且教会了大家如何调试自己的插件,接下来我们要开始学习如何调试别人写的插件,也就是三方插件,例如android团队给我们写的application插件又或者是kotlin团队给我们写的插件。下面我们开始今天的内容,大概需要花费5分钟的时候看完整篇文章。

从报错入手

首先我们要明确我们debug的作用是用来解决问题的,可以解决自身项目的一些疑难问题,又或者第三方的插件存在不兼容的情况,我们甚至可以给他们提交一些pull request。其实这并非什么难事,大家要相信自己。由此我们今天的文章由一个错误引出我们今天的主题。在编译项目中遇到如下错误

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':app:shrinkXXXXReleaseRes'.
> A failure occurred while executing com.android.build.gradle.internal.transforms.ShrinkProtoResourcesAction
   > ParseError at [row,col]:[1,1]
     Message: Content is not allowed in prolog.

* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.

请问当你遇到的的时候你该如何解决这个问题。请自我思考10秒钟,同时也是对自己的检验。通常大家会有以下几种解决方案。

谷歌或者百度查询

选择这种方法可能是大多数人的想法,那么咱们去百度摘一下数据如下图所示

图片1.png

看上去似乎第二条是我们需要的答案,我们打开进去看一下如何解决,如下图

图片2.png

看上去就是咱们需要的解决方案,在gradle.properties中写上android.enableNewResourceShrinker=false,这个方案就是将新的压缩方案给禁用,本质上来说只解决了表面问题。可以看到百度的方案并不是那么的完美。

stackoverflow搜索

由于篇幅有限,这里就不每个方案都深入讲解了,这里大概率找到的也是跟谷歌百度的类似。

根据提示 --stacktrace 将堆栈打印出来

现在我们来使用--stacktrace看下报错的堆栈,这个方案其实很多场景都会用到,已经可以超过绝大多数的程序员了,你能想到这种方案那么说明你很优秀,给自己鼓个掌。现在我使用这种方案来执行一下

./gradlew :app:shrinkXXXXRelease --stacktrace

Caused byjavax.xml.stream.XMLStreamExceptionParseError at [row,col]:[1,1]
MessageContent is not allowed in prolog.
        at java.xml/com.sun.org.apache.xerces.internal.impl.XMLStreamReaderImpl.next(XMLStreamReaderImpl.java:652)
        at com.android.build.gradle.internal.res.shrinker.usages.ToolsAttributeUsageRecorder.processResourceToolsAttributes(ToolsAttributeUsageRecorder.kt:69)
        at com.android.build.gradle.internal.res.shrinker.usages.ToolsAttributeUsageRecorder.processRawXml(ToolsAttributeUsageRecorder.kt:49)
        at com.android.build.gradle.internal.res.shrinker.usages.ToolsAttributeUsageRecorder.access$processRawXml(ToolsAttributeUsageRecorder.kt:37)
        at com.android.build.gradle.internal.res.shrinker.usages.ToolsAttributeUsageRecorder$recordUsages$2.accept(ToolsAttributeUsageRecorder.kt:45)
        at com.android.build.gradle.internal.res.shrinker.usages.ToolsAttributeUsageRecorder$recordUsages$2.accept(ToolsAttributeUsageRecorder.kt:45)
        at com.android.build.gradle.internal.res.shrinker.usages.ToolsAttributeUsageRecorder.recordUsages(ToolsAttributeUsageRecorder.kt:45)
        at com.android.build.gradle.internal.res.shrinker.ResourceShrinkerImpl.analyze(ResourceShrinkerImpl.kt:85)
        at com.android.build.gradle.internal.transforms.ShrinkProtoResourcesAction.doExecute(ShrinkResourcesNewShrinkerTask.kt:305)
        at com.android.build.gradle.internal.workeractions.WorkActionAdapter.execute(WorkActionAdapter.kt:39)
        ... 2 more

可以看到,是jdk内部XMLStreamReaderImpl类的next方法抛出的,由ShrinkProtoResourcesAction执行最终到了XMLStreamReaderImpl这个类的652行抛出的异常,到这里我们只能知道是AGP插件内部执行出现了问题,似乎跟百度得出的答案一样,通过enableNewResourceShrinker=false禁用资源压缩来解决才可以。

引出插件Debug

到了这一步,是不是会想如果我们能像自己开发一样,可以debug他们的代码,是不是就可以知道到底是哪里出了问题呢。是的,我们的AGP(com.android.tools.build)是可以debug的,也许你会很惊讶,A连这个也能调试吗。是的,你没听错,下面我将带大家从这个错误开始,通过调试插件的方式找到真正的问题所在。

第一步:依赖你需要调试的插件

*** 需要调试的Gradle插件及版本 ***:这是我们要调试的东西,如下图所示,在app模块加入如下依赖

dependencies {
    // 省略...
    implementation "com.android.tools.build:gradle:7.4.2" //(实际版本)
    // 省略...
}

你没看错,这里跟我们依赖其他第三方库一样

第二步:插入断点

在Android Studio中打开External Libraries找到刚依赖的插件,如下图所示

图片3.png

展开并找到我们需要断点的类,这里我们先断点XMLStreamReaderImpl这个最终抛出异常的类和ToolsAttributeUsageRecorder中processResourceToolsAttributes方法,如下图所示

图片4.png

在java.xml/com.sun.org.apache.xerces.internal.impl.XMLStreamReaderImpl.next打下一个断点

第三步:启动gradle任何

通过上一篇文章我们可以知道在命令后面加上-Dorg.gradle.debug=true 就可以等待调试器的进入,我们现在来执行一下。看到如下所示之后在进行下一步

 ./gradlew :app:shrinkXXXXXXReleaseRes -Dorg.gradle.debug=true --no-daemon
To honour the JVM settings for this build a single-use Daemon process will be forked. See https://docs.gradle.org/7.5/userguide/gradle_daemon.html#sec:disabling_the_daemon.

> Starting Daemon

第四步:开始调试

通过Android Studio的run选择Edit Configure,如图

图片5.png

图片6.png

点击Apply确定然后通过菜单选择小虫子也就是debug,如图

图片7.png

然后就是静静等待代码走到我们所打的断点处,如图所示

图片8.png

我们可以看到这里抛出了一个异常,异常内容正是Content is not allowed in prolog.那么我们来观察一下该方法是否能知道是哪个文件导致的错误,源码

 public int next() throws XMLStreamException {
        if (!hasNext()) {
            ... 省略
        }
        try {
            fEventType = fScanner.next();

            if (versionStr == null) {
                versionStr = getVersion();
            }

            return fEventType;
        } catch (IOException ex) {
            // else real error
            throw new XMLStreamException(ex.getMessage(), getLocation(), ex);
        } catch (XNIException ex) {
            throw new XMLStreamException(
                    ex.getMessage(),
                    getLocation(),
                    ex.getException());
        }
    } 

可以看到该方法无法知道是哪个文件,没有任何跟文件路径有关的变量,根据java特性,java的调用是栈结构的,我们来到上一层调用栈再来找一找是否有跟文件或者路径有关系,能识别出是哪个文件解析出现问题了。可以在Android Studio调试器的左边查看调用栈,依次点开查看该方法是否有跟路径有关的。如下图

图片9.png

图片10.png

发现第二个调用栈processResourceToolsAttributes方法中有path并且这里指向了一个xml文件,那么到这一步就能确定问题了,就是这个文件导致的错误,我们找到该文件看是否存在一些不符合xml规范的点。

图片11.png

通过打开该文件我们并未发现有不符合xml规范或者语法错误的地方,这时候我们再回到错误提示Content is not allowed in prolog. prolog什么意思?prolog 直接翻译的话是序言或者序幕,其实这里指戴的就是一种逻辑开头,也就是说内容的开头不符合规则。但是我们又发现开头也没空格,也没语法错误,那么这里答案其实就已经确定了,既然都看不出来,那么多半可以确定的是文件编码导致的,文件编码会导致。 我们继续跟踪堆栈,从第一个调用栈的stackTrace中看到一些蛛丝马迹,如图

图片12.png

经过对比发现com.sun.org.apache.xerces.internal.impl.XMLDocumentScannerImpl$PrologDriver中的next方法978行抛出的判断当fScannerState的状态为SCANNER_STATE_CONTENT时抛出,我们继续看什么时候会被赋值为SCANNER_STATE_CONTENT,最终发现当

switch (fScannerState) {
    case SCANNER_STATE_PROLOG: {
            fEntityScanner.skipSpaces();
            if (fEntityScanner.skipChar('<', null)) {
                setScannerState(SCANNER_STATE_START_OF_MARKUP);
            } else if (fEntityScanner.skipChar('&', NameType.REFERENCE)) {
                setScannerState(SCANNER_STATE_REFERENCE);
            } else {
                setScannerState(SCANNER_STATE_CONTENT);
            }
            break;
    }
}

状态为SCANNER_STATE_PROLOG时去判断非<开头非&包含的时候就会设置为SCANNER_STATE_CONTENT状态,我们可以在这里设置一个断点,然后重新启动一次流程,如下图

图片13.png

通过图片我们可以看到,xml开始的第一个char不是<而是\uFEFF,直到这里,我们证实了我们的猜想,确实是文件编码的文件,文件的编码是UTF-8带有BOM的,我们应该把该文件修改为UTF-8就可以了。修改之后我们在重新编译就解决这个问题了。

结束语

嗯,看起来调试第三方Gradle插件并不是一件很可怕的事情,对吧?只要你有耐心和幽默感,你就能轻松地解决这个问题。希望这篇干货版的博客能让你在调试时多一些乐趣!不要忘记与我们分享你的调试经验,或者提出你的问题,我们将尽力帮助你。

最后,请记住编程是一项有趣的工作,就像是解谜游戏一样。愿你在调试的道路上越走越远,越走越开心!不管何时遇到问题,只要积极向上,你就一定能找到解决办法。

感谢阅读,祝愿你的Android开发之路一帆风顺,笑口常开!

本公众号『专注于移动技术开发领域』,在设计可扩展、高性能和可维护的应用程序架构方面有卓越的能力,欢迎大家关注AntonioShare:

image.png