You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android Kotlin库中String扩展复用ArrayList的单例Context问题

解决方案:安全复用ArrayList的String扩展方法(基于Context加载)

嘿,这个问题我之前做Android Library的时候也碰到过!核心是要解决单例安全持有Context同时只加载一次ArrayList的问题,毕竟频繁调用扩展方法,重复加载太浪费性能了。我给你几个实用的方案,都是经过项目验证的:

方案1:ApplicationContext注入式单例(适合需调用方主动初始化的场景)

这种方案需要Library的使用者在Application初始化时传入ApplicationContext,好处是逻辑清晰,可控性强。

第一步:改造单例类,安全持有ApplicationContext

我们用双重校验锁实现单例,并且只持有ApplicationContext(避免内存泄漏),同时用by lazy延迟加载ArrayList,确保只初始化一次:

class StringSearchManager private constructor(private val appContext: Context) {
    // 延迟加载列表,线程安全且仅初始化一次
    val searchList: ArrayList<String> by lazy {
        loadSearchListFromSource(appContext)
    }

    companion object {
        @Volatile
        private var instance: StringSearchManager? = null

        // 对外暴露初始化方法,要求传入ApplicationContext
        fun initialize(appContext: Context) {
            if (instance == null) {
                synchronized(this) {
                    instance ?: StringSearchManager(appContext.applicationContext).also { instance = it }
                }
            }
        }

        // 获取单例实例,确保已初始化
        fun getInstance(): StringSearchManager {
            return checkNotNull(instance) {
                "StringSearchManager not initialized! Call initialize() with ApplicationContext first."
            }
        }
    }

    // 这里写你的列表加载逻辑,比如从assets、raw资源读取
    private fun loadSearchListFromSource(context: Context): ArrayList<String> {
        return try {
            context.assets.open("search_dictionary.txt")
                .bufferedReader()
                .readLines()
                .toCollection(ArrayList())
        } catch (e: Exception) {
            // 加载失败返回空列表或做错误处理
            ArrayList()
        }
    }
}

第二步:实现String扩展方法

扩展方法直接调用单例的已加载列表,不需要每次都初始化:

fun String.searchMatchingItem(): String? {
    val searchList = StringSearchManager.getInstance().searchList
    // 这里替换成你的搜索逻辑,比如模糊匹配、精确匹配
    return searchList.firstOrNull { it.contains(this, ignoreCase = true) }
}

第三步:提醒调用方初始化

在Library的文档里说明,需要在Application的onCreate()中初始化:

class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        StringSearchManager.initialize(this)
    }
}

方案2:ContentProvider自动初始化(适合无侵入式Library)

如果不想让调用方手动初始化,Android的ContentProvider机制可以帮我们在App启动时自动完成初始化,完全无侵入。

第一步:改造单例类,支持内部初始化

调整单例,新增内部初始化方法,对外暴露无参的getInstance:

class StringSearchManager private constructor(private val appContext: Context) {
    val searchList: ArrayList<String> by lazy {
        loadSearchListFromSource(appContext)
    }

    companion object {
        @Volatile
        private var instance: StringSearchManager? = null

        // 供ContentProvider内部调用的初始化方法
        internal fun initialize(appContext: Context) {
            if (instance == null) {
                synchronized(this) {
                    instance ?: StringSearchManager(appContext.applicationContext).also { instance = it }
                }
            }
        }

        fun getInstance(): StringSearchManager {
            return checkNotNull(instance) {
                "StringSearchManager failed to initialize! Check if the ContentProvider is registered."
            }
        }
    }

    private fun loadSearchListFromSource(context: Context): ArrayList<String> {
        // 同方案1的加载逻辑
        return try {
            context.assets.open("search_dictionary.txt")
                .bufferedReader()
                .readLines()
                .toCollection(ArrayList())
        } catch (e: Exception) {
            ArrayList()
        }
    }
}

第二步:创建初始化用的ContentProvider

ContentProvider会在App启动时被系统实例化,我们可以在这里完成单例的初始化:

class SearchListInitProvider : ContentProvider() {
    override fun onCreate(): Boolean {
        context?.let {
            // 初始化单例,传入ApplicationContext
            StringSearchManager.initialize(it.applicationContext)
        }
        return true
    }

    // 以下方法不需要实现,返回默认值即可
    override fun query(uri: Uri, projection: Array<String>?, selection: String?, selectionArgs: Array<String>?, sortOrder: String?): Cursor? = null
    override fun getType(uri: Uri): String? = null
    override fun insert(uri: Uri, values: ContentValues?): Uri? = null
    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int = 0
    override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<String>?): Int = 0
}

第三步:在Library的Manifest中注册Provider

在你的Library模块的AndroidManifest.xml中添加这个Provider的注册:

<provider
    android:name=".SearchListInitProvider"
    android:authorities="${applicationId}.search_list_init_provider"
    android:exported="false" />

第四步:实现String扩展方法

和方案1一样,直接调用单例即可,调用方无需任何额外操作:

fun String.searchMatchingItem(): String? {
    val searchList = StringSearchManager.getInstance().searchList
    return searchList.firstOrNull { it.equals(this, ignoreCase = true) }
}

关键注意事项

  • 永远用ApplicationContext:单例是全局生命周期,持有Activity/Fragment的Context会导致内存泄漏,必须用applicationContext。
  • lazy加载确保只初始化一次:by lazy是线程安全的,保证ArrayList只在第一次使用时加载,之后复用。
  • 错误处理:加载列表时要捕获异常(比如文件不存在、读取失败),避免Crash。

内容的提问来源于stack exchange,提问作者Duncan Hill

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.21 06:40:30