Blog


  • 首页

  • 关于

  • 标签

  • 分类

  • 归档

  • 搜索

Android App Bundles分析

发表于 2021-01-19 | 分类于 Android插件化

Android App Bundles(简称AAB)是一款全新动态化框架,与Instant App不同,AAB是借助Split Apk完成动态加载。

AAB与Instant Apps有何不同:Instant Apps是应用程序未下载,用户通过链接即可体验其部分功能,Instant Apps应用程序是运行在google play service上,而AAB插件是运行在应用程序进程内。AAB强调的是减少app包体积同时提供一样的用户功能体验,提供按需下载安装模式。

好处:

  • Size更小【个人理解是相对用户来感知来说更小】
  • 安装更快【base.apk被优化相对来说安装会更快】
  • 支持动态发布

限制

  • 仅限于通过 Google Play 发布的应用,(Google进一步巩固自身生态)
  • 需要加入到 Google 的 beta program
  • 最低支持版本Android 5.0 (API level 21),低于Android 5.0 (API level 21) 的版本GooglePlay会优化Size,但不支持动态交付。

原理

结合Google Play Dynamic Delivery (动态交付) , 实现动态功能。Android App Bundle 支持模块化,通过Dynamic Delivery with split APKs,将一个apk拆分成多个apk,按需加载(包括加载C/C++ libraries),这样开发者可以随时按需交付功能,而不是仅限在安装过程中。

  • Base Apk 首次安装的apk,公共代码和资源,所以其他的模块都基于Base Apk
  • Configuration APKs native libraries 和适配当前手机屏幕分辨率的资源
  • Dynamic feature APKs 不需要在首次安装就加载的模块

下面介绍下SplitApk。

Split Apks

split apks是Android 5.0开始提供多apk构建机制,借助split apks可以将一个apk基于ABI和屏幕密度两个维度拆分城多个apk,这样可以有效减少apk体积。当用户下载应用程序安装包时,只会包含对应平台的so和资源。因为需要google play支持,所以国内就没戏了。针对不同cpu架构问题,国内应用开发商大部分都会将so文件只放在armabi目录下,如此做虽然可以有效减少包体积,但可能带来性能问题。

安装应用程序时,首先安装base apk,然后安装split apks。为了解splite apks运作原理,我们还是结合源码做简要分析。因为splite apks是Android 5.0开始支持,所以我们以5.0版本开始分析。

在ApplicationInfo中,增加splites apk相关字段。

1
2
3
4
5
6
7
8
9
10
11
12
/**
* Full paths to zero or more split APKs that, when combined with the base
* APK defined in {@link #sourceDir}, form a complete application.
*/
public String[] splitSourceDirs;

/**
* Full path to the publicly available parts of {@link #splitSourceDirs},
* including resources and manifest. This may be different from
* {@link #splitSourceDirs} if an application is forward locked.
*/
public String[] splitPublicSourceDirs;

LoadeApk中有PathClassLoader和Resources创建过程。LoadedApk#mClassLoader是PathClassLoader实例引用,接着分析PathClassLoader创建过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public ClassLoader getClassLoader() {
synchronized (this) {
if (mClassLoader != null) {
return mClassLoader;
}

if (mIncludeCode && !mPackageName.equals("android")) {

......

final ArrayList<String> zipPaths = new ArrayList<>();
final ArrayList<String> libPaths = new ArrayList<>();

.......

zipPaths.add(mAppDir);
//将split apk路径追加到zipPaths中
if (mSplitAppDirs != null) {
Collections.addAll(zipPaths, mSplitAppDirs);
}

libPaths.add(mLibDir);

......

final String zip = TextUtils.join(File.pathSeparator, zipPaths);
final String lib = TextUtils.join(File.pathSeparator, libPaths);

......
//如果mSplitAppDirs不为空,则zip将包含split apps所有路径。
mClassLoader = ApplicationLoaders.getDefault().getClassLoader(zip, lib,
mBaseClassLoader);

StrictMode.setThreadPolicy(oldPolicy);
} else {
if (mBaseClassLoader == null) {
mClassLoader = ClassLoader.getSystemClassLoader();
} else {
mClassLoader = mBaseClassLoader;
}
}
return mClassLoader;
}
}

在创建PathClassLoader时,dex文件路径包含base app和split apps路径。LoadedApk#mResources是Resources实例引用,其创建过程如下。

1
2
3
4
5
6
7
public Resources getResources(ActivityThread mainThread) {
if (mResources == null) {
mResources = mainThread.getTopLevelResources(mResDir, mSplitResDirs, mOverlayDirs,
mApplicationInfo.sharedLibraryFiles, Display.DEFAULT_DISPLAY, null, this);
}
return mResources;
}

该方法中,split apks资源路径(LoadedApk#mSplitResDirs)也会被增加至Resources中。

以上简要介绍split apks加载过程,包括code和resources加载。split apks并不支持动态加载split apk,即base apk 和split apks在app安装时,全部安装。但通过split apks工作原理,可以发现其是能够支持按需加载。

Play Core Library

Play Core Library是AAB提供的核心库,用于下载、安装dynamic feature模块。

主工程模块app,首先分析MainActivity.kt文件。在MainActivity.kt的onCreate方法中,增加如下逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
manager = SplitInstallManagerFactory.create(this)
initializeViews()
val installedList = manager.installedModules.toList()
for (item in installedList) {
toastAndLog("installed module : " + item.toString())
}
val splits = applicationInfo.splitSourceDirs
for ( item in splits) {
toastAndLog("split dir : " + item.toString())
}
}

打印结果如下:

1
2
3
4
5
6
7
8
D/DynamicFeatures: installed module : native
D/DynamicFeatures: installed module : java
D/DynamicFeatures: installed module : kotlin
D/DynamicFeatures: installed module : assets
D/DynamicFeatures: split dir : /data/app/com.google.android.samples.dynamicapps.ondemand-1/split_assets.apk
D/DynamicFeatures: split dir : /data/app/com.google.android.samples.dynamicapps.ondemand-1/split_java.apk
D/DynamicFeatures: split dir : /data/app/com.google.android.samples.dynamicapps.ondemand-1/split_kotlin.apk
D/DynamicFeatures: split dir : /data/app/com.google.android.samples.dynamicapps.ondemand-1/split_native.apk

从运行结果可知,split apks(即使是on-demand模块)在debug模式下,是紧接着base apk安装完成后安装。

SplitInstallManager类提供获取已安装模块方法。

1
Set<String> getInstalledModules();

因为Play Core Library非对外暴露接口都是混淆过的,因此就不直接附源码分析。但通过追踪分析源码可知,获取已安装模块的核心过程是:

1
2
3
4
5
6
7
8
9
private final String[] a() {
try {
PackageInfo var1;
return (var1 = this.d.getPackageManager().getPackageInfo(this.e, 0)) != null ? var1.splitNames : null;
} catch (NameNotFoundException var2) {
a.c("App is not found in PackageManager", new Object[0]);
return null;
}
}

通过PackageInfo#splitNames字段获取。

在示例中,每当我们需要启动dynamic feature模块时,都要判断该模块是否安装。如果没有安装,则启动下载,Play Core Library提供了比较完善的下载状态回调,比如下载进度,下载失败原因等等。

通过粗略分析这些混淆源码可知,下载与安装on-demand模块均是通过ipc交由google play完成。

兼容性问题

OS版本不高于6.0

当app运行设备版本不高于6.0时,需要使用SplitCompat库才能立即访问下载模块代码和资源。AAB提供SplitCompatApplication类用于开启SplitCompat。

1
2
3
4
5
6
7
8
9
public class SplitCompatApplication extends Application {
public SplitCompatApplication() {
}

protected void attachBaseContext(Context var1) {
super.attachBaseContext(var1);
SplitCompat.install(this);
}
}

在Application#attachBaseContext(Context)中调用SplitCompat.install(Context)。在该方法中主要完成split apks代码(dex和so)和资源的安装。

因为代码都是混淆过的,因此只能大概知道SplitCompat做了哪些操作。在SplitCompat#a(boolean)方法调用了
com.google.android.play.core.splitcompat.b.b.a()方法,其中有对不同版本OS兼容性处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static a a() {
if (VERSION.SDK_INT == 21) {
//com.google.android.play.core.splitcompat.b.c
return new c();
} else if (VERSION.SDK_INT == 22) {
//com.google.android.play.core.splitcompat.b.f
return new f();
} else if (VERSION.SDK_INT == 23) {
//com.google.android.play.core.splitcompat.b.g
return new g();
} else {
throw new AssertionError();
}
}

分别查看com.google.android.play.core.splitcompat.b.c、com.google.android.play.core.splitcompat.b.f、com.google.android.play.core.splitcompat.b.g,得知其主要做so加载和dex加载(dex前插,与mutil-dex类似)。split apks资源加载在SplitCompat#a(boolean)方法有反射调用AssetManager#addAssetPath(String)。

OS版本不低于8.0

在Android 8.0中,Instant Apps相关代码嵌入至Framework。因此如果on-demand模块用于Instant Apps中,需要在on-demand下载成功中,调用SplitInstallHelper.updateAppInfo(Context)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void updateAppInfo(Context var0) {
if (VERSION.SDK_INT > 25) {
a.a("Calling dispatchPackageBroadcast!", new Object[0]);

try {
Class var1;
Method var2;
(var2 = (var1 = Class.forName("android.app.ActivityThread")).getMethod("currentActivityThread")).setAccessible(true);
Object var3 = var2.invoke((Object)null);
Field var4;
(var4 = var1.getDeclaredField("mAppThread")).setAccessible(true);
Object var5;
(var5 = var4.get(var3)).getClass().getMethod("dispatchPackageBroadcast", Integer.TYPE, String[].class).invoke(var5, 3, new String[]{var0.getPackageName()});
a.a("Calling dispatchPackageBroadcast", new Object[0]);
} catch (Exception var6) {
a.a(var6, "Update app info with dispatchPackageBroadcast failed!", new Object[0]);
}
}
}

从上述代码得知其反射调用ActivityThread#dispatchPackageBroadcast方法。最终是调用至LoadedApk#updateApplicationInfo。该方法做了如下事情

  • 重新创建mClassLoader
  • 重新创建mResources
  • 更新applicationInfo(调用LoadedApk#setApplicationInfo完成)。

参考资料

https://zhuanlan.zhihu.com/p/36902641
https://www.jianshu.com/p/57cccc680bb6

AndroidX 迁移

发表于 2020-12-01 | 分类于 Android知识点

AndroidX介绍

按照官方文档说明 AndroidX 是对 android.support.xxx 包的整理后产物。由于之前的 support 包过于混乱,所以,Google 推出了AndroidX。

由于在后续版本中,会逐步放弃对 support 的升级和维护,所以,我们必须迁移到 AndroidX。

注意:我们建议在单独的分支中执行迁移。此外,还应设法避免在执行迁移时重构代码。

编译环境

AndroidStudio3.2及以上

targetSdkVersion版本为28及以上

gradle要3.2.0及以上

迁移步骤

1. 修改gradle.properties

1
2
android.useAndroidX=true
android.enableJetifier=true

android.useAndroidX=true 表示启用 androidx

android.enableJetifier=true 表示将依赖包也迁移到androidx 。如果取值为false,表示不迁移依赖包到androidx,但在使用依赖包中的内容时可能会出现问题,如果项目中没有使用任何三方依赖,可以设置为false。

2. 更新依赖

将app依赖的三方库尽量都升级到支持androidx的版本,这样可以避免在迁移中发生冲突。

3. 在AS中执行Refactor->Migrate to AndroidX

在执行该操作时会提醒我们是否将当前项目打包备份。如果你提前已经做好了备份,可以忽略;如果没有备份,则先备份。

当然一般情况,这一步执行完以后还有很多support库的引用没有更换为androidx。

Activity/Fragment/XML(包括涉及到使用support包的工具类等),原来引用support包中的类,在Migrate后并不能完全对应,会有很多错误(方便还是有代价的,通往幸福的路总是充满坎坷),所以需要改成对应的androidX中的类引用。

注:可以手动迁移,除了麻烦点并没有太大风险。

4. 混淆更改

1
2
3
4
5
6
7
8
9
10
11
12
# androidx
-keep class com.google.android.material.** {*;}
-keep class androidx.** {*;}
-keep public class * extends androidx.**
-keep interface androidx.** {*;}
-keep @androidx.annotation.Keep class *
-keepclassmembers class * {
@androidx.annotation.Keep *;
}
-dontwarn com.google.android.material.**
-dontnote com.google.android.material.**
-dontwarn androidx.**

5. 统一版本号

一般来说弄完上面一步就可以解决问题了。但是也有不一般的情况,就是androidX的版本不一致导致的问题 大概报错提示可以看到androidx.core:core:1.1.0 ... androidx.core:core:1.0.0 之类的,就说明是版本不一致了

这时就要固定版本,让插件的版本也保持一致:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// androidx version: 固定版本
resolutionStrategy {
resolutionStrategy.eachDependency { details ->
if (details.requested.group == 'androidx.core') {
details.useVersion "1.3.1"
}
if (details.requested.group == 'androidx.lifecycle') {
details.useVersion "2.0.0"
}
if (details.requested.group == 'androidx.versionedparcelable') {
details.useVersion "1.1.0"
}
if (details.requested.group == 'androidx.fragment') {
details.useVersion "1.1.0"
}
if (details.requested.group == 'androidx.appcompat') {
details.useVersion "1.2.0"
}
}
}

版本库统一管理

由于升级插件的原因,原来的使用gradle全局管理依赖会直接被替换,比如以前引用是这样的

1
implementation rootProject.ext.dependencies.libSupportAppcompatV7

被替换之后就是这样了

1
implementation "androidx.appcompat:appcompat:1.0.0"

为了我们方便管理我们需要手动更改一下,然后根据我们的需要在对应的引入文件替换。

1
2
3
4
5
6
7
8
9
10
11
12
//androidx
libXAnnotation : "androidx.annotation:annotation:${ANDROIDX_LIB_VERSION}",
libXAppcompat : "androidx.appcompat:appcompat:${ANDROIDX_LIB_VERSION}",
libXRecyclerview : "androidx.recyclerview:recyclerview:${ANDROIDX_LIB_VERSION}",
libXCoreKtx : "androidx.core:core-ktx:$ANDROIDX_LIB_VERSION",
libXFragmentKtx : "androidx.fragment:fragment-ktx:$ANDROIDX_LIB_VERSION",
libXSwiperefreshlayout : "androidx.swiperefreshlayout:swiperefreshlayout:$ANDROIDX_LIB_VERSION",
libXMaterial : "com.google.android.material:material:${ANDROIDX_LIB_VERSION}",//替代com.android.support:design

libXMultidex : 'androidx.multidex:multidex:2.0.0',
libConstraintLayout : 'androidx.constraintlayout:constraintlayout:1.1.3',//暂时有需要用到
libXCardView : 'androidx.cardview:cardview:1.0.0',//替代com.android.support:cardview-v7

具体查看官方组建映射对照表,更换之后编译解决报错即可,主要是一些三方依赖造成依赖冲突的问题,可以类似第一步强制指定版本号。

修改未自动迁移的三方库

虽然我们从 gradle 中配置了迁移三方库的参数,但是,由于三方库的版本更新问题,也可能会迁移失败。在三方库迁移失败时,如果使用了数据绑定,通常会报如下错误:

碰到上述错误之后,我们可以按下列步骤处理:

1、在 gradle 文件中,将可升级的三方库升级(通常情况下,可升级的三方库会有黄色提示)

2、如果 gradle 中可升级的库都升级之后依旧报上述错误,那么,可以新建一个项目,然后将 gradle 中的依赖库逐个拷贝到新项目中,每拷贝一个编译一次,这样可以确认是哪个三方库有问题。(实际操作时可以使用二分法的方式进行,每次拷贝一半的依赖库,然后编译)。然后就可以有针对性的处理了

解决AndroidX与第三方库依赖冲突

如果你的项目引用了AndroidX 但是现在起GitHub上很多的第三方库,引用的还是Android support依赖。这样就会产生类似于Error: Program type already present: androidx.legacy.app.ActionBarDrawerToggle$Delegate的错误提示。这个表示AndroidX 与 Android support冲突。

解决方案:

implementation(‘第三方库的依赖’) { exclude group: ‘com.android.support’ }

其它坑点

1. 注解处理器冲突

运行报错发现是因为项目中有用到butterknife,使用的版本没有androidX支持,更新到最新的10.1.0。(Glide、Dagger之类的也会出现)

2. androidX严格检查引起的问题

  • AndroidManifest.XML文件报错(注释检查)

  • 修复 DataBinding 中的错误(重名id错误)

  • 去除 attr.xml 中重复的属性名称(自定义控件中属性与系统已有属性重名)

  • 包名不规范(包名以小写或者“_”的形式,不要用大写等)

3. 莫名问题的解决

迁移过程中如果爆出一些 android 包本身或者其他莫名其妙的问题时,先去 xml 布局文件 或 .java 文件中找一下,是否有继续引用 xxx.support.xxx 的情况,如果有,记得替换成 androidx.xxx.xxx 包下的对应控件。( xxx 泛指任意内容)

参考资料

1、官方迁移指南

2、AndroidX终极迁移指南

3、androidx升级

4、你好,androidX!

5、是时候迁移至 AndroidX 了

6、androidX库版本

Flutter动态化方案

发表于 2020-10-13 | 分类于 Hybrid Develop

Flutter 跨端技术一经推出便在业内赢得了不错的口碑,它在“多端一致”和“渲染性能”上的优势让其他跨端方案很难比拟。虽然 Flutter 的成长曲线和未来前景看起来都很好,但不可否认的是,目前 Flutter 仍处在发展阶段,很多大型互联网企业都无法毫无顾虑地让全线 App 接入,而其中最主要的顾虑是包大小与动态化。

动态化代表着更短的需求上线路径,代表着大大压缩了原始包的大小,从而获得更高的用户下载意向,也代表着更健全的线上质量维护体系。当明白这些意义后,我们也就不难理解,在 Flutter 的应用与适配趋近完善时,动态化自然就成为了一个无法避开的话题。RN 和 Weex 等成熟技术甚至让大家认为动态化是跨端技术的标配。

产物替换

Flutter的动态化,对于Android而言,一个很清晰的思路就是动态替换flutter_assets的所有资源文件,因为Flutter加载代码和资源的工作目录即是应用沙盒目录下的app_flutter目录,我们把这个目录下的文件进行对应替换即可,而对于IOS,由于本身系统的限制,官方目前也没相应方案。

Flutter 在 Release 模式下构建的是 AOT 编译产物,iOS 是 AOT Assembly,Android 默认 AOTBlob。同时 Flutter 也支持 JIT Release模式,可以动态加载 Kernel snapshot 或 App-JIT snapshot。如果在 AOT 上支持 JIT,就可以实现动态化能力。但问题在于,AOT 依赖的 Dart VM 和 JIT 并不一样,AOT 需要一个编译后的 “Dart VM”(更准确地说是 Precompiled Runtime),JIT 依赖的是 Dart VM(一个虚拟机,提供语言执行环境);并且 JIT Release 并不支持 iOS 设备,构建的应用也不能在 AppStore 上发布。

具体可参考:Android平台上的Dynamic Patch
https://juejin.im/post/6844903984113647623#heading-2

类似React Native 框架

我们先来看看React Native 的架构:

React Native 要转为android(ios) 的原生组件,再进行渲染。用React Native的设计思路,把XML DSL转为Flutter 的原子widget组件,让Flutter 来渲染。技术上说是可行的。

基于JS的高性能Flutter动态化框架:

MXFlutter (Matrix Flutter)核心思路是把 Flutter 的渲染逻辑中的三棵树中的第一棵,放到 JavaScript 中生成。用 JavaScript 完整实现了 Flutter 控件层封装,可以使用 JavaScript,用极其类似 Dart 的开发方式,开发Flutter应用,利用JavaScript版的轻量级Flutter Runtime,生成UI描述,传递给Dart层的UI引擎,UI引擎把UI描述生产真正的 Flutter 控件。一句话介绍MXFlutter,就是用JavaScript,以Flutter的写法开发Flutter。

这套方案通过JSCore代替DartVM,使用JS来写Widget,从而实现了动态化。但至于MXFlutter所宣传单“高性能”,我觉得还是有待商榷,比起原生的Flutter,JS与Native、Dart间的通信增加了不小的额外开销。同时由于J2V8库的引入,对于包体积也会有不小的增加,但这的确是一条可行的动态化思路。

具体可参考:https://github.com/mxflutter/mxflutter

页面动态组件框架

由粗粒度的Widget组件动态拼装出页面(此处DSL 的基本原理就是对AST抽象语法树内数据的一个描述,并附带一些其他操作)。Native端已经有很多成熟的框架,如天猫的Tangram,淘宝的DinamicX,它在性能、动态性,开发周期上取得较好平衡。关键它能满足大部分的动态性需求,能解决问题。

在Flutter上使用粗力度的组件动态拼装来构建页面,需要一整套的前后端服务和工具。

语法树的选择

Native端的Tangram ,DinamicX等框架他们有个共同点,都是Xml或者Html 做为DSL。但是Flutter 是React Style语法。他自己的语法已经能很好的表达页面。无需要自定义的Xml 语法,自定义的逻辑表达式。用Flutter 源码做为DSL 能大大减轻开发,测试过程,不需要额外的工具支持。所以选择了Flutter 源码作为DSL,来实现动态化。

如何解析DSL

Flutter源码做为DSL,那我们需要对源码进行很好的解析和分析。Flutter analyzer给了我们一些思路,Flutter analyzer是一个代码风格检测工具。它使用package:analyzer来解析dart 源码,拿到ASTNode。

看下Flutter analyze 源码结构,它使用了dart sdk 里面的 package:analyzer

1
2
3
4
5
6
7
8
9
dart-sdk:  
analysis_server:
analysis_server.dart
handleRequest(Request request)

analyzer:
parseCompilationUnit()
parseDartFile
parseDirectives

Flutter analyze 解析源码得到ASTNode过程。

插件或者命令对analysis server发起请求,请求中带需要分析的文件path,和分析的类型,analysis_server经过使用 package:analyzer 获取 commilationUnit (ASTNode),再对astNode,经过computer分析,返回一个分析结果list。

同样我们也可以把使用 package:analyzer 把源文件转换为commilationUnit (ASTNode),ASTNode是一个抽象语法树,抽象语法树(abstract syntax tree或者缩写为AST)是源代码的抽象语法结构的树状表现形式.

所有利用抽象语法树能很好的解析dart 源码。

解析渲染引擎

下面重点介绍渲染模块

架构图:

1.源码解析过程

1.AST树的结构

如下面这段Flutter组件源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import 'package:flutter/material.dart';

class FollowedTopicCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new Container(
padding: const EdgeInsets.fromLTRB(12.0, 8.0, 12.0, 0.0),
child: new InkWell(
child: new Center(
child: const Text('Plugin example app'),
),
onTap: () {},
),
);
}
}

它的AST结构:

从AST结构看,他是有规律的.

2.AST 到widget Node

我们拿到了ASTNode,但ASTNode 和widget node tree 完全是两个不一样的概念,
需要递归ASTNode 转化为 widget node tree.

widget Node 需要的元素

用Name 来记录是什么类型的widget

widget的arguments放在 map里面

widget 的literals 放在list 里面

widget 的children 放在lsit 里面

widget 的触发事件 函数map里面

widget node 加fromjson ,tojson 方法

可以在递归astNode tree 时候,识别InstanceCreationExpression来创建一个widget node。

2.组件数据渲染

框架sdk 中注册支持的组件,组件包括:

a.原子组件:Flutter sdk 中的 Flutter 的widget

b.本地组件:本地写好到一个大颗粒的组件,卡片widget组件

c.逻辑组件:本地包装了逻辑的widget组件

d.动态组件:通过源码dsl动态渲染的widget

具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 const Map<String, CreateDynamicApi> allWidget = <String,  
CreateDynamicApi>{
'Container': wrapContainer,
………….
}
static Widget wrapContainer(Map<String, dynamic> pars) {
return new Container(
padding: pars['padding'],
color: pars['color'],
child: pars['child'],
decoration: pars['decoration'],
width: pars['width'],
height: pars['height'],
alignment: pars['alignment']
);
}

一般我们通过网络请求拿到的数据是一个map。
比如源码中写了这么一个 ‘${data.urls[1]}’
AST 解析时候,拿到这么一个string,或者AST 表达式,通过解析它 ,肯定能从map 中拿到对应的值。

3.逻辑和事件

a.支持逻辑

Flutter 概念万物都是widget ,可以把表达式,逻辑封装成一个自定义widget。如果在源码里面写了if else,变量等,会加重sdk解析的过程。所以把逻辑封装到widget中。这些逻辑widget,当作组件当成框架组件。

b.支持事件

把页面跳转,弹框,等服务,注册在sdk里面。约定使用者仅限sdk 的服务。

4.规则和检测工具

a.检测规则

需要对源码的格式制定规则。比如不支持 直接写if else ,需要使用逻辑wiget组件来代替if else 语句。如果不制定规则,那ast Node 到widget node 的解析过程会很复杂。理论上都可以解析,只要解析sdk 够强大。制定规则,可以减轻sdk的解析逻辑。

