月度归档: 2021 年 8 月

Android:手把手带你深入剖析 Retrofit 2.0 源码

Android:手把手带你深入剖析 Retrofit 2.0 源码

前言

  • Andrroid开发中,网络请求十分常用
  • 而在Android网络请求库中,Retrofit是当下*热的一个网络请求库

Github截图

  • 今天,我将手把手带你深入剖析Retrofit v2.0的源码,希望你们会喜欢

在阅读本文前,建议先阅读文章:这是一份很详细的 Retrofit 2.0 使用教程(含实例讲解)


目录

目录


1. 简介

Retrofit简介

特别注意:

  • 准确来说,Retrofit 是一个 RESTful 的 HTTP 网络请求框架的封装。
  • 原因:网络请求的工作本质上是 OkHttp 完成,而 Retrofit 仅负责 网络请求接口的封装

流程图

  • App应用程序通过 Retrofit 请求网络,实际上是使用 Retrofit 接口层封装请求参数、Header、Url 等信息,之后由 OkHttp 完成后续的请求操作
  • 在服务端返回数据之后,OkHttp 将原始的结果交给 Retrofit,Retrofit根据用户的需求对结果进行解析

2. 与其他网络请求开源库对比

除了Retrofit,如今Android中主流的网络请求框架有:

  • Android-Async-Http
  • Volley
  • OkHttp

下面是简单介绍:

网络请求加载 - 介绍

一图让你了解全部的网络请求库和他们之间的区别!

网络请求库 - 对比


附:各个主流网络请求库的Github地址

  • Android-Async-Http
  • Volley
  • OkHttp
  • Retrofit

3. Retrofit 的具体使用

具体请看我写的文章:这是一份很详细的 Retrofit 2.0 使用教程(含实例讲解)


4. 源码分析

4.1 Retrofit的本质流程

一般从网络通信过程如下图:

网络请求的过程

  • 其实Retrofit的本质和上面是一样的套路
  • 只是Retrofit通过使用大量的设计模式进行功能模块的解耦,使得上面的过程进行得更加简单 & 流畅

如下图:

Retrofit的本质

具体过程解释如下:

  1. 通过解析 网络请求接口的注解 配置 网络请求参数
  2. 通过 动态代理 生成 网络请求对象
  3. 通过 网络请求适配器 将 网络请求对象 进行平台适配

    平台包括:Android、Rxjava、Guava和java8

  4. 通过 网络请求执行器 发送网络请求
  5. 通过 数据转换器 解析服务器返回的数据
  6. 通过 回调执行器 切换线程(子线程 ->>主线程)
  7. 用户在主线程处理返回结果

下面介绍上面提到的几个角色

角色说明

特别注意:因下面的 源码分析 是根据 使用步骤 逐步带你debug进去的,所以必须先看文章这是一份很详细的 Retrofit 2.0 使用教程(含实例讲解)

4.2 源码分析

先来回忆Retrofit的使用步骤:
1. 创建Retrofit实例
2. 创建 网络请求接口实例 并 配置网络请求参数
3. 发送网络请求

封装了 数据转换、线程切换的操作
4. 处理服务器返回的数据

4.2.1 创建Retrofit实例

a. 使用步骤

 Retrofit retrofit = new Retrofit.Builder()
                                 .baseUrl("http://fanyi.youdao.com/")
                                 .addConverterFactory(GsonConverterFactory.create())
                                 .build();

 

b. 源码分析

Retrofit实例是使用建造者模式通过Builder类进行创建的

建造者模式:将一个复杂对象的构建与表示分离,使得用户在不知道对象的创建细节情况下就可以直接创建复杂的对象。具体请看文章:建造者模式(Builder Pattern)- *易懂的设计模式解析

接下来,我将分五个步骤对创建Retrofit实例进行逐步分析

分析步骤

步骤1

步骤1

<-- Retrofit类 -->
 public final class Retrofit {

  private final Map<Method, ServiceMethod> serviceMethodCache = new LinkedHashMap<>();
  // 网络请求配置对象(对网络请求接口中方法注解进行解析后得到的对象)
  // 作用:存储网络请求相关的配置,如网络请求的方法、数据转换器、网络请求适配器、网络请求工厂、基地址等

  private final HttpUrl baseUrl;
  // 网络请求的url地址

  private final okhttp3.Call.Factory callFactory;
  // 网络请求器的工厂
  // 作用:生产网络请求器(Call)
  // Retrofit是默认使用okhttp

   private final List<CallAdapter.Factory> adapterFactories;
  // 网络请求适配器工厂的集合
  // 作用:放置网络请求适配器工厂
  // 网络请求适配器工厂作用:生产网络请求适配器(CallAdapter)
  // 下面会详细说明


  private final List<Converter.Factory> converterFactories;
  // 数据转换器工厂的集合
  // 作用:放置数据转换器工厂
  // 数据转换器工厂作用:生产数据转换器(converter)

  private final Executor callbackExecutor;
  // 回调方法执行器

private final boolean validateEagerly; 
// 标志位
// 作用:是否提前对业务接口中的注解进行验证转换的标志位


<-- Retrofit类的构造函数 -->
Retrofit(okhttp3.Call.Factory callFactory, HttpUrl baseUrl,  
      List<Converter.Factory> converterFactories, List<CallAdapter.Factory> adapterFactories,  
      Executor callbackExecutor, boolean validateEagerly) {  
    this.callFactory = callFactory;  
    this.baseUrl = baseUrl;  
    this.converterFactories = unmodifiableList(converterFactories); 
    this.adapterFactories = unmodifiableList(adapterFactories);   
    // unmodifiableList(list)近似于UnmodifiableList<E>(list)
    // 作用:创建的新对象能够对list数据进行访问,但不可通过该对象对list集合中的元素进行修改
    this.callbackExecutor = callbackExecutor;  
    this.validateEagerly = validateEagerly;  
  ...
  // 仅贴出关键代码
}

 

成功建立一个Retrofit对象的标准:配置好Retrofit类里的成员变量,即配置好:

  • serviceMethod:包含所有网络请求信息的对象
  • baseUrl:网络请求的url地址
  • callFactory:网络请求工厂
  • adapterFactories:网络请求适配器工厂的集合
  • converterFactories:数据转换器工厂的集合
  • callbackExecutor:回调方法执行器

所谓xxxFactory、“xxx工厂”其实是设计模式中工厂模式的体现:将“类实例化的操作”与“使用对象的操作”分开,使得使用者不用知道具体参数就可以实例化出所需要的“产品”类。

具体请看我写的文章
简单工厂模式(SimpleFactoryPattern)- *易懂的设计模式解析
工厂方法模式(Factory Method)- *易懂的设计模式解析
抽象工厂模式(Abstract Factory)- *易懂的设计模式解析

这里详细介绍一下:CallAdapterFactory:该Factory生产的是CallAdapter,那么CallAdapter又是什么呢?

CallAdapter详细介绍

  • 定义:网络请求执行器(Call)的适配器
    1. Call在Retrofit里默认是OkHttpCall
    2. 在Retrofit中提供了四种CallAdapterFactory: ExecutorCallAdapterFactory(默认)、GuavaCallAdapterFactory、Java8CallAdapterFactory、RxJavaCallAdapterFactory
  • 作用:将默认的网络请求执行器(OkHttpCall)转换成适合被不同平台来调用的网络请求执行器形式
    1. 如:一开始Retrofit只打算利用OkHttpCall通过ExecutorCallbackCall切换线程;但后来发现使用Rxjava更加方便(不需要Handler来切换线程)。想要实现Rxjava的情况,那就得使用RxJavaCallAdapterFactoryCallAdapterOkHttpCall转换成Rxjava(Scheduler)
// 把response封装成rxjava的Observeble,然后进行流式操作
Retrofit.Builder.addCallAdapterFactory(newRxJavaCallAdapterFactory().create()); 
// 关于RxJava的使用这里不作更多的展开

 

  1. Retrofit还支持java8、Guava平台。
  • 好处:用*小代价兼容更多平台,即能适配更多的使用场景

所以,接下来需要分析的步骤2、步骤3、步骤4、步骤4的目的是配置好上述所有成员变量

步骤2

步骤2

我们先来看Builder类

请按下面提示的步骤进行查看

<-- Builder类-->
public static final class Builder {
    private Platform platform;
    private okhttp3.Call.Factory callFactory;
    private HttpUrl baseUrl;
    private List<Converter.Factory> converterFactories = new ArrayList<>();
    private List<CallAdapter.Factory> adapterFactories = new ArrayList<>();
    private Executor callbackExecutor;
    private boolean validateEagerly;

// 从上面可以发现, Builder类的成员变量与Retrofit类的成员变量是对应的
// 所以Retrofit类的成员变量基本上是通过Builder类进行配置
// 开始看步骤1

<-- 步骤1 -->
// Builder的构造方法(无参)
 public Builder() {
      this(Platform.get());
// 用this调用自己的有参构造方法public Builder(Platform platform) ->>步骤5(看完步骤2、3、4再看)
// 并通过调用Platform.get()传入了Platform对象
// 继续看Platform.get()方法 ->>步骤2
// 记得*后继续看步骤5的Builder有参构造方法
    }
...
}

<-- 步骤2 -->
class Platform {

  private static final Platform PLATFORM = findPlatform();
  // 将findPlatform()赋给静态变量

  static Platform get() {
    return PLATFORM;    
    // 返回静态变量PLATFORM,即findPlatform() ->>步骤3
  }

<-- 步骤3 -->
private static Platform findPlatform() {
    try {

      Class.forName("android.os.Build");
      // Class.forName(xxx.xx.xx)的作用:要求JVM查找并加载指定的类(即JVM会执行该类的静态代码段)
      if (Build.VERSION.SDK_INT != 0) {
        return new Android(); 
        // 此处表示:如果是Android平台,就创建并返回一个Android对象 ->>步骤4
      }
    } catch (ClassNotFoundException ignored) {
    }

    try {
      // 支持Java平台
      Class.forName("java.util.Optional");
      return new Java8();
    } catch (ClassNotFoundException ignored) {
    }

    try {
      // 支持iOS平台
      Class.forName("org.robovm.apple.foundation.NSObject");
      return new IOS();
    } catch (ClassNotFoundException ignored) {
    }

// 从上面看出:Retrofit2.0支持3个平台:Android平台、Java平台、IOS平台
// *后返回一个Platform对象(指定了Android平台)给Builder的有参构造方法public Builder(Platform platform)  --> 步骤5
// 说明Builder指定了运行平台为Android
    return new Platform();
  }
...
}

<-- 步骤4 -->
// 用于接收服务器返回数据后进行线程切换在主线程显示结果

static class Android extends Platform {

    @Override
      CallAdapter.Factory defaultCallAdapterFactory(Executor callbackExecutor) {

      return new ExecutorCallAdapterFactory(callbackExecutor);
    // 创建默认的网络请求适配器工厂
    // 该默认工厂生产的 adapter 会使得Call在异步调用时在指定的 Executor 上执行回调
    // 在Retrofit中提供了四种CallAdapterFactory: ExecutorCallAdapterFactory(默认)、GuavaCallAdapterFactory、Java8CallAdapterFactory、RxJavaCallAdapterFactory
    // 采用了策略模式

    }

    @Override 
      public Executor defaultCallbackExecutor() {
      // 返回一个默认的回调方法执行器
      // 该执行器作用:切换线程(子->>主线程),并在主线程(UI线程)中执行回调方法
      return new MainThreadExecutor();
    }

    static class MainThreadExecutor implements Executor {

      private final Handler handler = new Handler(Looper.getMainLooper());
      // 获取与Android 主线程绑定的Handler 

      @Override 
      public void execute(Runnable r) {


        handler.post(r);
        // 该Handler是上面获取的与Android 主线程绑定的Handler 
        // 在UI线程进行对网络请求返回数据处理等操作。
      }
    }

// 切换线程的流程:
// 1. 回调ExecutorCallAdapterFactory生成了一个ExecutorCallbackCall对象
//2. 通过调用ExecutorCallbackCall.enqueue(CallBack)从而调用MainThreadExecutor的execute()通过handler切换到主线程
  }

// 下面继续看步骤5的Builder有参构造方法
<-- 步骤5 -->
//  Builder类的构造函数2(有参)
  public  Builder(Platform platform) {

  // 接收Platform对象(Android平台)
      this.platform = platform;

// 通过传入BuiltInConverters()对象配置数据转换器工厂(converterFactories)

// converterFactories是一个存放数据转换器Converter.Factory的数组
// 配置converterFactories即配置里面的数据转换器
      converterFactories.add(new BuiltInConverters());

// BuiltInConverters是一个内置的数据转换器工厂(继承Converter.Factory类)
// new BuiltInConverters()是为了初始化数据转换器
    }

 

对Builder类分析完毕,总结:Builder设置了默认的

  • 平台类型对象:Android
  • 网络请求适配器工厂:CallAdapterFactory

    CallAdapter用于对原始Call进行再次封装,如Call到Observable

  • 数据转换器工厂: converterFactory
  • 回调执行器:callbackExecutor

特别注意,这里只是设置了默认值,但未真正配置到具体的Retrofit类的成员变量当中

步骤3

步骤3

还是按部就班按步骤来观看

<-- 步骤1 -->
public Builder baseUrl(String baseUrl) {

      // 把String类型的url参数转化为适合OKhttp的HttpUrl类型
      HttpUrl httpUrl = HttpUrl.parse(baseUrl);     

    // *终返回带httpUrl类型参数的baseUrl()
    // 下面继续看baseUrl(httpUrl) ->> 步骤2
      return baseUrl(httpUrl);
    }


<-- 步骤2 -->
    public Builder baseUrl(HttpUrl baseUrl) {

      //把URL参数分割成几个路径碎片
      List<String> pathSegments = baseUrl.pathSegments();   

      // 检测*后一个碎片来检查URL参数是不是以"/"结尾
      // 不是就抛出异常    
      if (!"".equals(pathSegments.get(pathSegments.size() - 1))) {
        throw new IllegalArgumentException("baseUrl must end in /: " + baseUrl);
      }     
      this.baseUrl = baseUrl;
      return this;
    }

 

  • 至此,步骤3分析完毕
  • 总结:baseUrl()用于配置Retrofit类的网络请求url地址

    将传入的String类型url转化为适合OKhttp的HttpUrl类型的url

步骤4

步骤4

我们从里往外看,即先看GsonConverterFactory.creat()

public final class GsonConverterFactory extends Converter.Factory {

<-- 步骤1 -->
  public static GsonConverterFactory create() {
    // 创建一个Gson对象
    return create(new Gson()); ->>步骤2
  }

<-- 步骤2 -->
  public static GsonConverterFactory create(Gson gson) {
    // 创建了一个含有Gson对象实例的GsonConverterFactory
    return new GsonConverterFactory(gson); ->>步骤3
  }

  private final Gson gson;

<-- 步骤3 -->
  private GsonConverterFactory(Gson gson) {
    if (gson == null) throw new NullPointerException("gson == null");
    this.gson = gson;
  }

 

  • 所以,GsonConverterFactory.creat()是创建了一个含有Gson对象实例的GsonConverterFactory,并返回给addConverterFactory()
  • 接下来继续看:addConverterFactory()

// 将上面创建的GsonConverterFactory放入到 converterFactories数组
// 在第二步放入一个内置的数据转换器工厂BuiltInConverters()后又放入了一个GsonConverterFactory
  public Builder addConverterFactory(Converter.Factory factory) {
      converterFactories.add(checkNotNull(factory, "factory == null"));
      return this;
    }

 

  • 至此,分析完毕
  • 总结:步骤4用于创建一个含有Gson对象实例的GsonConverterFactory并放入到数据转换器工厂converterFactories里
    1. 即Retrofit默认使用Gson进行解析
    2. 若使用其他解析方式(如Json、XML或Protocobuf),也可通过自定义数据解析器来实现(必须继承 Converter.Factory)

 

步骤5

步骤5

终于到了*后一个步骤了。

    public Retrofit build() {

 <--  配置网络请求执行器(callFactory)-->
      okhttp3.Call.Factory callFactory = this.callFactory;
      // 如果没指定,则默认使用okhttp
      // 所以Retrofit默认使用okhttp进行网络请求
      if (callFactory == null) {
        callFactory = new OkHttpClient();
      }

 <--  配置回调方法执行器(callbackExecutor)-->
      Executor callbackExecutor = this.callbackExecutor;
      // 如果没指定,则默认使用Platform检测环境时的默认callbackExecutor
      // 即Android默认的callbackExecutor
      if (callbackExecutor == null) {
        callbackExecutor = platform.defaultCallbackExecutor();
      }

 <--  配置网络请求适配器工厂(CallAdapterFactory)-->
      List<CallAdapter.Factory> adapterFactories = new ArrayList<>(this.adapterFactories);
      // 向该集合中添加了步骤2中创建的CallAdapter.Factory请求适配器(添加在集合器末尾)
      adapterFactories.add(platform.defaultCallAdapterFactory(callbackExecutor));
    // 请求适配器工厂集合存储顺序:自定义1适配器工厂、自定义2适配器工厂...默认适配器工厂(ExecutorCallAdapterFactory)

 <--  配置数据转换器工厂:converterFactory -->
      // 在步骤2中已经添加了内置的数据转换器BuiltInConverters()(添加到集合器的首位)
      // 在步骤4中又插入了一个Gson的转换器 - GsonConverterFactory(添加到集合器的首二位)
      List<Converter.Factory> converterFactories = new ArrayList<>(this.converterFactories);
      // 数据转换器工厂集合存储的是:默认数据转换器工厂( BuiltInConverters)、自定义1数据转换器工厂(GsonConverterFactory)、自定义2数据转换器工厂....

// 注:
//1. 获取合适的网络请求适配器和数据转换器都是从adapterFactories和converterFactories集合的首位-末位开始遍历
// 因此集合中的工厂位置越靠前就拥有越高的使用权限

      // *终返回一个Retrofit的对象,并传入上述已经配置好的成员变量
      return new Retrofit(callFactory, baseUrl, converterFactories, adapterFactories,
          callbackExecutor, validateEagerly);
    }

 

  • 至此,步骤5分析完毕
  • 总结:在*后一步中,通过前面步骤设置的变量,将Retrofit类的所有成员变量都配置完毕。
  • 所以,成功创建了Retrofit的实例

总结

Retrofit 使用建造者模式通过Builder类建立了一个Retrofit实例,具体创建细节是配置了:

 

  • 平台类型对象(Platform – Android)
  • 网络请求的url地址(baseUrl)
  • 网络请求工厂(callFactory)

默认使用OkHttpCall

  • 网络请求适配器工厂的集合(adapterFactories)

    本质是配置了网络请求适配器工厂- 默认是ExecutorCallAdapterFactory

  • 数据转换器工厂的集合(converterFactories)

    本质是配置了数据转换器工厂

  • 回调方法执行器(callbackExecutor)
    默认回调方法执行器作用是:切换线程(子线程 – 主线程)

由于使用了建造者模式,所以开发者并不需要关心配置细节就可以创建好Retrofit实例,建造者模式get。在创建Retrofit对象时,你可以通过更多更灵活的方式去处理你的需求,如使用不同的Converter、使用不同的CallAdapter,这也就提供了你使用RxJava来调用Retrofit的可能


2. 创建网络请求接口的实例

2.1 使用步骤

<-- 步骤1:定义接收网络数据的类 -->
<-- JavaBean.java -->
public class JavaBean {
  .. // 这里就不介绍了
  }

<-- 步骤2:定义网络请求的接口类 -->
<-- AccessApi.java -->
public interface AccessApi {
    // 注解GET:采用Get方法发送网络请求
    // Retrofit把网络请求的URL分成了2部分:1部分baseurl放在创建Retrofit对象时设置;另一部分在网络请求接口设置(即这里)
    // 如果接口里的URL是一个完整的网址,那么放在创建Retrofit对象时设置的部分可以不设置
    @GET("openapi.do?keyfrom=Yanzhikai&key=2032414398&type=data&doctype=json&version=1.1&q=car")

    // 接受网络请求数据的方法
    Call<JavaBean> getCall();
    // 返回类型为Call<*>,*是解析得到的数据类型,即JavaBean
}

<-- 步骤3:在MainActivity创建接口类实例  -->
AccessApi NetService = retrofit.create(AccessApi.class);

<-- 步骤4:对发送请求的url进行封装,即生成*终的网络请求对象  --> 
        Call<JavaBean> call = NetService.getCall();

2.2 源码分析

  • 结论:Retrofit是通过外观模式 & 代理模式 使用create()方法创建网络请求接口的实例(同时,通过网络请求接口里设置的注解进行了网络请求参数的配置)
    1. 外观模式:定义一个统一接口,外部与通过该统一的接口对子系统里的其他接口进行访问。具体请看:外观模式(Facade Pattern) – *易懂的设计模式解析
    2. 代理模式:通过访问代理对象的方式来间接访问目标对象。具体请看:代理模式(Proxy Pattern)- *易懂的设计模式解析
    3. 下面主要分析步骤3和步骤4:
<-- 步骤3:在MainActivity创建接口类实例  -->
AccessApi NetService = retrofit.create(NetService.class);

<-- 步骤4:对发送请求的url进行封装,即生成*终的网络请求对象  --> 
        Call<JavaBean> call = NetService.getCall();

步骤3讲解:AccessApi NetService = retrofit.create(NetService.class);

 public <T> T create(final Class<T> service) {

       if (validateEagerly) {  
      // 判断是否需要提前验证
      eagerlyValidateMethods(service); 
      // 具体方法作用:
      // 1. 给接口中每个方法的注解进行解析并得到一个ServiceMethod对象
      // 2. 以Method为键将该对象存入LinkedHashMap集合中
     // 特别注意:如果不是提前验证则进行动态解析对应方法(下面会详细说明),得到一个ServiceMethod对象,*后存入到LinkedHashMap集合中,类似延迟加载(默认)
    }  


        // 创建了网络请求接口的动态代理对象,即通过动态代理创建网络请求接口的实例 (并*终返回)
        // 该动态代理是为了拿到网络请求接口实例上所有注解
    return (T) Proxy.newProxyInstance(
          service.getClassLoader(),      // 动态生成接口的实现类 
          new Class<?>[] { service },    // 动态创建实例
          new InvocationHandler() {     // 将代理类的实现交给 InvocationHandler类作为具体的实现(下面会解释)
          private final Platform platform = Platform.get();

         // 在 InvocationHandler类的invoke()实现中,除了执行真正的逻辑(如再次转发给真正的实现类对象),还可以进行一些有用的操作
         // 如统计执行时间、进行初始化和清理、对接口调用进行检查等。
          @Override 
           public Object invoke(Object proxy, Method method, Object... args)
              throws Throwable {

            // 下面会详细介绍 invoke()的实现
            // 即下面三行代码
            ServiceMethod serviceMethod = loadServiceMethod(method);     
            OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args);
            return serviceMethod.callAdapter.adapt(okHttpCall);
          }
        });
  }

// 特别注意
// return (T) roxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces,  InvocationHandler invocationHandler)
// 可以解读为:getProxyClass(loader, interfaces) .getConstructor(InvocationHandler.class).newInstance(invocationHandler);
// 即通过动态生成的代理类,调用interfaces接口的方法实际上是通过调用InvocationHandler对象的invoke()来完成指定的功能
// 先记住结论,在讲解步骤4的时候会再次详细说明


<-- 关注点1:eagerlyValidateMethods() -->
private void eagerlyValidateMethods(Class<?> service) {  
    Platform platform = Platform.get();  
    for (Method method : service.getDeclaredMethods()) {  
      if (!platform.isDefaultMethod(method)) {  loadServiceMethod(method); } 
      // 将传入的ServiceMethod对象加入LinkedHashMap<Method, ServiceMethod>集合
     // 使用LinkedHashMap集合的好处:lruEntries.values().iterator().next()获取到的是集合*不经常用到的元素,提供了一种Lru算法的实现
    }  
}  

创建网络接口实例用了外观模式 & 代理模式:

使用外观模式进行访问,里面用了代理模式

1. 外观模式

  • 外观模式:定义一个统一接口,外部与通过该统一的接口对子系统里的其他接口进行访问。具体请看:外观模式(Facade Pattern) – *易懂的设计模式解析
  • Retrofit对象的外观(门店) = retrofit.create()
  • 通过这一外观方法就可以在内部调用各个方法创建网络请求接口的实例配置网络请求参数

    大大降低了系统的耦合度

2. 代理模式

  • 代理模式:通过访问代理对象的方式来间接访问目标对象

    分为静态代理 & 动态代理:
    1. 静态代理:代理类在程序运行前已经存在的代理方式
    2. 动态代理:代理类在程序运行前不存在、运行时由程序动态生成的代理方式
    具体请看文章代理模式(Proxy Pattern)- *易懂的设计模式解析

  • return (T) roxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler invocationHandler)通过代理模式中的动态代理模式,动态生成网络请求接口的代理类,并将代理类的实例创建交给InvocationHandler类 作为具体的实现,并*终返回一个动态代理对象。
    生成实例过程中含有生成实现类的缓存机制(单例模式),下面会详细分析

