这是我参与「第四届青训营 」笔记创作活动的第4天
Android应用软件的网络通信与数据存储 | 青训营笔记
App通过网络请求或读取本地资源来获取内容。
网络请求框架对比
| 请求方式 | 作者 | 包体积增量 | 使用成本 | 特点 | 使用场景 |
|---|---|---|---|---|---|
| HttpURLConnection | Android sdk | 0KB | 2n | 需要自己封装,例如线程池管理、返回的数据解析 | 只有少量网络请求的工具类App |
| Volley | 57KB | n | 1. 适合网络请求 2. 不适合用来上传文件和下载 3. 已停更 | 之前使用volley,且无需大文件下载的App | |
| OkHttp | square公司 | 262KB | 1.5n | 1. 可以设置拦截器,支持大文件上传和下载 2. OkHttp基于NIO和Okio,性能更好 3. 一般需要二次封装使用 | 一般比较少直接使用,可搭配Volley或Retrofit |
| Retrofit | square公司 | 343KB | 2n | 具备OkHttp所有优点,且更出色 1. restful api设计风格 2. 通过注解配置请求,包括请求方法、请求参数、请求头等 3. 可以搭配多种Converter将获得的数据解析,支持Gson、jackson、Protobur等 | 团队内有研发人员对Retrofit比较熟悉时可用 |
Retrofit
Retrofit是对OkHttp的封装。
使用方法:
- 引入Retrofit库
- 创建用于描述网络请求的接口
- 使用Retrofit实例发起网络请求
// 1. 引入Retrofit库
dependencies {
implementation 'com.squareUp.retrofit2:retrofit:2.4.0'
}
// 最新版本见github
// 2. 创建用于描述网络请求的接口
interface IUserInfoService {
@GET("users/{uid}/name")
fun getUserName(@Path("uid") uid : Int): Call<ResponseBody>
// 后续可以增加其他接口,一个接口对应一个api请求
}
/**
* 定义说明:
* 接口类名:可自定义,尽量和这类请求的含义相关
* 函数名:可自定义,需要能识别出该接口的作用。interface里可以增加多个不同的函数
* @GET注解:用于指定该接口的相对路径,并采用Get方法发起请求
* @Path注解:需要外部调用时,传入一个uid,该uid会替换@Get注解里的{uid}
* 返回值Call<ResponseBody>: 这里用ResponseBody,,可以直接拿到请求的String内容,如果要自动转为Model类,这里直接替换为Model类就好,比如User
*/
// 3. 发起网络请求
TTNet
注解
注解,可以理解为标签,可以加在类、方法、参数、成员变量上,并且可以在合适的时机读取注解的内容,进行处理。
常见注解有:@Override``@Nullable
描述请求接口时,有修饰方法名的@GET注解,有修饰参数的@Path注解
注解的处理一般有3个时机,也就是注解的生命周期@Retention
- SOURCE: 只有在源码中有效,编译时抛弃,例如
@Override - CLASS: 编译class文件时有效,一般会使用到注解处理器
- RUNTIME: 在运行期间,获取对应的注解,进行相关的处理
.java -> .class -> .dex -> .apk -> runtime
Retrofit注解@GET定义
@Document
@Target(METHOD)
@Retention(RUNTIME)
public @interface GET {
String value() default "";
}
注解的获取和使用
在运行阶段,通过反射获取到Method,有以下一些接口来获取注解的内容
Method.getGenericReturnType()获取返回类型Method.getAnnotations()获取方法的注解Method.getParameterAnnotations()获取参数注解
Method[] declaredMethods = IUserInfoService.class.getDeclaredMethods();
for (Method method : declaredMethods) {
Type type = method.getGenericReturnType(); // 正式返回类型
Annotation[] methodAnnotations = method.getAnnotations(); // 方法注解
Annotation[][] parameterAnnotationsArray = method.getparameterAnnotations(); // 方法参数注解
}
Retrofit是在运行期间,配合Java动态代理,获取方法和参数的注解,并构造Request对象的。
Java动态代理Proxy.newProxyInstance
- 利用反射技术,代理某个interface,一旦调用interface里的某个方法时,实际通过代理调用InvocationHandler的invoke方法
- 通过Method对象,调用Method.getAnnotations()和Method.getParameterAnnotation()来获取该方法和方法参数的注解内容。
Retrofit主流程
- 通过Builder模式,创建RetrofitConfig,保存baseUrl等内容
- 创建动态代理对象
- 创建OkHttpCall
- 发起网络请求
Retrofit调用OkHttp
网络通信总结
数据存储
数据存储方式对比
持久性的本地存储可以保证在应用被杀死的情况下,保持数据不会被清楚。常见的数据存储方式有以下4种:
| 存储方式 | 特点 | 使用场景 |
|---|---|---|
| SharedPreferences | 1.只能存boolean、int、float、long、String 2.键值对存储 | 记录app的各种配置信息,例如用户自己切换的开关、服务端下发的某个配置等 |
| 文件存储 | 1.可以存各种格式的文件到手机中 2.默认情况下文件不能跨app共享 | 1.网络下载的zip包 2.txt文件的存储 |
| ContentProvider | 1.可跨App进行数据共享 2.通过Uri对象进行访问 | 音频、视频、图片、通信录的读写 |
| SQLite存储数据 | 1.可存储结构化数据 2.对数据进行增删改查较为方便 | 保存feed流数据,并进行增删改查 |
| 数据库 | 包增量 | n | 使用成本 | 支持多表联合查询 | 支持LiveDate、协程 | 支持数据库加密 | 作者 |
|---|---|---|---|---|---|---|---|
| Room | 165KB | n | n | Yes | Yes | Yes | |
| GreenDao | 151KB | 0.5n | n | Yes | No | Yes | 三方 |
| ObjectBox | 1879KB | 0.3n | 0.6n | No | No | No | 三方 |
Room介绍和实践案例
Room是在SQLite上提供的抽象层,以便在充分利用SQLite的强大功能的同时,能够流畅地访问数据库。
Room有3个主要组件:
- 数据库类
Datebase,用于保存数据并作为应用持久性数据底层连接的主要访问点。 - 数据实体
Entity,用于表示应用的数据库中的表。 - 数据访问对象DateAccessObject
DAO,提供可用于增删改查的方法
Room接入: ① Gradle目录的build.gradle文件里添加
dependencies {
def room_version = "2.4.2"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
}
② 数据表设计
uid | first_name | last_name
----------------------------
1 | 傅 | 红雪
2 | 沈 | 孤雁
3 | 独孤 | 飞云
...
88 | 燕 | 南飞
③ 新建Entity:定义一个User数据实体,User的每个实例都代表App数据库中的user表的一行
@Entity
data class User(
@PrimaryKey(autoGenerate = true) val uid: Int?,
@ColumnInfo(name = "first_name") var firstName: String?,
@ColumnInfo(name = "last_name") var lastName: String?)
PS:
- 所有属性必须是public、或者有get、set方法
@PrimaryKey:表示单个主键,当主键值为null且autoGeneriate为true时可以帮助自动生成键值@ColumnInfo: 列名的注释
④ 新增DAO:定义一个名为UserDao的DAO,用来对User表的增删改查
@Dao
interface UserDao {
@Query("SELECT * FROM user")
fun getAll(): List<User>?
@Query("SELECT * FROM user WHERE uid IN (:userIds)")
fun loadAllByIds(userIds: IntArray): List<User>?
@Query("SELECT * FROM user WHERE first_name LIKE :first AND last_name LIKE :last")
fun findByName(first: String, last: String): User?
@Insert
fun insertAll(vararg users: User)
@Delete
fun delete(user: User)
}
⑤ 新建数据库类,进行数据库配置,并需满足以下几个条件(配置数据库):
- 新增一个RoomDatebase的抽象子类
- 子类需加注解
@Database(entities = [xxx], version = n),entities包含数据实体,将会在这个数据库中创建对应的表,version是数据的版本号 - 对于与数据库关联的每个
DAO类,数据库必须定义一个无参抽象方法,并返回DAO实例
@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
⑥获取DAO对象,即可进行数据库的增删改查操作
val db = Room.databaseBuilder(applicationContext, AppDatabase::class.java, "database-name").build()
userDao = db.userDao()
// 插入数据库
val user1 = User(1, "傅", "红雪")
val user2 = User(2, "沈", "孤雁")
val user3 = User(3, "独孤", "飞云")
userDao.insertAll(user1, user2, user3)
// 查询:获取User表中所有的数据
val userList = userDao.getAll()
// 条件查询
val user = userDao.findByName("沈", "孤雁")
// 删除
if (user != null) {
userDao.delete(user)
}