网络性能优化完全指南
优化项方案预期收益DNS优化HttpDNS + 预解析 + 缓存首次请求减少200-400ms连接优化连接池 + 预热 + HTTP/2后续请求减少350ms传输优化流量减少60-80%弱网优化重试 + 降级 + 断点续传弱网成功率提升50%+缓存优化三级缓存 + HTTP缓存二次访问速度提升95%+流量优化图片压缩 + 增量更新总流量减少40-60%
·
网络性能优化完全指南
核心主题:系统掌握DNS优化(HttpDNS、预解析)、连接优化(连接池、预热、HTTP/2/3)、传输优化(GZIP、Protobuf、分片传输)、弱网优化(重试、降级、断点续传)、流量优化(图片压缩、增量更新、多级缓存)等全方位网络性能调优技术
关键知识点速查
1. DNS优化
HttpDNS原理
传统DNS问题:
1. 运营商DNS劫持(返回错误IP)
2. DNS污染(缓存被篡改)
3. 解析慢(递归查询耗时长)
4. 调度不准确(基于LocalDNS,非用户真实IP)
HttpDNS优势:
1. 绕过运营商LocalDNS
2. 直接请求权威DNS服务器
3. 基于HTTP/HTTPS(更可靠)
4. 精准调度(基于客户端IP)
传统DNS流程:
App -> LocalDNS -> 根DNS -> .com DNS -> 权威DNS -> LocalDNS -> App
耗时:200-500ms
HttpDNS流程:
App -> HttpDNS服务器 -> App
耗时:50-150ms
HttpDNS实现
class HttpDnsResolver {
private val dnsCache = ConcurrentHashMap<String, DnsCacheEntry>()
private val httpClient = OkHttpClient()
data class DnsCacheEntry(
val ip: String,
val ttl: Long,
val timestamp: Long
)
// HttpDNS解析(阿里云为例)
suspend fun resolve(hostname: String): String? = withContext(Dispatchers.IO) {
// 1. 检查缓存
val cached = dnsCache[hostname]
if (cached != null && !isExpired(cached)) {
println("DNS缓存命中: $hostname -> ${cached.ip}")
return@withContext cached.ip
}
try {
// 2. 请求HttpDNS(使用IP直接请求,避免域名解析)
val accountId = "12345" // 阿里云账号ID
val url = "http://203.107.1.1/$accountId/d?host=$hostname"
val request = Request.Builder()
.url(url)
.build()
val response = httpClient.newCall(request).execute()
val json = JSONObject(response.body?.string() ?: "{}")
// 3. 解析结果
val ips = json.getJSONArray("ips")
if (ips.length() > 0) {
val ip = ips.getString(0)
val ttl = json.optLong("ttl", 60L)
// 4. 缓存结果
dnsCache[hostname] = DnsCacheEntry(
ip = ip,
ttl = ttl,
timestamp = System.currentTimeMillis()
)
println("HttpDNS解析: $hostname -> $ip (TTL=${ttl}s)")
return@withContext ip
}
} catch (e: Exception) {
println("HttpDNS解析失败: ${e.message},降级到系统DNS")
}
// 5. 降级到系统DNS
return@withContext systemDnsResolve(hostname)
}
// 系统DNS解析
private fun systemDnsResolve(hostname: String): String? {
return try {
InetAddress.getByName(hostname).hostAddress
} catch (e: Exception) {
println("系统DNS解析失败: ${e.message}")
null
}
}
// 检查缓存是否过期
private fun isExpired(entry: DnsCacheEntry): Boolean {
val age = (System.currentTimeMillis() - entry.timestamp) / 1000
return age > entry.ttl
}
// 预解析(App启动时)
fun preResolve(hostnames: List<String>) {
GlobalScope.launch(Dispatchers.IO) {
hostnames.forEach { hostname ->
try {
resolve(hostname)
} catch (e: Exception) {
println("预解析失败[$hostname]: ${e.message}")
}
}
}
}
// 清理过期缓存
fun cleanExpiredCache() {
val iterator = dnsCache.iterator()
while (iterator.hasNext()) {
val entry = iterator.next()
if (isExpired(entry.value)) {
iterator.remove()
}
}
}
}
// OkHttp集成HttpDNS
class HttpDnsIntegration(private val dnsResolver: HttpDnsResolver) : Dns {
override fun lookup(hostname: String): List<InetAddress> {
// HttpDNS解析
val ip = runBlocking { dnsResolver.resolve(hostname) }
return if (ip != null) {
listOf(InetAddress.getByName(ip))
} else {
// 降级到系统DNS
Dns.SYSTEM.lookup(hostname)
}
}
}
// 使用示例
val dnsResolver = HttpDnsResolver()
// App启动时预解析
dnsResolver.preResolve(listOf(
"api.T8000.com",
"device.T8000.com",
"cdn.T8000.com"
))
// OkHttp配置
val client = OkHttpClient.Builder()
.dns(HttpDnsIntegration(dnsResolver))
.build()
HTTPDNS + IP直连 + Host Header
// 问题:使用IP直连会导致HTTPS证书验证失败
// 解决:保留Host Header,使用IP直连
class HttpDnsWithHostHeader(private val dnsResolver: HttpDnsResolver) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
val originalUrl = request.url
// 获取IP
val ip = runBlocking { dnsResolver.resolve(originalUrl.host) }
if (ip != null) {
// 替换Host为IP
val newUrl = originalUrl.newBuilder()
.host(ip)
.build()
// 保留原始Host Header
request = request.newBuilder()
.url(newUrl)
.header("Host", originalUrl.host) // 关键:保留Host
.build()
println("IP直连: ${originalUrl.host} -> $ip")
}
return chain.proceed(request)
}
}
// 使用
val client = OkHttpClient.Builder()
.addInterceptor(HttpDnsWithHostHeader(dnsResolver))
.build()
2. 连接优化
连接复用(Keep-Alive)
// HTTP/1.1默认启用Keep-Alive
// 一个TCP连接可以发送多个HTTP请求
// OkHttp自动管理连接池
val connectionPool = ConnectionPool(
maxIdleConnections = 10, // 最大空闲连接数
keepAliveDuration = 5, // 连接保活时长(分钟)
timeUnit = TimeUnit.MINUTES
)
val client = OkHttpClient.Builder()
.connectionPool(connectionPool)
.build()
// 连接复用效果:
// 第一次请求:DNS解析(100ms) + TCP握手(150ms) + SSL握手(200ms) + HTTP请求(50ms) = 500ms
// 第二次请求:HTTP请求(50ms) = 50ms(复用连接,节省450ms)
连接预热(Warm-up)
class ConnectionWarmer(private val client: OkHttpClient) {
// 预建立连接
suspend fun warmup(urls: List<String>) = withContext(Dispatchers.IO) {
urls.forEach { url ->
try {
// 发送HEAD请求(不获取Body,只建立连接)
val request = Request.Builder()
.url(url)
.head()
.build()
client.newCall(request).execute().use { response ->
if (response.isSuccessful) {
println("连接预热成功: $url")
}
}
} catch (e: Exception) {
println("连接预热失败[$url]: ${e.message}")
}
}
}
}
// 使用(App启动时)
val warmer = ConnectionWarmer(client)
GlobalScope.launch {
warmer.warmup(listOf(
"https://api.T8000.com",
"https://device.T8000.com",
"https://cdn.T8000.com"
))
}
HTTP/2支持
// OkHttp默认支持HTTP/2
// HTTP/2优势:
// 1. 多路复用:一个TCP连接支持多个并发请求
// 2. 头部压缩:HPACK算法压缩Header
// 3. 服务器推送:服务器主动推送资源
// 4. 二进制分帧:更高效的传输
// 对比:
// HTTP/1.1:
// 请求1 -----> [连接1] -----> 服务器
// 请求2 -----> [连接2] -----> 服务器
// 请求3 -----> [连接3] -----> 服务器
// (需要3个TCP连接)
// HTTP/2:
// 请求1 ---|
// 请求2 ---|--> [连接1] -----> 服务器
// 请求3 ---|
// (只需1个TCP连接)
// 检查是否使用HTTP/2
client.newCall(request).execute().use { response ->
println("协议: ${response.protocol}") // h2 = HTTP/2, http/1.1 = HTTP/1.1
}
3. 传输优化
GZIP压缩
// OkHttp自动处理GZIP压缩
// 请求头:
// Accept-Encoding: gzip
// 响应头:
// Content-Encoding: gzip
// 压缩效果(JSON数据):
// 原始大小:100KB
// GZIP压缩后:20KB(压缩率80%)
// 手动启用GZIP(默认已启用)
val client = OkHttpClient.Builder()
.addInterceptor { chain ->
val request = chain.request().newBuilder()
.header("Accept-Encoding", "gzip")
.build()
chain.proceed(request)
}
.build()
Protocol Buffers(Protobuf)
// Protobuf vs JSON
// JSON(文本格式):
// {"deviceId":"camera001","online":true,"battery":80}
// 大小:55字节
// Protobuf(二进制格式):
// [0a 0a 63 61 6d 65 72 61 30 30 31 10 01 18 50]
// 大小:15字节(节省73%)
// 定义Protobuf消息
syntax = "proto3";
message DeviceStatus {
string device_id = 1;
bool online = 2;
int32 battery = 3;
}
// 生成Kotlin代码
// protoc --kotlin_out=. device.proto
// 使用Protobuf
class ProtobufConverter : Converter.Factory() {
override fun responseBodyConverter(
type: Type,
annotations: Array<Annotation>,
retrofit: Retrofit
): Converter<ResponseBody, *> {
return ProtobufResponseConverter()
}
override fun requestBodyConverter(
type: Type,
parameterAnnotations: Array<Annotation>,
methodAnnotations: Array<Annotation>,
retrofit: Retrofit
): Converter<*, RequestBody> {
return ProtobufRequestConverter()
}
}
class ProtobufResponseConverter : Converter<ResponseBody, Any> {
override fun convert(value: ResponseBody): Any {
val bytes = value.bytes()
return DeviceStatus.parseFrom(bytes)
}
}
class ProtobufRequestConverter : Converter<Any, RequestBody> {
override fun convert(value: Any): RequestBody {
val message = value as GeneratedMessageV3
val bytes = message.toByteArray()
return bytes.toRequestBody("application/x-protobuf".toMediaType())
}
}
// Retrofit配置
val retrofit = Retrofit.Builder()
.baseUrl("https://api.T8000.com/")
.addConverterFactory(ProtobufConverter())
.build()
数据分片(Chunked Transfer)
// 大文件上传使用分片传输
class ChunkedUploader(private val client: OkHttpClient) {
suspend fun upload(
file: File,
uploadUrl: String,
chunkSize: Int = 1024 * 1024, // 1MB
onProgress: (Int) -> Unit
) = withContext(Dispatchers.IO) {
val totalSize = file.length()
var uploadedSize = 0L
file.inputStream().use { input ->
val buffer = ByteArray(chunkSize)
var chunkIndex = 0
while (true) {
val bytesRead = input.read(buffer)
if (bytesRead == -1) break
// 上传分片
val chunk = buffer.copyOf(bytesRead)
uploadChunk(uploadUrl, chunk, chunkIndex, totalSize)
// 更新进度
uploadedSize += bytesRead
val progress = (uploadedSize * 100 / totalSize).toInt()
withContext(Dispatchers.Main) {
onProgress(progress)
}
chunkIndex++
}
}
}
private fun uploadChunk(url: String, chunk: ByteArray, index: Int, totalSize: Long) {
val requestBody = chunk.toRequestBody("application/octet-stream".toMediaType())
val request = Request.Builder()
.url(url)
.post(requestBody)
.header("Content-Range", "bytes ${index * chunk.size}-${index * chunk.size + chunk.size - 1}/$totalSize")
.header("X-Chunk-Index", index.toString())
.build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) {
throw IOException("分片上传失败: ${response.code}")
}
}
}
}
// 使用
val uploader = ChunkedUploader(client)
GlobalScope.launch {
uploader.upload(
file = File("/sdcard/log.zip"),
uploadUrl = "https://api.T8000.com/upload"
) { progress ->
println("上传进度: $progress%")
}
}
4. 弱网优化
超时重试
class RetryInterceptor(
private val maxRetry: Int = 3,
private val retryDelay: Long = 1000L
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
var response: Response? = null
var lastException: Exception? = null
repeat(maxRetry) { attempt ->
try {
response?.close()
response = chain.proceed(request)
// 成功或客户端错误不重试
if (response!!.isSuccessful || response!!.code < 500) {
return response!!
}
println("请求失败[${response!!.code}],重试 ${attempt + 1}/$maxRetry")
Thread.sleep(retryDelay * (attempt + 1)) // 线性增长延迟
} catch (e: SocketTimeoutException) {
lastException = e
println("请求超时,重试 ${attempt + 1}/$maxRetry")
Thread.sleep(retryDelay * (attempt + 1))
} catch (e: IOException) {
lastException = e
println("网络错误,重试 ${attempt + 1}/$maxRetry")
Thread.sleep(retryDelay * (attempt + 1))
}
}
return response ?: throw lastException ?: IOException("请求失败")
}
}
// 使用
val client = OkHttpClient.Builder()
.addInterceptor(RetryInterceptor(maxRetry = 3))
.build()
降级策略
class DegradationManager {
// 网络质量评估
enum class NetworkQuality {
EXCELLENT, // 优秀(4G/5G/Wi-Fi强信号)
GOOD, // 良好(4G/Wi-Fi弱信号)
POOR, // 较差(3G)
BAD // 很差(2G/无信号)
}
fun getNetworkQuality(context: Context): NetworkQuality {
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val network = cm.activeNetwork ?: return NetworkQuality.BAD
val capabilities = cm.getNetworkCapabilities(network) ?: return NetworkQuality.BAD
return when {
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> {
val signalStrength = getWifiSignalStrength(context)
if (signalStrength > -60) NetworkQuality.EXCELLENT
else NetworkQuality.GOOD
}
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> {
val networkType = getNetworkType(context)
when (networkType) {
"5G", "4G" -> NetworkQuality.GOOD
"3G" -> NetworkQuality.POOR
else -> NetworkQuality.BAD
}
}
else -> NetworkQuality.BAD
}
}
// 根据网络质量调整策略
fun getOptimizedConfig(quality: NetworkQuality): NetworkConfig {
return when (quality) {
NetworkQuality.EXCELLENT -> NetworkConfig(
imageQuality = ImageQuality.HIGH,
videoQuality = VideoQuality.HD,
enablePreload = true,
timeout = 30_000
)
NetworkQuality.GOOD -> NetworkConfig(
imageQuality = ImageQuality.MEDIUM,
videoQuality = VideoQuality.SD,
enablePreload = true,
timeout = 45_000
)
NetworkQuality.POOR -> NetworkConfig(
imageQuality = ImageQuality.LOW,
videoQuality = VideoQuality.LD,
enablePreload = false,
timeout = 60_000
)
NetworkQuality.BAD -> NetworkConfig(
imageQuality = ImageQuality.THUMBNAIL,
videoQuality = VideoQuality.LD,
enablePreload = false,
timeout = 90_000
)
}
}
private fun getWifiSignalStrength(context: Context): Int {
val wifiManager = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
return wifiManager.connectionInfo.rssi
}
private fun getNetworkType(context: Context): String {
val tm = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
return when (tm.dataNetworkType) {
TelephonyManager.NETWORK_TYPE_NR -> "5G"
TelephonyManager.NETWORK_TYPE_LTE -> "4G"
TelephonyManager.NETWORK_TYPE_UMTS, TelephonyManager.NETWORK_TYPE_HSDPA -> "3G"
else -> "2G"
}
}
}
data class NetworkConfig(
val imageQuality: ImageQuality,
val videoQuality: VideoQuality,
val enablePreload: Boolean,
val timeout: Long
)
enum class ImageQuality { HIGH, MEDIUM, LOW, THUMBNAIL }
enum class VideoQuality { HD, SD, LD }
// 使用
val degradationManager = DegradationManager()
val quality = degradationManager.getNetworkQuality(context)
val config = degradationManager.getOptimizedConfig(quality)
println("网络质量: $quality")
println("图片质量: ${config.imageQuality}")
println("视频质量: ${config.videoQuality}")
断点续传
class ResumeDownloader(private val client: OkHttpClient) {
suspend fun download(
url: String,
destFile: File,
onProgress: (downloaded: Long, total: Long) -> Unit
): Boolean = withContext(Dispatchers.IO) {
var downloadedBytes = 0L
// 检查已下载部分
if (destFile.exists()) {
downloadedBytes = destFile.length()
println("继续下载,已下载: ${downloadedBytes / 1024}KB")
}
while (true) {
try {
// Range请求(断点续传)
val request = Request.Builder()
.url(url)
.header("Range", "bytes=$downloadedBytes-") // 关键:Range Header
.build()
val response = client.newCall(request).execute()
when (response.code) {
206 -> {
// 部分内容(断点续传成功)
val contentLength = response.body?.contentLength() ?: 0
val totalLength = downloadedBytes + contentLength
response.body?.byteStream()?.use { input ->
FileOutputStream(destFile, true).use { output -> // append模式
val buffer = ByteArray(8192)
var bytes = input.read(buffer)
while (bytes >= 0) {
output.write(buffer, 0, bytes)
downloadedBytes += bytes
// 通知进度
withContext(Dispatchers.Main) {
onProgress(downloadedBytes, totalLength)
}
bytes = input.read(buffer)
}
}
}
println("下载完成: ${destFile.absolutePath}")
return@withContext true
}
200 -> {
// 完整内容(服务器不支持断点续传,重新下载)
println("服务器不支持断点续传,重新下载")
destFile.delete()
downloadedBytes = 0L
}
416 -> {
// 文件已完整下载
println("文件已完整")
return@withContext true
}
else -> {
throw IOException("响应码错误: ${response.code}")
}
}
} catch (e: Exception) {
println("下载异常: ${e.message},3秒后重试...")
delay(3000)
}
}
}
}
// 使用
val downloader = ResumeDownloader(client)
GlobalScope.launch {
val success = downloader.download(
url = "https://cdn.T8000.com/firmware/v1.2.3.bin",
destFile = File(context.filesDir, "firmware.bin")
) { downloaded, total ->
val progress = (downloaded * 100 / total).toInt()
println("下载进度: $progress%")
}
}
5. 流量优化
图片压缩
class ImageCompressor {
// 压缩图片
fun compress(
bitmap: Bitmap,
quality: Int = 80, // 0-100
maxWidth: Int = 1080,
maxHeight: Int = 1920
): ByteArray {
// 1. 缩放
val scaled = if (bitmap.width > maxWidth || bitmap.height > maxHeight) {
val ratio = min(
maxWidth.toFloat() / bitmap.width,
maxHeight.toFloat() / bitmap.height
)
val targetWidth = (bitmap.width * ratio).toInt()
val targetHeight = (bitmap.height * ratio).toInt()
Bitmap.createScaledBitmap(bitmap, targetWidth, targetHeight, true)
} else {
bitmap
}
// 2. 压缩
val output = ByteArrayOutputStream()
scaled.compress(Bitmap.CompressFormat.JPEG, quality, output)
return output.toByteArray()
}
// 压缩文件
fun compressFile(
inputFile: File,
outputFile: File,
quality: Int = 80,
maxWidth: Int = 1080,
maxHeight: Int = 1920
) {
val bitmap = BitmapFactory.decodeFile(inputFile.absolutePath)
val compressed = compress(bitmap, quality, maxWidth, maxHeight)
outputFile.outputStream().use { it.write(compressed) }
val originalSize = inputFile.length() / 1024
val compressedSize = outputFile.length() / 1024
val ratio = (compressedSize * 100 / originalSize).toInt()
println("压缩完成: ${originalSize}KB -> ${compressedSize}KB (${100 - ratio}%)")
}
// Glide集成(自动压缩)
fun loadCompressedImage(context: Context, url: String, imageView: ImageView) {
Glide.with(context)
.load(url)
.override(1080, 1920) // 最大尺寸
.format(DecodeFormat.PREFER_RGB_565) // 使用RGB_565(节省50%内存)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.into(imageView)
}
}
增量更新
// 使用bsdiff算法生成差分包
// 服务器端生成差分包:
// bsdiff old.apk new.apk patch.bin
// 客户端应用差分包:
class IncrementalUpdater {
suspend fun update(
oldApkPath: String,
patchUrl: String,
newApkPath: String,
onProgress: (Int) -> Unit
) = withContext(Dispatchers.IO) {
// 1. 下载差分包
val patchFile = File(context.cacheDir, "patch.bin")
downloadFile(patchUrl, patchFile, onProgress)
// 2. 应用差分包(使用bspatch库)
BsPatch.patch(
oldFile = oldApkPath,
newFile = newApkPath,
patchFile = patchFile.absolutePath
)
// 3. 验证新APK
if (verifyApk(newApkPath)) {
println("增量更新成功")
installApk(newApkPath)
} else {
throw Exception("APK校验失败")
}
}
private fun verifyApk(apkPath: String): Boolean {
// 计算MD5并验证
val md5 = calculateMd5(File(apkPath))
val expectedMd5 = getExpectedMd5()
return md5 == expectedMd5
}
private fun calculateMd5(file: File): String {
val digest = MessageDigest.getInstance("MD5")
file.inputStream().use { input ->
val buffer = ByteArray(8192)
var bytes = input.read(buffer)
while (bytes >= 0) {
digest.update(buffer, 0, bytes)
bytes = input.read(buffer)
}
}
return digest.digest().joinToString("") { "%02x".format(it) }
}
}
// 优势:
// 完整APK:20MB
// 差分包:2MB(节省90%流量)
缓存策略
class CacheManager {
// 三级缓存
// 1. 内存缓存(LruCache)
private val memoryCache = object : LruCache<String, Bitmap>(
(Runtime.getRuntime().maxMemory() / 8).toInt() // 最大内存的1/8
) {
override fun sizeOf(key: String, value: Bitmap): Int {
return value.byteCount
}
}
// 2. 磁盘缓存(DiskLruCache)
private val diskCache = DiskLruCache.open(
File(context.cacheDir, "image_cache"),
1,
1,
50 * 1024 * 1024 // 50MB
)
// 3. 网络请求
fun getBitmap(url: String): Bitmap? {
// 1. 从内存缓存获取
memoryCache.get(url)?.let {
println("内存缓存命中")
return it
}
// 2. 从磁盘缓存获取
val key = hashKeyForDisk(url)
diskCache.get(key)?.let { snapshot ->
val bitmap = BitmapFactory.decodeStream(snapshot.getInputStream(0))
memoryCache.put(url, bitmap) // 写入内存缓存
println("磁盘缓存命中")
return bitmap
}
// 3. 从网络获取
println("网络请求")
val bitmap = downloadBitmap(url)
if (bitmap != null) {
memoryCache.put(url, bitmap)
saveToDiskCache(url, bitmap)
}
return bitmap
}
private fun saveToDiskCache(url: String, bitmap: Bitmap) {
val key = hashKeyForDisk(url)
val editor = diskCache.edit(key) ?: return
try {
val output = editor.newOutputStream(0)
bitmap.compress(Bitmap.CompressFormat.PNG, 100, output)
editor.commit()
} catch (e: Exception) {
editor.abort()
}
}
private fun hashKeyForDisk(url: String): String {
return MessageDigest.getInstance("MD5")
.digest(url.toByteArray())
.joinToString("") { "%02x".format(it) }
}
private fun downloadBitmap(url: String): Bitmap? {
// 网络下载
return null
}
}
// 缓存策略对比
/*
无缓存:
每次请求都从网络下载
流量:100MB
耗时:30秒
内存缓存:
第二次加载相同图片快速显示
流量:100MB(首次)
耗时:30秒(首次),0.1秒(后续)
磁盘缓存:
重启App后依然有效
流量:100MB(首次)
耗时:30秒(首次),0.5秒(磁盘读取)
三级缓存:
最优体验
流量:100MB(首次)
耗时:30秒(首次),0.1秒(内存),0.5秒(磁盘)
*/
6. IPv6支持
检测IPv6可用性
class IPv6Support {
fun isIPv6Available(): Boolean {
return try {
val interfaces = NetworkInterface.getNetworkInterfaces()
while (interfaces.hasMoreElements()) {
val networkInterface = interfaces.nextElement()
val addresses = networkInterface.inetAddresses
while (addresses.hasMoreElements()) {
val address = addresses.nextElement()
if (address is Inet6Address && !address.isLoopbackAddress) {
println("IPv6地址: ${address.hostAddress}")
return true
}
}
}
false
} catch (e: Exception) {
false
}
}
// DNS解析(支持IPv6)
fun resolveHost(hostname: String): List<InetAddress> {
return InetAddress.getAllByName(hostname).toList()
// 返回IPv4和IPv6地址
// [203.0.113.1, 2001:0db8:85a3::8a2e:0370:7334]
}
// 优先使用IPv6
fun preferIPv6(addresses: List<InetAddress>): InetAddress {
return addresses.firstOrNull { it is Inet6Address }
?: addresses.first()
}
}
// OkHttp自动支持IPv6
// 无需特殊配置
7. HTTP/2和HTTP/3
HTTP/2
// OkHttp默认支持HTTP/2
// HTTP/2特性验证
val client = OkHttpClient.Builder()
.protocols(listOf(Protocol.HTTP_2, Protocol.HTTP_1_1))
.build()
val request = Request.Builder()
.url("https://api.T8000.com/devices")
.build()
client.newCall(request).execute().use { response ->
println("协议: ${response.protocol}") // HTTP_2
// HTTP/2服务器推送
response.request.url.newBuilder().build()
}
// HTTP/2性能对比(10个并发请求)
/*
HTTP/1.1:
- 需要6个TCP连接(Chrome限制)
- 总耗时:2.5秒
- 连接开销大
HTTP/2:
- 只需1个TCP连接
- 总耗时:1.2秒
- 节省52%时间
*/
HTTP/3 (QUIC)
// OkHttp暂不支持HTTP/3
// Cronet库支持QUIC
// 添加依赖
implementation 'org.chromium.net:cronet-api:119.6045.31'
implementation 'org.chromium.net:cronet-common:119.6045.31'
// 使用Cronet
val cronetEngine = CronetEngine.Builder(context)
.enableQuic(true) // 启用QUIC
.build()
// HTTP/3 (QUIC) 优势:
// 1. 0-RTT连接(首次1-RTT,后续0-RTT)
// 2. 改进的拥塞控制
// 3. 连接迁移(Wi-Fi切换到4G不断开)
// 4. 基于UDP(避免TCP队头阻塞)
架构图/流程图
HttpDNS流程
┌──────────┐
│ App │
└────┬─────┘
│ 1. 解析api.T8000.com
▼
┌─────────────────┐
│ DNS缓存检查 │ --命中--> 返回IP
└────┬────────────┘
│ 未命中
▼
┌──────────────────────┐
│ HttpDNS服务器 │
│ (203.107.1.1) │
└────┬─────────────────┘
│ 2. HTTP请求
▼
┌──────────────────────┐
│ 返回IP + TTL │
│ {"ip":"203.0.113.1",│
│ "ttl":60} │
└────┬─────────────────┘
│ 3. 缓存结果
▼
┌─────────────────┐
│ 使用IP建立连接 │
└─────────────────┘
三级缓存架构
请求图片
↓
内存缓存(LruCache)
├─ 命中 → 返回Bitmap
└─ 未命中
↓
磁盘缓存(DiskLruCache)
├─ 命中 → 返回Bitmap → 写入内存缓存
└─ 未命中
↓
网络请求
↓
下载图片
↓
写入磁盘缓存 + 内存缓存
↓
返回Bitmap
面试高频问题
Q1: 如何优化App首次网络请求的速度?
答:首次请求延迟 = DNS解析 + TCP握手 + SSL握手 + HTTP请求,优化策略:
- DNS预解析:App启动时预解析核心域名,将DNS耗时(100-500ms)降为0
- 连接预热:启动时发送HEAD请求预建立TCP+SSL连接
- HttpDNS:绕过运营商LocalDNS,解析时间从200-500ms降至50-150ms
- HTTP/2:多路复用减少连接数,首次建连后所有请求共享一个连接
- IP直连:跳过DNS解析直接使用IP,需配合Host Header解决证书问题
Q2: 弱网环境下如何保证用户体验?
答:分层降级策略:
- 检测层:实时检测网络质量(RTT、丢包率、带宽),分为优/良/差/极差四级
- 请求层:动态调整超时时间(优30s→极差90s)、启用重试(指数退避)
- 数据层:降低图片质量、关闭预加载、使用缓存优先策略
- UI层:显示网络状态提示、支持离线模式、队列化操作(恢复网络后自动执行)
- 协议层:大数据传输使用断点续传、考虑使用QUIC(0-RTT连接、连接迁移)
Q3: HTTP/2相比HTTP/1.1有哪些优化?
答:
| 特性 | HTTP/1.1 | HTTP/2 |
|---|---|---|
| 传输格式 | 文本 | 二进制分帧 |
| 连接复用 | 每域名6个连接 | 一个连接多路复用 |
| 头部 | 每次发完整头部 | HPACK压缩(减少85%+) |
| 服务器推送 | 不支持 | 支持Server Push |
| 请求优先级 | 不支持 | 支持流优先级 |
| 队头阻塞 | 有(HTTP层) | 无HTTP层队头阻塞(仍有TCP层) |
Q4: 三级缓存的设计原理?
答:
- 内存缓存(LruCache):读取速度~0.1ms,App进程内有效,容量为最大内存的1/8
- 磁盘缓存(DiskLruCache):读取速度~5ms,App生命周期内有效,通常50-100MB
- 网络请求:读取速度>100ms,始终最新
命中策略:内存→磁盘→网络,每层命中后回写上层缓存
淘汰策略:LRU(最近最少使用),定期清理过期数据
Q5: Protobuf相比JSON有什么优势?什么时候该用?
答:
| 维度 | JSON | Protobuf |
|---|---|---|
| 格式 | 文本 | 二进制 |
| 体积 | 基准 | 减少60-80% |
| 解析速度 | 基准 | 快3-10倍 |
| 可读性 | 好 | 差(需要proto文件) |
| Schema | 无 | 强类型定义 |
| 兼容性 | 通用 | 需要双端集成 |
适用场景:高频请求(设备状态轮询)、大数据量传输、带宽敏感(移动网络),不适合调试阶段和简单API
最佳实践总结
网络优化检查清单
| 优化项 | 方案 | 预期收益 |
|---|---|---|
| DNS优化 | HttpDNS + 预解析 + 缓存 | 首次请求减少200-400ms |
| 连接优化 | 连接池 + 预热 + HTTP/2 | 后续请求减少350ms |
| 传输优化 | GZIP + Protobuf | 流量减少60-80% |
| 弱网优化 | 重试 + 降级 + 断点续传 | 弱网成功率提升50%+ |
| 缓存优化 | 三级缓存 + HTTP缓存 | 二次访问速度提升95%+ |
| 流量优化 | 图片压缩 + 增量更新 | 总流量减少40-60% |
更多推荐
所有评论(0)