Android 启动优化详解

一、启动耗时检测

启动定义:点击图标到 Launch Image 完全消失第一帧。

1. 查看Logcat

在Android Studio Logcat中过滤关键字“Displayed”,可以看到对应的冷启动耗时日志。

2. Adb shell

使用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

一般查看得到的TotalTime,即应用的启动时间,包括创建进程 + Application初始化 + Activity初始化到界面显示的过程。

该方法的特点:

  • 线下使用方便,不能带到线上
  • 2、非严谨、精确时间

3. 监控方法启动耗时

(1) 使用startMethodTracing

// 开启方法追踪
Debug.startMethodTracing(new File(getExternalFilesDir(""),"trace").getAbsolutePath(),8*1024*1024,1_000);
// 停止方法追踪
Debug.stopMethodTracing()

通过上述方法会在data/data/package下边生成trace文件,记录每个方法的时间,CPU信息。

特点: 由于startMethodTracing会记录所有方法的执行信息,所以对运行时性能有较大影响。

(2) 使用startMethodTracingSampling

// 开启方法采样追踪
Debug.startMethodTracingSampling(new File(getExternalFilesDir(""),"trace").getAbsolutePath(),8*1024*1024,1_000);
// 停止方法追踪
Debug.stopMethodTracing();

特点: 相比于Trace Java Methods会记录每个方法的时间、CPU信息,它会在应用的Java代码执行期间频繁采样捕获应用的调用堆栈,对运行时性能的影响比较小,能够记录更大的数据区域。

二、启动优化方案

1.启动窗口优化

默认情况下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>

此优化仅仅是视觉上的优化,并不能真正减少启动时间。

2. Application优化

如果在Application做了繁重的初始化操作,比如多个第三方库的初始化,会占用相当大的启动时间。因此需要根据自身业务进行优化,如将不影响业务的第三方库放到子线程中进行,如下:

private void initLazyComponent() {
    new Thread(new Runnable() {
        @Override
        public void run() {
        // 推送SDK的初始化
            pushSDKInit();
            // 分享SDK的初始化
            setupShareSDK();
            // 二维码库初始化
            ZXingLibrary.initDisplayOpinion(getContext());
            // 检查并清理缓存的操作
            checkAndClearStorage();
            // ...
        }
    }).start();
}

另外,对于耗时特别长的第三方框架进行重点优化。这里结合项目中使用的ARouter为例。

(1) 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缓存然后加载到内存即可。

(2) ARouter启动优化

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

3. 主页面的优化

对于主页面的优化可以考虑从布局文件着手,减少冗余布局、减少嵌套布局、减少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);
            }
        });

    }
}

4. 减少冷启动的次数

冷启动耗时最长,因此可以在用户非主动退出的情况下,只返回Home,不退出进程。主页面中重写onBackPressed方法,如下:

@Override
public void onBackPressed() {
    try {
        moveTaskToBack(false);
    } catch(Exception ignored) {}
}

教程来源于Github,感谢zhpanvip大佬的无私奉献,致敬!

技术教程推荐

重学前端 -〔程劭非(winter)〕

软件工程之美 -〔宝玉〕

浏览器工作原理与实践 -〔李兵〕

分布式技术原理与算法解析 -〔聂鹏程〕

分布式金融架构课 -〔任杰〕

爆款文案修炼手册 -〔乐剑峰〕

业务开发算法50讲 -〔黄清昊〕

Go进阶 · 分布式爬虫实战 -〔郑建勋〕

高并发系统实战课 -〔徐长龙〕