牛骨文教育服务平台(让学习变的简单)

上一回说到,用户选择是否升级,若用户选择不升级,那么就要进入程序的主界面。下面要做的是从splash界面跳转到main界面。

MainActivity创建

1.首先新建MainActivity:

package com.liuhao.mobilesafe.ui;

import com.liuhao.mobilesafe.R;

import android.app.Activity;
import android.os.Bundle;

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //Set the activity content from a layout resource. The resource will be inflated, adding all top-level views to the activity
        setContentView(R.layout.main);
    }
   
} 

2.MainActivity对应的布局文件:main.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".SplashActivity" >

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/hello_world" />

</RelativeLayout> 

3.AndroidManifest.xml中配置MainActivity

 <activity android:name="com.liuhao.mobilesafe.ui.MainActivity"
            android:label="@string/main_screen">
        </activity>

至此便完成了MainActivity的创建,下面就是SplashActivity到MainActivity的切换。

SplashActivity到MainActivity的切换

在SplashActivity新建loadMainUI()方法

 private void loadMainUI(){
        Intent intent = new Intent(this, MainActivity.class);
        startActivity(intent);
        finish(); //将当前Activity从任务栈中移除
    }

然后在之前需要进入主界面的地方调用此方法即可:

  if(versiontext.equals(version)){
                Log.i(TAG, "版本号相同,无需升级,进入到主界面");
                loadMainUI();
                return false;
            }

②       

catch (Exception e) {
            e.printStackTrace();
            /**
             * Toast使用场景
             * 1、需要提示用户,但又不需要用户点击“确定”或者“取消”按钮。
             * 2、不影响现有Activity运行的简单提示。
             */
            Toast.makeText(this, "获取更新信息异常", 2).show();//弹出文本,并保持2秒
            Log.i(TAG, "获取更新信息异常,进入到主界面");
            loadMainUI();
            return false;
        }

③      

 builder.setNegativeButton("取消", new OnClickListener() {
           
            @Override
            public void onClick(DialogInterface dialog, int which) {
                Log.i(TAG, "用户取消升级,进入程序主界面");
                loadMainUI();
            }
        });

知识点

任务栈TaskStack和Activity的管理

上面的这段代码就是在一个activity里通过Intent启动另一个activity的实例。就像前面提到的,一个activity可以启动另一个,包括那些定义在不同应用程序中的。

一个activity就是一个用户界面,可以连续启动很多activity以提高用户体验。当然这个数量是有限的,这要根据手机的硬件配置来决定。上一篇文章中已经介绍,所有打开的activity都是存储在栈中的,所以如果打开的activity过多,会消占去很多的内存和处理器资源,严重时会导致当前应用无法启动或者系统崩溃。

activity是和任务紧密联系的。因为在android系统中,为了保持用户体验,用户想做某件事情是以任务的结构作为逻辑,以应用的形式来表现的。而一个应用又可以包含很多的activity,所以就涉及到activity和task的关系问题。

activity简单解释

Android中,Activity是所有程序的根本,所有程序的流程都运行在Activity之中。简单的的说,任务就是用户所体验到的“应用程序”。它是一组相关的activity,分配到 一个栈中。栈中的根activity,是任务的开始——一般来说,它是用户组应用程序加载器中选择的activity。在栈顶的activity正是当前 正在运行的——集中处理用户动作的那个。当一个activity启动了另外一个,这个新的activity将压入栈中,它将成为正在运行中的 activity。前一个activity保留在栈中。当用户按下后退按键,当前的这个activity将中栈中弹出,而前面的那个activity恢复 成运行中状态。

activity的生命周期:

Activity具有自己的生命周期,由系统控制生命周期,程序无法改变,但可以用onSaveInstanceState保存其状态。对于Activity,关键是其生命周期的把握(如下图),其次就是状态的保存和恢复(onSaveInstanceState onRestoreInstanceState),以及Activity之间的跳转和数据传输(intent)。

onCreate()------->onStart()-------->onResume()--------->onSaveInstanceState()----->onPause()------->onStop()--------->onDestroy().

Activity中常用的函数有SetContentView()   findViewById()    finish()   startActivity(),其生命周期涉及的函数有:

void onCreate(Bundle *savedInstanceState*)
void onStart()
void onRestart()
void onResume()
void onPause()
void onStop()
void onDestroy()

注意的是,Activity的使用需要在Manifest文件中添加相应的<Activity>,并设置其属性和intent-filter。

activity的各种状态的转换:

当启动一个activity时,首先是在onCreate处进行初始化界面所需要的信息,如:界面的views,buttons,分配的引用变量等;初始化完成后就会调用onStart,此时就可以看到界面了;当用户与界面进行交互时就会调用onResume函数;当此activity因为被其他activity没有完全覆盖而进入pause状态时就调用 onPause,当被其他activity完全覆盖时就调用onStop函数。在后面的这两种情况下都会调用onSaveInstanceState 方法来暂时保存被覆盖的activity的状态,在这些被覆盖的activity重新回到界面上的时候会恢复这些状态;当调用finish方法使,这个 activity就被destroy了,也就从栈中移除了。

一个手机应用程序通常包含多个Activities。每个Activity的设计,都是为了完成某种明确的功能及跳转到其他应用程序的Activity。比如,一个邮件收发应用程序,有一个Title的列表Activity,当点击列表标题时,跳转到另外一个Activity去显示邮件内容。

一个Activity中,也可以去打开另外一个在同一设备上的其他应用程序的Activity。比如,当你发送邮件时,你发出一个发送邮件的意图Intent,这个Intent包含了邮件地址,邮件消息。另外一个发送邮件应用程序定义了接收这个意图Intent的处理方式,那么就可以打开发送邮件的应用程序进行邮件发送了。如果多个应用程序支持处理这个意图Intent,那么系统会提示选择其中一个执行。当你发送完成之后,就会回到你自己的应用程序,这样看起来就好比邮件发送应用程序是你自己应用程序中的一部分。不管怎么样,毕竟是不同应用程序的Activities,是Android系统的Task让用户感知完全不一样了。

Task是一个Activities的收集器,专门收集用户操作交互所打开的Activity。这些Activities都被安排在一个回收栈back stack中,安排的顺序和它们打开的顺序一致。即先打开的安排在最底部,最后一个打开的安排在顶部。

设备的主屏幕就是为了放置更多的任务。当用户点击某个应用程序图标打开一个应用时,那么这个任务就处于前端。如果这个应用程序之前未被打开过,就会创建一个新的任务Task。

当当前的Activity打开其他Activities时,新打开的Activity处于back stack的最顶端并处于用户获取焦点状态,当前的Activity被保存置于stack中,处于stopped状态。当用户点击BACK键时,stack最顶部的Activity被销毁,前一个Activity被恢复。Back stack的操作遵循“后进先出”的原则。

Android 开发指南(一) 任务Task及回收栈back stack介绍 - Android  鱼 - Android 鱼

如果用户继续按返回,那么在栈中所有的activity都会被弹出,直到用户返回到主屏幕(或者到该任务开始的地方)。当所有的activity都从栈中移除后,任务就不复存在了。

一个Task任务是一个完整的单元,它可以运行于后台。当用户开启一个新的任务Task,或者通过HOME键跳到主屏幕时,这个任务就处于后台运行,不过这个Task包含所有的Activities都处于Stopped状态。但back stack维护者这个任务的完整数据,只是简单的被其他task替换焦点而已。

图2.两个任务:任务B到了前台,任务A于是被打入后台,伺机恢复.

举个例子,有三个activity在当前任务(任务A)的栈中--其中两个在当前activity的下面。这时,用户按下Home键回到主屏幕,然后启动了一个新的应用。当显示主屏幕时,任务A进入后台。当新应用启动时,系统为该应用启动了一个新任务(任务B)。当用户与该应用交互完毕之后,重新回到主界面并且选择任务A的应用。这时,任务A回到前台--栈中的三个activity都原封未动并且恢复在栈顶的activity。在这个时候,用户依然可以Home键返回主屏幕,选择任务B的应用图标来切换到任务B(也可以通过最近使用应用列表启动)。这就是android多任务的一个例子。

注:在后台可以同时存在多个任务。但是,如果用户在运行多个后台任务,系统可能会销毁后台activity来回收内存,导致activity的状态丢失。关于这方面内容后面小节讲述。

