在开发一款文件管理器,出现一个难题:因为要显示apk文件的图标,导致在拔出U盘的时候进程被杀,继而crash。
本文就是针对此问题,提出我的解决办法。
首先来看一下crash的log:
W/Vold ( 139): Failed to unmount /storage/udisk2 (Device or resource busy, retries 9, action 0)
E/ProcessKiller( 139): Process com.xxx.filemanager (2426) has open file /mnt/media_rw/udisk2/apk/Test.apk
W/Vold ( 139): Failed to unmount /storage/udisk2 (Device or resource busy, retries 8, action 0)
E/ProcessKiller( 139): Process com.xxx.filemanager (2426) has open file /mnt/media_rw/udisk2/apk/Test.apk
W/Vold ( 139): Failed to unmount /storage/udisk2 (Device or resource busy, retries 7, action 0)
I/ProcessorService( 1018): sleep, syncing time, now:91842237
E/ProcessKiller( 139): Process com.xxx.filemanager (2426) has open file /mnt/media_rw/udisk2/apk/Test.apk
W/Vold ( 139): Failed to unmount /storage/udisk2 (Device or resource busy, retries 6, action 0)
D/dalvikvm( 612): GC_FOR_ALLOC freed 526K, 29% free 1604K/2260K, paused 12ms, total 12ms
E/ProcessKiller( 139): Process com.xxx.filemanager (2426) has open file /mnt/media_rw/udisk2/apk/Test.apk
W/Vold ( 139): Failed to unmount /storage/udisk2 (Device or resource busy, retries 5, action 0)
E/ProcessKiller( 139): Process com.xxx.filemanager (2426) has open file /mnt/media_rw/udisk2/apk/Test.apk
I/CRASHLOG( 154): monitorDiskSpace : Percent of Using Data partition = 3 %
W/Vold ( 139): Failed to unmount /storage/udisk2 (Device or resource busy, retries 4, action 0)
E/ProcessKiller( 139): Process com.xxx.filemanager (2426) has open file /mnt/media_rw/udisk2/apk/Test.apk
W/Vold ( 139): Failed to unmount /storage/udisk2 (Device or resource busy, retries 3, action 0)
D/dalvikvm( 612): GC_FOR_ALLOC freed 513K, 30% free 1603K/2260K, paused 13ms, total 13ms
E/ProcessKiller( 139): Process com.xxx.filemanager (2426) has open file /mnt/media_rw/udisk2/apk/Test.apk
W/Vold ( 139): Failed to unmount /storage/udisk2 (Device or resource busy, retries 2, action 1)
E/ProcessKiller( 139): Process com.xxx.filemanager (2426) has open file /mnt/media_rw/udisk2/apk/Test.apk
W/ProcessKiller( 139): Sending SIGHUP to process 2426
I/WindowState( 374): WIN DEATH: Window{a60d6c50 u0 com.xxx.filemanager/com.xxx.filemanager.FileActivity}
I/ActivityManager( 374): Process com.xxx.filemanager (pid 2426) has died.
W/ActivityManager( 374): Force removing ActivityRecord{a5d80268 u0 com.xxx.filemanager/.FileActivity t5}: app died, no saved state
V/PhoneStatusBar( 532): onReceive: title=Launcher3 appname=Launcher3
V/Activity( 1045): onResume com.android.launcher3.Launcher@a5d881d8 title=Launcher3 applicationName=Launcher3
E/BufferQueue( 141): [com.android.launcher3/com.android.launcher3.Launcher] dequeueBuffer: can't dequeue multiple buffers without setting the buffer count
I/DecodeHelper( 812): reset
W/InputMethodManagerService( 374): Got RemoteException sending setActive(false) notification to pid 2426 uid 10049
D/Zygote ( 142): Process 2426 terminated by signal (15)
I/Vold ( 139): /storage/udisk2 sucessfully unmounted
I/Vold ( 139): /mnt/media_rw/udisk2 sucessfully unmounted
I/Vold ( 139): /mnt/media_rw/udisk2 unmounted successfully
D/Vold ( 139): Volume udisk2 state changing 5 (Unmounting) -> 1 (Idle-Unmounted)
D/DirectVolume( 139): Crisis averted
D/DirectVolume( 139): Volume udisk2 /mnt/media_rw/udisk2 disk 8:0 removed
D/Vold ( 139): Volume udisk2 state changing 1 (Idle-Unmounted) -> 0 (No-Media)
我们来分析一下log:
ProcessKiller( 139): Process com.xxx.filemanager (2426) has open file /mnt/media_rw/udisk2/apk/Test.apk
log中可以看出来,拔出U盘的时候,因为filemanager(我的文件管理器)占用了U盘中的Test.apk文件,导致process被系统kill了,所以app就挂了。
我们就先来看看,到底是哪里占用了apk文件资源:
首先,加载未安装apk图标的方式我知道的有三种:
-
- 纯反射;
- 调用PackageParser(隐藏api)
- 调用PackageManager(最简单好用)
-
后来研究了一下PackageManager、PackageInfo、PackageParser、ApplicationInfo、Resources 和 AssetManager 的源码,我发现问题就在于AssetManager,也只有AssetManager存在close()方法,然后就解决了问题。
-
我们来一一看一下三种方法(详细加载apk图标和防止crash的办法都写在下面方法的注释里了):
private static final String PACKAGE_PARSER = "android.content.pm.PackageParser";
private static final String ASSET_MANAGER = "android.content.res.AssetManager";
public static Drawable getUninstallApkIcon(Context context, String archiveFilePath) {
Drawable icon = null;
try {
Class pkgParserCls = Class.forName(PACKAGE_PARSER);
Class[] typeArgs = new Class[1];
typeArgs[0] = String.class;
Constructor pkgParserCt = pkgParserCls.getConstructor(typeArgs);
Object[] valueArgs = new Object[1];
valueArgs[0] = archiveFilePath;
Object pkgParser = pkgParserCt.newInstance(valueArgs);
DisplayMetrics metrics = new DisplayMetrics();
metrics.setToDefaults();
typeArgs = new Class[4];
typeArgs[0] = File.class;
typeArgs[1] = String.class;
typeArgs[2] = DisplayMetrics.class;
typeArgs[3] = Integer.TYPE;
Method pkgParser_parsePackageMtd = pkgParserCls.getDeclaredMethod("parsePackage", typeArgs);
valueArgs = new Object[4];
valueArgs[0] = new File(archiveFilePath);
valueArgs[1] = archiveFilePath;
valueArgs[2] = metrics;
valueArgs[3] = 0;
Object pkgParserPkg = pkgParser_parsePackageMtd.invoke(pkgParser, valueArgs);
Field appInfoFld = pkgParserPkg.getClass().getDeclaredField("applicationInfo");
ApplicationInfo info = (ApplicationInfo) appInfoFld.get(pkgParserPkg);
Class assetMagCls = Class.forName(ASSET_MANAGER);
Constructor assetMagCt = assetMagCls.getConstructor((Class[]) null);
Object assetMag = assetMagCt.newInstance((Object[]) null);
typeArgs = new Class[1];
typeArgs[0] = String.class;
Method assetMag_addAssetPathMtd = assetMagCls.getDeclaredMethod("addAssetPath", typeArgs);
valueArgs = new Object[1];
valueArgs[0] = archiveFilePath;
//通过发射获取AssetManager.close()方法
Method assetMag_close = assetMagCls.getDeclaredMethod("close");
Resources res = context.getResources();
typeArgs = new Class[3];
typeArgs[0] = assetMag.getClass();
typeArgs[1] = res.getDisplayMetrics().getClass();
typeArgs[2] = res.getConfiguration().getClass();
Constructor resCt = Resources.class.getConstructor(typeArgs);
valueArgs = new Object[3];
valueArgs[0] = assetMag;
valueArgs[1] = res.getDisplayMetrics();
valueArgs[2] = res.getConfiguration();
res = (Resources) resCt.newInstance(valueArgs);
icon = ((BitmapDrawable) res.getDrawable(info.icon));
//在此调用AssetManager.close()方可治愈crash的毛病
assetMag_close.invoke(assetMag);
} catch (Exception e) {
e.printStackTrace();
}
return icon;
}
PackageParser属于隐藏api,需要签名,不适合通用app,不过最适合于我的app。
-
要想调用隐藏api,你需要使用源码编译出的一个jar包:
out\target\common\obj\JAVA_LIBRARIES\framework_intermediates\classes.jar
-
eclipse:右键app目录 -> Properties-> Java Build Path -> Libraries -> Add Library -> User Library(起个名字,比如:framework) -> 导入classes.jar -> Order and Export -> 选中framework(起的名字)-> up (让framework位于系统sdk(比如:Android 5.1)之上)-> ok
这样就可以在eclipse里直接调用隐藏api了。
public static Drawable getUninstallApkIcon(Context context, String apkPath) {
PackageParser packageParser = new PackageParser(apkPath);
DisplayMetrics metrics = new DisplayMetrics();
metrics.setToDefaults();
PackageParser.Package mPkgInfo = packageParser.parsePackage(new File(apkPath), apkPath, metrics, 0);
ApplicationInfo apinfo = mPkgInfo.applicationInfo;
Resources pRes = context.getResources();
AssetManager assmgr = new AssetManager();
assmgr.addAssetPath(apkPath);
Resources res = new Resources(assmgr, pRes.getDisplayMetrics(), pRes.getConfiguration());
if (apinfo.icon != 0) {
Drawable icon = res.getDrawable(apinfo.icon);
//在此调用AssetManager.close()方可治愈crash的毛病
assmgr.close();
return icon;
}
return null;
}
public static Drawable getUninstallApkIcon(Context context, String apkPath) {
Resources res = getResource(context, apkPath);
PackageManager pm = context.getPackageManager();
PackageInfo info = pm.getPackageArchiveInfo(apkPath, PackageManager.GET_ACTIVITIES);
ApplicationInfo appInfo = info.applicationInfo;
if (appInfo.icon != 0) {
Drawable icon = res.getDrawable(appInfo.icon);
//AssetManager must be closed to avoid ProcessKiller after unmounting usb disk.
res.getAssets().close();
return icon;
}
return null;
}
public static Resources getResource(Context context, String apkPath) {
AssetManager assetManager = createAssetManager(apkPath);
return new Resources(assetManager, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());
}
//利用反射调用AssetManager的addAssetPath()方法
private static AssetManager createAssetManager(String apkPath) {
try {
AssetManager assetManager = AssetManager.class.newInstance();
AssetManager.class.getDeclaredMethod("addAssetPath", String.class).invoke(
assetManager, apkPath);
return assetManager;
} catch (Throwable th) {
th.printStackTrace();
}
return null;
}
请apk图标容易,送apk图标不容易,一切都是因为AssetManager没有关好门,不关门的话,系统就要把你杀掉!
这个问题比较冷门,google也没帮我找到解决办法,只有自己动手,丰衣足食了。