OkHttp缓存机制详细分析

OkHttp缓存机制详细分析

OkHttp是一个高效的HTTP客户端,其缓存机制是其核心功能之一。本文通过源码分析,详细解析OkHttp缓存的设计与实现。

1. 缓存架构概览

OkHttp的缓存系统由多个组件组成,它们协同工作,提供完整的HTTP缓存功能。

graph TD Client[OkHttpClient] --> Interceptors[拦截器链] Interceptors --> CacheInterceptor[缓存拦截器] CacheInterceptor --> CacheStrategy[缓存策略] CacheInterceptor --> Cache[缓存] Cache --> DiskLruCache[磁盘LRU缓存] DiskLruCache --> FileSystem[文件系统] Cache --> InternalCache[内部缓存接口] CacheStrategy --> Request[请求] CacheStrategy --> Response[响应] Cache --> Relay[数据中继] Relay --> FileOperator[文件操作]

2. 核心组件详解

2.1 Cache类

Cache类是OkHttp缓存的主要入口点,它封装了DiskLruCache,实现了InternalCache接口。

kotlin 复制代码
class Cache(
  directory: Path,
  maxSize: Long,
  fileSystem: FileSystem = FileSystem.SYSTEM
) : Closeable, Flushable {
  // 底层存储
  private val cache: DiskLruCache

  // 缓存统计
  private var writeSuccessCount = 0
  private var writeAbortCount = 0
  private var networkCount = 0
  private var hitCount = 0

  // 缓存操作
  fun get(request: Request): Response? { ... }
  fun put(response: Response): CacheRequest? { ... }
  fun remove(request: Request) { ... }
  fun update(cached: Response, network: Response) { ... }
  fun trackConditionalCacheHit() { ... }
  fun trackResponse(cacheStrategy: CacheStrategy) { ... }
}

Cache类的主要职责:

  • 管理HTTP响应的缓存存储和检索
  • 处理请求/响应头的解析和序列化
  • 提供缓存统计信息
  • 支持缓存清理和管理操作

2.2 CacheInterceptor类

CacheInterceptor是OkHttp拦截器链中处理缓存逻辑的拦截器。

kotlin 复制代码
class CacheInterceptor(private val cache: Cache?) : Interceptor {
  @Throws(IOException::class)
  override fun intercept(chain: Interceptor.Chain): Response {
    // 尝试从缓存获取响应
    val cacheCandidate = cache?.get(chain.request())

    // 应用缓存策略
    val strategy = CacheStrategy.Factory(now, chain.request(), cacheCandidate).compute()

    // 处理请求和响应
    // ...

    // 更新缓存
    // ...

    return response
  }
}

CacheInterceptor的工作流程:

sequenceDiagram participant Client as OkHttpClient participant CacheInt as CacheInterceptor participant Strategy as CacheStrategy participant Cache as Cache participant Network as NetworkInterceptor Client->>CacheInt: 发送请求 CacheInt->>Cache: 查找缓存 Cache-->>CacheInt: 返回缓存候选 CacheInt->>Strategy: 计算缓存策略 Strategy-->>CacheInt: 返回策略(网络请求/缓存) alt 使用缓存 CacheInt-->>Client: 返回缓存响应 else 需要网络请求 CacheInt->>Network: 转发请求 Network-->>CacheInt: 返回网络响应 CacheInt->>Cache: 存储响应 CacheInt-->>Client: 返回网络响应 end

2.3 CacheStrategy类

CacheStrategy决定是使用缓存响应还是发送网络请求。

kotlin 复制代码
class CacheStrategy(
  val networkRequest: Request?,
  val cacheResponse: Response?
) {
  // 工厂类用于创建缓存策略
  class Factory(
    private val nowMillis: Long,
    private val request: Request,
    private val cacheResponse: Response?
  ) {
    fun compute(): CacheStrategy {
      // 根据请求和缓存响应计算策略
      // ...
      return CacheStrategy(networkRequest, cacheResponse)
    }
  }
}

缓存策略决策流程:

flowchart TD A[开始] --> B{有缓存响应?} B -->|否| C[使用网络请求] B -->|是| D{请求指定不使用缓存?} D -->|是| C D -->|否| E{缓存响应已过期?} E -->|是| F{可以使用条件请求?} F -->|是| G[使用条件网络请求] F -->|否| C E -->|否| H{缓存响应新鲜?} H -->|是| I[使用缓存响应] H -->|否| J{请求允许陈旧响应?} J -->|是| I J -->|否| C