使用动态代理的好处:

  • NetService对象调用getCall()接口中方法时会进行拦截,调用都会集中转发到 InvocationHandler#invoke (),可集中进行处理
  • 获得网络请求接口实例上的所有注解
  • 更方便封装ServiceMethod

下面看源码分析

下面将详细分析`InvocationHandler类 # invoke()`里的具体实现

 new InvocationHandler() {   
          private final Platform platform = Platform.get();

  @Override 
           public Object invoke(Object proxy, Method method, Object... args)
              throws Throwable {

            // 将详细介绍下面代码
            // 关注点1
            // 作用:读取网络请求接口里的方法,并根据前面配置好的属性配置serviceMethod对象
            ServiceMethod serviceMethod = loadServiceMethod(method);     

            // 关注点2
            // 作用:根据配置好的serviceMethod对象创建okHttpCall对象 
            OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args);

            // 关注点3
            // 作用:调用OkHttp,并根据okHttpCall返回rejava的Observe对象或者返回Call
            return serviceMethod.callAdapter.adapt(okHttpCall);
          }

下面将详细介绍3个关注点的代码。

关注点1: ServiceMethod serviceMethod = loadServiceMethod(method);

<-- loadServiceMethod(method)方法讲解 -->
// 一个 ServiceMethod 对象对应于网络请求接口里的一个方法
// loadServiceMethod(method)负责加载 ServiceMethod:

  ServiceMethod loadServiceMethod(Method method) {
    ServiceMethod result;
      // 设置线程同步锁
    synchronized (serviceMethodCache) {

      result = serviceMethodCache.get(method);
      // ServiceMethod类对象采用了单例模式进行创建
      // 即创建ServiceMethod对象前,先看serviceMethodCache有没有缓存之前创建过的网络请求实例

      // 若没缓存,则通过建造者模式创建 serviceMethod 对象
      if (result == null) {
      // 下面会详细介绍ServiceMethod生成实例的过程
        result = new ServiceMethod.Builder(this, method).build();
        serviceMethodCache.put(method, result);
      }
    }
    return result;
  }
// 这里就是上面说的创建实例的缓存机制:采用单例模式从而实现一个 ServiceMethod 对象对应于网络请求接口里的一个方法
// 注:由于每次获取接口实例都是传入 class 对象
// 而 class 对象在进程内单例的,所以获取到它的同一个方法 Method 实例也是单例的,所以这里的缓存是有效的。

下面,我将分3个步骤详细分析serviceMethod实例的创建过程:
Paste_Image.png

步骤1:ServiceMethod类 构造函数

Paste_Image.png


<-- ServiceMethod 类 -->
public final class ServiceMethod {
final okhttp3.Call.Factory callFactory;   // 网络请求工厂  
final CallAdapter<?> callAdapter;  
// 网络请求适配器工厂
// 具体创建是在new ServiceMethod.Builder(this, method).build()*后的build()中
// 下面会详细说明

private final Converter<ResponseBody, T> responseConverter; 
// Response内容转换器  
// 作用:负责把服务器返回的数据(JSON或者其他格式,由 ResponseBody 封装)转化为 T 类型的对象;

private final HttpUrl baseUrl; // 网络请求地址  
private final String relativeUrl; // 网络请求的相对地址  
private final String httpMethod;   // 网络请求的Http方法  
private final Headers headers;  // 网络请求的http请求头 键值对  
private final MediaType contentType; // 网络请求的http报文body的类型  

private final ParameterHandler<?>[] parameterHandlers;  
  // 方法参数处理器
  // 作用:负责解析 API 定义时每个方法的参数,并在构造 HTTP 请求时设置参数;
  // 下面会详细说明

// 说明:从上面的成员变量可以看出,ServiceMethod对象包含了访问网络的所有基本信息

<-- ServiceMethod 类的构造函数 -->
// 作用:传入各种网络请求参数
ServiceMethod(Builder<T> builder) {

    this.callFactory = builder.retrofit.callFactory();  
    this.callAdapter = builder.callAdapter;   
    this.responseConverter = builder.responseConverter;   

    this.baseUrl = builder.retrofit.baseUrl();   
    this.relativeUrl = builder.relativeUrl;   
    this.httpMethod = builder.httpMethod;  
    this.headers = builder.headers;  
    this.contentType = builder.contentType; .  
    this.hasBody = builder.hasBody; y  
    this.isFormEncoded = builder.isFormEncoded;   
    this.isMultipart = builder.isMultipart;  
    this.parameterHandlers = builder.parameterHandlers;  
}

步骤2:ServiceMethod的Builder()

Paste_Image.png


   public Builder(Retrofit retrofit, Method method) {
      this.retrofit = retrofit;
      this.method = method;

      // 获取网络请求接口方法里的注释
      this.methodAnnotations = method.getAnnotations();
      // 获取网络请求接口方法里的参数类型       
      this.parameterTypes = method.getGenericParameterTypes();  
      //获取网络请求接口方法里的注解内容    
      this.parameterAnnotationsArray = method.getParameterAnnotations();    
    }

步骤3:ServiceMethod的build()

Paste_Image.png


// 作用:控制ServiceMethod对象的生成流程

 public ServiceMethod build() {

      callAdapter = createCallAdapter();    
      // 根据网络请求接口方法的返回值和注解类型,从Retrofit对象中获取对应的网络请求适配器  -->关注点1

      responseType = callAdapter.responseType();    
     // 根据网络请求接口方法的返回值和注解类型,从Retrofit对象中获取该网络适配器返回的数据类型

      responseConverter = createResponseConverter();    
      // 根据网络请求接口方法的返回值和注解类型,从Retrofit对象中获取对应的数据转换器  -->关注点3
      // 构造 HTTP 请求时,我们传递的参数都是String
      // Retrofit 类提供 converter把传递的参数都转化为 String 
      // 其余类型的参数都利用 Converter.Factory 的stringConverter 进行转换
      // @Body 和 @Part 类型的参数利用Converter.Factory 提供的 requestBodyConverter 进行转换
      // 这三种 converter 都是通过“询问”工厂列表进行提供,而工厂列表我们可以在构造 Retrofit 对象时进行添加。


       for (Annotation annotation : methodAnnotations) {
        parseMethodAnnotation(annotation);
      }
      // 解析网络请求接口中方法的注解
      // 主要是解析获取Http请求的方法
     // 注解包括:DELETE、GET、POST、HEAD、PATCH、PUT、OPTIONS、HTTP、retrofit2.http.Headers、Multipart、FormUrlEncoded
     // 处理主要是调用方法 parseHttpMethodAndPath(String httpMethod, String value, boolean hasBody) ServiceMethod中的httpMethod、hasBody、relativeUrl、relativeUrlParamNames域进行赋值

     int parameterCount = parameterAnnotationsArray.length;
     // 获取当前方法的参数数量

      parameterHandlers = new ParameterHandler<?>[parameterCount];
      for (int p = 0; p < parameterCount; p++) {
        Type parameterType = parameterTypes[p];
        Annotation[] parameterAnnotations = parameterAnnotationsArray[p];
        // 为方法中的每个参数创建一个ParameterHandler<?>对象并解析每个参数使用的注解类型
        // 该对象的创建过程就是对方法参数中注解进行解析
        // 这里的注解包括:Body、PartMap、Part、FieldMap、Field、Header、QueryMap、Query、Path、Url 
        parameterHandlers[p] = parseParameter(p, parameterType, parameterAnnotations);
      } 
      return new ServiceMethod<>(this);

<-- 总结 -->
// 1. 根据返回值类型和方法标注从Retrofit对象的的网络请求适配器工厂集合和内容转换器工厂集合中分别获取到该方法对应的网络请求适配器和Response内容转换器;
// 2. 根据方法的标注对ServiceMethod的域进行赋值
// 3. *后为每个方法的参数的标注进行解析,获得一个ParameterHandler<?>对象
// 该对象保存有一个Request内容转换器——根据参数的类型从Retrofit的内容转换器工厂集合中获取一个Request内容转换器或者一个String内容转换器。
    }


<-- 关注点1:createCallAdapter() -->
 private CallAdapter<?> createCallAdapter() {

      // 获取网络请求接口里方法的返回值类型
      Type returnType = method.getGenericReturnType();      

      // 获取网络请求接口接口里的注解
      // 此处使用的是@Get
      Annotation[] annotations = method.getAnnotations();       
      try {

      return retrofit.callAdapter(returnType, annotations); 
      // 根据网络请求接口方法的返回值和注解类型,从Retrofit对象中获取对应的网络请求适配器
      // 下面会详细说明retrofit.callAdapter() -- >关注点2
      }
...


<-- 关注点2:retrofit.callAdapter()  -->
 public CallAdapter<?> callAdapter(Type returnType, Annotation[] annotations) {
    return nextCallAdapter(null, returnType, annotations);
  }

 public CallAdapter<?> nextCallAdapter(CallAdapter.Factory skipPast, Type returnType,
      Annotation[] annotations) {

    // 创建 CallAdapter 如下
    // 遍历 CallAdapter.Factory 集合寻找合适的工厂(该工厂集合在*步构造 Retrofit 对象时进行添加(*步时已经说明))
    // 如果*终没有工厂提供需要的 CallAdapter,将抛出异常
    for (int i = start, count = adapterFactories.size(); i < count; i++) {
      CallAdapter<?> adapter = adapterFactories.get(i).get(returnType, annotations, this);      
      if (adapter != null) {
        return adapter;
      }
    }


<--   关注点3:createResponseConverter() -->

 private Converter<ResponseBody, T> createResponseConverter() {
      Annotation[] annotations = method.getAnnotations();
      try {

        // responseConverter 还是由 Retrofit 类提供  -->关注点4
        return retrofit.responseBodyConverter(responseType, annotations);
      } catch (RuntimeException e) { 
        throw methodError(e, "Unable to create converter for %s", responseType);
      }
    }

<--   关注点4:responseBodyConverter() -->
  public <T> Converter<ResponseBody, T> responseBodyConverter(Type type, Annotation[] annotations) {
    return nextResponseBodyConverter(null, type, annotations);
  }

 public <T> Converter<ResponseBody, T> nextResponseBodyConverter(Converter.Factory skipPast,

    int start = converterFactories.indexOf(skipPast) + 1;
    for (int i = start, count = converterFactories.size(); i < count; i++) {

       // 获取Converter 过程:(和获取 callAdapter 基本一致)
         Converter<ResponseBody, ?> converter =
          converterFactories.get(i).responseBodyConverter(type, annotations, this); 
       // 遍历 Converter.Factory 集合并寻找合适的工厂(该工厂集合在构造 Retrofit 对象时进行添加(*步时已经说明))
       // 由于构造Retroifit采用的是Gson解析方式,所以取出的是GsonResponseBodyConverter
       // Retrofit - Converters 还提供了 JSON,XML,ProtoBuf 等类型数据的转换功能。
       // 继续看responseBodyConverter() -->关注点5    
    }


<--   关注点5:responseBodyConverter() -->
@Override
public Converter<ResponseBody, ?> responseBodyConverter(Type type, 
    Annotation[] annotations, Retrofit retrofit) {


  TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));
  // 根据目标类型,利用 Gson#getAdapter 获取相应的 adapter
  return new GsonResponseBodyConverter<>(gson, adapter);
}

// 做数据转换时调用 Gson 的 API 即可。
final class GsonResponseBodyConverter<T> implements Converter<ResponseBody, T> {
  private final Gson gson;
  private final TypeAdapter<T> adapter;

  GsonResponseBodyConverter(Gson gson, TypeAdapter<T> adapter) {
    this.gson = gson;
    this.adapter = adapter;
  }

  @Override 
   public T convert(ResponseBody value) throws IOException {
    JsonReader jsonReader = gson.newJsonReader(value.charStream());
    try {
      return adapter.read(jsonReader);
    } finally {
      value.close();
    }
  }
}
  • 当选择了RxjavaCallAdapterFactory后,Rxjava通过策略模式选择对应的adapter

    关于策略模式的讲解,请看文章策略模式(Strategy Pattern)- *易懂的设计模式解析

  • 具体过程是:根据网络接口方法的返回值类型来选择具体要用哪种CallAdapterFactory,然后创建具体的CallAdapter实例

采用工厂模式使得各功能模块高度解耦

  • 上面提到了两种工厂:CallAdapter.Factory & Converter.Factory分别负责提供不同的功能模块
  • 工厂负责如何提供、提供何种功能模块
  • Retrofit 只负责提供选择何种工厂的决策信息(如网络接口方法的参数、返回值类型、注解等)

这正是所谓的高内聚低耦合,工厂模式get。

关于工厂模式请看我写的文章:
简单工厂模式(SimpleFactoryPattern)- *易懂的设计模式解析
工厂方法模式(Factory Method)- *易懂的设计模式解析
抽象工厂模式(Abstract Factory)- *易懂的设计模式解析

终于配置完网络请求参数(即配置好ServiceMethod对象)。接下来将讲解第二行代码:okHttpCall对象的创建

第二行:OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args);

根据*步配置好的ServiceMethod对象和输入的请求参数创建okHttpCall对象

<--OkHttpCall类 -->
public class OkHttpCall {
    private final ServiceMethod<T> serviceMethod; // 含有所有网络请求参数信息的对象  
    private final Object[] args; // 网络请求接口的参数 
    private okhttp3.Call rawCall; //实际进行网络访问的类  
    private Throwable creationFailure; //几个状态标志位  
    private boolean executed;  
    private volatile boolean canceled;  

<--OkHttpCall构造函数 -->
  public OkHttpCall(ServiceMethod<T> serviceMethod, Object[] args) {  
    // 传入了配置好的ServiceMethod对象和输入的请求参数
    this.serviceMethod = serviceMethod;  
    this.args = args;  
}  

第三行:return serviceMethod.callAdapter.adapt(okHttpCall);

将第二步创建的OkHttpCall对象传给*步创建的serviceMethod对象中对应的网络请求适配器工厂的adapt()

返回对象类型:Android默认的是Call<>;若设置了RxJavaCallAdapterFactory,返回的则是Observable<>

<--  adapt()详解-->
public <R> Call<R> adapt(Call<R> call) {
        return new ExecutorCallbackCall<>(callbackExecutor, call);  
      }

   ExecutorCallbackCall(Executor callbackExecutor, Call<T> delegate) {
      this.delegate = delegate; 
      // 把上面创建并配置好参数的OkhttpCall对象交给静态代理delegate
      // 静态代理和动态代理都属于代理模式
     // 静态代理作用:代理执行被代理者的方法,且可在要执行的方法前后加入自己的动作,进行对系统功能的拓展

      this.callbackExecutor = callbackExecutor;
      // 传入上面定义的回调方法执行器
      // 用于进行线程切换   
    }
  • 采用了装饰模式:ExecutorCallbackCall = 装饰者,而里面真正去执行网络请求的还是OkHttpCall
  • 使用装饰模式的原因:希望在OkHttpCall发送请求时做一些额外操作。这里的额外操作是线程转换,即将子线程切换到主线程
    1. OkHttpCall的enqueue()是进行网络异步请求的:当你调用OkHttpCall.enqueue()时,回调的callback是在子线程中,需要通过Handler转换到主线程进行回调。ExecutorCallbackCall就是用于线程回调;
    2. 当然以上是原生Retrofit使用的切换线程方式。如果你用Rxjava,那就不会用到这个ExecutorCallbackCall而是RxJava的Call,此处不过多展开

步骤4讲解:Call<JavaBean> call = NetService.getCall();

