彻底搞懂 Retrofit:使用、封装与 Converter 原理

版权归作者所有,如有转发,请注明文章出处:cyrus-studio.github.io/blog/

Retrofit 简介

Retrofit 是 Square 公司开发的一个 类型安全的 HTTP 网络请求库,广泛应用于 Android 开发中。它极大地简化了 REST API 的调用方式,让网络请求就像调用本地接口一样简单。

基于 OkHttp 实现底层通信,提供了:

  • 注解方式定义 API 接口

  • 自动将 JSON/XML 响应解析为 Java/Kotlin 对象

  • 支持多种 Converter(如 Gson、Moshi、Protobuf)

  • 支持协程、RxJava、LiveData 等异步处理方式

与 OkHttp 关系:

  • Retrofit 是高级封装,负责将请求/响应转换为 Java 对象

  • OkHttp 是底层网络传输库,处理连接、缓存、拦截器等

  • Retrofit 默认使用 OkHttp,可以无缝集成 OkHttp 的功能(如添加 Token 拦截器)

开源地址:github.com/square/retr...

集成 Retrofit

在 app 的 build.gradle.kts 添加如下依赖:

scss 复制代码
// Retrofit 核心库
implementation("com.squareup.retrofit2:retrofit:2.9.0")
// JSON 转换器(使用 Gson)
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
// OkHttp
implementation("com.squareup.okhttp3:okhttp:4.12.0")
// OkHttp 日志拦截器(可选,用于调试)
implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")

然后点击 "Sync Now" 进行 Gradle 同步。

编辑 AndroidManifest.xml,添加访问网络权限:

ini 复制代码
<uses-permission android:name="android.permission.INTERNET" />

使用实例

1. 定义数据类

kotlin 复制代码
package com.cyrus.example.retrofit.model

data class User(
    val id: Int,
    val name: String,
    val email: String
)

2. Retrofit 接口定义(API Interface)

使用注解描述请求类型、路径、参数等:

kotlin 复制代码
package com.cyrus.example.retrofit.network

import com.cyrus.example.retrofit.model.User
import retrofit2.http.GET
import retrofit2.http.Path

interface ApiService {
    @GET("users/{id}")
    suspend fun getUser(@Path("id") id: Int): User
}

3. Retrofit 实例构建

kotlin 复制代码
package com.cyrus.example.retrofit.network

import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

object RetrofitClient {
    private const val BASE_URL = "https://jsonplaceholder.typicode.com/"

    private val loggingInterceptor = HttpLoggingInterceptor().apply {
        level = HttpLoggingInterceptor.Level.BODY
    }

    private val client = OkHttpClient.Builder()
        .addInterceptor(loggingInterceptor)
        .build()

    val api: ApiService by lazy {
        Retrofit.Builder()
            .baseUrl(BASE_URL)
            .client(client)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
            .create(ApiService::class.java)
    }
}

4. 发起请求

ini 复制代码
CoroutineScope(Dispatchers.IO).launch {
    try {
        val user = RetrofitClient.api.getUser(1)
        resultText = "用户1: ${user.name} (${user.email})"
    } catch (e: Exception) {
        resultText = "请求失败: ${e.message}"
    }
}

Retrofit 会自动将请求结果 JSON 响应解析为 Kotlin 对象

日志输出如下:

yaml 复制代码
2025-07-17 23:01:50.355  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  --> GET https://jsonplaceholder.typicode.com/users/1
2025-07-17 23:01:50.355  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  --> END GET
2025-07-17 23:01:52.416  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  <-- 200 https://jsonplaceholder.typicode.com/users/1 (2060ms)
2025-07-17 23:01:52.417  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  date: Thu, 17 Jul 2025 15:01:52 GMT
2025-07-17 23:01:52.417  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  content-type: application/json; charset=utf-8
2025-07-17 23:01:52.417  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  access-control-allow-credentials: true
2025-07-17 23:01:52.417  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  cache-control: max-age=43200
2025-07-17 23:01:52.417  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  etag: W/"1fd-+2Y3G3w049iSZtw5t1mzSnunngE"
2025-07-17 23:01:52.417  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  expires: -1
2025-07-17 23:01:52.417  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  nel: {"report_to":"heroku-nel","response_headers":["Via"],"max_age":3600,"success_fraction":0.01,"failure_fraction":0.1}
2025-07-17 23:01:52.418  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  pragma: no-cache
2025-07-17 23:01:52.418  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  report-to: {"group":"heroku-nel","endpoints":[{"url":"https://nel.heroku.com/reports?s=YWQRu2P%2FvZPdRO%2BFKUJHT1Kv87wV6qY2%2BEllROkIyHs%3D\u0026sid=e11707d5-02a7-43ef-b45e-2cf4d2036f7d\u0026ts=1751617599"}],"max_age":3600}
2025-07-17 23:01:52.418  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  reporting-endpoints: heroku-nel="https://nel.heroku.com/reports?s=YWQRu2P%2FvZPdRO%2BFKUJHT1Kv87wV6qY2%2BEllROkIyHs%3D&sid=e11707d5-02a7-43ef-b45e-2cf4d2036f7d&ts=1751617599"
2025-07-17 23:01:52.418  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  server: cloudflare
2025-07-17 23:01:52.418  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  vary: Origin, Accept-Encoding
2025-07-17 23:01:52.418  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  via: 2.0 heroku-router
2025-07-17 23:01:52.418  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  x-content-type-options: nosniff
2025-07-17 23:01:52.419  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  x-powered-by: Express
2025-07-17 23:01:52.419  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  x-ratelimit-limit: 1000
2025-07-17 23:01:52.419  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  x-ratelimit-remaining: 999
2025-07-17 23:01:52.419  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  x-ratelimit-reset: 1751617644
2025-07-17 23:01:52.419  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  age: 21523
2025-07-17 23:01:52.419  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  cf-cache-status: HIT
2025-07-17 23:01:52.419  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  server-timing: cfCacheStatus;desc="HIT"
2025-07-17 23:01:52.419  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  server-timing: cfEdge;dur=5,cfOrigin;dur=0
2025-07-17 23:01:52.420  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  cf-ray: 960a9df9ab308969-PDX
2025-07-17 23:01:52.420  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  alt-svc: h3=":443"; ma=86400
2025-07-17 23:01:52.424  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  {
2025-07-17 23:01:52.424  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I    "id": 1,
2025-07-17 23:01:52.424  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I    "name": "Leanne Graham",
2025-07-17 23:01:52.424  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I    "username": "Bret",
2025-07-17 23:01:52.424  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I    "email": "Sincere@april.biz",
2025-07-17 23:01:52.424  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I    "address": {
2025-07-17 23:01:52.424  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I      "street": "Kulas Light",
2025-07-17 23:01:52.424  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I      "suite": "Apt. 556",
2025-07-17 23:01:52.424  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I      "city": "Gwenborough",
2025-07-17 23:01:52.424  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I      "zipcode": "92998-3874",
2025-07-17 23:01:52.424  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I      "geo": {
2025-07-17 23:01:52.425  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I        "lat": "-37.3159",
2025-07-17 23:01:52.425  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I        "lng": "81.1496"
2025-07-17 23:01:52.425  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I      }
2025-07-17 23:01:52.425  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I    },
2025-07-17 23:01:52.425  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I    "phone": "1-770-736-8031 x56442",
2025-07-17 23:01:52.425  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I    "website": "hildegard.org",
2025-07-17 23:01:52.425  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I    "company": {
2025-07-17 23:01:52.425  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I      "name": "Romaguera-Crona",
2025-07-17 23:01:52.425  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I      "catchPhrase": "Multi-layered client-server neural-net",
2025-07-17 23:01:52.425  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I      "bs": "harness real-time e-markets"
2025-07-17 23:01:52.425  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I    }
2025-07-17 23:01:52.425  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  }
2025-07-17 23:01:52.425  3886-18211 okhttp.OkHttpClient     com.cyrus.example                    I  <-- END HTTP (509-byte body)

请求失败

日志输出如下:

yaml 复制代码
2025-07-17 23:23:59.994  3886-19788 okhttp.OkHttpClient     com.cyrus.example                    I  --> GET https://jsonplaceholder.typicode.com/users/2
2025-07-17 23:23:59.994  3886-19788 okhttp.OkHttpClient     com.cyrus.example                    I  --> END GET
2025-07-17 23:25:10.160  3886-19788 okhttp.OkHttpClient     com.cyrus.example                    I  <-- HTTP FAILED: java.net.SocketTimeoutException: failed to connect to jsonplaceholder.typicode.com/104.21.64.1 (port 443) from /192.168.0.101 (port 47128) after 10000ms

