“小云朵像棉花糖,长颈鹿嫌自己脖子不够长。”
Activity Result API 前世今生
前言
起因是看到以前的代码中,ComponentActivity中的startActivityForResult()和onActivityResult()被弃用了(但是startActivity()没有被弃用),然后点进去一看,发现他们在androidx的activity:1.2.0-alpha02和fragment:1.3.0-alpha02中被弃用(在 appcompat 库中则是 1.3.0 被弃用)。并且官方推荐了Activity Result API作为替代方法。
正好看到社团的小朋友也写了一篇,就顺便拿别人的砖抛自己的砖啦~
Old API:startActivityForResult() 和 onActivityResult()
在探究为什么弃用之前,我们先来回顾一下他们的使用方法吧。
因为弃用的两个方法名字有点长,所以我们统称Old API好了
基本用法
startActivityForResult()和startActivity()的功能一样, 都是通过启动Intent来进行页面跳转,不同点在于它的第二个参数接收了一个Request Code,用来表示启动的这个 Intent。
onActivityResult()则可以通过Request Code和Result Code(通常在新的 Activity 中,在finish()前通过setResult()设置)来对某次跳转或某种结果进行特定的后续操作。
//in MainActivity
//第一个Activity中,启动intent进行页面跳转
startActivityForResult(intent, RequestCode);
//回到初始页面处理结果
onActivityResult(requestCode, resultCode, intent) {
if (resultCode == RESULT_OK) { //通过resultCode判断是否进行后续操作
switch (requestCode) { //通过requestCode针对跳转不同进行不同的后续操作
case 1:
//do something
case 2:
//do something
}
}
}
当startActivityForResult()调用的时候,我们会跳到第二个 Activity,在其中操作结束后,利用setResult()设置要传回的Result Code和数据(包裹在Intent里),最后finish()结束第二个 Activity 并回到第一个 Activity:
//in SecondActivity
//第二个Activity中,点击按钮表示结束,传回Result Code和数据
public void onClick(View view) {
Intent goBackIntent = new Intent(SecondActivity.this, MainActivity.class);
setResult(RESULT_OK, goBackIntent);
finish();
}
回到第一个 Activity 后,就会自动调用onActivityResult()方法,以此对回传的数据进行处理,并进行后续操作。(这里的RESULT_OK是 Android 自带的)
可以看出Old API实际上是想模仿一个前后端的交互的过程,Request Code用来区分请求来源,而Result Code就好比后端返回的状态码。
小坑点
项目中偶遇一个 Fragment 和关联它的 Activity 都是实现了onActivityResult(),而当我们在 Fragment 中调用startActivityForResult()之后,理所应当地想要在 Fragment 中的onActivityResult()处理回传结果。但是很遗憾,Fragment 中的onActivityResult()没能接收到结果,反而是给到了 Activity 的onActivityResult()。
在 Fragment 和 Activity 同时实现了onActivityResult()的情况下,如果想要从 Fragment 的startActivityForResult()出发的请求回到 Fragment 的onActivityResult(),需要满足以下条件:
- Fragment 应直接调用
startActivityForResult(),而不是调用getActivity().startActivityForResult() - 如果 Activity 有自己的
onActivityResult(),那么其中要加上super.onActivityResult()(可以在方法最开始,也可以在最后)。
缺点
和大部分被弃用的方法不同,Old API 的方法并没有功能上的问题,它并不是因为有线程安全、内存泄漏等隐患而被弃用的,更多的是因为——随着应用的扩展,onActivityResult()会陷入各种嵌套,耦合严重且难以维护。
也就是说,Old API 可以用,但是用起来很难受(除非程序的体量比较小,功能比较简洁)。
一个很直观的表现就是我们需要维护越来越多的常量标识,除了Intent本身putExtra和getExtra所用到的标识外,不同的 Activity 还要有不同的Request Code;目标 Activity 则需要根据处理结果的不同,返回不同的Result Code。在某些场景下,一个 Activiy 不同的功能还得持有不同的Request Code。
而大量的常量标识,尤其是Request Code,使得onActivityResult()不得不使用更多的if-else或者switch来进行区分,让其越来越臃肿,更别说那些还得针对不同Result Code进行处理的情况了。
于是,Activity Result API出现了。
Activity Result API
基本用法
Activity Result API 主要在androidx.activity.result这个包中
它提供了一个启动类ActivityResultLauncher,我们可以通过调用registerForActivityResult()来获得一个Launcher,这个方法接收三个参数:
ActivityResultContract<I, O> contract:Contract,称为协议或约束,用于规定输入类型(I)和输出类型(O),其内部通过构造 Intent 实现页面之间的跳转。ActivityResultRegistry registry:Registry,协议注册器,通常在 Activity / Fragment 以外的地方接收回传的Result时使用。ActivityResultCallback<O> callback:回调方法,收到Result后进行后续操作,相当于 Old API 中的onActivityResult()方法
我们先看比较简单的情况,大部分情况下我们是在 Activity / Fragment 之间进行交互时使用 Activity Result API,因此不需要传入 Registry(registerForActivityResult()有一个只用传入Contract和回调方法的重载);而 Android 本身已经有了一些预定义的协议,所以简单的调用如下:
//in MainActivity
private ActivityResultLauncher<Intent> toSecondActivityLauncher = registerForActivityResult(
new StartActivityForResult(), new ActivityResultCallback<ActivityResult>() {
@Override
public void onActivityResult(ActivityResult result) {
if (result.getResultCode() == RESULT_OK) {
Intent resultIntent = result.getData();
//todo things...
}
}
});
//......
//点击事件触发跳转
public void onClick(View view) {
Intent intent = new Intent(MainActivity.this, SecondActivity.class);
toSecondActivityLauncher.launch(intent);
}
我们通过Launcher.launch()来启动 Intent 并实现跳转,当我们跳转到 SecondActivity 后,其中不需要任何改动——仍是通过setResult(resultCode, intent)和finish()回到 FirstActivity(MainActivity)。
这里我们使用了StartActivityForResult这个预定义的约束,它规定我们的输入类型是Intent,输出类型是ActivityResult。因此我们调用Launcher.launch()的时候传入的就是Intent;而回调接口的范型和回调方法的参数则是ActivityResult,表示输出回来的结果,拿到这个结果后我们就可以在回调方法中进一步处理。
当然,这里作为输出类型的ActivityResult也是 Android 提供给我们的,主要就是通过result.getResultCode()来获取resultCode,通过result.getData()来获取Intent(来自
setResult(resultCode, intent))。(ActivityResult只有这两个属性)
回调函数用的是也自带的ActivityResultCallback,其通过onActivityResult()进行结果的处理(虽然这个方法和 Old API 的方法重名,实际上是不同的方法)。
更多时候,我们喜欢用lambda来实现回调方法:
//in MainActivity
private ActivityResultLauncher<Intent> toSecondActivityLauncher = registerForActivityResult(
new StartActivityForResult(), result -> {
if (result.getResultCode() == RESULT_OK) {
Intent resultIntent = result.getData();
//todo things...
}
});
因为参数和范型有点多,所以这里小小总结一下:起决定作用的还是约束ActivityResultContract<I, O>,我们用其中的<I, O>分别表示输入类型和输出类型:
输入类型I:决定了ActivityResultLauncher<I>的范型,以及Launcher.launch(I)的参数类型;输出类型O:决定了ActivityResultCallback<O>的范型,以及回调函数onActivityResult(O)的参数
Contract 约束
预定义的 Contract
之前提到,StartActivityForResult是一个预定义的Contract,当然除了它之外,还有许多其他给定的 Contract 供我们使用,这里列一些比较常见的:
| Contract | 功能 |
|---|---|
StartActivityForResult<Intent, ActivityResult> |
多用于 App 內 Activity 的跳转 |
RequestPermission<String, Boolean> |
用于请求单个权限 |
RequestMultiplePermissions<String[], Map<String, Boolean>> |
用于请求一组权限 |
TakePicturePreview<Void, Bitmap> |
拍照,返回 Bitmap 图片 |
TakePicture<Uri, Boolean> |
拍照,保存至 Uri 处,保存成功返回 true |
TakeVideo<Uri, Boolean> |
拍摄视频,保存至 Uri 处,返回一张缩略图 |
PickContact<Void, Uri> |
从通讯录获取联系人 |
GetContent<String, Uri> |
选择内容,返回其 Uri 地址 |
CreateDocument<String, Uri> |
创建一个文档,返回其 Uri |
OpenDocument<String[], Uri> |
选择一个文档,返回其 Uri |
OpenMultipleDocuments<String[], List<Uri>> |
选择多个文档,返回它们 Uri 的 List |
OpenDocumentTree<Uri, Uri> |
选择一个目录,返回其 Uri |
当看到 OpenDocument<String[], Uri>的时候我还有些疑问,为啥打开一个文件要传入String数组。事实上,当启动这个 Contract 之后,会打开类似文件管理器的页面,然后让我们选择文件:
//In MainActivity
private ActivityResultLauncher<String[]> openDocumentLauncher = registerForActivityResult(new OpenMultipleDocuments(), result -> {
Log.d("TAG", "uri: " + result.getPath());
//选择一张图片后输出:/document/image:15649
});
openDocumentLauncher.launch(new String[]{"image/*", "video/*"});
可以看到,如果 String 数组中有"image/*",则会打开图片的目录,可以选择其中的图片文件;如果带上"video/*",则会打开视频的目录等。当然还有可以传入“*/*”,表示打开所有的目录。因此,上传头像等功能就能用它来帮助实现啦。
GetContent、OpenMultipleDocuments、OpenDocumentTree等也是相似的操作流程。
由于运行时请求权限的场景比较多,所以来看看相关的 Contract:
返回结果代表着权限申请的成功与否,因此我们可以根据结果来判断是继续申请/退出,还是进一步执行操作。
private ActivityResultLauncher<String> requestPermissionLauncher = registerForActivityResult(new RequestPermission(), result -> {
String resultMessage = result ? "Permission grated." : "Permission denied.";
Log.d("Callback:", resultMessage);
});
private ActivityResultLauncher<String[]> requestMultiPermissionsLauncher = registerForActivityResult(new RequestMultiplePermissions(), result -> {
for (Map.Entry<String, Boolean> entry : result.entrySet()) {
Log.d("Callback:", "Request Permission of " + entry.getKey() + " " + entry.getValue());
}
});
//申请权限:
requestPermissionLauncher.launch(permission.ACCESS_FINE_LOCATION);
//输出:true
requestMultiPermissionsLauncher.launch(new String[] {
permission.ACCESS_FINE_LOCATION,
permission.BLUETOOTH,
permission.NFC});
//输出:
//Request Permission of android.permission.ACCESS_FINE_LOCATION true
//Request Permission of android.permission.BLUETOOTH true
//Request Permission of android.permission.NFC true
其实用到最多的还是StartActivityForResult和几个请求权限相关的 Contract,而其他的 Contract 都是在和其他 App(系统 App)交互的时候才用到,使用场景比较受限(打开相机、通讯录、文件管理器啥的)。
如果没有额外的需求,这些预定义的 Contract 完全够我们使用的,而其中的实现过程对我们来说是透明的,不需要关心,因此,整个操作流程就变的更加方便和简洁。
Contract 的内部操作
Contract既然规定了输入类型和输出类型,那么它内部应该是进行了一系列操作来进行转换的。我们就更进一步,看看其内部进行了哪些操作。(Result API中的Contract其实是由Kotlin实现的,Contract 约束的概念也是在 Kotlin 中引入的,我这里 Decompile 成 Java 展示)
以预定义的StartActivityForResult为例,我们看看其内部是如何实现的:
public static final class StartActivityForResult extends ActivityResultContract<Intent, ActivityResult> {
@Override
public Intent createIntent(Context context, Intent input) {
return input;
}
@Override
public ActivityResult parseResult(int resultCode, Intent intent) {
return new ActivityResult(resultCode, intent);
}
}
可以看到,StartActivityForResult是抽象类ActivityResultContract的一个终类(final class)。事实上,所有预定义的 Contract 都是它的终类,区别就是输入类型和输出类型的范型不同。我们先瞟一眼ActivityResultContract这个大家的抽象父类:
public abstract class ActivityResultContract<I, O> {
public abstract Intent createIntent(Context context, I input);
public abstract O parseResult(int resultCode, Intent intent);
//...
}
可以看到,主要方法就是接收输入的createIntent()和返回输出的parseResult()两个方法。
再回来看StartActivityForResult,当我们调用launch()的时候,createIntent()会被执行,其生成的Intent会被启动从而实现页面的跳转。
当页面返回,则会调用parseResult()接收来自第二个 Activity 回传的数据(setResult(resultCode, intent)),并将其转变为输出类型 O,在这里就是ActivityResult。最后我们在外面,通过ActivityResultCallback来接收parseResult()的结果,进行后续处理。
也就是说,从launch()开始算起,我们的数据流向大致是这样的:
[FirstActivity]: launch() -> [Contract]createIntent() -> [SecondActivity] -> setResult() -> [Contract]parseResult() -> [FirstActivity]: callback()
Contract成为了两个Activity之间信息传递的桥梁。
自定义的 Contract
既然有ActivityResultContract是个抽象类,那么当然,只要继承它,我们就可以自定义 Contract,创造自己的Custom Contract了——只要实现createIntent()和parseResult()这两个抽象方法就好了嘛。
class CustomContract extends ActivityResultContract<String, String> {
@Override
public Intent createIntent(@NonNull Context context, String s) {
Intent intent = new Intent(context, SecondActivity.class);
intent.putExtra("createIntentStringKey", s);
return intent;
}
@Override
public String parseResult(int i, @Nullable Intent intent) {
String parseResultStringKey = intent.getStringExtra("parseResultStringKey");
if (parseResultStringKey != null) {
return parseResultStringKey;
} else {
return "no string in result";
}
}
}
这里我们定义了输入(I)和输出(O)都是 String的 Contract。它将携带输入的 String 内容,并跳转到SecondActivity。而当我们从SecondActivity返回的时候,则会将返回Intent中的 String 提取出来。
接下来我们就可以在MainActivity中利用它来进行跳转了:
//in MainActivity
private ActivityResultLauncher<String> mLauncher = registerForActivityResult(new CustomContract(), result -> {
Log.d("Callback", result);
});
//点击启动
public void onClick(View view) {
mLauncher.launch("hello");
}
而在SecondActivity中,我们可以通过之前设定的Key("createIntentStringKey")来获取Intent中的 String 内容;同时给返回的Intent带上相应Key("parseResultStringKey")的 String,以便Contract获取到我们返回的内容:
//in SecondActivity
String message = getIntent().getStringExtra("createIntentStringKey");
Log.e("SecondActivity: ", message);
//点击返回MainActivity
public void onClick(View view) {
Intent goBackIntent = new Intent();
goBackIntent.putExtra("parseResultStringKey", "world");
setResult(RESULT_OK, goBackIntent);
finish();
}
我们从MainActivity跳转到SecondActivity,获取到launch()传来的"hello",Log 输出;然后回到MainActivity,触发回调,Log 输出setResult()传来的"world"(其实传回的是Intent,不过我们的Contract“从中作梗”,已经将其加工成 String 给我们了):
D/SecondActivity: hello
D/Callback: world
注册器 ActivityResultRegistry
我们发现,在之前的使用过程中,我们并没有接触到注册器ActivityResultRegistry,那么它到底是怎么工作的呢?
我们第一次提到Registry,是说它作为registerForActivityResult()的一个参数,那么我们就从这个方法入手:
//in ComponentActivity.java
//两个参数的重载
public final <I, O> ActivityResultLauncher<I> registerForActivityResult(
@NonNull ActivityResultContract<I, O> contract,
@NonNull ActivityResultCallback<O> callback) {
return registerForActivityResult(contract, mActivityResultRegistry, callback);
}
//真正的调用
public final <I, O> ActivityResultLauncher<I> registerForActivityResult(
@NonNull final ActivityResultContract<I, O> contract,
@NonNull final ActivityResultRegistry registry,
@NonNull final ActivityResultCallback<O> callback) {
return registry.register(
"activity_rq#" + mNextLocalRequestCode.getAndIncrement(), this, contract, callback);
}
我们看到,我们最常使用的两个参数的重载,并非是没用到Registy,而是因为Activity中自带了一个mActivityResultRegistry。实际上,最终还是通过Registry的register()来获取的Launcher。
mActivityResultRegistry是ActivityResultRegistry的一个实例对象,仅实现了其onLaunch()的抽象方法。而在ActivityResultRegistry中,比较重要的方法有以下几个onLaunch(),register(),dispatchResult()
onLaunch()是一个抽象方法,而mActivityResultRegistry就实现了它,内部进行了一些权限获取,最终通过ActivityCompat.startActivityForResult()来启动 Intent。(代码有点长就不贴了)
//in ActivityResultRegistry.java
public abstract <I, O> void onLaunch(
int requestCode,
ActivityResultContract<I, O> contract,
I input,
ActivityOptionsCompat options);
register()是一个 final 方法,它主要进行了 Lifecycle 的一系列操作,利用 LifecycleContainer 存储一些数据变量啥的,最后返回一个ActivityResultLauncher。它本身也是一个抽象类,这里在 return 的时候顺便实现了它的一些抽象方法
//in ActivityResultRegistry.java
public final <I, O> ActivityResultLauncher<I> register(
final String key,
final LifecycleOwner lifecycleOwner,
final ActivityResultContract<I, O> contract,
final ActivityResultCallback<O> callback) {
Lifecycle lifecycle = lifecycleOwner.getLifecycle();
//...
return new ActivityResultLauncher<I>() {
@Override
public void launch(I input, ActivityOptionsCompat options) {
Integer innerCode = mKeyToRc.get(key);
if (innerCode == null) {
//throw Exception
mLaunchedKeys.add(key);
try {
onLaunch(innerCode, contract, input, options); //1
} catch (Exception e) { //throw Exception }
}
@Override
public void unregister() {
ActivityResultRegistry.this.unregister(key); //2
}
@Override
public ActivityResultContract<I, ?> getContract() {
return contract;
}
};
}
可以看到,这里为ActivityResultLauncher实现的launch()最终调用的是ActivityResultRegistry自己的抽象方法onlaunch()(注释 1),而这个抽象的onlaunch()的实现则是由ComponentActivity中的mActivityResultRegistry实现(默认情况下);而unregister()实际上也是ActivityResultRegistry自己的unregister()(注释 2)。
也就是说,对于一个ActivityResultLauncher来说,它的unregister()是在ActivityResultRegistry中实现的,而他的launch()则是由Activity实现的。
感觉回调了好多层,我已经开始有些头晕了=x=
至于dispatchResult(),他们的代码有点长,我删删减减了一些:
//in ActivityResultRegistry.java
public final boolean dispatchResult(int requestCode, int resultCode, Intent data) {
String key = mRcToKey.get(requestCode);
if (key == null) { return false; }
doDispatch(key, resultCode, data, mKeyToCallback.get(key));
return true;
}
public final <O> boolean dispatchResult(int requestCode, O result) {
String key = mRcToKey.get(requestCode);
if (key == null) { return false; }
//...
ActivityResultCallback<O> callback =
(ActivityResultCallback<O>) callbackAndContract.mCallback;
if (mLaunchedKeys.remove(key)) {
callback.onActivityResult(result);
}
return true;
}
private <O> void doDispatch(String key, int resultCode, Intent data, CallbackAndContract<O> callbackAndContract) {
//...
ActivityResultCallback<O> callback = callbackAndContract.mCallback;
ActivityResultContract<?, O> contract = callbackAndContract.mContract;
callback.onActivityResult(contract.parseResult(resultCode, data));
mLaunchedKeys.remove(key);
//...
}
总的来说,最后都是通过callback.onActivityResult()来将结果通过回调接口传给外部,这里的回调接口(CallbackAndContract.mCallback)就是我们的ActivityResultCallback。因此外部(包括非 Activity/Fragment)就可以通过dispatchResult()来获取回传的结果。
实际上,在弃用的ComponentActivity.onActivityResult()中就有dispatchResult(),用于拦截返回结果,将结果分发给ActivityResultRegistry进行处理。如果拦截失败则交给onActivityResult()继续传递。
//in ComponentActivity
@Deprecated
@CallSuper
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (!this.mActivityResultRegistry.dispatchResult(requestCode, resultCode, data)) {
super.onActivityResult(requestCode, resultCode, data);
}
}
不过在大多数情况下我们是不用自己实现Registry的,自带的mActivityResultRegistry能满足基本需求哒。
用生命周期组件 Lifecycle 进行包装
大部分情况下我们都只在Activity和Fragment中会用到Result API,我们可以通过registerForActivityResult()直接生成一个Launcher,因为ConponentActivity和Fragment都实现了ActivityResultCaller接口,重写了这个方法。
而在一些特殊情况中,我们需要在非 Activity/Fragment的位置接收 Activity 回传的数据,这时候就要用到注册器ActivityResultRegistry了
我们可以新建一个生命周期组件,实现DefaultLifecycleObserver,它分别持有注册器ActivityResultRegistry和启动器ActivityResultLauncher,利用注册器来生成启动器。
其实也可以直接声明一个ActivityResultRegistry实例,但是我们更习惯将其用LifecycleObserver进行一个包装。原因是当我们成功注册一个 Launcher 后,为了保证资源释放,需要在最后调用launcher.unregister()来将其释放。
不过由于 Activity 和 Fragment 都有自己的生命周期,其LifecycleOwner会在onDestroy()中自动释放 Launcher,不用我们操心。但是我们使用注册器的前提是“可能在非 Activity/Fragment的位置调用 Launcher”,这些位置不一定有自己的 Lifecycle,因此,为了避免每次手动调用unregister(),我们用生命周期组件将其包装,以实现 Launcher 的自动释放。
When using the
ActivityResultRegistryAPIs, it’s strongly recommended to use the APIs that take aLifecycleOwner, as theLifecycleOwnerautomatically removes your registered launcher when theLifecycleis destroyed. However, in cases where aLifecycleOwneris not available, eachActivityResultLauncherclass allows you to manually callunregister()as an alternative.
具体的实现如下:
class MyLifecycleObserver implements DefaultLifecycleObserver {
private final ActivityResultRegistry mRegistry;
private ActivityResultLauncher<String> mLauncher;
MyLifecycleObserver(ActivityResultRegistry registry) {
mRegistry = registry;
}
public void onCreate(LifecycleOwner owner) {
mLauncher = mRegistry.register("key", owner, new CustomContract(), result -> {
Log.d("callback", "Lifecycle onCreate: " + result);
});
}
public void startLauncher(String inputString) {
mLauncher.launch(inputString);
}
}
其中用到的 Contract 是我们自己自定义的CustomContract,接收一个 String 作为launch()参数,返回一个 String 作为回调接收的内容。
接下来我们就可以在MainActivity中实例化这个MyLifecycleObserver,进而使用其中的 Launcher 了,以此来代替之前通过registerForActivityResult()获取 Launcher 的方式。
//In Other Class
private MyLifecycleObserver mObserver;
//onCreate中实例化Observer并绑定
@Override
protected void onCreate(Bundle savedInstanceState) {
//...
mObserver = new MyLifecycleObserver(getActivityResultRegistry());
getLifecycle().addObserver(mObserver);
}
//通过mObserver来启动Launcher
mObserver.startLauncher("hello");
当我们用生命周期组件 Lifecycle 进行包装后,即使在其他的一些类中,我们也能轻松启动 Launcher,而不用关心自己应该在什么时候将其释放。
小结 - 两者对比
有一个很直观的一点就是,使用 Result API 之后,我们不再需要Request Code了。
在 Old API 中,我们需要 RequestCode 来告诉onActivityResult()我们的请求是来自哪里的;而在 Result API 中,我们采用了Launcher-Contract-Callback来进行请求和结果处理。每个 Launcher 都有自己的 Callback,相当于每个请求都有属于自己的onActivityResult(),不再需要统一处理。
我们看到 Registry 的代码中有很多 key 的出现,这个 key 就是用来生成每个 Launcher 的唯一识别码的(因此我们能用同一个 Registry 生成多个 Launcher)。相当于Request Code被偷偷藏在内部,对外部透明了,我们就不需要考虑它啦。
这也是 Result API 最为直观的优点,取消了Request Code,则就没有了越来越多的常量 Flag;没有了onActivityResult(),则就没有了越来越多的耦合和嵌套。 (不过我们会有越来越多的 Launcher 和 Callback,所以代码总量还是基本不变的)
此外,相比于 Old API,Result API 有着更加广泛的功能——至少我们能用它来进行运行时的权限请求了,而这也得益于 Contract 的引入。
不仅如此,支持自定义的 Contract 也让我们有了更好的扩展性,可以针对自己的需求来自创 Contract,让请求的发送和结果的处理更加流畅。
话虽如此,Result API 也有着一定的学习成本,毕竟它有着Launcher、Contract、Registry等组件,是以往不曾接触的。但是简单的使用还是比较简单的,而且掌握后其带来的多功能、高扩展也挺让人受益的