数据存储&网络通信|青训营笔记

199 阅读13分钟

网络请求&数据存储

这是我参加「第四届青训营」笔记创作活动的第三天,学习有关数据存储和网络通信有关知识。

课程回顾

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.网络库开源框架对比

请求方式作者包体积增量使用成本特点使用场景
HttpURLConnectionAndroid SDK0KB2n需要自己做封装,例如线程池管理、返回的数据解析只有少量网络请求的工具类app
VolleyGoogle57KBn1.适合网络请求频繁,传输数据量小 2.不适合用来上传文件和下载 (不支持输入输出流)3.已停更之前使用volley,且无需大文件下载的app
OkHttpsquare公司262KB1.5n1.可以设置拦截器,支持大文件上传和下载 2. OKHttp基于NIO和Okio,性能更好 3.一般需要二次封装使用一般比较少直接使用,可以搭配Volley或者Retrofit
Retrofitsquare公司343KB2n具备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的一小部分注解。

注解类型.png 通过这些注解的组合,可以实现更加丰富的功能。

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主流程

总结

网络请求.png

02.数据存储

了解本地储存的四大方式和使用场景,重点认识Room数据库

1.数据存储方式对比

持久性的本地数据存储时Android中常见的能力,可以在应用被杀死的情况下,二保持数据不会被清除。根据不同场景的诉求,选用不同的存储方式。

常见的数据存储主要有以下四种:

存储方式特点使用场景
SharedPreferences1.只能存boolean、int、float、long、String 5种简单类型 2.键值对存储记录app的各种配置信息,例如用户自己切换的开关、服务端下发的某个配置等
文件存储1.可以村各种格式的文件到手机中 2.默认情况下文件不能跨app共享1.网络下载的zip包 2.txt文件的存储
ContentProvider1.可以跨app进行数据共享 2.通过Uri对下进行访问音频、食品、图片、通信录的读写
SQLite存储数据1.可存储结构化数据 2.对数据及逆行增删改查较为方便保存feed六数据,并进行增删改查

2.数据库框架对比

相对来说,数据库的使用会先相对复杂些。

几个主流的数据库框架对比:

数据库包增量性能对比使用成本支持多表联合查询支持LiveData、协程支持数据库加密
Room165KBnnYesYesYes
GreenDao151KB0.5nnYesNoYes
ObjectBox1879KB0.3n0.6nNoNoNo

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个字段。

uidfirst_namelast_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.新建数据库类,进行数据库配置,并需满足以下条件:

  • 新增一个RoomDatabaseabstract子类
  • 子类需加注解@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 >中返回

总结

数据存储.png

引入问题解决

1.信息流的更新,要能无限刷新

使用网络请求,实时从服务端获取数据;可以按需选择合适的网络框架,也可以自己做一个二次封装

2.每次冷启,要能快速的展现信息流内容

使用数据库存储,将数据保存在本地,冷启时直接从数据库读取,直接进行数据解析和展示

补充