  • NetService对象实际上是动态代理对象Proxy.newProxyInstance()(步骤3中已说明),并不是真正的网络请求接口创建的对象
  • NetService对象调用getCall()时会被动态代理对象Proxy.newProxyInstance()拦截,然后调用自身的InvocationHandler # invoke()
  • invoke(Object proxy, Method method, Object... args)会传入3个参数:Object proxy:(代理对象)、
    Method method(调用的getCall()
    Object... args(方法的参数,即getCall(*)中的*)
  • 接下来利用Java反射获取到getCall()的注解信息,配合args参数创建ServiceMethod对象

    如上面步骤3描述,此处不再次讲解

*终创建并返回一个OkHttpCall类型的Call对象
1. OkHttpCall类是OkHttp的包装类
2. 创建了OkHttpCall类型的Call对象还不能发送网络请求,需要创建Request对象才能发送网络请求

总结

Retrofit采用了 外观模式 统一调用创建网络请求接口实例和网络请求参数配置的方法,具体细节是:

  • 动态创建网络请求接口的实例(代理模式 – 动态代理)
  • 创建 serviceMethod 对象(建造者模式 & 单例模式(缓存机制))
  • 对 serviceMethod 对象进行网络请求参数配置:通过解析网络请求接口方法的参数、返回值和注解类型,从Retrofit对象中获取对应的网络请求的url地址、网络请求执行器、网络请求适配器 & 数据转换器。(策略模式)
  • 对 serviceMethod 对象加入线程切换的操作,便于接收数据后通过Handler从子线程切换到主线程从而对返回数据结果进行处理(装饰模式)
  • *终创建并返回一个OkHttpCall类型的网络请求对象

3. 执行网络请求

  • Retrofit默认使用OkHttp,即OkHttpCall类(实现了 retrofit2.Call<T>接口)

    但可以自定义选择自己需要的Call类

  • OkHttpCall提供了两种网络请求方式:
    1. 同步请求:OkHttpCall.execute()
    2. 异步请求:OkHttpCall.enqueue()

下面将详细介绍这两种网络请求方式。
对于OkHttpCall的enqueue()、execute()此处不往下分析,有兴趣的读者可以看OkHttp的源码

3.1 同步请求OkHttpCall.execute()

3.1.1 发送请求过程

  • 步骤1:对网络请求接口的方法中的每个参数利用对应ParameterHandler进行解析,再根据ServiceMethod对象创建一个OkHttpRequest对象
  • 步骤2:使用OkHttpRequest发送网络请求;
  • 步骤3:对返回的数据使用之前设置的数据转换器(GsonConverterFactory)解析返回的数据,*终得到一个Response<T>对象

3.1.2 具体使用

Response<JavaBean> response = call.execute();  
  • 1

上面简单的一行代码,其实包含了整个发送网络同步请求的三个步骤。

3.1.3 源码分析

@Override 
public Response<T> execute() throws IOException {
  okhttp3.Call call;

 // 设置同步锁
  synchronized (this) {
    call = rawCall;
    if (call == null) {
      try {
        call = rawCall = createRawCall();
        // 步骤1:创建一个OkHttp的Request对象请求 -->关注1
      } catch (IOException | RuntimeException e) {
        creationFailure = e;
        throw e;
      }
    }
  }

  return parseResponse(call.execute());
  // 步骤2:调用OkHttpCall的execute()发送网络请求(同步)
  // 步骤3:解析网络请求返回的数据parseResponse() -->关注2
}

<-- 关注1:createRawCall()  -->
private okhttp3.Call createRawCall() throws IOException {

  Request request = serviceMethod.toRequest(args);
  // 从ServiceMethod的toRequest()返回一个Request对象
  okhttp3.Call call = serviceMethod.callFactory.newCall(request);
  // 根据serviceMethod和request对象创建 一个okhttp3.Request

  if (call == null) {
    throw new NullPointerException("Call.Factory returned null.");
  }
  return call;
}

<--  关注2:parseResponse()-->
Response<T> parseResponse(okhttp3.Response rawResponse) throws IOException {
  ResponseBody rawBody = rawResponse.body();

  rawResponse = rawResponse.newBuilder()
      .body(new NoContentResponseBody(rawBody.contentType(), rawBody.contentLength()))
      .build();
  // 收到返回数据后进行状态码检查
  // 具体关于状态码说明下面会详细介绍
  int code = rawResponse.code();
  if (code < 200 || code >= 300) {
  }

  if (code == 204 || code == 205) {
    return Response.success(null, rawResponse);
  }

  ExceptionCatchingRequestBody catchingBody = new ExceptionCatchingRequestBody(rawBody);
  try {
    T body = serviceMethod.toResponse(catchingBody);
   // 等Http请求返回后 & 通过状态码检查后,将response body传入ServiceMethod中,ServiceMethod通过调用Converter接口(之前设置的GsonConverterFactory)将response body转成一个Java对象,即解析返回的数据


// 生成Response类
    return Response.success(body, rawResponse);
  } catch (RuntimeException e) {
    ... // 异常处理
  }
}

特别注意:

  • ServiceMethod几乎保存了一个网络请求所需要的数据
  • 发送网络请求时,OkHttpCall需要从ServiceMethod中获得一个Request对象
  • 解析数据时,还需要通过ServiceMethod使用Converter(数据转换器)转换成Java对象进行数据解析

    为了提高效率,Retrofit还会对解析过的请求ServiceMethod进行缓存,存放在Map<Method, ServiceMethod> serviceMethodCache = new LinkedHashMap<>();对象中,即第二步提到的单例模式

  • 关于状态码检查时的状态码说明:

Paste_Image.png

以上便是整个以同步的方式发送网络请求的过程。

3.2 异步请求OkHttpCall.enqueue()

3.2.1 发送请求过程
  • 步骤1:对网络请求接口的方法中的每个参数利用对应ParameterHandler进行解析,再根据ServiceMethod对象创建一个OkHttpRequest对象
  • 步骤2:使用OkHttpRequest发送网络请求;
  • 步骤3:对返回的数据使用之前设置的数据转换器(GsonConverterFactory)解析返回的数据,*终得到一个Response<T>对象
  • 步骤4:进行线程切换从而在主线程处理返回的数据结果

    若使用了RxJava,则直接回调到主线程

异步请求的过程跟同步请求类似,唯一不同之处在于:异步请求会将回调方法交给回调执行器在指定的线程中执行。
指定的线程此处是指主线程(UI线程)

3.2.2 具体使用
call.enqueue(new Callback<JavaBean>() {
            @Override
            public void onResponse(Call<JavaBean> call, Response<JavaBean> response) {
                System.out.println(response.isSuccessful());
                if (response.isSuccessful()) {
                    response.body().show();
                }
                else {
                    try {
                        System.out.println(response.errorBody().string());
                    } catch (IOException e) {
                        e.printStackTrace();
                    } ;
                }
            }
  • 从上面分析有:call是一个静态代理
  • 使用静态代理的作用是:在okhttpCall发送网络请求的前后进行额外操作

    这里的额外操作是:线程切换,即将子线程切换到主线程,从而在主线程对返回的数据结果进行处理

3.2.3 源码分析
<--  call.enqueue()解析  -->
@Override 
public void enqueue(final Callback<T> callback) {

      delegate.enqueue(new Callback<T>() {
     // 使用静态代理 delegate进行异步请求 ->>分析1
     // 等下记得回来
        @Override 
        public void onResponse(Call<T> call, final Response<T> response) {
          // 步骤4:线程切换,从而在主线程显示结果
          callbackExecutor.execute(new Runnable() {
          // *后Okhttp的异步请求结果返回到callbackExecutor
          // callbackExecutor.execute()通过Handler异步回调将结果传回到主线程进行处理(如显示在Activity等等),即进行了线程切换
          // 具体是如何做线程切换 ->>分析2
              @Override 
               public void run() {
              if (delegate.isCanceled()) {
                callback.onFailure(ExecutorCallbackCall.this, new IOException("Canceled"));
              } else {
                callback.onResponse(ExecutorCallbackCall.this, response);
              }
            }
          });
        }

        @Override 
        public void onFailure(Call<T> call, final Throwable t) {
          callbackExecutor.execute(new Runnable() {
            @Override public void run() {
              callback.onFailure(ExecutorCallbackCall.this, t);
            }
          });
        }
      });
    }


<-- 分析1:delegate.enqueue()解析 -->
@Override 
public void enqueue(final Callback<T> callback) {

    okhttp3.Call call;
    Throwable failure;

// 步骤1:创建OkHttp的Request对象,再封装成OkHttp.call
     // delegate代理在网络请求前的动作:创建OkHttp的Request对象,再封装成OkHttp.call
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already executed.");
      executed = true;

      call = rawCall;
      failure = creationFailure;
      if (call == null && failure == null) {
        try {

          call = rawCall = createRawCall(); 
          // 创建OkHttp的Request对象,再封装成OkHttp.call
         // 方法同发送同步请求,此处不作过多描述  
        } catch (Throwable t) {
          failure = creationFailure = t;
        }
      }

// 步骤2:发送网络请求
    // delegate是OkHttpcall的静态代理
    // delegate静态代理*终还是调用Okhttp.enqueue进行网络请求
    call.enqueue(new okhttp3.Callback() {
      @Override 
        public void onResponse(okhttp3.Call call, okhttp3.Response rawResponse)
          throws IOException {
        Response<T> response;
        try {

          // 步骤3:解析返回数据
          response = parseResponse(rawResponse);
        } catch (Throwable e) {
          callFailure(e);
          return;
        }
        callSuccess(response);
      }

      @Override 
         public void onFailure(okhttp3.Call call, IOException e) {
        try {
          callback.onFailure(OkHttpCall.this, e);
        } catch (Throwable t) {
          t.printStackTrace();
        }
      }

      private void callFailure(Throwable e) {
        try {
          callback.onFailure(OkHttpCall.this, e);
        } catch (Throwable t) {
          t.printStackTrace();
        }
      }

      private void callSuccess(Response<T> response) {
        try {
          callback.onResponse(OkHttpCall.this, response);
        } catch (Throwable t) {
          t.printStackTrace();
        }
      }
    });
  }

// 请回去上面分析1的起点

<-- 分析2:异步请求后的线程切换-->
// 线程切换是通过一开始创建Retrofit对象时Platform在检测到运行环境是Android时进行创建的:(之前已分析过)
// 采用适配器模式
static class Android extends Platform {

    // 创建默认的回调执行器工厂
    // 如果不将RxJava和Retrofit一起使用,一般都是使用该默认的CallAdapter.Factory
    // 后面会对RxJava和Retrofit一起使用的情况进行分析
    @Override
      CallAdapter.Factory defaultCallAdapterFactory(Executor callbackExecutor) {
      return new ExecutorCallAdapterFactory(callbackExecutor);
    }

    @Override 
      public Executor defaultCallbackExecutor() {
      // 返回一个默认的回调方法执行器
      // 该执行器负责在主线程(UI线程)中执行回调方法
      return new MainThreadExecutor();
    }

    // 获取主线程Handler
    static class MainThreadExecutor implements Executor {
      private final Handler handler = new Handler(Looper.getMainLooper());


      @Override 
      public void execute(Runnable r) {
        // Retrofit获取了主线程的handler
        // 然后在UI线程执行网络请求回调后的数据显示等操作。
        handler.post(r);
      }
    }

// 切换线程的流程:
// 1. 回调ExecutorCallAdapterFactory生成了一个ExecutorCallbackCall对象
// 2. 通过调用ExecutorCallbackCall.enqueue(CallBack)从而调用MainThreadExecutor的execute()通过handler切换到主线程处理返回结果(如显示在Activity等等)
  }

以上便是整个以 异步方式发送网络请求的过程。


5. 总结

Retrofit 本质上是一个 RESTful 的HTTP 网络请求框架的封装,即通过 大量的设计模式 封装了 OkHttp ,使得简洁易用。具体过程如下:

  1. Retrofit 将 Http请求 抽象 成 Java接口
  2. 在接口里用 注解 描述和配置 网络请求参数
  3. 用动态代理 的方式,动态将网络请求接口的注解 解析 成HTTP请求
  4. *后执行HTTP请求

*后贴一张非常详细的Retrofit源码分析图:

Retrofit源码分析图

6. *后

  • 看完本文,相信你已经非常熟悉 Retrofit 2.0 的源码分析
  • 关于Retrofit 2.0的详细使用教程,请看文章这是一份很详细的 Retrofit 2.0 使用教程(含实例讲解)
  • 接下来,我将继续分析与 Retrofit 配合使用的 RxJava

这是一份很详细的 Retrofit 2.0 使用教程

这是一份很详细的 Retrofit 2.0 使用教程(含实例讲解)

前言

  • Andrroid开发中,网络请求十分常用
  • 而在Android网络请求库中,Retrofit是当下*热的一个网络请求库

Github截图

  • 今天,我将献上一份非常详细Retrofit v2.0的使用教程,希望你们会喜欢。

如果对Retrofit v2.0的源码感兴趣,可看文章:Android:手把手带你深入剖析 Retrofit 2.0 源码


目录

目录


1. 简介

Retrofit简介

特别注意:

  • 准确来说,Retrofit 是一个 RESTful 的 HTTP 网络请求框架的封装。
  • 原因:网络请求的工作本质上是 OkHttp 完成,而 Retrofit 仅负责 网络请求接口的封装

本质过程

  • App应用程序通过 Retrofit 请求网络,实际上是使用 Retrofit 接口层封装请求参数、Header、Url 等信息,之后由 OkHttp 完成后续的请求操作
  • 在服务端返回数据之后,OkHttp 将原始的结果交给 Retrofit,Retrofit根据用户的需求对结果进行解析

2. 与其他开源请求库对比

除了Retrofit,如今Android中主流的网络请求框架有:

  • Android-Async-Http
  • Volley
  • OkHttp

下面是简单介绍:

网络请求加载 - 介绍

一图让你了解全部的网络请求库和他们之间的区别!

网络请求库 - 对比


附:各个主流网络请求库的Github地址

  • Android-Async-Http
  • Volley
  • OkHttp
  • Retrofit

3. 使用介绍

使用 Retrofit 的步骤共有7个:

步骤1:添加Retrofit库的依赖
步骤2:创建 接收服务器返回数据 的类
步骤3:创建 用于描述网络请求 的接口
步骤4:创建 Retrofit 实例
步骤5:创建 网络请求接口实例 并 配置网络请求参数
步骤6:发送网络请求(异步 / 同步)

封装了 数据转换、线程切换的操作

步骤7: 处理服务器返回的数据

接下来,我们一步步进行讲解。

步骤1:添加Retrofit库的依赖

1. 在 Gradle加入Retrofit库的依赖

由于Retrofit是基于OkHttp,所以还需要添加OkHttp库依赖

build.gradle

dependencies {
    compile 'com.squareup.retrofit2:retrofit:2.0.2'
    // Retrofit库
    compile 'com.squareup.okhttp3:okhttp:3.1.2'
    // Okhttp库
  }

 

2. 添加 网络权限
AndroidManifest.xml

<uses-permission android:name="android.permission.INTERNET"/>

 

步骤2:创建 接收服务器返回数据 的类

Reception.java

public class Reception {
    ...
    // 根据返回数据的格式和数据解析方式(Json、XML等)定义
    // 下面会在实例进行说明
        }

 

步骤3:创建 用于描述网络请求 的接口

  • Retrofit将 Http请求 抽象成 Java接口:采用 注解 描述网络请求参数 和配置网络请求参数
    1. 用 动态代理 动态 将该接口的注解“翻译”成一个 Http 请求,*后再执行 Http 请求
    2. 注:接口中的每个方法的参数都需要使用注解标注,否则会报错

GetRequest_Interface.interface

public interface GetRequest_Interface {

    @GET("openapi.do?keyfrom=Yanzhikai&key=2032414398&type=data&doctype=json&version=1.1&q=car")
    Call<Translation>  getCall();
    // @GET注解的作用:采用Get方法发送网络请求

    // getCall() = 接收网络请求数据的方法
    // 其中返回类型为Call<*>,*是接收数据的类(即上面定义的Translation类)
    // 如果想直接获得Responsebody中的内容,可以定义网络请求返回值为Call<ResponseBody>
}

 

下面详细介绍Retrofit 网络请求接口 的注解类型。

注解类型

注解类型

注解说明

*类:网络请求方法

网络请求方法注解

详细说明:
a. @GET、@POST、@PUT、@DELETE、@HEAD
以上方法分别对应 HTTP中的网络请求方式

public interface GetRequest_Interface {

    @GET("openapi.do?keyfrom=Yanzhikai&key=2032414398&type=data&doctype=json&version=1.1&q=car")
    Call<Translation>  getCall();
    // @GET注解的作用:采用Get方法发送网络请求
    // getCall() = 接收网络请求数据的方法
    // 其中返回类型为Call<*>,*是接收数据的类(即上面定义的Translation类)
}

 

此处特意说明URL的组成:Retrofit把 网络请求的URL 分成了两部分设置:

// 第1部分:在网络请求接口的注解设置
 @GET("openapi.do?keyfrom=Yanzhikai&key=2032414398&type=data&doctype=json&version=1.1&q=car")
Call<Translation>  getCall();

// 第2部分:在创建Retrofit实例时通过.baseUrl()设置
Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("http://fanyi.youdao.com/") //设置网络请求的Url地址
                .addConverterFactory(GsonConverterFactory.create()) //设置数据解析器
                .build();

// 从上面看出:一个请求的URL可以通过 替换块 和 请求方法的参数 来进行动态的URL更新。
// 替换块是由 被{}包裹起来的字符串构成
// 即:Retrofit支持动态改变网络请求根目录

 

  • 网络请求的完整 Url =在创建Retrofit实例时通过.baseUrl()设置 +网络请求接口的注解设置(下面称 “path“ )
  • 具体整合的规则如下:

URL整合规则

建议采用第三种方式来配置,并尽量使用同一种路径形式。

b. @HTTP

  • 作用:替换@GET、@POST、@PUT、@DELETE、@HEAD注解的作用 及 更多功能拓展
  • 具体使用:通过属性method、path、hasBody进行设置
public interface GetRequest_Interface {
    /**
     * method:网络请求的方法(区分大小写)
     * path:网络请求地址路径
     * hasBody:是否有请求体
     */
    @HTTP(method = "GET", path = "blog/{id}", hasBody = false)
    Call<ResponseBody> getCall(@Path("id") int id);
    // {id} 表示是一个变量
    // method 的值 retrofit 不会做处理,所以要自行保证准确
}

 

第二类:标记

标记类注解

a. @FormUrlEncoded

  • 作用:表示发送form-encoded的数据

每个键值对需要用@Filed来注解键名,随后的对象需要提供值。

b. @Multipart

 

  • 作用:表示发送form-encoded的数据(适用于 有文件 上传的场景)

每个键值对需要用@Part来注解键名,随后的对象需要提供值。
具体使用如下:
GetRequest_Interface

public interface GetRequest_Interface {
        /**
         *表明是一个表单格式的请求(Content-Type:application/x-www-form-urlencoded)
         * <code>Field("username")</code> 表示将后面的 <code>String name</code> 中name的取值作为 username 的值
         */
        @POST("/form")
        @FormUrlEncoded
        Call<ResponseBody> testFormUrlEncoded1(@Field("username") String name, @Field("age") int age);

        /**
         * {@link Part} 后面支持三种类型,{@link RequestBody}、{@link okhttp3.MultipartBody.Part} 、任意类型
         * 除 {@link okhttp3.MultipartBody.Part} 以外,其它类型都必须带上表单字段({@link okhttp3.MultipartBody.Part} 中已经包含了表单字段的信息),
         */
        @POST("/form")
        @Multipart
        Call<ResponseBody> testFileUpload1(@Part("name") RequestBody name, @Part("age") RequestBody age, @Part MultipartBody.Part file);

}

// 具体使用
       GetRequest_Interface service = retrofit.create(GetRequest_Interface.class);
        // @FormUrlEncoded 
        Call<ResponseBody> call1 = service.testFormUrlEncoded1("Carson", 24);

        //  @Multipart
        RequestBody name = RequestBody.create(textType, "Carson");
        RequestBody age = RequestBody.create(textType, "24");

        MultipartBody.Part filePart = MultipartBody.Part.createFormData("file", "test.txt", file);
        Call<ResponseBody> call3 = service.testFileUpload1(name, age, filePart);

 

第三类:网络请求参数

网络请求参数注解

详细说明

a. @Header & @Headers

  • 作用:添加请求头 &添加不固定的请求头
  • 具体使用如下:
// @Header
@GET("user")
Call<User> getUser(@Header("Authorization") String authorization)

// @Headers
@Headers("Authorization: authorization")
@GET("user")
Call<User> getUser()

// 以上的效果是一致的。
// 区别在于使用场景和使用方式
// 1. 使用场景:@Header用于添加不固定的请求头,@Headers用于添加固定的请求头
// 2. 使用方式:@Header作用于方法的参数;@Headers作用于方法

 

b. @Body

 

  • 作用:以 Post方式 传递 自定义数据类型 给服务器
  • 特别注意:如果提交的是一个Map,那么作用相当于 @Field

不过Map要经过 FormBody.Builder 类处理成为符合 Okhttp 格式的表单,如:

FormBody.Builder builder = new FormBody.Builder();
builder.add("key","value");

 

c. @Field & @FieldMap

  • 作用:发送 Post请求 时提交请求的表单字段
  • 具体使用:与 @FormUrlEncoded 注解配合使用
public interface GetRequest_Interface {
        /**
         *表明是一个表单格式的请求(Content-Type:application/x-www-form-urlencoded)
         * <code>Field("username")</code> 表示将后面的 <code>String name</code> 中name的取值作为 username 的值
         */
        @POST("/form")
        @FormUrlEncoded
        Call<ResponseBody> testFormUrlEncoded1(@Field("username") String name, @Field("age") int age);

/**
         * Map的key作为表单的键
         */
        @POST("/form")
        @FormUrlEncoded
        Call<ResponseBody> testFormUrlEncoded2(@FieldMap Map<String, Object> map);

}

// 具体使用
         // @Field
        Call<ResponseBody> call1 = service.testFormUrlEncoded1("Carson", 24);

        // @FieldMap
        // 实现的效果与上面相同,但要传入Map
        Map<String, Object> map = new HashMap<>();
        map.put("username", "Carson");
        map.put("age", 24);
        Call<ResponseBody> call2 = service.testFormUrlEncoded2(map);

 

d. @Part & @PartMap

  • 作用:发送 Post请求 时提交请求的表单字段

    与@Field的区别:功能相同,但携带的参数类型更加丰富,包括数据流,所以适用于 有文件上传 的场景

  • 具体使用:与 @Multipart 注解配合使用
public interface GetRequest_Interface {

          /**
         * {@link Part} 后面支持三种类型,{@link RequestBody}、{@link okhttp3.MultipartBody.Part} 、任意类型
         * 除 {@link okhttp3.MultipartBody.Part} 以外,其它类型都必须带上表单字段({@link okhttp3.MultipartBody.Part} 中已经包含了表单字段的信息),
         */
        @POST("/form")
        @Multipart
        Call<ResponseBody> testFileUpload1(@Part("name") RequestBody name, @Part("age") RequestBody age, @Part MultipartBody.Part file);

        /**
         * PartMap 注解支持一个Map作为参数,支持 {@link RequestBody } 类型,
         * 如果有其它的类型,会被{@link retrofit2.Converter}转换,如后面会介绍的 使用{@link com.google.gson.Gson} 的 {@link retrofit2.converter.gson.GsonRequestBodyConverter}
         * 所以{@link MultipartBody.Part} 就不适用了,所以文件只能用<b> @Part MultipartBody.Part </b>
         */
        @POST("/form")
        @Multipart
        Call<ResponseBody> testFileUpload2(@PartMap Map<String, RequestBody> args, @Part MultipartBody.Part file);

        @POST("/form")
        @Multipart
        Call<ResponseBody> testFileUpload3(@PartMap Map<String, RequestBody> args);
}

// 具体使用
 MediaType textType = MediaType.parse("text/plain");
        RequestBody name = RequestBody.create(textType, "Carson");
        RequestBody age = RequestBody.create(textType, "24");
        RequestBody file = RequestBody.create(MediaType.parse("application/octet-stream"), "这里是模拟文件的内容");

        // @Part
        MultipartBody.Part filePart = MultipartBody.Part.createFormData("file", "test.txt", file);
        Call<ResponseBody> call3 = service.testFileUpload1(name, age, filePart);
        ResponseBodyPrinter.printResponseBody(call3);

        // @PartMap
        // 实现和上面同样的效果
        Map<String, RequestBody> fileUpload2Args = new HashMap<>();
        fileUpload2Args.put("name", name);
        fileUpload2Args.put("age", age);
        //这里并不会被当成文件,因为没有文件名(包含在Content-Disposition请求头中),但上面的 filePart 有
        //fileUpload2Args.put("file", file);
        Call<ResponseBody> call4 = service.testFileUpload2(fileUpload2Args, filePart); //单独处理文件
        ResponseBodyPrinter.printResponseBody(call4);
}

 

e. @Query和@QueryMap

  • 作用:用于 @GET 方法的查询参数(Query = Url 中 ‘?’ 后面的 key-value)

    如:url = http://www.println.net/?cate=android,其中,Query = cate

  • 具体使用:配置时只需要在接口方法中增加一个参数即可:
   @GET("/")    
   Call<String> cate(@Query("cate") String cate);
}

// 其使用方式同 @Field与@FieldMap,这里不作过多描述

 

f. @Path

  • 作用:URL地址的缺省值
  • 具体使用:
public interface GetRequest_Interface {

        @GET("users/{user}/repos")
        Call<ResponseBody>  getBlog(@Path("user") String user );
        // 访问的API是:https://api.github.com/users/{user}/repos
        // 在发起请求时, {user} 会被替换为方法的*个参数 user(被@Path注解作用)
    }

 

g. @Url

  • 作用:直接传入一个请求的 URL变量 用于URL设置
  • 具体使用:
public interface GetRequest_Interface {

        @GET
        Call<ResponseBody> testUrlAndQuery(@Url String url, @Query("showAll") boolean showAll);
       // 当有URL注解时,@GET传入的URL就可以省略
       // 当GET、POST...HTTP等方法中没有设置Url时,则必须使用 {@link Url}提供

}

 

汇总

汇总

步骤4:创建 Retrofit 实例

 Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("http://fanyi.youdao.com/") // 设置网络请求的Url地址
                .addConverterFactory(GsonConverterFactory.create()) // 设置数据解析器
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) // 支持RxJava平台
                .build();

 

a. 关于数据解析器(Converter)