b.工具检测

用工具来检测源码是否符合制定的规则,以保证所有的源码都能解析出来。

性能和效果

帧率大于50fps,体验上看比weex相同功能的页面更加流畅,Samsung galaxy s8上,感觉不出组件是通过动态渲染的.

数据结构

服务端请求到的数据,我们可以约定一种格式如下:

1
2
3
4
5
6
7
class DataModel {

Map<dynamic, dynamic> data;

String type;

}

每个page 都是由组件组成的,每个组件的数据都是 DataModel来渲染。
根据type 来找到对应的模版,模版+data,渲染出界面。

动态模版管理模块

我们把Widget Node Tree 转换为一个组件Json模版,它需要一套管理平台,来支持版本控制,动态下载,升级,回滚,更新等。

框架的边界

该框架是通过组件的组装,组件布局动态变更,页面布局动态变更来实现动态化。所以它适合运营变化较快的首页,详情,订单,我的等页面。一些复杂的逻辑需要封装在组件里面,把组件内置到框架中,当作本地组件。框架侧重于动态组件的组装,而引擎对于源码复杂的逻辑表达式的解析是弱化的。

参考资料

Android平台上的Dynamic Patch
https://juejin.im/post/6844903984113647623#heading-2
MXFlutter
https://github.com/mxflutter/mxflutter
页面动态组件
美团方案

Kotlin Coroutines Flow

发表于 2020-09-07 | 分类于 Android知识点

Flow 有点类似 RxJava 的 Observable。因为 Observable 也有 Cold 、Hot 之分。

使用

Flow 能够返回多个异步计算的值,例如下面的 flow builder :

1
2
3
4
5
6
7
8
flow {
for (i in 1..5) {
delay(100)
emit(i)
}
}.collect{
println(it)
}

其中 Flow 接口,只有一个 collect 函数

1
2
3
4
public interface Flow<out T> {
@InternalCoroutinesApi
public suspend fun collect(collector: FlowCollector<T>)
}

如果熟悉 RxJava 的话,则可以理解为 collect() 对应subscribe(),而 emit() 对应onNext()。

创建

除了刚刚展示的 flow builder 可以用于创建 flow,还有其他的几种方式:

flowOf()

1
2
3
4
5
6
7
flowOf(1,2,3,4,5)
.onEach {
delay(100)
}
.collect{
println(it)
}

asFlow()

1
2
3
4
5
6
listOf(1, 2, 3, 4, 5).asFlow()
.onEach {
delay(100)
}.collect {
println(it)
}

channelFlow()

1
2
3
4
5
6
7
8
channelFlow {
for (i in 1..5) {
delay(100)
send(i)
}
}.collect{
println(it)
}

最后的 channelFlow builder 跟 flow builder 是有一定差异的。

flow 是 Cold Stream。在没有切换线程的情况下,生产者和消费者是同步非阻塞的。
channel 是 Hot Stream。而 channelFlow 实现了生产者和消费者异步非阻塞模型。

看下面的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
//使用 flow builder 的情况,大致花费1秒
fun main() = runBlocking {

val time = measureTimeMillis {
flow {
for (i in 1..5) {
delay(100)
emit(i)
}
}.collect{
delay(100)
println(it)
}
}

print("cost $time")
}

//使用 channelFlow builder 的情况,大致花费700毫秒:
fun main() = runBlocking {

val time = measureTimeMillis{
channelFlow {
for (i in 1..5) {
delay(100)
send(i)
}
}.collect{
delay(100)
println(it)
}
}

print("cost $time")
}

//当然,flow 如果切换线程的话,花费的时间也是大致700毫秒,跟使用 channelFlow builder 效果差不多。
fun main() = runBlocking {

val time = measureTimeMillis{
flow {
for (i in 1..5) {
delay(100)
emit(i)
}
}.flowOn(Dispatchers.IO)
.collect {
delay(100)
println(it)
}
}

print("cost $time")
}

切换线程

相比于 RxJava 需要使用 observeOn、subscribeOn 来切换线程,flow 会更加简单。只需使用 flowOn,下面的例子中,展示了 flow builder 和 map 操作符都会受到 flowOn 的影响。

1
2
3
4
5
6
7
8
9
10
11
flow {
for (i in 1..5) {
delay(100)
emit(i)
}
}.map {
it * it
}.flowOn(Dispatchers.IO)
.collect {
println(it)
}

而 collect() 指定哪个线程,则需要看整个 flow 处于哪个 CoroutineScope 下。

值得注意的地方,不要使用 withContext() 来切换 flow 的线程。

flow 取消

如果 flow 是在一个挂起函数内被挂起了,那么 flow 是可以被取消的,否则不能取消。

Terminal flow operators

Flow 的 API 有点类似于 Java Stream 的 API。它也同样拥有 Intermediate Operations、Terminal Operations。

Flow 的 Terminal 运算符可以是 suspend 函数,如 collect、single、reduce、toList 等;也可以是 launchIn 运算符,用于在指定 CoroutineScope 内使用 flow。

1
2
3
4
@ExperimentalCoroutinesApi // tentatively stable in 1.3.0
public fun <T> Flow<T>.launchIn(scope: CoroutineScope): Job = scope.launch {
collect() // tail-call
}

整理一下 Flow 的 Terminal 运算符

  • collect
  • single/first
  • toList/toSet/toCollection
  • count
  • fold/reduce
  • launchIn/produceIn/broadcastIn

Flow VS Sequences

每一个 Flow 其内部是按照顺序执行的,这一点跟 Sequences 很类似。

Flow 跟 Sequences 之间的区别是 Flow 不会阻塞主线程的运行,而 Sequences 会阻塞主线程的运行。

Flow VS RxJava

Kotlin 协程库的设计本身也参考了 RxJava ,下图展示了如何从 RxJava 迁移到 Kotlin 协程。(火和冰形象地表示了 Hot、Cold Stream)

Cold Stream

flow 的代码块只有调用 collect() 才开始运行,正如 RxJava 创建的 Observables 只有调用 subscribe() 才开始运行一样。

Hot Stream

如图上所示,可以借助 Kotlin Channel 来实现 Hot Stream。

Completion

Flow 完成时(正常或出现异常时),如果需要执行一个操作,它可以通过两种方式完成:imperative、declarative。

1.imperative

通过使用 try … finally 实现

1
2
3
4
5
6
7
8
9
10
11
12
fun main() = runBlocking {
try {
flow {
for (i in 1..5) {
delay(100)
emit(i)
}
}.collect { println(it) }
} finally {
println("Done")
}
}

2.declarative

通过 onCompletion() 函数实现

1
2
3
4
5
6
7
8
9
fun main() = runBlocking {
flow {
for (i in 1..5) {
delay(100)
emit(i)
}
}.onCompletion { println("Done") }
.collect { println(it) }
}

3.onCompleted (借助扩展函数实现)

借助扩展函数可以实现类似 RxJava 的 onCompleted() 功能,只有在正常结束时才会被调用:

1
2
3
4
5
6
fun <T> Flow<T>.onCompleted(action: () -> Unit) = flow {

collect { value -> emit(value) }

action()
}

它的使用类似于 onCompletion()

1
2
3
4
5
6
7
8
9
fun main() = runBlocking {
flow {
for (i in 1..5) {
delay(100)
emit(i)
}
}.onCompleted { println("Completed...") }
.collect{println(it)}
}

但是假如 Flow 异常结束时,是不会执行 onCompleted() 函数的。

Backpressure

Backpressure 是响应式编程的功能之一。RxJava2 Flowable 支持的 Backpressure 策略,包括:

  • MISSING:创建的 Flowable 没有指定背压策略,不会对通过 OnNext 发射的数据做缓存或丢弃处理。
  • ERROR:如果放入 Flowable 的异步缓存池中的数据超限了,则会抛出 MissingBackpressureException 异常。
  • BUFFER:Flowable 的异步缓存池同 Observable 的一样,没有固定大小,可以无限制添加数据,不会抛出 MissingBackpressureException 异常,但会导致 OOM。
  • DROP:如果 Flowable 的异步缓存池满了,会丢掉将要放入缓存池中的数据。
  • LATEST:如果缓存池满了,会丢掉将要放入缓存池中的数据。这一点跟 DROP 策略一样,不同的是,不管缓存池的状态如何,LATEST 策略会将最后一条数据强行放入缓存池中。

而 Flow 的 Backpressure 是通过 suspend 函数实现。

buffer() 对应 BUFFER 策略,conflate() 对应 LATEST 策略。示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
fun currTime() = System.currentTimeMillis()

var start: Long = 0

fun main() = runBlocking {

val time = measureTimeMillis {
(1..5)
.asFlow()
.onStart { start = currTime() }
.onEach {
delay(100)
println("Emit $it (${currTime() - start}ms) ")
}
.buffer()
.collect {
println("Collect $it starts (${currTime() - start}ms) ")
delay(500)
println("Collect $it ends (${currTime() - start}ms) ")
}
}

println("Cost $time ms")
}

执行结果:
Emit 1 (104ms)
Collect 1 starts (108ms)
Emit 2 (207ms)
Emit 3 (309ms)
Emit 4 (411ms)
Emit 5 (513ms)
Collect 1 ends (613ms)
Collect 2 starts (613ms)
Collect 2 ends (1114ms)
Collect 3 starts (1114ms)
Collect 3 ends (1615ms)
Collect 4 starts (1615ms)
Collect 4 ends (2118ms)
Collect 5 starts (2118ms)
Collect 5 ends (2622ms)
Collected in 2689 ms

---------------------------------------------------------------------------

fun main() = runBlocking {

val time = measureTimeMillis {
(1..5)
.asFlow()
.onStart { start = currTime() }
.onEach {
delay(100)
println("Emit $it (${currTime() - start}ms) ")
}
.conflate()
.collect {
println("Collect $it starts (${currTime() - start}ms) ")
delay(500)
println("Collect $it ends (${currTime() - start}ms) ")
}
}

println("Cost $time ms")
}

执行结果:
Emit 1 (106ms)
Collect 1 starts (110ms)
Emit 2 (213ms)
Emit 3 (314ms)
Emit 4 (419ms)
Emit 5 (520ms)
Collect 1 ends (613ms)
Collect 5 starts (613ms)
Collect 5 ends (1113ms)
Cost 1162 ms

DROP 策略

RxJava 的 contributor:David Karnok, 他写了一个kotlin-flow-extensions库,其中包括:FlowOnBackpressureDrop.kt,这个类支持 DROP 策略。

1
2
3
4
5
/**
* Drops items from the upstream when the downstream is not ready to receive them.
*/
@FlowPreview
fun <T> Flow<T>.onBackpressurureDrop() : Flow<T> = FlowOnBackpressureDrop(this)

使用这个库的话,可以通过使用 Flow 的扩展函数 onBackpressurureDrop() 来支持 DROP 策略。

Flow 异常处理

Flow 可以使用传统的 try…catch 来捕获异常:

1
2
3
4
5
6
7
8
9
10
11
fun main() = runBlocking {
flow {
emit(1)
try {
throw RuntimeException()
} catch (e: Exception) {
e.stackTrace
}
}.onCompletion { println("Done") }
.collect { println(it) }
}

另外,也可以使用 catch 操作符来捕获异常。

catch 操作符

上面讲述过 onCompletion 操作符。但是 onCompletion 不能捕获异常,只能用于判断是否有异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fun main() = runBlocking {
flow {
emit(1)
throw RuntimeException()
}.onCompletion { cause ->
if (cause != null)
println("Flow completed exceptionally")
else
println("Done")
}.collect { println(it) }
}

执行结果:
1
Flow completed exceptionally
Exception in thread "main" java.lang.RuntimeException
......

catch 操作符可以捕获来自上游的异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fun main() = runBlocking {
flow {
emit(1)
throw RuntimeException()
}
.onCompletion { cause ->
if (cause != null)
println("Flow completed exceptionally")
else
println("Done")
}
.catch{ println("catch exception") }
.collect { println(it) }
}

执行结果:
1
Flow completed exceptionally
catch exception

上面的代码如果把 onCompletion、catch 交换一下位置,则 catch 操作符捕获到异常后,不会影响到下游。因此,onCompletion 操作符不再打印”Flow completed exceptionally”

catch 操作符用于实现异常透明化处理。例如在 catch 操作符内,可以使用 throw 再次抛出异常、可以使用 emit() 转换为发射值、可以用于打印或者其他业务逻辑的处理等等。

但是,catch 只是中间操作符不能捕获下游的异常,类似 collect 内的异常。对于下游的异常,可以多次使用 catch 操作符来解决。

对于 collect 内的异常,除了传统的 try…catch 之外,还可以借助 onEach 操作符。把业务逻辑放到 onEach 操作符内,在 onEach 之后是 catch 操作符,最后是 collect()。

1
2
3
4
5
6
7
8
9
10
fun main() = runBlocking<Unit> {
flow {
......
}
.onEach {
......
}
.catch { ... }
.collect()
}

retry、retryWhen 操作符

像 RxJava 一样,Flow 也有重试的操作符。

如果上游遇到了异常,并使用了 retry 操作符,则 retry 会让 Flow 最多重试 retries 指定的次数。

1
2
3
4
5
6
7
public fun <T> Flow<T>.retry(
retries: Long = Long.MAX_VALUE,
predicate: suspend (cause: Throwable) -> Boolean = { true }
): Flow<T> {
require(retries > 0) { "Expected positive amount of retries, but had $retries" }
return retryWhen { cause, attempt -> attempt < retries && predicate(cause) }
}

例如,下面打印了三次”Emitting 1”、”Emitting 2”,最后两次是通过 retry 操作符打印出来的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
fun main() = runBlocking {

(1..5).asFlow().onEach {
if (it == 3) throw RuntimeException("Error on $it")
}.retry(2) {

if (it is RuntimeException) {
return@retry true
}
false
}
.onEach { println("Emitting $it") }
.catch { it.printStackTrace() }
.collect()
}

执行结果:
Emitting 1
Emitting 2
Emitting 1
Emitting 2
Emitting 1
Emitting 2
java.lang.RuntimeException: Error on 3
......

retry 操作符最终调用的是 retryWhen 操作符。下面的代码跟刚才的执行结果一致:

1
2
3
4
5
6
7
8
9
10
11
12
fun main() = runBlocking {

(1..5).asFlow().onEach {
if (it == 3) throw RuntimeException("Error on $it")
}
.onEach { println("Emitting $it") }
.retryWhen { cause, attempt ->
attempt < 2
}
.catch { it.printStackTrace() }
.collect()
}

因为 retryWhen 操作符的参数是谓词,当谓词返回 true 时才会进行重试。谓词还接收一个 attempt 作为参数表示尝试的次数,该次数是从0开始的。

Flow Lifecycle

RxJava 的 do 操作符能够监听 Observables 的生命周期的各个阶段。

Flow 并没有多那么丰富的操作符来监听其生命周期的各个阶段,目前只有 onStart、onCompletion 来监听 Flow 的创建和结束。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fun main() = runBlocking {

(1..5).asFlow().onEach {
if (it == 3) throw RuntimeException("Error on $it")
}
.onStart { println("Starting flow") }
.onEach { println("On each $it") }
.catch { println("Exception : ${it.message}") }
.onCompletion { println("Flow completed") }
.collect()
}

执行结果:
Starting flow
On each 1
On each 2
Flow completed
Exception : Error on 3

Flow 线程操作

Flow 只需使用 flowOn 操作符,而不必像 RxJava 需要去深入理解 observeOn、subscribeOn 之间的区别。

RxJava 的 observeOn 操作符,接收一个 Scheduler 参数,用来指定下游操作运行在特定的线程调度器 Scheduler 上。Flow 的 flowOn 操作符,接收一个 CoroutineContext 参数,影响的是上游的操作。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
val customerDispatcher = Executors.newFixedThreadPool(5).asCoroutineDispatcher()

fun main() = runBlocking {

flow {
for (i in 1..5) {
delay(100)
emit(i)
}
}.map {
it * it
}.flowOn(Dispatchers.IO)
.map {
it+1
}
.flowOn(customerDispatcher)
.collect {
println("${Thread.currentThread().name}: $it")
}
}

flow builder 和两个 map 操作符都会受到两个flowOn的影响,其中 flow builder 和 map 操作符都会受到第一个flowOn的影响并使用 Dispatchers.io 线程池,第二个 map 操作符会切换到指定的 customerDispatcher 线程池。

buffer 实现并发操作

上面介绍了 buffer 操作符对应 RxJava Backpressure 中的 BUFFER 策略。

事实上 buffer 操作符也可以并发地执行任务,它是除了使用 flowOn 操作符之外的另一种方式,只是不能显示地指定 Dispatchers。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
fun main() = runBlocking {
val time = measureTimeMillis {
flow {
for (i in 1..5) {
delay(100)
emit(i)
}
}
.buffer()
.collect { value ->
delay(300)
println(value)
}
}
println("Collected in $time ms")
}

执行结果:
1
2
3
4
5
Collected in 1676 ms

在上述例子中,所有的 delay 所花费的时间是2000ms。然而通过 buffer 操作符并发地执行 emit,再顺序地执行 collect 函数后,所花费的时间在 1700ms 左右。

flatMapMerge 实现并行操作

在讲解并行操作之前,先来了解一下并发和并行的区别。

  • 并发(concurrency):是指一个处理器同时处理多个任务。
  • 并行(parallelism):是多个处理器或者是多核的处理器同时处理多个不同的任务。并行是同时发生的多个并发事件,具有并发的含义,而并发则不一定是并行。

RxJava 可以借助 flatMap 操作符实现并行,亦可以使用 ParallelFlowable 类实现并行操作。Flow 也有相应的操作符 flatMapMerge 可以实现并行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fun main() = runBlocking {

val result = arrayListOf<Int>()
for (index in 1..100){
result.add(index)
}

result.asFlow()
.flatMapMerge {
flow {
emit(it)
}
.flowOn(Dispatchers.IO)
}
.collect { println("$it") }
}

Flow 其他的操作符

transform

在使用 transform 操作符时,可以任意多次调用 emit ,这是 transform 跟 map 最大的区别:

1
2
3
4
5
6
7
8
9
10
fun main() = runBlocking {

(1..5).asFlow()
.transform {
emit(it * 2)
delay(100)
emit(it * 4)
}
.collect { println(it) }
}

transform 也可以使用 emit 发射任意值:

1
2
3
4
5
6
7
8
9
10
fun main() = runBlocking {

(1..5).asFlow()
.transform {
emit(it * 2)
delay(100)
emit("emit $it")
}
.collect { println(it) }
}

take

take 操作符只取前几个 emit 发射的值。

1
2
3
4
5
6
fun main() = runBlocking {

(1..5).asFlow()
.take(2)
.collect { println(it) }
}

reduce

类似于 Kotlin 集合中的 reduce 函数,能够对集合进行计算操作。

例如,对平方数列求和:

1
2
3
4
5
6
7
8
fun main() = runBlocking {

val sum = (1..5).asFlow()
.map { it * it }
.reduce { a, b -> a + b }

println(sum)
}

例如,计算阶乘:

1
2
3
4
5
6
fun main() = runBlocking {

val sum = (1..5).asFlow().reduce { a, b -> a * b }

println(sum)
}

fold

也类似于 Kotlin 集合中的 fold 函数,fold 也需要设置初始值。

1
2
3
4
5
6
7
8
fun main() = runBlocking {

val sum = (1..5).asFlow()
.map { it * it }
.fold(0) { a, b -> a + b }

println(sum)
}

在上述代码中,初始值为0就类似于使用 reduce 函数实现对平方数列求和。

而对于计算阶乘:

1
2
3
4
5
6
fun main() = runBlocking {

val sum = (1..5).asFlow().fold(1) { a, b -> a * b }

println(sum)
}

zip

zip 是可以将2个 flow 进行合并的操作符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fun main() = runBlocking {

val flowA = (1..5).asFlow()
val flowB = flowOf("one", "two", "three","four","five")
flowA.zip(flowB) { a, b -> "$a and $b" }
.collect { println(it) }
}

执行结果:
1 and one
2 and two
3 and three
4 and four
5 and five

zip 操作符会把 flowA 中的一个 item 和 flowB 中对应的一个 item 进行合并。即使 flowB 中的每一个 item 都使用了 delay() 函数,在合并过程中也会等待 delay() 执行完后再进行合并。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fun main() = runBlocking {

val flowA = (1..5).asFlow()
val flowB = flowOf("one", "two", "three", "four", "five").onEach { delay(100) }

val time = measureTimeMillis {
flowA.zip(flowB) { a, b -> "$a and $b" }
.collect { println(it) }
}

println("Cost $time ms")
}

执行结果:
1 and one
2 and two
3 and three
4 and four
5 and five
Cost 561 ms

如果 flowA 中 item 个数大于 flowB 中 item 个数,执行合并后新的 flow 的 item 个数 = 较小的 flow 的 item 个数。:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fun main() = runBlocking {

val flowA = (1..6).asFlow()
val flowB = flowOf("one", "two", "three","four","five")
flowA.zip(flowB) { a, b -> "$a and $b" }
.collect { println(it) }
}

执行结果:
1 and one
2 and two
3 and three
4 and four
5 and five

combine

combine 虽然也是合并,但是跟 zip 不太一样。

使用 combine 合并时,每次从 flowA 发出新的 item ,会将其与 flowB 的最新的 item 合并。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fun main() = runBlocking {

val flowA = (1..5).asFlow().onEach { delay(100) }
val flowB = flowOf("one", "two", "three","four","five").onEach { delay(200) }
flowA.combine(flowB) { a, b -> "$a and $b" }
.collect { println(it) }
}

执行结果:
1 and one
2 and one
3 and one
3 and two
4 and two
5 and two
5 and three
5 and four
5 and five

flattenMerge

其实,flattenMerge 不会组合多个 flow ,而是将它们作为单个流执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fun main() = runBlocking {

val flowA = (1..5).asFlow()
val flowB = flowOf("one", "two", "three","four","five")

flowOf(flowA,flowB)
.flattenConcat()
.collect{ println(it) }
}

执行结果:
1
2
3
4
5
one
two
three
four
five

为了能更清楚地看到 flowA、flowB 作为单个流的执行,对他们稍作改动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fun main() = runBlocking {

val flowA = (1..5).asFlow().onEach { delay(100) }
val flowB = flowOf("one", "two", "three","four","five").onEach { delay(200) }

flowOf(flowA,flowB)
.flattenMerge(2)
.collect{ println(it) }
}

执行结果:
1
one
2
3
two
4
5
three
four
five

flatMapConcat

flatMapConcat 由 map、flattenConcat 操作符实现。

1
2
3
@FlowPreview
public fun <T, R> Flow<T>.flatMapConcat(transform: suspend (value: T) -> Flow<R>): Flow<R> =
map(transform).flattenConcat()

在调用 flatMapConcat 后,collect 函数在收集新值之前会等待 flatMapConcat 内部的 flow 完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
fun currTime() = System.currentTimeMillis()

var start: Long = 0

fun main() = runBlocking {

(1..5).asFlow()
.onStart { start = currTime() }
.onEach { delay(100) }
.flatMapConcat {
flow {
emit("$it: First")
delay(500)
emit("$it: Second")
}
}
.collect {
println("$it at ${System.currentTimeMillis() - start} ms from start")
}
}

执行结果:
1: First at 114 ms from start
1: Second at 619 ms from start
2: First at 719 ms from start
2: Second at 1224 ms from start
3: First at 1330 ms from start
3: Second at 1830 ms from start
4: First at 1932 ms from start
4: Second at 2433 ms from start
5: First at 2538 ms from start
5: Second at 3041 ms from start

flatMapMerge

flatMapMerge 由 map、flattenMerge 操作符实现。

1
2
3
4
5
@FlowPreview
public fun <T, R> Flow<T>.flatMapMerge(
concurrency: Int = DEFAULT_CONCURRENCY,
transform: suspend (value: T) -> Flow<R>
): Flow<R> = map(transform).flattenMerge(concurrency)

flatMapMerge 是顺序调用内部代码块,并且并行地执行 collect 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
fun currTime() = System.currentTimeMillis()

var start: Long = 0

fun main() = runBlocking {

(1..5).asFlow()
.onStart { start = currTime() }
.onEach { delay(100) }
.flatMapMerge {
flow {
emit("$it: First")
delay(500)
emit("$it: Second")
}
}
.collect {
println("$it at ${System.currentTimeMillis() - start} ms from start")
}
}

执行结果:
1: First at 116 ms from start
2: First at 216 ms from start
3: First at 319 ms from start
4: First at 422 ms from start
5: First at 525 ms from start
1: Second at 618 ms from start
2: Second at 719 ms from start
3: Second at 822 ms from start
4: Second at 924 ms from start
5: Second at 1030 ms from start

flatMapMerge 操作符有一个参数 concurrency ,它默认使用DEFAULT_CONCURRENCY,如果想更直观地了解 flatMapMerge 的并行,可以对这个参数进行修改。例如改成2,就会发现不一样的执行结果。

flatMapLatest

当发射了新值之后,上个 flow 就会被取消。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
fun currTime() = System.currentTimeMillis()

