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

Android新闻APP保存文章时APK加载I/O错误排查求助

新闻APP保存文章时崩溃问题排查与解决方案

崩溃复现步骤

  • 展示新闻列表
  • 点击某条新闻进入ArticleFragment查看详情
  • 点击悬浮按钮(FAB)将文章保存到SavedNewsFragment时,APP崩溃,抛出以下错误:
Failed to open APK '/data/app/~~Binr6SVO9dmLCUcPgCpAyA==/com.example.newspaper-yFV7fjvEt5i0gI4--yMcTA==/base.apk': I/O error
Failed to open APK '/data/app/~~Binr6SVO9dmLCUcPgCpAyA==/com.example.newspaper-yFV7fjvEt5i0gI4--yMcTA==/base.apk': I/O error
failed to add asset path '/data/app/~~Binr6SVO9dmLCUcPgCpAyA==/com.example.newspaper-yFV7fjvEt5i0gI4--yMcTA==/base.apk'
java.io.IOException: Failed to load asset path /data/app/~~Binr6SVO9dmLCUcPgCpAyA==/com.example.newspaper-yFV7fjvEt5i0gI4--yMcTA==/base.apk
 at android.content.res.ApkAssets.nativeLoad(Native Method)
 at android.content.res.ApkAssets.<init>(ApkAssets.java:295)
 at android.content.res.ApkAssets.loadFromPath(ApkAssets.java:144)
 at android.app.ResourcesManager.loadApkAssets(ResourcesManager.java:454)
 at android.app.ResourcesManager.access$000(ResourcesManager.java:72)
 at android.app.ResourcesManager$ApkAssetsSupplier.load(ResourcesManager.java:168)
 at android.app.ResourcesManager.createAssetManager(ResourcesManager.java:530)
 at android.app.ResourcesManager.createResourcesImpl(ResourcesManager.java:612)
 at android.app.ResourcesManager.findOrCreateResourcesImplForKeyLocked(ResourcesManager.java:664)
 at android.app.ResourcesManager.createResources(ResourcesManager.java:1011)
 at android.app.ResourcesManager.getResources(ResourcesManager.java:1114)
 at android.app.ActivityThread.getTopLevelResources(ActivityThread.java:2414)
 at android.app.ApplicationPackageManager.getResourcesForApplication(ApplicationPackageManager.java:1751)
 at android.app.ApplicationPackageManager.getResourcesForApplication(ApplicationPackageManager.java:1737)
 at android.app.ApplicationPackageManager.getDrawable(ApplicationPackageManager.java:1506)
 at android.app.ApplicationPackageManager.loadUnbadgedItemIcon(ApplicationPackageManager.java:3029)
 at android.content.pm.PackageItemInfo.loadUnbadgedIcon(PackageItemInfo.java:290)
 at com.android.systemui.toast.SystemUIToast.getBadgedIcon(SystemUIToast.java:284)
 at com.android.systemui.toast.SystemUIToast.inflateToastView(SystemUIToast.java:198)
 at com.android.systemui.toast.SystemUIToast.<init>(SystemUIToast.java:90)
 at com.android.systemui.toast.SystemUIToast.<init>(SystemUIToast.java:77)
 at com.android.systemui.toast.ToastFactory.createToast(ToastFactory.java:78)
 at com.android.systemui.toast.ToastUI.lambda$showToast$0(ToastUI.java:113)
 at com.android.systemui.toast.ToastUI.$r8$lambda$w_gPCh3F8Xxn1jN4lkQZoUci71c(Unknown Source:0)
 at com.android.systemui.toast.ToastUI$$ExternalSyntheticLambda0.run(Unknown Source:16)
 at com.android.systemui.toast.ToastUI.showToast(ToastUI.java:140)
 at com.android.systemui.statusbar.CommandQueue$H.handleMessage(CommandQueue.java:1441)
 at android.os.Handler.dispatchMessage(Handler.java:106)
 at android.os.Looper.loopOnce(Looper.java:201)
 at android.os.Looper.loop(Looper.java:288)
 at android.app.ActivityThread.main(ActivityThread.java:7842)
 at java.lang.reflect.Method.invoke(Native Method)
 at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
 at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003)

相关代码文件

NewsViewModel

class NewsViewModel(val repo : NewsRepository) : ViewModel()
{
    val breakingNews: MutableLiveData<Resource<NewsResponse>> = MutableLiveData()
    val breakingNewsPage = 1        // 后续更新