  • Retrofit支持多种数据解析方式
  • 使用时需要在Gradle添加依赖
数据解析器 Gradle依赖
Gson com.squareup.retrofit2:converter-gson:2.0.2
Jackson com.squareup.retrofit2:converter-jackson:2.0.2
Simple XML com.squareup.retrofit2:converter-simplexml:2.0.2
Protobuf com.squareup.retrofit2:converter-protobuf:2.0.2
Moshi com.squareup.retrofit2:converter-moshi:2.0.2
Wire com.squareup.retrofit2:converter-wire:2.0.2
Scalars com.squareup.retrofit2:converter-scalars:2.0.2

b. 关于网络请求适配器(CallAdapter)

  • Retrofit支持多种网络请求适配器方式:guava、Java8和rxjava

    使用时如使用的是 Android 默认的 CallAdapter,则不需要添加网络请求适配器的依赖,否则则需要按照需求进行添加
    Retrofit 提供的 CallAdapter

  • 使用时需要在Gradle添加依赖:
网络请求适配器 Gradle依赖
guava com.squareup.retrofit2:adapter-guava:2.0.2
Java8 com.squareup.retrofit2:adapter-java8:2.0.2
rxjava com.squareup.retrofit2:adapter-rxjava:2.0.2

步骤5:创建 网络请求接口实例

        // 创建 网络请求接口 的实例
        GetRequest_Interface request = retrofit.create(GetRequest_Interface.class);

        //对 发送请求 进行封装
        Call<Reception> call = request.getCall();

 

步骤6:发送网络请求(异步 / 同步)

封装了 数据转换、线程切换的操作

//发送网络请求(异步)
        call.enqueue(new Callback<Translation>() {
            //请求成功时回调
            @Override
            public void onResponse(Call<Translation> call, Response<Translation> response) {
                //请求处理,输出结果
                response.body().show();
            }

            //请求失败时候的回调
            @Override
            public void onFailure(Call<Translation> call, Throwable throwable) {
                System.out.println("连接失败");
            }
        });

// 发送网络请求(同步)
Response<Reception> response = call.execute();

 

步骤7:处理返回数据

通过response类的 body()对返回的数据进行处理

//发送网络请求(异步)
        call.enqueue(new Callback<Translation>() {
            //请求成功时回调
            @Override
            public void onResponse(Call<Translation> call, Response<Translation> response) {
                // 对返回数据进行处理
                response.body().show();
            }

            //请求失败时候的回调
            @Override
            public void onFailure(Call<Translation> call, Throwable throwable) {
                System.out.println("连接失败");
            }
        });

// 发送网络请求(同步)
  Response<Reception> response = call.execute();
  // 对返回数据进行处理
  response.body().show();

 


4. 实例讲解

接下来,我将用两个实例分别对 Retrofit GET方式 和 POST方式进行 网络请求 讲解。

4.1 实例1

  • 实现功能:将中文翻译成英文
  • 实现方案:采用Get方法对 金山词霸API 发送网络请求

    采用 Gson 进行数据解析

金山词典

  • 步骤说明

步骤1:添加Retrofit库的依赖
步骤2:创建 接收服务器返回数据 的类
步骤3:创建 用于描述网络请求 的接口
步骤4:创建 Retrofit 实例
步骤5:创建 网络请求接口实例 并 配置网络请求参数
步骤6:发送网络请求(采用*常用的异步方式)

封装了 数据转换、线程切换的操作

步骤7: 处理服务器返回的数据

接下来,我们一步步进行讲解。

  • 具体使用

步骤1:添加Retrofit库的依赖

1. 在 Gradle加入Retrofit库的依赖

由于Retrofit是基于OkHttp,所以还需要添加OkHttp库依赖

build.gradle

dependencies {
    compile 'com.squareup.retrofit2:retrofit:2.0.2'
    // Retrofit库
    compile 'com.squareup.okhttp3:okhttp:3.1.2'
    // Okhttp库
  }

 

2. 添加 网络权限
AndroidManifest.xml

<uses-permission android:name="android.permission.INTERNET"/>

 

步骤2:创建 接收服务器返回数据 的类

  • 金山词霸API 的数据格式说明如下:
// URL模板
http://fy.iciba.com/ajax.php

// URL实例
http://fy.iciba.com/ajax.php?a=fy&f=auto&t=auto&w=hello%20world

// 参数说明:
// a:固定值 fy
// f:原文内容类型,日语取 ja,中文取 zh,英语取 en,韩语取 ko,德语取 de,西班牙语取 es,法语取 fr,自动则取 auto
// t:译文内容类型,日语取 ja,中文取 zh,英语取 en,韩语取 ko,德语取 de,西班牙语取 es,法语取 fr,自动则取 auto
// w:查询内容

 

API格式说明

  • 根据 金山词霸API 的数据格式,创建 接收服务器返回数据 的类:

Translation.java

public class Translation {
        private int status;

    private content content;
    private static class content {
        private String from;
        private String to;
        private String vendor;
        private String out;
        private int errNo;
    }

    //定义 输出返回数据 的方法
    public void show() {
        System.out.println(status);

        System.out.println(content.from);
        System.out.println(content.to);
        System.out.println(content.vendor);
        System.out.println(content.out);
        System.out.println(content.errNo);
    }
}

 

步骤3:创建 用于描述网络请求 的接口

采用 注解 描述 网络请求参数。
GetRequest_Interface.java

public interface GetRequest_Interface {

 @GET("ajax.php?a=fy&f=auto&t=auto&w=hello%20world")
    Call<Translation> getCall();
    // 注解里传入 网络请求 的部分URL地址
    // Retrofit把网络请求的URL分成了两部分:一部分放在Retrofit对象里,另一部分放在网络请求接口里
    // 如果接口里的url是一个完整的网址,那么放在Retrofit对象里的URL可以忽略
    // getCall()是接受网络请求数据的方法
}

 

接下来的步骤均在GetRequest.java内实现(看注释)

步骤4:创建Retrofit对象
步骤5:创建 网络请求接口 的实例
步骤6:发送网络请求

以*常用的 异步请求 为例

步骤7:处理返回数据

GetRequest.java

public class GetRequest extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        request();
        // 使用Retrofit封装的方法
    }
    public void request() {

        //步骤4:创建Retrofit对象
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("http://fy.iciba.com/") // 设置 网络请求 Url
                .addConverterFactory(GsonConverterFactory.create()) //设置使用Gson解析(记得加入依赖)
                .build();

        // 步骤5:创建 网络请求接口 的实例
        GetRequest_Interface request = retrofit.create(GetRequest_Interface.class);

        //对 发送请求 进行封装
        Call<Translation> call = request.getCall();

        //步骤6:发送网络请求(异步)
        call.enqueue(new Callback<Translation>() {
            //请求成功时回调
            @Override
            public void onResponse(Call<Translation> call, Response<Translation> response) {
                // 步骤7:处理返回的数据结果
                response.body().show();
            }

            //请求失败时回调
            @Override
            public void onFailure(Call<Translation> call, Throwable throwable) {
                System.out.println("连接失败");
            }
        });
    }
}

 

由于此处采用了 Gson 解析,所以需要在 Gradle加入依赖
build.gradle

compile 'com.squareup.retrofit2:converter-gson:2.0.2'
  • 1

运行结果

运行结果

Demo地址

Carson_Ho的Github:https://github.com/Carson-Ho/RetrofitDemo


4.2 实例2

  • 实现的功能:将 英文 翻译成 中文
  • 实现方法:采用Post方法对 有道API 发送网络请求

    采用 Gson 进行数据解析

有道翻译

  • 使用步骤

步骤1:添加Retrofit库的依赖
步骤2:创建 接收服务器返回数据 的类
步骤3:创建 用于描述网络请求 的接口
步骤4:创建 Retrofit 实例
步骤5:创建 网络请求接口实例 并 配置网络请求参数
步骤6:发送网络请求(采用*常用的异步方式)

封装了 数据转换、线程切换的操作

步骤7: 处理服务器返回的数据

接下来,我们一步步进行Retrofit的使用。

  • 具体使用

步骤1:添加Retrofit库的依赖

1. 在 Gradle加入Retrofit库的依赖

由于Retrofit是基于OkHttp,所以还需要添加OkHttp库依赖

build.gradle

dependencies {
    compile 'com.squareup.retrofit2:retrofit:2.0.2'
    // Retrofit库
    compile 'com.squareup.okhttp3:okhttp:3.1.2'
    // Okhttp库
  }

 

2. 添加 网络权限
AndroidManifest.xml

<uses-permission android:name="android.permission.INTERNET"/>

步骤2:创建 接收服务器返回数据 的类

  • API 的数据格式说明如下:
// URL
http://fanyi.youdao.com/translate

// URL实例
http://fanyi.youdao.com/translate?doctype=json&jsonversion=&type=&keyfrom=&model=&mid=&imei=&vendor=&screen=&ssid=&network=&abtest=


// 参数说明
// doctype:json 或 xml
// jsonversion:如果 doctype 值是 xml,则去除该值,若 doctype 值是 json,该值为空即可
// xmlVersion:如果 doctype 值是 json,则去除该值,若 doctype 值是 xml,该值为空即可
// type:语言自动检测时为 null,为 null 时可为空。英译中为 EN2ZH_CN,中译英为 ZH_CN2EN,日译中为 JA2ZH_CN,中译日为 ZH_CN2JA,韩译中为 KR2ZH_CN,中译韩为 ZH_CN2KR,中译法为 ZH_CN2FR,法译中为 FR2ZH_CN
// keyform:mdict. + 版本号 + .手机平台。可为空
// model:手机型号。可为空
// mid:平台版本。可为空
// imei:???。可为空
// vendor:应用下载平台。可为空
// screen:屏幕宽高。可为空
// ssid:用户名。可为空
// abtest:???。可为空

// 请求方式说明
// 请求方式:POST
// 请求体:i
// 请求格式:x-www-form-urlencoded

 

数据格式说明

  • 根据 有道API 的数据格式,创建 接收服务器返回数据 的类:

Translation.java

public class Translation1 {

    private String type;
    private int errorCode;
    private int elapsedTime;
    private List<List<TranslateResultBean>> translateResult;

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public int getErrorCode() {
        return errorCode;
    }

    public void setErrorCode(int errorCode) {
        this.errorCode = errorCode;
    }

    public int getElapsedTime() {
        return elapsedTime;
    }

    public void setElapsedTime(int elapsedTime) {
        this.elapsedTime = elapsedTime;
    }

    public List<List<TranslateResultBean>> getTranslateResult() {
        return translateResult;
    }

    public void setTranslateResult(List<List<TranslateResultBean>> translateResult) {
        this.translateResult = translateResult;
    }

    public static class TranslateResultBean {
        /**
         * src : merry me
         * tgt : 我快乐
         */

        public String src;
        public String tgt;

        public String getSrc() {
            return src;
        }

        public void setSrc(String src) {
            this.src = src;
        }

        public String getTgt() {
            return tgt;
        }

        public void setTgt(String tgt) {
            this.tgt = tgt;
        }
    }

}

 

步骤3:创建 用于描述网络请求 的接口

采用 注解 描述 网络请求参数。

PostRequest_Interface.java

public interface PostRequest_Interface {

    @POST("translate?doctype=json&jsonversion=&type=&keyfrom=&model=&mid=&imei=&vendor=&screen=&ssid=&network=&abtest=")
    @FormUrlEncoded
    Call<Translation1> getCall(@Field("i") String targetSentence);
    //采用@Post表示Post方法进行请求(传入部分url地址)
    // 采用@FormUrlEncoded注解的原因:API规定采用请求格式x-www-form-urlencoded,即表单形式
    // 需要配合@Field 向服务器提交需要的字段
}

 

接下来的步骤均在PostRequest.java内实现(看注释)

步骤4:创建Retrofit对象
步骤5:创建 网络请求接口 的实例
步骤6:发送网络请求

以*常用的 异步请求 为例

步骤7:处理返回数据

PostRequest.java

public class PostRequest extends AppCompatActivity {


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        request();
    }
    public void request() {

        //步骤4:创建Retrofit对象
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("http://fanyi.youdao.com/") // 设置 网络请求 Url
                .addConverterFactory(GsonConverterFactory.create()) //设置使用Gson解析(记得加入依赖)
                .build();

        // 步骤5:创建 网络请求接口 的实例
        PostRequest_Interface request = retrofit.create(PostRequest_Interface.class);

        //对 发送请求 进行封装(设置需要翻译的内容)
        Call<Translation1> call = request.getCall("I love you");

        //步骤6:发送网络请求(异步)
        call.enqueue(new Callback<Translation1>() {

            //请求成功时回调
            @Override
            public void onResponse(Call<Translation1> call, Response<Translation1> response) {
                // 步骤7:处理返回的数据结果:输出翻译的内容
                System.out.println(response.body().getTranslateResult().get(0).get(0).getTgt());
            }

            //请求失败时回调
            @Override
            public void onFailure(Call<Translation1> call, Throwable throwable) {
                System.out.println("请求失败");
                System.out.println(throwable.getMessage());
            }
        });
    }


}

 

由于此处采用了 Gson 解析,所以需要在 Gradle 加入依赖
build.gradle

compile 'com.squareup.retrofit2:converter-gson:2.0.2'
  • 1

运行结果

运行结果

Demo地址

Carson_Ho的Github:https://github.com/Carson-Ho/RetrofitDemo


5. Retrofit 的拓展使用

  • Retrofit的使用场景非常丰富,如支持RxJavaPrototocobuff
  • 具体设置也非常简单 & 方便:
<-- 主要在创建Retrofit对象中设置 -->
Retrofit retrofit = new Retrofit.Builder()
  .baseUrl(""http://fanyi.youdao.com/"")
  .addConverterFactory(ProtoConverterFactory.create()) // 支持Prototocobuff解析
  .addConverterFactory(GsonConverterFactory.create()) // 支持Gson解析
  .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) // 支持RxJava
  .build();

 

具体关于 RxJava的使用这里就不展开,请期待下篇关于 Rxjava的文章。


6. 总结

  • 看完本文,相信你已经非常熟悉 Retrofit 2.0 的使用
  • 如果你希望继续阅读 Retrofit 2.0 的源码,请看我写的文章:Android:手把手带你深入剖析 Retrofit 2.0 源码
  • 接下来,我将继续分析与 Retrofit 配合使用的 RxJava

这可能是*好的RxJava 2.x 入门教程(一)

前言