var start: Long = 0

fun main() = runBlocking {

(1..5).asFlow()
.onStart { start = currTime() }
.onEach { delay(100) }
.flatMapLatest {
flow {
emit("$it: First")
delay(500)
emit("$it: Second")
}
}
.collect {
println("$it at ${System.currentTimeMillis() - start} ms from start")
}
}

执行结果:
1: First at 114 ms from start
2: First at 220 ms from start
3: First at 321 ms from start
4: First at 422 ms from start
5: First at 524 ms from start
5: Second at 1024 ms from start

互操作性

Flow 仍然属于响应式范畴。开发者通过 kotlinx-coroutines-reactive 模块中 Flow.asPublisher() 和 Publisher.asFlow() ,可以方便地将 Flow 跟 Reactive Streams 进行互操作。

参考资料

https://www.jianshu.com/p/d672744ad3e0

Android Application启动流程

发表于 2020-08-19 | 分类于 Android知识点

Android Application与其他移动平台有两个重大不同点:

  • 每个Android App都在一个独立空间里, 意味着其运行在一个单独的进程中, 拥有自己的VM, 被系统分配一个唯一的user ID.
  • Android App由很多不同组件组成, 这些组件还可以启动其他App的组件. 因此, Android App并没有一个类似程序入口的main()方法.

Android Application组件包括:

  • Activities: 前台界面, 直接面向User, 提供UI和操作.
  • Services: 后台任务.
  • Broadcast Receivers: 广播接收者.
  • Contexnt Providers: 数据提供者.

Android进程与Linux进程一样. 默认情况下, 每个apk运行在自己的Linux进程中. 另外, 默认一个进程里面只有一个线程—主线程. 这个主线程中有一个Looper实例, 通过调用Looper.loop()从Message队列里面取出Message来做相应的处理.

启动App流程

用户点击Home上的一个App图标, 启动一个应用时:

Click事件会调用startActivity(Intent), 会通过Binder IPC机制, 最终调用到ActivityManagerService. 该Service会执行如下操作:

  • 第一步通过PackageManager的resolveIntent()收集这个intent对象的指向信息.
  • 指向信息被存储在一个intent对象中.
  • 下面重要的一步是通过grantUriPermissionLocked()方法来验证用户是否有足够的权限去调用该intent对象指向的Activity.
  • 如果有权限, ActivityManagerService会检查并在新的task中启动目标activity.
  • 现在, 是时候检查这个进程的ProcessRecord是否存在了.

如果ProcessRecord是null, ActivityManagerService会创建新的进程来实例化目标activity.

创建进程

ActivityManagerService调用startProcessLocked()方法来创建新的进程, 该方法会通过前面讲到的socket通道传递参数给Zygote进程. Zygote孵化自身, 并调用ZygoteInit.main()方法来实例化ActivityThread对象并最终返回新进程的pid.

ActivityThread随后依次调用Looper.prepareLoop()和Looper.loop()来开启消息循环.

流程图如下:

绑定Application

接下来要做的就是将进程和指定的Application绑定起来. 这个是通过上节的ActivityThread对象中调用bindApplication()方法完成的. 该方法发送一个BIND_APPLICATION的消息到消息队列中, 最终通过handleBindApplication()方法处理该消息. 然后调用makeApplication()方法来加载App的classes到内存中.

流程如下:

启动Activity

经过前两个步骤之后, 系统已经拥有了该application的进程. 后面的调用顺序就是普通的从一个已经存在的进程中启动一个新进程的activity了.

实际调用方法是realStartActivity(), 它会调用application线程对象中的sheduleLaunchActivity()发送一个LAUNCH_ACTIVITY消息到消息队列中, 通过 handleLaunchActivity()来处理该消息.

假设点击的是一个视频浏览的App, 其流程如下:

影响启动速度的原因

高耗时任务
数据库初始化、某些第三方框架初始化、大文件读取、MultiDex加载等,导致CPU阻塞

复杂的View层级
使用的嵌套Layout过多,层级加深,导致View在渲染过程中,递归加深,占用CPU资源,影响Measure、Layout等方法的速度

类过于复杂
Java对象的创建也是需要一定时间的,如果一个类中结构特别复杂,new一个对象将消耗较高的资源,特别是一些单例的初始化,需要特别注意其中的结构

主题及Activity配置
有一些App是带有Splash页的,有的则直接进入主界面,由于主题切换,可能会导致白屏,或者点了Icon,过一会儿才出现主界面

冷启动的优化

减少在Application和第一个Activity的onCreate()方法的工作量;
不要让Application参与业务的操作;
不要在Application进行耗时操作;
不要以静态变量的方式在Application中保存数据;
减少布局的复杂性和深度;

冷启动秒开方式:
1、将背景图设置成我们APP的Logo图,作为APP启动的引导,现在市面上大部分的APP也是这么做的。

1
2
3
<style name="AppWelcome" parent="AppTheme">
<item name="android:windowBackground">@mipmap/bg_welcome_start</item>
</style>

2、将背景颜色设置为透明色,这样当用户点击桌面APP图片的时候,并不会”立即”进入APP,而且在桌面上停留一会,其实这时候APP已经是启动的了,只是我们心机的把Theme里的windowBackground的颜色设置成透明的,强行把锅甩给了手机应用厂商(手机反应太慢了啦,哈哈)。

1
<style name="Appwelcome" parent="android:Theme.Translucent.NoTitleBar.Fullscreen"/>

Dex懒加载

在APP功能日益复杂的今天,MultiDex几乎是已经无法避免了,为了启动速度的优化,可以将启动时必需的方法,放在主Dex中(即classes.dex),方法是在Gradle脚本中配置multiDexKeepFile或者multiDexKeepProguard属性(代码如下),待App启动完成后,再使用MultiDex.install来加载其他的Dex文件。这种方法风险比较高,而且实现成本比较大,如果启动依赖的库比较多,还是无法实现

1
2
3
4
5
6
7
8
android {
buildTypes {
release {
multiDexKeepFile file('multidex-config.txt') // multiDexKeepFile规则
multiDexKeepProguard file('multidex-config.pro') // 类似ProGuard的规则
}
}
}

配置文件示例:

1
2
3
4
5
6
7
8
9
10
11
# 常规的multiDexKeepFile规则

com/example/MyClass.class
com/example/MyOtherClass.class

# 类似ProGuard规则

-keep class com.example.MyClass
-keep class com.example.MyClassToo

-keep class com.example.** { *; } // All classes in the com.example package

多线程的思考

在App启动时,为了加快启动速度,通常会使用多线程手段来并行执行任务,充分发挥多核CPU的优势,提高运算效率。此方法固然能够对启动速度的优化,起到一定作用,但实际开发中,有以下几点值得深思:

并发的线程数,多少合适?(效率高但不至于阻塞)

频繁切换线程,是否带来负面影响?(频繁地从主线程扔进辅助线程操作再将结果抛回来会不会比直接执行更慢)

何时并行?何时串行?(有的任务能只能串,有的任务可以并行)

这个时候,拿Android经典的AsyncTask类来说事,再合适不过了!

1
2
3
4
5
6
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
// We want at least 2 threads and at most 4 threads in the core pool,
// preferring to have 1 less than the CPU count to avoid saturating
// the CPU with background work
private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;

上面的代码是AsyncTask确定线程池数量的部分,其中,核心执行池保证最少2个线程,最多不超过CPU可用核数-1,最大线程池数量为CPU核数的2倍+1

这样配置线程池的目的很简单:防止并发过大,导致CPU阻塞,影响效率

参考资料

[译]Android Application启动流程分析

MySQL 存储过程

发表于 2020-07-16 | 分类于 数据库

存储过程(Stored Procedure)是一种在数据库中存储复杂程序,以便外部程序调用的一种数据库对象。存储过程思想上很简单,就是数据库 SQL 语言层面的代码封装与重用。

存储过程是为了完成特定功能的SQL语句集,经编译创建并保存在数据库中,用户可通过指定存储过程的名字并给定参数(需要时)来调用执行。

MySQL 5.0 版本开始支持存储过程。

  • 优点
    存储过程可封装,并隐藏复杂的商业逻辑。
    存储过程可以回传值,并可以接受参数。
    存储过程无法使用 SELECT 指令来运行,因为它是子程序,与查看表,数据表或用户定义函数不同。
    存储过程可以用在数据检验,强制实行商业逻辑等。
  • 缺点
    存储过程,往往定制化于特定的数据库上,因为支持的编程语言不同。当切换到其他厂商的数据库系统时,需要重写原有的存储过程。
    存储过程的性能调校与撰写,受限于各种数据库系统。

创建存储过程

MySQL中,创建存储过程的基本形式如下:

CREATE PROCEDURE sp_name ([proc_parameter[,…]]) [characteristic …] routine_body

其中,sp_name参数是存储过程的名称;proc_parameter表示存储过程的参数列表; characteristic参数指定存储过程的特性;routine_body参数是SQL代码的内容,可以用BEGIN…END来标志SQL代码的开始和结束。

proc_parameter

proc_parameter中的每个参数由3部分组成。这3部分分别是输入输出类型、参数名称和参数类型。其形式如下:

[ IN | OUT | INOUT ] param_name type

其中,IN表示输入参数;OUT表示输出参数; INOUT表示既可以是输入,也可以是输出; param_name参数是存储过程的参数名称;type参数指定存储过程的参数类型,该类型可以是MySQL数据库的任意数据类型。

characteristic

characteristic参数有多个取值。其取值说明如下:

LANGUAGE SQL:说明routine_body部分是由SQL语言的语句组成,这也是数据库系统默认的语言。

[NOT] DETERMINISTIC:指明存储过程的执行结果是否是确定的。DETERMINISTIC表示结果是确定的。每次执行存储过程时,相同的输入会得到相同的输出。NOT DETERMINISTIC表示结果是非确定的,相同的输入可能得到不同的输出。默认情况下,结果是非确定的。

{ CONTAINS SQL | NO SQL | READS SQL DATA | MODIFIES SQL DATA }:指明子程序使用SQL语句的限制。CONTAINS SQL表示子程序包含SQL语句,但不包含读或写数据的语句;NO SQL表示子程序中不包含SQL语句;READS SQL DATA表示子程序中包含读数据的语句;MODIFIES SQL DATA表示子程序中包含写数据的语句。默认情况下,系统会指定为CONTAINS SQL。

SQL SECURITY { DEFINER | INVOKER }:指明谁有权限来执行。DEFINER表示只有定义者自己才能够执行;INVOKER表示调用者可以执行。默认情况下,系统指定的权限是DEFINER。

COMMENT ‘string’:注释信息。

技巧:创建存储过程时,系统默认指定CONTAINS SQL,表示存储过程中使用了SQL语句。但是,如果存储过程中没有使用SQL语句,最好设置为NO SQL。而且,存储过程中最好在COMMENT部分对存储过程进行简单的注释,以便以后在阅读存储过程的代码时更加方便。

【示例1】 下面创建一个名为num_from_employee的存储过程。代码如下:

1
2
3
4
5
6
7
CREATE  PROCEDURE  num_from_employee (IN emp_id INT, OUT count_num INT )  
READS SQL DATA
BEGIN
SELECT COUNT(*) INTO count_num
FROM employee
WHERE d_id=emp_id ;
END

上述代码中,存储过程名称为num_from_employee;输入变量为emp_id;输出变量为count_num。SELECT语句从employee表查询d_id值等于emp_id的记录,并用COUNT(*)计算d_id值相同的记录的条数,最后将计算结果存入count_num中。

说明:MySQL中默认的语句结束符为分号(;)。存储过程中的SQL语句需要分号来结束。为了避免冲突,首先用”DELIMITER &&”将MySQL的结束符设置为&&。最后再用”DELIMITER ;”来将结束符恢复成分号。这与创建触发器时是一样的。如下示例:

1
2
3
4
5
6
7
8
9
10
11
12
DELIMITER $$
DROP PROCEDURE IF EXISTS `TEST`.`getRecord` $$
CREATE PROCEDURE `TEST`.`getRecord` (
IN in_id INTEGER,
OUT out_name VARCHAR(20),
OUT out_age INTEGER)
BEGIN
SELECT name, age
INTO out_name, out_age
FROM Student where id = in_id;
END $$
DELIMITER ;

函数

在MySQL中,创建存储函数的基本形式如下:

CREATE FUNCTION sp_name ([func_parameter[,…]]) RETURNS type [characteristic …] routine_body

其中,sp_name参数是存储函数的名称;func_parameter表示存储函数的参数列表;RETURNS type指定返回值的类型;characteristic参数指定存储函数的特性,该参数的取值与存储过程中的取值是一样的;routine_body参数是SQL代码的内容,可以用BEGIN…END来标志SQL代码的开始和结束。

func_parameter可以由多个参数组成,其中每个参数由参数名称和参数类型组成,其形式如下:param_name type

其中,param_name参数是存储函数的参数名称;type参数指定存储函数的参数类型,该类型可以是MySQL数据库的任意数据类型。

【示例2】 下面创建一个名为name_from_employee的存储函数。代码如下:

1
2
3
4
5
6
7
CREATE  FUNCTION  name_from_employee (emp_id INT )  
RETURNS VARCHAR(20)
BEGIN
RETURN (SELECT name
FROM employee
WHERE num=emp_id );
END

上述代码中,存储函数的名称为name_from_employee;该函数的参数为emp_id;返回值是VARCHAR类型。SELECT语句从employee表查询num值等于emp_id的记录,并将该记录的name字段的值返回。

变量的使用

在存储过程和函数中,可以定义和使用变量。用户可以使用DECLARE关键字来定义变量。然后可以为变量赋值。这些变量的作用范围是BEGIN…END程序段中。本小节将讲解如何定义变量和为变量赋值。

1.定义变量

MySQL中可以使用DECLARE关键字来定义变量。定义变量的基本语法如下:

DECLARE var_name[,…] type [DEFAULT value]

其中, DECLARE关键字是用来声明变量的;var_name参数是变量的名称,这里可以同时定义多个变量;type参数用来指定变量的类型;DEFAULT value子句将变量默认值设置为value,没有使用DEFAULT子句时,默认值为NULL。

【示例3】 下面定义变量my_sql,数据类型为INT型,默认值为10。代码如下:

1
DECLARE  my_sql  INT  DEFAULT 10 ;

2.为变量赋值

MySQL中可以使用SET关键字来为变量赋值。SET语句的基本语法如下:

SET var_name = expr [, var_name = expr] …

其中,SET关键字是用来为变量赋值的;var_name参数是变量的名称;expr参数是赋值表达式。一个SET语句可以同时为多个变量赋值,各个变量的赋值语句之间用逗号隔开。

【示例4】 下面为变量my_sql赋值为30。代码如下:

1
SET  my_sql = 30 ;

MySQL中还可以使用SELECT…INTO语句为变量赋值。其基本语法如下:

SELECT col_name[,…] INTO var_name[,…] FROM table_name WEHRE condition

其中,col_name参数表示查询的字段名称;var_name参数是变量的名称;table_name参数指表的名称;condition参数指查询条件。

【示例5】 下面从employee表中查询id为2的记录,将该记录的d_id值赋给变量my_sql。代码如下:

1
SELECT  d_id  INTO  my_sql  FROM  employee  WEHRE  id=2 ;

定义条件和处理程序

定义条件和处理程序是事先定义程序执行过程中可能遇到的问题。并且可以在处理程序中定义解决这些问题的办法。这种方式可以提前预测可能出现的问题,并提出解决办法。这样可以增强程序处理问题的能力,避免程序异常停止。MySQL中都是通过DECLARE关键字来定义条件和处理程序。

定义条件

MySQL中可以使用DECLARE关键字来定义条件。其基本语法如下:

DECLARE condition_name CONDITION FOR condition_value
condition_value:
SQLSTATE [VALUE] sqlstate_value | mysql_error_code

其中,condition_name参数表示条件的名称;condition_value参数表示条件的类型;sqlstate_value参数和mysql_error_code参数都可以表示MySQL的错误。例如ERROR 1146 (42S02)中,sqlstate_value值是42S02,mysql_error_code值是1146。

【示例6】 下面定义”ERROR 1146 (42S02)”这个错误,名称为can_not_find。可以用两种不同的方法来定义,代码如下:

1
2
3
4
//方法一:使用sqlstate_value  
DECLARE can_not_find CONDITION FOR SQLSTATE '42S02' ;
//方法二:使用mysql_error_code
DECLARE can_not_find CONDITION FOR 1146 ;

定义处理程序

MySQL中可以使用DECLARE关键字来定义处理程序。其基本语法如下:

DECLARE handler_type HANDLER FOR condition_value[,…] sp_statement
handler_type:
CONTINUE | EXIT | UNDO
condition_value:
SQLSTATE [VALUE] sqlstate_value | condition_name | SQLWARNING | NOT FOUND | SQLEXCEPTION | mysql_error_code

其中,handler_type参数指明错误的处理方式,该参数有3个取值。这3个取值分别是CONTINUE、EXIT和UNDO。CONTINUE表示遇到错误不进行处理,继续向下执行;EXIT表示遇到错误后马上退出;UNDO表示遇到错误后撤回之前的操作,MySQL中暂时还不支持这种处理方式。

注意:通常情况下,执行过程中遇到错误应该立刻停止执行下面的语句,并且撤回前面的操作。但是,MySQL中现在还不能支持UNDO操作。因此,遇到错误时最好执行EXIT操作。如果事先能够预测错误类型,并且进行相应的处理,那么可以执行CONTINUE操作。

condition_value参数指明错误类型,该参数有6个取值。sqlstate_value和mysql_error_code与条件定义中的是同一个意思。condition_name是DECLARE定义的条件名称。SQLWARNING表示所有以01开头的sqlstate_value值。NOT FOUND表示所有以02开头的sqlstate_value值。SQLEXCEPTION表示所有没有被SQLWARNING或NOT FOUND捕获的sqlstate_value值。sp_statement表示一些存储过程或函数的执行语句。

【示例7】 下面是定义处理程序的几种方式。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
//方法一:捕获sqlstate_value  
DECLARE CONTINUE HANDLER FOR SQLSTATE '42S02' SET @info='CAN NOT FIND';
//方法二:捕获mysql_error_code
DECLARE CONTINUE HANDLER FOR 1146 SET @info='CAN NOT FIND';
//方法三:先定义条件,然后调用
DECLARE can_not_find CONDITION FOR 1146 ;
DECLARE CONTINUE HANDLER FOR can_not_find SET @info='CAN NOT FIND';
//方法四:使用SQLWARNING
DECLARE EXIT HANDLER FOR SQLWARNING SET @info='ERROR';
//方法五:使用NOT FOUND
DECLARE EXIT HANDLER FOR NOT FOUND SET @info='CAN NOT FIND';
//方法六:使用SQLEXCEPTION
DECLARE EXIT HANDLER FOR SQLEXCEPTION SET @info='ERROR';

上述代码是6种定义处理程序的方法。

第一种方法是捕获sqlstate_value值。如果遇到sqlstate_value值为42S02,执行CONTINUE操作,并且输出”CAN NOT FIND”信息。

第二种方法是捕获mysql_error_code值。如果遇到mysql_error_code值为1146,执行CONTINUE操作,并且输出”CAN NOT FIND”信息。

第三种方法是先定义条件,然后再调用条件。这里先定义can_not_find条件,遇到1146错误就执行CONTINUE操作。

第四种方法是使用SQLWARNING。SQLWARNING捕获所有以01开头的sqlstate_value值,然后执行EXIT操作,并且输出”ERROR”信息。

第五种方法是使用NOT FOUND。NOT FOUND捕获所有以02开头的sqlstate_value值,然后执行EXIT操作,并且输出”CAN NOT FIND”信息。

第六种方法是使用SQLEXCEPTION。SQLEXCEPTION捕获所有没有被SQLWARNING或NOT FOUND捕获的sqlstate_value值,然后执行EXIT操作,并且输出”ERROR”信息。

参考资料

https://www.cnblogs.com/mark5/p/11170577.html
https://www.runoob.com/w3cnote/mysql-stored-procedure.html

Spring 知识点笔记

发表于 2020-07-09 | 分类于 Java Web

体系结构

Spring 有可能成为所有企业应用程序的一站式服务点,然而,Spring 是模块化的,允许你挑选和选择适用于你的模块,不必要把剩余部分也引入。Spring 框架提供约 20 个模块,可以根据应用程序的要求来使用。

核心容器

核心容器由spring-core,spring-beans,spring-context,spring-context-support和spring-expression(SpEL,Spring表达式语言,Spring Expression Language)等模块组成,它们的细节如下:

  • spring-core模块提供了框架的基本组成部分,包括 IoC 和依赖注入功能。

  • spring-beans 模块提供 BeanFactory,工厂模式的微妙实现,它移除了编码式单例的需要,并且可以把配置和依赖从实际编码逻辑中解耦。

  • context模块建立在由core和 beans 模块的基础上建立起来的,它以一种类似于JNDI注册的方式访问对象。Context模块继承自Bean模块,并且添加了国际化(比如,使用资源束)、事件传播、资源加载和透明地创建上下文(比如,通过Servelet容器)等功能。Context模块也支持Java EE的功能,比如EJB、JMX和远程调用等。ApplicationContext接口是Context模块的焦点。spring-context-support提供了对第三方库集成到Spring上下文的支持,比如缓存(EhCache, Guava, JCache)、邮件(JavaMail)、调度(CommonJ, Quartz)、模板引擎(FreeMarker, JasperReports, Velocity)等。

  • spring-expression模块提供了强大的表达式语言,用于在运行时查询和操作对象图。它是JSP2.1规范中定义的统一表达式语言的扩展,支持set和get属性值、属性赋值、方法调用、访问数组集合及索引的内容、逻辑算术运算、命名变量、通过名字从Spring IoC容器检索对象,还支持列表的投影、选择以及聚合等。

它们的完整依赖关系如下图所示:

数据访问/集成

数据访问/集成层包括 JDBC,ORM,OXM,JMS 和事务处理模块,它们的细节如下:注:JDBC=Java Data Base Connectivity,ORM=Object Relational Mapping,OXM=Object XML Mapping,JMS=Java Message Service)

  • JDBC 模块提供了JDBC抽象层,它消除了冗长的JDBC编码和对数据库供应商特定错误代码的解析。

  • ORM 模块提供了对流行的对象关系映射API的集成,包括JPA、JDO和Hibernate等。通过此模块可以让这些ORM框架和spring的其它功能整合,比如前面提及的事务管理。

  • OXM 模块提供了对OXM实现的支持,比如JAXB、Castor、XML Beans、JiBX、XStream等。

  • JMS 模块包含生产(produce)和消费(consume)消息的功能。从Spring 4.1开始,集成了spring-messaging模块。。

事务模块为实现特殊接口类及所有的 POJO 支持编程式和声明式事务管理。(注:编程式事务需要自己写beginTransaction()、commit()、rollback()等事务管理方法,声明式事务是通过注解或配置由spring自动处理,编程式事务粒度更细)

Web

Web 层由 Web,Web-MVC,Web-Socket 和 Web-Portlet 组成,它们的细节如下:

  • Web 模块提供面向web的基本功能和面向web的应用上下文,比如多部分(multipart)文件上传功能、使用Servlet监听器初始化IoC容器等。它还包括HTTP客户端以及Spring远程调用中与web相关的部分。。

  • Web-MVC 模块为web应用提供了模型视图控制(MVC)和REST Web服务的实现。Spring的MVC框架可以使领域模型代码和web表单完全地分离,且可以与Spring框架的其它所有功能进行集成。

  • Web-Socket 模块为 WebSocket-based 提供了支持,而且在 web 应用程序中提供了客户端和服务器端之间通信的两种方式。

  • Web-Portlet 模块提供了用于Portlet环境的MVC实现,并反映了spring-webmvc模块的功能。

其他

还有其他一些重要的模块,像 AOP,Aspects,Instrumentation,Web 和测试模块,它们的细节如下:

  • AOP 模块提供了面向方面的编程实现,允许你定义方法拦截器和切入点对代码进行干净地解耦,从而使实现功能的代码彻底的解耦出来。使用源码级的元数据,可以用类似于.Net属性的方式合并行为信息到代码中。

  • Aspects 模块提供了与 AspectJ 的集成,这是一个功能强大且成熟的面向切面编程(AOP)框架。

  • Instrumentation 模块在一定的应用服务器中提供了类 instrumentation 的支持和类加载器的实现。

  • Messaging 模块为 STOMP 提供了支持作为在应用程序中 WebSocket 子协议的使用。它也支持一个注解编程模型,它是为了选路和处理来自 WebSocket 客户端的 STOMP 信息。

  • 测试模块支持对具有 JUnit 或 TestNG 框架的 Spring 组件的测试。

Spring IoC 容器

Spring 容器是 Spring 框架的核心。容器将创建对象,把它们连接在一起,配置它们,并管理他们的整个生命周期从创建到销毁。Spring 容器使用依赖注入(DI)来管理组成一个应用程序的组件。这些对象被称为 Spring Beans。

