Android支持Java8新特性

Android从KitKat(Android SDK 4.4, API Level 19)开始支持Java 7,但之后过了很长一段时间,Google却一直没有给出支持Java 8的计划(估计是和Oracle的Java API版权之争的原因),期间有大神推出了Retrolambda,Jack toolchain(Google出品)等替代方案来部分支持Java 8的新特性,满足开发者的需求。不过,今年Google在Android开发者博客中宣布将弃用Jack toolchain,改为在最新的Android Studio内置支持Java 8的新特性。

这几种方案的实现原理基本都是在编译时对字节码进行一次处理,大致如下图所示:

Google desugar bytecode transformations
Google desugar bytecode transformations

Android Studio 3.0+

近年来闹的沸沸扬扬的Google和Oracle的Java API版权之争随着Google的失败终于告一段落。当时我就猜测Google很有可能不会再全面支持Java的版本更新了,果不其然,在Google I/O 2017大会上官方正式宣布未来Kotlin将成为Android的第一官方语言,进一步证实了我的猜测。

不过现阶段Kotlin并未普及,即使Android Studio也需要在当前还未正式发布的3.0版本才开始默认支持Kotlin,3.0以下的版本仍需要配合扩展插件才能支持Kotlin。故在较长一段时间内,Java仍是Android的主力开发语言。

因此,Google也在Android Studio 3.0+里面内置支持了部分Java 8的新特性:

不过,可能是为了向前兼容,当IDE检测到工程中使用了Jack,Retrolambda或DexGuard时将不会激活IDE自带的Java 8特性支持。因此,要想使用Android Studio的扩展插件方案,需要删除原有的第三方插件方案,具体细节可参见手册:Use Java 8 language features

Retrolambda

Retrolambda是目前相对较为成熟的一套第三方插件方案,能够在Java 7,6,5上支持如下特性:

引入Retrolambda

Retrolambda只是针对JDK的扩展,如果是在Android上使用,可以使用Gradle Retrolambda Plugin插件,该插件依赖于Retrolambda,可配合Gradle(依赖的Android Gradle Plugin最小版本为1.5.0,Gradle Plugin最小版本为2.5)自动构建Android工程。

Note: The minimum android gradle plugin is 1.5.0 and the minimum gradle plugin is 2.5.

需要在build.gradle中添加如下内容:

buildscript {
   dependencies {
      classpath 'me.tatarka:gradle-retrolambda:3.6.1'
   }
}

apply plugin: 'me.tatarka.retrolambda'

android {
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
}

配置Retrolambda

如需修改Retrolambda的配置项,可如下添加:

apply plugin: 'me.tatarka.retrolambda'
retrolambda {
    javaVersion JavaVersion.VERSION_1_7
    jvmArgs '-noverify'
    defaultMethods false
    incremental true
}

亦可如下添加:

apply plugin: 'me.tatarka.retrolambda'

android {
    retrolambda {
        javaVersion JavaVersion.VERSION_1_7
        jvmArgs '-noverify'
        defaultMethods false
        incremental true
    }
}

添加在这两个位置的效果是一样的。具体的配置参数说明可查看手册

Retrolambda的配置里需要重点关注的是javaVersion的配置

javaVersion Set the java version to compile to. The default is 6. Only 5, 6 or 7 are accepted.

Java 6和Java 7编译的区别

插件默认使用java 6来编译,这是有原因的。前面说到Retrolambda可在Java 5,6上支持部分Java 7特性,而Android是在Kitkat才开始支持Java 7的,因此,若APP的minSdkVersion小于19时,最好使用Java 6来编译,否则Retrolambda不会处理Java 7特性相关的代码,在老版本手机上运行时有可能会不生效(有些手机厂商会自己做一些处理,所以不是所有手机都会有问题,但也是大概率事件)而导致问题。同理,若APP的minSdkVersion大于等于19时,最好使用Java 7来编译,因为没必要再做转译处理。

我们以Objects.requireNonNull()为例来看看Java 6和Java 7编译后的代码区别。

我们在HomeActivity.java添加如下方法,然后分别使用Java 6和7来编译。

private String parseContent(@NonNull String content) {
    return Objects.requireNonNull(content) + ".suffix";
}

编译后,可参考路径app -> build -> intermediates -> transforms -> retrolambda -> ... -> HomeActivity.class找到对应java文件的字节码文件。

  • JavaVersion.VERSION_1_7可看到没有做任何处理
private String parseContent(@NonNull String content) {
    return (String)Objects.requireNonNull(content) + ".suffix";
}
  • JavaVersion.VERSION_1_6可看到做了处理:用object.getClass()的方式来模拟Objects.requireNonNull()中对空指针抛异常的处理
private String parseContent(@NonNull String content) {
    StringBuilder var10000 = new StringBuilder();
    content.getClass();
    return var10000.append((String)content).append(".suffix").toString();
}

Java 7编译配置

在配置java 7来编译时,若只配置javaVersion JavaVersion.VERSION_1_7,不配置jvmArgs '-noverify',编译时会报错:

Error:Execution failed for task ':app:transformClassesWithRetrolambdaForDebug'.
Process 'command '/Library/Java/JavaVirtualMachines/jdk1.8.0_91.jdk/Contents/Home/bin/java'' finished with non-zero exit value 1

这有可能是系统没有安装jdk 7导致的,不过只是猜测(在不指定javaVersion或只配置javaVersion JavaVersion.VERSION_1_6时,系统没有安装jdk 6也不会有问题),没有搭建环境去验证。目前加上两个配置项后即可正常编译运行,暂时也没有遇到任何问题。