RxJava 对大家而言肯定不陌生,其受欢迎程度不言而喻。而在去年的早些时候,官方便宣布,将在一段时间后不再对 RxJava 1.x 进行维护,而在仓库中另辟蹊径,开始对 RxJava 2.x 进行推广起来,我原本是不想写这么一套教程的,因为 RxJava 受欢迎度这么高,而且这 2.x 也出来了这么久,我坚信网上一定有很多超级大牛早已为大家避雷。然而很难过的是,我搜索了些时间,能搜出来的基本都是对 RxJava 1.x 的讲解,或者是 Blog 标题就没说清楚是否是 2.x 系列(对于我们这种标题党来说很难受)。这不,我就来抛砖引玉了。

咱们先不提别的,先为大家带点可能你早已熟知的干货——来自扔物线大神的给Android开发者的 RxJava 详解。

该文详细地为大家讲解了 RxJava 的优势、原理以及使用方式和适用情景,一定被众多的 Android 开发者视为神器。可惜,文章历史比较久远,基本都是讲解的 RxJava 1.x了。

那关注的小伙伴一定会问,那我没用过 RxJava 1.x ,还有必要先学习 1.x 的内容吗?

个人觉得不必要,因为 RxJava 2.x 是按照 Reactive-Streams specification 规范完全的重写的,完全独立于 RxJava 1.x 而存在,它改变了以往 RxJava 的用法。

额,由于个人能力水平有限,所以对于英文基础好的,大家可以去官网查阅相关 API 介绍,而对于英文不那么流畅的童鞋,我也为大家准备了干货:RxJava2Examples (正在更新)。

与RxJava 1.x的差异

其实,我标题为入门教程,按理说应该从简单入门开始讲的,原谅我突然偏题了,因为我觉得可能大多数人都了解或者使用过RxJava 1.x(因为它真的太棒了)。虽然可能熟悉1.x 的你可以直接扒文档就可以了,但这么大的变化,请原谅我还在这里瞎比比。

  • Nulls
    这是一个很大的变化,熟悉 RxJava 1.x 的童鞋一定都知道,1.x 是允许我们在发射事件的时候传入 null 值的,但现在我们的 2.x 不支持了,不信你试试? 大大的 NullPointerException 教你做人。这意味着 Observable<Void> 不再发射任何值,而是正常结束或者抛出空指针。
  • 2、Flowable
    在 RxJava 1.x 中关于介绍 backpressure 部分有一个小小的遗憾,那就是没有用一个单独的类,而是使用 Observable 。而在 2.x 中 Observable 不支持背压了,将用一个全新的 Flowable 来支持背压。
    或许对于背压,有些小伙伴们还不是特别理解,这里简单说一下。大概就是指在异步场景中,被观察者发送事件的速度远快于观察者的处理速度的情况下,一种告诉上游的被观察者降低发送速度的策略。感兴趣的小伙伴可以模拟这种情况,在差距太大的时候,我们的内存会猛增,直到OOM。而我们的 Flowable 一定意义上可以解决这样的问题,但其实并不能完全解决,这个后面可能会提到。
  • Single/Completable/Maybe
    其实这三者都差不多,Single 顾名思义,只能发送一个事件,和 Observable接受可变参数完全不同。而 Completable 侧重于观察结果,而 Maybe 是上面两种的结合体。也就是说,当你只想要某个事件的结果(true or false)的时候,你可以使用这种观察者模式。
  • 线程调度相关
    这一块基本没什么改动,但细心的小伙伴一定会发现,RxJava 2.x 中已经没有了 Schedulers.immediate()这个线程环境,还有 Schedulers.test()
  • Function相关
    熟悉 1.x 的小伙伴一定都知道,我们在1.x 中是有 Func1Func2…..FuncN的,但 2.x 中将它们移除,而采用 Function 替换了 Func1,采用 BiFunction 替换了 Func 2..N。并且,它们都增加了 throws Exception,也就是说,妈妈再也不用担心我们做某些操作还需要 try-catch 了。
  • 其他操作符相关
    如 Func1...N 的变化,现在同样用 Consumer 和 BiConsumer 对 Action1 和 Action2 进行了替换。后面的 Action 都被替换了,只保留了 ActionN

附录

下面从官方截图展示 2.x 相对 1.x 的改动细节,仅供参考。

 

%title插图%num
%title插图%num
%title插图%num
%title插图%num
%title插图%num
%title插图%num
%title插图%num
%title插图%num

Android 动态加载 (三) PAK 详解

Android 动态加载 (三) PAK 详解

pak文件经常出现于游戏的安装目录中,其实pak文件是一种特殊的游戏压缩文件,用于压缩声音、图片等资料。由于pak文件专门针对游戏设计文件结构,pak文件就是将多个文件(图片、音乐、文本)打包为一个单独文件,在pak文件中保存着多个文件的数据。

pak是什么文件?

现 在大部分游戏的客户端都采用pak压缩格式,以便于游戏的开发。由于是一种压缩格式,pak文件可以用一些专业压缩软件(如WinRAR、 WinZip)打开,但由于其特殊性与一般的压缩文件格式打开方式有所不同。pak文件一般是游戏的文件,里面存了很多游戏所需的重要文件,并且是加密了 的,现在虽说有能打开pak文件的软件,但也仅限于未加密的。不过有一些专门的浏览工具可以提取其中的声音进行修改后保存为其他格式音乐。

pak是什么文件

pak文件如何打开?

那么pak文件怎么打开呢?下面小编就收集了一些支持pak文件的软件供大家使用。

pak文件如何打开?
特点:
1.声音,地图,3D模型,材质贴图等文件是按着保留目录路径结构的方式压缩入PAK文件的。
2.压缩的时候可以令压缩也可以按照一定的压缩率压缩。
使用pak这种文件格式对于游戏的开发者和玩家都有著无以伦比的方便性和可塑性(或者说自定义性)。具体如何实现的,等一下我将举例说明。使用pak包这种形式有几点好处:
1.对于程开发人员来说资源调用方便,易于管理;
2.由于结构特殊,对初学者有屏蔽作用避免被乱改,对高手却很容易修改,比较方便;
3.所有模式都是利用这个特点做的;
4.pak包被游戏的引擎视为一个目录,能利用外置pak文件的形式进行升级而无需删除原有文件。利於增加效果包,新地图,改进界面,增加model等扩展内容;
5.修改还原便利,文件之间互不影响。
如果想要打开pak结尾的文件,使用PAk浏览器
RPGViewer 3.0
下载了以后一定要看说明书(readme.txt)
可以直接查看PAK格式的内容
PAK解压:
工具里面有“解压包”的按钮,可以解压PAK

Android 动态加载 (二) 态加载机制 案例二

Android 动态加载 (二) 态加载机制 案例二

探秘腾讯Android手机游戏平台之不安装游戏APK直接启动法

 

重要说明

 

在实践的过程中大家都会发现资源引用的问题,这里重点声明两点:
1. 资源文件是不能直接inflate的,如果简单的话直接在程序中用代码书写。
2. 资源文件是不能用R来引用的,因为上下文已经不同了,腾讯的做法是将资源文件打包(*.pak文件和APK打包在一起),虽然APK是没有进行安装,但是 资源文件是另外解压到指定文件夹下面的,然后将文件夹的地址传给了第三方应用程序,这样第三方应用程序通过File的inputstream流还是可以读 取和使用这些资源的。

 

实践

 

我实现了一个小小的Demo,麻雀虽小五脏俱全,为了突出原理,我就尽量简化了程序,通过这个实例来让大家明白后台的工作原理。

 

1、下载demo的apk程序apks ,其中包括了两个apk,分别是A和B

2、这两个APK可分别安装和运行,A程序界面只显示一个Button,B程序界面会动态显示当前的时间

3、下面的三幅图片分别为直接启动运行A程序(安装TestA.apk),直接启动运行B程序(安装TestB.apk)和由A程序动态启动B程序 (安装TestA.apk,TestB.apk不用安装,而是放在/mnt/sdcard/目录中,即 SD卡上)的截图,细心的同学可以停下来观察一下他们之间的不同

 

%title插图%num%title插图%num%title插图%num

后两幅图片的不同,也即Title的不同,则解释出了我们将要分析的后台实现原理的机制

 

实现原理

 

  1. @Override  
  2. public void onCreate(Bundle savedInstanceState) {  
  3.     super.onCreate(savedInstanceState);  
  4.     setContentView(R.layout.main);
  5.     Button btn = (Button) findViewById(R.id.btn);
  6.     btn.setOnClickListener(new OnClickListener() {  
  7.         @Override  
  8.         public void onClick(View v) {  
  9.             Bundle paramBundle = new Bundle();  
  10.             paramBundle.putBoolean(“KEY_START_FROM_OTHER_ACTIVITY”, true);  
  11.             String dexpath = “/mnt/sdcard/TestB.apk”;  
  12.             String dexoutputpath = “/mnt/sdcard/”;  
  13.             LoadAPK(paramBundle, dexpath, dexoutputpath);
  14.         }
  15.     });
  16. }

 

代码解析:这就是OnCreate函数要做的事情,装载view界面,绑定button事件,大家都熟悉了,还有就是设置程序B的放置路径,因为我程序中 代码是从 /mnt/sdcard/TestB.apk中动态加载,这也就是为什么要让大家把TestB.apk放在SD卡上面的原因了。关键的函数就是*后一个了 LoadAPK,它来实现动态加载B程序。

 

  1. public void LoadAPK(Bundle paramBundle, String dexpath, String dexoutputpath) {  
  2.     ClassLoader localClassLoader = ClassLoader.getSystemClassLoader();
  3.     DexClassLoader localDexClassLoader = new DexClassLoader(dexpath,  
  4.             dexoutputpath, null, localClassLoader);  
  5.     try {  
  6.         PackageInfo plocalObject = getPackageManager()
  7.                 .getPackageArchiveInfo(dexpath, 1);  
  8.         if ((plocalObject.activities != null)  
  9.                 && (plocalObject.activities.length > 0)) {  
  10.             String activityname = plocalObject.activities[0].name;  
  11.             Log.d(TAG, “activityname = ” + activityname);  
  12.             Class localClass = localDexClassLoader.loadClass(activityname);
  13.             Constructor localConstructor = localClass
  14.                     .getConstructor(new Class[] {});  
  15.             Object instance = localConstructor.newInstance(new Object[] {});  
  16.             Log.d(TAG, “instance = ” + instance);  
  17.             Method localMethodSetActivity = localClass.getDeclaredMethod(
  18.                     “setActivity”, new Class[] { Activity.class });  
  19.             localMethodSetActivity.setAccessible(true);  
  20.             localMethodSetActivity.invoke(instance, new Object[] { this });  
  21.             Method methodonCreate = localClass.getDeclaredMethod(
  22.                     “onCreate”, new Class[] { Bundle.class });  
  23.             methodonCreate.setAccessible(true);  
  24.             methodonCreate.invoke(instance, new Object[] { paramBundle });  
  25.         }
  26.         return;  
  27.     } catch (Exception ex) {  
  28.         ex.printStackTrace();
  29.     }
  30. }

 

代码解析:这个函数要做的工作如下:加载B程序的APK文件,通过类加载器DexClassLoader来解析APK文件,这样会在SD卡上面生成一个同 名的 后缀为dex的文件,例如/mnt/sdcard/TestB.apk==>/mnt/sdcard/TestB.dex,接下来就是通过java 反射机制,动态实例化B中的Activity对象,并依次调用了其中的两个函数,分别为setActivity和onCreate.看到这里,大家是不是 觉得有点奇怪,Activity的启动函数是onCreate,为什么要先调用setActivity,而更奇怪的是setActivity并不是系统的 函数,确实,那是我们自定义的,这也就是核心的地方。

 

好了带着这些疑问,我们再来分析B程序的主代码:

 

  1. public class TestBActivity extends Activity {  
  2.     private static final String TAG = “TestBActivity”;  
  3.     private Activity otherActivity;  
  4.     @Override  
  5.     public void onCreate(Bundle savedInstanceState) {  
  6.         boolean b = false;  
  7.         if (savedInstanceState != null) {  
  8.             b = savedInstanceState.getBoolean(“KEY_START_FROM_OTHER_ACTIVITY”, false);  
  9.             if (b) {  
  10.                 this.otherActivity.setContentView(new TBSurfaceView(  
  11.                         this.otherActivity));  
  12.             }
  13.         }
  14.         if (!b) {  
  15.             super.onCreate(savedInstanceState);  
  16.             // setContentView(R.layout.main);  
  17.             setContentView(new TBSurfaceView(this));  
  18.         }
  19.     }
  20.     public void setActivity(Activity paramActivity) {  
  21.         Log.d(TAG, “setActivity…” + paramActivity);  
  22.         this.otherActivity = paramActivity;  
  23.     }
  24. }

 

代码解析:看完程序B的实现机制,大家是不是有种恍然大悟的感觉,这根本就是“偷梁换柱”嘛,是滴,程序B动态借用了程序A的上下文执行环境,这也就是上 面后两幅图 的差异,*后一幅图运行的是B的程序,但是title表示的却是A的信息,而没有重新初始化自己的,实际上这也是不可能的,所以有些童鞋虽然通过java 的反射机制,正确呼叫了被调程序的onCreate函数,但是期望的结果还是没有出现,原因就是这个上下文环境没有正确建立起来,但是若通过 startActivity的方式来启动APK的话,android系统会替你建立正确的执行时环境,所以就没问题。至于那个 TBSurfaceView,那就是自定义的一个view画面,动态画当前的时间

 

  1. public class TBSurfaceView extends SurfaceView implements Callback, Runnable {  
  2.     private SurfaceHolder sfh;  
  3.     private Thread th;  
  4.     private Canvas canvas;  
  5.     private Paint paint;  
  6.     public TBSurfaceView(Context context) {  
  7.         super(context);  
  8.         th = new Thread(this);  
  9.         sfh = this.getHolder();  
  10.         sfh.addCallback(this);  
  11.         paint = new Paint();  
  12.         paint.setAntiAlias(true);  
  13.         paint.setColor(Color.RED);
  14.         this.setKeepScreenOn(true);  
  15.     }
  16.     public void surfaceCreated(SurfaceHolder holder) {  
  17.         th.start();
  18.     }
  19.     private void draw() {  
  20.         try {  
  21.             canvas = sfh.lockCanvas();
  22.             if (canvas != null) {  
  23.                 canvas.drawColor(Color.WHITE);
  24.                 canvas.drawText(“Time: ” + System.currentTimeMillis(), 100,  
  25.                         100, paint);  
  26.             }
  27.         } catch (Exception ex) {  
  28.             ex.printStackTrace();
  29.         } finally {  
  30.             if (canvas != null) {  
  31.                 sfh.unlockCanvasAndPost(canvas);
  32.             }
  33.         }
  34.     }
  35.     public void run() {  
  36.         while (true) {  
  37.             draw();
  38.             try {  
  39.                 Thread.sleep(100);  
  40.             } catch (InterruptedException e) {  
  41.                 e.printStackTrace();
  42.             }
  43.         }
  44.     }
  45.     public void surfaceChanged(SurfaceHolder holder, int format, int width,  
  46.             int height) {  
  47.     }
  48.     public void surfaceDestroyed(SurfaceHolder holder) {  
  49.     }
  50. }

 

平台解析:

说了这么多,都是背景,O(∩_∩)O哈哈~

其实腾讯游戏平台就是这么个实现原理,我也是通过它才学习到这种方式的,还得好好感谢感谢呢。

腾讯Android游戏平台的游戏分成两类,*类是腾讯自主研发的,像斗地主,五子棋,连连看什么的,所以实现机制就如上面的所示,A代表游戏大 厅,B代表斗地主类的小游戏。第二类是第三方软件公司开发的,可就不能已这种方式来运作了,毕竟腾讯不能限制别人开发代码的方式啊,所以腾讯就开放了一个 sdk包出来,让第三方应用可以和游戏大厅相结合,具体可参见QQ游戏中心开发者平台 ,但这同时就损失了一个优点,那就是第三方开发的游戏要通过安装的方式才能运行。

 

结论:

看到这里,相信大家都比较熟悉这个背后的原理了吧,也希望大家能提供更好的反馈信息!

程序源码下载source

 

来源:http://blog.zhourunsheng.com/2011/09/%E6%8E%A2%E7%A7%98%E8%85%BE%E8 %AE%AFandroid%E6%89%8B%E6%9C%BA%E6%B8%B8%E6%88%8F%E5%B9%B3%E5%8F%B0%E4%B9%8B%E4%B8%8D%E5%AE%89%E8%A3%85%E6%B8%B8%E6%88%8Fapk%E7%9B%B4%E6%8E%A5%E5%90%AF%E5%8A%A8%E6%B3%95/

其他参考:

Android 动态加载APK–代码安装、获取资源及Intent调用已安装apk

Android应用开发提高系列(4)——Android动态加载(上)——加载未安装APK中的类

Android应用开发提高系列(5)——Android动态加载(下)——加载已安装APK中的类和资源

前言

近期做换肤功能,由于换肤程度较高,受限于平台本身,实现起来较复杂,暂时搁置了该功能,但也积累了一些经验,将分两篇文章来写这部分的内容,欢迎交流!

关键字:Android动态加载

 

声明

欢迎转载,但请保留文章原始出处:)

博客园:http://www.cnblogs.com

农民伯伯: http://over140.cnblogs.com

Android中文Wiki:http://wikidroid.sinaapp.com

 

正文

一、前提

目的:动态加载SD卡中Apk的类。

注意:被加载的APK是未安装的。

相关:本文是本博另外一篇文章:Android动态加载jar/dex的升级版。

 

    截图: 成功截图:

%title插图%num

二、准备

准备调用Android工程:TestB

ITest

复制代码
复制代码
public interface ITest {
String getMoney();
}
复制代码
复制代码

TestBActivity

复制代码
复制代码
public class TestBActivity extends Activity implements ITest {
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
}@Override
public String getMoney() {
return “1”;
}

}

复制代码
复制代码

代码说明:很简单的代码。将生成后的TestB.apk拷贝到SD卡的根目录下。

 

三、调用

调用工程TestA

复制代码
复制代码
public class TestAActivity extends Activity {
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);String path = Environment.getExternalStorageDirectory() + “/”;
String filename = “TestB.apk”;
DexClassLoader classLoader = new DexClassLoader(path + filename, path,
null, getClassLoader());

try {
Class mLoadClass = classLoader.loadClass(“com.nmbb.TestBActivity”);
Constructor constructor = mLoadClass.getConstructor(new Class[] {});
Object TestBActivity = constructor.newInstance(new Object[] {});

Method getMoney = mLoadClass.getMethod(“getMoney”, null);
getMoney.setAccessible(true);
Object money = getMoney.invoke(TestBActivity, null);
Toast.makeText(this, money.toString(), Toast.LENGTH_LONG).show();

} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SecurityException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}

复制代码
复制代码

执行的时候可以发现会自动生成TestB.dex文件。动态加载方面还可以搜索一下”Java动态加载”方面的资料,很有参考价值。可以发现比Android动态加载jar/dex使用起来方便得多。

 

四、下载

TestA.zip

TestB.zip

 

五、注意

6.1  别忘了加上SDCARD的写权限:

android.permission.WRITE_EXTERNAL_STORAGE

6.2  同样注意,不要再两个工程包含package和名称相同的接口,否则报错。(参见Android动态加载jar/dex的后期维护)

 

六、扩展阅读

探秘腾讯Android手机游戏平台之不安装游戏APK直接启动法

(强烈推荐:QQ游戏动态调用Activity的方法:通过ClassLoader,loadClass Activity类,然后分别在主工程的onDestroy、onKeyDown、onPause、onRestart、onResume等生命周期方法 中反射调用(Method、invoke)子工程的类方法来模拟实现整个生命周期。此外巧妙的通过解压缩APK文件来获取游戏的资源)

 

    Android中文Wiki:DexFile

 

 

七、缺点

6.1  由于是使用反射,无法取得Context,也就是TestBActivity与普通的类毫无区别,没有生命周期。

 

八、推荐

Android版 程序员专用搜索

Android 动态加载 (一) 态加载机制 案例一

Android 动态加载 (一) 态加载机制 案例一

在目前的软硬件环境下,Native App与Web App在用户体验上有着明显的优势,但在实际项目中有些会因为业务的频繁变更而频繁的升级客户端,造成较差的用户体验,而这也恰恰是Web App的优势。本文对网上Android动态加载jar的资料进行梳理和实践在这里与大家一起分享,试图改善频繁升级这一弊病。