2.4 DiskLruCache类

DiskLruCache提供基于磁盘的LRU缓存实现。

kotlin 复制代码
class DiskLruCache(
  fileSystem: FileSystem,
  val directory: Path,
  private val appVersion: Int,
  internal val valueCount: Int,
  maxSize: Long,
  taskRunner: TaskRunner,
) : Closeable, Flushable, Lockable {
  // 缓存操作
  operator fun get(key: String): Snapshot?
  fun edit(key: String, expectedSequenceNumber: Long = ANY_SEQUENCE_NUMBER): Editor?
  fun remove(key: String): Boolean

  // 内部类
  inner class Snapshot
  inner class Editor
  internal inner class Entry
}

DiskLruCache的工作原理:

graph TD A[DiskLruCache] --> B[Journal文件] A --> C[缓存文件] B --> D[DIRTY记录] B --> E[CLEAN记录] B --> F[READ记录] B --> G[REMOVE记录] A --> H[Entry] H --> I[Snapshot] H --> J[Editor] J --> K[写入操作] I --> L[读取操作]

2.5 cache2目录组件

2.5.1 FileOperator类

FileOperator提供对文件的随机访问操作。

kotlin 复制代码
class FileOperator(private val randomAccessFile: RandomAccessFile) {
  fun write(pos: Long, source: Buffer, byteCount: Long)
  fun read(pos: Long, sink: Buffer, byteCount: Long)
}
2.5.2 Relay类

Relay在多个读取器之间共享数据。

kotlin 复制代码
class Relay(
  private val upstream: Source?,
  private val file: RandomAccessFile?,
  private val upstreamPos: Long,
  private val metadata: ByteString,
  private val bufferMaxSize: Long
) : Source {
  // 工厂方法
  companion object {
    fun edit(
      file: RandomAccessFile,
      upstream: Source,
      bufferMaxSize: Long,
      metadata: ByteString
    ): Relay

    fun read(file: RandomAccessFile): Relay
  }

  // Source接口实现
  override fun read(sink: Buffer, byteCount: Long): Long
  override fun timeout(): Timeout
  override fun close()
}

Relay的工作原理:

sequenceDiagram participant Upstream as 上游Source participant Relay as Relay participant File as 文件 participant Reader1 as 读取器1 participant Reader2 as 读取器2 Upstream->>Relay: 提供数据 Relay->>File: 写入数据 Reader1->>Relay: 请求数据 Relay->>File: 读取数据 Relay-->>Reader1: 返回数据 Reader2->>Relay: 请求数据 Relay->>File: 读取数据 Relay-->>Reader2: 返回数据

3. 缓存工作流程详解

3.1 缓存初始化

sequenceDiagram participant Client as OkHttpClient.Builder participant Cache as Cache participant DiskLruCache as DiskLruCache Client->>Cache: new Cache(directory, maxSize) Cache->>DiskLruCache: new DiskLruCache(...) DiskLruCache-->>DiskLruCache: 检查journal文件 alt journal文件存在 DiskLruCache-->>DiskLruCache: 读取journal DiskLruCache-->>DiskLruCache: 处理journal记录 else journal文件不存在 DiskLruCache-->>DiskLruCache: 创建新journal end DiskLruCache-->>Cache: 初始化完成 Cache-->>Client: 缓存就绪

3.2 缓存查找流程

sequenceDiagram participant CacheInt as CacheInterceptor participant Cache as Cache participant DiskLruCache as DiskLruCache participant Entry as Entry CacheInt->>Cache: get(request) Cache->>Cache: 计算key Cache->>DiskLruCache: get(key) DiskLruCache->>Entry: snapshot() Entry-->>DiskLruCache: 返回Snapshot DiskLruCache-->>Cache: 返回Snapshot Cache-->>Cache: 解析响应头和体 Cache-->>CacheInt: 返回缓存响应

3.3 缓存存储流程

sequenceDiagram participant CacheInt as CacheInterceptor participant Cache as Cache participant DiskLruCache as DiskLruCache participant Editor as Editor CacheInt->>Cache: put(response) Cache->>Cache: 检查是否可缓存 Cache->>Cache: 计算key Cache->>DiskLruCache: edit(key) DiskLruCache-->>Cache: 返回Editor Cache->>Editor: 写入响应头 Cache->>Editor: 创建响应体Sink Editor-->>Cache: 返回Sink Cache-->>CacheInt: 返回CacheRequest CacheInt->>CacheInt: 写入响应体 CacheInt->>Cache: 完成写入 Cache->>Editor: commit() Editor->>DiskLruCache: completeEdit(editor, true) DiskLruCache-->>DiskLruCache: 更新journal