因为在回退栈中的activity从来不会被重排,如果你的应用允许用户从多个activity启动一个特定的activity,那么会新创建该activity的一个实例并且把它放到放到栈顶。因此,在你的应用中一个activity可能被实例化多次,如下图所示。因此,用户使用回退键返回,那么每个activity的实例会按照被打开的反向顺序被显示。但是,如果你不想把一个activity实例化多次,你可以修改这种行为。关于如何修改,我们稍后会在“任务管理”一节中讨论。

图3.一个activity被实例化多次.

让我们来总结一下activity和任务的默认行为:

  • 当Activity A启动Activity B,Activity A会停止,但是系统会保存Activity A的状态(例如滚动条位置,编辑框中的文字等)。如果在Activity B时,玩家按返回键,会使用保存的状态恢复Activity A。
  • 当用户按下Home键离开一个任务,当前的Activity会被停止并且当前任务会进入后台。系统保存该任务中所有Activity的状态。如果用户通过启动图标再次启动该任务,该任务会回到前台并且恢复栈顶端的Activity。
  • 如果用户按下回退键,当前的Activity会从栈中弹出并且销毁。栈中的前一个Activity被恢复。当一个Activity被销毁时,系统不会保存该Activity的状态。
  • Activity会被实例化多次,即使是由其他任务启动的。

保存Activity的状态

如前一节所述,系统默认下会在activity停止的时候保存其状态。如此一来,当用户导航到前一个activity时,其用户介面显示得跟离开时一样。然后,你可以—并且应该—提前使用你的activity的回调方法们保持它的状态,因为activity可能会被销毁然后被重新创建。当系统停止了你的一个activitie(比如当新的activity启动或任务被移到后台),系统可能为了释放内存会完全销毁那个activity。当这种情况发生时,activity的状态就会丢失。如果真发生了这种现象,系统依然知道那个activity在后退栈中占有一个位置,但是当activity被弄到前台时,系统必须重新创建它(而不是仅仅恢复它)。为了避免丢掉用户的工作,你应该通过实现activity的onSaveInstanceState() 来提前保存状态.

如何进行任务管理

刚才说明的那些行为,是activity和任务的默认行为。Android管理任务和后退栈的方式,如前面文章所述是通过把所有接连启动的 activity放在同一个任务中并且是同一个后进先出的栈中完成的。你不必要去担心你的Activites如何与Task任务进行交互,它们是如何存在于back stack中。但是,你也许想改变这种正常的管理方式。比如,你希望你的某个Activity在一个新的任务中进行管理,或者你只想对某个Activity实例化,又或者你想在用户离开任务时清理Task中的所有Activities,除了根Activity。

要完成这些特殊的管理方式,activity和任务的关联,activity在任务中的行为,受控于启动activity的行为对象的标志位和清单文件中的 元素的属性的互相作用。

  1. 在manifest文件中,对接收请求(即Intent对象)的activity组件设置一些属性。
  2. 在发送的请求(即Intent对象)中设置一些标记。

所以在请求者和接收者中都可以进行控制。

Activity在manifest可配置的属性有:

  • taskAffinity
  • launchMode
  • allowTaskReparenting
  • clearTaskOnLaunch
  • alwaysRetainTaskState
  • finishOnTaskLaunch

Intents 标识有:

  • FLAG_ACTIVITY_NEW_TASK
  • FLAG_ACTIVITY_CLEAR_TOP
  • FLAG_ACTIVITY_SINGLE_TOP

注意:大多数应用不应改变activity和任务的默认行为。如果你确定必须要改变默认行为,你必需小心并且保证测试了activity在启动时和后退导航到它时的可用性。确保测试了与用户习惯相冲突的导航行为。

定义启动模式

你可以通过定义运行模式来定义Activities如何与Task进行交互。定义的两种方式如下:

1.使用manifest文件

当你在你的manifest文件中声明一个activity时,你可以指定activity在启动时如何与任务相关联.

2.使用Intent的flag

当你调用startActivity()时,你可以在Intent中包含指明新的activity如何(或是否)与当前栈关联的flag.

