关于应用启动优化,这算是个老生常谈的话题了,相信大家对于一些基本概念应该是有一定认识了,比如启动方式的区分冷启动、热启动之类的,这篇文章主要是通过一个实例来分析各个环节需要处理的问题。下面就是直接上手环节。
明确概念
命令行: adb shell am start -W [PACKAGE-NAME]/[ACTIVITY-NAME]
有没有想过,我们启动一个只有 Empty Activity 的 App 是怎么样的,没有对比就没有伤害,那我们直接试试
场景创建
为了说明问题,我们先来模拟一个实际开发中常见的项目环境。通常我们项目中会接入一些第三方库来支撑起应用的基础服务,有些三方库会要求在应用启动时就进行初始化。又比如应用可能会有一些日志上报和事件统计的数据要在启动时发,还有像启动时读取本地配置,页面需要预加载资源内容等等。这些呢,都是现实业务场景中经常会遇到的一些内容,如果开发只考虑业务上的实现而不考虑性能问题,那么就很有可能会出现一些不太好的用户体验。比如说用户启动应用后要长时间等待,或着页面交互时卡顿等等,这些问题在一些旧设备或低端机上面会更明显。所以说找台性能差的设备多体验下还是很有必要的,这样可以更直观的暴露问题。
为什么出现白屏
冷启动白屏持续时间可能会很长,这可是个槽糕的体验,它的启动速度是由于以下引起的:
1、Application的onCreate流程,对于大型的APP来说,通常会在这里做大量的通用组件的初始化操作;
建议:很多第三方SDK都放在Application初始化,我们可以放到用到的地方才进行初始化操作。
2、Activity的onCreate流程,特别是UI的布局与渲染操作,如果布局过于复杂很可能导致严重的启动性能问题;
建议:Activity仅初始化那些立即需要的对象,xml布局减少冗余或嵌套布局。
优化APP启动速度意义重大,启动时间过长,可能会使用户直接卸载APP。
启动耗时分析
分析启动,除了上面提到的运用 ADB 命令来看,可以通过借助 Android Studio 中的工具来分析,这里推荐结合我前面一篇文章中提到的 Android 应用性能分析工具 — CPU Profiler
来使用吧,这里不作展开了。下面直接来看看实际优化的措施。
实际优化措施
1. 设置启动页面
设置一个 Activity 作为启动页是否有必要,这要根据自身实际业务考量吧,但是提升App 的启动速度肯定是有一定帮助的,因为通常应用的Home 页肯定会复杂很多。减少启动渲染耗时,从页面交互上考虑。
public class SplashActivity extends BaseActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 不设置 ContentView 可节约布局加载时间,使用主题的背景图片即可
getWindow().getDecorView().postDelayed(new Runnable() {
@Override
public void run() {
startActivity(MainActivity.class);
finish();
}
}, 1000);
}
}
2.设置启动动画
从用户点击桌面图标到应用呈现渲染,这个过程如果只是干等着,会显得应用是在点击以后没有反应,所以及时响应用户这个点击动作就显得非常有必要了。
<activity android:name=".SplashActivity"
android:theme="@style/AppTheme.Launcher"
android:launchMode="singleTask"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity
android:name=".GameActivity"
android:configChanges="keyboard|locale|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize"
android:hardwareAccelerated="true"
android:launchMode="singleTask"
android:screenOrientation="portrait"
android:theme="@style/AppTheme"
android:windowSoftInputMode="stateHidden|adjustResize"/>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="android:screenOrientation">portrait</item>
<item name="android:windowEnableSplitTouch">false</item>
<item name="android:splitMotionEvents">false</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowBackground">@null</item>
<item name="android:windowAnimationStyle">@style/Animation</item>
<item name="windowNoTitle">true</item>
</style>
<style name="AppTheme.Launcher" parent="AppTheme">
<item name="android:windowBackground">@drawable/launch_screens</item>
</style>
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 顶层启动图 -->
<item>
<bitmap
android:src="@drawable/splash_bg" />
</item>
</layer-list>
效果
3. 页面数据懒加载
关于懒加载的概念相信很多人都不会陌生,我们常常会在实际开发中接触到。这里的页面数据懒加载,其实就是为了减少用户在视觉上的等待时间,对于数据填充可以延后进行,减少用户页面切换时的等待。
// 懒加载
public class MainActivity extends Activity {
....
@Override
protected void onCreate(Bundle savedInstanceState) {
....
// 页面启动时懒加载
this.getWindow().getDecorView().post(new Runnable() {
@Override
public void run() {
mHandler.post(mLoadingRunnable);
}
});
}
}
private Runnable mLoadingRunnable = new Runnable() {
@Override
public void run() {
// 数据填充,懒加载
}
};
注意:如果配置上面的 Style 后发现点击桌面图标没有理解展示启动图,很有可能是配置了android:windowIsTranslucent属性导致的:
<item name="android:windowIsTranslucent">true</item>
Application 启动异步初始化第三方库
应用中很多三方库会要求在启动时初始化,并且建议在 Application 的 onCreate 中进行初始化方法调用,这就有可能导致了启动时方法处理任务过重,影响启动速度。
一般建议把三方库进行分类,考虑优先级,把除了把部分支持库的初始化延后到实际需要时才进行,也可在 Application 中执行异步加载的方式。
下面就举例通过 IntentService 的方式来进行三方库的集中异步初始化。
/**
* An {@link IntentService} subclass for handling asynchronous task requests in
* a service on a separate handler thread.
* helper methods.
* @author baishixian
*/
public class InitializeService extends IntentService {
/**
* IntentService can perform
*/
private static final String ACTION_INIT_WHEN_APP_CREATE = "gdut.bai.service.action.INIT";
public InitializeService() {
super("InitializeService");
}
/**
* Starts this service to perform action init with the given parameters. If
* the service is already performing a task this action will be queued.
*
* @see IntentService
*/
public static void startActionInit(Context context) {
Intent intent = new Intent(context, InitializeService.class);
intent.setAction(ACTION_INIT_WHEN_APP_CREATE);
context.startService(intent);
}
@Override
protected void onHandleIntent(Intent intent) {
if (intent != null) {
final String action = intent.getAction();
if (ACTION_INIT_WHEN_APP_CREATE.equals(action)) {
handleActionInit();
}
}
}
/**
* Handle action Init in the provided background thread with the provided
* parameters.
*/
private void handleActionInit() {
LogUtils.d("InitializeService handleActionInit begin " + System.currentTimeMillis());
DataCacheHelper.initDataCacheHelper(this.getApplicationContext());
String channelName = ChannelUtil.getChannelName(this.getApplicationContext());
// App ID: 在TalkingData Game Analytics创建应用后会得到App ID。
// 渠道 ID: 是渠道标识符,可通过不同渠道单独追踪数据。
TalkingDataGA.init(this.getApplicationContext(), "xxxxxxxx", channelName);
// AdTrack
TalkingDataAppCpa.init(this.getApplicationContext(),"xxxxxxxx", channelName);
// App Analytics
TCAgent.LOG_ON=true;
// App ID: 在TalkingData创建应用后,进入数据报表页中,在“系统设置”-“编辑应用”页面里查看App ID。
// 渠道 ID: 是渠道标识符,可通过不同渠道单独追踪数据。
TCAgent.init(this.getApplicationContext(), "xxxxxxxx", channelName);
// 如果已经在AndroidManifest.xml配置了App ID和渠道ID,调用TCAgent.init(this)即可;或与AndroidManifest.xml中的对应参数保持一致。
TCAgent.setReportUncaughtExceptions(true);
// 开启反作弊功能
TCAgent.setAntiCheatingEnabled(this.getApplicationContext(), true);
// 为了提高webview场景稳定性,及时发现并解决x5相关问题,
// 当客户端发生crash等异常情况并上报给服务器时带上x5内核相关信息
// 以bugly日志上报为例
CrashReport.UserStrategy strategy = new CrashReport.UserStrategy(this.getApplicationContext());
strategy.setCrashHandleCallback(new CrashReport.CrashHandleCallback() {
@Override
public Map<String, String> onCrashHandleStart(int crashType, String errorType, String errorMessage, String errorStack) {
LinkedHashMap<String, String> map = new LinkedHashMap<>();
String x5CrashInfo = com.tencent.smtt.sdk.WebView.getCrashExtraMessage(InitializeService.this.getApplicationContext());
map.put("x5crashInfo", x5CrashInfo);
return map;
}
@Override
public byte[] onCrashHandleStart2GetExtraDatas(int crashType, String errorType, String errorMessage, String errorStack) {
try {
return "Extra data.".getBytes("UTF-8");
} catch (Exception e) {
return null;
}
}
});
Bugly.setAppChannel(this.getApplicationContext(), channelName);
Bugly.init(this.getApplicationContext(), "xxxxxxxx", false, strategy);
LogUtils.d("InitializeService handleActionInit end " + System.currentTimeMillis());
}
}
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
InitializeService.startActionInit(this);
}
}
参考
Android应用启动优化:一种DelayLoad的实现和原理(下篇)
Android性能优化典范 - 第6季
Android Performance