以Android7.0为例,我们知道Android应用在启动时候,PhoneWindowManager会添加一个空白启动窗口,叫做addStartingWindow,在界面加载完毕后,会removeStartingWindow。
/** {@inheritDoc} */
@Override
public View addStartingWindow(IBinder appToken, String packageName, int theme,
CompatibilityInfo compatInfo, CharSequence nonLocalizedLabel, int labelRes,
int icon, int logo, int windowFlags) {
if (!SHOW_STARTING_ANIMATIONS) {
return null;
}
if (packageName == null) {
return null;
}
WindowManager wm = null;
View view = null;
try {
Context context = mContext;
if (DEBUG_STARTING_WINDOW) Slog.d(TAG, "addStartingWindow " + packageName
+ ": nonLocalizedLabel=" + nonLocalizedLabel + " theme="
+ Integer.toHexString(theme));
if (theme != context.getThemeResId() || labelRes != 0) {
try {
context = context.createPackageContext(packageName, 0);
context.setTheme(theme);
} catch (PackageManager.NameNotFoundException e) {
// Ignore
}
}
PhoneWindow win = new PhoneWindow(context);
win.setIsStartingWindow(true);
final TypedArray ta = win.getWindowStyle();
if (ta.getBoolean(
com.android.internal.R.styleable.Window_windowDisablePreview, false)
|| ta.getBoolean(
com.android.internal.R.styleable.Window_windowShowWallpaper,false)) {
return null;
}
Resources r = context.getResources();
win.setTitle(r.getText(labelRes, nonLocalizedLabel));
win.setType(
WindowManager.LayoutParams.TYPE_APPLICATION_STARTING);
synchronized (mWindowManagerFuncs.getWindowManagerLock()) {
// Assumes it's safe to show starting windows of launched apps while
// the keyguard is being hidden. This is okay because starting windows never show
// secret information.
if (mKeyguardHidden) {
windowFlags |= FLAG_SHOW_WHEN_LOCKED;
}
}
// Force the window flags: this is a fake window, so it is not really
// touchable or focusable by the user. We also add in the ALT_FOCUSABLE_IM
// flag because we do know that the next window will take input
// focus, so we want to get the IME window up on top of us right away.
win.setFlags(
windowFlags|
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE|
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE|
WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM,
windowFlags|
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE|
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE|
WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
win.setDefaultIcon(icon);
win.setDefaultLogo(logo);
win.setLayout(WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.MATCH_PARENT);
final WindowManager.LayoutParams params = win.getAttributes();
params.token = appToken;
params.packageName = packageName;
params.windowAnimations = win.getWindowStyle().getResourceId(
com.android.internal.R.styleable.Window_windowAnimationStyle, 0);
params.privateFlags |=
WindowManager.LayoutParams.PRIVATE_FLAG_FAKE_HARDWARE_ACCELERATED;
params.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS;
if (!compatInfo.supportsScreen()) {
params.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_COMPATIBLE_WINDOW;
}
params.setTitle("Starting " + packageName);
wm = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
view = win.getDecorView();
if (win.isFloating()) {
// Whoops, there is no way to display an animation/preview
// of such a thing! After all that work... let's skip it.
// (Note that we must do this here because it is in
// getDecorView() where the theme is evaluated... maybe
// we should peek the floating attribute from the theme
// earlier.)
return null;
}
if (DEBUG_STARTING_WINDOW) Slog.d(
TAG, "Adding starting window for " + packageName
+ " / " + appToken + ": "
+ (view.getParent() != null ? view : null));
wm.addView(view, params);
// Only return the view if it was successfully added to the
// window manager... which we can tell by it having a parent.
return view.getParent() != null ? view : null;
} catch (WindowManager.BadTokenException e) {
// ignore
Log.w(TAG, appToken + " already running, starting window not displayed. " +
e.getMessage());
} catch (RuntimeException e) {
// don't crash if something else bad happens, for example a
// failure loading resources because we are loading from an app
// on external storage that has been unmounted.
Log.w(TAG, appToken + " failed creating starting window", e);
} finally {
if (view != null && view.getParent() == null) {
Log.w(TAG, "view not successfully added to wm, removing view");
wm.removeViewImmediate(view);
}
}
return null;
}
空白启动窗口给用户的操作体验是很差的,尤其是低端手机,或者应用冷启动时间较长时。所以就想到能不能在应用冷启动的时候,对启动窗口进行截屏,然后将截图缓存起来。当下一次应用冷启动时候,用截图的窗口代替原来的空白启动窗口呢?答案是可以的。流程如下图所示。问题的关键是截屏时机在哪里?冷启动过程中,WindowState中的mAttrs(WindowManager.LayoutParams)的type,有一个切换过程,从WindowManager.LayoutParams.TYPE_APPLICATION_STARTING --->WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW,
if (mService.isFastStartingWindowSupport() && mWin.mAttrs.type == WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW && mService.lastLayoutParamsType== android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING && mWin.mAppToken != null) {
mCanScreenshot = true;
doCacheBitmap();
}
mService.lastLayoutParamsType = mWin.mAttrs.type;//application start , mWin.mAttrs.type from 3--->1
lastLayoutParamsType是在 WMS中新声明的
public int lastLayoutParamsType = -1;
当然这个变化还有你的应用androidmanifest中application的设置有关。
上面截屏的逻辑是定义在WindowStateAnimator中performShowLocked的,在此时截屏,其实整个界面还是没有绘制完成的,看下面的doCacheBitmap,需要子线程延时一定时间,同时,对截屏的图像进行判断。
void doCacheBitmap() {
AsyncTask task = new AsyncTask() {
@Override
protected Void doInBackground(Void... para) {
//android.os.Process.setThreadPriority
// (android.os.Process.THREAD_PRIORITY_FOREGROUND);
try {
if (mWmSleep != -1) {
Thread.sleep(mWmSleep);
} else {
Thread.sleep(250);
}
if(mWin.mWinAnimator.mSurfaceController == null){
// in some case , mSurfaceController is null
return null;
}
if(!mCanScreenshot){
return null;
}
Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "AsyncScreenshot");
Bitmap bmShot = SurfaceControl.screenshot(new Rect(),
(int) mWin.mWinAnimator.mSurfaceController.mSurfaceW,
(int) mWin.mWinAnimator.mSurfaceController.mSurfaceH,
mSurfaceController.mSurfaceLayer,
mSurfaceController.mSurfaceLayer, false, 0);
Slog.i(TAG, "doCacheBitmap, mToken =" + mWin.mToken);
int leftTopColor = bmShot.getPixel(0, 0);
int rightTopColor = bmShot.getPixel(mContext.getResources().getDisplayMetrics().widthPixels - 1, 0);
int leftBottomColor = bmShot.getPixel(0, mContext.getResources().getDisplayMetrics().heightPixels - 1);
int rightBottomColor = bmShot.getPixel(mContext.getResources().getDisplayMetrics().widthPixels - 1, mContext.getResources().getDisplayMetrics().heightPixels - 1);
int averageColor = (getAverageRGB(leftTopColor) + getAverageRGB(rightTopColor) + getAverageRGB(leftBottomColor) + getAverageRGB(rightBottomColor)) / 4;
if (averageColor < 5) {
//in some case, the bitmap is a starting window(it is a picture with black boder,or the picture is whole black) , not a started window , so return
return null;
}
if (bmShot != null && mWin.mToken != null) {
mService.setBitmapByToken(mWin.mToken.token
, bmShot.copy(bmShot.getConfig(), true));
bmShot.recycle();
}
Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
};
task.execute();
}
应用快速启动流程如下图所示:
(1) 用户在点击应用的时候,ActivityManagerService会执行startActivity方法,然后ActivityStack执行startActivityLocked方法,然后ActivityRecord执行showStartingWindow方法,最后调用WindowManagerService的setAppStartingWindow来设置一个启动窗口。
(2) WindowManagerService会判断当前系统是否支持FSW并且当前窗口是否有Bitmap缓存,如果是开机后第一次启动,则执行Android系统原有方法addStartingWindow,添加一个空白窗口。如果系统支持FSW并且当前窗口有Bitmap缓存,则执行addFastStartingWindow方法,添加一个非空白窗口。
(3) 应用冷启动时,WindowMangerService通过WindowStateAnimator进行窗口缓存。WindowMangerService收到configChange广播,则清除bitmap缓存。
加入该技术后,给用户的感觉是应用秒开,对比测试友商手机,冷启动速度都要比我们的差,加入该技术的应用,冷启动速度只有300ms。
该技术的局限性在于,仅适用于启动界面变化较小的APP,变化较大的应用不适合,第三方应用基本都不适合。
第一篇博客就写到这里把。