网络请求&数据存储
这是我参加「第四届青训营」笔记创作活动的第三天,学习有关数据存储和网络通信有关知识。
课程回顾
1.以下哪种动画适合实现类似GIF效果的动画(A)
A.帧动画 B.补间动画 C.属性动画 D.表情包动画
2.Android的四大组件有(ABCD)
A.界面组件:Activity/Fragment B.服务组件:Service C.广播组件:Broadcast Receiver D.数据组件:Content Provider E.应用组件:Application
3.线程之间的通信主要使用(A)进行
A.Handler B.Binder C.Socket D.Message
4.进程之间的通信主要使用(B)进行
A.Handler B.Binder C.Application D.Message
问题引入
假设我们要做一个今日头条,里面有大量的信息流内容,有以下要求,我们该如何做呢?
1.信息流的更新,要能无限刷新
2.每次冷启,要能快速的展现信息流内容
01.网络通信
大型app内容的获取都是通过网络的请求
1.网络库开源框架对比
| 请求方式 | 作者 | 包体积增量 | 使用成本 | 特点 | 使用场景 |
|---|---|---|---|---|---|
| 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等 | 团队内有研发人员对Retroift比较熟悉时可用 |
2.Retrofit介绍
Retrofit是目前Android平台上,可以说是最热门的网络请求封装框架,是对OkHttp的一个封装。
Retrofit快速使用
1.Retrofit库的引入
2.创建用于描述网络请求的接口
3.使用Retrofit实力发起网络请求
例如:客户端知道了一个用户的uid,想通过服务端查询这个用户的姓名,通过Retrofit如何实现呢?
假设接口:https://www.bytedance.com/users/{uid}/name
其中:{uid}要替换为实际的uid,例如123,
最终请求https://www.bytedance.com/users/123/name
类型:GET请求
接口返回:
{
"massage":"success",
"data":{
"uid":"123"
"first_name":"张"
"last_name":"三"
}
}
Retrofit使用介绍
1.在需要用到Retrofit接口的module中,新增依赖 最新版本:github.com/square/retr…
dependencies{
implementation 'com.squareup.retrofit2:retrofit:2.4.0'
//…其他依赖
}
2.创建用于描述网络请求的接口
interface IUserInfoService{
@GET("user/{uid}/name")
fun getUserName(@Path("uid") uid:Int): Call<ResponseBody>
//后续可以增加其他接口,一个接口对应一个api请求
}
定义说明:
-
接口类名:可自定义,尽量和这类请求的含义相关
-
函数名:可自定义,需要能识别出该接口的作用,该interface里可以增加多个不同的函数
-
@GET注解:用于指定该接口的
相对路径,并采用Get方法发起请求 -
@Path注解:需要外部调用时,传入一个uid,该uid会替换@GET注解里相对路径的{uid}
-
返回值
Call<ResponseBody>:这里用,我们可以直接拿到请求的String内容,如果要自动转为Model类,例如User,这里直接替换为User就好(ResponseBody:Retrofit自带的一个封装)
3.发起网络请求
3.1 创建Retrofit实例
3.2 创建请求接口的实例,并获取Call实例
3.3 调用call.enqueue进行异步请求
3.4 处理返回的数据
fun getUsername(View){
}
注解类型 在上面的IUserInfoService中,我们看到了@GET、@Path的注解,但这仅仅是Retrofit的一小部分注解。
通过这些注解的组合,可以实现更加丰富的功能。
3.TTNET介绍
字节跳动用的是哪个网络库?
技术选型背景:
- 研发:希望用法稳定,不要经常变化,最好是用上优秀的Retrofit
- 产品:为了保护用户隐私,公司内所有app的网络请求,都需要去掉敏感参数,做好封装(可快速复用)
- 技术TL:公司内部所有app都得做好网络各个阶段的性能监控,并不断优化(深度定制)
- cornet:公司优秀的网络库,做了非常多优化,比OkHttp还适合业务的需求,不能抛弃(复用cornet)
Cornet与OkHttp区别: Cornet比OkHttp多了不少优化,在请求成功率和网络延时等方面都有不少又是,所以我们希望能够继续使用Cornet。 但Cornet是C++实现的,不方便直接使用,需要进行二次封装。
目标:打造一款字节跳动自己的网络库,并提供给字节跳动内部所有app使用
要求: 1.使用Cornet,进行二次封装 2.封装的接口,要有高易用性 3.使用app多,功能要全面
综合考虑:Retrofit是一款优秀的网络封装库,应用了较多的设计模式,有较多的优势,满足我们的诉求;我们基于Retrofit进行二次开发,并替换Retrofit底层的OkHttp为Cornet是一个比较合理的方案。
字节跳动的网络库——TTNet
TTNet字节内部大部分app都在使用的网络请求框架
有以下优点:
- 基于Retrofit改造,具备了Retrofit所具有的优点
- 支持多个Http网络库的动态切换(OkHttp和Cornet)
- 支持网络拦截配置:添加公共参数,动态切换协议及Host,动态选路等
- 支持流解析,json序列化
- ……
Retrofit与TTNet简单用法对比: Retrofit
TTNet
由以上对比发现用法基本一致
那TTNet是怎么把Retrofit底层的OkHttp网络库给改了的呢?
思路: 1.先明确Retrofit是怎么封装和使用OkHttp发起网络请求的 2.既然TTNet基于Retrofit修改,那可以在使用OkHttp的地方都改为使用Cornet来发起网络请求
注解介绍:
Retrofit用到了非常多的注解。
注解,也可以理解为是一个标签,这个标签可以加载类、方法、参数、成员变量上,并且可在合适的实际读取注解的内容,进行处理。
例如,我们最常见的是
@Override:标注一个方法是重写了父类的实现
@Nullable:标注所描述对象有可能为空
例如我们在定义这个描述请求的接口时, 有修饰方法名的@GET注解, 有修复参数的@Path注解
interface IUserInfoService{
@GET("user/{uid}/name")
fun getUserName(@Path("uid") uid:Int): Call<ResponseBody>
//后续可以增加其他接口,一个接口对应一个api请求
}
有定义和使用注解的地方,肯定还需要获取注解并处理注解内容的地方,不然注解没啥作用。
注解的处理,一般有3个时机(也就是注解的生命周期@Retention)
1.SOURCE:只有在源码中有效,编译时抛弃,例如@Override
2.CLASS:编译class文件时有效,一般会使用到注解处理器(APT)
3.RUNTIME:在运行期间,获取对应的注解,并做对应的处理
Retrofit注解@GET定义
@Documented
@Target(METHOD)
@Retention(RUNTIME)
//新建注解类
public @interface GET{
String value() default "";
}
@Target:指定作用的对象,这里是METHOD,说明这个注解是作用在方法上
其他枚举值:
- PARAMETER:参数
- FIELD:类成员
- ……
@Retention:指定注解的生命周期,这里是RUNTIME,说明这个注解要一直保留到运行时
注解的获取和使用
通过反射获取到Method对象后,有以下一些接口来获取注解内容
1.Method.getGenericReturnType()获取返回类型
2.Method.getAnnotations()获取方法的注解
3.Method.getParamaterAnnotations()获取参数注解
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(新增一个代理对象)
- 利用Java的反射技术(Java Reflection),代理某个interface,一旦调用interface里的某个方法时,实际通过代理调用InvocationHandler的invoke方法
- 通过Method对象,就可以调用Method.getAnnotations()和Method.getParameterAnnotations()来获取该方法和方法参数的注解内容
public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,
InvocationHandler h) throws IllegalArgumentException{}
public interface InvocationHandler{
public Object invoke(Object proxy,Method method,Object[] args) throws Throwable;
}
Retrofit主流程
主流程如下
- 1.通过Builder模式,创建RetrofitConfig,保存baseUrl等内容
- 2.创建动态代理对象
- 3.创建OkHttpCall
- 4.发起网络请求
Retrofit调用OkHttp
Retrofit里OkHttpClient创建时机
Retrofit的Buider构造行数中 如果未指定callFactory,则会自动创建一个OKHttpClient
PS:
创建好的OKHttpClient将会保存在Retrofit实例中
ExecutorCallAdapterFactory主要是用来控制Retrofit在子线程触发请求,在主线程回调结果
OkHttpCall的创建 当我们通过代理对象调用我们的接口方法IUserInfoService#getUserName()时,会触发InvocationHanlder#invoke方法。
TTNet类图设计
TTNet主流程
总结
02.数据存储
了解本地储存的四大方式和使用场景,重点认识Room数据库
1.数据存储方式对比
持久性的本地数据存储时Android中常见的能力,可以在应用被杀死的情况下,二保持数据不会被清除。根据不同场景的诉求,选用不同的存储方式。
常见的数据存储主要有以下四种:
| 存储方式 | 特点 | 使用场景 |
|---|---|---|
| SharedPreferences | 1.只能存boolean、int、float、long、String 5种简单类型 2.键值对存储 | 记录app的各种配置信息,例如用户自己切换的开关、服务端下发的某个配置等 |
| 文件存储 | 1.可以村各种格式的文件到手机中 2.默认情况下文件不能跨app共享 | 1.网络下载的zip包 2.txt文件的存储 |
| ContentProvider | 1.可以跨app进行数据共享 2.通过Uri对下进行访问 | 音频、食品、图片、通信录的读写 |
| SQLite存储数据 | 1.可存储结构化数据 2.对数据及逆行增删改查较为方便 | 保存feed六数据,并进行增删改查 |
2.数据库框架对比
相对来说,数据库的使用会先相对复杂些。
几个主流的数据库框架对比:
| 数据库 | 包增量 | 性能对比 | 使用成本 | 支持多表联合查询 | 支持LiveData、协程 | 支持数据库加密 |
|---|---|---|---|---|---|---|
| Room | 165KB | n | n | Yes | Yes | Yes |
| GreenDao | 151KB | 0.5n | n | Yes | No | Yes |
| ObjectBox | 1879KB | 0.3n | 0.6n | No | No | No |
3.Room数据库使用介绍
Room是Google Jetpack 家族里的一员,Room在SQLite上提供了一个抽象层,以便在充分利用SQLite的强大功能的同时,能够流畅地访问数据库。
graph
Client-->Room-->SupportSQLiteOpenHelper
Room数据库主要的3个组件:
1.数据库类(Database):用于保存数据并作为应用持久性数据底层连接的主要访问点。
2.数据实体(Entity):用于表示应用的数据库中的表
3.数据访问对象(DAO):提供您的应用可用于查询、更新、插入和删除数据库中的数据的方法
数据库Room实践案例
Room接入
1.Gradle目录的build.gradle文件里添加如下:
dependencies{
def room_version="2.4.2"
implementation "android.room:room-runtime:$room_version"
kapt "android.room:room_compile:$room_version"
//…其他依赖
}
2.数据表设计:假设设计一个表,表名为user,数据库包含uid、first_name、last_name 3个字段。
| uid | first_name | last_name |
|---|---|---|
| 1 | 张 | 三 |
| 2 | 李 | 四 |
| … | … | … |
| 89 | 唐 | 三 |
3.新建Entity:定义一个User数据实体,User的每个实例都代表app数据库中的user表的一行
@Entity
data class User(
@PrimaryKey(autoGenerate=true) val uis:Int?,
@ColumnInfo(name="first_name") var firstName:String?,
@ColumnInfo(name="last_name") var lastName:String?
)
PS:
-
所有属性必须是public或者有get、set方法,保证属性能够被外部访问
-
@PrimaryKey:表示单个主键,当主键值为null且autoGenerate为true时可以帮助自动生成键值
-
@ColumnInfo:列名的注解
4.新增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 usesr:User)
@Delete
fun delete(user:User)
}
5.新建数据库类,进行数据库配置,并需满足以下条件:
- 新增一个
RoomDatabase的abstract子类; - 子类需加注解@Database(entities=[xxx],version=n),entities包含数据实体,将会在这个数据库中创建独立的表,version是数据的版本号;
- 对于与数据库关联的每个DAO类,数据库类必须定义一个无参的抽象方法,并返回DAO类实例。
@Database(entities=[User::class],version=1)
abstract class AppDatabase:RoomDatabase(){
abstract fun userDao():UserDao
}
6.获取对象,即可进行数据库的增删改查操作
获取到dao对象后,可以直接调用dao里自定义的几个方法:insertAll(),getAll(),findByName(),delete(xxx)
val db=Room.databaseBuilder(applicationContext,AppDatabbase::class.java,"database-name")
userDao=db.userDao()
//插入
val user1=User(1,"张","三")
val user2=User(2,"李","四")
userDao.insertAll(user1,user2)
//查询:获取User表中所有数据
val userList=userDao.getAll()
//条件查询
val user=userDao.findByName("张","三")
//删除
if(user!=null){
userDao.delete(user)
}
更多使用方式,参考官网文档:使用 Room 将数据保存到本地数据库 | Android 开发者 | Android Developers
4.Room数据库原理介绍
核心
1.编译期,通过kapt处理@DAO、@Database注解,动态生成对应的实现类
2.底层使用Android提供的SupportSQLiteOpenHelper实现数据库的增删改查等操作
原理介绍
1.kapt注解处理
Room在编译期,通过kapt处理@DAO和@Database注解,生成DAO和Database实现类 AppDatabase-->AppDatabase_Impl UserDao-->UserDao_Impl
kapt生成的代码在build/generated/source/kapt/
AppDatabase_Impl:数据库实例的具体实现,自动生成,主要有以下方法:
- createOpenHelper():Room.databaseBuilder().build()创建Database时,会调用实现类的createOpenHelper()创建SupportSQLiteOpenHelper,此Helper用来创建DB以及管理版本。
- userDao():创建UserDao_Impl
UserDao_Impl:UserDao的具体实现,自动生成,主要有以下3个成员变量以及UserDao里定义的接口
3个核心成员变量:
- __db:RoomDatabase的实例
- __insertionAdapterOfUser:EntityInsertionAdapterd实例,用于数据insert
- __deletionAdapterOfUser:EntityDeletionOrUpdateAdapter实例,用于数据的更新update和删除delete
n个UserDao里我们自己定义的接口的具体实现
- insertAll()
- delete()
- getAll()
- loadAllByIds()
- findByNames()
UserDao_Impl#insertAll():
使用_db开启事务,使用_insertionAdapterOfUser执行插入操作
@Override
public void insertAll(final User...users){
__db.asserrNotSuspendingTransaction();
__db.beginTransaction();
try{
__insertionAdapterOfUser.insert(users);
__db.setTransactionSuccessful();
}finally{
__db.endTransaction();
}
}
UserDao_Impl#delete():
使用_db开启事务,使用_deletionAdapterOfUser执行删除操作
@Override
public void detele(final User user){
__db.asserrNotSuspendingTransaction();
__db.beginTransaction();
try{
__deletionAdapterOfUser.insert(users);
__db.setTransactionSuccessful();
}finally{
__db.endTransaction();
}
}
UserDao_Impl#getAll():
(1) 自动生成sql语句“SELECT * FROM USER"
(2) 获取到数据库的Cursor光标
(3) 使用cursor循环读取数据库的每条记录
(4) 读取Cursor里的内容,并保存在List< User >中返回
总结
引入问题解决
1.信息流的更新,要能无限刷新
使用网络请求,实时从服务端获取数据;可以按需选择合适的网络框架,也可以自己做一个二次封装
2.每次冷启,要能快速的展现信息流内容
使用数据库存储,将数据保存在本地,冷启时直接从数据库读取,直接进行数据解析和展示