Android應用程式的安裝位置
從API Level8以後,android允許將應用程式安裝在外部空間,之前則只能安裝到內部空間。
一、安裝到外部空間的特點
二、安裝路徑選擇的策略
它由多方面的因素影響,包括應用本身的設定、安裝應用的方式以及手機系統的預設選項等。下面來一一討論;
1.應用自身可以指定安裝路徑:
android:installLocation="preferExternal"可選的還有auto和
internalOnly
,當然也可以不宣告此引數,則預設為PackageInfo.INSTALL_LOCATION_UNSPECIFIED
在PackageInfo.java中定義如下:
public static final int INSTALL_LOCATION_UNSPECIFIED = -1; public static final int INSTALL_LOCATION_AUTO = 0; public static final int INSTALL_LOCATION_INTERNAL_ONLY = 1; public static final int INSTALL_LOCATION_PREFER_EXTERNAL = 2;
/frameworks/base/core/res/res/values/attrs_manifest.xml檔案有如下定義:
<!-- The default install location defined by an application. --> <attr name="installLocation"> <!-- Let the system decide ideal install location --> <enum name="auto" value="0" /> <!-- Explicitly request to be installed on internal phone storage only. --> <enum name="internalOnly" value="1" /> <!-- Prefer to be installed on SD card. There is no guarantee that the system will honor this request. The application might end up being installed on internal storage if external media is unavailable or too full. --> <enum name="preferExternal" value="2" /> </attr>
這個資訊是在哪裡解析並儲存到哪裡的呢?
我們知道,解析apk檔案的工作是由PackageParser.java來實現的,所以這裡解析AndroidManifest.xml檔案中的installLocation資訊是由它的一個方法實現的:
public static PackageLite parsePackageLite(String packageFilePath, int flags) private static PackageLite parsePackageLite(Resources res, XmlPullParser parser, AttributeSet attrs, int flags, String[] outError)
這裡共有兩個方法,第一個執行過程中又呼叫第二個,而在程式碼中搜索,只有一個檔案呼叫了這個public方法:DefaultContainerService.java。請特別留意,這裡傳入的flags引數的值為0:
PackageParser.PackageLite pkg = PackageParser.parsePackageLite(packagePath, 0);
既然說到這裡,那就不得不提提PackageParser.PackageLite這個類,不過它實在太簡單了,直接看吧:
public static class PackageLite {
public final String packageName;
public final int versionCode;
public final int installLocation;
public final VerifierInfo[] verifiers;
public PackageLite(String packageName, int versionCode,
int installLocation, List<VerifierInfo> verifiers) {
this.packageName = packageName;
this.versionCode = versionCode;
this.installLocation = installLocation;
this.verifiers = verifiers.toArray(new VerifierInfo[verifiers.size()]);
}
}
所以parsePackageLite這個方法就沒什麼可講的了,只是把xml檔案的幾個資訊解析出來,儲存到PackageLite的一個例項中而已。
我們需要知道的只是它解析了Manifest.xml檔案中的installLocation資訊,若未指定,則預設值為-1,否則為0、1或2(分別代表的含義簽名已經很明確了)。
它將作為系統最終判斷該把應用程式安裝到內部空間還是外部空間的一條重要依據。後面我們會看到更詳細的。
2.關於推薦安裝位置的策略:
首先認識一個簡單的類PackageInfoLite,它幾乎只是由幾個成員變數組成,僅次而已。
public class PackageInfoLite implements Parcelable {
public String packageName;
public int versionCode;
/**
* Specifies the recommended install location. Can be one of
* {@link #PackageHelper.RECOMMEND_INSTALL_INTERNAL} to install on internal storage
* {@link #PackageHelper.RECOMMEND_INSTALL_EXTERNAL} to install on external media
* {@link PackageHelper.RECOMMEND_FAILED_INSUFFICIENT_STORAGE} for storage errors
* {@link PackageHelper.RECOMMEND_FAILED_INVALID_APK} for parse errors.
*/
public int recommendedInstallLocation;
public int installLocation;
public VerifierInfo[] verifiers;
public PackageInfoLite() {
}
... ...
}
相比PackageParser.PackageLite,它只增加了一個成員變數:public int recommendedInstallLocation;DefaultContainerService.java的getMinimalPackageInfo方法:
PackageParser.PackageLite pkg = PackageParser.parsePackageLite(packagePath, 0);
ret.packageName = pkg.packageName;
ret.versionCode = pkg.versionCode;
ret.installLocation = pkg.installLocation;
ret.verifiers = pkg.verifiers;
ret.recommendedInstallLocation = recommendAppInstallLocation(pkg.installLocation, packagePath, flags, threshold);
下面重點來分析recommendedInstallLocation的賦值,請留意這裡傳入的recommendAppInstallLocation的兩個引數:pkg.installLocation--它是解析Manifest.xml檔案而得的一個引數;
flags--它是PackageManagerService中呼叫getMinimalPackageInfo方法時傳入的引數,這裡它應該程式碼的是在install某個應用時命令的附加引數,比如要求應用安裝在SD中或內部空間中等。
下面來重點分析recommendAppInstallLocation這個方法:
private int recommendAppInstallLocation(int installLocation, String archiveFilePath, int flags,
long threshold) {
int prefer;
//checkBoth的含義是:當確定了要安裝在哪個位置後,我們應該去檢查這個空間大小是否足夠,如果checkBoth為true,則內部/外部我們都得查
boolean checkBoth = false;
final boolean isForwardLocked = (flags & PackageManager.INSTALL_FORWARD_LOCK) != 0;
check_inner : {
/*
* Explicit install flags should override the manifest settings.
*/
//如果安裝應用時通過設定flags明確指出安裝路徑,那麼flags說了算,直接退出check_inner
if ((flags & PackageManager.INSTALL_INTERNAL) != 0) {
prefer = PREFER_INTERNAL;
break check_inner;
} else if ((flags & PackageManager.INSTALL_EXTERNAL) != 0) {
prefer = PREFER_EXTERNAL;
break check_inner;
}
/* No install flags. Check for manifest option. */
//如果安裝應用時沒有明確指出安裝路徑,那麼以apk本身的manifest中的值為準
//注意一:如果apk中選擇的是auto,那麼它和internalOnly的結果是一樣的
//注意二:如果apk中選擇的不是internalOnly,這裡會設定checkBoth = true,它的意思是我們需要內部和外部空間的大小我們都要check,因為最終有可能還是安裝在內部
if (installLocation == PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY) {
prefer = PREFER_INTERNAL;
break check_inner;
} else if (installLocation == PackageInfo.INSTALL_LOCATION_PREFER_EXTERNAL) {
prefer = PREFER_EXTERNAL;
checkBoth = true;
break check_inner;
} else if (installLocation == PackageInfo.INSTALL_LOCATION_AUTO) {
// We default to preferring internal storage.
prefer = PREFER_INTERNAL;
checkBoth = true;
break check_inner;
}
// Pick user preference
//如果前兩者都沒有指定,那麼系統資料庫中有個預設的安裝位置,以此值為準,這個值是我們作為手機開發者更改apk預設安裝位置可以修改的地方
//同樣地,當這個值既不是內部、也不是外部時(正常地應該是auto),我們仍然將其置為內部
int installPreference = Settings.Global.getInt(getApplicationContext()
.getContentResolver(),
Settings.Global.DEFAULT_INSTALL_LOCATION,
PackageHelper.APP_INSTALL_AUTO);
if (installPreference == PackageHelper.APP_INSTALL_INTERNAL) {
prefer = PREFER_INTERNAL;
break check_inner;
} else if (installPreference == PackageHelper.APP_INSTALL_EXTERNAL) {
prefer = PREFER_EXTERNAL;
break check_inner;
}
/*
* Fall back to default policy of internal-only if nothing else is specified.
*/
prefer = PREFER_INTERNAL;
}
//判斷外部設定是否有效
final boolean emulated = Environment.isExternalStorageEmulated();
//需要根據apk檔案的大小來衡量手機中空間是否足夠
final File apkFile = new File(archiveFilePath);
//fitsOnInternal代表安裝在內部是否合適,所謂合適是指通過之前的判斷,你想安裝在內部空間中,而且內部空間大小足夠
boolean fitsOnInternal = false;
if (checkBoth || prefer == PREFER_INTERNAL) {
try {
fitsOnInternal = isUnderInternalThreshold(apkFile, isForwardLocked, threshold);
} catch (IOException e) {
return PackageHelper.RECOMMEND_FAILED_INVALID_URI;
}
}
//fitsOnSd代表安裝在外部是否合適,所謂合適是指通過之前的判斷,你想安裝在外部空間中,而且外部空間大小足夠
boolean fitsOnSd = false;
if (!emulated && (checkBoth || prefer == PREFER_EXTERNAL)) {
try {
fitsOnSd = isUnderExternalThreshold(apkFile, isForwardLocked);
} catch (IOException e) {
return PackageHelper.RECOMMEND_FAILED_INVALID_URI;
}
}
//如果你想安裝在內部/外部,而且內部/外部也是合適的,那就推薦你安裝在內部/外部
if (prefer == PREFER_INTERNAL) {
if (fitsOnInternal) {
return PackageHelper.RECOMMEND_INSTALL_INTERNAL;
}
} else if (!emulated && prefer == PREFER_EXTERNAL) {
if (fitsOnSd) {
return PackageHelper.RECOMMEND_INSTALL_EXTERNAL;
}
}
if (checkBoth) {
if (fitsOnInternal) {
return PackageHelper.RECOMMEND_INSTALL_INTERNAL;
} else if (!emulated && fitsOnSd) {
return PackageHelper.RECOMMEND_INSTALL_EXTERNAL;
}
}
/*
* If they requested to be on the external media by default, return that
* the media was unavailable. Otherwise, indicate there was insufficient
* storage space available.
*/
if (!emulated && (checkBoth || prefer == PREFER_EXTERNAL)
&& !Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
return PackageHelper.RECOMMEND_MEDIA_UNAVAILABLE;
} else {
return PackageHelper.RECOMMEND_FAILED_INSUFFICIENT_STORAGE;
}
}
2