例子:Activity A 启动 Activity B。如果B在manifest中定义了运行模式,并且A在启动B时,也在Intent中指定了B的运行模式,那么A在Intent的定义将覆盖B在manifest中的定义。

注:有些运行模式在manifest中定义有效未必在Intent中也有效,同样,在Intent定义有效的运行模式在manifest中未必生效。

如何使用manifest配置管理

在manifest配置文件中,你可以通过元素的的launchMode属性来指定4中不同的运行模式,指定activity如何与一个任务关联。launchMode属性指明了activity如何启动到一个任务中去。有四种不同的启动模式你可以用于指定给launchMode属性:

  1. Standard:标准默认模式

在这种默认模式下,Activity可以被多次实例化,也可以运行在多个Task中,一个Task可以拥有多个Activity实例。activity可以被多次实例化,每个实例可以属于不同的任务,也可以属于同一个任务.

  1. singleTop

在这种模式下,如果一个Activity实例已经存在于当前Task的最顶部,那么系统将调用onNewIntent()方法路由到这个实例,而不是创建一个新的Activity实例。

一个Activity可以被实例化多次,且可以从属于不同的Task任务,并且一个任务中可以存在多个Activity实例(这情况仅仅存在于Activity实例不在Task任务的顶端)。

假设一个任务的后退栈中有根ActivityA-B-C-D,D位于顶端:再开启D,back stack中的情形:

在标准模式下,一个新的类的实例被启动并且栈变为 A-B-C-D-D

在singleTop模式,那么这个已存在的ActivityD就通过onNewIntent()接收到intent,因为它在栈的顶端—栈于是依然保持为A-B-C-D

如果开启B

则在singleTop模式下为 A-B-C-D-B

注:当一个新的activity的实例被创建,用户可以按下后退键回到上一个activity。但当一个已存在的activity实例处理了一个新intent,用户就不能按下后退键回到当前的activity在intent来之前的状态.

  1. singTask

这种模式下,系统创建一个新的Task,并在Task的底部实例化Activities。然而,当一个activity的实例存在于另一个独立的Task时,系统不是去创建一个新的实例,而是调用onNewIntent()路由到其他任务的实例。在同一时间,只存在一个Activity实例。

注:尽管activity在一个新任务中启动,后退键依然可以返回到上一个activity.

  1. singInstance

于singTask相似,唯独一点不同的是,这个实例只能在一个单独的Task中使用。activity永远是任务的唯一;任何由这个activity启动的其它activity都在另一个任务中打开.

接上文,关于后退栈,先举个例子:

Android浏览器应用声明网页浏览activity必须在它自己的任务中打开—通过在元素中指定 singleTask启动模式。这表示如果你的应用发出一个intent来打开Android浏览器,它的activity不会放到你的应用所在的任务中。代替的是,可能一个新的任务为浏览器启动,或者,如果浏览器已经运行于后台,它所在的任务就被弄到前台并接受这个intent。

不论一个从一个新任务启动activity还是在一个已存在这种activity的任务中启动,后退键总是能后退到前一个activity。然而,如果你在任务A中启动一个声明为singleTask模式的activity,而这个activity可能在后台已有一个属于一个任务(任务B)的实例。任务B于是被弄到前台并处理这个新的intent。那么此时后退键会先一层层返回任务BActivity,然后再返回到任务A的顶端activity。图 4演示了这种情形.

Android Task和Back Stack详解 - philn - IT基础知识

图4.演示一个"singleTask"启动模式的acitvity如何被添加到一个后退栈中.如果这个activity已经是一个后台任务(任务B)自己的栈的一部分,那么整个后退栈被弄到前台,位于当前任务 (任务A)的上面.

注:你使用launchMode属性的指定的actvitiy的行为可以被intent的flag覆盖.

如何使用Intent配置管理

在启动Activity时,你可以通过传递一个Intent入参给startActivity()方法,来实现与manifest配置类似功能,改变Activity在task中的行为。

1.FLAG_ACTIVITY_NEW_TASK

在新的任务中启动activity-即不在本任务中启动。如果一个包含这个activity的任务已经在运行,那个任务就被弄到前台并恢复其UI状态,然后这个已存在的activity在onNewIntent()中接收新的intent。

