Retrofit中的反射耗时,罪魁祸首并不在动态代理,而在反射注解

文章详细探讨了Retrofit框架为何使用反射以及动态代理来处理注解,解释了如何通过缓存ServiceMethod来优化性能,强调了解析注解和创建请求模板的开销,并指出缓存策略能有效减少重复反射操作的影响。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

众所周知反射对性能会有损耗,反射的好,在于可以在运行期间调用对象的任何方法,访问它的任何属性,但随之而来的就是性能损失,同时丧失了编译时类型检查的好处,执行反射访问所需要的代码也非常笨拙且冗长。

用到反射的框架,会想办法减免重复的反射工作,从而在程序预热之后,降低反射带来的性能影响。例如网络请求框架Retrofit对反射的注解、跨进程通信反射调用方法对反射到的类实例进行了缓存。如题,今天针对Retrofit的反射注解部分对代码的优化逻辑进行分析。

Retrofit为什么会用到反射呢?在使用Retrofit的时候,我们通常会写一个Service接口来定义网络请求方法,类似如下:

public interface IDemoService{
    /**
     * baseurl/user
     */
    @GET("user")
    Call<ResponseBody> getData0();
    
    @POST("user/emails")
    @FormUrlEncoded
    Call<ResponseBody> getPostData1(@Field("email")String userEmail);
    
    /**
     * 指定请求路径,路径由参数形式传入
     * baseurl/valueOfParamUrl?id=1
     */
    @GET
    Call<ResponseBody> getUrlData(@Url String url, @Query("id") long id);
    
    /**
     * :@Multipart 表示请求实体是一个支持文件上传的表单,需要配合 @Part和@PartMap使用
     * :@Part 用于表单字段,适用于文件上传类型,@Part支持:RequestBody、MultipartBody.Part、任意类型
     * :@PartMap 用于不确定个数文件上传 主要修饰 Map<String,Object>,Object为上述支持的类型
     * @return
     */
    @Multipart
    @POST("user/followers")
    Call<ResponseBody> getPartData(@Part("name") RequestBody name, @Part MultipartBody.Part file);
}

看到这种设计:将一些配置参数放在注解中。后续如果要使用这些注解中的值,必然需要反射注解。Retrofit通过注解来减少开发者的冗余重复代码量,但同时也引入了反射。

定义好这个网络请求接口之后,就可以使用这个服务的代理进行网络请求:

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://localhost/")//注意要以/结尾
    .addConverterFactory(GsonConverterFactory.create())
    .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
    .build();
//获得该Service的代理
IDemoService userServiceProxy = retrofit.create(IDemoService.class);
//调用该Service的方法,可以获得一个Retrofit2.Call<>对象,用于发起网络请求
Call<ResponseBody> executorCallbackCall = userServiceProxy.getData0();
executorCallbackCall.enqueue(new Callback<ResponseBody>() {
    //返回到这里,默认回调到主线程!
    @Override
    public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {

    }

    @Override
    public void onFailure(Call<ResponseBody> call, Throwable t) {

    }
});

这么设计的好处要和OkHttp3的网络请求进行对比:

  • 将请求的配置逻辑解耦出来,业务层只需要关注传输的数据。
  • 固定模板,使得请求拼接的代码可以复用

一方面让代码更加简洁,让类之间的分工更加精确,易维护易使用。易使用也是因为Retrofit使用了外观设计模式,为OkHttp3的核心功能设计了新的统一的调用方式。框架总是双面剑,开发效率提高了,代码简洁了,性能就相应地打了折扣。Retrofit使用注解的和动态代理的方式,通过Service模板拼接生成对象。

Retrofit使用动态代理生成代理

Retrofit生成代理的方式在文章 Android框架源码分析——从设计模式角度看 Retrofit 核心源码 中说的有点不确切。Retrofit使用的动态代理仍然是标准的动态代理设计模式,需要通过接口生成一个代理类。只不过调用方法的时候,并不是让哪个实体类(委托者)去执行,而是通过解析这个方法上的注解信息,以此生成一个ServiceMethod,再紧接着调用该ServiceMethod对象的 invoke() 方法,生成一个 Retrofit2.Call<> 对象返回出去。先来看到对Service接口的动态代理:

IDemoService userServiceProxy = retrofit.create(IDemoService.class);
Call<ResponseBody> executorCallbackCall = userServiceProxy.getData0("username");

这里可能有点绕,因为这和我们常见的动态代理模式不太一样,我们见到的动态代理通常是代理类执行一个方法后,将执行结果返回。但Retrofit的设计则是返回一个Retrofit2.Call<>对象。这也是因为开发者写的Service接口的作用是提供请求体的配置信息,并不是请求体本身,Retrofit通过动态代理,用户执行代理类的该方法时,其实内部是去解析该方法,将传入参数拼接到请求体,并将解析后生成的 Retrofit2.Call<> 返回给开发者,用于发起实际的请求。

简而言之,Retrofit的代理类的方法不是直接发起请求,而是通过解析方法上的注解和传入参数,生成一个可以用于请求的对象。

Retrofit通过create()方法,将Service接口生成一个代理类:

public <T> T create(final Class<T> service) {
    validateServiceInterface(service);
    return (T)
        //通过动态代理生成代理类
        Proxy.newProxyInstance(
        //第一个参数为类加载器,一般直接使用委托者或者接口类的类加载器
        service.getClassLoader(),
        //需要生成代理的接口类,根据接口类定义的方法生成代理
        new Class<?>[] {service},
        //代理类调用方法的时候,会走到InvocationHandler的invoke()方法,执行真正的逻辑
        new InvocationHandler() {
            private final Platform platform = Platform.get();
            private final Object[] emptyArgs = new Object[0];

            @Override
            public @Nullable Object invoke(Object proxy, Method method, @Nullable Object[] args)
                throws Throwable {
                if (method.getDeclaringClass() == Object.class) {
                    return method.invoke(this, args);
                }
                args = args != null ? args : emptyArgs;
                //如果这个方法在接口类中被default修饰,就直接调用其本身逻辑,否则就认为是需要实现的接口方法,使用invoke进行方法调用。
                return platform.isDefaultMethod(method)
                    ? platform.invokeDefaultMethod(method, service, proxy, args)
                    : loadServiceMethod(method).invoke(args);
            }
        });
}

由Proxy.newProxyInstance()需要传入三个参数,第一个就是类的加载器,一般直接使用委托者或者接口类的加载器,第二个是需要代理的接口类,根据接口定义的方法生成代理。最后一个参数是InvocationHandler,生成的代理类中方法的调用都会执行到 invocationHandler.invoke()方法来执行具体的逻辑,代理类中不作任何处理。既然用了动态代理,那么method.invoke()的反射耗时就无法避免了,那么Retrofit对性能可以在哪里下文章呢?先来看到这里的loadServiceMethod() 这个方法进行了Service方法的解析,其中就包括了反射各种注解,这一步是非常耗时的!

//Retrofit
ServiceMethod<?> loadServiceMethod(Method method) {
    ServiceMethod<?> result = serviceMethodCache.get(method);
    if (result != null) return result;

    synchronized (serviceMethodCache) {
        result = serviceMethodCache.get(method);
        if (result == null) {
            //解析注解!非常耗时!
            result = ServiceMethod.parseAnnotations(this, method);
            //解决办法:解析之后放到缓存,下次不用再解析
            serviceMethodCache.put(method, result);
        }
    }
    return result;
}

之所以说解析注解耗时,就是因为它通过反射将方法上的各种注解全都解析一遍。看到ServiceMethod.parseAnnotations()方法:

//ServiceMethod.java
static <T> ServiceMethod<T> parseAnnotations(Retrofit retrofit, Method method) {
    //解析方法上的注解,生成一个RequestFactory
    RequestFactory requestFactory = RequestFactory.parseAnnotations(retrofit, method);
	//反射获取返回值类型,约定返回值不能为void
    Type returnType = method.getGenericReturnType();
    //检查注解,构造用于发起HTTP请求的对象,其invoke方法返回一个Retrofit2.Call<>对象,具体这个Call对象如何生成,由CallFactory工厂来决定。
    return HttpServiceMethod.parseAnnotations(retrofit, method, requestFactory);
}

RequestFactory.parseAnnotations()最终调用到RequestFactory.build()方法,其中反射了方法上的注解,生成了一个RequestFactory对象:

//RequestFactory
RequestFactory build() {
    //反射获取annotations
    this.methodAnnotations = method.getAnnotations();
    for (Annotation annotation : methodAnnotations) {
        parseMethodAnnotation(annotation);
    }
    //...一系列验证
    return new RequestFactory(this);
}

private void parseMethodAnnotation(Annotation annotation) {
    if (annotation instanceof DELETE) {
        parseHttpMethodAndPath("DELETE", ((DELETE) annotation).value(), false);
    } else if (annotation instanceof GET) {
        parseHttpMethodAndPath("GET", ((GET) annotation).value(), false);
   	//...
    } else if (annotation instanceof HTTP) {
        HTTP http = (HTTP) annotation;
        parseHttpMethodAndPath(http.method(), http.path(), http.hasBody());
    } else if (annotation instanceof retrofit2.http.Headers) {
        String[] headersToParse = ((retrofit2.http.Headers) annotation).value();
        }
        headers = parseHeaders(headersToParse);
    } else if (annotation instanceof Multipart) {
        isMultipart = true;
    } else if (annotation instanceof FormUrlEncoded) {
        isFormEncoded = true;
    }
}

不仅构建RequestFactory时反射耗时,HttpServiceMethod.parseAnnotations()中也是用到了反射。总而言之,如果我们每次调用一个Service中定义好的方法,都经过反射注解,拼接请求模板的过程,性能会很差。解决办法就是将这些解析之后的结果缓存起来,用于复用。Retrofit将生成的HttpServiceMethod对象(ServiceMethod的子类)放进了serviceMethodCache这个缓存map中:

public final class Retrofit{
    private final Map<Method, ServiceMethod<?>> serviceMethodCache = new ConcurrentHashMap<>();
    public <T> T create(final Class<T> service) {
        //...
    }
    ServiceMethod<?> loadServiceMethod(Method method) {
        ServiceMethod<?> result = serviceMethodCache.get(method);
        if (result != null) return result;
        synchronized (serviceMethodCache) {
            //先尝试从缓存中获取之前解析过的ServiceMethod
            result = serviceMethodCache.get(method);
            if (result == null) {
                result = ServiceMethod.parseAnnotations(this, method);
                //将生成的ServiceMethod缓存到map中
                serviceMethodCache.put(method, result);
            }
        }
        return result;
    }
}

这样的设计可以拓展到很多地方,如果我们会通过反射建立一个模板对象,这个对象可以被复用,例如Retrofit的这个ServiceMethod对象,它是一个请求的模板对象,下次再调用这个ServiceMethod代理的方法的时候,直接从缓存中(复用池中)复用这个ServiceMethod对象,减免每次都一样的解析注解的步骤。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值