Android 模块打包成aar遇到的空指针的问题

AGP 7.4.2 AAR打包问题及升级AGP 8.4.0解决方案

问题背景

在Androd项目开发中遇到了AGP 7.4.2版本的AAR打包问题,以及升级到AGP 8.4.0后R.id字段变化导致的编译问题。

问题1:AGP 7.4.2 AAR打包失败

问题现象

项目使用AGP 7.4.2时出现奇怪的现象:

  • 直接引入module:编译和运行完全正常
  • 打包成AAR后引入:出现D8编译器错误

错误信息:

bash 复制代码
> Task :library:mergeDebugJavaResource FAILED
Execution failed for task ':library:mergeDebugJavaResource'.
> A failure occurred while executing com.android.build.gradle.internal.tasks.MergeJavaResWorkAction
> com.android.tools.r8.CompilationFailedException: Compilation failed to complete
> com.android.tools.r8.utils.AbortException: Error: java.lang.NullPointerException
    at YourClass$1.class (Anonymous inner class)

问题分析

这是AGP 7.4.2中D8编译器的一个已知Bug,具体表现为:

  • 在AAR打包过程中,D8编译器处理匿名内部类时出现NullPointerException
  • 直接module引用时不会触发此bug,只有在AAR打包时才会出现
  • 错误指向YourClass$1.class等匿名内部类文件

尝试的解决方案

方案1:修改匿名内部类

将匿名内部类改为命名内部类:

java 复制代码
// 修改前
Timer timer = new Timer();
timer.schedule(new TimerTask() {
    @Override
    public void run() {
        // 执行任务
    }
}, delay);

// 修改后
private static class CustomTimerTask extends TimerTask {
    @Override
    public void run() {
        // 执行任务
    }
}
Timer timer = new Timer();
timer.schedule(new CustomTimerTask(), delay);

结果:失败,问题依然存在

方案2:移除Stream API

移除Java 8 Stream API的使用:

java 复制代码
// 修改前
import java.util.stream.Collectors;
List<DataItem> result = dataList.stream()
    .filter(item -> item.isValid())
    .collect(Collectors.toList());

// 修改后
List<DataItem> result = new ArrayList<>();
for (DataItem item : dataList) {
    if (item.isValid()) {
        result.add(item);
    }
}

结果:失败,问题依然存在

方案3:降级AGP版本

尝试降级到AGP 7.3.1:

gradle 复制代码
classpath 'com.android.tools.build:gradle:7.3.1'

结果:失败,问题依然存在

方案4:调整gradle配置

尝试各种gradle.properties配置:

properties 复制代码
android.enableD8.desugaring=true
android.enableR8=false
android.enableR8.fullMode=false

结果:失败,问题依然存在

最终解决方案:升级到AGP 8.4.0

经过各种尝试都无法解决问题后,最终选择升级到AGP 8.4.0:

gradle 复制代码
// 项目级 build.gradle
buildscript {
    dependencies {
        classpath 'com.android.tools.build:gradle:8.4.0'
        classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.20'
    }
}
properties 复制代码
# gradle/wrapper/gradle-wrapper.properties
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip

配套更新:

gradle 复制代码
// 模块级 build.gradle
android {
    namespace 'com.example.yourpackage'  // AGP 8.0+强制要求
    compileSdkVersion 34
    
    defaultConfig {
        targetSdkVersion 34
    }
    
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_11
        targetCompatibility JavaVersion.VERSION_11
    }
}

结果:成功解决D8编译器Bug,AAR打包正常


问题2:AGP 8.0+ R.id字段变化导致的switch语句问题

问题现象

升级到AGP 8.4.0后,出现新的编译错误:

bash 复制代码
> Task :app:compileDebugJavaWithJavac FAILED
MainActivity.java:56: 错误: 需要常量表达式
    case R.id.button1:
                     ^

问题原因

AGP 8.0+改变了R.id字段的生成方式:

AGP 7.x生成的R.java:

