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可以先加载一个简单的桌面界面等解锁后再加载一次。