3.4 缓存淘汰流程

flowchart TD A[开始] --> B{缓存大小 > 最大限制?} B -->|否| C[结束] B -->|是| D[查找最旧的条目] D --> E[删除条目] E --> F{缓存大小 > 最大限制?} F -->|是| D F -->|否| C

4. 核心代码分析

4.1 缓存键的生成

kotlin 复制代码
// Cache.kt
private fun key(request: Request): String {
  return request.url.toString().encodeUtf8().md5().hex()
}

OkHttp使用请求URL的MD5哈希作为缓存键,这确保了键的唯一性和固定长度。

4.2 缓存策略计算

kotlin 复制代码
// CacheStrategy.kt
fun compute(): CacheStrategy {
  val candidate = computeCandidate()

  // 如果网络请求被禁用但缓存不可用,返回失败结果
  if (candidate.networkRequest != null && request.cacheControl.onlyIfCached) {
    return CacheStrategy(null, null)
  }

  return candidate
}

private fun computeCandidate(): CacheStrategy {
  // 如果没有缓存响应,必须使用网络
  if (cacheResponse == null) {
    return CacheStrategy(request, null)
  }

  // 如果请求指定不使用缓存,必须使用网络
  if (request.isNoCache || hasConditions(request)) {
    return CacheStrategy(request, null)
  }

  // 计算缓存响应的年龄和新鲜度
  val ageMillis = cacheResponseAge()
  val freshMillis = computeFreshnessLifetime()

  // 应用请求的最小新鲜度要求
  val minFreshMillis = request.cacheControl.minFreshSeconds

  // 应用响应的最大陈旧度容忍
  val maxStaleMillis = request.cacheControl.maxStaleSeconds

  // 根据计算结果决定使用缓存还是网络
  // ...
}

这段代码展示了CacheStrategy如何根据HTTP缓存规范计算缓存策略。

4.3 条件请求的处理

kotlin 复制代码
// CacheStrategy.kt
private fun buildConditionRequest(): Request {
  val conditionRequestBuilder = request.newBuilder()

  // 如果有ETag,添加If-None-Match头
  val etag = cacheResponse.header("ETag")
  if (etag != null) {
    conditionRequestBuilder.header("If-None-Match", etag)
  }

  // 如果有Last-Modified,添加If-Modified-Since头
  val lastModified = cacheResponse.header("Last-Modified")
  if (lastModified != null) {
    conditionRequestBuilder.header("If-Modified-Since", lastModified)
  }

  return conditionRequestBuilder.build()
}

这段代码展示了如何构建条件请求,通过添加If-None-MatchIf-Modified-Since头,使服务器能够判断资源是否已更改。

4.4 Relay的数据共享机制

kotlin 复制代码
// Relay.kt
override fun read(sink: Buffer, byteCount: Long): Long {
  synchronized(upstreamLock) {
    check(!upstreamComplete || upstreamPos > expectedPos)

    // 等待数据可用
    while (expectedPos >= upstreamPos && !upstreamComplete) {
      try {
        upstreamReader.wait()
      } catch (e: InterruptedException) {
        Thread.currentThread().interrupt()
        return -1
      }
    }

    // 如果已到达文件末尾,返回-1
    if (expectedPos >= upstreamPos) return -1

    // 计算实际可读取的字节数
    val toRead = minOf(byteCount, upstreamPos - expectedPos)

    // 从文件读取数据
    fileOperator.read(expectedPos, sink, toRead)

    // 更新位置
    expectedPos += toRead
    return toRead
  }
}

Relay通过同步机制和文件操作,实现了多个读取器之间的数据共享,同时保证了数据的一致性和完整性。

5. 缓存系统的优缺点分析

5.1 优点

graph TD A[OkHttp缓存优点] --> B[减少网络请求] A --> C[提高响应速度] A --> D[降低服务器负载] A --> E[离线访问能力] A --> F[标准HTTP缓存实现] A --> G[可配置性强] A --> H[健壮的错误处理] A --> I[支持条件请求]
  1. 标准遵循:完全实现HTTP缓存规范(RFC 7234),正确处理各种缓存控制头
  2. 高效性能:使用Okio提供高效I/O,支持并发读取,文件操作的原子性保证
  3. 健壮性:容错处理、崩溃恢复机制、并发安全的实现
  4. 灵活配置:可配置的缓存大小和目录,可通过CacheControl自定义缓存行为
  5. 透明使用:对开发者友好,可以无感知地集成到应用中

