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

381 阅读8分钟

这是我参与「第四届青训营 」笔记创作活动的第7天

数据存储 & 网络通信 课堂笔记

课程回顾

看回放的 PPT 一共有三道题目,但是讲课时只提了一道:

这里就直接放知识点了:

① 安卓的四大组件:界面组件Avtivity&Fragment、服务组件 Service、广播组件 Broadcast、数据组件 ContentProvider&ContentResolver

② 线程间的通信主要通过 Handler

③ 进程间的通信主要通过 Binder

网络请求

简介

  • 客户端向服务端发起请求,服务端返回数据给到客户端
  • 由于网络请求在大型App里使用非常频繁,为了更好的支持业务迭代,一般会进行网络请求的封装

网络框架对比

请求方式作者包体积增量使用成本特点使用场景
HttpURLConnectionAndroid SDK0KB2n需要自己封装,例如线程池管理、返回的数据解析只有少量网络请求的工具类 APP
VolleyGoogle57KBn1. 适合网络请求频繁,传输数据量小 2. 不适合用来上传文件和下载 3. 官方已停止更新之前用了 Volley,并且没有新需求和大文件下载的APP
OkHttpsquare262KB1.5n1. 可以设置拦截器,支持大文件的上传和下载 2. 基于 NIO 和 Okio,性能更好一般进行封装后使用
Retrofitsquare343KB2n对 OkHttp 进行了封装,具备OkHttp 所有的优点熟悉就可用,推荐使用

说明:

  • Volley的Request和Response都是把数据方法放到byte[]数组里,不支持输入输出流,把数据放到数组中,如果大文件多了,数组就会非常大且多,消耗内存
  • 行业内,目前基本上都是Retrofit 和 OkHttp组合的这种方式来进行网络请求
  • IO 和 NIO这两个都是Java中的概念,如果我从硬盘读取数据,第一种方式就是程序一直等,数据读完后才能继续操作这种是最简单的也叫阻塞式IO,还有一种是你读你的,程序接着往下执行,等数据处理完你再来通知我,然后再处理回调。而第二种就是 NIO 的方式,非阻塞式, 所以NIO当然要比IO的性能要好了,而 Okio是 Square 公司基于IO和NIO基础上做的一个更简单、高效处理数据流的一个库。

总结:

  • 目前Retrofit和OkHttp的组合,功能更加全面,封装更加彻底,当下最为流行的网络请求方式,我们本文也会重点来关注Retrofit的使用和原理的介绍。

Retrofit的使用介绍

Retrofit其实是对OkHttp的一个封装,也是当前最为流行的一种网络请求组合方式。

使用举例

场景假设:客户端知道了一个用户的uid,想通过服务端查下这个用户的姓名,通过Retrofit如何实现呢? 接口:www.bytedance.com/users/{uid}… 其中{uid}要替换为实际的uid,例如1123,最终请求为www.bytedance.com/users/1123/…

类型:GET请求 接口返回:

 {
     "message": "success",
     "data": {
         "uid":"1123",
         "first_name":"张",
         "last_name":"三丰"
     }
 }

