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() } }
问题根源分析
- Room实体缺失主键:
Article作为Room数据库的实体类,必须定义主键才能让Room正常执行增删改查操作。当前代码注释掉了主键字段,导致数据库操作触发底层异常。 - hashCode方法引用不存在的字段:
Article类的hashCode()方法中调用了已被注释的id字段的hashCode(),会直接抛出NullPointerException,这是触发崩溃的直接原因。 - 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
相关产品推荐
相关产品推荐

