Mobile Development 21 min read

Analyzing and Optimizing Retrofit Performance in Android Applications

The article uncovers a lock‑induced latency in Retrofit’s service‑method cache during cold‑start, demonstrates how moving the synchronization to a class‑level lock and customizing Retrofit.create eliminates the bottleneck, and then shows how dynamic‑proxy hooks combined with Kotlin coroutines can add transparent caching and low‑intrusion BFF aggregation without altering the library.

NetEase Cloud Music Tech Team
NetEase Cloud Music Tech Team
NetEase Cloud Music Tech Team
Analyzing and Optimizing Retrofit Performance in Android Applications

Retrofit is an open‑source networking library from Square that is widely used in Android development. This article starts from a simple cold‑start performance optimization and digs into Retrofit’s implementation details, then explores further usages.

The performance problem was discovered during a cold‑start optimization of an app. By analyzing the main‑thread cost and moving heavy work to an IO thread, the home‑page API request still showed a latency higher than the average, causing a slow first‑screen data load. Using systrace the trace revealed a long waiting period for a lock rather than actual network I/O.

// retrofit2/Retrofit.java
public <T> T create(final Class<T> service) {
validateServiceInterface(service);
return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
new InvocationHandler() {
@Override public @Nullable Object invoke(Object proxy, Method method,
@Nullable Object[] args) throws Throwable {
...
return loadServiceMethod(method).invoke(args != null ? args : emptyArgs);
}
});
}
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;
}

The lock is taken on serviceMethodCache while a ServiceMethod instance is created. Because the creation of ServiceMethod involves generating Moshi JsonAdapter objects, which recursively reflect over Kotlin data classes, the lock becomes a bottleneck during cold‑start, especially under concurrent requests.

To optimize, the article proposes moving the expensive part out of the lock. One approach is to replace the default Retrofit.create implementation with a custom version that obtains the cache via reflection and synchronizes on the service class instead of the cache map.

private ServiceMethod<?> loadServiceMethod(Method method) {
// 反射取到Retrofit内部的缓存
Map<Method, ServiceMethod<?>> serviceMethodCache = null;
try {
serviceMethodCache = cacheField != null ? (Map<Method, ServiceMethod<?>>) cacheField.get(retrofit) : null;
} catch (IllegalAccessException e) {
e.printStackTrace();
}
if (serviceMethodCache == null) {
return retrofit.loadServiceMethod(method);
}
ServiceMethod<?> result = serviceMethodCache.get(method);
if (result != null) return result;
synchronized (serviceMethodCache) {
result = serviceMethodCache.get(method);
if (result != null) return result;
}
synchronized (service) { // 这里替换成类锁
result = ServiceMethod.parseAnnotations(retrofit, method);
}
synchronized (serviceMethodCache) {
serviceMethodCache.put(method, result);
}
return result;
}

After applying this change, the systrace no longer shows lock‑waiting time and the startup request latency returns to normal.

The article then explores a higher‑level use case: intercepting every Retrofit request at the dynamic‑proxy layer to implement custom caching and even Backend‑for‑Frontend (BFF) aggregation without modifying Retrofit’s source.

For traditional Call based APIs, a wrapper callback can capture the response meta‑object:

class WrapperCallback<T>(private val cb : Callback<T>) : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
val result = response.body() // 这里response.body()就是返回的meta
cb.onResponse(call, response)
}
}

For suspend functions, Retrofit uses SuspendForBody or SuspendForResponse which ultimately call KotlinExtensions.await . The await implementation suspends the coroutine by returning COROUTINE_SUSPENDED and resumes it in the OkHttp callback.

suspend fun <T : Any> Call<T>.await(): T {
return suspendCancellableCoroutine { continuation ->
continuation.invokeOnCancellation { cancel() }
enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
if (response.isSuccessful) {
val body = response.body()
if (body == null) { … continuation.resumeWithException(e) }
else { continuation.resume(body) }
} else { continuation.resumeWithException(HttpException(response)) }
}
override fun onFailure(call: Call<T>, t: Throwable) { continuation.resumeWithException(t) }
})
}
}

By replacing the Continuation with a custom implementation, the returned meta‑object can be intercepted, cached, or transformed.

@Nullable
public T hookSuspend(Method method, Object[] args) {
Continuation<T> realContinuation = (Continuation<T>) args[args.length - 1];
Continuation<T> hookedContinuation = new Continuation<T>() {
@NonNull
@Override
public CoroutineContext getContext() { return realContinuation.getContext(); }
@Override
public void resumeWith(@NonNull Object o) { realContinuation.resumeWith(o); }
};
args[args.length - 1] = hookedContinuation;
return method.invoke(args);
}

Using this hook, a lightweight in‑memory cache can be built with a simple Map keyed by the method signature. The article defines LoadInfo , CacheWriter , and CacheReader classes to control cache write/read behavior, and shows how to annotate Retrofit interfaces with @Tag to pass cache configuration without affecting the actual network request.

sealed class LoadInfo(
val id: String = "", // 请求id,默认不需要设置
val timeout: Long // 超时时间
)
class CacheWriter(
id: String = "",
timeout: Long = 10000
) : LoadInfo(id, timeout)
class CacheReader(
id: String = "",
timeout: Long = 10000,
val asCache: Boolean = false
) : LoadInfo(id, timeout)

Finally, the article sketches a strategy for implementing a Backend‑for‑Frontend (BFF) layer using the same dynamic‑proxy hook. By annotating Retrofit methods with a custom @BFF annotation, generating glue code at compile time, and swapping the original calls with a combined BFF request, developers can achieve zero‑intrusion aggregation of multiple API calls.

@Retention(RetentionPolicy.CLASS)
@Target({ElementType.FIELD, ElementType.METHOD})
public @interface BFF {
String source() default "";
boolean primary() default false;
}

The overall workflow demonstrates how deep understanding of Retrofit’s internals, combined with Kotlin coroutines and Java reflection, enables powerful performance tuning, caching, and architectural patterns without altering the library itself.

In summary, the article walks through discovering a lock‑related performance issue in Retrofit, optimizing the lock usage, extending Retrofit with custom dynamic‑proxy hooks for caching, and outlining a low‑intrusion BFF implementation, providing valuable insights for Android developers seeking to improve app startup time and network efficiency.

performanceAndroidcachingKotlinBFFcoroutinesRetrofit
NetEase Cloud Music Tech Team
Written by

NetEase Cloud Music Tech Team

Official account of NetEase Cloud Music Tech Team

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.