java 复制代码
public final class R {
    public static final class id {
        public static final int button1 = 0x7f050001;  // 编译时常量
        public static final int button2 = 0x7f050002;  // 编译时常量
    }
}

AGP 8.0+生成的R.java:

java 复制代码
public final class R {
    public static final class id {
        public static int button1;  // 运行时赋值,不是常量
        public static int button2;  // 运行时赋值,不是常量
    }
}

由于Java要求switch语句的case标签必须是编译时常量,而AGP 8.0+的R.id字段不再是编译时常量,因此switch语句无法编译通过。

解决方案:switch改为if-else

需要将所有使用R.id的switch语句改为if-else结构:

修改前:

java 复制代码
@Override
public void onClick(View v) {
    switch (v.getId()) {
        case R.id.button1:
            doAction1();
            break;
        case R.id.button2:
            doAction2();
            break;
    }
}

修改后:

java 复制代码
@Override
public void onClick(View v) {
    int id = v.getId();
    if (id == R.id.button1) {
        doAction1();
    } else if (id == R.id.button2) {
        doAction2();
    }
}

复杂switch语句的处理

对于复杂的switch语句,如DetailActivity.java

修改前:

java 复制代码
switch (v.getId()) {
    case R.id.btn_back:
        finish();
        break;
    case R.id.btn_play:
        if (isPlaying) {
            mediaPlayer.pause();
        } else {
            startPlayback();
        }
        break;
    case R.id.btn_settings:
        // 处理设置相关逻辑
        break;
    // ... 更多case
}

修改后:

java 复制代码
int id = v.getId();
if (id == R.id.btn_back) {
    finish();
} else if (id == R.id.btn_play) {
    if (isPlaying) {
        mediaPlayer.pause();
    } else {
        startPlayback();
    }
} else if (id == R.id.btn_settings) {
    // 处理设置相关逻辑
}
// ... 更多else if

注意:内部使用常量的switch语句可以保留,如:

java 复制代码
// 这种switch可以保留,因为使用的是常量
switch (dataItem.getType()) {
    case Constants.TYPE_HEADER:
        // 处理逻辑
        break;
    case Constants.TYPE_CONTENT:
        // 处理逻辑
        break;
}

总结

问题解决路径

  1. AGP 7.4.2 AAR打包问题

    • 现象:module正常,AAR打包失败
    • 原因:D8编译器Bug
    • 解决:升级到AGP 8.4.0
  2. AGP 8.0+ R.id问题

    • 现象:switch语句编译失败
    • 原因:R.id不再是编译时常量
    • 解决:switch改为if-else

关键配置

最终的项目配置:

gradle 复制代码
// build.gradle
classpath 'com.android.tools.build:gradle:8.4.0'

// gradle-wrapper.properties  
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip

// 模块build.gradle
android {
    namespace 'com.example.yourpackage'
    compileSdkVersion 34
    targetSdkVersion 34
    
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_11
        targetCompatibility JavaVersion.VERSION_11
    }
}

通过升级AGP版本和修改switch语句,最终彻底解决了所有编译问题。

相关推荐
Gracker7 小时前
Android Weekly #202520
android
weixin_411191848 小时前
原生安卓与flutter混编的实现
android·flutter
呼啸长风8 小时前
记一次未成功的 MMKV Pull Request
android·ios·开源
小墙程序员9 小时前
Android 性能优化(六)使用 Callstacks Sample 和 Java/Kotlin Method Recording 分析方法的耗时
android·性能优化·android studio
hcgeng12 小时前
android中相近方法对比
android·方法比对
这儿有一堆花12 小时前
eSIM技术深度解析:从物理芯片到数字革命
android·ios
雨白15 小时前
开发 SunnyWeather:Android 天气预报 App(下)
android
_extraordinary_16 小时前
Java 字符串常量池 +反射,枚举和lambda表达式
android·java·开发语言
alexhilton16 小时前
学会说不!让你彻底学会Kotlin Flow的取消机制
android·kotlin·android jetpack