5.2 缺点和限制

graph TD A[OkHttp缓存限制] --> B[仅支持标准HTTP缓存] A --> C[不支持自定义缓存键] A --> D[缓存策略不可扩展] A --> E[无内存缓存层] A --> F[缓存统计有限] A --> G[无缓存预热机制]
  1. 仅支持标准HTTP缓存:不支持自定义缓存逻辑或非标准缓存控制
  2. 缓存键限制:仅使用URL作为缓存键,不考虑请求头或请求体
  3. 无内存缓存:没有提供内存缓存层,每次都需要从磁盘读取
  4. 缓存统计有限:提供的缓存统计信息相对基础
  5. 无缓存预热:没有内置的缓存预热机制

6. 实际应用与最佳实践

6.1 缓存配置示例

kotlin 复制代码
// 创建缓存目录
val cacheDirectory = File(context.cacheDir, "http-cache")
// 设置缓存大小为10MB
val cacheSize = 10L * 1024L * 1024L // 10 MiB
// 创建缓存对象
val cache = Cache(cacheDirectory, cacheSize)

// 配置OkHttpClient
val client = OkHttpClient.Builder()
    .cache(cache)
    .build()

6.2 自定义缓存控制

kotlin 复制代码
// 强制使用网络
val request = Request.Builder()
    .url("https://example.com/data")
    .cacheControl(CacheControl.FORCE_NETWORK)
    .build()

// 强制使用缓存
val request = Request.Builder()
    .url("https://example.com/data")
    .cacheControl(CacheControl.FORCE_CACHE)
    .build()

// 自定义缓存控制
val cacheControl = CacheControl.Builder()
    .maxAge(10, TimeUnit.MINUTES)
    .build()
val request = Request.Builder()
    .url("https://example.com/data")
    .cacheControl(cacheControl)
    .build()

6.3 缓存监控

kotlin 复制代码
// 获取缓存统计信息
val hitCount = cache.hitCount()
val networkCount = cache.networkCount()
val requestCount = cache.requestCount()

// 计算缓存命中率
val hitRate = hitCount.toFloat() / requestCount.toFloat()
println("Cache hit rate: ${hitRate * 100}%")

// 获取缓存大小
val size = cache.size()
val maxSize = cache.maxSize()
println("Cache size: $size / $maxSize bytes (${size * 100 / maxSize}%)")

7. 总结

OkHttp的缓存实现是一个设计精良的系统,通过分层架构和明确的职责划分,实现了高效、健壮的HTTP缓存功能。从高层的缓存策略到底层的磁盘存储,每个组件都有明确的职责,共同协作提供完整的缓存解决方案。

graph TD A[OkHttp缓存系统] --> B[架构设计] A --> C[标准实现] A --> D[性能优化] A --> E[健壮性] A --> F[易用性] B --> B1[分层设计] B --> B2[职责分离] B --> B3[组件化] C --> C1[HTTP缓存规范] C --> C2[条件请求] C --> C3[缓存控制] D --> D1[Okio高效I/O] D --> D2[并发读取] D --> D3[LRU淘汰] E --> E1[容错处理] E --> E2[崩溃恢复] E --> E3[并发安全] F --> F1[简单API] F --> F2[透明集成] F --> F3[可配置性]

通过深入理解OkHttp的缓存机制,开发者可以更好地利用缓存来提高应用性能,减少网络请求,提升用户体验。同时,了解其内部实现也有助于在遇到缓存相关问题时进行有效的调试和优化。

8. OkHttp缓存与其他HTTP客户端缓存比较

OkHttp的缓存实现与其他流行的HTTP客户端相比有其独特的特点。下面是一个比较图:

graph TD A[HTTP客户端缓存比较] --> B[OkHttp] A --> C[Retrofit] A --> D[Volley] A --> E[HttpURLConnection] B --> B1[完整HTTP缓存规范] B --> B2[磁盘LRU缓存] B --> B3[拦截器架构] B --> B4[高效I/O] C --> C1[基于OkHttp缓存] C --> C2[类型安全API] D --> D1[内存+磁盘缓存] D --> D2[请求优先级] D --> D3[自定义缓存键] E --> E1[基础HTTP缓存] E --> E2[无LRU实现] E --> E3[配置有限]