使用介绍

  • 添加Retrofit库的依赖

    • 在需要用到Retrofit接口的module中,新增依赖(最新的版本可看GitHub
 dependencies {
 implementation 'com.squareup.retrofit2:retrofit:2.4.0'
     //...其他依赖
 }
 复制代码
  • 创建 用于描述网络请求 的接口
 //接口类名:可自定义,尽量和这类请求的含义相关
 interface IUserInfoService {
 ​
     @GET("users/{uid}/name")
     fun getUserName(@Path("uid") uid: Int): Call<ResponseBody>
 ​
     //@GET("users/{name}/uid")
     //fun getRequest(@Path("name" name:String)) Call<User>
     
     //后续可以增加其他的接口,一个接口对应一个api请求
 }
 ​
 //函数名:可自定义,需要能识别出该接口的作用,该interface里可以增加多个不同的函数
 //@GET 注解:用于指定该接口的相对路径,并采用Get方法发起请求
 //@Path 注解:需要外部调用时,传入一个uid,该uid会替换@GET注解里相对路径的{uid}
 //返回值Call<ResponseBody>,这里用ResponseBody,我们可以直接拿到请求的String内容
 //如果要自动转为Model类,例如User,这里直接替换为User就好。
 复制代码
  • 发起网络请求
 fun getUserName(view: View) {
     //创建Retrofit实例
     val retrofit = Retrofit.Builder()
         .baseUrl("https://www.bytedance.com/")
         .build()
     //创建iUserInfoService实例
     val iUserInfoService = retrofit.create(IUserInfoService::class.java)
     //创建网络请求Call对象
     val call = iUserInfoService.getUserName(1123)
     //发起异步请求
     call.enqueue(object : Callback<ResponseBody> {
 ​
         override fun onResponse(call: Call<ResponseBody>,
                                 response: Response<ResponseBody>) {
             //请求成功时回调
             request_result_tv.text = "请求成功:" + response.body()!!.string()
         }
 ​
         override fun onFailure(call: Call<ResponseBody>, e: Throwable) {
             //请求失败时候的回调
             request_result_tv.text = "请求失败:" + e.message
         }
     })
 }
 复制代码

总结

  • 引入依赖库

  • 创建 用于描述网络请求 的接口

  • 发起网络请求

    • 创建Retrofit实例
    • 创建iUserInfoService实例
    • 创建网络请求Call对象
    • 使用Call对象发起异步请求
  • 其他更多的用法,更多的注解,可以看Retrofit官网square.github.io/retrofit/

TTNet

TTNet是字节跳动通用的网络请求封装框架,用来向服务端发起请求。

目前Android端上的头条,抖音,番茄小说都在使用该库作为网络请求封装框架,作为字节内部的基础库,提供了一整套Android客户端网络请求解决方案。

  • 它有哪些突出的优点:
    • 基于Retrofit改造,具备了Retrofit所具有的优点
    • 支持多个Http网络库的动态切换(okhttp和cronet)
    • 支持网络拦截配置:添加公共参数,动态切换协议及Host,动态选路等
    • 支持流解析,json序列化

为什么要做TTNet?

Retrofit作为网络封装框架,已经足够优秀了,那么我们为什么还要基于Retrofit改造做我们自己的TTNet网络封装框架呢?

Retrofit是开源的网络框架,我们只能被动依赖,无法主动修改以适应我们的业务发展。如:Retrofit只支持OkHttp网络库,不支持其他网络库。

那么,问题来了。我们有更优秀的网络库cronet,我们想使用它,但是我们又迷恋Retrofit的优秀的封装特性和接口调用的简易特性?

答案:打造自己维护的网络框架 TTNet

TTNet与Retrofit的使用对比

Retrofit使用OkHttp的流程介绍

Retrofit的主流程
Retrofit调用底层OkHttp的方式

暂时无法在飞书文档外展示此内容

在主流程图中,okhttp网络库的api,在Retrofit中是何时发起请求的,对应api已列出,相同颜色即意味着调用关系。

如:

绿色代码:标记1处,创建Retrofit对象时,对应创建了OkHttpClient对象;

红色代码:标记4处,使用RetrofitCall对象发起请求时,对应创建OkHttp网络库中Request对象和Call对象并发起请求。

Retrofit里的OkHttpClient的调用时机
 public void getRequest() {
     Retrofit retrofit = new Retrofit.Builder()
             .baseUrl("https://www.bytedance.com/")
             //.addCallAdapterFactory(okhttp3.Call.Factory)
             .build() ;  // 标记(1)
 }
 ​
 // Retrofit$Builder
 public Retrofit build() {
   okhttp3.Call.Factory callFactory = this.callFactory;
   if (callFactory == null) {
     callFactory = new OkHttpClient() ; // 标记(2)
   }
     
   List<CallAdapter.Factory> adapterFactories = new ArrayList<>(this.adapterFactories);
   // platform为Android的实例
   // 集合里的实例为ExecutorCallAdapterFactory的实例。这里要留意下
   adapterFactories.add(platform.defaultCallAdapterFactory(callbackExecutor));
   return new Retrofit(callFactory, baseUrl, converterFactories, adapterFactories,
       callbackExecutor, validateEagerly);
 }
 ​
 static class Android extends Platform {
   @Override public Executor defaultCallbackExecutor() {
     return new MainThreadExecutor();
   }
   
   @Override CallAdapter.Factory defaultCallAdapterFactory(Executor callbackExecutor) {
     return new ExecutorCallAdapterFactory(callbackExecutor);
   } 
 }
 ​
 // Retrofit
 public final class Retrofit {
   private  final okhttp3.Call.Factory callFactory;
   Retrofit(okhttp3.Call.Factory callFactory, HttpUrl baseUrl,
       List<Converter.Factory> converterFactories, List<CallAdapter.Factory> adapterFactories,
       Executor callbackExecutor, boolean validateEagerly) {
     this .callFactory = callFactory; // 标记(3)
   }
 }
 复制代码

OkHttpClient对象的创建,我们已经看到了。那么创建好之后,在何处完成调用呢? 我们往下走,答案就在下面的流程梳理里面。

不过,我们先留意下,adapterFactories集合里的对象。里面有一个默认的CallAdapter.Factory实例,它就是子类ExecutorCallAdapterFactory的实例。我们在后续解析请求方法时,会用到它。

OkHttpCall的创建

当我们通过代理对象调用我们的接口方法IUserNameService#getUserName时,会触发InvocationHandler#invoke方法。请求物料的封装细节较多,单独拉了一篇文档。可以参考:13.2请求物料的封装

 public void getRequest(){
     Retrofit retrofit = new Retrofit.Builder()
             .baseUrl("https://www.bytedance.com/")
             .build();
     IUserNameService iUserNameService = retrofit.create(IUserNameService.class);
     // 标记1
     Call<ResponseBody> call = iUserNameService.getUserName( 1123 );
 }
 ​
 // Retrofit
 public <T> T create(final Class<T> service) {
   
   return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
       new InvocationHandler() {
         private final Platform platform = Platform.get();
         // 标记(2)
         @Override public Object invoke(Object proxy, Method method, Object... args) {
           ServiceMethod serviceMethod = loadServiceMethod(method);
           OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args);
           return serviceMethod.callAdapter.adapt(okHttpCall);
         }
       });
 }
 复制代码