IOC 容器具有依赖注入功能的容器,它可以创建对象,IOC 容器负责实例化、定位、配置应用程序中的对象及建立这些对象间的依赖。通常new一个实例,控制权由程序员控制,而“控制反转”是指new实例工作不由程序员来做而是交给Spring容器来做。Spring 提供了以下两种不同类型的容器:

ApplicationContext 容器包括 BeanFactory 容器的所有功能,所以通常建议超过 BeanFactory。BeanFactory 仍然可以用于轻量级的应用程序,如移动设备或基于 applet 的应用程序,其中它的数据量和速度是显著。

Spring 的 BeanFactory 容器(忽略)

这是一个最简单的容器,它主要的功能是为依赖注入 (DI) 提供支持,这个容器接口在 org.springframework.beans.factory.BeanFactor 中被定义。BeanFactory 和相关的接口,比如BeanFactoryAware、DisposableBean、InitializingBean,仍旧保留在 Spring 中,主要目的是向后兼容已经存在的和那些 Spring 整合在一起的第三方框架。

在 Spring 中,有大量对 BeanFactory 接口的实现。其中,最常被使用的是 XmlBeanFactory 类。这个容器从一个 XML 文件中读取配置元数据,由这些元数据来生成一个被配置化的系统或者应用。

Spring ApplicationContext 容器

Application Context 是 BeanFactory 的子接口,也被成为 Spring 上下文。 这个容器在 org.springframework.context.ApplicationContext interface 接口中定义。

最常被使用的 ApplicationContext 接口实现:

  • FileSystemXmlApplicationContext:该容器从 XML 文件中加载已被定义的 bean。在这里,你需要提供给构造器 XML 文件的完整路径。

  • ClassPathXmlApplicationContext:该容器从 XML 文件中加载已被定义的 bean。在这里,你不需要提供 XML 文件的完整路径,只需正确配置 CLASSPATH 环境变量即可,因为,容器会从 CLASSPATH 中搜索 bean 配置文件。

  • WebXmlApplicationContext:该容器会在一个 web 应用程序的范围内加载在 XML 文件中已被定义的 bean。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public class HelloSpringSingleton {

public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("/res/applicationContext.xml");
HelloWorld objA = (HelloWorld) context.getBean("helloWorld");
objA.setName("I'm object A");
objA.sqyHello();
HelloWorld objB = (HelloWorld) context.getBean("helloWorld");
objB.sqyHello();
}

}

public class HelloWorld {
private String name;

public void setName(String name) {
this.name = name;
}

public String getName() {
System.out.println(name);
return name;
}

public void init(){
System.out.println("Bean is going through init.");
}
public void destroy(){
System.out.println("Bean will destroy now.");
}

}

// /res/applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="helloWorld" class="com.zsm.test.HelloWorld"
scope="singleton">
</bean>

<bean id="helloWorld1" class="com.zsm.test.HelloWorld"
init-method="init" destroy-method="destroy">
<property name="name" value="Hello World!"/>
</bean>

</beans>

Spring Bean 定义

被称作 bean 的对象是构成应用程序的支柱也是由 Spring IoC 容器管理的。bean 是一个被实例化,组装,并通过 Spring IoC 容器所管理的对象。这些 bean 是由用容器提供的配置元数据创建的。

bean 定义包含称为配置元数据的信息,下述容器也需要知道配置元数据:

  • 如何创建一个 bean

  • bean 的生命周期的详细信息

  • bean 的依赖关系

Spring IoC 容器完全由实际编写的配置元数据的格式解耦。有下面三个重要的方法把配置元数据提供给 Spring 容器:

  • 基于 XML 的配置文件

  • 基于注解的配置

  • 基于 Java 的配置

Spring Bean 作用域

Spring Bean 生命周期

为了定义安装和拆卸一个 bean,我们只要声明带有 init-method 或 destroy-method 参数的 。init-method 属性指定一个方法,在实例化 bean 时,立即调用该方法。同样,destroy-method 指定一个方法,只有从容器中移除 bean 之后,才能调用该方法。

Bean的生命周期可以表达为:Bean的定义——Bean的初始化——Bean的使用——Bean的销毁。

初始化及销毁回调

org.springframework.beans.factory.InitializingBean 接口指定一个单一的方法:

1
void afterPropertiesSet() throws Exception;

因此,你可以简单地实现上述接口和初始化工作可以在 afterPropertiesSet() 方法中执行,如下所示:

1
2
3
4
5
public class ExampleBean implements InitializingBean {
public void afterPropertiesSet() {
// do some initialization work
}
}

在基于 XML 的配置元数据的情况下,你可以使用 init-method 属性来指定带有 void 无参数方法的名称。例如:

1
<bean id="exampleBean" class="examples.ExampleBean" init-method="init"/>

下面是类的定义:

1
2
3
4
5
public class ExampleBean {
public void init() {
// do some initialization work
}
}

销毁回调 org.springframework.beans.factory.DisposableBean 与上述初始化回调类似。

main方法中,你需要注册一个在 AbstractApplicationContext 类中声明的关闭 hook 的 registerShutdownHook() 方法。它将确保正常关闭,并且调用相关的 destroy 方法。

1
2
3
4
5
6
public static void main(String[] args) {
AbstractApplicationContext context = new ClassPathXmlApplicationContext("Beans.xml");
HelloWorld obj = (HelloWorld) context.getBean("helloWorld");
obj.getMessage();
context.registerShutdownHook();
}

建议不要使用 InitializingBean 或者 DisposableBean 的回调方法,因为 XML 配置在命名方法上提供了极大的灵活性。

默认的初始化和销毁方法

如果你有太多具有相同名称的初始化或者销毁方法的 Bean,那么你不需要在每一个 bean 上声明初始化方法和销毁方法。框架使用 元素中的 default-init-method 和 default-destroy-method 属性提供了灵活地配置这种情况,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"
default-init-method="init"
default-destroy-method="destroy">

<bean id="..." class="...">
<!-- collaborators and configuration for this bean go here -->
</bean>

</beans>

Spring——Bean 后置处理器

Bean 后置处理器允许在调用初始化方法前后对 Bean 进行额外的处理。

BeanPostProcessor 接口定义回调方法,你可以实现该方法来提供自己的实例化逻辑,依赖解析逻辑等。你可以配置多个 BeanPostProcessor 接口,通过设置 BeanPostProcessor 实现的 Ordered 接口提供的 order 属性来控制这些 BeanPostProcessor 接口的执行顺序。

ApplicationContext 会自动检测由 BeanPostProcessor 接口的实现定义的 bean,注册这些 bean 为后置处理器,然后通过在容器中创建 bean,在适当的时候调用它。

示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
public class InitHelloWorld implements BeanPostProcessor {
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("BeforeInitialization : " + beanName);
return bean; // you can return any other object as well
}
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("AfterInitialization : " + beanName);
return bean; // you can return any other object as well
}
}

<bean class="com.zsm.test.InitHelloWorld" />

Bean 定义继承

Spring Bean 定义的继承与 Java 类的继承无关,但是继承的概念是一样的。示例如下:

1
2
3
4
5
6
7
8
9
<bean id="helloWorld" class="com.tutorialspoint.HelloWorld">
<property name="message1" value="Hello World!"/>
<property name="message2" value="Hello Second World!"/>
</bean>

<bean id="helloIndia" class="com.tutorialspoint.HelloIndia" parent="helloWorld">
<property name="message1" value="Hello India!"/>
<property name="message3" value="Namaste India!"/>
</bean>

Spring 依赖注入

基于构造函数的依赖注入

几种写法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<beans>

<!-- Definition for textEditor bean -->
<bean id="textEditor" class="com.zsm.test.di.TextEditor">
<constructor-arg ref="spellChecker"/>
</bean>

<!-- Definition for spellChecker bean -->
<!--<bean id="spellChecker" class="com.zsm.test.di.SpellChecker"/>-->

<bean id="spellChecker" class="com.zsm.test.di.SpellChecker">
<!--存在不止一个参数时,构造函数的参数在 bean 定义中的顺序就是把这些参数提供给适当的构造函数的顺序就可以了。考虑下面的类:-->
<!--<constructor-arg ref="helloWorld"/>
<constructor-arg ref="helloWorld2"/>-->

<!--如果你使用 type 属性显式的指定了构造函数参数的类型,容器也可以使用与简单类型匹配的类型。例如:-->
<!--<constructor-arg type="int" value="2001"/>
<constructor-arg type="java.lang.String" value="Zara"/>-->

<!--最后并且也是最好的传递构造函数参数的方式,使用 index 属性来显式的指定构造函数参数的索引。下面是基于索引为 0 的例子,如下所示:-->
<constructor-arg index="0" value="2001"/>
<constructor-arg index="1" value="Zara"/>
</bean>

<bean id="helloWorld" class="com.zsm.test.HelloWorld">
<property name="name" value="Hello World!"/>
</bean>
<bean id="helloWorld2" class="com.zsm.test.HelloWorld2">
<property name="name2" value="Hello World2!"/>
</bean>

</beans>

public class SpellChecker {
public SpellChecker() {
System.out.println("Inside SpellChecker constructor.");
}

public SpellChecker(int num, String desc) {
System.out.println("Inside SpellChecker constructor:" + num + ";" + desc);
}

public SpellChecker(HelloWorld num, HelloWorld2 desc) {
System.out.println("Inside SpellChecker constructor:" + num.getName() + ";" + desc.getName2());
}

public void checkSpelling() {
System.out.println("Inside checkSpelling.");
}

}

public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("Beans.xml");
TextEditor te = (TextEditor) context.getBean("textEditor");
te.spellCheck();
}

基于设值函数的依赖注入

和构造函数注入唯一的区别就是在基于构造函数注入中,我们使用的是〈bean〉标签中的〈constructor-arg〉元素,而在基于设值函数的注入中,我们使用的是〈bean〉标签中的〈property〉元素。

第二个需要注意的点是,如果你要把一个引用传递给一个对象,那么你需要使用 标签的 ref 属性,而如果你要直接传递一个值,那么你应该使用 value 属性。

如果你有许多的设值函数方法,那么在 XML 配置文件中使用 p-namespace 是非常方便的(ref则须加后缀-ref)。示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

<bean id="john-classic" class="com.example.Person">
<property name="name" value="John Doe"/>
<property name="spouse" ref="jane"/>
</bean>

<bean name="jane" class="com.example.Person">
<property name="name" value="John Doe"/>
</bean>

</beans>


//上述 XML 配置文件可以使用 p-namespace 以一种更简洁的方式重写,如下所示:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

<bean id="john-classic" class="com.example.Person"
p:name="John Doe"
p:spouse-ref="jane"/>
</bean>

<bean name="jane" class="com.example.Person"
p:name="John Doe"/>
</bean>

</beans>

注入内部 Beans

正如你所知道的 Java 内部类是在其他类的范围内被定义的,同理,inner beans 是在其他 bean 的范围内定义的 bean。定义入下:

1
2
3
4
5
<bean id="outerBean" class="...">
<property name="target">
<bean id="innerBean" class="..."/>
</property>
</bean>

示例:

1
2
3
4
5
<bean id="textEditor" class="com.zsm.test.interclass.TextEditor">
<property name="spellChecker">
<bean class="com.zsm.test.interclass.SpellChecker"/>
</property>
</bean>

注入集合

现在如果你想传递多个值,如 Java Collection 类型 List、Set、Map 和 Properties,应该怎么做呢。为了处理这种情况,Spring 提供了四种类型的集合的配置元素,如下所示:

示例入下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

<!-- Definition for javaCollection -->
<bean id="javaCollection" class="com.tutorialspoint.JavaCollection">

<!-- results in a setAddressList(java.util.List) call -->
<property name="addressList">
<list>
<value>INDIA</value>
<value>Pakistan</value>
<value>USA</value>
<value>USA</value>
</list>
</property>

<!-- results in a setAddressSet(java.util.Set) call -->
<property name="addressSet">
<set>
<value>INDIA</value>
<value>Pakistan</value>
<value>USA</value>
<value>USA</value>
</set>
</property>

<!-- results in a setAddressMap(java.util.Map) call -->
<property name="addressMap">
<map>
<entry key="1" value="INDIA"/>
<entry key="2" value="Pakistan"/>
<entry key="3" value="USA"/>
<entry key="4" value="USA"/>
</map>
</property>

<!-- results in a setAddressProp(java.util.Properties) call -->
<property name="addressProp">
<props>
<prop key="one">INDIA</prop>
<prop key="two">Pakistan</prop>
<prop key="three">USA</prop>
<prop key="four">USA</prop>
</props>
</property>

</bean>

</beans>

也可以将引用和值混合在一起,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- Passing bean reference  for java.util.Set -->
<property name="addressSet">
<set>
<ref bean="address1"/>
<ref bean="address2"/>
<value>Pakistan</value>
</set>
</property>

<!-- Passing bean reference for java.util.Map -->
<property name="addressMap">
<map>
<entry key="one" value="INDIA"/>
<entry key ="two" value-ref="address1"/>
<entry key ="three" value-ref="address2"/>
</map>
</property>

注入 null 和空字符串的值

1
2
3
<bean id="..." class="exampleBean">
<property name="email" value=""/>
</bean>

相当于:exampleBean.setEmail(“”)。

1
2
3
<bean id="..." class="exampleBean">
<property name="email"><null/></property>
</bean>

相当于:exampleBean.setEmail(null)。

Beans 自动装配

Spring 容器可以在不使用<constructor-arg>和<property> 元素的情况下自动装配相互协作的 bean 之间的关系,这有助于减少编写一个大的基于 Spring 的应用程序的 XML 配置的数量。

你可以使用<bean>元素的 autowire 属性为一个 bean 定义指定自动装配模式:

可以使用 byType 或者 constructor 自动装配模式来连接数组和其他类型的集合。

自动装配的局限性

当自动装配始终在同一个项目中使用时,它的效果最好。如果通常不使用自动装配,它可能会使开发人员混淆的使用它来连接只有一个或两个 bean 定义。不过,自动装配可以显著减少需要指定的属性或构造器参数,但你应该在使用它们之前考虑到自动装配的局限性和缺点。

自动装配 ‘byName’

这种模式由属性名称指定自动装配。

例如,在配置文件中,如果一个 bean 定义设置为自动装配 byName,并且它包含 spellChecker 属性(即,它有一个 setSpellChecker(…) 方法),那么 Spring 就会查找定义名为 spellChecker 的 bean,并且用它来设置这个属性。你仍然可以使用 标签连接其余的属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
<!--<bean id="textEditor" class="com.zsm.test.interclass.TextEditor">
<property name="spellChecker" ref="spellChecker"/>
<property name="name" value="Generic Text Editor" />
</bean>-->

//使用autowire后
<bean id="textEditor" class="com.tutorialspoint.TextEditor"
autowire="byName">
<property name="name" value="Generic Text Editor" />
</bean>

<bean id="spellChecker" class="com.tutorialspoint.SpellChecker">
</bean>

自动装配 ‘byType’

这种模式由属性类型指定自动装配。

由构造函数自动装配

这种模式与 byType 非常相似,但它应用于构造器参数。

基于注解的配置

从 Spring 2.5 开始就可以使用注解来配置依赖注入,而不是采用 XML 来描述一个 bean 连线。在 XML 注入之前进行注解注入,因此后者的配置将通过两种方式的属性连线被前者重写。

注解连线在默认情况下在 Spring 容器中不打开。启用配置如下:

1
<context:annotation-config/>

让我们来看看几个重要的注解,并且了解它们是如何工作的:

@Required 注释

@Required 注释应用于 bean 属性的 setter 方法,它表明受影响的 bean 属性在配置时必须放在 XML 配置文件中,否则容器就会抛出一个 BeanInitializationException 异常。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Student {
private Integer age;
private String name;
@Required
public void setAge(Integer age) {
this.age = age;
}
public Integer getAge() {
return age;
}
@Required
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
}

<bean id="student" class="com.tutorialspoint.Student">
<property name="name" value="Zara" />
<property name="age" value="11"/>
</bean>

@Autowired 注释

@Autowired 注释对在哪里和如何完成自动连接提供了更多的细微的控制

1.Setter 方法中的 @Autowired
你可以在 XML 文件中的 setter 方法中使用 @Autowired 注释来除去 元素。当 Spring遇到一个在 setter 方法中使用的 @Autowired 注释,它会在方法中视图执行 byType 自动连接。

2.构造函数中的 @Autowired
你也可以在构造函数中使用 @Autowired。一个构造函数 @Autowired 说明当创建 bean 时,即使在 XML 文件中没有使用 元素配置 bean ,构造函数也会被自动连接。

3.属性中的 @Autowired
你可以在属性中使用 @Autowired 注释来除去 setter 方法。当时使用 为自动连接属性传递的时候,Spring 会将这些传递过来的值或者引用自动分配给那些属性。

4.@Autowired 的(required=false)选项
默认情况下,@Autowired 注释意味着依赖是必须的,它类似于 @Required 注释,然而,你可以使用 @Autowired 的 (required=false) 选项关闭默认行为。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class TextEditor {
@Autowired
private SpellChecker spellChecker;
public TextEditor() {
System.out.println("Inside TextEditor constructor." );
}
public SpellChecker getSpellChecker( ){
return spellChecker;
}
public void spellCheck(){
spellChecker.checkSpelling();
}
}

<!-- Definition for textEditor bean -->
<bean id="textEditor" class="com.tutorialspoint.TextEditor">
</bean>

<!-- Definition for spellChecker bean -->
<bean id="spellChecker" class="com.tutorialspoint.SpellChecker">
</bean>

@Qualifier 注释

当你创建多个具有相同类型的 bean 时,并且想要用一个属性只为它们其中的一个进行装配,在这种情况下,你可以使用 @Qualifier 注释和 @Autowired 注释通过指定哪一个真正的 bean 将会被装配来消除混乱。相当于别名。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class Profile {
@Autowired
@Qualifier("student1")
private Student student;
public Profile(){
System.out.println("Inside Profile constructor." );
}
public void printAge() {
System.out.println("Age : " + student.getAge() );
}
public void printName() {
System.out.println("Name : " + student.getName() );
}
}

<!-- Definition for profile bean -->
<bean id="profile" class="com.tutorialspoint.Profile">
</bean>

<!-- Definition for student1 bean -->
<bean id="student1" class="com.tutorialspoint.Student">
<property name="name" value="Zara" />
<property name="age" value="11"/>
</bean>

<!-- Definition for student2 bean -->
<bean id="student2" class="com.tutorialspoint.Student">
<property name="name" value="Nuha" />
<property name="age" value="2"/>
</bean>

Spring JSR-250 注释

Spring还使用基于 JSR-250 注释,它包括 @PostConstruct, @PreDestroy 和 @Resource 注释。这些注释并不是必需的,只是一个替代方案。

为了定义一个 bean 的安装和卸载,我们使用 init-method 和/或 destroy-method 参数简单的声明一下 。init-method 属性指定了一个方法,该方法在 bean 的实例化阶段会立即被调用。同样地,destroy-method 指定了一个方法,该方法只在一个 bean 从容器中删除之前被调用。你可以使用 @PostConstruct 注释作为初始化回调函数的一个替代,@PreDestroy 注释作为销毁回调函数的一个替代。

示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class TextEditor {
private SpellChecker spellChecker;
@Resource(name= "spellChecker")
public void setSpellChecker( SpellChecker spellChecker ){
this.spellChecker = spellChecker;
}
public SpellChecker getSpellChecker(){
return spellChecker;
}
public void spellCheck(){
spellChecker.checkSpelling();
}
}


public class HelloWorld {
private String message;
public void setMessage(String message){
this.message = message;
}
public String getMessage(){
System.out.println("Your Message : " + message);
return message;
}
@PostConstruct
public void init(){
System.out.println("Bean is going through init.");
}
@PreDestroy
public void destroy(){
System.out.println("Bean will destroy now.");
}
}

<bean id="helloWorld"
class="com.tutorialspoint.HelloWorld"
init-method="init" destroy-method="destroy">
<property name="message" value="Hello World!"/>
</bean>

基于 Java 的配置

带有 @Configuration 的注解类表示这个类可以使用 Spring IoC 容器作为 bean 定义的来源。@Bean 注解告诉 Spring,一个带有 @Bean 的注解方法将返回一个对象,该对象应该被注册为在 Spring 应用程序上下文中的 bean。最简单可行的 @Configuration 类如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class HelloWorldConfig {
@Bean
public HelloWorld helloWorld(){
return new HelloWorld();
}
}

// 上面的代码将等同于下面的 XML 配置:
<beans>
<bean id="helloWorld" class="com.tutorialspoint.HelloWorld" />
</beans>

在这里,带有 @Bean 注解的方法名称作为 bean 的 ID,它创建并返回实际的 bean。你的配置类可以声明多个 @Bean。一旦定义了配置类,你就可以使用 AnnotationConfigApplicationContext 来加载并把他们提供给 Spring 容器,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(HelloWorldConfig.class);
HelloWorld helloWorld = ctx.getBean(HelloWorld.class);
helloWorld.setMessage("Hello World!");
helloWorld.getMessage();
}

//你可以加载各种配置类,如下所示:
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(AppConfig.class, OtherConfig.class);
ctx.register(AdditionalConfig.class);
ctx.refresh();
MyService myService = ctx.getBean(MyService.class);
myService.doStuff();
}

@import

@import 注解允许从另一个配置类中加载 @Bean 定义。考虑 ConfigA 类,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Configuration
public class ConfigA {
@Bean
public A a() {
return new A();
}
}

//你可以在另一个 Bean 声明中导入上述 Bean 声明,如下所示:
@Configuration
@Import(ConfigA.class)
public class ConfigB {
@Bean
public B a() {
return new A();
}
}

现在,当实例化上下文时,不需要同时指定 ConfigA.class 和 ConfigB.class,只有 ConfigB 类需要提供,如下所示:

1
2
3
4
5
6
7
public static void main(String[] args) {
ApplicationContext ctx =
new AnnotationConfigApplicationContext(ConfigB.class);
// now both beans A and B will be available...
A a = ctx.getBean(A.class);
B b = ctx.getBean(B.class);
}

生命周期回调

@Bean 注解支持指定任意的初始化和销毁的回调方法,就像在 bean 元素中 Spring 的 XML 的初始化方法和销毁方法的属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Foo {
public void init() {
// initialization logic
}
public void cleanup() {
// destruction logic
}
}

@Configuration
public class AppConfig {
@Bean(initMethod = "init", destroyMethod = "cleanup" )
public Foo foo() {
return new Foo();
}
}

指定 Bean 的范围。默认范围是单实例,但是你可以重写带有 @Scope 注解的该方法,如下所示:

1
2
3
4
5
6
7
8
@Configuration
public class AppConfig {
@Bean
@Scope("prototype")
public Foo foo() {
return new Foo();
}
}

事件处理

Spring 的核心是 ApplicationContext,它负责管理 beans 的完整生命周期。当加载 beans 时,ApplicationContext 发布某些类型的事件。例如,当上下文启动时,ContextStartedEvent 发布,当上下文停止时,ContextStoppedEvent 发布。

Spring 提供了以下的标准事件:

由于 Spring 的事件处理是单线程的,所以如果一个事件被发布,直至并且除非所有的接收者得到的该消息,该进程被阻塞并且流程将不会继续。因此,如果事件处理被使用,在设计应用程序时应注意。

自定义事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import org.springframework.context.ApplicationEvent;
public class CustomEvent extends ApplicationEvent{
public CustomEvent(Object source) {
super(source);
}
public String toString(){
return "My Custom Event";
}
}

//下面是 CustomEventPublisher.java 文件的内容:
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
public class CustomEventPublisher implements ApplicationEventPublisherAware {
private ApplicationEventPublisher publisher;
public void setApplicationEventPublisher(ApplicationEventPublisher publisher){
this.publisher = publisher;
}
public void publish() {
CustomEvent ce = new CustomEvent(this);
publisher.publishEvent(ce);
}
}

//下面是 CustomEventHandler.java 文件的内容:
import org.springframework.context.ApplicationListener;
public class CustomEventHandler implements ApplicationListener<CustomEvent>{
public void onApplicationEvent(CustomEvent event) {
System.out.println(event.toString());
}
}

//下面是 MainApp.java 文件的内容:
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class MainApp {
public static void main(String[] args) {
ConfigurableApplicationContext context = new ClassPathXmlApplicationContext("Beans.xml");
CustomEventPublisher cvp = (CustomEventPublisher) context.getBean("customEventPublisher");
cvp.publish();
cvp.publish();
}
}

//下面是配置文件 Beans.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

<bean id="customEventHandler"
class="com.tutorialspoint.CustomEventHandler"/>

<bean id="customEventPublisher"
class="com.tutorialspoint.CustomEventPublisher"/>

</beans>


//输出以下信息:
My Custom Event
My Custom Event

Spring 框架的 AOP

Spring 框架的一个关键组件是面向方面的编程(AOP)框架。面向方面的编程需要把程序逻辑分解成不同的部分称为所谓的关注点。跨一个应用程序的多个点的功能被称为横切关注点,这些横切关注点在概念上独立于应用程序的业务逻辑。有各种各样的常见的很好的方面的例子,如日志记录、审计、声明式事务、安全性和缓存等。