Android应用开发在一般情况下,常规的开发方式和代码架构就能满足我们的普通需求。但是有些特殊问题,常常引发我们进一步的沉思。我们从沉思中产生顿悟,从而产生新的技术形式。
如何开发一个可以自定义控件的Android应用?就像eclipse一样,可以动态加载插件;如何让Android应用执行服务器上的不可预知的代码?如何对Android应用加密,而只在执行时自解密,从而防止被破解?……
熟悉Java技术的朋友,可能意识到,我们需要使用类加载器灵活的加载执行的类。这在Java里已经算是一项比较成熟的技术了,但是在Android中,我们大多数人都还非常陌生。
类加载机制
Dalvik虚拟机如同其他Java虚拟机一样,在运行程序时首先需要将对应的类加载到内存中。而在Java标准的虚拟机中,类加载可以从class文 件中读取,也可以是其他形式的二进制流,因此,我们常常利用这一点,在程序运行时手动加载Class,从而达到代码动态加载执行的目的
然而Dalvik虚拟机毕竟不算是标准的Java虚拟机,因此在类加载机制上,它们有相同的地方,也有不同之处。我们必须区别对待

例如,在使用标准Java虚拟机时,我们经常自定义继承自ClassLoader的类加载器。然后通过defineClass方法来从一个二进制流中加 载Class。然而,这在Android里是行不通的,大家就没必要走弯路了。参看源码我们知道,Android中ClassLoader的 defineClass方法具体是调用VMClassLoader的defineClass本地静态方法。而这个本地方法除了抛出一个 “UnsupportedOperationException”之外,什么都没做,甚至连返回值都为空

  1. static void Dalvik_java_lang_VMClassLoader_defineClass(const u4* args,JValue* pResult){  
  2.     Object* loader = (Object*) args[0];
  3.     StringObject* nameObj = (StringObject*) args[1];
  4.     const u1* data = (const u1*) args[2];  
  5.     int offset = args[3];  
  6.     int len = args[4];  
  7.     Object* pd = (Object*) args[5];
  8.     char* name = NULL;  
  9.     name = dvmCreateCstrFromString(nameObj);
  10.     LOGE(“ERROR: defineClass(%p, %s, %p, %d, %d, %p)\n”,loader, name, data, offset, len, pd);  
  11.     dvmThrowException(“Ljava/lang/UnsupportedOperationException;”,”can’t load this type of class file”);  
  12.     free(name);
  13.     RETURN_VOID();
  14. }

Dalvik虚拟机类加载机制
那如果在Dalvik虚拟机里,ClassLoader不好使,我们如何实现动态加载类呢?Android为我们从ClassLoader派生出了两个 类:DexClassLoader和PathClassLoader。其中需要特别说明的是PathClassLoader中一段被注释掉的代码:

  1. /* –this doesn’t work in current version of Dalvik– 
  2.     if (data != null) { 
  3.         System.out.println(“— Found class ” + name 
  4.             + ” in zip[” + i + “] ‘” + mZips[i].getName() + “‘”); 
  5.         int dotIndex = name.lastIndexOf(‘.’); 
  6.         if (dotIndex != -1) { 
  7.             String packageName = name.substring(0, dotIndex); 
  8.             synchronized (this) { 
  9.                 Package packageObj = getPackage(packageName); 
  10.                 if (packageObj == null) { 
  11.                     definePackage(packageName, null, null, 
  12.                             null, null, null, null, null); 
  13.                 } 
  14.             } 
  15.         } 
  16.         return defineClass(name, data, 0, data.length); 
  17.     } 
  18. */  

这从另一方面佐证了defineClass函数在Dalvik虚拟机里确实是被阉割了。而在这两个继承自ClassLoader的类加载器,本质上是重载了ClassLoader的findClass方法。在执行loadClass时,我们可以参看ClassLoader部分源码:

  1. protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {  
  2. Class<?> clazz = findLoadedClass(className);
  3.     if (clazz == null) {  
  4.         try {  
  5.             clazz = parent.loadClass(className, false);  
  6.         } catch (ClassNotFoundException e) {  
  7.             // Don’t want to see this.  
  8.         }
  9.         if (clazz == null) {  
  10.             clazz = findClass(className);
  11.         }
  12.     }
  13.     return clazz;  
  14. }

因 此DexClassLoader和PathClassLoader都属于符合双亲委派模型的类加载器(因为它们没有重载loadClass方法)。也就是 说,它们在加载一个类之前,回去检查自己以及自己以上的类加载器是否已经加载了这个类。如果已经加载过了,就会直接将之返回,而不会重复加载。
DexClassLoader 和PathClassLoader其实都是通过DexFile这个类来实现类加载的。这里需要顺便提一下的是,Dalvik虚拟机识别的是dex文件,而 不是class文件。因此,我们供类加载的文件也只能是dex文件,或者包含有dex文件的.apk或.jar文件。
也许有人想到,既然 DexFile可以直接加载类,那么我们为什么还要使用ClassLoader的子类呢?DexFile在加载类时,具体是调用成员方法 loadClass或者loadClassBinaryName。其中loadClassBinaryName需要将包含包名的类名中的”.”转换 为”/”我们看一下loadClass代码就清楚了:

  1. public Class loadClass(String name, ClassLoader loader) {  
  2.         String slashName = name.replace(‘.’, ‘/’);  
  3.         return loadClassBinaryName(slashName, loader);  
  4. }