我们需要重点关注invoke方法中OkHttpCall对象的创建,serviceMethod.callAdapter.adapt(okHttpCall)中方法调用的实现,serviceMethod.callAdapter.adapt(okHttpCall)的返回值。一会儿会用到

okhttp.Request对象和okhttp.Call对象的创建及请求发起
 public void getRequest(){
     Retrofit retrofit = new Retrofit.Builder()
             .baseUrl("https://www.bytedance.com/")
             .build();
     IUserNameService iUserNameService = retrofit.create(IUserNameService.class);
     // call对象的取值为InvocationHandler#invoke()方法的返回值,默认为ExecutorCallbackCall对象
     Call<ResponseBody> call = iUserNameService.getUserName(1123);
     // 标记1: 由请求物料的封装过程,我们知道 call为ExecutorCallbackCall的实例
     call.enqueue(new Callback<ResponseBody>() {
         @Override
         public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
 ​
         }
 ​
         @Override
         public void onFailure(Call<ResponseBody> call, Throwable t) {
 ​
         }
     });
 }
 ​
 // Retrofit
 public <T> T create(final Class<T> service) {
   
   return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
       new InvocationHandler() {
         private final Platform platform = Platform.get();
         // 标记(2)
         @Override public Object invoke(Object proxy, Method method, Object... args) {
           ServiceMethod serviceMethod = loadServiceMethod(method);
           OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args);
           return serviceMethod.callAdapter.adapt(okHttpCall);
         }
       });
 }
 ​
 // ExecutorCallbackCall
 @Override 
 public void enqueue(final Callback<T> callback) {
     // ExecutorCallbackCall实例所拥有的delegate为OkHttpCall对象。
   delegate.enqueue(new Callback<T>() {
     @Override 
     public void onResponse(Call<T> call, final Response<T> response) {
       // ...
     }
   });
 }
 ​
 //OkHttpCall 这里的内容是不是似曾相识,没错它就是OkHttp完成请求的过程。我们上面已贴出
 public void enqueue(final Callback<T> callback) {
   // 这里完成的了Request的创建
 Request request = serviceMethod.toRequest(args);
  // 这里完成了Call对象的创建
 okhttp3.Call call = serviceMethod.callFactory.newCall(request);
   // 通过call对象发起异步请求
   call.enqueue( new okhttp3.Callback() {
  @Override 
  public  void onResponse(okhttp3.Call call, okhttp3.Response rawResponse)
 throws IOException {
 Response<T> response = parseResponse(rawResponse);
 callSuccess(response);
 }
 ​
  @Override 
  public  void  onFailure ( okhttp3.Call call, IOException e ) {
 callback.onFailure(OkHttpCall.this, e);
 }
 ​
 });
 }
 ​
 // ServiceMethod
 Request toRequest(Object... args) throws IOException {
   RequestBuilder requestBuilder = new RequestBuilder(httpMethod, baseUrl, relativeUrl, headers,
       contentType, hasBody, isFormEncoded, isMultipart);
 ParameterHandler<Object>[] handlers = (ParameterHandler<Object>[]) parameterHandlers;
   for (int p = 0; p < argumentCount; p++) {
     handlers[p].apply(requestBuilder, args[p]);
   }
   return requestBuilder.build();
 }
 复制代码