Spring AOP 模块提供拦截器来拦截一个应用程序,例如,当执行一个方法时,你可以在方法执行之前或之后添加额外的功能。

AOP 术语

通知的类型

Spring 方面可以使用下面提到的五种通知工作:

基于 AOP 的 XML架构

声明一个 aspect

一个 aspect 是使用 元素声明的,支持的 bean 是使用 ref 属性引用的,如下所示:

1
2
3
4
5
6
7
8
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
...
</aop:aspect>
</aop:config>
<bean id="aBean" class="...">
...
</bean>

声明一个切入点

一个切入点有助于确定使用不同建议执行的感兴趣的连接点(即方法):

1
2
3
4
5
6
7
8
9
10
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service.*.*(..))"/>
...
</aop:aspect>
</aop:config>
<bean id="aBean" class="...">
...
</bean>

声明建议

你可以使用 <aop:{ADVICE NAME}> 元素在一个 中声明五个建议中的任何一个,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service.*.*(..))"/>
<!-- a before advice definition -->
<aop:before pointcut-ref="businessService"
method="doRequiredTask"/>
<!-- an after advice definition -->
<aop:after pointcut-ref="businessService"
method="doRequiredTask"/>
<!-- an after-returning advice definition -->
<!--The doRequiredTask method must have parameter named retVal -->
<aop:after-returning pointcut-ref="businessService"
returning="retVal"
method="doRequiredTask"/>
<!-- an after-throwing advice definition -->
<!--The doRequiredTask method must have parameter named ex -->
<aop:after-throwing pointcut-ref="businessService"
throwing="ex"
method="doRequiredTask"/>
<!-- an around advice definition -->
<aop:around pointcut-ref="businessService"
method="doRequiredTask"/>
...
</aop:aspect>
</aop:config>
<bean id="aBean" class="...">
...
</bean>

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
public class Logging {
/**
* This is the method which I would like to execute
* before a selected method execution.
*/
public void beforeAdvice(){
System.out.println("Going to setup student profile.");
}
/**
* This is the method which I would like to execute
* after a selected method execution.
*/
public void afterAdvice(){
System.out.println("Student profile has been setup.");
}
/**
* This is the method which I would like to execute
* when any method returns.
*/
public void afterReturningAdvice(Object retVal){
System.out.println("Returning:" + retVal.toString() );
}
/**
* This is the method which I would like to execute
* if there is an exception raised.
*/
public void AfterThrowingAdvice(IllegalArgumentException ex){
System.out.println("There has been an exception: " + ex.toString());
}
}

public class Student {
private Integer age;
private String name;
public void setAge(Integer age) {
this.age = age;
}
public Integer getAge() {
System.out.println("Age : " + age );
return age;
}
public void setName(String name) {
this.name = name;
}
public String getName() {
System.out.println("Name : " + name );
return name;
}
public void printThrowException(){
System.out.println("Exception raised");
throw new IllegalArgumentException();
}
}

public class MainApp {
public static void main(String[] args) {
ApplicationContext context =
new ClassPathXmlApplicationContext("BeansAop.xml");
Student student = (Student) context.getBean("student");
student.getName();
student.getAge();
student.printThrowException();
}
}

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd ">

<aop:config>
<aop:aspect id="log" ref="logging">
<aop:pointcut id="selectAll"
expression="execution(* com.zsm.test.aop.*.*(..))"/>
<aop:before pointcut-ref="selectAll" method="beforeAdvice"/>
<aop:after pointcut-ref="selectAll" method="afterAdvice"/>
<aop:after-returning pointcut-ref="selectAll"
returning="retVal"
method="afterReturningAdvice"/>
<aop:after-throwing pointcut-ref="selectAll"
throwing="ex"
method="AfterThrowingAdvice"/>
</aop:aspect>
</aop:config>

<!--<aop:config>
<aop:aspect id="log" ref="logging">
<aop:pointcut id="selectAll"
expression="execution(* com.zsm.test.aop.Student.getName(..))"/>
<aop:before pointcut-ref="selectAll" method="beforeAdvice"/>
<aop:after pointcut-ref="selectAll" method="afterAdvice"/>
</aop:aspect>
</aop:config>-->

<!-- Definition for student bean -->
<bean id="student" class="com.zsm.test.aop.Student">
<property name="name" value="Zara" />
<property name="age" value="11"/>
</bean>

<!-- Definition for logging aspect -->
<bean id="logging" class="com.zsm.test.aop.Logging"/>

</beans>

//运行结果
Going to setup student profile.
Name : Zara
Student profile has been setup.
Returning:Zara
Going to setup student profile.
Age : 11
Student profile has been setup.
Returning:11
Going to setup student profile.
Exception raised
Student profile has been setup.
There has been an exception: java.lang.IllegalArgumentException
Exception in thread "main" java.lang.IllegalArgumentException
at com.zsm.test.aop.Student.printThrowException(Student.java:22)
...

基于 AOP 的 @AspectJ

@AspectJ 作为通过 Java 5 注释注释的普通的 Java 类,它指的是声明 aspects 的一种风格。通过在你的基于架构的 XML 配置文件中包含以下元素,@AspectJ 支持是可用的。

1
<aop:aspectj-autoproxy/>

注意:aspectjrt.jar,aspectjweaver.jar添加到lib

声明一个 aspect

Aspects 类和其他任何正常的 bean 一样,除了它们将会用 @AspectJ 注释之外,它和其他类一样可能有方法和字段,如下所示:

1
2
3
@Aspect
public class AspectModule {
}

声明一个切入点

一个切入点有助于确定使用不同建议执行的感兴趣的连接点(即方法)。在处理基于配置的 XML 架构时,切入点的声明有两个部分:

  • 一个切入点表达式决定了我们感兴趣的哪个方法会真正被执行。

  • 一个切入点标签包含一个名称和任意数量的参数。方法的真正内容是不相干的,并且实际上它应该是空的。

下面的示例中定义了一个名为 ‘businessService’ 的切入点,该切入点将与 com.xyz.myapp.service 包下的类中可用的每一个方法相匹配:

1
2
@Pointcut("execution(* com.xyz.myapp.service.*.*(..))") // expression 
private void businessService() {}

声明建议

你可以使用 @{ADVICE-NAME} 注释声明五个建议中的任意一个,如下所示。这假设你已经定义了一个切入点标签方法 businessService():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Before("businessService()")
public void doBeforeTask(){
...
}
@After("businessService()")
public void doAfterTask(){
...
}
@AfterReturning(pointcut = "businessService()", returning="retVal")
public void doAfterReturnningTask(Object retVal){
// you can intercept retVal here.
...
}
@AfterThrowing(pointcut = "businessService()", throwing="ex")
public void doAfterThrowingTask(Exception ex){
// you can intercept thrown exception here.
...
}
@Around("businessService()")
public void doAroundTask(){
...
}

你可以为任意一个建议定义你的切入点内联。下面是在建议之前定义内联切入点的一个示例:

1
2
3
4
@Before("execution(* com.xyz.myapp.service.*.*(..))")
public doBeforeTask(){
...
}

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
@Aspect
public class Logging {
/** Following is the definition for a pointcut to select
* all the methods available. So advice will be called
* for all the methods.
*/
@Pointcut("execution(* com.zsm.test.aop.anno.*.*(..))")
private void selectAll(){}
/**
* This is the method which I would like to execute
* before a selected method execution.
*/
@Before("selectAll()")
public void beforeAdvice(){
System.out.println("Going to setup student profile.");
}
/**
* This is the method which I would like to execute
* after a selected method execution.
*/
@After("selectAll()")
public void afterAdvice(){
System.out.println("Student profile has been setup.");
}
/**
* This is the method which I would like to execute
* when any method returns.
*/
@AfterReturning(pointcut = "selectAll()", returning="retVal")
public void afterReturningAdvice(Object retVal){
System.out.println("Returning:" + retVal.toString() );
}
/**
* This is the method which I would like to execute
* if there is an exception raised by any method.
*/
@AfterThrowing(pointcut = "selectAll()", throwing = "ex")
public void AfterThrowingAdvice(IllegalArgumentException ex){
System.out.println("There has been an exception: " + ex.toString());
}
}

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd ">

<aop:aspectj-autoproxy/>

<!-- Definition for student bean -->
<bean id="student" class="com.zsm.test.aop.anno.Student">
<property name="name" value="Zara" />
<property name="age" value="11"/>
</bean>

<!-- Definition for logging aspect -->
<bean id="logging" class="com.zsm.test.aop.anno.Logging"/>

</beans>

Spring JDBC 框架

Spring JDBC 提供几种方法和数据库中相应的不同的类与接口。我将给出使用 JdbcTemplate 类框架的经典和最受欢迎的方法。这是管理所有数据库通信和异常处理的中央框架类。

JdbcTemplate 类

JdbcTemplate 类执行 SQL 查询、更新语句和存储过程调用,执行迭代结果集和提取返回参数值。它也捕获 JDBC 异常并转换它们到 org.springframework.dao 包中定义的通用类、更多的信息、异常层次结构。

JdbcTemplate 类的实例是线程安全配置的。所以你可以配置 JdbcTemplate 的单个实例,然后将这个共享的引用安全地注入到多个 DAOs 中。

使用 JdbcTemplate 类时常见的做法是在你的 Spring 配置文件中配置数据源,然后共享数据源 bean 依赖注入到 DAO 类中,并在数据源的设值函数中创建了 JdbcTemplate。

配置数据源

我们在数据库 TEST 中创建一个数据库表 Student。假设你正在使用 MySQL 数据库,如果你使用其他数据库,那么你可以改变你的 DDL 和相应的 SQL 查询。

1
2
3
4
5
6
CREATE TABLE Student(
ID INT NOT NULL AUTO_INCREMENT,
NAME VARCHAR(20) NOT NULL,
AGE INT NOT NULL,
PRIMARY KEY (ID)
);

现在,我们需要提供一个数据源到 JdbcTemplate 中,所以它可以配置本身来获得数据库访问。你可以在 XML 文件中配置数据源,其中一段代码如下所示:

1
2
3
4
5
6
7
<bean id="dataSource"
class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/TEST"/>
<property name="username" value="root"/>
<property name="password" value="password"/>
</bean>

注意点:
1.Could not load JDBC driver class [com.mysql.jdbc.Driver]异常: mysql-connector-java-5.1.8.jar
2.MySQL在高版本需要指明是否进行SSL连接,在mysql连接字符串url中加入ssl=true或者false即可,如:url=jdbc:mysql://127.0.0.1:3306/framework?useSSL=true

执行 SQL 语句

我们看看如何使用 SQL 和 jdbcTemplate 对象在数据库表中执行 CRUD(创建、读取、更新和删除)操作。

查询一个整数类型:

1
2
String SQL = "select count(*) from Student";
int rowCount = jdbcTemplateObject.queryForInt( SQL );

查询一个 long 类型:

1
2
String SQL = "select count(*) from Student";
long rowCount = jdbcTemplateObject.queryForLong( SQL );

一个使用绑定变量的简单查询:

1
2
String SQL = "select age from Student where id = ?";
int age = jdbcTemplateObject.queryForInt(SQL, new Object[]{10});

查询字符串:

1
2
String SQL = "select name from Student where id = ?";
String name = jdbcTemplateObject.queryForObject(SQL, new Object[]{10}, String.class);

查询并返回一个对象:

1
2
3
4
5
6
7
8
9
10
11
String SQL = "select * from Student where id = ?";
Student student = jdbcTemplateObject.queryForObject(SQL, new Object[]{10}, new StudentMapper());
public class StudentMapper implements RowMapper<Student> {
public Student mapRow(ResultSet rs, int rowNum) throws SQLException {
Student student = new Student();
student.setID(rs.getInt("id"));
student.setName(rs.getString("name"));
student.setAge(rs.getInt("age"));
return student;
}
}

查询并返回多个对象:

1
2
3
4
5
6
7
8
9
10
11
String SQL = "select * from Student";
List<Student> students = jdbcTemplateObject.query(SQL, new StudentMapper());
public class StudentMapper implements RowMapper<Student> {
public Student mapRow(ResultSet rs, int rowNum) throws SQLException {
Student student = new Student();
student.setID(rs.getInt("id"));
student.setName(rs.getString("name"));
student.setAge(rs.getInt("age"));
return student;
}
}

在表中插入一行:

1
2
String SQL = "insert into Student (name, age) values (?, ?)";
jdbcTemplateObject.update( SQL, new Object[]{"Zara", 11} );

更新表中的一行:

1
2
String SQL = "update Student set name = ? where id = ?";
jdbcTemplateObject.update( SQL, new Object[]{"Zara", 10} );

从表中删除一行:

1
2
String SQL = "delete Student where id = ?";
jdbcTemplateObject.update( SQL, new Object[]{20} );

执行 DDL 语句

你可以使用 jdbcTemplate 中的 execute(..) 方法来执行任何 SQL 语句或 DDL 语句。下面是一个使用 CREATE 语句创建一个表的示例:

1
2
3
4
5
6
String SQL = "CREATE TABLE Student( " +
"ID INT NOT NULL AUTO_INCREMENT, " +
"NAME VARCHAR(20) NOT NULL, " +
"AGE INT NOT NULL, " +
"PRIMARY KEY (ID));"
jdbcTemplateObject.execute( SQL );

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
public class StudentJDBCTemplate implements StudentDAO {
private DataSource dataSource;
private JdbcTemplate jdbcTemplateObject;
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
this.jdbcTemplateObject = new JdbcTemplate(dataSource);
}
public void create(String name, Integer age) {
String SQL = "insert into Student (name, age) values (?, ?)";
jdbcTemplateObject.update( SQL, name, age);
System.out.println("Created Record Name = " + name + " Age = " + age);
return;
}
public Student getStudent(Integer id) {
String SQL = "select * from Student where id = ?";
Student student = jdbcTemplateObject.queryForObject(SQL,
new Object[]{id}, new StudentMapper());
return student;
}
public List<Student> listStudents() {
String SQL = "select * from Student";
List <Student> students = jdbcTemplateObject.query(SQL,
new StudentMapper());
return students;
}
public void delete(Integer id){
String SQL = "delete from Student where id = ?";
jdbcTemplateObject.update(SQL, id);
System.out.println("Deleted Record with ID = " + id );
return;
}
public void update(Integer id, Integer age){
String SQL = "update Student set age = ? where id = ?";
jdbcTemplateObject.update(SQL, age, id);
System.out.println("Updated Record with ID = " + id );
return;
}
}

public class StudentMapper implements RowMapper<Student> {
public Student mapRow(ResultSet rs, int rowNum) throws SQLException {
Student student = new Student();
student.setId(rs.getInt("id"));
student.setName(rs.getString("name"));
student.setAge(rs.getInt("age"));
return student;
}
}

public class MainApp {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("BeansJdbc.xml");
StudentJDBCTemplate studentJDBCTemplate = (StudentJDBCTemplate)context.getBean("studentJDBCTemplate");
System.out.println("------Records Creation--------" );
studentJDBCTemplate.create("Zara", 11);
studentJDBCTemplate.create("Nuha", 2);
studentJDBCTemplate.create("Ayan", 15);
System.out.println("------Listing Multiple Records--------" );
List<Student> students = studentJDBCTemplate.listStudents();
for (Student record : students) {
System.out.print("ID : " + record.getId() );
System.out.print(", Name : " + record.getName() );
System.out.println(", Age : " + record.getAge());
}
System.out.println("----Updating Record with ID = 2 -----" );
studentJDBCTemplate.update(2, 20);
System.out.println("----Listing Record with ID = 2 -----" );
Student student = studentJDBCTemplate.getStudent(2);
System.out.print("ID : " + student.getId() );
System.out.print(", Name : " + student.getName() );
System.out.println(", Age : " + student.getAge());
}
}

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd ">

<!-- Initialization for data source -->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/test?useSSL=false"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</bean>

<!-- Definition for studentJDBCTemplate bean -->
<bean id="studentJDBCTemplate" class="com.zsm.test.jdbc.StudentJDBCTemplate">
<property name="dataSource" ref="dataSource" />
</bean>
</beans>

SQL 的存储过程

SimpleJdbcCall 类可以被用于调用一个包含 IN 和 OUT 参数的存储过程。你可以在处理任何一个 RDBMS 时使用这个方法,就像 Apache Derby, DB2, MySQL, Microsoft SQL Server, Oracle,和 Sybase。

为了了解这个方法,我们使用 Student 表,它可以在 MySQL TEST 数据库中使用下面的 DDL 进行创建:

1
2
3
4
5
6
CREATE TABLE Student(
ID INT NOT NULL AUTO_INCREMENT,
NAME VARCHAR(20) NOT NULL,
AGE INT NOT NULL,
PRIMARY KEY (ID)
);

下一步,考虑接下来的 MySQL 存储过程,该过程使用 学生 Id 并且使用 OUT 参数返回相应的学生的姓名和年龄。所以让我们在你的 TEST 数据库中使用 MySQL 命令提示符创建这个存储过程:

1
2
3
4
5
6
7
8
9
10
11
12
DELIMITER $$
DROP PROCEDURE IF EXISTS `TEST`.`getRecord` $$
CREATE PROCEDURE `TEST`.`getRecord` (
IN in_id INTEGER,
OUT out_name VARCHAR(20),
OUT out_age INTEGER)
BEGIN
SELECT name, age
INTO out_name, out_age
FROM Student where id = in_id;
END $$
DELIMITER ;

示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class StudentJDBCTemplate implements StudentDAO {
private DataSource dataSource;
private SimpleJdbcCall jdbcCall;
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
this.jdbcCall = new SimpleJdbcCall(dataSource).withProcedureName("getRecord");
}
public void create(String name, Integer age) {
JdbcTemplate jdbcTemplateObject = new JdbcTemplate(dataSource);
String SQL = "insert into Student (name, age) values (?, ?)";
jdbcTemplateObject.update( SQL, name, age);
System.out.println("Created Record Name = " + name + " Age = " + age);
}
public Student1 getStudent(Integer id) {
SqlParameterSource in = new MapSqlParameterSource().addValue("in_id", id);
Map<String, Object> out = jdbcCall.execute(in);
Student1 student = new Student1();
student.setId(id);
student.setName((String) out.get("out_name"));
student.setAge((Integer) out.get("out_age"));
return student;
}
}

事务管理

一个数据库事务是一个被视为单一的工作单元的操作序列。这些操作应该要么完整地执行,要么完全不执行。事务管理是一个重要组成部分,RDBMS 面向企业应用程序,以确保数据完整性和一致性。事务的概念可以描述为具有以下四个关键属性说成是 ACID:

  • 原子性(Atomicity):事务应该当作一个单独单元的操作,这意味着整个序列操作要么是成功,要么是失败的。

  • 一致性(Consistency):这表示数据库的引用完整性的一致性,表中唯一的主键等。

  • 隔离性(Isolation):可能同时处理很多有相同的数据集的事务,每个事务应该与其他事务隔离,以防止数据损坏。

  • 持久性(Durability):一个事务一旦完成全部操作后,这个事务的结果必须是永久性的,不能因系统故障而从数据库中删除。

一个真正的 RDBMS 数据库系统将为每个事务保证所有的四个属性。使用 SQL 发布到数据库中的事务的简单视图如下:

  • 使用 begin transaction 命令开始事务。

  • 使用 SQL 查询语句执行各种删除、更新或插入操作。

如果所有的操作都成功,则执行提交操作,否则回滚所有操作。

Spring 框架在不同的底层事务管理 APIs 的顶部提供了一个抽象层。Spring 的事务支持旨在通过添加事务能力到 POJOs 来提供给 EJB 事务一个选择方案。Spring 支持编程式和声明式事务管理。EJBs 需要一个应用程序服务器,但 Spring 事务管理可以在不需要应用程序服务器的情况下实现。

编程式 vs. 声明式

Spring 支持两种类型的事务管理:

  • 编程式事务管理 :这意味着你在编程的帮助下有管理事务。这给了你极大的灵活性,但却很难维护。

  • 声明式事务管理 :这意味着你从业务代码中分离事务管理。你仅仅使用注释或 XML 配置来管理事务。

声明式事务管理比编程式事务管理更可取,尽管它不如编程式事务管理灵活,但它允许你通过代码控制事务。但作为一种横切关注点,声明式事务管理可以使用 AOP 方法进行模块化。Spring 支持使用 Spring AOP 框架的声明式事务管理。

事务抽象

Spring事务管理的五大属性:隔离级别、传播行为、是否只读、事务超时、回滚规则

Spring 事务抽象的关键是由 org.springframework.transaction.PlatformTransactionManager 接口定义,如下所示:

1
2
3
4
5
6
7
8
public interface PlatformTransactionManager {
//根据指定的传播行为,该方法返回当前活动事务或创建一个新的事务。
TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
//该方法提交给定的事务和关于它的状态。
void commit(TransactionStatus status) throws TransactionException;
//该方法执行一个给定事务的回滚。
void rollback(TransactionStatus status) throws TransactionException;
}

TransactionDefinition 是在 Spring 中事务支持的核心接口,它的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
public interface TransactionDefinition {
//该方法返回传播行为。Spring 提供了与 EJB CMT 类似的所有的事务传播选项。
int getPropagationBehavior();
//该方法返回该事务独立于其他事务的工作的程度。
int getIsolationLevel();
//该方法返回该事务的名称。
String getName();
//该方法返回以秒为单位的时间间隔,事务必须在该时间间隔内完成。
int getTimeout();
//该方法返回该事务是否是只读的。
boolean isReadOnly();
}

TransactionStatus 接口为事务代码提供了一个简单的方法来控制事务的执行和查询事务状态。

1
2
3
4
5
6
7
8
9
10
11
12
public interface TransactionStatus extends SavepointManager {
//在当前事务时新的情况下,该方法返回 true。
boolean isNewTransaction();
//该方法返回该事务内部是否有一个保存点,也就是说,基于一个保存点已经创建了嵌套事务。
boolean hasSavepoint();
//该方法设置该事务为 rollback-only 标记。
void setRollbackOnly();
//该方法返回该事务是否已标记为 rollback-only。
boolean isRollbackOnly();
//该方法返回该事务是否完成,也就是说,它是否已经提交或回滚。
boolean isCompleted();
}

编程式事务管理

我们可以直接使用 PlatformTransactionManager 来实现编程式方法从而实现事务。要开始一个新事务,你需要有一个带有适当的 transaction 属性的 TransactionDefinition 的实例。当 TransactionDefinition 创建后,你可以通过调用 getTransaction() 方法来开始你的事务,该方法会返回 TransactionStatus 的一个实例。 TransactionStatus 对象帮助追踪当前的事务状态,并且最终,如果一切运行顺利,你可以使用 PlatformTransactionManager 的 commit() 方法来提交这个事务,否则的话,你可以使用 rollback() 方法来回滚整个操作。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public void create(String name, Integer age, Integer marks, Integer year){
TransactionDefinition def = new DefaultTransactionDefinition();
TransactionStatus status = transactionManager.getTransaction(def);//开始事务
try {
String SQL1 = "insert into Student (name, age) values (?, ?)";
jdbcTemplateObject.update( SQL1, name, age);
// Get the latest student id to be used in Marks table
String SQL2 = "select max(id) from Student";
int sid = jdbcTemplateObject.queryForObject( SQL2, Integer.class );
String SQL3 = "insert into Marks(sid, marks, year) " +
"values (?, ?, ?)";
jdbcTemplateObject.update( SQL3, sid, marks, year);
System.out.println("Created Name = " + name + ", Age = " + age);
transactionManager.commit(status);//提交事务
} catch (DataAccessException e) {
System.out.println("Error in creating record, rolling back");
transactionManager.rollback(status);
throw e;
}
return;
}

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd ">

<!-- Initialization for data source -->
<bean id="dataSource"
class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/test?useSSL=false"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</bean>

<!-- Initialization for TransactionManager -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>

<!-- Definition for studentJDBCTemplate bean -->
<bean id="studentJDBCTemplate" class="com.zsm.test.transaction.StudentJDBCTemplate">
<property name="dataSource" ref="dataSource" />
<property name="transactionManager" ref="transactionManager" />
</bean>

</beans>

声明式事务管理

声明式事务管理方法允许你在配置的帮助下而不是源代码硬编程来管理事务。这意味着你可以将事务管理从事务代码中隔离出来。 bean 配置会指定事务型方法。下面是与声明式事务相关的步骤:

  • 我们使用标签,它创建一个事务处理的建议,同时,我们定义一个匹配所有方法的切入点,我们希望这些方法是事务型的并且会引用事务型的建议。

  • 如果在事务型配置中包含了一个方法的名称,那么创建的建议在调用方法之前就会在事务中开始进行。

  • 目标方法会在 try / catch 块中执行。

  • 如果方法正常结束,AOP 建议会成功的提交事务,否则它执行回滚操作。

示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

<!-- Initialization for data source -->
<bean id="dataSource"
class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/test?useSSL=false"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</bean>

<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="create"/>
</tx:attributes>
</tx:advice>

<aop:config>
<aop:pointcut id="createOperation" expression="execution(* com.zsm.test.transac.StudentJDBCTemplate.create(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="createOperation"/>
</aop:config>

<!-- Initialization for TransactionManager -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>