8.1 与Retrofit的比较

Retrofit实际上使用OkHttp作为其底层HTTP客户端,因此继承了OkHttp的所有缓存功能。主要区别在于:

  • Retrofit提供了更高级别的API抽象
  • Retrofit专注于API接口定义,而OkHttp专注于HTTP传输
  • 在Retrofit中配置缓存仍然需要通过OkHttp实例

8.2 与Volley的比较

kotlin 复制代码
// Volley缓存配置示例
val cache = DiskBasedCache(cacheDir, 1024 * 1024) // 1MB缓存
val network = BasicNetwork(HurlStack())
val requestQueue = RequestQueue(cache, network).apply {
    start()
}

Volley的缓存系统与OkHttp有显著差异:

  • Volley同时提供内存和磁盘缓存
  • Volley允许自定义缓存键生成
  • Volley的缓存实现相对简单,不完全符合HTTP缓存规范
  • Volley支持请求优先级,这在OkHttp中需要自行实现

8.3 与HttpURLConnection的比较

HttpURLConnection是Java标准库提供的HTTP客户端,其缓存功能相对基础:

  • 需要手动配置缓存目录和策略
  • 缺乏高级缓存控制选项
  • 没有内置的LRU实现
  • 缓存行为不一致,取决于Android版本

9. 缓存调试与故障排除

9.1 常见缓存问题及解决方案

flowchart TD A[缓存问题诊断] --> B{缓存未生效?} B -->|是| C{响应头允许缓存?} C -->|否| D[检查Cache-Control/Expires头] C -->|是| E{缓存配置正确?} E -->|否| F[检查Cache实例化和大小] E -->|是| G{请求方法可缓存?} G -->|否| H[只有GET请求可缓存] G -->|是| I{URL包含查询参数?} I -->|是| J[查询参数会影响缓存键] A --> K{缓存过期太快?} K -->|是| L[检查max-age设置] K -->|否| M{磁盘空间不足?} M -->|是| N[增加缓存大小或清理空间]

9.2 缓存日志分析

启用OkHttp的日志拦截器可以帮助调试缓存问题:

kotlin 复制代码
val loggingInterceptor = HttpLoggingInterceptor().apply {
    level = HttpLoggingInterceptor.Level.HEADERS
}

val client = OkHttpClient.Builder()
    .cache(cache)
    .addNetworkInterceptor(loggingInterceptor)
    .build()

日志输出示例及分析:

lua 复制代码
--> GET https://api.example.com/data
--> END GET

<-- 200 OK https://api.example.com/data (150ms)
Cache-Control: max-age=600
Content-Type: application/json
Content-Length: 1234
<-- END HTTP

这表示一个网络请求,响应可以缓存600秒。

lua 复制代码
--> GET https://api.example.com/data
--> END GET

<-- 200 OK https://api.example.com/data (15ms)
Cache-Control: max-age=600
Content-Type: application/json
Content-Length: 1234
X-Android-Response-Source: CACHE 200
<-- END HTTP

这表示响应来自缓存,注意响应时间显著减少。

9.3 缓存验证工具

创建一个简单的缓存验证工具:

kotlin 复制代码
fun validateCache(client: OkHttpClient, url: String) {
    // 第一次请求 - 应该从网络获取
    val firstResponse = client.newCall(Request.Builder().url(url).build()).execute()
    println("First request - from network: ${firstResponse.networkResponse != null}")
    println("Cache headers: ${firstResponse.headers("Cache-Control")}")
    firstResponse.close()

    // 第二次请求 - 如果缓存正常工作,应该从缓存获取
    val secondResponse = client.newCall(Request.Builder().url(url).build()).execute()
    println("Second request - from cache: ${secondResponse.cacheResponse != null}")
    println("From network: ${secondResponse.networkResponse != null}")
    secondResponse.close()
}

10. 性能优化建议

10.1 缓存性能优化

graph TD A[缓存性能优化] --> B[合理设置缓存大小] A --> C[预热关键请求] A --> D[避免缓存大文件] A --> E[定期维护缓存] A --> F[使用条件请求] B --> B1[考虑设备存储容量] B --> B2[考虑应用数据量] C --> C1[应用启动时预加载] C --> C2[后台定期刷新] D --> D1[大文件使用专用下载] D --> D2[考虑分块传输] E --> E1[定期删除过期条目] E --> E2[监控缓存大小] F --> F1[利用ETag] F --> F2[利用Last-Modified]