当请求出错时会抛出异常,现在是通过 try-catch 去捕获处理请求出错的情况,但这样做代码中会存在大量 try-catch。

如何更优雅的处理请求结果?

更优雅地处理请求结果

封装一个更优雅的 request 方法来实现以下功能:

  • 自动在 IO 线程中执行网络请求;

  • 自动在主线程中处理结果;

  • 支持链式调用;

  • 不再依赖外部 try-catch。

代码实现如下:

kotlin 复制代码
package com.cyrus.example.retrofit.network

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

class RequestResult<T>(
    private val block: suspend () -> T
) {
    private var success: ((T) -> Unit)? = null
    private var failure: ((Throwable) -> Unit)? = null

    fun onSuccess(block: (T) -> Unit): RequestResult<T> {
        success = block
        return this
    }

    fun onFailure(block: (Throwable) -> Unit): RequestResult<T> {
        failure = block
        return this
    }

    fun launch(scope: CoroutineScope) {
        scope.launch(Dispatchers.IO) {
            try {
                val result = block()
                withContext(Dispatchers.Main) {
                    success?.invoke(result)
                }
            } catch (e: Throwable) {
                withContext(Dispatchers.Main) {
                    failure?.invoke(e)
                }
            }
        }
    }
}

fun <T> request(block: suspend () -> T): RequestResult<T> {
    return RequestResult(block)
}

调用方式如下:

ini 复制代码
request {
    RetrofitClient.api.getUser(2)
}.onSuccess {
    resultText = "用户2: ${it.name} (${it.email})"
}.onFailure {
    resultText = "❌ 请求失败:${it.message}"
}.launch(coroutineScope)

请求成功

请求失败

Retrofit 是如何实现类型转换的?

Retrofit 实现类型转换的核心在于其 Converter 转换器机制,它可以将 HTTP 请求的 body 或响应的 body 与 Java/Kotlin 对象之间互相转换。

1. 接口定义与创建代理

接口方法定义

kotlin 复制代码
interface ApiService {
    @GET("users/{id}")
    suspend fun getUser(@Path("id") id: Int): User
}

创建接口的 Service 实例。

arduino 复制代码
retrofit.create(ApiService::class.java)

Retrofit 动态代理解析注解,构建一个 ServiceMethod,封装了请求构建器、参数处理器、响应解析器等。

scss 复制代码
Retrofit.create(MyApi.class)
    └──-> validateServiceInterface(MyApi.class) // 检查传入的是接口、没有泛型参数等
    └──-> Proxy.newProxyInstance(...) // 创建接口代理
         └──-> ServiceMethod.loadServiceMethod(method) // 缓存 + 构建 ServiceMethod
              └──-> ServiceMethod.parseAnnotations(retrofit, method)  // 根据方法注解生成实际的 ServiceMethod 实例,它是 Retrofit 中对"接口方法"的抽象表示,封装了请求构建、响应转换等逻辑。
                   └──-> RequestFactory.parseAnnotations(...) // 解析方法和参数注解,构造请求路径、Query、Body、Header 等,生成一个 RequestFactory,能用于构造 okhttp3.Request。
                       └── new Builder(...).build()
                         └── parseParameterAnnotations(...):
                              for each parameter
                              └── parseParameterAnnotation(annotation, ...)
                                   ↳ if (annotation is @Body)
                                       └── retrofit.requestBodyConverter(type, parameterAnnotations, methodAnnotations)
                                            ↳ 遍历 List<Converter.Factory> converterFactories
                                            ↳ 返回 Converter<T, RequestBody>
                   └──-> HttpServiceMethod.parseAnnotations(...) // 生成具体类型的 ServiceMethod 子类,用于实际发起请求并处理返回值。
                             └──-> 调用 CallAdapter 来适配返回值
                             └──-> 调用 Converter 将响应体 ResponseBody 转为目标类型

github.com/square/retr...

Retrofit 通过 Proxy.newProxyInstance 创建接口的动态代理。每个接口方法调用都会触发 invoke(...)