<!-- Definition for studentJDBCTemplate bean -->
<bean id="studentJDBCTemplate" class="com.zsm.test.transac.StudentJDBCTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>

</beans>

public class StudentJDBCTemplate implements StudentDAO{
private JdbcTemplate jdbcTemplateObject;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplateObject = new JdbcTemplate(dataSource);
}
public void create(String name, Integer age, Integer marks, Integer year){
try {
String SQL1 = "insert into Student (name, age) values (?, ?)";
jdbcTemplateObject.update( SQL1, name, age);
// Get the latest student id to be used in Marks table
String SQL2 = "select max(id) from Student";
int sid = jdbcTemplateObject.queryForObject( SQL2, Integer.class );
String SQL3 = "insert into Marks(sid, marks, year) " + "values (?, ?, ?)";
jdbcTemplateObject.update( SQL3, sid, marks, year);
System.out.println("Created Name = " + name + ", Age = " + age);
// to simulate the exception.
throw new RuntimeException("simulate Error condition") ;
} catch (DataAccessException e) {
System.out.println("Error in creating record, rolling back");
throw e;
}
}
public List<StudentMarks> listStudents() {
String SQL = "select * from Student, Marks where Student.id=Marks.sid";
List <StudentMarks> studentMarks=jdbcTemplateObject.query(SQL,
new StudentMarksMapper());
return studentMarks;
}
}

//会输出如下所示的异常。在这种情况下,事务会回滚并且在数据库表中不会创建任何记录。
Created Name = Zara, Age = 11
Exception in thread "main" java.lang.RuntimeException: simulate Error condition

Spring Web MVC 框架

Spring web MVC 框架提供了模型-视图-控制的体系结构和可以用来开发灵活、松散耦合的 web 应用程序的组件。

Spring Web 模型-视图-控制(MVC)框架是围绕 DispatcherServlet 设计的,DispatcherServlet 用来处理所有的 HTTP 请求和响应。Spring Web MVC DispatcherServlet 的请求处理的工作流程如下图所示:

下面是对应于 DispatcherServlet 传入 HTTP 请求的事件序列:

  • 收到一个 HTTP 请求后,DispatcherServlet 根据 HandlerMapping 来选择并且调用适当的控制器。

  • 控制器接受请求,并基于使用的 GET 或 POST 方法来调用适当的 service 方法。Service 方法将设置基于定义的业务逻辑的模型数据,并返回视图名称到 DispatcherServlet 中。

  • DispatcherServlet 会从 ViewResolver 获取帮助,为请求检取定义视图。

  • 一旦确定视图,DispatcherServlet 将把模型数据传递给视图,最后呈现在浏览器中。

上面所提到的所有组件,即 HandlerMapping、Controller 和 ViewResolver 是 WebApplicationContext 的一部分,而 WebApplicationContext 是带有一些对 web 应用程序必要的额外特性的 ApplicationContext 的扩展。

需求的配置

你需要映射你想让 DispatcherServlet 处理的请求,通过使用在 web.xml 文件中的一个 URL 映射。

web.xml 文件将被保留在你的应用程序的 WebContent/WEB-INF 目录下。好的,在初始化 HelloWeb DispatcherServlet 时,该框架将尝试加载位于该应用程序的 WebContent/WEB-INF 目录中文件名为 [servlet-name]-servlet.xml 的应用程序内容。

如果你不想使用默认文件名 [servlet-name]-servlet.xml 和默认位置 WebContent/WEB-INF,你可以通过在 web.xml 文件中添加 servlet 监听器 ContextLoaderListener 自定义该文件的名称和位置,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
<web-app...>
....
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/HelloWeb-servlet.xml</param-value>
</context-param>
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
</web-app>

现在,检查 HelloWeb-servlet.xml 文件的请求配置,该文件位于 web 应用程序的 WebContent/WEB-INF 目录下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd">

<context:component-scan base-package="com.tutorialspoint" />

<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/jsp/" />
<property name="suffix" value=".jsp" />
</bean>

</beans>

以下是关于 HelloWeb-servlet.xml 文件的一些要点:

  • [servlet-name]-servlet.xml 文件将用于创建 bean 定义,重新定义在全局范围内具有相同名称的任何已定义的 bean。

  • context:component-scan标签将用于激活 Spring MVC 注释扫描功能,该功能允许使用注释,如 @Controller 和 @RequestMapping 等等。

  • InternalResourceViewResolver 将使用定义的规则来解决视图名称。按照上述定义的规则,一个名称为 hello 的逻辑视图将发送给位于 /WEB-INF/jsp/hello.jsp 中实现的视图。

定义控制器

DispatcherServlet 发送请求到控制器中执行特定的功能。@Controller 注释表明一个特定类是一个控制器的作用。@RequestMapping 注释用于映射 URL 到整个类或一个特定的处理方法。

1
2
3
4
5
6
7
8
9
@Controller
@RequestMapping("/hello")
public class HelloController{
@RequestMapping(method = RequestMethod.GET)
public String printHello(ModelMap model) {
model.addAttribute("message", "Hello Spring MVC Framework!");
return "hello";
}
}

@Controller 注释定义该类作为一个 Spring MVC 控制器。在这里,第一次使用的 @RequestMapping 表明在该控制器中处理的所有方法都是相对于 /hello 路径的。下一个注释 @RequestMapping(method = RequestMethod.GET) 用于声明 printHello() 方法作为控制器的默认 service 方法来处理 HTTP GET 请求。你可以在相同的 URL 中定义其他方法来处理任何 POST 请求。

理解 Robust

发表于 2020-05-13 | 分类于 Android插件化

Robust,美团开源的新一代热更新系统,对 Android 版本无差别兼容,无需发版就可以做到随时修改线上 bug,快速对重大线上问题作出反应。Robust 热更新系统借鉴 Instant Run 原理,实现了一个兼容性更强而且实时生效的热更新方案。其基本思路是,Robust 热更新系统在一个方法的入口处插入一段跳转代码,当发现某个方法出现 bug 就跳转执行补丁中的代码,略过原有代码的执行,否则执行原有方法体逻辑。

优缺点

  • 支持Android2.3-10版本
  • 高兼容性、高稳定性,修复成功率高达99.9%
  • 补丁实时生效,不需要重新启动
  • 支持方法级别的修复,包括静态方法
  • 支持增加方法和类
  • 支持ProGuard的混淆、内联、优化等操作

需要保存打包时生成的mapping文件以及build/outputs/robust/methodsMap.robust文件

当然,这套方案虽然对开发者是透明的,但毕竟在编译阶段有插件侵入了产品代码,对运行效率、方法数、包体积还是产生了一些副作用。

原理

Robust插件对每个产品代码的每个函数都在编译打包阶段自动的插入了一段代码,插入过程对业务开发是完全透明。如State.java的getIndex函数:

1
2
3
public long getIndex() {
return 100;
}

被处理成如下的实现:

1
2
3
4
5
6
7
8
9
10
public static ChangeQuickRedirect changeQuickRedirect;
public long getIndex() {
if(changeQuickRedirect != null) {
//PatchProxy中封装了获取当前className和methodName的逻辑,并在其内部最终调用了changeQuickRedirect的对应函数
if(PatchProxy.isSupport(new Object[0], this, changeQuickRedirect, false)) {
return ((Long)PatchProxy.accessDispatch(new Object[0], this, changeQuickRedirect, false)).longValue();
}
}
return 100L;
}

可以看到Robust为每个class增加了个类型为ChangeQuickRedirect的静态成员,而在每个方法前都插入了使用changeQuickRedirect相关的逻辑,当 changeQuickRedirect不为null时,可能会执行到accessDispatch从而替换掉之前老的逻辑,达到fix的目的。 如果需将getIndex函数的返回值改为return 106,那么对应生成的patch,主要包含两个class:PatchesInfoImpl.java和StatePatch.java。 PatchesInfoImpl.java:

1
2
3
4
5
6
7
8
public class PatchesInfoImpl implements PatchesInfo {
public List<PatchedClassInfo> getPatchedClassesInfo() {
List<PatchedClassInfo> patchedClassesInfos = new ArrayList<PatchedClassInfo>();
PatchedClassInfo patchedClass = new PatchedClassInfo("com.meituan.sample.d", StatePatch.class.getCanonicalName());
patchedClassesInfos.add(patchedClass);
return patchedClassesInfos;
}
}

StatePatch.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class StatePatch implements ChangeQuickRedirect {
@Override
public Object accessDispatch(String methodSignature, Object[] paramArrayOfObject) {
String[] signature = methodSignature.split(":");
if (TextUtils.equals(signature[1], "a")) {//long getIndex() -> a
return 106;
}
return null;
}

@Override
public boolean isSupport(String methodSignature, Object[] paramArrayOfObject) {
String[] signature = methodSignature.split(":");
if (TextUtils.equals(signature[1], "a")) {//long getIndex() -> a
return true;
}
return false;
}
}

客户端拿到含有PatchesInfoImpl.java和StatePatch.java的patch.dex后,用DexClassLoader加载patch.dex,反射拿到PatchesInfoImpl.java这个class。拿到后,创建这个class的一个对象。然后通过这个对象的getPatchedClassesInfo函数,知道需要patch的class为com.meituan.sample.d(com.meituan.sample.State混淆后的名字),再反射得到当前运行环境中的com.meituan.sample.d class,将其中的changeQuickRedirect字段赋值为用patch.dex中的StatePatch.java这个class new出来的对象。这就是打patch的主要过程。通过原理分析,其实Robust只是在正常的使用DexClassLoader,所以可以说这套框架是没有兼容性问题的。

大体流程如下:

方法数

使用了Robust插件后,会导致方法数增加,原来能被ProGuard内联的函数不能被内联了。看了下ProGuard的Optimizer.java的相关片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
if (methodInliningUnique) {
// Inline methods that are only invoked once.
programClassPool.classesAccept(
new AllMethodVisitor(
new AllAttributeVisitor(
new MethodInliner(configuration.microEdition,
configuration.allowAccessModification,
true,
methodInliningUniqueCounter))));
}
if (methodInliningShort) {
// Inline short methods.
programClassPool.classesAccept(
new AllMethodVisitor(
new AllAttributeVisitor(
new MethodInliner(configuration.microEdition,
configuration.allowAccessModification,
false,
methodInliningShortCounter))));
}

通过注释可以看出,如果只被调用一次或者足够小的函数,都可能被内联。深入分析代码,我们发现确实如此,只被调用了一次的私有函数、只有一行函数体的函数(比如get、set函数等)都极可能内联。 其实仔细思考下,那些可能被内联的只有一行函数体的函数,真的有被插件处理的必要吗?别说一行代码的函数出问题的可能性小,就算出问题了也可以通过patch内联它的那个函数来解决问题,或者patch这一行代码调用的那个函数。只调用了一次的函数其实是一样的。所以通过分析,这样的函数其实是可以不被插件处理的。那么有了这个认识,我们对插件做了处理函数的判断,跳过被ProGuard内联可能性比较大的函数。通过对打出来apk中的dex做分析,发现优化后的插件还是影响了内联效果,不过只导致方法数增加了不到1000个,所以算是临时简单的解决了这个问题。

补丁问题

要制作出补丁,我们可能会面临如下两个问题: 1. 如何解决混淆问题? 2. 被补的函数中使用了super相关的调用怎么办?

混淆问题

混淆的问题比较好处理。先针对混淆前的代码生成patch.class,然后利用生成release包时对应的mapping文件中的class的映射关系,对patch.class做字符串上的处理,让它使用线上运行环境中混淆的class。

super相关的调用

比如某个Activity的onCreate方法中需要调用super.onCreate,而现在这个bad.Class的badMethod就是这个Activity的onCreate方法,那么在patched.class的patchedMethod中如何通过这个Activity的对象,调用它父类的onCreate方法呢?通过分析Instant Run对这个问题的处理,发现它是在每个class中都添加了一个代理函数,专门来处理super的问题的。为每个class都增加一个函数无疑会增加总的方法数,这样做肯定会遇到65536这个问题。所以直接使用Instant Run的做法显然是不可取的。 在Java中super是个关键字,也无法通过别的对象来访问到。看来,想直接在patched.java代码中通过Activity的对象调用到它父类的onCreate方法有点不太可能了。不过通过对class文件做分析,发现普通的函数调用是使用JVM指令集的invokevirtual指令,而super.onCreate的调用使用的是invokesuper指令。那是不是将class文件中这个调用的指令改为invokesuper就好了?

看如下的例子: 产品代码SuperClass.java:

1
2
3
4
5
6
7
8
9
public class SuperClass {
String uuid;
public void setUuid(String id) {
uuid = id;
}
public void thisIsSuper() {
Log.d("SuperClass", "thisIsSuper "+uuid);
}
}

产品代码TestSuperClass.java:

1
2
3
4
5
6
7
8
9
10
11
public class TestSuperClass extends SuperClass{
String subUuid;
public void setSubUuid(String id) {
subUuid = id;
}

@Override
public void thisIsSuper() {
Log.d("TestSuperClass", "thisIsSuper no call");
}
}

TestSuperPatch.java是DexClassLoader将要加载的代码:

1
2
3
4
5
6
7
8
9
public class TestSuperPatch {
public static void testSuperCall() {
TestSuperClass testSuperClass = new TestSuperClass();
String t = UUID.randomUUID().toString();
Log.d("TestSuperPatch", "UUID " + t);
testSuperClass.setUuid(t);
testSuperClass.thisIsSuper();
}
}

对TestSuperPatch.class的testSuperClass.thisIsSuper()调用做invokesuper的替换,并且将invokesuper的调用作用在testSuperClass这个对象上,然后加载运行:

1
Caused by: java.lang.NoSuchMethodError: No super method thisIsSuper()V in class Lcom/meituan/sample/TestSuperClass; or its super classes (declaration of 'com.meituan.sample.TestSuperClass' appears in /data/app/com.meituan.robust.sample-3/base.apk)

报错信息说在TestSuperClass和TestSuperClass的父类中没有找到thisIsSuper()V函数!但是实际上TestSuperClass和父类中是存在thisIsSuper()V函数的,而且通过apk反编译看也确实存在的,那怎么就找不到呢?分析invokesuper指令的实现,发现系统会在执行指令所在的class的父类中去找需要调用的方法,所以要将TestSuperPatch跟TestSuperClass一样作为SuperClass的子类。修改如下:

1
2
3
public class TestSuperPatch extends SuperClass {
...
}

然后再做一次尝试:

1
2
08-11 09:12:03.012 1787-1787/? D/TestSuperPatch: UUID c5216480-5c3a-4990-896d-58c3696170c5
08-11 09:12:03.012 1787-1787/? D/SuperClass: thisIsSuper c5216480-5c3a-4990-896d-58c3696170c5

看一下testSuperCall的实现,将UUID.randomUUID().toString()的结果,通过setUuid赋值给了testSuperClass这个对象的父类的uuid字段。从日志可以看出,对testSuperClass.thisIsSuper处理后,确实是调用到了testSuperClass这个对象的super的thisIsSuper函数。OK,super的问题看来解决了,而且这种方式不会增加方法数。

参考资料

1.Robust源码
2.官方原理:Android热更新方案Robust
3.热修复之仿Robust实现
4.Android-美团Robust热修复接入实践问记录

Kotlin知识点总结

发表于 2020-04-15 | 分类于 Android知识点

Kotlin 是一种在 Java 虚拟机上运行的静态类型编程语言 ,由 JetBrains 开发。Kotlin可以编译成Java字节码,也可以编译成JavaScript,方便在没有JVM的设备上运行。

设计目标

创建一种兼容Java的语言

让它比Java更安全,能够静态检测常见的陷阱。如:引用空指针

让它比Java更简洁,通过支持variable type inference,higher-order functions (closures),extension functions,mixins and first-class delegation等实现。

Kotlin,类似 Xtend 一样,旨在提供一种更好的 Java 而非重建整个新平台。这两种语言都向下编译为字节码(虽然 Xtend 是首先转换成相应的 Java 代码,再让 Java 编译器完成繁重的工作),而且两者都引入了函数和扩展函数(在某个有限范围内静态地增加一个新方法到某个已有类型的能力)。Xtend 是基于 Eclipse 的,而 Kotlin 是基于 IntelliJ 的,两者都提供无界面构建。

内联函数inline

调用某个方法实际上将程序执行顺序转移到该方法所存放在内存中某个地址,将方法的程序内容执行完后,再返回到转去执行该方法前的地方。这种转移操作要求在转去前要保护现场并记忆执行的地址,转回后先要恢复现场,并按原来保存地址继续执行。也就是通常说的压栈和出栈。因此,函数调用要有一定的时间和空间方面的开销。那么对于那些函数体代码不是很大,又频繁调用的函数来说,这个时间和空间的消耗会很大。 因此对于这种内容较短却又反复使用的方法我们可以通过使用内联函数来提升运行效率。

java中final关键字只是告诉编译器,在编译的时候考虑性能的提升,可以将final函数视为内联函数。但最后编译器会怎么处理,编译器会分析将final函数处理为内联和不处理为内联的性能比较了。(和垃圾处理机制类似,程序员只有建议权而没有决定权)

inline 的工作原理就是将内联函数的函数体复制到调用处实现内联。

inline 修饰符影响函数本身和传给它的 lambda 表达式:所有这些都将内联到调用处。

内联可能导致生成的代码增加;不过如果我们使用得当(即避免内联过大函数),性能上会有所提升,尤其是在循环中的“超多态(megamorphic)”调用处。

reified: 普通函数(非内联函数),不能包含具体化类型参数;若一个类型没有运行时表示(run-time representation)(如非具体化类型参数(non-reified type parameter)或虚拟类型,比如“Nothing”)不能作为一个具体化类型参数的实参。

公有 API 内联函数体内不允许使用非公有声明。

在Kotlin中对Java中的一些的接口的回调做了一些优化,可以使用一个lambda函数来代替。可以简化写一些不必要的嵌套回调方法。但是需要注意:在lambda表达式,只支持单抽象方法模型,也就是说设计的接口里面只有一个抽象的方法,才符合lambda表达式的规则,多个回调方法不支持。

Let

@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R = block(this)

let函数适用的场景
场景一: 最常用的场景就是使用let函数处理需要针对一个可null的对象统一做判空处理。
场景二: 然后就是需要去明确一个变量所处特定的作用域范围内可以使用

With

@kotlin.internal.InlineOnly
public inline fun <T, R> with(receiver: T, block: T.() -> R): R = receiver.block()

with函数的适用的场景
适用于调用同一个类的多个方法时,可以省去类名重复,直接调用类的方法即可,经常用于Android中RecyclerView中onBinderViewHolder中,数据model的属性映射到UI上

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//java实现
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
ArticleSnippet item = getItem(position);
if (item == null) {
return;
}
holder.tvNewsTitle.setText(StringUtils.trimToEmpty(item.titleEn));
holder.tvNewsSummary.setText(StringUtils.trimToEmpty(item.summary));
String gradeInfo = "难度:" + item.gradeInfo;
String wordCount = "单词数:" + item.length;
String reviewNum = "读后感:" + item.numReviews;
String extraInfo = gradeInfo + " | " + wordCount + " | " + reviewNum;
holder.tvExtraInfo.setText(extraInfo);
...
}

//kotlin实现
override fun onBindViewHolder(holder: ViewHolder, position: Int){
val item = getItem(position)?: return

with(item){
holder.tvNewsTitle.text = StringUtils.trimToEmpty(titleEn)
holder.tvNewsSummary.text = StringUtils.trimToEmpty(summary)
holder.tvExtraInf.text = "难度:$gradeInfo | 单词数:$length | 读后感: $numReviews"
...
}
}

Run

@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R = block()

适用于let,with函数任何场景。因为run函数是let,with两个函数结合体,准确来说它弥补了let函数在函数体内必须使用it参数替代对象,在run函数中可以像with函数一样可以省略,直接访问实例的公有属性和方法,另一方面它弥补了with函数传入对象判空问题,在run函数中可以像let函数一样做判空处理

示例:

1
2
3
4
5
6
7
8
9
10
11
//借助上个例子kotlin代码,使用run函数后的优化
override fun onBindViewHolder(holder: ViewHolder, position: Int){

getItem(position)?.run{
holder.tvNewsTitle.text = StringUtils.trimToEmpty(titleEn)
holder.tvNewsSummary.text = StringUtils.trimToEmpty(summary)
holder.tvExtraInf = "难度:$gradeInfo | 单词数:$length | 读后感: $numReviews"
...
}

}

Apply

@kotlin.internal.InlineOnly
public inline fun T.apply(block: T.() -> Unit): T { block(); return this }

从结构上来看apply函数和run函数很像,唯一不同点就是它们各自返回的值不一样,run函数是以闭包形式返回最后一行代码的值,而apply函数的返回的是传入对象的本身。apply一般用于一个对象实例初始化的时候,需要对对象中的属性进行赋值。或者动态inflate出一个XML的View的时候需要给View绑定数据也会用到,这种情景非常常见。特别是在我们开发中会有一些数据model向View model转化实例化的过程中需要用到。

示例:

1
2
3
4
5
6
7
8
mSheetDialogView = View.inflate(activity, R.layout.biz_exam_plan_layout_sheet_inner, null).apply{
course_comment_tv_label.paint.isFakeBoldText = true
course_comment_tv_score.paint.isFakeBoldText = true
course_comment_tv_cancel.paint.isFakeBoldText = true
course_comment_tv_confirm.paint.isFakeBoldText = true
course_comment_seek_bar.max = 10
course_comment_seek_bar.progress = 0
}

多层级判空示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mSectionMetaData?.apply{

//mSectionMetaData不为空的时候操作mSectionMetaData

}?.questionnaire?.apply{

//questionnaire不为空的时候操作questionnaire

}?.section?.apply{

//section不为空的时候操作section

}?.sectionArticle?.apply{

//sectionArticle不为空的时候操作sectionArticle

}

Also

@kotlin.internal.InlineOnly
@SinceKotlin(“1.1”)
public inline fun T.also(block: (T) -> Unit): T { block(this); return this }

适用于let函数的任何场景,also函数和let很像,只是唯一的不同点就是let函数最后的返回值是最后一行的返回值,而also函数的返回值是返回当前的这个对象。一般可用于多个扩展函数链式调用

协程Coroutine

协程就像非常轻量级的线程,是运行在单线程中的并发程序。正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。

线程是由系统调度的,线程切换或线程阻塞的开销都比较大(涉及到同步锁;涉及到线程阻塞状态和可运行状态之间的切换;涉及到线程的创建及上下文的切换)。而协程依赖于线程,但协程挂起时不需要阻塞线程,几乎是无代价的,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。因此,协程的开销远远小于线程的开销。

对于多线程应用,CPU通过切片的方式来切换线程间的执行,线程切换时需要耗时(保存状态,下次继续)。协程,则只使用一个线程,在一个线程中规定某个代码块执行顺序。协程能保留上一次调用时的状态,不需要像线程一样用回调函数,所以性能上会有提升。缺点是本质是个单线程,不能利用到单个CPU的多个核。

优势

  • 因为在同一个线程里,协程之间的切换不涉及线程上下文的切换和线程状态的改变,不存在资源、数据并发,所以不用加锁,只需要判断状态就OK,所以执行效率比多线程高很多

  • 协程是非阻塞式的(也有阻塞API),一个协程在进入阻塞后不会阻塞当前线程,当前线程会去执行其他协程任务

协程的切换,是通过yield API 让协程在空闲时(比如等待io,网络数据未到达)放弃执行权,然后在合适的时机再通过resume API 唤醒协程继续运行。协程一旦开始运行就不会结束,直到遇到yield交出执行权。Yield、resume 这一对 API 可以非常便捷的实现异步,这可是目前所有高级语法孜孜不倦追求的

suspend 关键字

协程天然亲近方法,协程表现为标记、切换方法、代码段,协程里使用 suspend 关键字修饰方法,既该方法可以被协程挂起,没用suspend修饰的方法不能参与协程任务,suspend修饰的方法只能在协程中只能与另一个suspend修饰的方法交流

一个协程内有多个 suspend 修饰的方法顺序书写时,代码也是顺序运行的,为什么,suspend 函数会将整个协程挂起,而不仅仅是这个 suspend 函数。

1
2
3
4
5
6
7
8
9
10
11
suspend fun requestToken(): Token { ... }   // 挂起函数
suspend fun createPost(token: Token, item: Item): Post { ... } // 挂起函数

fun postItem(item: Item) {
GlobalScope.launch { // 创建一个新协程
val token = requestToken()
val post = createPost(token, item)
processPost(post)
// 需要异常处理,直接加上 try/catch 语句即可
}
}

创建协程

kotlin 中 GlobalScope 类提供了几个构造函数:

  • launch - 创建协程
  • async - 创建带返回值的协程,返回的是 Deferred 类
  • withContext - 不创建新的协程,在指定协程上运行代码块
  • runBlocking - 不是 GlobalScope 的 API,可以独立使用,区别是 runBlocking 里面的 delay 会阻塞线程,而 launch 创建的不会

