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语句,最终彻底解决了所有编译问题。

相关推荐
嘉小华几秒前
ThreadLocal 详解
android
wkj00134 分钟前
php 如何通过mysqli操作数据库?
android·数据库·php
kymjs张涛2 小时前
零一开源|前沿技术周报 #7
android·前端·ios
wuwu_q4 小时前
RK3566/RK3568 Android11 修改selinux模式
android·rk3568
_一条咸鱼_5 小时前
Android Runtime内存共享与访问控制原理剖析(71)
android·面试·android jetpack
嘉小华5 小时前
第三章:焦点分发全链路源码解析
android
嘉小华5 小时前
Android 协程全景式深度解析:第六章 高阶并发模式
android
嘉小华5 小时前
Android 协程全景式深度解析:第七章 协程调试与性能优化
android
你过来啊你5 小时前
Android开发中RxJava的使用与原理
android
你过来啊你6 小时前
Android Glide使用与底层机制详解
android