OkHttpCall#enqueue方法里,通过ServiceMethod#toRequest方法,完成了Request的创建。因为ServiceMethod拥有接口方法IUserNameService#getUserName的全部请求物料信息,传入请求实参后,便可直接构造request对象。

构建完request对象后,serviceMethod.callFactoryOkHttpClient实例。通过OkHttpClient#newCall方法构建Call对象。

构建Call对象完成后,通过Call对象发起异步请求。

请求完毕后,返回响应对象Response。到此OkHttp请求调用过程完毕。

到这里Retrofit只支持OkHttp网络库就得到了解答:

通过代理对象对方法接口进行调用时,会在InvocationHandler#invoke方法回调里为serviceMethod.callAdapter.adapt()设置okHttpCall对象,进而发起请求时,委托OkHttpCall对象发起网络请求。而OkHttpCall#enqueue 调用okHttp网络库发起请求。

所以serviceMethod.callAdapter.adapt()设置死了为okHttpCall对象。Retrofit发起请求只能对OkHttp网络库进行支持了。

 public <T> T create(final Class<T> service) {
   
   return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
       new InvocationHandler() {
         private final Platform platform = Platform.get();
         // 标记(2)
         @Override public Object invoke(Object proxy, Method method, Object... args) {
           ServiceMethod serviceMethod = loadServiceMethod(method);
           OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args);
           return serviceMethod.callAdapter.adapt(okHttpCall);
         }
       });
 }
 复制代码

所以我们要支持多个网络库的改造,那么就在**OkHttpCall** 的改造上做文章就好。

总结

  • 关于命名的大多数规范核心在于考虑上下文
  • 人们在阅读理解代码的时候也可以看成是计算机运行程序,好的命名能让人把关注点留在主流程上,清晰地理解程序的功能,避免频繁切换到分支细节,增加理解成本

1.5 TTNet实现原理

TTNet基于Retrofit的进行二次开发,核心主要是替换Retrofit其中的2点

  • 替换底层用到的OKHttpClient
  • 替换底层用到的OKHttpCall