在这段代码前有一段注释,截取关键一部分就是说:If you are not calling this from a class loader, this is most likely not going to do what you want. Use {@link Class#forName(String)} instead. 这就是我们需要使用ClassLoader子类的原因。至于它是如何验证是否是在ClassLoader中调用此方法的,我没有研究,大家如果有兴趣可以 继续深入下去。
有一个细节,可能大家不容易注意到。PathClassLoader是通过构造函数new DexFile(path)来产生DexFile对象的;而DexClassLoader则是通过其静态方法loadDex(path, outpath, 0)得到DexFile对象。这两者的区别在于DexClassLoader需要提供一个可写的outpath路径, 用来释放.apk包或者.jar包中的dex文件。换个说法来说,就是PathClassLoader不能主动从zip包中释放出dex,因此只支持直接 操作dex格式文件,或者已经安装的apk(因为已经安装的apk在cache中存在缓存的dex文件)。而DexClassLoader可以支 持.apk、.jar和.dex文件,并且会在指定的outpath路径释放出dex文件。

另外,PathClassLoader在加载类时调用的是DexFile的loadClassBinaryName,而DexClassLoader调用的是loadClass。因此,在使用PathClassLoader时类全名需要用”/”替换”.”

 

实际操作

使用到的工具都比较常规:javac、dx、eclipse等其中dx工具*好是指明–no-strict,因为class文件的路径可能不匹配
加 载好类后,通常我们可以通过Java反射机制来使用这个类但是这样效率相对不高,而且老用反射代码也比较复杂凌乱。更好的做法是定义一个 interface,并将这个interface写进容器端。待加载的类,继承自这个interface,并且有一个参数为空的构造函数,以使我们能够通 过Class的newInstance方法产生对象然后将对象强制转换为interface对象,于是就可以直接调用成员方法了,下面是具体的实现步骤 了:
*步:

编写好动态代码类:

%title插图%num

  1. package com.dynamic.interfaces;  
  2. import android.app.Activity;  
  3. /** 
  4.  * 动态加载类的接口 
  5.  */  
  6. public interface IDynamic {  
  7.     /**初始化方法*/  
  8.     public void init(Activity activity);  
  9.     /**自定义方法*/  
  10.     public void showBanner();  
  11.     public void showDialog();  
  12.     public void showFullScreen();  
  13.     public void showAppWall();  
  14.     /**销毁方法*/  
  15.     public void destory();  
  16. }

实现类代码如下:

  1. package com.dynamic.impl;  
  2. import android.app.Activity;  
  3. import android.widget.Toast;  
  4. import com.dynamic.interfaces.IDynamic;  
  5. /** 
  6.  * 动态类的实现 
  7.  * 
  8.  */  
  9. public class Dynamic implements IDynamic{  
  10.     private Activity mActivity;  
  11.     @Override  
  12.     public void init(Activity activity) {  
  13.         mActivity = activity;
  14.     }
  15.     @Override  
  16.     public void showBanner() {  
  17.         Toast.makeText(mActivity, “我是ShowBannber方法”, 1500).show();  
  18.     }
  19.     @Override  
  20.     public void showDialog() {  
  21.         Toast.makeText(mActivity, “我是ShowDialog方法”, 1500).show();  
  22.     }
  23.     @Override  
  24.     public void showFullScreen() {  
  25.         Toast.makeText(mActivity, “我是ShowFullScreen方法”, 1500).show();  
  26.     }
  27.     @Override  
  28.     public void showAppWall() {  
  29.         Toast.makeText(mActivity, “我是ShowAppWall方法”, 1500).show();  
  30.     }
  31.     @Override  
  32.     public void destory() {  
  33.     }
  34. }

这样动态类就开发好了

 

第二步:

将上面开发好的动态类打包成.jar,这里要注意的是只打包实现类Dynamic.java,不打包接口类IDynamic.java,

%title插图%num

然后将打包好的jar文件拷贝到android的安装目录中的platform-tools目录下,使用dx命令:(我的jar文件是dynamic.jar)

dx –dex –output=dynamic_temp.jar dynamic.jar

这样就生成了dynamic_temp.jar,这个jar和dynamic.jar有什么区别呢?

其 实这条命令主要做的工作是:首先将dynamic.jar编译成dynamic.dex文件(Android虚拟机认识的字节码文件),然后再将 dynamic.dex文件压缩成dynamic_temp.jar,当然你也可以压缩成.zip格式的,或者直接编译成.apk文件都可以的,这个后面 会说到。

到这里还不算完事,因为你想想用什么来连接动态类和目标类呢?那就是动态类的接口了,所以这时候还要打个.jar包,这时候只需要打接口类IDynamic.java了

%title插图%num

然后将这个.jar文件引用到目标类中,下面来看一下目标类的实现:

 

  1. package com.jiangwei.demo;  
  2. import java.io.File;  
  3. import java.util.List;  
  4. import android.app.Activity;  
  5. import android.content.Intent;  
  6. import android.content.pm.ActivityInfo;  
  7. import android.content.pm.PackageManager;  
  8. import android.content.pm.ResolveInfo;  
  9. import android.os.Bundle;  
  10. import android.os.Environment;  
  11. import android.view.View;  
  12. import android.widget.Button;  
  13. import android.widget.Toast;  
  14. import com.dynamic.interfaces.IDynamic;  
  15. import dalvik.system.DexClassLoader;  
  16. import dalvik.system.PathClassLoader;  
  17. public class AndroidDynamicLoadClassActivity extends Activity {  
  18.     //动态类加载接口  
  19.     private IDynamic lib;  
  20.     @Override  
  21.     public void onCreate(Bundle savedInstanceState) {  
  22.         super.onCreate(savedInstanceState);  
  23.         setContentView(R.layout.main);
  24.         //初始化组件  
  25.         Button showBannerBtn = (Button) findViewById(R.id.show_banner_btn);
  26.         Button showDialogBtn = (Button) findViewById(R.id.show_dialog_btn);
  27.         Button showFullScreenBtn = (Button) findViewById(R.id.show_fullscreen_btn);
  28.         Button showAppWallBtn = (Button) findViewById(R.id.show_appwall_btn);
  29.         /**使用DexClassLoader方式加载类*/  
  30.         //dex压缩文件的路径(可以是apk,jar,zip格式)  
  31.         String dexPath = Environment.getExternalStorageDirectory().toString() + File.separator + “Dynamic.apk”;  
  32.         //dex解压释放后的目录  
  33.         //String dexOutputDir = getApplicationInfo().dataDir;  
  34.         String dexOutputDirs = Environment.getExternalStorageDirectory().toString();
  35.         //定义DexClassLoader  
  36.         //*个参数:是dex压缩文件的路径  
  37.         //第二个参数:是dex解压缩后存放的目录  
  38.         //第三个参数:是C/C++依赖的本地库文件目录,可以为null  
  39.         //第四个参数:是上一级的类加载器  
  40.         DexClassLoader cl = new DexClassLoader(dexPath,dexOutputDirs,null,getClassLoader());  
  41.         /**使用PathClassLoader方法加载类*/  
  42.         //创建一个意图,用来找到指定的apk:这里的”com.dynamic.impl是指定apk中在AndroidMainfest.xml文件中定义的<action name=”com.dynamic.impl”/>    
  43.         Intent intent = new Intent(“com.dynamic.impl”, null);    
  44.         //获得包管理器    
  45.         PackageManager pm = getPackageManager();
  46.         List<ResolveInfo> resolveinfoes =  pm.queryIntentActivities(intent, 0);    
  47.         //获得指定的activity的信息    
  48.         ActivityInfo actInfo = resolveinfoes.get(0).activityInfo;    
  49.         //获得apk的目录或者jar的目录    
  50.         String apkPath = actInfo.applicationInfo.sourceDir;
  51.         //native代码的目录    
  52.         String libPath = actInfo.applicationInfo.nativeLibraryDir;
  53.         //创建类加载器,把dex加载到虚拟机中    
  54.         //*个参数:是指定apk安装的路径,这个路径要注意只能是通过actInfo.applicationInfo.sourceDir来获取  
  55.         //第二个参数:是C/C++依赖的本地库文件目录,可以为null  
  56.         //第三个参数:是上一级的类加载器  
  57.         PathClassLoader pcl = new PathClassLoader(apkPath,libPath,this.getClassLoader());  
  58.         //加载类  
  59.         try {  
  60.             //com.dynamic.impl.Dynamic是动态类名  
  61.             //使用DexClassLoader加载类  
  62.             //Class libProviderClazz = cl.loadClass(“com.dynamic.impl.Dynamic”);  
  63.             //使用PathClassLoader加载类  
  64.             Class libProviderClazz = pcl.loadClass(“com.dynamic.impl.Dynamic”);  
  65.             lib = (IDynamic)libProviderClazz.newInstance();
  66.             if(lib != null){  
  67.                 lib.init(AndroidDynamicLoadClassActivity.this);  
  68.             }
  69.         } catch (Exception exception) {  
  70.             exception.printStackTrace();
  71.         }
  72.         /**下面分别调用动态类中的方法*/  
  73.         showBannerBtn.setOnClickListener(new View.OnClickListener() {  
  74.             public void onClick(View view) {  
  75.                if(lib != null){  
  76.                    lib.showBanner();
  77.                }else{  
  78.                    Toast.makeText(getApplicationContext(), “类加载失败”, 1500).show();  
  79.                }
  80.             }
  81.         });
  82.         showDialogBtn.setOnClickListener(new View.OnClickListener() {  
  83.             public void onClick(View view) {  
  84.                if(lib != null){  
  85.                    lib.showDialog();
  86.                }else{  
  87.                    Toast.makeText(getApplicationContext(), “类加载失败”, 1500).show();  
  88.                }
  89.             }
  90.         });
  91.         showFullScreenBtn.setOnClickListener(new View.OnClickListener() {  
  92.             public void onClick(View view) {  
  93.                if(lib != null){  
  94.                    lib.showFullScreen();
  95.                }else{  
  96.                    Toast.makeText(getApplicationContext(), “类加载失败”, 1500).show();  
  97.                }
  98.             }
  99.         });
  100.         showAppWallBtn.setOnClickListener(new View.OnClickListener() {  
  101.             public void onClick(View view) {  
  102.                if(lib != null){  
  103.                    lib.showAppWall();
  104.                }else{  
  105.                    Toast.makeText(getApplicationContext(), “类加载失败”, 1500).show();  
  106.                }
  107.             }
  108.         });
  109.     }
  110. }

这里面定义了一个IDynamic接口变量,同时使用了DexClassLoader和PathClassLoader来加载类,这里面先来说一说DexClassLoader方式加载:

 

  1. //定义DexClassLoader  
  2. //*个参数:是dex压缩文件的路径  
  3. //第二个参数:是dex解压缩后存放的目录  
  4. //第三个参数:是C/C++依赖的本地库文件目录,可以为null  
  5. //第四个参数:是上一级的类加载器  
  6. DexClassLoader cl = new DexClassLoader(dexPath,dexOutputDirs,null,getClassLoader());  

上面已经说了,DexClassLoader是继承ClassLoader类的,这里面的参数说明:

*个参数是:dex压缩文件的路径:这个就是我们将上面编译后的dynamic_temp.jar存放的目录,当然也可以是.zip和.apk格式的

第二个参数是:dex解压后存放的目录:这个就是将.jar,.zip,.apk文件解压出的dex文件存放的目录,这个就和PathClassLoader方法有区别了,同时你也可以看到PathClassLoader方法中没有这个参数,这个也真是这两个类的区别:

PathClassLoader 不能主动从zip包中释放出dex,因此只支持直接操作dex格式文件,或者已经安装的apk(因为已经安装的apk在手机的data/dalvik目录 中存在缓存的dex文件)。而DexClassLoader可以支持.apk、.jar和.dex文件,并且会在指定的outpath路径释放出dex文 件。

%title插图%num

然 而我们可以通过DexClassLoader方法指定解压后的dex文件的存放目录,但是我们一般不这么做,因为这样做无疑的暴露了dex文件,所以我们 一般不会将.jar/.zip/.apk压缩文件存放到用户可以察觉到的位置,同时解压dex的目录也是不能让用户看到的。

第三个参数和第四个参数用到的不是很多,所以这里就不做太多的解释了。

这里还要注意一点就是PathClassLoader方法的时候,*个参数是dex存放的路径,这里传递的是:

 

  1. //获得apk的目录或者jar的目录    
  2. String apkPath = actInfo.applicationInfo.sourceDir;

指定的apk安装路径,这个值只能这样获取,不然会加载类失败的

 

第三步:

运行目标类:

要做的工作是:

如果用的是DexClassLoader方式加载类:这时候需要将.jar或者.zip或者.apk文件放到指定的目录中,我这里为了方便就放到sd卡的根目录中

如果用的是PathClassLoader方法加载类:这时候需要先将Dynamic.apk安装到手机中,不然找不到这个activity,同时要注意的是:

  1. //创建一个意图,用来找到指定的apk:这里的”com.dynamic.impl是指定apk中在AndroidMainfest.xml文件中定义的<action name=”com.dynamic.impl”/>    
  2. Intent intent = new Intent(“com.dynamic.impl”, null);    

这里的com.dynamic.impl是一个action需要在指定的apk中定义,这个名称是动态apk和目标apk之间约定好的%title插图%num

运行结果

%title插图%num

点击showBanner显示一个Toast,成功的运行了动态类中的代码!

其实更好的办法就是将动态的.jar.zip.apk文件从网络上获取,安全可靠,同时本地的目标项目不需要改动代码就可以执行不同的逻辑了

 

1、运行这段代码时4.0.1以上版本会报:java.lang.IllegalArgumentException: optimizedDirectory not readable/writable: /storage/sdcard0
可以通过这个授权解决:

1
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

2、授权之后又会报:java.lang.IllegalArgumentException: Optimized data directory /storage/sdcard0 is not owned by the current user. Shared storage cannot protect your application from code injection attacks.
这个问题的原因是:在4.1系统由于This class loader requires an application-private, writable directory to cache optimized classes为了防止一下问题:
External storage does not provide access controls necessary to protect your application from code injection attacks.
所以加了一个判断Libcore.os.getuid() != Libcore.os.stat(parent).st_uid判断两个程序是不是同一个uid

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private DexFile(String sourceName, String outputName, int flags) throws IOException {
 if (outputName != null) {
 try {
 String parent = new File(outputName).getParent();
if (Libcore.os.getuid() != Libcore.os.stat(parent).st_uid) {
 throw new IllegalArgumentException("Optimized data directory " + parent
 + " is not owned by the current user. Shared storage cannot protect"
 + " your application from code injection attacks.");
 }
 } catch (ErrnoException ignored) {
 // assume we'll fail with a more contextual error later
 }
 }
 
mCookie = openDexFile(sourceName, outputName, flags);
 mFileName = sourceName;
 guard.open("close");
 //System.out.println("DEX FILE cookie is " + mCookie);
 }

解决方法是:指定dexoutputpath为APP自己的缓存目录

1
2
File dexOutputDir = context.getDir("dex", 0);
DexClassLoader dexClassLoader = new DexClassLoader(dexPath,dexOutputDir.getAbsolutePath(),null,getClassLoader());

 

 

关于代码加密的一些设想
*初设想将dex文件加密,然后通过JNI将解密代码写在Native层。解密之后直接传上二进制流,再通过defineClass将类加载到内存中。
现在也可以这样做,但是由于不能直接使用defineClass,而必须传文件路径给dalvik虚拟机内核,因此解密后的文件需要写到磁盘上,增加了被破解的风险。
Dalvik虚拟机内核仅支持从dex文件加载类的方式是不灵活的,由于没有非常深入的研究内核,我不能确定是Dalvik虚拟机本身不支持还是 Android在移植时将其阉割了。不过相信Dalvik或者是Android开源项目都正在向能够支持raw数据定义类方向努力。
我们可以在文档中看到Google说:Jar or APK file with “classes.dex”. (May expand this to include “raw DEX” in the future.);在Android的Dalvik源码中我们也能看到RawDexFile的身影(不过没有具体实现)
在RawDexFile出来之前,我们都只能使用这种存在一定风险的加密方式。需要注意释放的dex文件路径及权限管理,另外,在加载完毕类之后,除非出于其他目的否则应该马上删除临时的解密

2021年8月中国采购经理指数运行情况

国家统计局服务业调查中心中国物流与采购联合会   一、中国制造业采购经理指数运行情况   8月份,中国制造业采购经理指数(PMI)为50.1%,继续位于临界点以上,低于上月0.3个百分点,制造业扩张力度有所减弱。    从企业规模看,大型企业PMI为50.3% ,比上月回落1.4个百分点,继续高于临界点;中型企业PMI为51.2%,比上月上升1.2个百分点,高于临界点;小型企业PMI为48.2%,比上月回升0.4个百分点,低于临界点。   从分类指数看,在构成制造业PMI的5个分类指数中,生产指数高于临界点,新订单指数、原材料库存指数、从业人员指数和供应商配送时间指数均低于临界点。   生产指数为50.9%,比上月微落0.1个百分点,高于临界点,表明制造业生产扩张总体平稳。   新订单指数为49.6%,比上月下降1.3个百分点,低于临界点,表明制造业市场需求有所减弱。   原材料库存指数为47.7%,虽与上月持平,但仍低于临界点,表明制造业主要原材料库存量较上月有所下降。   从业人员指数为49.6%,虽与上月持平,但仍低于临界点,表明制造业企业用工需求略有回落。   供应商配送时间指数为48.0%,比上月回落0.9个百分点,低于临界点,表明制造业原材料供应商交货时间有所延长。  表1  中国制造业PMI及构成指数(经季节调整) 单位:%   PMI   生产 新订单 原材料 库存 从业人员 供应商配送 时间 2020年8月 51.0 53.5 52.0 47.3 49.4 50.4 2020年9月 51.5 54.0 52.8 48.5 49.6 50.7 2020年10月 51.4 53.9 52.8 48.0 49.3 50.6 2020年11月 52.1 54.7 53.9 48.6 49.5 50.1 2020年12月 51.9 54.2 53.6 48.6 49.6 49.9 2021年1月 51.3 53.5 52.3 49.0 48.4 48.8 2021年2月 50.6 51.9 51.5 47.7 48.1 47.9 2021年3月 51.9 53.9 53.6 48.4 50.1 50.0 2021年4月 51.1 52.2 52.0 48.3 49.6 48.7 2021年5月 51.0 52.7 51.3 47.7 48.9 47.6 2021年6月 50.9 51.9 51.5 48.0 49.2 47.9 2021年7月 50.4 51.0 50.9 47.7 49.6 48.9 2021年8月 50.1 50.9 49.6 47.7 49.6 48.0   表2  中国制造业PMI其他相关指标情况(经季节调整) 单位:%   新出口 订单 进口 采购量 主要原材料购进价格 出厂 价格 产成品 库存 在手 订单 生产经营 活动预期 2020年8月 49.1 49.0 51.7 58.3 53.2 47.1 46.0 58.6 2020年9月 50.8 50.4 53.6 58.5 52.5 48.4 46.1 58.7 2020年10月 51.0 50.8 53.1 58.8 53.2 44.9 47.2 59.3 2020年11月 51.5 50.9 53.7 62.6 56.5 45.7 46.7 60.1 2020年12月 51.3 50.4 53.2 68.0 58.9 46.2 47.1 59.8 2021年1月 50.2 49.8 52.0 67.1 57.2 49.0 47.3 57.9 2021年2月 48.8 49.6 51.6 66.7 58.5 48.0 46.1 59.2 2021年3月 51.2 51.1 53.1 69.4 59.8 46.7 46.6 58.5 2021年4月 50.4 50.6 51.7 66.9 57.3 46.8 46.4 58.3 2021年5月 48.3 50.9 51.9 72.8 60.6 46.5 45.9 58.2 2021年6月 48.1 49.7 51.7 61.2 51.4 47.1 46.6 57.9 2021年7月 47.7 49.4 50.8 62.9 53.8 47.6 46.1 57.8 2021年8月 46.7 48.3 50.3 61.3 53.4 47.7 45.9 57.5    二、中国非制造业采购经理指数运行情况   8月份,非制造业商务活动指数为47.5%,低于上月5.8个百分点,降至临界点以下,表明受近期多省多点疫情等因素影响,非制造业景气度明显回落。    分行业看,建筑业商务活动指数为60.5%,高于上月3.0个百分点。服务业商务活动指数为45.2%,低于上月7.3个百分点。从行业情况看,道路运输、航空运输、住宿、餐饮、文化体育娱乐等行业商务活动指数大幅降至临界点以下;批发、邮政、电信广播电视及卫星传输服务、货币金融服务、资本市场服务等行业商务活动指数高于临界点。     新订单指数为42.2%,比上月下降7.5个百分点,低于临界点,表明非制造业市场需求有所减弱。分行业看,建筑业新订单指数为51.4%,比上月上升1.4个百分点;服务业新订单指数为40.5%,比上月下降9.2个百分点。   投入品价格指数为51.3%,比上月回落2.2个百分点,高于临界点,表明非制造业企业用于经营活动的投入品价格与上月相比涨幅收窄。分行业看,建筑业投入品价格指数为55.3%,比上月回落1.8个百分点;服务业投入品价格指数为50.5%,比上月回落2.3个百分点。   销售价格指数为49.3%,比上月下降2.0个百分点,低于临界点,表明非制造业销售价格有所下降。分行业看,建筑业销售价格指数为53.6%,比上月上升0.7个百分点;服务业销售价格指数为48.5%,比上月下降2.6个百分点。   从业人员指数为47.0%,比上月下降1.2个百分点,表明非制造业企业用工景气度有所降低。分行业看,建筑业从业人员指数为50.6%,比上月回落1.5个百分点;服务业从业人员指数为46.4%,比上月下降1.1个百分点。   业务活动预期指数为57.4%,比上月回落3.3个百分点,仍位于较高景气区间,表明随着此轮疫情得到有效控制,非制造业企业对近期市场发展信心总体稳定。分行业看,建筑业业务活动预期指数为58.4%,比上月回落5.6个百分点;服务业业务活动预期指数为57.3%,比上月回落2.8个百分点。  表3  中国非制造业主要分类指数(经季节调整) 单位:%    商务活动 新订单 投入品 价格 销售价格 从业人员 业务活动 预期 2020年8月 55.2 52.3 51.9 50.1 48.3 62.1 2020年9月 55.9 54.0 50.6 48.9 49.1 63.0 2020年10月 56.2 53.0 50.9 49.4 49.4 62.9 2020年11月 56.4 52.8 52.7 51.0 48.9 61.2 2020年12月 55.7 51.9 54.3 52.3 48.7 60.6 2021年1月 52.4 48.7 54.5 51.4 47.8 55.1 2021年2月 51.4 48.9 54.7 50.1 48.4 64.0 2021年3月 56.3 55.9 56.2 52.2 49.7 63.7 2021年4月 54.9 51.5 54.9 51.2 48.7 63.0 2021年5月 55.2 52.2 57.7 52.8 48.9 62.9 2021年6月 53.5 49.6 53.4 51.4 48.0 60.8 2021年7月 53.3 49.7 53.5 51.3 48.2 60.7 2021年8月 47.5 42.2 51.3 49.3 47.0 57.4   表4  中国非制造业其他分类指数(经季节调整) 单位:%   新出口订单 在手订单 存货 供应商配送时间 2020年8月 45.1 44.6 48.5 52.4 2020年9月 49.1 46.3 48.5 52.2 2020年10月 47.0 44.9 48.7 52.3 2020年11月 49.0 45.2 48.8 51.8 2020年12月 47.5 44.7 47.0 51.2 2021年1月 48.0 44.0 47.4 49.8 2021年2月 45.7 44.0 45.9 49.8 2021年3月 50.3 45.9 48.2 51.8 2021年4月 48.1 45.8 47.2 50.9 2021年5月 47.6 44.7 47.2 50.8 2021年6月 45.4 43.8 47.0 51.0 2021年7月 47.7 44.8 47.3 51.3 2021年8月 43.9 42.9 45.9 49.2    三、中国综合PMI产出指数运行情况   8月份,综合PMI产出指数为48.9%,比上月回落3.5个百分点,表明我国企业生产经营活动较上月明显放缓。    附注   1.主要指标解释   采购经理指数(PMI),是通过对企业采购经理的月度调查结果统计汇总、编制而成的指数,它涵盖了企业采购、生产、流通等各个环节,包括制造业和非制造业领域,是国际上通用的监测宏观经济走势的先行性指数之一,具有较强的预测、预警作用。综合PMI产出指数是PMI指标体系中反映当期全行业(制造业和非制造业)产出变化情况的综合指数。PMI高于50%时,反映经济总体较上月扩张;低于50%,则反映经济总体较上月收缩。   2.调查范围   涉及《国民经济行业分类》(GB/T4754-2017)中制造业的31个行业大类,3000家调查样本;非制造业的43个行业大类,4200家调查样本。   3.调查方法   采购经理调查采用PPS(Probability Proportional to Size)抽样方法,以制造业或非制造业行业大类为层,行业样本量按其增加值占全部制造业或非制造业增加值的比重分配,层内样本使用与企业主营业务收入成比例的概率抽取。   本调查由国家统计局直属调查队具体组织实施,利用国家统计联网直报系统对企业采购经理进行月度问卷调查。   4.计算方法   (1)分类指数的计算方法。制造业采购经理调查指标体系包括生产、新订单、新出口订单、在手订单、产成品库存、采购量、进口、主要原材料购进价格、出厂价格、原材料库存、从业人员、供应商配送时间、生产经营活动预期等13个分类指数。非制造业采购经理调查指标体系包括商务活动、新订单、新出口订单、在手订单、存货、投入品价格、销售价格、从业人员、供应商配送时间、业务活动预期等10个分类指数。分类指数采用扩散指数计算方法,即正向回答的企业个数百分比加上回答不变的百分比的一半。由于非制造业没有合成指数,国际上通常用商务活动指数反映非制造业经济发展的总体变化情况。   (2)制造业PMI指数的计算方法。制造业PMI是由5个扩散指数(分类指数)加权计算而成。5个分类指数及其权数是依据其对经济的先行影响程度确定的。具体包括:新订单指数,权数为30%;生产指数,权数为25%;从业人员指数,权数为20%;供应商配送时间指数,权数为15%;原材料库存指数,权数为10%。其中,供应商配送时间指数为逆指数,在合成制造业PMI指数时进行反向运算。   (3)综合PMI产出指数的计算方法。综合PMI产出指数由制造业生产指数与非制造业商务活动指数加权求和而成,权数分别为制造业和非制造业占GDP的比重。   5.季节调整   采购经理调查是一项月度调查,受季节因素影响,数据波动较大。现发布的指数均为季节调整后的数据。 

快手因果推断与实验设计

[ 导读 ]理解和识别用户行为指标的相互关系是实验分析的目标。在社区氛围下,影响用户行为的因素更为复杂,关系识别更为困难,如何使用各种学科的方法,对社区进行宏观或微观的建模分析,系统性的评估各种策略的长期生态影响,是所要解决的重要问题。

本文金雅然博士将以快手直播的现实任务为例进行展开,介绍快手因果推断与实验设计的相关工作,主要内容包括:① 快手直播场景中遇到的因果推断问题及技术框架;② 基于观测数据或实验数据的因果推断技术案例;③ 涉及到网络效应的复杂实验设计。

01 快手直播场景中遇到的因果推断问题及技术框架

在快手我们主要会遇到这四类问题:用户激励设计、推荐策略评估、产品功能迭代以及预估产品和方向的长期价值。

%title插图%num

遇到这些问题我们通常有几种方式来解决:

  • 基于观测数据的因果推断,即从已有实验和非实验数据中提炼因果关系;
  • 在产品设计上构建正确的AB实验,合理计算指标,度量产品功能和迭代的影响;
  • 通过经济模型、机器学习算法和数据、实验的结合构造反事实推理来回答长期效应问题。

解决这些问题的核心是使用因果推断方法。

%title插图%num

因果推断的核心是在数据中存在关联关系的前提下,考虑数据之间的因果关系。任务是在给定的假设中,选择模型框架,将因果关系从关联中分割,对因果分析的大小作出正确的估计,并且通过统计推断的方法,验证推断的正确度,并回答推断结果存在多大波动。

在因果推断中,我们通常应用以下两种框架:

%title插图%num

Rubin虚拟事实模型(Potential Outcome)的核心是寻找合适的对照组。通常情况下,我们想要度量用户是否被实验影响到的两者差异是多少,而对于同一个用户,我们只能观测到被影响/不被影响一个状态,因此需要寻找合适的对照组,估计无法被观测到的影响。我们通常会构造一些识别实验,比如,经济学上通过RCT实验,互联网常使用AB实验,或者根据观测数据使用恰当的方法来寻找对照组。

%title插图%num

Pearl因果图模型(Causal Graph Model)使用有向图描述变量之间的因果关系。通过计算因果图中的条件分布,获得变量之间的因果关系。有向图指导我们使用这些条件分布来消除估计偏差,其核心也是估计检验分布、消除其他变量带来的偏差。

Pearl框架和Rubin框架有一些关联,简单图中,Potential Outcome模型中通过工具变量和匹配法消除估计偏差和Pearl的框架思想是一致的。

但是Pearl的框架可以处理多个变量之间相互作用的复杂关系。

%title插图%num

总结来说,Potential Outcome和Causal Graph是两种互补的推测虚拟事实的方法,目的都是为了计算存在混淆变量时,干预变量时对结果的影响,都需要对因果关系作假设,以及控制带来偏差的变量;不同点在于Rubin框架估计的因果效应主要是干预前后的期望差值,而Pearl框架下,我们估计的是干预前后的分布差异,Rubin框架解决的问题是因果效应的估计和统计推断,Pearl框架更偏向于因果关系的识别。

%title插图%num

从这两种框架延伸,在不同情景下,快手会使用不同工具解决实际问题,AB实验帮助我们观测策略或产品变动影响,在一些不能做实验或者多个实验相结合的场景下,会有一些其他的方法,下面会对具体方法展开介绍。

02 基于观测数据或实验数据的因果推断技术案例

1. 产品功能的评估:DID及其拓展案例

%title插图%num

双重差分适用于存在不可观测的个体固定效应场景,通过差分消除固定效应,其关键假设是,政策干扰前存在平行趋势,且实验干扰效应不随时间变化。双重差分可以用来消除那些对后期可能存在干扰因素,得到实验效果估计。

%title插图%num

%title插图%num

双重差分假设用户开始受影响的时间是一样的,实验处理效应对用户的影响是一样的,而这些假设难以满足。比如穿云箭红包,当实验效果上线后,用户的行为会发生变化,且不同用户的行为是不一致的,当不同表现用户都在实验组,传统的DID模型估计实验效应会产生偏差。因此我们在DID方法上进行修正,按照用户的状态是否更改分为不同类型,对不同类型用户分别做DID估计,再进行加权平均,得到修正后DID实验效果值。

%title插图%num

当treatment施加到一个群体或者地区上时,很难找到单一的对照组,这种时候采用合成控制方法构造虚拟对照组进行比较,原理是构造一个虚拟的对照组,通过treatment前的数据上学习的权重,拟合实验组在实验开始前的数据,模拟实验组用户在没有接受实验情况下的结果,构造合成控制组,实验开始后,评估实验组和合成控制组之间的差异。

2. 推荐策略的评估:因果推断与机器学习

%title插图%num

因果分析与机器学习存在一些差异点。因果分析的语言,核心在于因果关系的识别,即合理的估计处理前和处理后现有条件期望的差异,也可以是一种处理缺失数据的问题,在因果推断上我们非常关心的是如何准确的估计结果以及结果的方差。而在机器学习中,我们使用准确度来衡量机器学习模型的好坏,其目标是在训练集上估计一个条件期望,使得测试集上MSE*小。机器学习可以通过cross-validation(模型参数)的方法去数据驱动的选择一个*佳模型形式,与传统计量经济学方法相比不需要复杂的假设,例如function form的假设,从这种意义上机器学习能够更准确的预测。

%title插图%num

但是在因果推断问题上,机器学习的局限性在于,无论用什么机器学习方法,因果识别的条件都不能被放松;同时在机器学习模型通常使用的正则化和过拟合处理,会带来有偏估计,因此我们需要消除这种估计的偏差;在统计推断上,机器学习的局限性在于,有些模型不能直接计算方差,并且有时即使可以计算,方差的收敛速度也未必能够达到预期,所以针对这些问题,下面介绍了几种方法。

  • ① 双重机器学习模型

%title插图%num

很多时候因果推断会遇到混淆变量的问题,比如想要去分析直播推荐多样性对用户活跃度的影响,但是这些都和用户历史相关。传统计量经济学方法可以解决这个问题,但是依赖很多强假设,强假设下,得到的估计不一定合理,双重机器学习为这个问题提供了解决的思路。

双重机器学习假设所有混淆变量都可以被观测,其正则化过程能够达到高维变量选择的目的,与Frisch-Waugh-Lovell定理相似,模型通过正交化解决正则化带来的偏差。

除了上面所描述的,还有一些问题待解决,比如在ML模型下存在偏差和估计有效性的问题,这个时候可以通过Sample Splitting 和 Cross Fitting的方式来解决,具体做法是我们把数据分成一个训练集和估计集,在训练集上我们分别使用机器学习来拟合影响,在估计集上我们根据拟合得到的函数来做残差的估计,通过这种方法,可以对偏差进行修正。在偏差修正的基础上,我们可以对整个估计方法去构造一个moment condition,得到置信区间的推断,从而得到一个有良好统计的估计。

  • ② 因果随机森林模型

%title插图%num

我们通常探究策略对于不同用户异质性的影响,即哪些用户更容易被影响以及影响有多大,传统做法是多维分析,但是效率低,容易犯错。这时可以结合机器学习的方法,这里选择了决策树方法,因为决策树的分桶特性能够帮助解决异质性问题,相对于传统方法因果树做了两点改动:

  • 把数据分成训练集和估计集,一部分训练集去构造树,另一部分估计集去估计因果效应和方差;
  • 在树的分区方式上,使用各个节点的方差对目标函数加以修正。

通常情况下,我们结合实验来做分析。比如在实验中,通过因果树得到因果效应的分布,然后挑选出来那些实验效果显著的用户,去分析他们的特征,以及找到敏感用户,帮助我们了解策略的影响,作出下一步迭代。

  • ③ Meta-Learner for Uplift Modeling

%title插图%num

Uplift-modeling是另一种定位敏感人群的方法,和因果树的步骤有差别。核心是利用实验数据对实验结果变量建模,利用得到的模型估计条件平均处理效果。Uplift-modeling具有不同的学习方式,主要有S-Learner 、T-Learner和X-learner。和因果树相比,Meta-Learner是一种间接建模方式,实现快但一些场景下误差较大。

3. 用户行为链路的研究:因果图

%title插图%num

我们通常通过因果图来进行用户行为链路的研究。Rubin流派常用来估计变量之间的一度关系,但当我们面对一些未知问题时,我们想了解的是有哪些变量真正影响我们关心的结果变量,以及变量之间的相互影响和用户行为链路是什么,有效过程指标是什么,这些时候我们用到因果图的方法。

在生成因果图中,常遇到的限制是算法层面的,比如我们在优化目标函数的时候,需要遍历所有的因果图,是一个NP-hard问题,我们需要有效的算法得到想要的估计,市面上的算法大概分为两类:

  • Constraint-based Algorithms
  • Score-based Algorithms

03 复杂实验设计

%title插图%num

在实验设计上我们通常遇到的难点是网络效应的检测和应对,在直播下,网络效应有好多种表现方式,在这种网络效应存在的情况下尝试了一些方式,比如说:双边实验、时间片轮转实验、合成控制方法。

1. 双边实验设计

%title插图%num

在双边实验中,同时进行了主播侧和观众侧的分流,主播侧一部分是上了挂件,观众侧一部分能看到一部分看不到,双边实验的优点是可以同时检测两端的效果,同时可以帮助检测到组间的转移和溢出。在了解到组间溢出和干扰下,通过双边实验我们可以更加准确的测算处理效应,在挂件场景下,我们认为N3是代表完全没有处理过的效果,Y代表处理后的结果,N3和Y进行差分,计算产品功能推全后的影响,而且,双边实验能够更好的帮助我们归因。

%title插图%num

然而双边实验只能描述简单的组间溢出,在个体和个体之间存在干扰的复杂情况下,双边实验是无法帮助我们判断实验效果,例如直播PK暴击时刻这种情况下,我们通过时间片轮转实验解决,即在一定实验对象上进行实验组策略和对照组策略的反复切换。

2. 时间片轮转实验

%title插图%num

时间片轮转的核心在于:

  • 时间片的选择
  • 实验总周期选择
  • 随机切换时间点是什么样子的

当时间粒度约粗糙,时间上的干扰造成的偏差会越小,但是方差会越大,影响实验的检验效果,针对这个问题,采取的方案是*优设计。

%title插图%num

*优设计的核心假设是:

  • Outcome有一个*对上界
  • 用户无法知晓下一个时间是否是实验组
  • 如果时间片之间存在干扰,干扰的影响是固定且有限的

腾讯AI Lab、港中文杰出论文:用单语记忆实现高性能NMT

自然语言处理(NLP)领域顶级会议 ACL 2021 于 8 月 2 日至 5 日在线上举行。据官方数据, 本届 ACL 共收到 3350 篇论文投稿,其中主会论文录用率为 21.3%。腾讯 AI Lab 共入选 27 篇论文(含 9 篇 findings)。

在不久之前公布的获*论文中,腾讯 AI Lab 与香港中文大学合作完成的《Neural Machine Translation with Monolingual Translation Memory》获得杰出论文。本文作者也受邀参与机器之心举办的 ACL 2021 论文分享会,感兴趣的同学可以点击阅读原文查看回顾视频。

下面我们来看一下这篇论文的具体内容。

%title插图%num

论文地址:

https://arxiv.org/abs/2105.11269

先前的一些工作已经证明翻译记忆库(TM)可以提高神经机器翻译 (NMT) 的性能。与使用双语语料库作为 TM 并采用源端相似性搜索进行记忆检索的现有工作相比,该研究提出了一种新框架,该框架使用单语记忆并以跨语言方式执行可学习的记忆检索。该框架具有一些独特的优势:

  • 首先,跨语言记忆检索器允许大量的单语数据作为 TM;
  • 其次,记忆检索器和 NMT 模型可以联合优化以达到*终的翻译目标。

实验表明,该研究提出的方法获得了实质性的改进。值得注意的是,即使不使用额外单语数据,这种方法也要优于使用双语TM的 「TM-augmented NMT」基线方法。由于能够利用单语数据,该研究还证明了所提模型在低资源和领域适应场景中的有效性。

方法

该研究首先将翻译任务转化为两步过程:检索和生成,并在论文中描述了跨语言记忆检索模型和记忆增强型(memory-augmented)翻译模型的模型设计。*后,该论文展示了如何使用标准*大似然训练联合优化这两个组件,并通过交叉对齐预训练解决了冷启动(cold-start)问题。

%title插图%num

该方法的整体框架如图 1 所示,其中 TM 是目标语言%title插图%num中句子的集合。给定源语言中的输入 x,检索模型首先会根据相关函数%title插图%num,选择一些来自 Z 的可能有用的句子%title插图%num,其中%title插图%num。然后,翻译模型以检索到的集合%title插图%num和原始输入 x 为条件,使用概率模型%title插图%num来生成输出 y。

值得注意的是,相关性分数%title插图%num也是翻译模型输入的一部分,它能够鼓励翻译模型更多地关注更相关的句子。在训练期间,该研究借助翻译参考的*大似然改进了翻译模型和检索模型。

检索模型

检索模型负责从大型单语 TM 中为源语句选出*相关的语句。这可能涉及测量源语句和数百万个候选目标语句之间的相关性分数,带来了严重的计算挑战。为了解决这个问题,该研究使用一个简单的双编码器框架(Bromley 等, 1993)来实现检索模型,这样*相关句子选择可以利用*大内积搜索实现(MIPS, Maximum Inner Product Search)。借助高性能数据结构和搜索算法(例如 Shrivastava 和 Li,2014;Malkov 和 Yashunin,2018),可以高效地进行检索。具体来说,该研究将源语句 x 和候选语句 z 之间的相关性分数 f(x, z) 定义为它们的密集向量表征的点积:

%title插图%num

翻译模型

给定一个源语句 x、相关 TM 的小型集合%title插图%num、相关性分数%title插图%num,翻译模型会定义一个如下形式的条件概率%title插图%num

该翻译模型建立在标准的编码器 – 解码器 NMT 模型上:(源)编码器将源语句 x 转换为密集向量表征,解码器以自回归方式生成输出序列 y。在每一个时间步(time step)t,解码器都会处理先前生成的序列%title插图%num和源编码器的输出,生成隐藏状态 h_t。然后隐藏状态 h_t 通过线性投影转换为 next-token 概率,接着会有一个 softmax 函数操作,即%title插图%num%title插图%num

为了容纳额外的记忆输入,该研究使用记忆编码器扩展了标准的编码器 – 解码器 NMT 框架,并允许使用从解码器到记忆编码器的交叉注意力机制。具体来说,记忆编码器对每个 TM 语句 z_i 单独进行编码,从而产生一组上下文 token 嵌入%title插图%num,其中 L_i 是 token 序列 z_i 的长度。研究者计算了所有 TM 语句的交叉注意力:

%title插图%num

为了使从翻译输出到检索模型的梯度流有效,该研究将注意力分数与相关性分数进行了偏置处理,重写了等式(1)如下所示:

%title插图%num

训练

该研究在负对数似然损失函数%title插图%num中使用随机梯度下降来优化模型参数 θ 和 φ,其中%title插图%num指参考翻译。

然而,如果检索模型从随机初始化开始,那么所有 top TM 语句 z_i 可能都与 x 无关(或无用)。这导致检索模型无法接收有意义的梯度并进行改进,翻译模型将学会完全忽略 TM 输入。为了避免这种冷启动问题,该研究提出了两个交叉对齐任务来热启动检索模型。

*个任务是句子级的交叉对齐。具体来说,该研究在每个训练 step 上对训练语料库采样 B 个源 – 目标对。设 X 和 Z 分别对应由 E_src 和 E_tgt 编码的源向量和目标向量的 (B×d) 矩阵。%title插图%num是一个相关性分数的 (B×B) 矩阵 ,其中每一行对应一个源语句,每列对应一个目标语句。当 i = j 时,任何%title插图%num对都应该对齐。目标是*大化矩阵对角线上的分数,然后减小矩阵中其他元素的值。损失函数可以写成:

%title插图%num

第二个任务是 token 级交叉对齐,其目的是在给定源语句表征的情况下预测目标语言中的 token,反之亦然。该研究使用词袋损失:

%title插图%num

其中%title插图%num表示第 i 个源(目标)语句中的 token 集,token 概率由线性投影和 softmax 函数计算。

实验结果

该研究在三种设置下进行了实验:

(1)可用的 TM 仅限于双语训练语料库的常规设置;

(2)双语训练对很少,但用单语数据作为额外 TM 的低资源设置;

(3)基于单语 TM 的非参数域自适应设置。

常规设置

为了研究每个模型组件的效果,研究人员实现了一系列的模型变体(如表 2 中的 #1 – #5):

%title插图%num

如上表 2 所示,可以观察到:

(1)该研究使用异步索引刷新训练的完整模型(模型  #5),在四个翻译任务的测试集上获得了*佳性能,比 non-TM 基线(模型 #1)平均高出 3.26 个 BLEU 点,*高可达 3.86 个 BLEU 点( De⇒En)。这一结果证实了单语 TM 可以提高 NMT 的性能。

(2)端到端学习检索器模型是大幅提高性能的关键,使用预训练的固定跨语言检索器只能提供中等的测试性能,微调 E_src 和固定 E_tgt 显著提高了性能,同时微调 E_src 和 E_tgt 则能获得*强的性能(模型 #5 > 模型 # 4 > 模型 #3)。

(3)跨语言检索(模型 #4 和模型 #5)可以获得比源相似性搜索(模型 #2)更好的结果。

低资源设置

图 2 为在测试集上的主要结果,所有实验的一般模式都是一致的,由结果可得:TM 越大,模型的翻译性能越好。当使用所有可用的单语数据 (4/4) 时,翻译质量显著提高。未经重新训练的模型的性能与经过重新训练的模型的性能相当,甚至更好。此外,该研究还观察到,当训练对非常少时(只有 1/4 的双语对可用),小型 TM 甚至会影响模型的性能,这可能是出于过拟合的原因。该研究推测,根据不同的 TM 大小调整模型超参数将获得更好的结果。

%title插图%num

该研究还与反向翻译 (BT)进行了比较,这是一种将单语数据用于 NMT 的流行方法。该研究使用双语对训练目标到源的 Transformer Base 模型,并使用得到的模型翻译单语语句以获得额外的合成并行数据。如表 3 所示,该研究所用方法在 2/4 双语对上比 BT 表现得更好,但在 1/4 双语对上表现较差。 *令人惊喜的是,结果表明两种方法是互补的,他们的结合使翻译性能取得了进一步的巨大提升。

%title插图%num

非参数领域自适应

由下表 4 可得,当仅使用双语数据时,与 non-TM 基线相比,TM 增强模型在数据较少的域中获得更高的 BLEU 分数,但在其他域中的分数略低。然而,当研究者将 TM 切换到特定域的 TM 时,所有域的翻译质量都得到了显著提升,将 non-TM 基线平均提高了 1.85 个 BLEU 点,在 Law 上提高了 2.57 个 BLEU 点,在 Medical 上提高了 2.51 个 BLEU 点。

该研究还尝试将所有特定领域的 TM 合并成一个 TM,并将其用于所有域(如表 4 *后一行所示),但实验结果并没有获得明显的改进。这表明域外数据不能提供帮助,因此较小的域内 TM 就足够了。

%title插图%num

运行速度

FAISS in-GPU 索引能够让搜索数百万个向量变得非常高效(通常在几十毫秒内完成)。在该研究中,记忆搜索的执行速度甚至比原生的 BM25 还要快。对于表 2 中的结果,以普通的 Transformer Base 模型(模型 #1)为基线模型,该研究模型(包括模型 #4 和模型 #5)的推断延迟大约是基线的 1.36 倍(所有模型都使用一个 Nvidia V100 GPU)。

至于训练成本,模型 #4 和模型 #5 每个训练 step 的平均时间成本分别是基线的 2.62 倍和 2.76 倍,与传统的 TM-augmented 基线相当(模型 #2 是 2.59 倍)( 全部使用两个 Nvidia V100 GPU),实验结果如下表 5 所示。此外,该研究还观察到,就训练 step 而言,记忆增强型模型的收敛速度比普通模型快得多。

%title插图%num

原文链接:

https://mp.weixin.qq.com/s/sFGm8m5Sb-bZeJTop8i0hA

盘点来自工业界的GPU共享方案

进年来工业界一直孜孜不倦地寻求提升GPU利用率的方案,能被更多用户理解和使用的GPU共享走进工程师的视野中。本文将总结目前有公开PR的、来自工业界的部分GPU容器计算共享方案,看看工业界对GPU共享的定位和需求。本文将依旧着眼于unix-like os上计算容器场景的资源隔离能力,不包括win os,VM,视频,游戏相关方案。受限于笔者能力,可能出现一些错漏,希望多多指正。

首先,回顾一下GPU共享方案的分类[21]。以下类型中,仅CUDA聚合为空分,其余为时分。%title插图%num

阿里 cGPU

来自阿里的cGPU(container GPU)[1]是*早提出的通过内核劫持来实现容器级GPU共享的方案。cGPU实现了一个内核模块cgpu_km,该模块可以对一个物理GPU虚拟出16个虚拟GPU设备。在容器挂载设备时,修改后的container runtime将挂载虚拟GPU设备,而不是真实GPU设备。通过这种方式实现了GPU劫持。当用户程序的请求下发至内核模块cgpu_km时,模块通过修改请求及回复来限制GPU显存资源。同时,内核模块也实现了简单的算力调度,通过限制每个容器可下发kernel的时间片来隔离算力资源。可以提供公平/抢占/权重三种算力分配模式。值得注意的是,cGPU目前不能中止已经发送到GPU上的请求,因此如追求算力隔离,需要延长时间片的长度,会造成一定的算力浪费。出于某些考虑未有开源。%title插图%num既然是容器级的GPU共享,接入到K8s的组件是必不可少的。阿里开源了相应的device plugin[3]和调度器[2]。设计的device plugin提供的核心资源是显存,这和cGPU是一脉相承的。另外由于当前K8s支持的资源类型是一维的,而GPU共享资源是二维的。为了实现调度能力,应用了一些tricky 的技巧,也让device plugin不得不和APIServer直接通信。

腾讯 GaiaGPU

腾讯提供了一整套GPU共享解决方案GaiaGPU[4],是完全开源的GPU共享方案,salute。GaiaGPU中的vCUDA(virtual CUDA)[5]是GPU资源限制组件,属于CUDA劫持。vCUDA通过劫持CUDA的显存申请和释放请求,为每个容器管理它的显存使用量,进而实现了显存隔离。唯一需要注意的是申请context并不通过malloc函数,因此无法知道进程在context使用了多少显存。因此vcuda每次都去向GPU查询当前的显存使用量。在算力隔离方面,使用者可以指定容器的GPU利用率。vCUDA将会监控利用率,并在超出限制利用率时做一些处理。此处可以支持硬隔离和软隔离。两者的不同点是,如果有资源空闲,软隔离允许任务超过设置,而硬隔离不允许。由于使用的是监控调节[22]的方案,因此无法在短时间内限制算力,只能保证长时间的效率公平。所以不适合推理等任务时间*短的场景。%title插图%numGaiaGPU也提供了Device plugin GPU manager[6]和调度器 GPU admission[7],GPU admission既允许用户申请一张虚拟卡,也允许用户像之前一样申请一机多卡,这可能可以满足一些小型集群的需要。GPU manager除实现了device plugin该实现的,也做了很多繁杂的功能,使得apiserver的负担更重了。

腾讯 qGPU

腾讯在内核劫持类GPU共享方向上,也推出了资源隔离方案qGPU(qos GPU)[8]。从架构图中就可以看出,qGPU和同属于内核劫持方案的cGPU类似。但值得注意的是,qGPU效仿Nvidia vGPU在必要时context switch,实现了强算力隔离,这也是其名字的由来。出于某些考虑未有开源。

%title插图%num

百度 MPS+CUDA Hook的GPU隔离方案

百度推出的GPU共享方案[9]也是一个CUDA劫持方案,通过经典CUDA劫持限制显存,在算力隔离方面使用了MPS。没有开源代码。MPS在限制算力方面,除了众所周知的错误影响问题,其实算力限制并不严格,且无法根据GPU状态灵活调节算力的限制。期待下一代方案。

%title插图%num

在K8s接入部分,也实现了Device plugin和调度器extender,不过未开源。

%title插图%num

爱奇艺 vGPU

爱奇艺的GPU共享方案也叫vGPU(和Nvidia的虚拟机方案vGPU重名)[10],也是CUDA劫持方案。在显存隔离上也是使用了经典的CUDA函数劫持的方法,由于没有开源代码因此不清楚context问题是如何解决的。在算力隔离方面比较特别,和RTA2019的Fractional GPU[11][12]思想颇为近似,通过将kernel限制在某些SM上来限制使用部分算力。但这实质上是一种空分的方法,需要将上下文合并才可以实现共享GPU,因此也会有错误传播的问题,场景限制颇大。

%title插图%num

在K8s接入部分,使用和阿里同样的方案。

第四范式 OpenAIOS vGPU

第四范式的GPU共享方案还叫vGPU[13],也是CUDA劫持方案。由于没有开源资源隔离部分的代码,从文档中推测,其实现和GaiaGPU的vcuda较为类似:显存隔离使用的是经典CUDA劫持方法,通过预估获得context大小;使用监控隔离的方案隔离算力。同样地,方案的优缺点也和vCUDA类似。较为特别的一点是,和阿里Antman[18]相同地,第四范式vGPU通过Nvidia UVM实现了虚拟显存。不过UVM实质上是使用内存来虚拟显存,因此会消耗较大的内存,且性能会有较大下降。若要使用虚拟显存功能,还需思考程序本身占用的内存和虚拟显存的trade off。第四范式开源了device plugin[14],使用了和nvidia device plugin中处理MIG设备一样的思路,将节点上所有虚拟GPU设备设定为同一大小。这丧失了一定的用户自由,但对大型集群来说,这样做更通用且更容易维护。同时,采用这种方案不需重新设计调度器。

AWS aws-virtual-gpu

AWS提供了一套非常简单的GPU共享方案[15],该方案通过tensorflow框架的参数per_process_gpu_memory_fraction实现了显存隔离,通过MPS的CUDA_MPS_ACTIVE_THREAD_PERCENTAGE实现了算力的限制。方案受限于tf框架,且使用了MPS,显然是个玩具之作。在接入K8s方面,AWS开源的device-plugin[16]没有考虑资源的二维关系,实现了非常简化的资源allocate。

%title插图%num

趋动科技 OrionX

趋动科技在AI算力资源池化解决方案OrionX中实现了GPU共享的能力[17]。在资源隔离方面,使用了CUDA劫持的方案,通过MPS以及其他方式限制算力。OrionX中也包含定制的device plugin和调度器方案,亦无开源。另,OrionX属于GPU池化类解决方案,GPU资源隔离仅为OrionX的部分能力,详细请参见评论区。

%title插图%num

%title插图%num总结%title插图%num

通过列举上述方案,可以看出各大公司主要还是处于试验期,应用尚不成熟。在设计上,倾向于对用户更易使用的,更通用的方案,而非考虑计算任务特性进而定制适合的方案。对规模很大的云场景,面向更多类型和水平的用户,如此设计是必行之举。对于GPU共享,一些资深工程师亦有深刻意见[19][20],讨论分析了在不同场景下技术的适用性问题。也推荐读者兼听则明。注:本文仅代表笔者的个人观点,有表述错误的可能性,请读者仅作参考。且本文不代表笔者所处的任何机构的观点。

友情链接: SITEMAP | 旋风加速器官网 | 旋风软件中心 | textarea | 黑洞加速器 | jiaohess | 老王加速器 | 烧饼哥加速器 | 小蓝鸟 | tiktok加速器 | 旋风加速度器 | 旋风加速 | quickq加速器 | 飞驰加速器 | 飞鸟加速器 | 狗急加速器 | hammer加速器 | trafficace | 原子加速器 | 葫芦加速器 | 麦旋风 | 油管加速器 | anycastly | INS加速器 | INS加速器免费版 | 免费vqn加速外网 | 旋风加速器 | 快橙加速器 | 啊哈加速器 | 迷雾通 | 优途加速器 | 海外播 | 坚果加速器 | 海外vqn加速 | 蘑菇加速器 | 毛豆加速器 | 接码平台 | 接码S | 西柚加速器 | 快柠檬加速器 | 黑洞加速 | falemon | 快橙加速器 | anycast加速器 | ibaidu | moneytreeblog | 坚果加速器 | 派币加速器 | 飞鸟加速器 | 毛豆APP | PIKPAK | 安卓vqn免费 | 一元机场加速器 | 一元机场 | 老王加速器 | 黑洞加速器 | 白石山 | 小牛加速器 | 黑洞加速 | 迷雾通官网 | 迷雾通 | 迷雾通加速器 | 十大免费加速神器 | 猎豹加速器 | 蚂蚁加速器 | 坚果加速器 | 黑洞加速 | 银河加速器 | 猎豹加速器 | 海鸥加速器 | 芒果加速器 | 小牛加速器 | 极光加速器 | 黑洞加速 | movabletype中文网 | 猎豹加速器官网 | 烧饼哥加速器官网 | 旋风加速器度器 | 哔咔漫画 | PicACG | 雷霆加速