×

第一章 初遇App冷启动

96
Me豪
2017.07.27 15:31* 字数 3432

回想起刚刚触碰到app的时候,整个地面,哦不对,是整个手机屏幕都变成白色了,而且好像过了好久才进入到了那个欢迎界面啊,好像打开的时候有倒计时什么的,没看清楚,怎么再试一次呢?

“退出app!!!”我大声喊道,哎呦,我去,还真的退出了啊,竟然可以进行声控的??厉害了我的哥,我也是惊呆了啊。

“请再次打开app”,突然底下一片白,等了个三秒钟左右才进入到了欢迎界面,然后在我眼前展示的是一系列超时代的高科技,在顶部显示着倒计时,然后下面是显示着各个初始化所耗的时间,一目了然,身临其境啊,无法想象是如何做到的。

等会,别感叹这个“高科技”先,这个可是我带大的app啊,在这个世界里启动时间无限放大了啊,之前还没感觉很长的啊,怎么会出现这样的情况呢?正当我充满疑虑的时候,发现底部有一些选项。

图一

这尼玛是什么选择啊,第四个选项竟然是“不管了,就这么着吧”,我就选择第四个看下,“选择第四个”,这时迟那时快,正中央弹出一个框,是这么写着“你确定要就这么着?选择是,你将从此只能虚度光阴,永劫不能超Android”,我心里在咒骂着,选择了否。“那就先选择第一个”,我倒要看看你葫芦里能卖什么药!

就在我选择第一个选项的一瞬间,app立刻重新启动了,并且是以慢动作50倍的方式重新启动的,还可以由我来调节这个倍率,真是到处都是高科技啊。随着app慢慢启动,倒计时开始出现,再接着,一段代码出现在我眼前。

public class AndroidStoryApplication extends Application{

    @Override
    public void onCreate() {
        super.onCreate();
        init();
    }

    private void init(){
        Context context = getApplicationContext();
        if (context == null) {
            return;
        }
        MiPushClient.registerPush(context, "", "");--------350ms
        TCAgent.LOG_ON = true;// TalkingData 统计SDK 打印统计的LOG
        TCAgent.init(context); // TalkingData 统计SDK 初始化
        TCAgent.setReportUncaughtExceptions(true); // TalkingData 统计SDK 自动捕获异常  ---------400ms
        //...以下是各种初始化第三方SDK
    }
}

怎么这段代码那么熟悉的?我去,这不是我做的那个app的代码吗?这样也行啊,竟然可以把我的代码完好无损的全部显示出来了。再仔细看下,每个初始化SDK的右边都有标记着耗时多少,加起来刚好三秒钟,不多不少,原来是初始化这些库还有数据的时候产生了耗时啊,这我就好奇了啊,怎么进行统计这个启动耗时的呢?“查看耗时统计方法”,我又对着空气喊道,做大事者不拘小节,对,就是这样的!

这时候,一堆文字从天而降。

当app启动时,任何一个地方有耗时操作都会拖慢我们应用的启动速度,而应用启动时间是用毫秒度量的,对于毫秒级别的快慢度量我们还是需要去精确的测量到到底应用启动花了多少时间,而根据这个时间来做衡量。从点击应用的启动图标开始创建出一个新的进程直到我们看到了界面的第一帧,这段时间就是应用的启动时间。所以我们统计的就是这一段时间,其中最为精确的时间统计方法就是使用 “adb shell” 命令的方式来进行统计。
adb shell am start -W [packageName]/[packageName.MainActivity]

当你执行完这个指令后会出现三个时间:

  1. ThisTime:一般和TotalTime时间一样,除非在应用启动时开了一个透明的Activity预先处理一些事再显示出主Activity,这样将比TotalTime小。
  2. TotalTime:应用的启动时间,包括创建进程+Application初始化+Activity初始化到界面显示。
  3. WaitTime:一般比TotalTime大点,包括系统影响的耗时。

看到这个时间后,瞬间我感觉整个人都不好了,然后我又重新执行了一遍,咦,怎么这次启动的时间少了那么多的,几百毫秒就能打开了,正当我满脑都是问号的时候,又一堆文字从天而降,“能不能不要从天而降了,从底下升起来多好啊”,我心里这样想的。

