-
Notifications
You must be signed in to change notification settings - Fork 116
启动优化
启动定义:点击图标到 Launch Image 完全消失第一帧。
在Android Studio Logcat中过滤关键字“Displayed”,可以看到对应的冷启动耗时日志。
使用adb shell获取应用的启动时间:
adb shell am start -W [packageName]/[packageName. AppstartActivity]
执行后会得到三个时间:ThisTime、TotalTime和WaitTime
Stopping: com.example.app
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.example.app/.MainActivity }
Status: ok
LaunchState:COLD
Activity: com.example.app/.MainActivity
ThisTime: 1059
TotalTime: 1059
WaitTime: 1073
Complete
- LaunchState 启动类型, COLD代表的是冷启动。
- ThisTime 表示最后一个Activity启动耗时
- TotalTime 表示所有Activity启动耗时
- WaitTime 表示AMS启动Activity的总耗时
一般查看得到的TotalTime,即应用的启动时间,包括创建进程 + Application初始化 + Activity初始化到界面显示的过程。
该方法的特点:
- 线下使用方便,不能带到线上。
- 2、非严谨、精确时间。
// 开启方法追踪
Debug.startMethodTracing(new File(getExternalFilesDir(""),"trace").getAbsolutePath(),8*1024*1024,1_000);
// 停止方法追踪
Debug.stopMethodTracing()
通过上述方法会在data/data/package下边生成trace文件,记录每个方法的时间,CPU信息。
特点: 由于startMethodTracing会记录所有方法的执行信息,所以对运行时性能有较大影响。
// 开启方法采样追踪
Debug.startMethodTracingSampling(new File(getExternalFilesDir(""),"trace").getAbsolutePath(),8*1024*1024,1_000);
// 停止方法追踪
Debug.stopMethodTracing();
特点: 相比于Trace Java Methods会记录每个方法的时间、CPU信息,它会在应用的Java代码执行期间频繁采样捕获应用的调用堆栈,对运行时性能的影响比较小,能够记录更大的数据区域。
默认情况下APP冷启动时会有短暂的白屏窗口,针对这一问题可以为Window添加一个theme来解决。
<style name="SplashTheme.CustomBackground">
<item name="android:windowTranslucentStatus">true</item>
<item name="android:windowBackground">@drawable/splash</item>
</style>
为了防止图片变形,windowBackground的drawable使用的是一个 bitmap
文件splash.xml,如下:
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
android:gravity="bottom|fill_horizontal"
android:src="@drawable/bg_splash" />
在AndroidManifest中为Splash页面设置theme:
<activity
android:name=".SplashActivity"
android:theme="@style/SplashTheme.CustomBackground">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
此优化仅仅是视觉上的优化,并不能真正减少启动时间。
如果在Application做了繁重的初始化操作,比如多个第三方库的初始化,会占用相当大的启动时间。因此需要根据自身业务进行优化,如将不影响业务的第三方库放到子线程中进行,如下:
private void initLazyComponent() {
new Thread(new Runnable() {
@Override
public void run() {
// 推送SDK的初始化
pushSDKInit();
// 分享SDK的初始化
setupShareSDK();
// 二维码库初始化
ZXingLibrary.initDisplayOpinion(getContext());
// 检查并清理缓存的操作
checkAndClearStorage();
// ...
}
}).start();
}
另外,对于耗时特别长的第三方框架进行重点优化。这里结合项目中使用的ARouter为例。
ARouter中使用编译时注解处理器在代码编译期间来生成存储路由的相关代码。如下,在MainActivity上添加@Route的注解,并指定path。
@Route(path = "/kotlin/test")
class KotlinTestActivity : Activity() {
...
}
@Route(path = "/kotlin/java")
public class TestNormalActivity extends AppCompatActivity {
...
}
在编译后ARouter会通过APT自动生成注册MainActivity的相关代码,如下:
public class ARouter$$Group$$kotlin implements IRouteGroup {
@Override
public void loadInto(Map<String, RouteMeta> atlas) {
atlas.put("/kotlin/java", RouteMeta.build(RouteType.ACTIVITY, TestNormalActivity.class, "/kotlin/java", "kotlin", null, -1, -2147483648));
atlas.put("/kotlin/test", RouteMeta.build(RouteType.ACTIVITY, KotlinTestActivity.class, "/kotlin/test", "kotlin", new java.util.HashMap<String, Integer>(){{put("name", 8); put("age", 3); }}, -1, -2147483648));
}
}
public class ARouter$$Root$$modulekotlin implements IRouteRoot {
@Override
public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
routes.put("kotlin", ARouter$$Group$$kotlin.class);
}
}
在使用ARouter的时候需要在Application创建时对其进行初始化操作,
// ARouter
ARouter.init(mApplication);
// _ARouter
protected static synchronized boolean init(Application application) {
mContext = application;
// 初始化路由
LogisticsCenter.init(mContext, executor);
hasInit = true;
mHandler = new Handler(Looper.getMainLooper());
return true;
}
ARouter的init方法调用了_ARouter中的init方法,然后通过LogisticsCenter进行初始化,代码如下:
// LogisticsCenter
public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
mContext = context;
executor = tpe;
try {
long startInit = System.currentTimeMillis();
//load by plugin first
loadRouterMap();
if (registerByPlugin) {
logger.info(TAG, "Load router map by arouter-auto-register plugin.");
} else {
Set<String> routerMap;
// debug模式下,每次启动都会重新构建Router/如果首次安装或者更新了版本也会重建Router
if (ARouter.debuggable() || PackageUtils.isNewVersion(context)) {
// 通过反射扫描对应包下所有的className
routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);
if (!routerMap.isEmpty()) {
// 将扫描结果存入SP中,下次启动就无需再扫描了
context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).edit().putStringSet(AROUTER_SP_KEY_MAP, routerMap).apply();
}
// Save new version name when router map update finishes.
PackageUtils.updateVersion(context);
} else {
// 从缓存中取出路由集合
routerMap = new HashSet<>(context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).getStringSet(AROUTER_SP_KEY_MAP, new HashSet<String>()));
}
startInit = System.currentTimeMillis();
for (String className : routerMap) {
if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_ROOT)) {
// This one of root elements, load root.
((IRouteRoot) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.groupsIndex);
} else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_INTERCEPTORS)) {
// Load interceptorMeta
((IInterceptorGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.interceptorsIndex);
} else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_PROVIDERS)) {
// Load providerIndex
((IProviderGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.providersIndex);
}
}
}
// ...
}
// ...
}
这里可以看到,如果是debug模式或者首次安装或者更新了版本下,那么启动时都会重新扫描所有类,然后重建路由。然后将路由信息缓存到SP中。
如果是在非debug模式,并且不是首次启动则直接读取缓存中的路由信息,然后加载到内存。
首次启动时扫描APP下所有的类是一个比较耗时的操作,同时写入SP也是一个耗时操作。非首次打开的情况则只需要读取SP缓存然后加载到内存即可。
public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
try {
loadRouterMap();
if (registerByPlugin) {
logger.info(TAG, "Load router map by arouter-auto-register plugin.");
} else {
// ...
}
}
}
注意到init方法中会首先执行loadRouterMap()方法,这个方法实际上仅仅是一个空方法:
private static void loadRouterMap() {
registerByPlugin = false;
// auto generate register code by gradle plugin: arouter-auto-register
// looks like below:
// registerRouteRoot(new ARouter..Root..modulejava());
// registerRouteRoot(new ARouter..Root..modulekotlin());
}
并且方法注释中提示可以使用 arouter-auto-register
来实现自动注册。这里其实是一个hook点,我们可以通过arouter-auto-register 插件完成字节码插装的方式加载路由表。插装后的反编代码实现如下:
private static void loadRouterMap() {
registerByPlugin = true;
register("com.alibaba.android.arouter.routes.ARouter$$Root$$modulejava");
register("com.alibaba.android.arouter.routes.ARouter$$Root$$modulekotlin");
register("com.alibaba.android.arouter.routes.ARouter$$Root$$arouterapi");
register("com.alibaba.android.arouter.routes.ARouter$$Interceptors$$modulejava");
register("com.alibaba.android.arouter.routes.ARouter$$Providers$$modulejava");
register("com.alibaba.android.arouter.routes.ARouter$$Providers$$modulekotlin");
register("com.alibaba.android.arouter.routes.ARouter$$Providers$$arouterapi");
}
通过字节码插装避免了扫描所有类的操作,从而加快了ARouter的初始化,减少了启动时间耗时。
ARouter优化参考连接 :https://juejin.cn/post/6945610863730491422
对于主页面的优化可以考虑从布局文件着手,减少冗余布局、减少嵌套布局、减少OverDraw,或者使用异步加载的LayoutInflater。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
new AsyncLayoutInflater(this).inflate(R.layout.activity_main, null, new AsyncLayoutInflater.OnInflateFinishedListener() {
@Override
public void onInflateFinished(@NonNull View view, int resid, @Nullable ViewGroup parent) {
setContentView(view);
TextView textView = findViewById(R.id.text_view);
}
});
}
}
冷启动耗时最长,因此可以在用户非主动退出的情况下,只返回Home,不退出进程。主页面中重写onBackPressed方法,如下:
@Override
public void onBackPressed() {
try {
moveTaskToBack(false);
} catch(Exception ignored) {}
}
- JMM与volatile关键字
- synchronized的实现原理
- synchronized等待与唤醒机制
- AQS的实现原理
- ReentrantLock的实现原理
- ReentrantLock等待与唤醒机制
- CAS、Unsafe类以及Automic并发包
- ThreadLocal的实现原理
- 线程池的实现原理
- Java线程中断机制
- 多线程与并发常见面试题
- Android基础知识汇总
- MVC、MVP与MVVM
- SparseArray实现原理
- ArrayMap的实现原理
- SharedPreferences
- Bitmap
- Activity的启动模式
- Fragment核心原理
- 组件化项目架构搭建
- 组件化WebView架构搭建
- 为什么 Activity.finish() 之后 10s 才 onDestroy ?
- Binder与AIDL
- Binder实现原理
- Android系统启动流程
- InputManagerService
- WindowManagerService
- Choreographer详解
- SurfaceFlinger
- ViewRootImpl
- ActivityManagerService
- APP启动流程
- PMS安装与签名校验
- Dalvik与ART
- 内存优化策略
- UI界面及卡顿优化
- App启动优化
- ANR问题
- 包体积优化
- APK打包流程
- 电池电量优化
- Android屏幕适配
- 线上性能监控1--线上监控切入点
- 线上性能监控2--Matrix实现原理
- Glide实现原理
- OkHttp实现原理
- Retrofit实现原理
- RxJava实现原理
- RxJava中的线程切换与线程池
- LeakCanary实现原理
- ButterKnife实现原理
- ARouter实现原理
- Tinker实现原理
- 2. 两数相加
- 19.删除链表的倒数第 N 个结点
- 21. 合并两个有序链表
- 24. 两两交换链表中的节点
- 61. 旋转链表
- 86. 分隔链表
- 92. 反转链表 II
- 141. 环形链表
- 160. 相交链表
- 206. 反转链表
- 206 反转链表 扩展
- 234. 回文链表
- 237. 删除链表中的节点
- 445. 两数相加 II
- 面试题 02.02. 返回倒数第 k 个节点
- 面试题 02.08. 环路检测
- 剑指 Offer 06. 从尾到头打印链表
- 剑指 Offer 18. 删除链表的节点
- 剑指 Offer 22. 链表中倒数第k个节点
- 剑指 Offer 35. 复杂链表的复制
- 1. 两数之和
- 11. 盛最多水的容器
- 53. 最大子序和
- 75. 颜色分类
- 124.验证回文串
- 167. 两数之和 II - 输入有序数组 -169. 多数元素
- 189.旋转数组
- 209. 长度最小的子数组
- 283.移动0
- 303.区域和检索 - 数组不可变
- 338. 比特位计数
- 448. 找到所有数组中消失的数字
- 643.有序数组的平方
- 977. 有序数组的平方