typescript 复制代码
return Proxy.newProxyInstance(
    service.getClassLoader(),
    new Class<?>[] {service},
    new InvocationHandler() {
        public Object invoke(Object proxy, Method method, Object[] args) {
            ServiceMethod<?> serviceMethod = loadServiceMethod(method);
            return serviceMethod.invoke(args); // 触发网络请求
        }
    }
);

2. Retrofit 请求 → 响应 → responseConverter

scss 复制代码
API 接口函数(如 suspend fun getUser())
       ↓
Retrofit.create(...) → 动态代理 → invoke()
       ↓
ServiceMethod.invoke(args)
       ↓
HttpServiceMethod.adapt(Call<ResponseT>, args)
       ↓
CallAdapter.adapt(...) → Kotlin 协程适配器
       ↓
OkHttpCall.enqueue(...) 异步请求
       ↓
OkHttp 拦截器链发起真实网络请求
       ↓
网络响应返回(Response)
       ↓
responseConverter.convert(ResponseBody) → JSON 解析成对象
       ↓
回到协程 → Kotlin 数据类对象返回

当有多个转换器 Retrofit 如何决定使用哪个?

数据模型与 Converter 之间是如何关联的?当有多个转换器 Retrofit 如何决定使用哪个?

你可以在注册 Converter 时同时添加 Gson 和 Protobuf,比如:

scss 复制代码
Retrofit.Builder()
    .baseUrl("https://your.api/")
    .addConverterFactory(ProtobufConverterFactory.create()) // 优先匹配 protobuf
    .addConverterFactory(GsonConverterFactory.create()) // fallback 到 Gson
    .build()

这样,如果你的接口参数/返回值是 MessageLite 类型(protobuf 的基类),就会走 ProtobufConverterFactory;否则 fallback 到 Gson。

如果多个 Converter 都能处理某类型怎么办?

Retrofit 的行为是:只使用最先注册的那个可以处理的 Converter。

Converter 与类型的关联是如何发生的?

  • Retrofit 持有一组 Converter.Factory 列表 converterFactories

  • 调用 responseBodyConverter(...) 方法遍历所有 factory

  • 哪个 factory 返回了 非 null 的 Converter,就代表这个 factory "能处理"这个类型,直接使用这个 Converter

  • 第一个非 null 的 converter 被使用,其余跳过

typescript 复制代码
/**
 * Returns a {@link Converter} for {@link ResponseBody} to {@code type} from the available
 * {@linkplain #converterFactories() factories}.
 */
public <T> Converter<ResponseBody, T> responseBodyConverter(Type type, Annotation[] annotations) {
  return nextResponseBodyConverter(null, type, annotations);
}

/**
 * 实际执行查找匹配 converter 的方法。
 *
 * @param skipPast 从哪个 Converter.Factory 之后开始查找(通常用于链式调用时跳过之前已经尝试过的)
 * @param type 数据模型的类型,例如 User::class.java、List<User>::class.java 等
 * @param annotations 方法或参数上的注解,可用于影响选择(如 @Streaming)
 */
public <T> Converter<ResponseBody, T> nextResponseBodyConverter(
    @Nullable Converter.Factory skipPast, Type type, Annotation[] annotations) {

  // 检查参数不为空
  Objects.requireNonNull(type, "type == null");
  Objects.requireNonNull(annotations, "annotations == null");

  // 找出从哪个 Factory 开始查找(如 skipPast = null,则从 0 开始)
  int start = converterFactories.indexOf(skipPast) + 1;

  // 遍历所有注册的 Converter.Factory
  for (int i = start, count = converterFactories.size(); i < count; i++) {
    // 调用工厂方法尝试获取能处理该类型的 Converter
    Converter<ResponseBody, ?> converter =
        converterFactories.get(i).responseBodyConverter(type, annotations, this);

    if (converter != null) {
      //noinspection unchecked
      return (Converter<ResponseBody, T>) converter;
    }
  }

  // 如果没有任何一个工厂返回非空 converter,说明 Retrofit 无法处理这个类型,抛出异常
  StringBuilder builder =
      new StringBuilder("Could not locate ResponseBody converter for ")
          .append(type)
          .append(".\n");

  // 打印哪些工厂被跳过
  if (skipPast != null) {
    builder.append("  Skipped:");
    for (int i = 0; i < start; i++) {
      builder.append("\n   * ").append(converterFactories.get(i).getClass().getName());
    }
    builder.append('\n');
  }

  // 打印尝试过的工厂
  builder.append("  Tried:");
  for (int i = start, count = converterFactories.size(); i < count; i++) {
    builder.append("\n   * ").append(converterFactories.get(i).getClass().getName());
  }

  throw new IllegalArgumentException(builder.toString());
}