TTNet主流程

网络请求总结

数据存储

简介

  • 数据存储的方式有很多种,其使用场景也不一样,本文主要介绍其中4种最为常见的数据存储方式和使用场景
  • 数据库是4种常见数据存储方式中较为复杂的能力,本文也会重点介绍

存储方式对比

持久性的本地数据存储是Android中常见的能力,可以在应用被杀死的情况下,而保持数据不会被清除。我们可以根据不同场景的诉求,可以选用不同的存储方式,常见的数据存储主要有以下4种。

数据库开源框架对比

数据库 相对来说,其使用会比较复杂些,我们单独进行探索,下面是几个主流的数据库框架对比

不同的产品,对功能的诉求不太应用,头条因为也有用到LiveData,同时考虑到是Google出品,其流行度和稳定性都有较好的保障,所以更倾向于使用Room数据库,下面会重点介绍下Room数据库。

Room数据库的使用

Room是 Google Jetpack 家族里的一员,Room 在 SQLite 上提供了一个抽象层,以便在充分利用 SQLite 的强大功能的同时,能够流畅地访问数据库

主要的3个组件

  • 数据库类(Database),用于保存数据库并作为应用持久性数据底层连接的主要访问点。
  • 数据实体(Entity),用于表示应用的数据库中的表。
  • 数据访问对象(DAO),提供您的应用可用于查询、更新、插入和删除数据库中的数据的方法。

Room接入

Gradle目录的build.gradle文件里添加如下:

数据表设计

下面假设设计一个表,表名为user,数据表包含uidfirst_namelast_name 3个字段

新建Entity

定义一个User数据实体,User的每个实例都代表App数据库中的user表的一行

新增DAO

定义一个名为UserDao的DAO。用来对User表的增删改查

新建数据库类

进行数据库配置,并需满足以下几个条件:

  • 新增一个RoomDatabaseabstract子类
  • 子类需加注解@Database(entities = [xxx], version = n)entities包含数据实体,将会在这个数据库中创建对应的表,version是数据的版本号
  • 对于与数据库关联的每个DAO类,数据库类必须定义一个无参的抽象方法,并返回DAO类实例

获取dao对象

可进行数据库的增删改查操作

Room原理介绍

核心

  • 编译期,通过kapt处理@``Dao@Database注解,动态生成对应的实现类
  • 底层使用Android提供的SupportSQLiteOpenHelper实现数据库的增删改查等操作

kapt注解处理

Room在编译期,通过kapt处理@Dao和@Database注解,生成DAO和Database的实现类 AppDatabase --> AppDatabase_Impl UserDao-->UserDao_Impl

kapt生成的代码在build/generated/source/kapt/

实现类利用Android SQLite进行数据库操作

AppDatabase_Impl:数据库实例的具体实现,自动生成,主要有以下几个方法

 createOpenHelper()`: `Room.databaseBuilder().build()`创建Database时,会调用实现类的 `createOpenHelper()`创建`SupportSQLiteOpenHelper`,此Helper用来创建DB以及管理版本 `userDao()`:创建`UserDao_Impl

UserDao_ImplUserDao的具体实现,自动生成,主要有以下3个属性以及UserDao里定义的接口

 __db`:`RoomDatabase`的实例 `__insertionAdapterOfUser` :`EntityInsertionAdapterd`实例,用于数据`insert` `__deletionAdapterOfUser`:`EntityDeletionOrUpdateAdapter`实例,用于数据的`update/delete

以下几个是UserDao里我们自己定义的接口

insertAll(): 使用__db开启事务,使用__insertionAdapterOfUser执行插入操作

delete():使用__db开启事务,使用__deletionAdapterOfUser执行删除操作 getAll():使用Cursor循环读取数据库的每条记录,并将结果保存在List<User>中返回 loadAllByIds():和getAll()类似,查询语句不同 findByNames():和getAll()类似,查询语句不同

数据库总结

参考资料