这个标志产生与"singleTask"相同的行为。

2.FLAG_ACTIVITY_SINGLE_TOP

如果正启动的activity就是当前的activity(位于后退栈的顶端),那么这个已存在的实例就接收到onNewIntent()的调用,而不是创建一个新的实例。

这产生与"singleTop"模式相同的行为.

3.FLAG_ACTIVITY_CLEAR_TOP

如果要启动的activity已经在当前任务中运行,那么在它之上的所有其它的activity都被销毁掉,然后这个activity被恢复,而且通过onNewIntent(),initent被发送到这个activity(现在位于顶部了)

没有launchMode属性值对应这种行为。

FLAG_ACTIVITY_CLEAR_TOP多数时候与FLAG_ACTIVITY_NEW_TASK联用。可以达到这样的效果:找到在其他Task中存在的Activity,并将它放置到一个可以相应Intent的地方。如果是standard模式,那么它将从stack移除,并新建一个Activity去相应Intent,因为这种模式下,总是新建Activity。

注:如果Activity的启动模式是"standard",FLAG_ACTIVITY_CLEAR_TOP会导致已存在的activity被从栈中移除然后在这个位置创建一个新的实例来处理到来的intent.这是因为"standard"模式会导致总是为新的intent创建新的实例.

启动模式的区别

哪个任务存放着activity,用来对行为进行响应。**对“standard ”和“singleTop ”模式来说,这个任务是产生行为(并且调用startActivity() )的那个——除非行为对象包含了 FLAG_ACTIVITY_NEW_TASK 标记。在这种情况下,像前面那节Affinities and new tasks  描述的一样,将会选择一个不同的任务。

  • 它们是否可以有多个实例。"standard "和“singleTop ”类型的activity可以被实例化多次。它们可以属于多个任务,一个特定的任务也可以拥有同一个activity的多个实例。
    作为比较"singleTask "和"singleInstance "类型的activity只限定有一个实例。因为这些activity是任务的根。这个限制意味着,在设备上不能同时有超过一个任务的实例。
  • 是否能有其他的activity在它所在的任务中。"singleInstance "类型的 activity是它所在任务中唯一的activity。如果它启动了其他的activity,不管那个activity的启动模式如何,它都会加载到一个不同的任务中——好像行为对象中的FLAG_ACTIVITY_NEW_TASK 标记。在其他的方面,"singleInstance " 和"singleTask "模式是相同的。
    其他三种模式运行任务中有多个activity。"singleTask "总是任务中的根activity,但是它可以启动其他的activity并分配到它所在的任务中。"standard "和"singleTop "类型的activity可以出现在任务中的任何地方。
  • 是否启动一个新的实例来处理一个新的行为。对默认的"standard "模式来说,对于每一个行为都会创建一个新的实例来响应。每个实例只处理一个行为。对于"singleTop "模式,如果一个已经存在的实例位于目标任务activity栈的栈顶,那么他将被重用来处理这个行为。如果它不在栈顶,它将不会被重用,而是为行为创建一个新的实例,并压入栈中。

处理任务亲和关系

默认情况下,一个应用程序中的activity组件彼此之间是亲属关系,也就是说它们属于同一个任务栈。但是我们可以通过设置某个标签的taskAffinity属性来为这个activity组件设置亲属关系。在不同的应用程序中定义的 activity组件可以共用同一个亲属关系,或者在同一个的应用程序中定义的activity组件可以使用不同的亲属关系。

亲属关系会在两种情况下发挥作用:

1)负责激活activity组件的Intent对象中包含了FLAG_ACTIVITY_NEW_TASK标志。

在默认情况下,新建一个Activity,会调用startActivity()方法进入Task当中。它放置到和启动它的Activity相同的back stack。但是,如果启动的Intent包含了FLAG_ACTIVITY_NEW_TASK标识,系统将为这个Activity寻找一个不同的Task。通常是新建一个新的Task。但是也未必全是这样,如果存在一与之相同taskAffinity定义的Task,那么这个Activity将运行在那里,否则新建Task。

2)被激活的activity组件的allowTaskReparenting属性被设置为“true”。