kotlin 在 1.3 之后要求协程必须由 CoroutineScope 创建,CoroutineScope 不阻塞当前线程,在后台创建一个新协程,也可以指定协程调度器。CoroutineScope 并不运行协程,它只是确保您不会失去对协程的追踪。CoroutineScope 会跟踪所有协程,并且可以取消由它所启动的所有协程。比如 CoroutineScope.launch{} 可以看成 new Coroutine

launch

launch 函数定义:

1
2
3
4
5
6
7
8
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job

//调用
GlobalScope.launch(Dispatchers.Unconfined) {...}

我们需要关心的是 launch 的3个参数和返回值 Job:

1.CoroutineContext - 可以理解为协程的上下文,在这里我们可以设置 CoroutineDispatcher 协程运行的线程调度器,有 4种线程模式:

Dispatchers.Default - 使用共享的后台线程池
Dispatchers.IO - 用于IO操作的协程
Dispatchers.Main - 主线程
Dispatchers.Unconfined - 没指定,就是在当前线程(用于不消耗CPU时间的任务以及不更新UI的协程)
用newSingleThreadContext创建的调度器:为协和的运行启动了一个线程(一个专用的纯种是一种非常昂贵的资源)

不写的话就是 Dispatchers.Default 模式的,或者我们可以自己创建协程上下文,也就是线程池,newSingleThreadContext 单线程,newFixedThreadPoolContext 线程池,具体的可以点进去看看,这2个都是方法

1
2
val singleThreadContext = newSingleThreadContext("aa")
GlobalScope.launch(singleThreadContext) { ... }

2.CoroutineStart - 启动模式,默认是DEAFAULT,也就是创建就启动;还有一个是LAZY,意思是等你需要它的时候,再调用启动

DEAFAULT - 模式模式,不写就是默认
ATOMIC -
UNDISPATCHED
LAZY - 懒加载模式,你需要它的时候,再调用启动,看这个例子

1
2
3
4
5
6
7
var job:Job = GlobalScope.launch( start = CoroutineStart.LAZY ){
Log.d("AA", "协程开始运行,时间: " + System.currentTimeMillis())
}

Thread.sleep( 1000L )
// 手动启动协程
job.start()

3.block - 闭包方法体,定义协程内需要执行的操作

Job - 协程构建函数的返回值,可以把 Job 看成协程对象本身,协程的操作方法都在 Job 身上了
job.start() - 启动协程,除了 lazy 模式,协程都不需要手动启动
job.join() - 等待协程执行完毕
job.cancel() - 取消一个协程
job.cancelAndJoin() - 等待协程执行完毕然后再取消

async

async 同 launch 唯一的区别就是 async 是有返回值的,看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
GlobalScope.launch(Dispatchers.Unconfined) {
val deferred = GlobalScope.async{
delay(1000L)
Log.d("AA","This is async ")
return@async "taonce"
}

Log.d("AA","协程 other start")
val result = deferred.await()
Log.d("AA","async result is $result")
Log.d("AA","协程 other end ")
}

Log.d("AA", "主线程位于协程之后的代码执行,时间: ${System.currentTimeMillis()}")

//运行结果
This is async
协程 other start
主线程位于协程之后的代码执行,时间: 1553866185250
async result is taonce
协程 other end

async 返回的是 Deferred 类型,Deferred 继承自 Job 接口,Job有的它都有,增加了一个方法 await ,这个方法接收的是 async 闭包中返回的值,async 的特点是不会阻塞当前线程,但会阻塞所在协程,也就是挂起。

但是注意啊,async 并不会阻塞线程,只是阻塞锁调用的协程

runBlocking

runBlocking 和 launch 区别的地方就是 runBlocking 的 delay 方法是可以阻塞当前的线程的,和Thread.sleep() 一样,看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fun main(args: Array<String>) {
runBlocking {
// 阻塞1s
delay(1000L)
println("This is a coroutines ${TimeUtil.getTimeDetail()}")
}

// 阻塞2s
Thread.sleep(2000L)
println("main end ${TimeUtil.getTimeDetail()}")
}

~~~~~~~~~~~~~~log~~~~~~~~~~~~~~~~
This is a coroutines 11:00:51
main end 11:00:53

runBlocking 通常的用法是用来桥接普通阻塞代码和挂起风格的非阻塞代码,在 runBlocking 闭包里面可以启动另外的协程,协程里面是可以嵌套启动别的协程的。

协程的挂起和恢复

1.协程执行时, 协程和协程,协程和线程内代码是顺序运行的
最简单的协程运行模式,不涉及挂起时,谁写在前面谁先运行,后面的等前面的协程运行完之后再运行。涉及到挂起时,前面的协程挂起了,那么线程不会空闲,而是继续运行下一个协程,而前面挂起的那个协程在挂起结速后不会马上运行,而是等待当前正在运行的协程运行完毕后再去执行

2.协程挂起时,就不会执行了,而是等待挂起完成且线程空闲时才能继续执行
一个协程内有多个 suspend 修饰的方法顺序书写时,代码也是顺序运行的,为什么,suspend 函数会将整个协程挂起,而不仅仅是这个 suspend 函数。

suspend 修饰的方法挂起的是协程本身,而非该方法,注意这点,看下面的代码体会下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
suspend fun getToken(): String {
delay(300)
Log.d("AA", "getToken 开始执行,时间: ${System.currentTimeMillis()}")
return "ask"
}

suspend fun getResponse(token: String): String {
delay(100)
Log.d("AA", "getResponse 开始执行,时间: ${System.currentTimeMillis()}")
return "response"
}

fun setText(response: String) {
Log.d("AA", "setText 执行,时间: ${System.currentTimeMillis()}")
}

// 运行代码
GlobalScope.launch(Dispatchers.Main) {
Log.d("AA", "协程 开始执行,时间: ${System.currentTimeMillis()}")
val token = getToken()
val response = getResponse(token)
setText(response)
}

//运行结果
协程 开始执行,时间: 1553848676780
getToken 开始执行,时间: 1553848676781
getResponse 开始执行,时间: 1553848677088
setText 执行,时间: 1553848677190

在 getToken 方法将协程挂起时,getResponse 函数永远不会运行,只有等 getToken 挂起结速将协程恢复时才会运行。

多协程间 suspend 函数运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
GlobalScope.launch(Dispatchers.Unconfined){
var token = GlobalScope.async(Dispatchers.Unconfined) {
return@async getToken()
}.await()

var response = GlobalScope.async(Dispatchers.Unconfined) {
return@async getResponse(token)
}.await()

setText(response)
}

//运行结果
getToken 开始执行,时间: 1553848676781
getResponse 开始执行,时间: 1553848677088
setText 执行,时间: 1553848677190

注意我外面要包裹一层 GlobalScope.launch,要不运行不了。这里我们搞了2个协程出来,但是我们在这里使用了await,这样就会阻塞外部协程,所以代码还是按顺序执行的。这样适用于多个同级 IO 操作的情况,这样写比 rxjava 要省事不少。

协程挂起后再恢复时在哪个线程运行:
哪个线程恢复的协程,协程就运行在哪个线程中。
注意协程内部,若是在前面有代码切换了线程,后面的代码若是没有指定线程,那么就是运行在这个切换到的线程上的。
我们最好给异步任务在外面套一个协程,这样我们可以挂起整个异步任务,然后给每段代码指定运行线程调度器,这样省的因为协程内部挂起恢复变更线程而带来的问题。
非 Dispatchers.Main 调度器的协程,会在协程挂起后把协程当做一个任务 DelayedResumeTask 放到默认线程池 DefaultExecutor 队列的最后,在延迟的时间到达才会执行恢复协程任务。虽然多个协程之间可能不是在同一个线程上运行的,但是协程内部的机制可以保证我们书写的协程是按照我们指定的顺序或者逻辑自行。

delay、yield 区别

delay 和 yield 方法是协程内部的操作,可以挂起协程,区别是 delay 是挂起协程并经过执行时间恢复协程,当线程空闲时就会运行协程;yield 是挂起协程,让协程放弃本次 cpu 执行机会让给别的协程,当线程空闲时再次运行协程。我们只要使用 kotlin 提供的协程上下文类型,线程池是有多个线程的,再次执行的机会很快就会有的。

除了 main 类型,协程在挂起后都会封装成任务放到协程默认线程池的任务队列里去,有延迟时间的在时间过后会放到队列里去,没有延迟时间的直接放到队列里去

协程的取消

我们在创建协程过后可以接受一个 Job 类型的返回值,我们操作 job 可以取消协程任务,job.cancel 就可以了。

协程的取消有些特质,因为协程内部可以在创建协程的,这样的协程组织关系可以称为父协程,子协程:

  • 父协程手动调用 cancel() 或者异常结束,会立即取消它的所有子协程(而抛出CancellationException却会当作正常的协程结束不会取消其父协程)
  • 父协程必须等待所有子协程完成(处于完成或者取消状态)才能完成
  • 子协程抛出未捕获的异常时,默认情况下会取消其父协程

现在问题来了,在 Thread 中我们想关闭线程有时候也不是掉个方法就行的,需要我们自行在线程中判断线程是不是已经结束了。在协程中一样,cancel 方法只是修改了协程的状态,在协程自身的方法比如 realy,yield 等中会判断协程的状态从而结束协程,但是若是在协程我们没有用这几个方法怎么办,比如都是逻辑代码,这时就要我们自己手动判断了,使用 job.isActive ,isActive 是个标记,用来检查协程状态

原理

每一个挂起点和初始挂起点对应的 Continuation 都会转化为一种状态,协程恢复只是跳转到下一种状态中。挂起函数将执行过程分为多个 Continuation 片段,并且利用状态机的方式保证各个片段是顺序执行的。

await()挂起函数恢复协程的原理是,将 launch 协程封装为 ResumeAwaitOnCompletion 作为 handler 节点添加到 aynsc 协程的 state.list,然后在 async 协程完成时会通知 handler 节点调用 launch 协程的 resume(result) 方法将结果传给 launch 协程,并恢复 launch 协程继续执行 await 挂起点之后的逻辑。

协程其实有三层包装。常用的launch和async返回的Job、Deferred,里面封装了协程状态,提供了取消协程接口,而它们的实例都是继承自AbstractCoroutine,它是协程的第一层包装。第二层包装是编译器生成的SuspendLambda的子类,封装了协程的真正运算逻辑,继承自BaseContinuationImpl,其中completion属性就是协程的第一层包装。第三层包装是线程调度时的DispatchedContinuation,封装了线程调度逻辑,包含了协程的第二层包装。三层包装都实现了Continuation接口,通过代理模式将协程的各层包装组合在一起,每层负责不同的功能。

下面是协程运行的流程图:

总结:协程就是一段可以挂起和恢复执行的运算逻辑,而协程的挂起是通过挂起函数实现的,挂起函数用状态机的方式用挂起点将协程的运算逻辑拆分为不同的片段,每次运行协程执行的不同的逻辑片段。所以协程有两个很大的好处:一是简化异步编程,支持异步返回;而是挂起不阻塞线程,提供线程利用率。

协程的并发

协程就是可以挂起和恢复执行的运算逻辑,挂起函数用状态机的方式用挂起点将协程的运算逻辑拆分为不同的片段,每次运行协程执行的不同的逻辑片段。所以协程在运行时只是线程中的一块代码,线程的并发处理方式都可以用在协程上。不过协程还提供两种特有的方式,一是不阻塞线程的互斥锁Mutex,一是通过 ThreadLocal 实现的协程局部数据。

Mutex 协程互斥锁

线程中锁都是阻塞式,在没有获取锁时无法执行其他逻辑,而协程可以通过挂起函数解决这个,没有获取锁就挂起协程,获取后再恢复协程,协程挂起时线程并没有阻塞可以执行其他逻辑。这种互斥锁就是 Mutex,它与 synchronized 关键字有些类似,还提供了 withLock 扩展函数,替代常用的 mutex.lock; try {…} finally { mutex.unlock() }

1
2
3
4
5
6
7
8
9
10
11
12
fun main(args: Array<String>) = runBlocking<Unit> {
val mutex = Mutex()
var counter = 0
repeat(10000) {
GlobalScope.launch {
mutex.withLock {
counter ++
}
}
}
println("The final count is $counter")
}

Mutex的使用比较简单,不过需要注意的是多个协程竞争的应该是同一个Mutex互斥锁。

ThreadLocal

线程中可以使用ThreadLocal作为线程局部数据,每个线程中的数据都是独立的。协程中可以通过ThreadLocal.asContextElement()扩展函数实现协程局部数据,每次协程切换会恢复之前的值。先看下面的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
fun main(args: Array<String>) = runBlocking<Unit> {
val threadLocal = ThreadLocal<String>().apply { set("Init") }
printlnValue(threadLocal)
val job = GlobalScope.launch(threadLocal.asContextElement("launch")) {
printlnValue(threadLocal)
threadLocal.set("launch changed")
printlnValue(threadLocal)
yield()
printlnValue(threadLocal)
}
job.join()
printlnValue(threadLocal)
}

private fun printlnValue(threadLocal: ThreadLocal<String>) {
println("${Thread.currentThread()} thread local value: ${threadLocal.get()}")
}

//输出如下:
Thread[main,5,main] thread local value: Init
Thread[DefaultDispatcher-worker-1,5,main] thread local value: launch
Thread[DefaultDispatcher-worker-1,5,main] thread local value: launch changed
Thread[DefaultDispatcher-worker-2,5,main] thread local value: launch
Thread[main,5,main] thread local value: Init

上面的输出有个疑问的地方,为什么执行yield()挂起函数后 threadLocal 的值不是launch changed而变回了launch?
最重要的牢记它的原理:启动和恢复时保存ThreadLocal在当前线程的值,并修改为 value,挂起和结束时修改当前线程ThreadLocal的值为之前保存的值。

委托by

委托模式是软件设计模式中的一项基本技巧。在委托模式中,有两个对象参与处理同一个请求,接受请求的对象将请求委托给另一个对象来处理。Kotlin 直接支持委托模式,更加优雅,简洁。Kotlin 通过关键字 by 实现委托。

类委托

类的委托即一个类中定义的方法实际是调用另一个类的对象的方法来实现的。

以下实例中派生类 Derived 继承了接口 Base 所有方法,并且委托一个传入的 Base 类的对象来执行这些方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 创建接口
interface Base {
fun print()
}

// 实现此接口的被委托的类
class BaseImpl(val x: Int) : Base {
override fun print() { print(x) }
}

// 通过关键字 by 建立委托类
class Derived(b: Base) : Base by b

fun main(args: Array<String>) {
val b = BaseImpl(10)
Derived(b).print() // 输出 10
}

在 Derived 声明中,by 子句表示,将 b 保存在 Derived 的对象实例内部,而且编译器将会生成继承自 Base 接口的所有方法, 并将调用转发给 b。

属性委托

属性委托指的是一个类的某个属性值不是在类中直接进行定义,而是将其托付给一个代理类,从而实现对该类的属性统一管理。

属性委托语法格式:

val/var <属性名>: <类型> by <表达式>

  • var/val:属性类型(可变/只读)
  • 属性名:属性名称
  • 类型:属性的数据类型
  • 表达式:委托代理类

by 关键字之后的表达式就是委托, 属性的 get() 方法(以及set() 方法)将被委托给这个对象的 getValue() 和 setValue() 方法。属性委托不必实现任何接口, 但必须提供 getValue() 函数(对于 var属性,还需要 setValue() 函数)。

该类需要包含 getValue() 方法和 setValue() 方法,且参数 thisRef 为进行委托的类的对象,prop 为进行委托的属性的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import kotlin.reflect.KProperty
// 定义包含属性委托的类
class Example {
var p: String by Delegate()
}

// 委托的类
class Delegate {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return "$thisRef, 这里委托了 ${property.name} 属性"
}

operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
println("$thisRef 的 ${property.name} 属性赋值为 $value")
}
}
fun main(args: Array<String>) {
val e = Example()
println(e.p) // 访问该属性,调用 getValue() 函数

e.p = "Runoob" // 调用 setValue() 函数
println(e.p)
}

输出结果为:
Example@433c675d, 这里委托了 p 属性
Example@433c675d 的 p 属性赋值为 Runoob
Example@433c675d, 这里委托了 p 属性

延迟属性 Lazy

lazy() 是一个函数, 接受一个 Lambda 表达式作为参数, 返回一个 Lazy 实例的函数,返回的实例可以作为实现延迟属性的委托: 第一次调用 get() 会执行已传递给 lazy() 的 lamda 表达式并记录结果, 后续调用 get() 只是返回记录的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
val lazyValue: String by lazy {
println("computed!") // 第一次调用输出,第二次调用不执行
"Hello"
}

fun main(args: Array<String>) {
println(lazyValue) // 第一次执行,执行两次输出表达式
println(lazyValue) // 第二次执行,只输出返回值
}

执行输出结果:
computed!
Hello
Hello

你可以将局部变量声明为委托属性。 例如,你可以使一个局部变量惰性初始化:

1
2
3
4
5
6
7
fun example(computeFoo: () -> Foo) {
val memoizedFoo by lazy(computeFoo)

if (someCondition && memoizedFoo.isValid()) {
memoizedFoo.doSomething()
}
}

memoizedFoo 变量只会在第一次访问时计算。 如果 someCondition 失败,那么该变量根本不会计算。

提供委托provideDelegate

通过定义 provideDelegate 操作符,可以扩展创建属性实现所委托对象的逻辑。 如果 by 右侧所使用的对象将 provideDelegate 定义为成员或扩展函数,那么会调用该函数来 创建属性委托实例。

provideDelegate 的一个可能的使用场景是在创建属性时(而不仅在其 getter 或 setter 中)检查属性一致性。

例如,如果要在绑定之前检查属性名称,可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class ResourceLoader<T>(id: ResourceID<T>) {
operator fun provideDelegate(
thisRef: MyUI,
prop: KProperty<*>
): ReadOnlyProperty<MyUI, T> {
checkProperty(thisRef, prop.name)
// 创建委托
}

private fun checkProperty(thisRef: MyUI, name: String) { …… }
}

fun <T> bindResource(id: ResourceID<T>): ResourceLoader<T> { …… }

class MyUI {
val image by bindResource(ResourceID.image_id)
val text by bindResource(ResourceID.text_id)
}

provideDelegate 的参数与 getValue 相同:

thisRef —— 必须与 属性所有者 类型(对于扩展属性——指被扩展的类型)相同或者是它的超类型
property —— 必须是类型 KProperty<*> 或其超类型。

在创建 MyUI 实例期间,为每个属性调用 provideDelegate 方法,并立即执行必要的验证。

如果没有这种拦截属性与其委托之间的绑定的能力,为了实现相同的功能, 你必须显式传递属性名,这不是很方便:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 检查属性名称而不使用“provideDelegate”功能
class MyUI {
val image by bindResource(ResourceID.image_id, "image")
val text by bindResource(ResourceID.text_id, "text")
}

fun <T> MyUI.bindResource(
id: ResourceID<T>,
propertyName: String
): ReadOnlyProperty<MyUI, T> {
checkProperty(this, propertyName)
// 创建委托
}

在生成的代码中,会调用 provideDelegate 方法来初始化辅助的 prop$delegate 属性。 比较对于属性声明 val prop: Type by MyDelegate() 生成的代码与 上面(当 provideDelegate 方法不存在时)生成的代码:

1
2
3
4
5
6
7
8
9
10
11
12
class C {
var prop: Type by MyDelegate()
}

// 这段代码是当“provideDelegate”功能可用时
// 由编译器生成的代码:
class C {
// 调用“provideDelegate”来创建额外的“delegate”属性
private val prop$delegate = MyDelegate().provideDelegate(this, this::prop)
val prop: Type
get() = prop$delegate.getValue(this, this::prop)
}

请注意,provideDelegate 方法只影响辅助属性的创建,并不会影响为 getter 或 setter 生成的代码。

方法

Unit

在定义的时候忽略返回值等于是隐式声明函数的返回值是空。在Kotlin中,这种隐式返回的类型称之为:Unit。这个Unit类型的作用类似Java语言中的void类型。Unit是一种只有一个值——Unit的类型。这个值不需要显式返回。

闭包

Kotlin中的闭包是一个功能性自包含模块,可以在代码中被当做参数传递或者直接使用。函数里面声明函数,函数里面返回函数,就是闭包。

闭包就是一个代码块,用“{ }”包起来。Kotlin语言中有三种闭包形式:全局函数、自嵌套函数、匿名函数体。下面举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fun main(args: Array<String>) {
// 执行test闭包的内容
test
}

// 定义一个比较测试闭包
val test = if (5 > 3) {
println("yes")
} else {
println("no")
}

// 原语句等价于 val test = println("yes") ,test是一个Unit对象,而Unit是个object,也就是个单例类,所以,上面的代码等价于
val test = Unit
object Unit {
init {
println("yes")
}
}
//因为单例对象只会初始化一次,所以不管定义多少次只会打印一次yes。
//实际上,val test = {println("yes")} ,大括号包裹的是另外一种,等价于 val test = fun(){ println("yes") } 这种才是函数的调用形式,使用test()

闭包的用途:能够读取其他函数的内部变量,另一个用处就是让这些变量的值始终保持在内存中(在内存中维持一个变量)。

注意:闭包会使得函数中的变量都被保存在内存中,内存消耗很大.闭包赋值给变量后,待变量销毁,内存释放

广义上来说,在Kotlin语言之中,函数、条件语句、控制流语句、花括号逻辑块、Lambda表达式都可以称之为闭包,但通常情况下,我们所指的闭包都是在说Lambda表达式。

双冒号 ::

Kotlin 中 双冒号操作符 表示把一个方法当做一个参数,传递到另一个方法中进行使用,通俗的来讲就是引用一个方法。

一般情况,我们调用当前类的方法 this 都是可省略的。为了防止作用域混淆 ,:: 调用的函数如果是类的成员函数或者是扩展函数,必须使用限定符,比如this。

参数默认值

1
2
3
4
5
6
7
8
9
10
11
/**
* 给一个字符串拼接前缀和后缀
* 前缀默认值:***
* 后缀默认值:###
*/
fun catString(myString: String, prefix: String = "***", suffix: String = "###"): String{
return prefix + myString + suffix
}

// 使用的时候,可以忽略带有默认值的参数不传
catString("hello")

可变个数参数(vararg)

声明可变个数形参需要用到vararg关键字,当参数传递进入函数体之后,参数在函数体内可以通过集合的形式访问。函数最多可以有一个可变个数的形参,而且它必须出现在参数列表的最后。如:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 求多个数字的和
*/
fun sumNumbers(vararg numbers : Double) : Double{
var result : Double = 0.0
for (number in numbers) {
result += number
}
return result
}

// 使用的时候,则可以传任意多个参数
sumNumbers(1.2,2.56,3.14)

嵌套函数

在结构化编程盛行的年代,嵌套函数被广泛使用,在一个函数体中定义另外一个函数体就为嵌套函数。嵌套函数默认对外界是隐藏的,但仍然可以通过它们包裹的函数调用和使用它,举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* 嵌套函数demo
*
* 比较数字numberA和数字numberB的二次幂谁大
*/
fun compare(numberA: Int, numberB: Int) : Int{
var powerB = 0

// 嵌套函数,求一个数字的二次幂
fun power(num : Int) : Int{
return num * num
}
powerB = power(numberB)

if (numberA > powerB) {
return numberA
} else {
return powerB
}
}


fun main(args: Array<String>) {
// 报错!!!
// 无法直接调用内部嵌套的函数
power()
}

参考资料

https://blog.csdn.net/Zachary_46/article/details/80446851
https://www.runoob.com/kotlin/kotlin-delegated.html
Coroutine 协程
Kotlin Coroutines(协程) 完全解析(三)

Android Jetpack

发表于 2020-04-09 | 分类于 Android知识点

Google 在2018年推出了 Android Jetpack。Jetpack 是一套库、工具和指南,可帮助开发者更轻松地编写优质应用。这些组件可帮助您遵循最佳做法、让您摆脱编写样板代码的工作并简化复杂任务,以便您将精力集中放在所需的代码上。

Android Jetpack 组件是库的集合,这些库是为协同工作而构建的,不过也可以单独采用,同时利用 Kotlin 语言功能帮助您提高工作效率。可全部使用,也可混合搭配!Jetpack 也包含了与平台 API 解除捆绑的 androidx.* 软件包库。

特点如下:

  • 加速开发:组件可以单独采用(不过这些组件是为协同工作而构建的),同时利用 Kotlin 语言功能帮助您提高工作效率。
  • 消除样板代码:Android Jetpack 可管理繁琐的 Activity(如后台任务、导航和生命周期管理),以便您可以专注于如何让自己的应用出类拔萃。
  • 构建高质量的强大应用:Android Jetpack 组件围绕现代化设计实践构建而成,具有向后兼容性,可以减少崩溃和内存泄漏。

JetPack的组成

WorkManager很强大,需要的地方可以替代以前的方案。LifeCycles也不错,扩展其他类具有关联生命周期的。还有Room数据库的框架,简单了很多。LiveData和ViewModel的结合基本上就是RxJava和RxAndroid的结合的功能了。

Navigation

Navigation是一个可简化Android导航的库和插件

Navigation是用来管理Fragment的切换,并且可以通过可视化的方式,看见App的交互流程。这完美的契合了Jake Wharton大神单Activity的建议。

