【Android PackageManager】使用PackageManager读取APK ICON时候的大坑

2,057 阅读2分钟

先来看一段常用的读取APK Icon的代码

public Drawable parseApkIcon(Context context, String apkFilePath){
     PackageManager packageManager = context.getPackageManager();
     PackageInfo packageInfo = packageManager.getPackageArchiveInfo(apkFilePath, 0);
     if(packageInfo == null){
          return null;
     }

     packageInfo.applicationInfo.sourceDir = apkFilePath;
     packageInfo.applicationInfo.publicSourceDir = apkFilePath;
     Drawable iconDrawable = packageInfo.applicationInfo.loadIcon(packageManager);
     return iconDrawable;
}

上述代码表面看没啥问题能够顺利的读取到APK文件的ICON,但是PackageManager却隐藏了一个细节,就是当无法读取到APK的Icon的时候PackageManager就会返回了一个公用的默认绿色机器人图标(相信凡是做Android应用开发的都见过这个图标)。

如果我们不考虑Bitmap回收的话这个是没有一点问题的(我们可能不考虑吗),当一旦考虑到Bitmap回收就会出问题,例如如下场景:

我们的应用要扫描所有本地APK文件,然后用列表显示给用户,列表中显示APP的名称和图标。

列表滚动的时候我们会依次启动异步任务去读取APK的图标,如果在这个列表中有多个APK都无法顺利读取到图标,那么这多个任务最终拿到的就会是同一张图片(不同的BitmapDrawable却指向同一个Bitmap)。

接下来在继续滚动的过程中如果其中某个请求由于某些原因取消了并且回收了它拿到的Bitmap,那么其它请求的Bitmap就也被回收了,但是其它请求并不知道自己拿到的Bitmap已经被回收了,解析来其它请求在显示图片的时候就会崩溃。

解决这个问题的关键点就是怎么判断拿到的icon是不是公用的默认图片,是的话就不要了。

通过查看loadIcon方法的源码我们发现,当读不到APK的icon的时候会通过packageManager.getDefaultActivityIcon()获取公用的默图标并返回,这下就好办了,我们只需比较一下就可以了,修改后的代码如下:

public Drawable parseApkIcon(Context context, String apkFilePath){
     PackageManager packageManager = context.getPackageManager();
     PackageInfo packageInfo = packageManager.getPackageArchiveInfo(apkFilePath, 0);
     if(packageInfo == null){
          return null;
     }

     packageInfo.applicationInfo.sourceDir = apkFilePath;
     packageInfo.applicationInfo.publicSourceDir = apkFilePath;
     Drawable iconDrawable = packageInfo.applicationInfo.loadIcon(packageManager);
     if(iconDrawable == null){
          return null;
     }

     if(iconDrawable instanceof BitmapDrawable && ((BitmapDrawable) iconDrawable).getBitmap() == ((BitmapDrawable) packageManager.getDefaultActivityIcon()).getBitmap()){
          return null;
     }

     return iconDrawable;
}

过滤了默认的图标问题就结束了吗?当然不!

首先PackageManager会缓存apk icon ,然后ActivityThread持有一个ResourcesManager,ResourcesManager会缓存Resources,而Resources又会缓存Drawable,于是乎在重重缓存之下即使我们拿到的是正常的ape icon 我们也不能回收它,因为你下次拿到的很可能是被你已回收的Drawable。

那么最终的解决办法就是拿到drawable后直接在本地建立缓存文件,将icon缓存到本地,下次就从本地读取,然后drawable用完就不管了,不做任何处理。