    val searchNews: MutableLiveData<Resource<NewsResponse>> = MutableLiveData()
    val searchNewsPage = 1        // 后续更新

    var savedNews: MutableLiveData<List<Article>> = MutableLiveData()

    init {
        getBreakingNews()
    }

/**BreakingNewsFragment相关**/
    private fun getBreakingNews(countryString : String = "eg") = viewModelScope.launch {
        breakingNews.postValue(Resource.Loading())
        val response = repo.getBreakingNews(countryString, breakingNewsPage)
        breakingNews.postValue(handleBreakingNewsResponse(response))
    }

    private fun handleBreakingNewsResponse(response: Response<NewsResponse>) : Resource<NewsResponse>
    {
        if(response.isSuccessful)
            response.body().let {
                return Resource.Success(it)
            }
        else
            return Resource.Error(response.message(), response.body())
    }

/**SearchNewsFragment相关**/
    fun searchForNews(text : String) = viewModelScope.launch {
        searchNews.postValue(Resource.Loading())
        val response = repo.getSearchedNews(text, searchNewsPage)
        searchNews.postValue(handleSearchForNewsResponse(response))
    }

    private fun handleSearchForNewsResponse(response: Response<NewsResponse>) : Resource<NewsResponse>
    {
        if(response.isSuccessful)
            response.body().let {
                return Resource.Success(it)
            }
        else
            return Resource.Error(response.message(), response.body())
    }

/**Room数据库操作**/
    fun saveThisArticle(article: Article) = viewModelScope.launch {
        repo.addIfNotExist(article)
        /*Log.d("NewsViewModel "," => 开始保存")
        val num = repo.addIfNotExist(article).wait()
        Log.d("NewsViewModel => saveThisArticle(article: Article) ", "--------- $num")*/
    }

    fun getMySavedArticles() = savedNews.postValue(repo.getSavedNews().value)

    fun deleteThisArticle(article: Article) = viewModelScope.launch {
        repo.deleteArticle(article)
    }


    class NewsVMFactory(val repo: NewsRepository) : ViewModelProvider.Factory {
        override fun <T : ViewModel> create(modelClass: Class<T>): T {
            if(modelClass.isAssignableFrom(NewsViewModel::class.java)){
                @Suppress("UNCHECKED_CAST")
                return NewsViewModel(repo) as T
            }
            throw IllegalArgumentException("未知的ViewModel类")
        }
    }
}

NewsRepository

class NewsRepository(val db: ArticleDatabase)
{
    /**远程数据请求**/
    suspend fun getBreakingNews(country_of_2Letters : String, pageNum : Int) =
        RetrofitInstance.newsAPI.getNewsHeadlines()

    suspend fun getSearchedNews(text : String, pageNum : Int) =
        RetrofitInstance.newsAPI.searchFor(text, pageNum)

    /**本地数据库操作**/
    suspend fun addIfNotExist(article : Article) =
        withContext(Dispatchers.IO) {
            db.newsDatabaseDao.upsert(article)
    }

    fun getSavedNews() = db.newsDatabaseDao.getAllArticles()

    suspend fun deleteArticle(article : Article) = db.newsDatabaseDao.deleteArticle(article)

}

NewsDao接口

@Dao
interface NewsDao {

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun upsert(article : Article): Long      // TODO 改为Int?

    @Query("SELECT * FROM articles")
    fun getAllArticles(): LiveData<List<Article>>   // TODO 后续会分两张表:一张存已保存文章,一张存缓存文章

    @Delete
    suspend fun deleteArticle(article: Article)

}

ArticleDatabase

@Database(entities = [Article::class], version = 1, exportSchema = false)
@TypeConverters(Converters::class)
abstract class ArticleDatabase : RoomDatabase() {                                   

    abstract val newsDatabaseDao: NewsDao       

    companion object {

        @Volatile
        var INSTANCE: ArticleDatabase? = null

        fun getInstance(context: Context): ArticleDatabase {
            synchronized(this) {
                return INSTANCE ?: Room.databaseBuilder(
                    context.applicationContext,
                    ArticleDatabase::class.java,
                    "article_database"
                )
                    .fallbackToDestructiveMigration()
                    .build()
            }
        }

    }
}

数据模型文件

data class NewsResponse(
    val articles: List<Article>,
    val status: String, // ok
    val totalResults: Int // 383
)