/**
 * 用于将数据类型转换为字符串(如 @Query("name") 参数),匹配逻辑同样遍历 converterFactories。
 */
public <T> Converter<T, String> stringConverter(Type type, Annotation[] annotations) {
  Objects.requireNonNull(type, "type == null");
  Objects.requireNonNull(annotations, "annotations == null");

  for (int i = 0, count = converterFactories.size(); i < count; i++) {
    Converter<?, String> converter =
        converterFactories.get(i).stringConverter(type, annotations, this);
    if (converter != null) {
      //noinspection unchecked
      return (Converter<T, String>) converter;
    }
  }

  // 没找到匹配,退而求其次使用默认的 toString 转换器
  //noinspection unchecked
  return (Converter<T, String>) BuiltInConverters.ToStringConverter.INSTANCE;
}

github.com/square/retr...

GsonConverterFactory

GsonConverterFactory 是 Retrofit 中基于 Gson 实现的一个 Converter.Factory。它负责为 Retrofit 提供序列化(Java/Kotlin → JSON)和反序列化(JSON → Java/Kotlin)的能力。

GsonConverterFactory 能够处理几乎所有类型,因为它依赖 Gson 的泛型适配能力:Gson 在运行时可以通过类型推导构造适当的 TypeAdapter,而不是硬编码某个具体的类型。

kotlin 复制代码
override fun responseBodyConverter(
    type: Type,
    annotations: Array<Annotation>,
    retrofit: Retrofit
): Converter<ResponseBody, *>? {
    val adapter: TypeAdapter<*> = gson.getAdapter(TypeToken.get(type))
    return GsonResponseBodyConverter(gson, adapter)
}

gson.getAdapter(TypeToken.get(type)) 它会根据 type 生成对应的 TypeAdapter<T>,并交给 GsonResponseBodyConverter 使用。

所以,一般 GsonConverterFactory 放最后兜底。

如何自定义转换器?

如果某个接口返回的是纯文本(如 text/plain),并且你希望直接以 String 形式接收,可以通过自定义 Converter.Factory 来实现对特定类型(如 String.class)的支持。

示例:自定义 StringResponseConverterFactory

kotlin 复制代码
package com.cyrus.example.retrofit.network

import okhttp3.ResponseBody
import retrofit2.Converter
import retrofit2.Retrofit
import java.lang.reflect.Type

class StringResponseConverterFactory : Converter.Factory() {

    override fun responseBodyConverter(
        type: Type, annotations: Array<Annotation>, retrofit: Retrofit
    ): Converter<ResponseBody, *>? {
        // 仅处理 String 类型的响应
        return if (type == String::class.java) {
            Converter<ResponseBody, String> { body -> body.string() }
        } else {
            null
        }
    }
}

当你初始化 Retrofit 时,将该工厂放在 GsonConverterFactory 之前:

scss 复制代码
private val retrofit = Retrofit.Builder()
    .baseUrl(BASE_URL)
    .client(client)
    .addConverterFactory(StringResponseConverterFactory())
    .addConverterFactory(GsonConverterFactory.create())
    .build()

接口定义示例:

kotlin 复制代码
package com.cyrus.example.retrofit.network

import retrofit2.http.GET

interface ApiService {
    
    @GET("users/1")
    suspend fun getPlainText(): String
}

接口调用示例:

ini 复制代码
request {
    RetrofitClient.api.getPlainText()
}.onSuccess {
    resultText = it
}.onFailure {
    resultText = "❌ 请求失败:${it.message}"
}.launch(coroutineScope)

效果如下:

完整源码

开源地址:github.com/CYRUS-STUDI...

相关推荐
阿巴斯甜16 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker16 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq952717 小时前
Andorid Google 登录接入文档
android
黄林晴19 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab1 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿1 天前
Android MediaPlayer 笔记
android
Jony_1 天前
Android 启动优化方案
android
阿巴斯甜1 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇1 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_2 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android