在安卓中应用的启动方式分为两种:冷启动和热启动。

  1. 冷启动:当启动应用时,后台没有该应用的进程,这时系统会重新创建一个新的进程分配给该应用,这个启动方式就是冷启动。
  2. 热启动:当启动应用时,后台已有该应用的进程(例:按back键,应用虽然会退出,但是该应用的进程是依然会保留在后台,可进入任务列表查看),所以在已有进程的情况下,这种启动会从已有的进程中来启动应用,这个方式叫热启动。

而刚刚第一次启动的时候就是所谓的冷启动,第二次启动就是热启动了,所以时间是有所不同的,了解了吗?

这,还会问我了解了吗?算你厉害。问题是知道了,但是有什么解决方案吗?回想了下刚刚的选择项,第三个好像就是查看解决方案的,但是后面还加了个需要充值,坑哥啊,还是收费的啊,作为初级程序员的我,衣着朴素,工资低,没钱啊!不管了,“查看解决方案”,我决定怎样都要探个究竟!

“恭喜你,踏出了人生的一大步,进入知识收费环节,钱在我们这个超级世界是没用的,你只要在学习完之后把系统提出的问题都回答正确之后,将可以进入下一个章节的学习,如果回答不了,那就,不好意思了,我们就此说拜拜吧!”

原来如此,不是要我的钱的,那还好,我深呼了一口气,然后继续看下去。

Application—>attachBaseContext()—>onCreate()—>Activity—>onCreate()—>配置主题中背景等属性—>onStart()—>onResume()—>测量布局绘制显示在界面上。很显然,我们需要优化这个启动时间,需要做的是Application从初始化到Activity显示这一过程,那么,如何去优化呢?

回来看代码,在 AndroidStoryApplication 的onCreate方法中,我们初始化了很多的一些第三方库,还有SharePreference的初始化,当然,你回想,如果不在这里初始化的话,一进入app就要使用到这些库或者数据的话,就会出现异常了啊,别急,且听我慢慢分解(还卖起关子来了)。

既然你都知道有些库或者数据必须要在Application初始化的时候进行初始化,那么我们就放在这里初始化,不过,我们一进来的时候并不着急初始化先,让它们乖乖待在那里,我们待会再来处理它们。跳过那些耗时的操作,我们是不是快速来到了欢迎界面?这个就是我们要达到的效果啊,接下来我们要处理的就是已经跳转到了欢迎界面了,如何进行哪些数据和第三方SDK的初始化。这里有两种方法可以供君选择。使用 IntentService ,另一种就是使用 EventBus 进行事件的回传。好吧,我知道你肯定会问这个 IntentService 是怎样的一个东西了。

简单说, IntentService 是继承于Service并处理异步请求的一个类,在IntentService内有一个工作线程来处理耗时操作,启动IntentService的方式和启动传统Service一样,同时,当任务执行完后,IntentService会自动停止,而不需要我们去手动控制。另外,可以启动IntentService多次,而每一个耗时操作会以工作队列的方式在IntentService的onHandleIntent回调方法中执行,并且,每次只会执行一个工作线程,执行完第一个再执行第二个,以此类推。而且,所有请求都在一个单线程中,不会阻塞应用程序的主线程(UI Thread),同一时间只处理一个请求。

public class InitializeService extends IntentService {    
    private static final String ACTION_INIT_WHEN_APP_CREATE = "com.mehao.androidstory.service.action.INIT";    
    public InitializeService() {        
        super("InitializeService");    
    }    
    public static void start(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)) {               
                //初始化
            }        
        }    
    }
}

我们看到InitializeService.java的代码中,有一个onHandleIntent的方法,这个就是根据不同action来进行操作的,所以我们可以通过在onHandleIntent中来初始化一些耗时的操作,然后在application中onCreate中开启就行了,是不是非常简单?

既然这个 IntentService 是一个异步请求的Service,那我们无法掌握是否初始化完了就进入main界面了啊,这是一个无法把控的一个过程。那接下来我们来讲解第二种方法,使用 EventBus 进行事件的回传,怎么个回传法呢?当跳转到欢迎界面了,我们不急着直接跳转到主界面,这时候我们进行事件回传到Application上,再进行初始化,等初始化完成了之后,再通知欢迎界面,我初始化好了,可以跳转了,欢迎界面就结束了,跳转到主界面,就是这么简单。

我瞬间有点懵逼了,大写的问号,什么跟什么啊,这个跳转那个跳转,这个传递然后又那个传递的啊?什么回事。

来来来,别懵逼了,我们来看改造过的代码吧。