若一个activity组件的allowTaskReparenting被置为“true”,则当与这个activity有相同的亲属关系的任务栈被切换到前台的时候,这个activity会从当前存在的任务栈中移动到与其有相同的亲属关系的任务栈中。

举个例子说明这个属性的作用:

假设一个选择城市查看天气的Activity是一个旅游应用程序的一部分。这个Activity与这个应用中的其他Activity有相同affinity,并且设置了这个属性为true。当你的其他Activity启动这个查看天气的Activity时,它和你的Activity属于同一个Task。但是,当你的旅游应用程序再次展现在前端时,这个查看天气的Activity会重新分配到旅游应用程序的Task中,并显示天气情况。

若从用户的角度来看,一个.apk文件包含了一个以上的“应用程序”,那你可能要为那些activity组件指定不同的亲属关系。

如何清理后退栈back stack

如果一个用户离开一个Task很长时间,系统会清理这个Task,当然除了根Activity。当用户回来时,只剩下这个根Activity,其他的都被销毁了。系统之所以这样做,是因为经过一大段时间之后,用户很可能已抛弃掉他们已经做的并且回到任务开始做一些新的事情。

以下属性可以设置并改变这种行为方式:

  • alwaysRetainTaskState

如果在Task的跟Activity中设置这个属性为true,默认的行为将不再发生,所有Activity的状态数据将永久保存。

  • clearTaskOnLaunch

如果在Task的跟Activity中设置这个属性为true,当用户离开或者回到这个Task时,都会清理除了根Activity之外的Activity。换句话说,它是与alwaysRetainTaskState反着来的.用户回到任务时永远见到的是初始状态,即使只离开了一小会。

  • finishOnTaskLaunch

类似clearTaskOnLaunch ,但只在一个单独的Activity中生效,而不是整个Task,它可以清理任何的Activity,包括跟Activity。你为当期会话保存Activity,当用户回来或者离去,都不将再恢复呈现。

启动一个task

通过将一个activity类型的intent-filter的动作设置为“android.intent.action.MAIN”,类别设置为 “android.intent.category.LAUNCHER”可以使这个activity实例称为一个任务栈的入口。拥有这种类型的 intent-filter的activity类型的图表和名字也会显示在application launcher中。

例如:

<activity... >
    <intent-filter... >
        <actionandroid:name="android.intent.action.MAIN" />
        <categoryandroid:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

一个intent类型的过滤器导致activity的一个图标和标签被显示于应用启动界面上,使得用户可以启动这个activity并且再次回到这个任务。第二个能力是很重要:用户必须能够使一个任务栈切换到后台,也可以随时将其切换到前台。出于这个原因,使activity在启动时新开任务栈的启动模式(即 “singleTask”和“singleInstance”模式)只应该被利用在拥有拥有“android.intent.action.MAIN”动作和“android.intent.category.LAUNCHER”类别的intent-filter的activity类型上。

想像一下如果没有这两个过滤器会发生什么:一个行为启动了"singleTask"模式的activity,启动了一个新的任务并且用户花了一些时间在这个任务上。然后用户按下了HOME键,这个任务被隐藏到了后台。因为没有在应用程序加载器上显示它,所以就没有办法返回到这个任务。

一个类似的麻烦事 FLAG_ACTIVITY_NEW_TASK 标志。如果这个标志导致activity启动了一个新任务,并且用户按下HOME键离开了它,必须有一些方法将用户引导回它。一些实体(像是通知管理器) 总是在一个外部的任务中启动activity,而不作为它们的一部分,所以他们总是将带有FLAG_ACTIVITY_NEW_TASK 标记的行为对象传递到startActivity() 。如果你有一个可以被外部实体使用这个标签调用的activity,要注意用户应该有办法返回到启动的任务。

但遇到那些你不希望用户能够回到一个activity的情况时怎么办呢?有办法:设置元素的finishOnTaskLaunch属性为"true"!

参考:

http://philn.blog.163.com/blog/static/10401475320135144639141/

http://blog.csdn.net/wbw1985/article/details/4916909

http://blog.csdn.net/oracleot/article/details/19036909

http://blog.163.com/cazwxy_12/blog/static/898763720122992149143/