You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Android正常联网状态下触发UnknownHostException的问题排查与解决方案咨询

针对三星/小米后台网络UnknownHostException的优化方案

首先,这个问题是国内定制ROM后台资源回收机制导致的典型问题——系统会清理长时间后台应用的DNS缓存、OkHttp连接池等网络相关资源,而OkHttp默认依赖的系统DNS无法自动恢复,最终引发域名解析失败。你的临时方案虽然能解决问题,但重启Activity的体验并不好,我给你几个更优雅的优化方向:

一、核心问题分析

UnknownHostException的根源是:

  • 三星/小米的后台清理机制会回收应用的系统DNS缓存OkHttp底层套接字资源
  • OkHttp默认使用DnsSystem(系统DNS),资源被回收后无法自动重建解析能力
  • 全局单例的OkHttpClient/Retrofit在后台长时间驻留后,持有已失效的网络资源引用

二、具体优化方案

1. 自定义DNS实现,绕过系统缓存限制

系统DNS缓存是重灾区,我们可以直接实现自己的DNS解析逻辑,避免依赖系统缓存:

class CustomDns : Dns {
    override fun lookup(hostname: String): List<InetAddress> {
        return try {
            // 直接调用底层解析方法,跳过系统可能失效的缓存
            InetAddress.getAllByName(hostname).toList()
        } catch (e: UnknownHostException) {
            // 失败时尝试备用公共DNS(比如谷歌8.8.8.8)
            runCatching {
                // 手动指定DNS服务器进行解析
                val resolver = InetAddress.getByName("8.8.8.8")
                val lookup = InetAddress.getAllByName(hostname)
                lookup.toList()
            }.getOrElse {
                // 仍失败则抛出原异常
                throw e
            }
        }
    }
}

然后在OkHttpClient中配置这个自定义DNS:

// 在NetworkModule的client方法中添加
OkHttpClient().newBuilder().run {
    // ...其他现有配置
    dns(CustomDns()) // 替换默认DNS
    build()
}

2. 给OkHttp添加重试逻辑+优化连接池

针对解析失败的情况,添加重试机制,同时调整连接池参数避免资源被快速回收:

// 自定义连接池,延长空闲连接存活时间
private val customConnectionPool = ConnectionPool(
    maxIdleConnections = 10,
    keepAliveDuration = 30,
    timeUnit = TimeUnit.MINUTES
)

// 在OkHttpClient构建时添加重试拦截器
OkHttpClient().newBuilder().run {
    // ...其他配置
    connectionPool(customConnectionPool)
    addInterceptor { chain ->
        val originalRequest = chain.request()
        var retryCount = 0
        val maxRetry = 3
        var lastException: IOException? = null

        while (retryCount < maxRetry) {
            try {
                val response = chain.proceed(originalRequest)
                if (response.isSuccessful) {
                    return@addInterceptor response
                }
            } catch (e: UnknownHostException) {
                lastException = e
                retryCount++
                // 重试前短暂休眠,给系统网络资源恢复时间
                Thread.sleep(500)
            }
        }
        throw lastException ?: IOException("Network request failed after max retries")
    }
    build()
}

3. 监听应用前后台,主动重置网络组件

当应用从后台回到前台时,主动重建OkHttpClient/Retrofit实例,抛弃已失效的资源:

第一步:实现前后台监听器

class AppLifecycleObserver(private val onForegroundChanged: (Boolean) -> Unit) : Application.ActivityLifecycleCallbacks {
    private var foregroundActivityCount = 0
    private var isInForeground = false

    override fun onActivityResumed(activity: Activity) {
        foregroundActivityCount++
        if (!isInForeground && foregroundActivityCount > 0) {
            isInForeground = true
            onForegroundChanged(true)
        }
    }

    override fun onActivityPaused(activity: Activity) {
        foregroundActivityCount--
        if (isInForeground && foregroundActivityCount <= 0) {
            isInForeground = false
            onForegroundChanged(false)
        }
    }

    // 其他生命周期方法空实现即可
    override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}
    override fun onActivityStarted(activity: Activity) {}
    override fun onActivityStopped(activity: Activity) {}
    override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
    override fun onActivityDestroyed(activity: Activity) {}
}

第二步:在Application中注册并重置网络组件

class MyApplication : Application() {
    // 用Provider来获取可重建的OkHttpClient实例,替代Hilt的全局单例
    private val okHttpClientProvider: Provider<OkHttpClient> = Provider { createOkHttpClient() }
    private var currentRetrofit: Retrofit? = null

    override fun onCreate() {
        super.onCreate()
        registerActivityLifecycleCallbacks(AppLifecycleObserver { isForeground ->
            if (isForeground) {
                // 回到前台时,重建OkHttpClient和Retrofit
                val newClient = okHttpClientProvider.get()
                currentRetrofit = Retrofit.Builder()
                    .baseUrl(getString(R.string.base_url))
                    .addConverterFactory(GsonConverterFactory.create(Gson()))
                    .client(newClient)
                    .build()
                // 这里可以通过LiveData或其他方式通知ViewModel使用新的Retrofit实例
            }
        })
    }

    private fun createOkHttpClient(): OkHttpClient {
        // 复制你NetworkModule中的client构建逻辑,确保包含自定义DNS和重试拦截器
        val authInterceptor = provideAuthInterceptor()
        val loggingInterceptor = httpLoggingInterceptor()
        val netLoggingInterceptor = providesNetLoggingInterceptor()
        return OkHttpClient().newBuilder().run {
            addInterceptor(authInterceptor)
            addInterceptor(loggingInterceptor)
            addInterceptor(PlutoInterceptor())
            addInterceptor(netLoggingInterceptor)
            connectTimeout(2, TimeUnit.MINUTES)
            readTimeout(2, TimeUnit.MINUTES)
            dns(CustomDns())
            connectionPool(customConnectionPool)
            build()
        }
    }

    // 供ViewModel获取最新的Retrofit实例
    fun getCurrentRetrofit(): Retrofit = currentRetrofit ?: createOkHttpClient().let {
        Retrofit.Builder()
            .baseUrl(getString(R.string.base_url))
            .addConverterFactory(GsonConverterFactory.create(Gson()))
            .client(it)
            .build()
    }
}

注意Hilt适配:

如果继续用Hilt,不建议用SingletonComponent,可以改用ActivityRetainedComponent(和ViewModel生命周期绑定),或者使用@Reusable注解让组件可以被重建,避免全局单例的资源失效问题。

4. 厂商特殊适配(可选)

引导用户将应用加入厂商的后台白名单,从根源避免资源被清理:

  • 在应用设置页添加引导文案,告诉用户如何关闭三星的“电池优化”或小米的“自启动限制”
  • 可以调用系统API检测是否在白名单中,若不在则弹出引导弹窗

三、方案优先级推荐

  1. 优先尝试方案1+方案2:自定义DNS+重试逻辑,实现简单,不需要修改组件生命周期,能解决绝大多数场景的问题
  2. 如果仍有问题,再叠加方案3:前后台监听重置网络组件,彻底避免失效资源的影响
  3. 方案4作为补充,提升长期稳定性

你的临时方案可以作为最后兜底,但优先用上面的方案优化用户体验。

内容的提问来源于stack exchange,提问作者Ammar Yasser

火山引擎 最新活动