配置Proguard

需要在proguard文件中添加如下内容:

-dontwarn java.lang.invoke.*
-dontwarn **$$Lambda$*

处理Lint的不兼容报错

Lint不识别Lambda表达式

老版本的Android Gradle Plugin的Lint不兼容Java 8新特性的语法,像Lambda表达式这种Java 8新特性的语法会报错(不清楚具体是从哪个版本开始支持)。可引入android-retrolambda-lombok来解决。

Lint不识别try-with-resources

如果Android工程的minSdkVersion小于19(Android从API 19开始支持Java 7),Lint则会对像try-with-resources这种Java 7新特性的语法报错。可通过配置Lint规则来解决。

配置Lint规则有多种方式,可针对某一工程全局配置,可针对某一代码单独配置,也可系统全局配置。

除非特殊场景需要针对某一代码单独配置以外,一般推荐针对某一工程全局配置,不建议系统全局配置,毕竟有可能同时开发多个项目,会影响项目间的差异性配置。

Android工程全局配置

在Android工程的SRC同级目录下新建或修改lint.xml文件,添加如下内容:

<?xml version="1.0" encoding="UTF-8"?>
<lint>
    <issue id="NewApi">
        <ignore regexp="Call requires API level 19"/>
        <ignore regexp="Try-with-resources requires API level 19"/>
        <ignore regexp="Multi-catch with these reflection exceptions requires API level 19"/>
    </issue>
</lint>

Note: 官方手册上说lint.xml放在Android工程的根目录下很容易让人误解,其实需要放在SRC同级目录下,如下所示:

Translator/

app/

src/
build.gradle
lint.xml

代码单独配置

通过注解@SuppressLint("xxx")来忽略Lint报错,具体的注解类型,可通过命令lint --list查看。

@SuppressLint("NewApi")
public final void setView(@NonNull T view) {
    mView = Objects.requireNonNull(view);
}

关闭没必要的报警

引入Retrolambda后,会有一些新特性实现方式的建议性报警,可有可无,对于有点强迫症的人,可以配置关掉这些报警,免得看着难受。

报警类型

  • Anonymous can be replaced with lambda@SuppressWarnings("Convert2Lambda")
mLoginBtn.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        startActivity(LoginActivity.class);
    }
});
  • Statement lambda can be replaced with expression lambda@SuppressWarnings("CodeBlock2Expr")
mLoginBtn.setOnClickListener(view -> {
    startActivity(LoginActivity.class);
});

全局配置方法

通过Android Studio做项目或系统全局配置会方便些。进入如下配置页面,不勾选相关报警选择即可。

  • 项目全局配置
    Preferences -> Inspections

  • 系统全局配置
    Other Settings -> Default Settings -> Inspections

代码单独配置方法

通过注解@SuppressWarnings("xxx")来忽略报警,具体的注解类型可查看Android SuppressWarnings list,这个老兄总结的比较全。

@SuppressWarnings("Convert2Lambda")
mLoginBtn.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        startActivity(LoginActivity.class);
    }
});

Lambda表达式

Lambda表达式(在Java中亦被人称为闭包或匿名函数)来源于函数式编程,用一种更为简介的语法来替代函数式接口(亦可称为SAM,Single Abstract Method,单个抽象方法类型。可简单理解为只有一个方法的接口)传统的内部类语法,解决常被人戏称的高度问题。所以像RxJava这类流式写法中,Lambda表达式能让代码显得更为简介。

<a name="lambda-expression-example"></a>

// 内部类
mLoginBtn.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        Activity activity = HomeActivity.this;
        Intent intent = new Intent(activity, LoginActivity.class);
        activity.startActivity(intent);
    }
});

// Lambda表达式
mLoginBtn.setOnClickListener(view -> {
    Intent intent = new Intent(this, LoginActivity.class);
    this.startActivity(intent);
});

Lambda表达式的用法和实现原理可以参考下面几篇文章,这里不再关注。

这里重点关注的是Lambda表达式和传统语法的差异点,毕竟这种语法糖是把双刃剑,虽然能让代码更为简介,但且降低了代码的可读性,而且用的不好有可能会导致问题。

this

内部类的this指向的是内部类对象的引用,不是指向外部类的引用。而Lambda表达式中的this是指向外部类的引用。

访问外部变量

内部类中访问的外部变量只能是final修饰的,而且编译器对这些规则要求很严格,如果没有final修饰也无法编译通过。

如果引入了Gradle Retrolambda Plugin,插件会在编译时为符合条件的外部变量自动加上final修饰符,此时访问外部变量的规则同Lambda表达式。

final String message = "test";
mLoginBtn.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        Log.d(TAG, message);
    }
});

Lambda表达式中访问的外部变量不再强制要求final修饰,只需要满足“有效只读”的变量即可(编译器会根据上下文推导),可简单理解为不能在Lambda表达式中修改变量的值。

String message = "test";
mLoginBtn.setOnClickListener(view -> Log.d(TAG, message));

像下面这种改变了外部变量值的Lambda表达式则会编译报错。

String message = "test";
mLoginBtn.setOnClickListener(view -> {
    message += "changed";
    Log.d(TAG, message);
});
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,529评论 5 475
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,015评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,409评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,385评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,387评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,466评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,880评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,528评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,727评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,528评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,602评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,302评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,873评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,890评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,132评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,777评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,310评论 2 342

推荐阅读更多精彩内容