原文连贯:https://mp.weixin.qq.com/s/V2haCRugRYCGDZrA9iw7bQ
前言
== 本次次要解说的内容:==
1、Jetpack 介绍
2、Jetpack 架构组件库的相干用法
一、Jetpack 介绍
1、什么是 Jetpack
Google 官网解释:
Jetpack 是一个由多个库组成的套件,可帮忙开发者遵循最佳做法,缩小样板代码并编写可在各种 Android 版本和设施中统一运行的代码,让开发者精力集中编写重要的代码。
Jetpack 是 Google 为解决 Android 开发碎片化,打造成熟衰弱生态圈提出的战略规划,是 Google 对 Android 将来提出的倒退方向,同时它也是泛滥优良 ++Android 组件的汇合 ++。
2、为何应用 Android Jetpack
Google 官网解释:
- 遵循最佳做法:Android Jetpack 组件采纳最新的设计办法构建,具备向后兼容性,能够缩小解体和内存泄露。
- 打消样板代码:Android Jetpack 能够治理各种繁琐的 Activity(如后台任务、导航和生命周期治理),以便您能够专一于打造杰出的利用。
- 缩小不统一:这些库可在各种 Android 版本和设施中以统一的形式运作,助您升高复杂性。
Jetpack 的劣势:
- [] Jetpack 领有基于生命周期感知的能力,能够缩小 NPE(空指针异样) 解体、内存透露,为开发出强壮且晦涩的程序提供强力保障;
- [] Jetpack 能够打消大量反复样板式的代码,能够减速 Android 的开发过程,组件可搭配工作,也可独自应用,同时配合 Kotlin 语言个性可能显著进步工作效率;
- [] 对立开发模式,摈弃传统的 MVC, MVP;
3、JetPack 的形成
如上图:Jetpack 次要包含 4 个局部,别离是【Architecture:架构】、【UI:界面】、【behavior:行为】和【foundation:根底】。
Architecture,架构组件
目标:帮忙开发者设计持重、可测试且易保护的利用;
- Lifecycle:具备宿主生命周期感知能力的组件
个性:持有组件 (如 Activity 或 Fragment) 生命周期状态的信息,并且容许其余对象察看此状态;
- LiveData:新一代具备生命周期感知能力的数据订阅、散发组件
个性:反对共享资源、反对黏性事件的散发、不再须要手动解决生命周期、确保界面合乎数据状态;
- ViewModel:具备生命周期感知能力的数据存储组件
个性:页面因配置变更导致的重启,此时数据不失落;能够实现跨页面 (跨 Activity) 的数据共享;
- SavedState 架构组件原理解析
个性:因内存不足,电量有余导致页面被回收时能够搭配 ViewModel 实现数据存储与复原;
- Room:轻量级 orm 数据库,实质上是一个 SQLite 形象层
个性:应用简略(相似于 Retrofit 库),通过注解的形式实现相干性能,编译时主动生成相干实现类
- DataBinding:只是一种工具,解决的是 View 和数据之间的双向绑定
个性:反对数据与视图双向绑定、数据绑定空平安、缩小模板代码、开释 Activity/Fragment 压力;
- Paging: 列表分页组件,能够轻松实现分页预加载以达到有限滑动的成果
个性:奇妙交融 LiveData、提供多种数据源加载形式;
不足之处:不反对列表数据增删改,列表增加 HeaderView,FooterView 定位不精确;
- Navigation 组件原理剖析:端内对立路由组件
个性:可能为 Activity,Fragment,Dialog,FloatWindow 提供对立的路由导航服务,能够传递参数,指定导航动画,还反对深度链接等次要能力;
有余:非常依赖 xml 配置文件不利于组件化,模块化
- WorkManager:新一代后台任务治理组件,service 能做的事件它都能做
个性:反对周期性任务调度、链式任务调度、丰盛的工作约束条件、程序即使退出,仍旧能保障工作的执行;
Foundationy,根底组件
目标:提供横向性能,例如向后兼容性、测试、平安、Kotlin 语言反对,并包含多个平台开发的组件;
- Android KTX:优化了供 Kotlin 应用的 Jetpack 和 Android 平台 API,帮忙开发者以更简洁、更愉悦、更习用的形式应用 Kotlin 进行 Android 开发;
- AppCompat:帮忙较低版本的 Android 零碎进行兼容;
- Auto:开发 Android Auto 利用的组件,提供了实用于所有车辆的标准化界面和用户交互;
- 检测:从 AndroidStudio 中疾速检测基于 Kotlin 或 Java 的代码;
- 多 Dex 解决:为具备多个 Dex 文件利用提供反对;
- 平安:平安的读写加密文件和共享偏好设置;
- 测试:用于单元和运行时界面测试的 Android 测试框架;
- TV:构建可让用户在大屏幕上体验沉迷式内容的利用;
- Wear OS:开发 Wear 利用的组件;
Behavior,行为组件
目标:帮忙开发者的利用与规范 Android 服务 (如告诉、权限、分享) 相集成;
- CameraX:帮忙开发简化相机利用的开发工作,提供统一且易于应用的界面,实用于大多数 Android 设施,并可向后兼容至 Android 5.0(API 21);
- DownloadManager:解决长时间运行的 HTTP 下载的零碎服务;
- 媒体和播放:用于媒体播放和路由 (包含 Google Cast) 的向后兼容 API;
- 告诉:提供向后兼容的告诉 API,反对 Wear 和 Auto;
- 权限:用于检查和申请利用权限的兼容性 API;
- 设置:创立交互式设置,倡议应用 AndroidX Preference Library 库将用户可配置设置集成到利用中;
- 分享操作:能够更轻松地实现敌对的用户分享操作;
- 切片:切片是一种 UI 模板,创立可在利用内部显示利用数据的灵便界面元素;
UI,界面组件
- Animation and Transition:该框架蕴含用于常见成果的内置动画,并容许开发者创立自定义动画和生命周期回调;
- Emoji Compatibility:即使用户没有更新 Android 零碎也能够获取最新的表情符号;
- Fragment:组件化界面的根本单位;
- 布局:用 XML 中申明 UI 元素或者在代码中实例化 UI 元素;
- 调色板:从调色板中提取出有用的信息;
留神:
下面的图之前在官网是存在的,当初官网曾经找不到此图,只有 jetpack 架构组件的文档入口;
猜测 google 官网也是旨在强化 Architecture 架构组件,因为 UI、Behavior、Foundation 这三局部大多是对已有内容的收集整理。
所以接下来的关注点次要是在 Architecture 架构局部
4、Jetpack 与 AndroidX
参考官网 Jetpack 应用
1、Jetpack 库能够独自应用,也能够组合应用,以满足利用的不同需要。
2、Jetpack 库在 androidx 命名空间中公布。
androidx 介绍:
==androidx 命名空间中的工件蕴含 Android Jetpack 库。与反对库一样,androidx 命名空间中的库与 Android 平台离开提供,并向后兼容各个 Android 版本。==
==AndroidX 对原始 Android 反对库进行了重大改良,后者当初已不再保护。androidx 软件包齐全取代了反对库,不仅提供等同的性能,而且提供了新的库。==
AndroidX 还包含以下性能:
++AndroidX 中的所有软件包都应用统一的命名空间,以字符串 androidx 结尾。反对库软件包已映射到对应的 androidx.* 软件包。无关所有旧类到新类以及旧构建工件到新构建工件的残缺映射,请参阅软件包重构页面。++
++ 与反对库不同,androidx 软件包会独自保护和更新。从版本 1.0.0 开始,androidx 软件包应用严格的语义版本控制。您能够独自更新我的项目中的各个 AndroidX 库。++
++ 版本 28.0.0 是反对库的最初一个版本。咱们将不再公布 android.support 库版本。所有新性能都将在 androidx 命名空间中开发。++
所以,目前咱们的我的项目【朴新网校】、【朴新助教】、【数学宝典】都曾经迁徙应用了 androidx 库。
androidx 迁徙参照官网
留神:Jetpack 是各种组件库的统称,AndroidX 是这些组件的对立包名。
二、Jetpack 架构组件库
1、利用架构指南
官网举荐利用架构:
在我的项目中使用到的框架的搭建根本基于改图,使用到的 jetpack 组件库有ViewModel、LiveData、DataBinding; 接下来逐个介绍他们的个性和用法。
2、ViewModel & LiveData
ViewModel 类旨在以重视生命周期的形式存储和治理界面相干的数据。ViewModel 类让数据可在产生屏幕旋转等配置更改后持续留存。
LiveData 是一种可察看的数据存储器类。与惯例的可察看类不同,LiveData 具备生命周期感知能力,意指它遵循其余利用组件(如 Activity、Fragment 或 Service)的生命周期。这种感知能力可确保 LiveData 仅更新处于沉闷生命周期状态的利用组件观察者。
LiveData,ViewModel 组件都是基于 Lifecycle 来实现的,LiveData&ViewModel 也是联合应用的,接下来具体看下如何应用
申明依赖项
dependencies {
def lifecycle_version = "2.2.0"
// ViewModel
implementation "androidx.lifecycle:lifecycle-viewmodel:$lifecycle_version"
// LiveData
implementation "androidx.lifecycle:lifecycle-livedata:$lifecycle_version"
}
实现 ViewModel
架构组件为界面控制器提供了 ViewModel 辅助程序类,该类负责为界面筹备数据。在配置更改期间会主动保留 ViewModel 对象,以便它们存储的数据立刻可供下一个 Activity 或 Fragment 实例应用。例如,如果您须要在利用中显示用户列表,请确保将获取和保留该用户列表的责任调配给 ViewModel,而不是 Activity 或 Fragment,如以下示例代码所示:
public class MyViewModel extends ViewModel {
private MutableLiveData<List<User>> users;
public LiveData<List<User>> getUsers() {if (users == null) {users = new MutableLiveData<List<User>>();
loadUsers();}
return users;
}
private void loadUsers() {// Do an asynchronous operation to fetch users.}
}
而后,您能够从 Activity 拜访该列表,如下所示:
public class MyActivity extends AppCompatActivity {public void onCreate(Bundle savedInstanceState) {// Create a ViewModel the first time the system calls an activity's onCreate() method.
// Re-created activities receive the same MyViewModel instance created by the first activity.
MyViewModel model = new ViewModelProvider(this).get(MyViewModel.class);
model.getUsers().observe(this, users -> {// update UI});
}
}
ViewModel 在 Fragment 之间共享数据
应用 ViewModel 对象解决这一常见的难点。这两个 Fragment 能够应用其 Activity 范畴共享 ViewModel 来解决此类通信
public class SharedViewModel extends ViewModel {private final MutableLiveData<Item> selected = new MutableLiveData<Item>();
public void select(Item item) {selected.setValue(item);
}
public LiveData<Item> getSelected() {return selected;}
}
// 获取 ViewModelProvider 时, 会收到雷同的 SharedViewModel 实例(其范畴限定为该 Activity)public class MasterFragment extends Fragment {
private SharedViewModel model;
public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {super.onViewCreated(view, savedInstanceState);
// 通过 requireActivity() Fragment 会检索蕴含它们的 Activity
model = new ViewModelProvider(requireActivity()).get(SharedViewModel.class);
itemSelector.setOnClickListener(item -> {model.select(item);
});
}
}
// 获取 ViewModelProvider 时, 会收到雷同的 SharedViewModel 实例(其范畴限定为该 Activity)public class DetailFragment extends Fragment {public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {super.onViewCreated(view, savedInstanceState);
// 通过 requireActivity() Fragment 会检索蕴含它们的 Activity
SharedViewModel model = new ViewModelProvider(requireActivity()).get(SharedViewModel.class);
model.getSelected().observe(getViewLifecycleOwner(), { item ->
// Update the UI.
});
}
}
该办法的劣势:
- Activity 不须要执行任何操作,也不须要对此通信有任何理解。
- 除了 SharedViewModel 约定之外,Fragment 不须要相互了解。如果其中一个 Fragment 隐没,另一个 Fragment 将持续照常工作。
- 每个 Fragment 都有本人的生命周期,而不受另一个 Fragment 的生命周期的影响。如果一个 Fragment 替换另一个 Fragment,界面将持续工作而没有任何问题。
应用 LiveData 对象
官网应用参考
1、创立 LiveData 对象
LiveData 是一种可用于任何数据的封装容器,其中包含可实现 Collections 的对象,如 List。LiveData 对象通常存储在 ViewModel 对象中,并可通过 getter 办法进行拜访,如以下示例中所示:
public class NameViewModel extends ViewModel {
// Create a LiveData with a String
private MutableLiveData<String> currentName;
public MutableLiveData<String> getCurrentName() {if (currentName == null) {currentName = new MutableLiveData<String>();
}
return currentName;
}
// Rest of the ViewModel...
}
最后,LiveData 对象中的数据并未通过设置。
留神:请确保用于更新界面的 LiveData 对象存储在 ViewModel 对象中,而不是将其存储在 Activity 或 Fragment 中,起因如下:1、防止 Activity 和 Fragment 过于宏大。当初,这些界面控制器负责显示数据,但不负责存储数据状态。2、将 LiveData 实例与特定的 Activity 或 Fragment 实例分来到,并使 LiveData 对象在配置更改后持续存在。
2、察看 LiveData 对象
在大多数状况下,在利用组件的 onCreate() 办法是开始察看 LiveData 对象起因如下:
1、确保零碎不会从 Activity 或 Fragment 的 onResume() 办法进行冗余调用。
2、确保 Activity 或 Fragment 变为沉闷状态后具备能够立刻显示的数据。一旦利用组件处于 STARTED 状态,就会从它正在察看的 LiveData 对象接管最新值。只有在设置了要察看的 LiveData 对象时,才会产生这种状况。
LiveData 仅在数据产生更改时才发送更新,并且仅发送给沉闷观察者。此行为的一种例外情况是,观察者从非沉闷状态更改为沉闷状态时也会收到更新。此外,如果观察者第二次从非沉闷状态更改为沉闷状态,则只有在自上次变为沉闷状态以来值产生了更改时,它才会收到更新。
以下示例代码阐明了如何开始察看 LiveData 对象:
public class NameActivity extends AppCompatActivity {
private NameViewModel model;
@Override
protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);
// Other code to setup the activity...
// Get the ViewModel.
model = new ViewModelProvider(this).get(NameViewModel.class);
// Create the observer which updates the UI.
final Observer<String> nameObserver = new Observer<String>() {
@Override
public void onChanged(@Nullable final String newName) {
// Update the UI, in this case, a TextView.
nameTextView.setText(newName);
}
};
// Observe the LiveData, passing in this activity as the LifecycleOwner and the observer.
model.getCurrentName().observe(this, nameObserver);
}
}
3、更新 LiveData 对象
LiveData 没有公开可用的办法来更新存储的数据。MutableLiveData 类将公开 setValue(T) 和 postValue(T) 办法,如果您须要批改存储在 LiveData 对象中的值,则必须应用这些办法。通常状况下会在 ViewModel 中应用 MutableLiveData,而后 ViewModel 只会向观察者公开不可变的 LiveData 对象。
设置观察者关系后,您能够更新 LiveData 对象的值(如以下示例中所示),这样当用户点按某个按钮时会触发所有观察者:
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
String anotherName = "John Doe";
model.getCurrentName().setValue(anotherName);
}
});
留神:您必须调用 setValue(T) 办法以从主线程更新 LiveData 对象。如果在 worker 线程中执行代码,则您能够改用 postValue(T) 办法来更新 LiveData 对象。
3、Data Binding
Data Binding 是一种反对库,借助该库,您能够应用申明性情式(而非程序化地)将布局中的界面组件绑定到利用中的数据源。
布局通常是应用调用界面框架办法的代码在 Activity 中定义的。例如,以下代码调用 findViewById() 来查找 TextView 微件并将其绑定到 viewModel 变量的 userName 属性:
TextView textView = findViewById(R.id.sample_text);
textView.setText(viewModel.getUserName());
以下示例展现了如何在布局文件中应用 Data Binding 将文本间接调配到 TextView。这样就无需调用上述任何 Java 代码。请留神赋值表达式中 @{} 语法的应用:
<TextView
android:text="@{viewmodel.userName}" />
留神:
如果您应用 Data Biding 的次要目标是取代 findViewById() 调用,请思考改用 ViewBinding。
应用过 ButterKnife 的都晓得,目前 ButterKnife 作者倡议切换至 ViewBindng 应用;在许多状况下,ViewBinding 可简化实现,进步性能,提供与 DataBinding 雷同的益处。
Data Binding 应用场景:
布局和绑定表达式
数据绑定的布局以根标记 layout 结尾,后跟 data 元素和 view 根元素。如下:
留神:布局表达式应放弃精简,因为它们无奈进行单元测试,并且领有的 IDE 反对也无限。为了简化布局表达式,能够应用自定义绑定适配器。
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<!--data 中的 user 变量形容了可在此布局中应用的属性。-->
<data>
<variable name="user" type="com.example.User"/>
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- 布局中的表达式应用“@{}”语法给控件赋值。-->
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName}"/>
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.lastName}"/>
</LinearLayout>
</layout>
== 零碎会为每个布局文件生成一个绑定类 ==。
1、默认状况下,类名称基于布局文件的名称,它会转换为驼峰模式并在开端增加 Binding 后缀。
2、以上布局文件名为 activity_main.xml,因而生成的对应类为 ActivityMainBinding,== 且都是 ViewDataBinding 的子类,所有布局对应的生成的绑定类都能够是 ViewDataBinding 类 ==
3、此类蕴含从布局属性(例如,user 变量)到布局视图的所有绑定,并且晓得如何为绑定表达式指定值。
4、倡议的绑定创立办法是在裁减布局时创立,如以下示例所示:
@Override
protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
User user = new User("Test", "User");
binding.setUser(user);
}
a、Activity 数据绑定 (DataBinding) :
1、DataBindingUtil 类办法:
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
2、生成的布局绑定类的 inflate()办法:
ActivityMainBinding binding = ActivityMainBinding.inflate(getLayoutInflater());
@Override
protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);
//DataBindingUtil 类办法
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
// 生成的布局绑定类的 inflate()办法
//ActivityMainBinding binding = ActivityMainBinding.inflate(getLayoutInflater());
User user = new User("Test", "User");
binding.setUser(user);
}
b、Fragment、ListView 或 RecyclerView 适配器中应用数据绑定 (DataBinding)
DataBindingUtil 或 生成的布局绑定类 deinflate() 办法,如以下代码示例所示:
ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);
// or
ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);
Data Binding 绑定表达式:
xml 里反对应用以下表达式:
- 算术运算符 + – / * %
- 字符串连贯运算符 +
- 逻辑运算符 && ||
- 二元运算符 & | ^
- 一元运算符 + – ! ~
- 移位运算符 >> >>> <<
- 比拟运算符 == > < >= <=(请留神,< 须要本义为 <)
- instanceof
- 分组运算符 ()
- 字面量运算符 – 字符、字符串、数字、null
- 类型转换
- 办法调用
- 字段拜访
- 数组拜访 []
- 三元运算符 ?:
不反对以下表达式:
- this
- super
- new
- 显式泛型调用
a、变量
1、生成的数据绑定代码会主动查看有没有 null 值并避免出现 Null 指针异样。
2、例如,在表达式 @{user.name} 中,如果 user 为 Null,则为 user.name 调配默认值 null。
3、如果您援用 user.age,其中 age 的类型为 int,则数据绑定应用默认值 0。
<!-- 变量给控件赋值 -->
android:text="@{user.name}"
<!-- 控件给变量赋值(双向绑定)-->
android:text="@={user.name}"
b、Null 合并运算符(空运算符)
如果右边运算数不是 null,则 Null 合并运算符 (??) 抉择右边运算数,如果右边运算数为 null,则抉择左边运算数。
android:text="@{user.displayName ?? user.lastName}"
// 等效于如下三目表达式
android:text="@{user.displayName != null ? user.displayName : user.lastName}"
c、视图援用
1、表达式能够通过以下语法按 ID 援用布局中的其余视图:
2、绑定类将 ID 转换为驼峰式大小写。
3、在以下示例中,TextView 视图援用同一布局中的 EditText 视图:android:text=”@{exampleText.text}”
<EditText
android:id="@+id/example_text"
android:layout_height="wrap_content"
android:layout_width="match_parent"/>
<TextView
android:id="@+id/example_output"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{exampleText.text}"/>
d、显示暗藏管制
1、首先在 xml 的 data 节点中援用 View
2、而后设置 visibility
<data>
<import type="android.view.View"/>
</data>
android:visibility="@{student.boy ? View.VISIBLE : View.INVISIBLE}"
e、事件处理
办法援用:
==android:onClick=”@{handlers::onClickFriend}”==
绑定表达式可将视图的点击监听器调配给 MyHandlers 类的 onClickFriend() 办法,如下所示:
public class MyHandlers {public void onClickFriend(View view) {...}
}
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="handlers" type="com.example.MyHandlers"/>
<variable name="user" type="com.example.User"/>
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName}"
android:onClick="@{handlers::onClickFriend}"/>
</LinearLayout>
</layout>
留神:
1、在表达式中,您能够援用合乎监听器办法签名的办法。
2、当表达式求值后果为办法援用时,数据绑定会将办法援用和所有者对象封装到监听器中,并在指标视图上设置该监听器。
3、如果表达式的求值后果为 null,则数据绑定不会创立监听器,而是设置 null 监听器。
4、表达式中的办法签名必须与监听器对象中的办法签名完全一致。
监听器绑定:
==android:onClick=”@{() -> presenter.onSaveClick(task)}”==
绑定表达式可将视图的点击事件绑定打给 Presenter 类的 onSaveClick(Task task) 办法,如下所示:
public class Presenter {public void onSaveClick(Task task){}}
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="task" type="com.android.example.Task" />
<variable name="presenter" type="com.android.example.Presenter" />
</data>
<LinearLayout android:layout_width="match_parent" android:layout_height="match_parent">
<Button android:layout_width="wrap_content" android:layout_height="wrap_content"
android:onClick="@{() -> presenter.onSaveClick(task)}" />
</LinearLayout>
</layout>
以上,咱们尚未定义传递给 onClick(View) 的 view 参数。
监听器绑定提供两个监听器参数选项:您能够疏忽办法的所有参数,也能够命名所有参数。
如果您想命名参数,则能够在表达式中应用这些参数。
例如,下面的表达式能够写成如下模式:
android:onClick="@{(view) -> presenter.onSaveClick(task)}"
或者,如果您想在表达式中应用参数,则采纳如下模式:
public class Presenter {public void onSaveClick(View view, Task task){}}
android:onClick="@{(theView) -> presenter.onSaveClick(theView, task)}"
监听长按事件,表达式应返回一个布尔值。
public class Presenter {public boolean onLongClick(View view, Task task) {}}
android:onLongClick="@{(theView) -> presenter.onLongClick(theView, task)}"
留神:
1、监听器绑定这些是在事件产生时进行求值的 lambda 表达式。
2、数据绑定始终会创立一个要在视图上设置的监听器。
3、事件被分派后,监听器会对 lambda 表达式进行求值。
< END >
【Android 进化之路】
微信扫描二维码,关注我的公众号。