@Entity(tableName = "articles")
data class Article(
  //  @PrimaryKey(autoGenerate = true)
  //  val id : Int? = null,      // 尝试过:var,Int,Int = 0,Int?
    @ColumnInfo(name = "author")
    val author: String? = "",
    @ColumnInfo(name = "content")
    val content: String? = "",
    @ColumnInfo(name = "description")
    val description: String? = "",
    @ColumnInfo(name = "published_at")
    val publishedAt: String, 
    @ColumnInfo(name = "source")
    val source: Source,
    @ColumnInfo(name = "title")
    val title: String,
    @ColumnInfo(name = "url")
    val url: String, 
    @ColumnInfo(name = "url_to_image")
    val urlToImage: String?
) : Serializable
{
    override fun hashCode(): Int {
        var result = id.hashCode()
        if(url.isNullOrEmpty()){
            result = 31 * result + url.hashCode()
        }
        return result
    }
}

data class Source(
    val id: String, // business-insider
    val name: String // The Guardian
)

Converters类

class Converters {

//    data class Source(
//    val id: String,
//    val name: String
//    )

    @TypeConverter
    fun fromSource(source: Source): String =
        Gson().toJson(source)      // 以字符串形式存入数据库

    @TypeConverter
    fun toSource(stringSource: String) : Source =
        Gson().fromJson(stringSource, Source::class.java)      // 从数据库取出并还原为原数据类型
}

ArticleFragment

class ArticleFragment : BaseFragment(R.layout.fragment_article) {

    lateinit var binding : FragmentArticleBinding
    val args : ArticleFragmentArgs by navArgs()

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        binding = FragmentArticleBinding.inflate(inflater)

        val data = args.anArticle

        binding.webView.apply {
            try{
                webViewClient = WebViewClient()
                    loadUrl(data.url)
            }
            catch (e: Exception){
                loadUrl("https://www.google.co.in/")
                Toast.makeText(context, "链接存在问题", Toast.LENGTH_LONG).show()
            }
        }

        binding.fab.setOnClickListener {
            viewModel.saveThisArticle(data)
            Toast.makeText(context, "保存成功 ✅", Toast.LENGTH_SHORT).show()
        }

        return binding.root
    }


    override fun onDestroyView() {
        super.onDestroyView()
        binding.unbind()
    }
}

问题根源分析

  1. Room实体缺失主键:Article作为Room数据库的实体类,必须定义主键才能让Room正常执行增删改查操作。当前代码注释掉了主键字段,导致数据库操作触发底层异常。
  2. hashCode方法引用不存在的字段:Article类的hashCode()方法中调用了已被注释的id字段的hashCode(),会直接抛出NullPointerException,这是触发崩溃的直接原因。
  3. Toast上下文不安全:Fragment中使用context可能为空,导致Toast初始化时无法正确加载应用资源,进而出现APK资源加载IO错误。

解决方案

1. 修复Article实体的主键定义

恢复并正确配置主键:

@Entity(tableName = "articles")
data class Article(
    @PrimaryKey(autoGenerate = true)
    val id: Int = 0, // 自增主键,默认值0不影响自增逻辑
    @ColumnInfo(name = "author")
    val author: String? = "",
    @ColumnInfo(name = "content")
    val content: String? = "",
    @ColumnInfo(name = "description")
    val description: String? = "",
    @ColumnInfo(name = "published_at")
    val publishedAt: String, 
    @ColumnInfo(name = "source")
    val source: Source,
    @ColumnInfo(name = "title")
    val title: String,
    @ColumnInfo(name = "url")
    val url: String, 
    @ColumnInfo(name = "url_to_image")
    val urlToImage: String?
) : Serializable

2. 修复hashCode方法

移除对不存在字段的引用,改用有效字段生成hashCode:

override fun hashCode(): Int {
    var result = id.hashCode()
    url?.let { result = 31 * result + it.hashCode() }
    return result
}

3. 确保Toast上下文安全

使用requireContext()替代context,保证上下文不为空:

binding.fab.setOnClickListener {
    viewModel.saveThisArticle(data)
    Toast.makeText(requireContext(), "保存成功 ✅", Toast.LENGTH_SHORT).show()
}

4. 可选:添加数据库操作异常捕获

在ViewModel中捕获数据库操作异常,避免异常扩散到UI层:

fun saveThisArticle(article: Article) = viewModelScope.launch {
    try {
        repo.addIfNotExist(article)
    } catch (e: Exception) {
        Log.e("NewsViewModel", "保存文章失败", e)
        // 可添加LiveData通知UI层保存失败
    }
}

内容的提问来源于stack exchange,提问作者fares hassan

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.14 23:15:53