10.2 缓存配置最佳实践

kotlin 复制代码
// 推荐的缓存配置
val cache = Cache(
    directory = File(context.cacheDir, "http-cache"),
    // 缓存大小根据应用需求调整,这里设置为50MB
    maxSize = 50L * 1024L * 1024L
)

val client = OkHttpClient.Builder()
    .cache(cache)
    // 添加离线缓存支持的拦截器
    .addInterceptor { chain ->
        var request = chain.request()
        if (!isNetworkAvailable()) {
            // 离线时强制使用缓存
            request = request.newBuilder()
                .header("Cache-Control", "public, only-if-cached, max-stale=${60 * 60 * 24 * 7}")
                .build()
        }
        chain.proceed(request)
    }
    .build()

10.3 缓存预热策略

kotlin 复制代码
// 缓存预热函数
fun warmupCache(client: OkHttpClient, urls: List<String>) {
    urls.forEach { url ->
        try {
            // 使用异步请求避免阻塞
            client.newCall(
                Request.Builder()
                    .url(url)
                    .cacheControl(CacheControl.Builder().maxStale(365, TimeUnit.DAYS).build())
                    .build()
            ).enqueue(object : Callback {
                override fun onFailure(call: Call, e: IOException) {
                    // 忽略错误
                }

                override fun onResponse(call: Call, response: Response) {
                    response.close() // 确保关闭响应
                }
            })
        } catch (e: Exception) {
            // 忽略错误
        }
    }
}

11. 缓存安全性考虑

11.1 缓存安全风险

graph TD A[缓存安全风险] --> B[敏感数据泄露] A --> C[缓存投毒攻击] A --> D[缓存不一致] A --> E[跨用户数据泄露] B --> B1[缓存包含认证令牌] B --> B2[缓存包含个人信息] C --> C1[中间人攻击缓存响应] C --> C2[服务器返回恶意缓存指令] D --> D1[缓存与服务器不同步] D --> D2[多设备缓存不一致] E --> E1[用户切换时缓存未清除] E --> E2[共享缓存目录]

11.2 缓存安全最佳实践

  1. 不缓存敏感数据
kotlin 复制代码
// 对包含敏感数据的请求禁用缓存
val request = Request.Builder()
    .url("https://api.example.com/user/profile")
    .header("Cache-Control", "no-store")
    .build()
  1. 用户特定的缓存目录
kotlin 复制代码
// 为每个用户创建单独的缓存目录
val userSpecificCache = Cache(
    directory = File(context.cacheDir, "user-${userId}-cache"),
    maxSize = 10L * 1024L * 1024L
)
  1. 用户切换时清除缓存
kotlin 复制代码
// 用户登出时清除缓存
fun onUserLogout() {
    // 异步清除缓存
    Thread {
        try {
            cache.evictAll()
        } catch (e: IOException) {
            // 处理错误
        }
    }.start()
}
  1. HTTPS与证书固定
kotlin 复制代码
// 使用证书固定增强安全性
val certificatePinner = CertificatePinner.Builder()
    .add("api.example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
    .build()

val client = OkHttpClient.Builder()
    .cache(cache)
    .certificatePinner(certificatePinner)
    .build()

通过实施这些安全最佳实践,可以显著降低与HTTP缓存相关的安全风险,同时仍然享受缓存带来的性能优势。

相关推荐
踢球的打工仔15 小时前
PHP面向对象(7)
android·开发语言·php
安卓理事人15 小时前
安卓socket
android
安卓理事人21 小时前
安卓LinkedBlockingQueue消息队列
android
万能的小裴同学1 天前
Android M3U8视频播放器
android·音视频
q***57741 天前
MySql的慢查询(慢日志)
android·mysql·adb
JavaNoober1 天前
Android 前台服务 "Bad Notification" 崩溃机制分析文档
android
城东米粉儿1 天前
关于ObjectAnimator
android
zhangphil1 天前
Android渲染线程Render Thread的RenderNode与DisplayList,引用Bitmap及Open GL纹理上传GPU
android
火柴就是我1 天前
从头写一个自己的app
android·前端·flutter
lichong9511 天前
XLog debug 开启打印日志,release 关闭打印日志
android·java·前端