App启动原理及过程详解
一、APP启动概述
在 Android 中把在 Launch 界面点击 App 图标(或快捷方式图标)到加载完成第一个 Activity 为止称为 APP 的启动,这是直观上的描述 APP 启动。在同一台手机上同一个 APP,两次启动一个 APP 所花的时间有可能不同,第一次会稍慢些第二次就比第一次稍快些,这是因为 Android 做了个巧妙的设计,把启动分为冷启动和热启动、首次启动(本质上也是冷启动)三大类:
1、冷启动 —— 当启动应用时,系统后台没有该应用的进程(对应的内存空间),这时系统会重新创建一个新的进程分配给该应用,这个启动方式就是冷启动。冷启动因为系统会重新创建一个新的进程分配给它,所以会先创建和初始化 Application 类,再创建和初始化 Activity 类,最后加载 Activity 对应的布局并显示在界面上。
2、热启动 —— 当启动应用时,后台已有该应用的进程(比如说按 back 键、home 键,应用虽然会从前台退出,但是该应用的进程是依然会保留在后台,可进入任务列表查看),所以在已有进程的情况下,这种启动会从已有的进程中来启动应用,这个方式叫热启动。热启动因为会从已有的进程中来启动,所以热启动就不会走 Application 这步了,而是直接走 Activity,所以热启动的过程不会创建和初始化 Application,因为一个应用从新进程的创建到进程的销毁,Application 只会初始化一次。
3、首次启动 —— 首次启动严格划分是冷启动中的一种特殊情况,之所以把首次启动单独列出来,一般来说,首次启动时间会比非首次启动要久,首次启动会做一些系统初始化工作,如缓存目录的生产、数据库的建立、SharedPreference 的初始化,如果存在多 dex 和插件的情况下,首次启动会有一些特殊需要处理的逻辑,而且对启动速度有很大的影响,所以首次启动的速度非常重要,毕竟影响用户对 App 的第一印象。
二、APP启动过程
说到 APP 启动过程就离不开三个概念:启动时间、白屏和黑屏
1、启动时间
从代码角度上来看,用户点击 Launcher 界面的 APP 图标到展示真正的 Activity 界面,这期间系统都是需要一定的时间准备(系统需要去分配对应的进程空间并加载对应的资源到内存中,而且一般来说时间不会很长,但是你在 APP 做了很多初始化工作就有可能变得很长,从一定程度上来说你的编码水平决定着启动时间的长短),这段时间就叫做启动时间。
2、白屏和黑屏
因为启动时间因 APP 而异,系统为了避免造成卡顿的误会,如果 APP 继承的主题是android:Theme.Light,则系统会主动预显示出一个白色背景的界面,如果不进行优化每次冷启动的时候都有可能先显示出白屏过了一段时间之后才真正跳转到我们的真正的 Activity,即所谓的白屏现象(至于为啥是白色的,是因为系统源码的样式资源里定义的颜色就是白色的,ctrl + 点击对应的主题一层层往下找就会发现 "Theme.AppCompat.Light" → "Base.Theme.AppCompat.Light" → "Base.V7.Theme.AppCompat.Light" → "Platform.AppCompat.Light" → "android:Theme.Holo.Light")
<style name="Platform.AppCompat.Light" parent="android:Theme.Holo.Light">
<item name="android:windowNoTitle">true</item>
<item name="android:windowActionBar">false</item>
<item name="android:buttonBarStyle">?attr/buttonBarStyle</item>
<item name="android:buttonBarButtonStyle">?attr/buttonBarButtonStyle</item>
<item name="android:borderlessButtonStyle">?attr/borderlessButtonStyle</item>
<!-- Window colors -->
<item name="android:colorForeground">@color/foreground_material_light</item>
<item name="android:colorForegroundInverse">@color/foreground_material_dark</item>
<item name="android:colorBackground">@color/background_material_light</item>
<item name="android:colorBackgroundCacheHint">@color/abc_background_cache_hint_selector_material_light</item>
<item name="android:disabledAlpha">@dimen/abc_disabled_alpha_material_light</item>
<item name="android:backgroundDimAmount">0.6</item>
黑白屏问题根源
<item name="android:windowBackground">@color/background_material_light</item>
</style>
反之如果 APP 主题没有继承 Theme.Holo.Light,则会显示黑屏,以上就是黑白屏的根源。
黑白屏的解决措施
系统进程在创建 Application 的过程中会产生一个 BackgroudWindow,等到 App 完成了第一次绘制,系统进程才会用 MainActivity 的界面替换掉原来的 BackgroudWindow,系统首先会读取当前 Activity 的 Theme,然后根据 Theme 中的配置来绘制,当 Activity 加载完毕后,才会替换为真正的界面。
1、设置 Windows 背景为透明
这种解决方案其实就相当于是欺骗用户,当用户点击 APP 时,系统会自动把 windowBacground 置为透明的,自然看不见黑白屏了,取而代之是看到系统当前的 Launcher 界面,容易造成点击了 APP 后没有响应的误解
,以前 QQ 和今日头条采用过这样的方式,不过可能会和转场动画冲突,遇到的较多都是因为继承关系错误造成的,还有适配 android8.0 的时候在 values-v26 中,取消 android:windowIsTranslucent 属性。
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- 将背景设成透明的 -->
<!-- 但是有 bug,在 Android 8.0 的设备上会出现打不开应用的情况,所以这个方法是个鸡肋,不推荐使用 -->
<item name="android:windowIsTranslucent">true</item>
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
解决启动 Android 应用程序时出现白屏或者黑屏的问题
以上方式都不是很友好,推荐使用下面的方式
2、单独定义用于特定 Activity 的 style(用这个)(闪屏界面)
这是 Google 官方提供的解决方案:通过对属性 android:windowBackground 来进行加载前的配置,较常见的是使用一个 layer-list 资源来作为 android:windowBackground 要显示的图,然后再替换上 Activity 真正的主题,具体步骤如下:
1、定义替换的背景 Drawable 资源文件
这里是通过 layer-list 来实现图片的叠加,让开发者可以自由组合。其中属性 android:opacity=“opaque” 是为了防止在启动的时候出现背景的闪烁,你也可以使用其他 Drawable 资源。
layer_splash.xml
@drawable/begin_page 闪屏页中的图片
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
android:opacity="opaque">
<item android:drawable="@color/white" />
<item>
<bitmap
android:gravity="center"
android:src="@drawable/begin_page" />
</item>
</layer-list>
2、单独定义 Activity 特有的预加载的样式,这里定义的样式是将要配置到对应的 Activity 上的。
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<!--将用于配置到特定的 Activity 上的样式-->
<style name="SplashStyle" parent="AppTheme">
<item name="android:windowBackground">@drawable/layer_splash</item>
</style>
</resources>
3、在清单 AndroidManifest 中给对应的 Activity 设置预加载的样式 style,需要注意一定是 Activity 的Theme,而不是 Application 的Theme
<activity android:name=".ui.main.activity.SplashActivity"
android:theme="@style/SplashStyle">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
4、在 Activity 加载真正的界面之前,将 Theme 还原回正常的 Theme(可以不写)
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
setTheme(R.style.AppTheme); // 还原回正常的 Theme
super.onCreate(savedInstanceState);
SystemClock.sleep(2000);
setContentView(R.layout.activity_main);
}
}
闪屏页中的逻辑
public class SplashActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_splash);
// 停留 2s
SystemClock.sleep(2000);
String token = (String) SpUtil.getParam(Constants.TOKEN, "");
// 判断有没有保存过,如果保存过就直接进入登录页面
Boolean b = (Boolean) SpUtil.getParam(Constants.INTRODUCTION, false);
// 判断之前有没有登陆过,登陆过直接跳主页面,没有登陆过就再判断有没有进入过引导页,进入过就直接进入登录页面,否则进入引导页
if (!token.equals("")) {
MainActivity.starAct(SplashActivity.this);
finish();
} else if (b) {
LoginActivity.startAct(SplashActivity.this,LoginActivity.TYPE_LOGIN);
finish();
} else {
Intent intent = new Intent(SplashActivity.this, IntroductionActivity.class);
startActivity(intent);
finish();
}
}
}
引导页面
指示器(是一个自定义view)
/**
* 用法:
* 初始化的时候
* mIndicator.initSize(80, 32, 6);
* mIndicator.setNumbers(3);
* 和 ViewPager 关联的时候
* setSelectedState(int position)
*/
public class PreviewIndicator extends LinearLayout {
// 指示器个数
private int INDICATOR_COUNT = 3;
private List<ImageView> mImageList = new ArrayList<>();
// 选中时对应指示器点的宽度
private int chooseSize = 80; // 80
// 未选中时指示器点的宽度
private int nomalSize = 32; // 32
// 指示器高度
private int height = 10; // 10
public PreviewIndicator(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public PreviewIndicator(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView();
}
// 设置指示器点的个数
public void setNumbers(int number) {
INDICATOR_COUNT = number;
initView();
}
// 初始化所有的指示器点
private void initView() {
// initSize();
removeAllViews();
mImageList.clear();
for (int i = 0; i < INDICATOR_COUNT; i++) {
ImageView imageView = new ImageView(getContext());
if (i == 0) {
setSelectedState(imageView);
} else {
setNomalState(imageView);
}
addView(imageView);
mImageList.add(imageView);
}
}
/**
* 初始化指示器点的宽高参数,单位 dp
* @param chosedSize 选中指示器点的宽度
* @param nomal 未选中时指示器点的宽度
* @param hei 指示器点的高度
*/
public void initSize(int chosedSize, int nomal, int hei) {
chooseSize = SystemUtil.dp2px(getContext(), chosedSize);
nomalSize = SystemUtil.dp2px(getContext(), nomal);
height = SystemUtil.dp2px(getContext(), hei);
}
/**
* 设置未选中指示器点的参数
* @param imageView
*/
private void setNomalState(ImageView imageView) {
imageView.setImageDrawable(ContextCompat.getDrawable(getContext(), R.drawable.bg_eaeaea_r6));
LayoutParams params = new LayoutParams(nomalSize, height);
params.setMargins(height, 0, 0, 0);
imageView.setLayoutParams(params);
}
/**
* 设置选中指示器点的宽度
* @param imageView
*/
private void setSelectedState(ImageView imageView) {
imageView.setImageDrawable(ContextCompat.getDrawable(getContext(), R.drawable.bg_fa6a13_r6));
LayoutParams params = new LayoutParams(chooseSize, height);
params.setMargins(height, 0, 0, 0);
imageView.setLayoutParams(params);
}
/**
* 设置哪个指示器的点为选中的点
* @param position 该选中的点
*/
public void setSelected(int position) {
for (int i = 0; i < mImageList.size(); i++) {
ImageView imageView = mImageList.get(i);
int curPosition = position % mImageList.size();
if (i == curPosition) {
setSelectedState(imageView);
} else {
setNomalState(imageView);
}
}
}
}
Activity 对应的 xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.IntroductionActivity"
android:fitsSystemWindows="true">
<android.support.v4.view.ViewPager
android:id="@+id/viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent">
</android.support.v4.view.ViewPager>
<com.example.lenovo.everywheretravel.widget.PreviewIndicator
android:id="@+id/previewIndicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_marginBottom="@dimen/dp_61"
android:layout_centerHorizontal="true">
</com.example.lenovo.everywheretravel.widget.PreviewIndicator>
<Button
android:id="@+id/experience"
android:layout_width="@dimen/dp_127"
android:layout_height="@dimen/dp_40"
android:text="@string/experience"
android:textSize="@dimen/sp_16"
android:textColor="@color/c_ffffff"
android:background="@drawable/color_ff9d53_fa6a13"
android:layout_alignParentBottom="true"
android:layout_marginBottom="@dimen/dp_43"
android:layout_centerHorizontal="true"
android:visibility="gone" />
</RelativeLayout>
3 个 view(基本一样)
item_main_content_one.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/guide_01"
android:scaleType="centerCrop"/>
<TextView
android:id="@+id/tv1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="@dimen/sp_30"
android:layout_marginTop="@dimen/dp_100"
android:layout_centerHorizontal="true"/>
</RelativeLayout>
Activity
public class IntroductionActivity extends BaseActivity<EmptyView, EmptyPresenter> implements EmptyView {
@BindView(R.id.viewpager)
ViewPager viewpager;
@BindView(R.id.previewIndicator)
PreviewIndicator previewIndicator;
@BindView(R.id.experience)
Button experience;
private ArrayList<View> arrayList;
private IntroductionViewPagerAdapter introductionViewPagerAdapter;
@Override
protected EmptyPresenter initPresenter() {
return new EmptyPresenter();
}
@Override
protected int initLayoutId() {
return R.layout.activity_introduction;
}
@Override
protected void initView() {
super.initView();
arrayList = new ArrayList<>();
initViewOne();
initViewTwo();
initViewThree();
previewIndicator.setNumbers(arrayList.size());
introductionViewPagerAdapter = new IntroductionViewPagerAdapter(arrayList, IntroductionActivity.this);
viewpager.setAdapter(introductionViewPagerAdapter);
}
@Override
protected void initListener() {
super.initListener();
// viewpager 滑动监听
viewpager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int i, float v, int i1) {
}
@Override
public void onPageSelected(int i) {
previewIndicator.setSelected(i);
switch (i) {
case 2:
// 隐藏指示器,显示“立即体验”按钮
previewIndicator.setVisibility(View.GONE);
experience.setVisibility(View.VISIBLE);
break;
case 0:
case 1:
// 隐藏“立即体验”按钮,显示指示器
previewIndicator.setVisibility(View.VISIBLE);
experience.setVisibility(View.GONE);
break;
}
}
@Override
public void onPageScrollStateChanged(int i) {
}
});
// 点击立即体验保存一个状态,当应用下次启动时就不进入引导页面了
experience.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
SpUtil.setParam(Constants.INTRODUCTION,true);
LoginActivity.startAct(IntroductionActivity.this,LoginActivity.TYPE_LOGIN);
finish();
}
});
}
private void initViewOne() {
View view1 = LayoutInflater.from(this).inflate(R.layout.item_introduction_one, null);
SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(getResources().getString(R.string.introduction_text1));
// 前景色
ForegroundColorSpan foregroundColorSpan1 = new ForegroundColorSpan(getResources().getColor(R.color.c_78d9ff));
spannableStringBuilder.setSpan(foregroundColorSpan1, 0, 6, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
ForegroundColorSpan foregroundColorSpan2 = new ForegroundColorSpan(getResources().getColor(R.color.c_fa6a13));
spannableStringBuilder.setSpan(foregroundColorSpan2, 6, 10, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
TextView tv1 = view1.findViewById(R.id.tv1);
tv1.setText(spannableStringBuilder);
arrayList.add(view1);
}
private void initViewTwo() {
View view2 = LayoutInflater.from(this).inflate(R.layout.item_introduction_two, null);
SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(getResources().getString(R.string.introduction_text2));
// 前景色
ForegroundColorSpan foregroundColorSpan1 = new ForegroundColorSpan(getResources().getColor(R.color.c_78d9ff));
spannableStringBuilder.setSpan(foregroundColorSpan1, 4, 8, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
ForegroundColorSpan foregroundColorSpan2 = new ForegroundColorSpan(getResources().getColor(R.color.c_fa6a13));
spannableStringBuilder.setSpan(foregroundColorSpan2, 0, 4, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
TextView tv2 = view2.findViewById(R.id.tv2);
tv2.setText(spannableStringBuilder);
arrayList.add(view2);
}
private void initViewThree() {
View view3 = LayoutInflater.from(this).inflate(R.layout.item_introduction_three, null);
SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(getResources().getString(R.string.introduction_text3));
// 前景色
ForegroundColorSpan foregroundColorSpan1 = new ForegroundColorSpan(getResources().getColor(R.color.c_78d9ff));
spannableStringBuilder.setSpan(foregroundColorSpan1, 4, 8, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
ForegroundColorSpan foregroundColorSpan2 = new ForegroundColorSpan(getResources().getColor(R.color.c_fa6a13));
spannableStringBuilder.setSpan(foregroundColorSpan2, 0, 4, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
TextView tv3 = view3.findViewById(R.id.tv3);
tv3.setText(spannableStringBuilder);
arrayList.add(view3);
}
}