-
在入口 Acitivity 中开启 IntentService 来下载广告页。 或者是其它异步下载操作。
-
在广告页图片 文件流完全写入后 记录图片大小,或者记录一个标识。
在下次的广告页加载中可以判断是否已经下载好了广告页图片以及图片是否完整,否则删除并且再次下载图片。
另外因为在闪屏页中仍然有 剩余展示时间,所以在这个时间段里如果用户已经下载好了图片并且图片完整,就可以显示广告页。否则进入主 Activity , 因为 IntentService 仍然在后台继续默默的下载并保存图片~
优化前 :
| Displayed | LaunchActivity | MainActivity |
| — | :-: | --: |
| | +2s526ms | +1s583ms |
| | +2s603ms | +1s533ms |
| | +2s372ms | +1s556ms |
优化后 :
| Displayed | LaunchActivity | MainActivity |
| — | :-: | --: |
| | +995ms | +1s191ms |
| | +911ms | +1s101ms |
| | +903ms | +1s187ms |
通过上面的几台手机的启动测试,发现优化后App冷启动的启动速度均提升了60% !!! ,并且我们可以再看一下手机冷启动时候的内存情况 :
优化前 : 伴随着大量对象的创建回收,15s内系统GC 5次。
内存使用波澜荡漾。
优化后 : 趋于平稳上升状态创建对象,15s内系统GC 2次。(后期业务拓展加入新功能,所以代码量增加。)之后总内存使用平缓下降。
**Other :**应用使用的系统不确定如何分类的内存。
**Code :**应用用于处理代码和资源(如 dex 字节码、已优化或已编译的 dex 码、.so 库和字体)的内存。
Stack : 应用中的原生堆栈和 Java 堆栈使用的内存。 这通常与您的应用运行多少线程有关。
Graphics:图形缓冲区队列向屏幕显示像素(包括 GL 表面、GL 纹理等等)所使用的内存。 (请注意,这是与 CPU 共享的内存,不是 GPU 专用内存。)
**Native :**从 C 或 C++ 代码分配的对象内存。即使应用中不使用 C++,也可能会看到此处使用的一些原生内存,因为 Android 框架使用原生内存代表处理各种任务,如处理图像资源和其他图形时,即使编写的代码采用 Java 或 Kotlin 语言。
**Java :**从 Java 或 Kotlin 代码分配的对象内存。
**Allocated :**应用分配的 Java/Kotlin 对象数。 它没有计入 C 或 C++ 中分配的对象。
更多查看 :
https://developer.android.google.cn/studio/profile/memory-profiler?hl=zh-cn
优化完我们的代码后,分析一下启动窗口的源码。基于 android-25 (7.1.1)
启动窗口是由 WindowManagerService 统一管理的 Window窗口,一般作为冷启动页入口 Activity 的预览窗口,启动窗口由 ActivityManagerService 来决定是否显示的,并不是每一个 Activity 的启动和跳转都会显示这个窗口。
WindowManagerService 通过窗口管理策略类 PhoneWindowManager 来创建启动窗口。
拿我之前源码分析的文章中的启动流程图来看看大致 :
Launcher 启动 Activity 的工作过程
https://blog.csdn.net/qian520ao/article/details/78156214
直奔主题,在 ActivityStarter的startActivityUnchecked()方法中,调用了ActivityStack(Activity 状态管理)的startActivityLocked()方法。此时Activity 还在启动过程中,窗口并未显示。
先上一张流程图,展示了启动窗口的显示过程。
首先,由 Activity 状态管理者ActivityStack开始执行显示启动窗口的流程。
//ActivityStack
final void startActivityLocked(ActivityRecord r, boolean newTask, boolean keepCurTransition,
ActivityOptions options) {
if (!isHomeStack() || numActivities() > 0) {//HOME_STACK表示Launcher桌面所在的Stack
// 1.首先当前启动栈不在Launcher的桌面栈里,并且当前系统已经有激活过Activity
boolean doShow = true;
if (newTask) {
// 2.要将该Activity组件放在一个新的任务栈中启动
if ((r.intent.getFlags() & Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) != 0) {
resetTaskIfNeededLocked(r, r);
doShow = topRunningNonDelayedActivityLocked(null) == r;
}
} else if (options != null && options.getAnimationType()
== ActivityOptions.ANIM_SCENE_TRANSITION) {
doShow = false;
}
if (r.mLaunchTaskBehind) {
//3. 热启动,不需要启动窗口
mWindowManager.setAppVisibility(r.appToken, true);
ensureActivitiesVisibleLocked(null, 0, !PRESERVE_WINDOWS);
} else if (SHOW_APP_STARTING_PREVIEW && doShow) {
//4. 显示启动窗口
r.showStartingWindow(prev, showStartingIcon);
}
} else {
// 当前启动的是桌面Launcher (开机启动)
// If this is the first activity, don’t do any fancy animations,
// because there is nothing for it to animate on top of.
}
}
首先判断当前要启动的 Activity 不在Launcher栈里
要启动的 Activity 是否处于新的 Task 里,并且没有转场动画
如果是热/温启动则不需要启动窗口,直接设置App的Visibility
接下来调用ActivityRecord的showStartingWindow()方法来设置启动窗口并且改变当前窗口的状态。
如果 App 的应用进程创建完成,并且入口 Activity 准备就绪,就可以根据 mStartingWindowState 来判断是否需要关闭启动窗口。
//ActivityRecord
void showStartingWindow(ActivityRecord prev, boolean createIfNeeded) {
final CompatibilityInfo compatInfo =
service.compatibilityInfoForPackageLocked(info.applicationInfo);
final boolean shown = service.mWindowManager.setAppStartingWindow(
appToken, packageName, theme, compatInfo, nonLocalizedLabel, labelRes, icon,
logo, windowFlags, prev != null ? prev.appToken : null, createIfNeeded);
if (shown) {
mStartingWindowState = STARTING_WINDOW_SHOWN;
}
}
WindowManagerService 会对当前 Activity 的token和主题进行判断。
//WindowManagerService
@Override
public boolean setAppStartingWindow(IBinder token, String pkg,
int theme, CompatibilityInfo compatInfo,
CharSequence nonLocalizedLabel, int labelRes, int icon, int logo,
int windowFlags, IBinder transferFrom, boolean createIfNeeded) {
synchronized(mWindowMap) {
//1. 启动窗口也是需要token的
AppWindowToken wtoken = findAppWindowToken(token);
//2. 如果已经设置过启动窗口了,不继续处理
if (wtoken.startingData != null) {
return false;
}
if (theme != 0) {
AttributeCache.Entry ent = AttributeCache.instance().get(pkg, theme,
com.android.internal.R.styleable.Window, mCurrentUserId);
//3. 一堆代码对主题判断,不符合要求则不显示启动窗口(如透明主题)
if (windowIsTranslucent) {
return false;
}
if (windowIsFloating || windowDisableStarting) {
return false;
}
}
//4. 创建StartingData,并且通过Handler发送消息
wtoken.startingData = new StartingData(pkg, theme, compatInfo, nonLocalizedLabel,
labelRes, icon, logo, windowFlags);
Message m = mH.obtainMessage(H.ADD_STARTING, wtoken);
mH.sendMessageAtFrontOfQueue(m);
}
return true;
}
启动窗口也需要和 Activity 拥有同样令牌 token ,虽然启动窗口可能是白屏,或者一张图片,但是仍然需要走绘制流程已经通过WMS显示窗口。
StartingData对象用来表示启动窗口的相关数据,描述了启动窗口的视图信息。
如果当前 Activity 是透明主题或者是浮动窗口等,那么就不需要启动窗口来过渡启动过程,所以在上面视觉优化中的设置透明主题就没有显示白色的启动窗口。
显示启动窗口也是一件心急火燎的事情,WMS的内部类H (handler) 处于主线程处理消息,所以需要将当前Message放置队列头部。
PS : 为什么需要通过 Handler 发送消息 ?
你可以在各大服务Service中见到 Handler 的身影,并且它们可能都有一个很吊的命名 H ,因为可能调用这个服务的某个执行方法处于子线程中,所以 Handler 的职责就是将它们切换到主线程中,并且也可以统一管理调度。
更多 Handler 了解可以查阅文章 :
你真的了解Handler?
https://blog.csdn.net/qian520ao/article/details/78262289
//WindowManagerService --> H
public void handleMessage(Message msg) {
switch (msg.what) {
case ADD_STARTING: {
final AppWindowToken wtoken = (AppWindowToken)msg.obj;
final StartingData sd = wtoken.startingData;
View view = null;
try {
final Configuration overrideConfig = wtoken != null && wtoken.mTask != null
? wtoken.mTask.mOverrideConfig : null;
view = mPolicy.addStartingWindow(wtoken.token, sd.pkg, sd.theme,
sd.compatInfo, sd.nonLocalizedLabel, sd.labelRes, sd.icon, sd.logo,
sd.windowFlags, overrideConfig);
} catch (Exception e) {
Slog.w(TAG_WM, “Exception when adding starting window”, e);
}
} break;
}
在当前的handleMessage方法中,会处于主线程处理消息,拿到token和StartingData启动数据后,便通过mPolicy.addStartingWindow()方法将启动窗口添加到WIndow上。
mPolicy为PhoneWindowManager,控制着启动窗口的添加删除和修改。
在PhoneWindowManager对启动窗口进行配置,获取当前Activity设置的主题和资源信息,设置到启动窗口中。
//PhoneWindowManager
@Override
public View addStartingWindow(IBinder appToken, String packageName, int theme,
CompatibilityInfo compatInfo, CharSequence nonLocalizedLabel, int labelRes,
int icon, int logo, int windowFlags, Configuration overrideConfig) {
//可以通过SHOW_STARTING_ANIMATIONS设置不显示启动窗口
if (!SHOW_STARTING_ANIMATIONS) {
return null;
}
WindowManager wm = null;
View view = null;
//1. 获取上下文Context和主题theme以及标题
Context context = mContext;
if (theme != context.getThemeResId() || labelRes != 0) {
try {
context = context.createPackageContext(packageName, 0);
context.setTheme(theme);
} catch (PackageManager.NameNotFoundException e) {
// Ignore
}
}
//2. 创建PhoneWindow 用来显示
final PhoneWindow win = new PhoneWindow(context);
win.setIsStartingWindow(true);
//3. 设置当前窗口type和flag,源码注释中描述的很清晰…
win.setType(
WindowManager.LayoutParams.TYPE_APPLICATION_STARTING);
win.setFlags(…);
view = win.getDecorView();
//4. WindowManager的绘制流程
wm.addView(view, params);
return view.getParent() != null ? view : null;
}
如果theme和labelRes的值不为0,那么说明开发者指定了启动窗口的主题和标题,那么就需要从当前要启动的Activity中获取这些信息,并设置到启动窗口中。
和其它窗口一样,启动窗口也需要通过PhoneWindow来设置布局信息DecorView。所以在上面视觉优化中的设置闪屏图片主题的启动窗口显示的就是图片内容。
启动窗口和普通窗口的不同之处在于它是 fake window ,不需要触摸事件
最后通过WindowManger走View的绘制流程(measure-layout-draw)将启动窗口显示出来,最后会请求WindowManagerService为启动窗口添加一个WindowState对象,真正的将启动窗口显示给用户,并且可以对启动窗口进行管理。
更多WindowManager的addView流程可以查阅 :
View的工作流程
https://blog.csdn.net/qian520ao/article/details/78657084
至此应用程序的启动优化和启动窗口的源码分析已经总结完毕,在项目的开发中要知其然而之所以然 ,并且对源码的分析有助于我们了解原理和解决问题的根源。
资源分享
- 最新大厂面试专题
这个题库内容是比较多的,除了一些流行的热门技术面试题,如Kotlin,数据库,Java虚拟机面试题,数组,Framework ,混合跨平台开发,等
- 对应导图的Android高级工程师进阶系统学习视频
最近热门的,NDK,热修复,MVVM,源码等一系列系统学习视频都有!
ManagerService为启动窗口添加一个WindowState对象,真正的将启动窗口显示给用户,并且可以对启动窗口进行管理。
更多WindowManager的addView流程可以查阅 :
View的工作流程
https://blog.csdn.net/qian520ao/article/details/78657084
至此应用程序的启动优化和启动窗口的源码分析已经总结完毕,在项目的开发中要知其然而之所以然 ,并且对源码的分析有助于我们了解原理和解决问题的根源。
资源分享
- 最新大厂面试专题
这个题库内容是比较多的,除了一些流行的热门技术面试题,如Kotlin,数据库,Java虚拟机面试题,数组,Framework ,混合跨平台开发,等
[外链图片转存中…(img-Xw1BlFuO-1643777394276)]
- 对应导图的Android高级工程师进阶系统学习视频
最近热门的,NDK,热修复,MVVM,源码等一系列系统学习视频都有!
[外链图片转存中…(img-muUMzmUi-1643777394276)]
下载方法:点赞+关注后 点击【Android高级工程师进阶学习】即可领取!