优点

  • 处理Fragment的切换(上文已说过)
  • 默认情况下正确处理Fragment的前进和后退
  • 为过渡和动画提供标准化的资源
  • 实现和处理深层连接
  • 可以绑定Toolbar、BottomNavigationView和ActionBar等
  • SafeArgs(Gradle插件) 数据传递时提供类型安全性
  • ViewModel支持

三要素

  1. Navigation Graph(New XML resource): 这是一个新的资源文件,用户在可视化界面可以看出他能够到达的Destination(用户能够到达的屏幕界面),以及流程关系。

  2. NavHostFragment(Layout XML view): 当前Fragment的容器

  3. NavController(Kotlin/Java object): 导航的控制者

可能我这么解释还是有点抽象,做一个不是那么恰当的比喻,我们可以将Navigation Graph看作一个地图,NavHostFragment看作一个车,以及把NavController看作车中的方向盘,Navigation Graph中可以看出各个地点(Destination)和通往各个地点的路径,NavHostFragment可以到达地图中的各个目的地,但是决定到什么目的地还是方向盘NavController,虽然它取决于开车人(用户)。

重要属性

属性 解释
app:startDestination navigation标签: 默认的起始位置
app:destination action标签: 跳转完成到达的fragment的Id
app:popUpTo action标签: 将fragment从栈中弹出,直到某个Id的fragment
app:argType argument标签: 标签的类型
android:defaultValue argument标签: 默认值
app:navGraph fragment标签: 存放的是第二步建好导航的资源文件,也就是确定了Navigation Graph
app:defaultNavHost=”true” fragment标签: 与系统的返回按钮相关联

导航示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
btnLogin.setOnClickListener {
// 设置动画参数
val navOption = navOptions {
anim {
enter = R.anim.slide_in_right
exit = R.anim.slide_out_left
popEnter = R.anim.slide_in_left
popExit = R.anim.slide_out_right
}
}
// 参数设置
val bundle = Bundle()
bundle.putString("name","TeaOf")
findNavController().navigate(R.id.login, bundle,navOption)
}

如果不用Safe Args,action可以由Navigation.createNavigateOnClickListener(R.id.next_action, null)方式生成

LiveData

LiveData有个内部类LifecycleBoundObserver,它实现了GenericLifecycleObserver,而GenericLifecycleObserver继承了LifecycleObserver接口。当组件(Fragment、Activity)生命周期变化时会通过onStateChanged()方法回调过来。

LiveData主要涉及到的时序有三个:
在Fragment/Activity中通过LiveData.observer()添加观察者(observer()方法中的第二个参数)。
根据Fragment/Activity生命周期发生变化时,移除观察者或者通知观察者更新数据。
当调用LiveData的setValue()、postValue()方法后,通知观察者更新数据(setValue必须发生在主线程,如果当前线程是子线程可以使用postValue)。

在LiveData.observe()方法中添加了组件(实现了LifecycleOwner接口的Fragment和Activity)生命周期观察者。而这个观察者就是LifecycleBoundObserver对象.

应用及知识点:

  1. 使用ViewModel在同一个Activity中的Fragment之间共享数据:想要利用ViewModel实现Fragment之间数据共享,前提是Fragment中的FragmentActivity得相同。

  2. map是你将你的函数用于你传参的livedata的数据通过函数体中的逻辑改变,然后将结果传到下游。而switchmap,转换跟map差不多,只不过传到下游的是livedata类型。

  3. MediatorLiveData 是 LiveData 的子类,允许您合并多个 LiveData 源。只要任何原始的 LiveData 源对象发生更改,就会触发 MediatorLiveData 对象的观察者。

  4. 使用LiveData共享数据:定义一个类然后继承LiveData,并使用单例模式即可。示例如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // 登录信息
    data class LoginInfo constructor(val account:String, val pwd:String, val email:String)

    /**
    * 自定义单例LiveData
    */
    class LoginLiveData:LiveData<LoginInfo>() {

    companion object {
    private lateinit var sInstance: LoginLiveData

    @MainThread
    fun get(): LoginLiveData {
    sInstance = if (::sInstance.isInitialized) sInstance else LoginLiveData()
    return sInstance
    }
    }
    }

注意:您可以使用 observeForever(Observer) 方法来注册未关联 LifecycleOwner 对象的观察者。在这种情况下,观察者会被视为始终处于活跃状态,因此它始终会收到关于修改的通知。您可以通过调用 removeObserver(Observer) 方法来移除这些观察者。

ViewModel

ViewModel类是被设计用来以可感知生命周期的方式存储和管理 UI 相关数据,ViewModel中数据会一直存活即使 activity configuration发生变化,比如横竖屏切换的时候。

由于 ViewModel 生命周期可能长与 activity 生命周期,所以为了避免内存泄漏 Google 禁止在 ViewModel 中持有 Context 或 activity 或 view 的引用。

viewmodel初始化:

ViewModelProviders.of(activity,factory).get(MyViewModel.class)

1、初始化了ViewModelProvider内部维护了 用于创建 VM 的 Factory,和用户存放 VM 的ViewModelStore;
2、初始化了 用来生成 ViewModel 的 Factory(默认为DefaultFactory);
3、通过ViewModelStores的静态方法实例化了 HolderFragment,并实例化了ViewModelStore
4、然后是ViewModelProvider的 get 方法

Room

DataBinding

DataBinding 是google发布的一个数据绑定框架,用于降低布局和逻辑的耦合性,使代码逻辑更加清晰。大量减少 Activity 内的代码,数据能够单向或双向绑定到 layout 文件中,有助于防止内存泄漏,而且能自动进行空检测以避免空指针异常。

配置

app及对应module添加如下代码:

1
2
3
4
5
6
android {
...
dataBinding {
enabled = true
}
}

使用

1.activity中使用

1
2
3
4
5
6
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val dataBinding: ActivityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)
// 给user初始化值
dataBinding.user = User("zhangsan", "12345")
}

2.在 Fragment 中的使用

1
2
3
4
5
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val blankFragmentBinding: BlankFragmentBinding =
DataBindingUtil.inflate(inflater, R.layout.blank_fragment, container, false)
return blankFragmentBinding.root
}

3.在RecyclerView中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerViewHolder 	{
val itemMvvmBinding = DataBindingUtil.inflate<ViewDataBinding>(
LayoutInflater.from(parent.context), R.layout.item_mvvm, parent, false)
itemMvvmBinding.getRoot().setOnClickListener(this)
return RecyclerViewHolder(itemMvvmBinding)
}

override fun onBindViewHolder(holder: RecyclerViewHolder, position: Int) {
val itemMvvmBinding = holder.getBinding()
val userBean = data.get(position)
itemMvvmBinding.setUser(userBean)
itemMvvmBinding.btnUpdate.setOnClickListener(OnBtnClickListener(1, userBean))
...
// 立刻执行绑定
itemMvvmBinding.executePendingBindings()
}

单向绑定

一个简单的ViewModel 类被更新后,并不会让 UI 自动更新。而数据绑定后,我们自然会希望数据变更后 UI 会即时刷新,Observable 就是为此而生的概念。实现数据变化自动驱动 UI 刷新的方式有三种:BaseObservable、ObservableField、ObservableCollection

BaseObservable

BaseObservable 提供了 notifyChange() 和 notifyPropertyChanged() 两个方法:

  • notifyChange() 它会刷新所有的值。
  • notifyPropertyChanged() 它只会根据对应的BR的flag更新,该 BR 的生成通过注释 @Bindable 生成,可以通过 BR notify 特定属性关联的视图。

//由于kotlin的属性默认是public修饰,所以可以直接在属性上@Bindable, 如何设置了修饰符且不为public的话,则可使用@get Bindable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class UserInfo : BaseObservable() {
// 对name进行@Bindable标志,然后会生成BR.name
@Bindable
var name: String = ""
set(value) {
field = value
// 当name,发生改变时只会刷新与name相关控件的值,不会刷新其他的值
notifyPropertyChanged(BR.name)
}

@get: Bindable
var password: String = ""
set(value) {
field = value
// 当password 发生改变时,也会刷新其他属性相关的控件的值
notifyChange()
}
}

实现了 Observable 接口的类允许注册一个监听器OnPropertyChangedCallback,当可观察对象的属性更改时就会通知这个监听器。

1
2
3
4
5
6
//当中 propertyId 就用于标识特定的字段
user.addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback(){
override fun onPropertyChanged(sender: Observable, propertyId: Int) {

}
})

ObservableField

继承于 Observable 类相对来说限制有点高,且也需要进行notify 操作,因此为了简单起见可以选择使用 ObservableField。 可以理解为官方对 BaseObservable 中字段的注解和刷新等操作的封装,官方原生提供了对基本数据类型的封装,例如 ObservableBoolean、ObservableByte、ObservableChar、ObservableShort、ObservableInt、ObservableLong、ObservableFloat、ObservableDouble 以及 ObservableParcelable ,也可通过 ObservableField 泛型来申明其他类型。

1
2
3
4
data class ObservableUser(
var name: ObservableField<String>,
var password: ObservableField<String>
)

ObservableCollection

dataBinding 也提供了包装类用于替代原生的 List 和 Map,分别是 ObservableList 和 ObservableMap,当其包含的数据发生变化时,绑定的视图也会随之进行刷新

双向数据绑定

双向绑定的意思即为当数据改变时同时使视图刷新,而视图改变时也可以同时改变数据。绑定变量的方式比单向绑定多了一个等号,如:android:text=”@={user.name}”

事件绑定

严格意义上来说,事件绑定也是一种变量绑定,只不过设置的变量是回调接口而已。

事件绑定包括方法引用和监听绑定:

  • 方法引用:参数类型和返回类型要一致,参考et_pwd EditText的android:onTextChanged引用。
  • 监听绑定:相比较于方法引用,监听绑定的要求就没那么高了,我们可以使用自行定义的函数,参考et_account EditText的android:onTextChanged引用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">

<data>
<!--需要的viewModel,通过mBinding.vm=mViewMode注入-->
<variable
name="model"
type="com.joe.jetpackdemo.viewmodel.LoginModel"/>

<variable
name="activity"
type="androidx.fragment.app.FragmentActivity"/>
</data>

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">

<TextView
android:id="@+id/txt_cancel"
android:onClick="@{()-> activity.onBackPressed()}"
/>

<TextView
android:id="@+id/txt_title"
app:layout_constraintTop_toTopOf="parent"
.../>

<EditText
android:id="@+id/et_account"
android:text="@{model.n.get()}"
android:onTextChanged="@{(text, start, before, count)->model.onNameChanged(text)}"
...
/>

<EditText
android:id="@+id/et_pwd"
android:text="@{model.p.get()}"
android:onTextChanged="@{model::onPwdChanged}"
...
/>

<Button
android:id="@+id/btn_login"
android:text="Sign in"
android:onClick="@{() -> model.login()}"
android:enabled="@{(model.p.get().isEmpty()||model.n.get().isEmpty()) ? false : true}"
.../>

</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

使用类方法

首先定义一个静态方法

1
2
3
4
5
6
object StringUtils {

fun toUpperCase( str:String):String {
return str.toUpperCase();
}
}

在 data 标签中导入该工具类

1
<import type="com.github.ixiaow.sample.StringUtils" />

然后就可以像对待一般的函数一样来调用了

1
2
3
4
5
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{()->userPresenter.onUserNameClick(userInfo)}"
android:text="@{StringUtils.toUpperCase(userInfo.name)}" />

表达式

  • 运算符 + - / * %
  • 字符串连接 +
  • 逻辑与或 && ||
  • 二进制 & | ^
  • 一元 + - ! ~
  • 移位 >> >>> <<
  • 比较 == > < >= <= (Note that < needs to be escaped as <)
  • instanceof
  • Grouping ()
  • Literals - character, String, numeric, null
  • Cast
  • 方法调用
  • 域访问
  • 数组访问
  • 三元操作符

除了上述之外,Data Binding新增了空合并操作符??,例如android:text=”@{user.displayName ?? user.lastName}”,它等价于android:text=”@{user.displayName != null ? user.displayName : user.lastName}”。

@BindingMethod

如果XXXView类有成员变量borderColor,并且XXXView类有setBoderColor(int color)方法,那么在布局中我们就可以借助Data Binding直接使用app:borderColor这个属性。示例如下:

1
2
3
4
5
<android.support.v4.widget.DrawerLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:scrimColor="@{@color/scrim}"
app:drawerListener="@{fragment.drawerListener}">

还用XXXView为例,它有成员变量borderColor,这次设置borderColor的方法是setBColor(总有程序员乱写方法名~),强行用app:borderColor显然是行不通的,可以这样用的前提是必须有setBoderColor(int color)方法,显然setBColor不匹配,但我们可以通过BindingMethods注解实现app:borderColor的使用,代码如下:

1
2
3
4
5
@BindingMethods(value = [
BindingMethod(
type = 包名.XXXView::class,
attribute = "app:borderColor",
method = "setBColor")])

@BindingAdapter

  • 用于标记修饰方法,方法必须为公共静态方法
  • 方法的第一个参数的类型必须为View类型,不然报错
  • 用来自定义view的任意属性

dataBinding 提供了 BindingAdapter 这个注解用于支持自定义属性,或者是修改原有属性。

示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@BindingAdapter({"url"})
public static void loadImage(ImageView view, String url) {
Log.d(TAG, "loadImage url : " + url);
}

//在 xml 文件中关联变量值,当中,bind 这个名称可以自定义
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:bind="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">

<data>
<import type="com.github.ixiaow.databindingsample.model.Image" />
<variable
name="image"
type="Image" />
</data>

<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">

<ImageView
android:id="@+id/image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_launcher_background"
bind:url="@{image.url}" />
</android.support.constraint.ConstraintLayout>
</layout>

BindingAdapter 更为强大的一点是可以覆盖 Android 原先的控件属性。例如,可以设定每一个 Button 的文本都要加上后缀:“-Button”:

1
2
3
4
5
6
7
8
9
10
@BindingAdapter("android:text")
public static void setText(Button view, String text) {
view.setText(text + "-Button");
}

<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{()->handler.onClick(image)}"
android:text='@{"改变图片Url"}'/>

@InverseBindingAdapter

  • 作用于方法,方法须为公共静态方法。
  • 方法的第一个参数必须为View类型,如TextView等
  • 用于双向绑定
  • 需要与@BindingAdapter配合使用

死循环绑定的解决方式:只处理新旧数据不一样的数据,参考源码中的例子:android.databinding.adapters.TextViewBindingAdapter。

需要注意的是,使用该语法必须要有反向绑定的方法,android原生view都是自带的,所以使用原生控件无须担心,但是自定义view的话需要我们通过InverseBindingAdapter注解类实现

@BindingConversion

dataBinding 还支持对数据进行转换,或者进行类型转换。作用于方法,被该注解标记的方法,被视为dataBinding的转换方法。方法必须为公共静态(public static)方法,且有且只能有1个参数。

与 BindingAdapter 类似,以下方法会将布局文件中所有以@{String}方式引用到的String类型变量加上后缀-conversionString

1
2
3
4
@BindingConversion
public static String conversionString(String text) {
return text + "-conversionString";
}

而 BindingConversion 的优先级要高些, 此外,BindingConversion 也可以用于转换属性值的类型:

1
2
3
4
5
6
7
8
9
10
11
@BindingConversion
public static Drawable convertStringToDrawable(String str) {
if (str.equals("红色")) {
return new ColorDrawable(Color.parseColor("#FF4081"));
}

if (str.equals("蓝色")) {
return new ColorDrawable(Color.parseColor("#3F51B5"));
}
return new ColorDrawable(Color.parseColor("#344567"));
}

其他

1.自定义生成的绑定类的类名

每个数据绑定布局文件都会生成一个绑定类,ViewDataBinding 的实例名是根据布局文件名来生成,采用驼峰命名法来命名,并省略布局文件名包含的下划线。控件的获取方式类似,但首字母小写。

通过如下方式自定义 ViewDataBinding 的实例名:

1
<data class="CustomBinding"></data>

2.alias 别名

如果存在 import 的类名相同的情况,可以使用 alias 指定别名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<data>
<import type="com.github.ixiaow.sample.model1.User" />
<import
alias="TempUser"
type="com.github.ixiaow.sample.model2.User" />
<variable
name="user"
type="User" />
<variable
name="tempUserInfo"
type="TempUser" />

<import type="java.util.List"/>
//<需要被替换成&lt;
<variable name="users" type="List&lt;User>"/>
</data>

3.默认值(默认值将只在预览视图中显示,且默认值不能包含引号)

1
android:text="@{userInfo.name,default=defaultValue}"

4.DataBinding的坑
官网上的demo很简单,简单到UserInfo中的所有字段都是string,它并没有告诉我们当字段是int时会有什么问题。假设我们没有在layout中对age写String.valueOf方法的话, userAge就是一个int对象,它会在这里被直接setText

1
2
3
4
5
// batch finished
if ((dirtyFlags & 0xdL) != 0) {
// api target 1
android.databinding.adapters.TextViewBindingAdapter.setText(this.tvAge, userAge); //<--设置UI的操作
}

对setText传一个int值,会被当做Resource索引,然后导致崩溃。如果你是刚接触DataBinding的新手,估计会看到下面这种崩溃原因

Resource #0x0

原因就是缺少了String.valueOf调用了。

原理

DataBinding使用了apt技术,我们build项目时DataBinding会生成多个文件。

DataBinding通过布局中的tag将控件查找出来,然后根据生成的配置文件将V与M进行对应的同步操作,设置一个全局的布局变化监听来实时更新,M通过他的set方法进行同步。

数据绑定框架的目标就是免除开发者繁琐的操作UI,它帮我们做这些事情就好了。 所以它通过注解在编译期生成了ActivityMainBinding类,就是下面这里:

1
2
3
4
5
6
7
8
9
public class MainActivity extends Activity {
....
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
mUser = new UserInfo();
binding.setUser(mUser);
}

ActivityMainBinding可以理解为观察者,它的父类是ViewDataBinding, 它有个抽象方法

1
2
3
4
/**
* @hide
*/
protected abstract void executeBindings();

所有的layout都会生成一个Binding类,这个类继承ViewDataBinding,然后实现了execute*方法。 理解DataBinding框架的关键代码就在这里,其他可以选择性忽略,我们看代码的时候是这样的,先抽脉络,细枝末节的处理可以在理解了框架之后再慢慢体会。 下面是这个抽象方法的具体实现逻辑,这些代码都是DataBinding帮我们生成的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@Override
protected void executeBindings() {
long dirtyFlags = 0;
synchronized(this) {
dirtyFlags = mDirtyFlags;
mDirtyFlags = 0;
}
java.lang.String userName = null;
java.lang.String stringValueOfUserAge = null;
int userAge = 0;
com.phoenix.databindingdemo.UserInfo user = mUser; //<--我们传入的对象

if ((dirtyFlags & 0xfL) != 0) {
if ((dirtyFlags & 0xbL) != 0) {
if (user != null) {
// read user.name
userName = user.getName();//<--UserInfo类中注解标识的get方法
}
}
if ((dirtyFlags & 0xdL) != 0) {
if (user != null) {
// read user.age
userAge = user.getAge();//<--UserInfo类中注解标识的get方法
}
// read String.valueOf(user.age)
stringValueOfUserAge = java.lang.String.valueOf(userAge);//<--layout中的String.valueOf
}
}
// batch finished
if ((dirtyFlags & 0xdL) != 0) {
// api target 1
android.databinding.adapters.TextViewBindingAdapter.setText(this.tvAge, stringValueOfUserAge); //<--设置UI的操作
}
if ((dirtyFlags & 0xbL) != 0) {
// api target 1
android.databinding.adapters.TextViewBindingAdapter.setText(this.tvName, userName); //<--设置UI的操作
}
}

我们在activity中把 mUser对象传入了binding类,在每次对它进行set操作的时候都会触发notify, 之后DataBinding框架会回调execute方法, 框架通过注解拿到get方法,然后拿到和UI所对应的数据,之后结合layout中对应的标注去更新UI。 整个观察者模式的逻辑基本就是这样。

WorkManager

service一直被用来做后台运行的操作,包括一些保活,上传数据之类的,这个后台运行的弊端很多,比如耗电,比如设计用户隐私之类的,谷歌对这些后台行为进行了一些处理,从Android Oreo(API 26) 开始,如果一个应用的目标版本为Android 8.0,当它在某些不被允许创建后台服务的场景下,调用了Service的startService()方法,该方法会抛出IllegalStateException。如果想继续使用service,必须调用Context.startForegroundService()。所以,在不久的将来,service的使用范围会越来越小,取而代之的,是谷歌推出的新的技术:WorkManager。

WorkManager 在工作的触发器 满足时, 运行可推迟的后台工作。WorkManager会根据设备API的情况,自动选用JobScheduler, 或是AlarmManager来实现后台任务,WorkManager里面的任务在应用退出之后还可以继续执行,这个技术适用于在应用退出之后任务还需要继续执行的需求。

WorkManager库的架构图如下所示:

WorkManager可以做很多事情: 取消任务, 组合任务, 构建任务链, 将一个任务的参数合并到另一个任务。大部分的后台任务处理,WorkManager都可以胜任:

相关应用架构

注意:任何应用编写方式都不可能是每种情况的最佳选择。话虽如此,但推荐的这个架构是个不错的起点,适合大多数情况和工作流。如果您已经有编写 Android 应用的好方法(遵循常见的架构原则),则无需更改。

常见的架构原则

如果您不应使用应用组件存储应用数据和状态,那么您应该如何设计应用呢?

分离关注点

要遵循的最重要的原则是分离关注点。一种常见的错误是在一个 Activity 或 Fragment 中编写所有代码。这些基于界面的类应仅包含处理界面和操作系统交互的逻辑。您应尽可能使这些类保持精简,这样可以避免许多与生命周期相关的问题。

请注意,您并非拥有 Activity 和 Fragment 的实现;它们只是表示 Android 操作系统与应用之间关系的粘合类。操作系统可能会根据用户互动或因内存不足等系统条件随时销毁它们。为了提供令人满意的用户体验和更易于管理的应用维护体验,您最好尽量减少对它们的依赖。

通过模型驱动界面

另一个重要原则是您应该通过模型驱动界面(最好是持久性模型)。模型是负责处理应用数据的组件。它们独立于应用中的 View 对象和应用组件,因此不受应用的生命周期以及相关的关注点的影响。

持久性是理想之选,原因如下:

  • 如果 Android 操作系统销毁应用以释放资源,用户不会丢失数据。
  • 当网络连接不稳定或不可用时,应用会继续工作。

应用所基于的模型类应明确定义数据管理职责,这样将使应用更可测试且更一致。

最佳做法

编程是一个创造性的领域,构建 Android 应用也不例外。无论是在多个 Activity 或 Fragment 之间传递数据,检索远程数据并将其保留在本地以在离线模式下使用,还是复杂应用遇到的任何其他常见情况,解决问题的方法都会有很多种。

虽然以下建议不是强制性的,但根据我们的经验,从长远来看,遵循这些建议会使您的代码库更强大、可测试性更高且更易维护:

避免将应用的入口点(如 Activity、Service 和广播接收器)指定为数据源。

相反,您应只将其与其他组件协调,以检索与该入口点相关的数据子集。每个应用组件存在的时间都很短暂,具体取决于用户与其设备的交互情况以及系统当前的整体运行状况。

在应用的各个模块之间设定明确定义的职责界限。

例如,请勿在代码库中将从网络加载数据的代码散布到多个类或软件包中。同样,也不要将不相关的职责(如数据缓存和数据绑定)定义到同一个类中。

尽量少公开每个模块中的代码。

请勿试图创建“就是那一个”快捷方式来呈现一个模块的内部实现细节。短期内,您可能会省点时间,但随着代码库的不断发展,您可能会反复陷入技术上的麻烦。

考虑如何使每个模块可独立测试。

例如,如果使用明确定义的 API 从网络获取数据,将会更容易测试在本地数据库中保留该数据的模块。如果您将这两个模块的逻辑混放在一处,或将网络代码分散在整个代码库中,那么即便能够进行测试,难度也会大很多。

专注于应用的独特核心,以使其从其他应用中脱颖而出。

不要一次又一次地编写相同的样板代码,这是在做无用功。相反,您应将时间和精力集中放在能让应用与众不同的方面上,并让 Android 架构组件以及建议的其他库处理重复的样板。

保留尽可能多的相关数据和最新数据。

这样,即使用户的设备处于离线模式,他们也可以使用您应用的功能。请注意,并非所有用户都能享受到稳定的高速连接。

将一个数据源指定为单一可信来源。

每当应用需要访问这部分数据时,这部分数据都应一律源于此单一可信来源。

参考资料

https://developer.android.google.cn/jetpack?hl=zh_cn
即学即用Android Jetpack
深入了解架构组件之ViewModel
应用架构指南
从Service到WorkManager
android-navigation demo
Android DataBinding 使用
DataBinding常用注解

12…8
Shuming Zhao

Shuming Zhao

78 日志
14 分类
31 标签

© 2021 Shuming Zhao
访客数人, 访问量次 |
由 Hexo 强力驱动
|
主题 — NexT.Pisces v5.1.4