public class StartPageActivity extends AppCompatActivity{

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.start_page_activity);

        EventBus.getDefault().register(this);

        EventBus.getDefault().post(new AndroidStoryEvent.IYourSuvInitEvent());// 通知Application要开始初始化数据了
    }

    public void onEventMainThread(AndroidStoryEvent.IYourSuvFinishInitEvent iYourSuvFinishInitEvent) {
        if (iYourSuvFinishInitEvent == null) {
            return;
        }
        // 初始化结束了,跳转到主界面吧
    }
}
public class AndroidStoryApplication extends Application{

    @Override
    public void onCreate() {
        super.onCreate();
        EventBus.getDefault().register(this);
//        init(); 不在这里进行初始化啦!!
    }

    private void init(){
        Context context = getApplicationContext();
        if (context == null) {
            return;
        }
        MiPushClient.registerPush(context, "", "");
        TCAgent.LOG_ON = true;// TalkingData 统计SDK 打印统计的LOG
        TCAgent.init(context); // TalkingData 统计SDK 初始化
        TCAgent.setReportUncaughtExceptions(true); // TalkingData 统计SDK 自动捕获异常
        //...以下是各种初始化第三方SDK
    }

    public void onEventMainThread(AndroidStoryEvent.IYourSuvInitEvent iYourSuvInitEvent) {
        if (iYourSuvInitEvent == null) {
            return;
        }
        init();// 在这里初始化数据啦!!!
        EventBus.getDefault().post(new AndroidStoryEvent.IYourSuvFinishInitEvent());// 初始化完成,回传到StartPageActivity
    }
}

看完之后,我恍然大悟,原来是这么个操作啊,心里就想,这也挺简单的嘛,就只是用到了EventBus来做事件的传递而已,不错不错,这个思路可以哦。

来,我们看看优化后还要多少耗时吧。

哎呦,不错了,瞬间就跳转到欢迎界面了,然后展示的是app的宣传页还有广告,有足够的时间给application中的数据和第三方SDK进行初始化了。不对,怎么刚刚点开的一瞬间,会闪一下白屏的?超级系统好像猜到了我在想什么那样,还没等我开口就对我说了,“孺子可教也!没错,刚刚确实还会闪一下白屏,这个我们再进行处理一下”,这...还孺子可教也,我也是无语了,别那么会调侃好么?

首先,我们要知道为什么会闪一下白屏的,我们看到 AndroidManifest.xml 配置文件,你会看到你的LAUNCHER activity的配置。

<activity    
    android:name=".StartPageActivity"    
    android:screenOrientation="portrait">    
        <intent-filter>        
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
</activity>

这个时候采用的是系统默认的主题theme,就会闪一下白屏。知道了问题所在就是这个主题上,那我们就有解决的方案了啊,把这个主题改为我们自定义的主题,然后我们自定义主题中设置为透明,那么这个问题不就迎刃而解了吗?

<style name="AppBlackTheme" parent="Theme.AppCompat.Light.NoActionBar">    
    <item name="android:windowIsTranslucent">true</item>
    <item name="android:windowBackground">@android:color/transparent</item>
</style>

然后再配到 AndroidManifest.xml 中去。

<activity    
    android:name=".StartPageActivity"    
    android:theme="@style/AppBlackTheme"
    android:screenOrientation="portrait">    
        <intent-filter>        
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
</activity>

试一下,重启app,嘿,还真的解决了闪白屏了啊!不错不错,又学到了。

好了,本期学习到此就要结束了,还记得我们刚开始立下的规定吗?知识付费,你只要回答出我的问题,就可以通过了,如果回答不出来,那不好意思,你将永远不能再进来学习了,准备好了吗?“准备好了”,这个时候我信心满满的。

问题一:

什么是冷启动和热启动?

问题二:

有什么方法可以优化启动耗时?

问题三:

启动过程闪白屏该如何处理?

哎呦,这也太简单了啊,不一会,我就回答出来了。

恭喜你,通过了本次考核,我们下次再见吧。“等会等会,以后我要怎么再进来学习啊?”,“只要你在心里默念AndroidStory,你就可以再次进来了,现在你赶紧回去把刚刚学到的利用到你的项目中去吧!”。

“砰...”,我睁开眼睛,看了下台面上的手机,这个梦做得也太真实了吧。不对,这不是梦,怎么我脑子里都是那些代码,难道我真的进入了超级世界进行学习了?不想了,赶紧把代码优化一番再说!

AndroidStory
Web note ad 1