android:process 的坑,你懂嗎?
許多知識知其然而不知其所以然,這也許就是大神與菜鳥的區別吧。
最近排查問題時發現一個問題: 一個在 Application 中啟動的定時任務在執行時會被呼叫多次,詭異的很,最後發現是一個前人留下的坑,原因就是對 android:process 不知其所以然造成的。
android:process 屬性
關於 android:process 屬性,相信大家都不陌生,android 官網是這樣說明的 :
預設情況下,同一應用的所有元件均在相同的程序中執行,且大多數應用都不會改變這一點。 但是,如果您發現需要控制某個元件所屬的程序,則可在清單檔案中執行此操作。
各類元件元素的清單檔案條目—<activity>、<service>、<receiver> 和 <provider>—均支援 android:process 屬性,此屬性可以指定該元件應在哪個程序執行。您可以設定此屬性,使每個元件均在各自的程序中執行,或者使一些元件共享一個程序,而其他元件則不共享。 此外,您還可以設定 android:process,使不同應用的元件在相同的程序中執行,但前提是這些應用共享相同的 Linux 使用者 ID 並使用相同的證書進行簽署。
此外, 元素還支援 android:process 屬性,以設定適用於所有元件的預設值。
如果記憶體不足,而其他為使用者提供更緊急服務的程序又需要記憶體時,Android 可能會決定在某一時刻關閉某一程序。在被終止程序中執行的應用元件也會隨之銷燬。 當這些元件需要再次執行時,系統將為它們重啟程序。
決定終止哪個程序時,Android 系統將權衡它們對使用者的相對重要程度。例如,相對於託管可見 Activity 的程序而言,它更有可能關閉託管螢幕上不再可見的 Activity 程序。 因此,是否終止某個程序的決定取決於該程序中所執行元件的狀態。 下面,我們介紹決定終止程序所用的規則。
在需要使用到新程序時,可以使用 android:process 屬性,如果被設定的程序名是以一個冒號開頭的,則這個新的程序對於這個應用來說是私有的,當它被需要或者這個服務需要在新程序中執行的時候,這個新程序將會被建立。如果這個程序的名字是以字元開頭,並且符合 android 包名規範(如 com.roger 等),則這個服務將執行在一個以這個名字命名的全域性的程序中,當然前提是它有相應的許可權。若以數字開頭(如 1Remote.com ),或不符合 android 包名規範(如 Remote),則在編譯時將會報錯
( INSTALL_PARSE_FAILED_MANIFEST_MALFORMED )。新建程序將允許在不同應用中的各種元件可以共享一個程序,從而減少資源的佔用。具體可以參考部落格:
重點來了,因為設定了 android:process 屬性將元件執行到另一個程序,相當於另一個應用程式,所以在另一個執行緒中也將新建一個 Application 的例項。因此,每新建一個程序 Application 的 onCreate 都將被呼叫一次。 如果在 Application 的 onCreate 中有許多初始化工作並且需要根據程序來區分的,那就需要特別注意了。
詳細介紹了新程序啟動的過程,其中我們重點看到 Step 17. ActivityThread.handleCreateService
中
public final class ActivityThread {
......
private final void handleCreateService(CreateServiceData data) {
// If we are getting ready to gc after going to the background, well
// we are back active so skip it.
unscheduleGcIdler();
LoadedApk packageInfo = getPackageInfoNoCheck(
data.info.applicationInfo);
Service service = null;
try {
java.lang.ClassLoader cl = packageInfo.getClassLoader();
service = (Service) cl.loadClass(data.info.name).newInstance();
} catch (Exception e) {
if (!mInstrumentation.onException(service, e)) {
throw new RuntimeException(
"Unable to instantiate service " + data.info.name
+ ": " + e.toString(), e);
}
}
try {
if (localLOGV) Slog.v(TAG, "Creating service " + data.info.name);
ContextImpl context = new ContextImpl();
context.init(packageInfo, null, this);
Application app = packageInfo.makeApplication(false, mInstrumentation);
context.setOuterContext(service);
service.attach(context, this, data.info.name, data.token, app,
ActivityManagerNative.getDefault());
service.onCreate();
mServices.put(data.token, service);
try {
ActivityManagerNative.getDefault().serviceDoneExecuting(
data.token, 0, 0, 0);
} catch (RemoteException e) {
// nothing to do.
}
} catch (Exception e) {
if (!mInstrumentation.onException(service, e)) {
throw new RuntimeException(
"Unable to create service " + data.info.name
+ ": " + e.toString(), e);
}
}
}
......
}
看到這行 Application app = packageInfo.makeApplication(false, mInstrumentation);
在這裡建立了 Application 。
解決方案
獲取當前執行程序的名稱:
方案1
public static String getProcessName(Context cxt, int pid) {
ActivityManager am = (ActivityManager) cxt.getSystemService(Context.ACTIVITY_SERVICE);
List<RunningAppProcessInfo> runningApps = am.getRunningAppProcesses();
if (runningApps == null) {
return null;
}
for (RunningAppProcessInfo procInfo : runningApps) {
if (procInfo.pid == pid) {
return procInfo.processName;
}
}
return null;
}
目前網上主流的方法,但效率沒有方案2高,感謝由王燚同學提供的方案2
方案2
public static String getProcessName() {
try {
File file = new File("/proc/" + android.os.Process.myPid() + "/" + "cmdline");
BufferedReader mBufferedReader = new BufferedReader(new FileReader(file));
String processName = mBufferedReader.readLine().trim();
mBufferedReader.close();
return processName;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
然後在 Application 的 onCreate 中獲取程序名稱並進行相應的判斷,例如:
String processName = getProcessName(this, android.os.Process.myPid());
if (!TextUtils.isEmpty(processName) && processName.equals(this.getPackageName())) {//判斷程序名,保證只有主程序執行
//主程序初始化邏輯
....
}
總結
知其然還需知其所以然,這才是總結並提高的法寶。希望能幫到有需要的同學 :)
Have a good day ~
參考
來源:https://www.rogerblog.cn/2016/03/17/android-proess/