Launcher概述
Android系統啓動的最後一步是啓動一個Home應用程序,這個應用程序用來顯示系統中已經安裝的應用程序,這個Home應用程序就叫做Launcher。應用程序Launcher在啓動過程中會PackageManagerService返回系統中已經安裝的應用程序的信息,並將這些信息封裝成一個快捷圖標列表顯示在系統屏幕上,這樣用戶可以通過點擊這些快捷圖標來啓動相應的應用程序。
Launcher的啓動流程
我們可以簡單的概括爲,Launcher是一個應用程序,這個應用程序在AndroidManifest.xml文件主Activity裏面配置了Action爲Intent.ACTION_MAIN,Category爲Intent.CATEGORY_HOME,如下所示。
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.launcher3">
<uses-sdk android:targetSdkVersion="23" android:minSdkVersion="21"/>
....
<application
...
android:largeHeap="@bool/config_largeHeap"
android:restoreAnyVersion="true"
android:supportsRtl="true" >
-->
<activity
android:name="com.android.launcher3.Launcher"
android:launchMode="singleTask"
android:clearTaskOnLaunch="true"
android:stateNotNeeded="true"
android:windowSoftInputMode="adjustPan"
android:screenOrientation="unspecified"
android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize"
android:resizeableActivity="true"
android:resumeWhilePausing="true"
android:taskAffinity=""
android:enabled="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.HOME" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.MONKEY"/>
<category android:name="android.intent.category.LAUNCHER_APP" />
</intent-filter>
</activity>
....
</application>
</manifest>
應用程序是由ActivityManagerService系統服務啓動的,手機重啓後AMS會啓動Launcher應用這個過程和啓動普通程序沒有太大區別,只是需要AMS先找到Launcher應用,如下是啓動Launcher的Intent方法,因爲Launcher應用是配置了Category爲Intent.CATEGORY_HOME所以AMS可以通過這個條件過濾出所有Launcher應用,如果有多個Launcher則會彈出框給用戶選擇。
Intent getHomeIntent() {
Intent intent = new Intent(mTopAction, mTopData != null ? Uri.parse(mTopData) : null);
intent.setComponent(mTopComponent);
intent.addFlags(Intent.FLAG_DEBUG_TRIAGED_MISSING);
if (mFactoryTest != FactoryTest.FACTORY_TEST_LOW_LEVEL) {
intent.addCategory(Intent.CATEGORY_HOME);
}
return intent;
}
問題分析:
按照正常流程在手機重啓後Launcher應該是很快就會被啓動的,但是當手機已經通電開機但是用戶並有解鎖鎖屏的時候,Android N運行於一個安全的模式,也就是Dierect Boot模式,該模式下普通應用程序無法啓動很多權限被禁掉了,所以AMS不會第一時間啓動Launcher應用程序,而是由Setting應用啓動,Setting程序是在監聽到解鎖廣播判斷Launcher應用是否啓動如果啓動了則關閉seting界面顯示launcher界面,從進入Setting界面到啓動Launcher進程再到Launcher界面顯示需要3秒左右時間,這就導致解鎖後無法第一時間看到Launcher界面啓動。
下面是Setting應用啓動Launcher的過程
在分析7.0過程中發現在啓動Launcher之前會先啓動一個FallbackHome,之後纔會啓動Launcher,通過調查發現FallbackHome屬於Settings中的一個activity,Settings的android:directBootAware爲true,並且FallbackHome在category中配置了Home屬性,而Launcher的android:directBootAware爲false,所有隻有FallbackHome可以在direct boot模式下啓動。
<!-- Triggered when user-selected home app isn't encryption aware -->
<activity
android:name=".FallbackHome"
android:excludeFromRecents="true"
android:label=""
android:screenOrientation="behind"
android:theme="@style/FallbackHome">
<intent-filter android:priority="-1000">
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.HOME"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
FallbackHome啓動Launcher流程
FallbackHome代碼非常簡單就是一個透明佈局提示Laucher正在啓動,監聽解鎖廣播Intent.ACTION_USER_UNLOCKED事件,收到解鎖事件後再用Handler循環判斷Launcher是否啓動,如果沒有啓動則繼續每個500毫秒判斷一次,直到Launcher啓動才關閉當前界面顯示Launcher界面
private void maybeFinish() {
if (getSystemService(UserManager.class).isUserUnlocked()) {
final Intent homeIntent = new Intent(Intent.ACTION_MAIN)
.addCategory(Intent.CATEGORY_HOME);
final ResolveInfo homeInfo = getPackageManager().resolveActivity(homeIntent, 0);
if (Objects.equals(getPackageName(), homeInfo.activityInfo.packageName)) {
if (UserManager.isSplitSystemUser()
&& UserHandle.myUserId() == UserHandle.USER_SYSTEM) {
// This avoids the situation where the system user has no home activity after
// SUW and this activity continues to throw out warnings. See b/28870689.
return;
}
Log.d(TAG, "User unlocked but no home; let's hope someone enables one soon?");
mHandler.sendEmptyMessageDelayed(0, 500);
} else {
Log.d(TAG, "User unlocked and real home found; let's go!");
getSystemService(PowerManager.class).userActivity(
SystemClock.uptimeMillis(), false);
finish();
}
}
}
完整代碼如下
public class FallbackHome extends Activity {
private static final String TAG = "FallbackHome";
private static final int PROGRESS_TIMEOUT = 2000;
private boolean mProvisioned;
private final Runnable mProgressTimeoutRunnable = () -> {
View v = getLayoutInflater().inflate(
R.layout.fallback_home_finishing_boot, null /* root */);
setContentView(v);
v.setAlpha(0f);
v.animate()
.alpha(1f)
.setDuration(500)
.setInterpolator(AnimationUtils.loadInterpolator(
this, android.R.interpolator.fast_out_slow_in))
.start();
getWindow().addFlags(LayoutParams.FLAG_KEEP_SCREEN_ON);
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Set ourselves totally black before the device is provisioned so that
// we don't flash the wallpaper before SUW
mProvisioned = Settings.Global.getInt(getContentResolver(),
Settings.Global.DEVICE_PROVISIONED, 0) != 0;
if (!mProvisioned) {
setTheme(R.style.FallbackHome_SetupWizard);
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
} else {
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
}
registerReceiver(mReceiver, new IntentFilter(Intent.ACTION_USER_UNLOCKED));
maybeFinish();
}
@Override
protected void onResume() {
super.onResume();
if (mProvisioned) {
mHandler.postDelayed(mProgressTimeoutRunnable, PROGRESS_TIMEOUT);
}
}
@Override
protected void onPause() {
super.onPause();
mHandler.removeCallbacks(mProgressTimeoutRunnable);
}
protected void onDestroy() {
super.onDestroy();
unregisterReceiver(mReceiver);
}
private BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
maybeFinish();
}
};
private void maybeFinish() {
if (getSystemService(UserManager.class).isUserUnlocked()) {
final Intent homeIntent = new Intent(Intent.ACTION_MAIN)
.addCategory(Intent.CATEGORY_HOME);
final ResolveInfo homeInfo = getPackageManager().resolveActivity(homeIntent, 0);
if (Objects.equals(getPackageName(), homeInfo.activityInfo.packageName)) {
if (UserManager.isSplitSystemUser()
&& UserHandle.myUserId() == UserHandle.USER_SYSTEM) {
// This avoids the situation where the system user has no home activity after
// SUW and this activity continues to throw out warnings. See b/28870689.
return;
}
Log.d(TAG, "User unlocked but no home; let's hope someone enables one soon?");
mHandler.sendEmptyMessageDelayed(0, 500);
} else {
Log.d(TAG, "User unlocked and real home found; let's go!");
getSystemService(PowerManager.class).userActivity(
SystemClock.uptimeMillis(), false);
finish();
}
}
}
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
maybeFinish();
}
};
}
快速啓動Launcher方案:
根據上面分析我們知道Launcher非directBootAware應用,PMS中的限制導致此時無法啓動Launcher,所以第一時間無法讓Launcher啓動,既然如此我們只要讓Launcher也支持directBootAware就可以了,這個過程中遇到了問題:將Launcher標記爲directBootAware應用會導致開機後Launcher crash。因爲Launcher中的widget仍舊是非directBootAware的,此時仍舊無法啓動,除非將widget相關的APP都標記爲directBootAware,這個我們可以對widget做一個延遲加載處理即可。
下面是具體實施過程:
1.首先我們需要支持Launcher支持directBootAware,如下在配置即可
android:defaultToDeviceProtectedStorage="true"
android:directBootAware="true"
<application
android:backupAgent=".LauncherBackupAgent"
android:fullBackupContent="@xml/backupscheme"
android:fullBackupOnly="true"
android:defaultToDeviceProtectedStorage="true"
android:directBootAware="true"
android:hardwareAccelerated="true"
android:icon="@drawable/ic_launcher_home"
android:label="@string/derived_app_name"
android:theme="@style/AppTheme"
android:largeHeap="@bool/config_largeHeap"
android:networkSecurityConfig="@xml/network_security_config"
android:restoreAnyVersion="true"
android:supportsRtl="true">
2.延遲加載widget,如下是加載widget方法,我們可以監聽解鎖廣播事件,在解鎖後延遲500毫秒再加載widget
mAppWidgetHost = new LauncherAppWidgetHost(this); mAppWidgetHost.startListening();
總結:
因爲很多廠商對Launcher做了定製化所以按照上面的方案可能會遇到啓動問題,不過這些問題都是可以逐個處理的,其實要想避免上面問題,我們可以j可以先加載一個簡單的桌面界面等解鎖後再加載一次。