Android 獲取 PackageInfo 引發 Crash 填坑
一般 Android 通過PackageInfo這個類來獲取應用安裝包信息,比如應用內包含的所有Activity名稱、應用版本號之類的。PackageInfo通過PackageManager來獲取,代碼如下:
PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
比如我們要獲取應用版本號時:
public static int getVersionCode(Context context) { PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); return info.versionCode; }
Tip: 獲取應用自身版本號,推薦使用BuildConfig.VERSION_CODE 方式,這里只是為了方便舉例說明問題。
一般情況下,上面的方法是可以正常拿到數據的,但是在某些情況下這也可能會引發 java.lang.RuntimeException: Package manager has died 異常。
java.lang.RuntimeException: Package manager has died
at android.app.ApplicationPackageManager.getPackageInfo(ApplicationPackageManager.java:82)
為了分析引發 Package manager has died 這個問題的具體原因,我們先來看看 getPackageInfo 這個方法:
frameworks/base/core/java/android/app/ApplicationPackageManager.java:
@Override public PackageInfo getPackageInfo(String packageName, int flags) throws NameNotFoundException { try { PackageInfo pi = mPM.getPackageInfo(packageName, flags, mContext.getUserId()); if (pi != null) { return pi; } } catch (RemoteException e) { throw new RuntimeException("Package manager has died", e); } throw new NameNotFoundException(packageName); }
從上面可以看出,getPackageInfo 具體實現是一個 Binder 調用,造成這個的原因是因為發生了 RemoteException 。
Binder 調用為什么會造成 Exception,下面再來看看 Binder 代碼frameworks/base/core/jni/android_util_Binder.cpp:
case FAILED_TRANSACTION: ALOGE("!!! FAILED BINDER TRANSACTION !!!"); // TransactionTooLargeException is a checked exception, only throw from certain methods. // FIXME: Transaction too large is the most common reason for FAILED_TRANSACTION // but it is not the only one. The Binder driver can return BR_FAILED_REPLY // for other reasons also, such as if the transaction is malformed or // refers to an FD that has been closed. We should change the driver // to enable us to distinguish these cases in the future. jniThrowException(env, canThrowRemoteException ? "android/os/TransactionTooLargeException" : "java/lang/RuntimeException", NULL); break;
可以看出造成 Binder crash 拋出 RuntimeException 是因為獲取應用 PackageInfo 中數據量太大了,超出了 Binder 可傳遞的最大容量,進而導致 PackageManager 崩潰。
對於上面這種情況,考慮如果只獲取versionName和versionCode兩個信息,不需要Activity等信息,設法讓PackageInfo的信息量小點,避免超出了 Binder 可傳遞的最大容量。
我們可以利用 getPackageInfo(String packageName, @PackageInfoFlags int flags) 它的第二個參數 flag ,使得該方法返回的對象容量減小,比如使用 PackageManager.GET_CONFIGURATIONS
此外,如果對與Binder的同時調用超出了限制就會拋出 TransactionTooLargeException這個異常,雖然這種場景比較少見,但是我們還是有比較避免多個線程同時來調用Binder就可以了。
優化后代碼如下:
public static int getVersionCode(Context context) { synchronized(Hold.class){ PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_CONFIGURATIONS); return info.versionCode; } }
