android 学习笔记2


1、new Handler(Looper.getMainLooper())

Handler一定要在主线程实例化吗?new Handler()和new Handler(Looper.getMainLooper())的区别
如果你不带参数的实例化:Handler handler = new Handler();那么这个会默认用当前线程的looper
一般而言,如果你的Handler是要来刷新操作UI的,那么就需要在主线程下跑。
情况:
1.要刷新UI,handler要用到主线程的looper。那么在主线程 Handler handler = new Handler(); 如果在其他线程,也要满足这个功能的话,要

Handler handler = new Handler(Looper.getMainLooper());

2.不用刷新ui,只是处理消息。 当前线程如果是主线程的话,Handler handler = new Handler(); 不是主线程的话,

Looper.prepare();
Handler handler = new Handler();
Looper.loop();

或者

Handler handler = new Handler (Looper.getMainLooper());

若是实例化的时候用 Looper.getMainLooper() 就表示放到主UI线程去处理。
如果不是的话,因为只有UI线程默认 Loop.prepare();  Loop.loop() 过,其他线程需要手动调用这两个,否则会报错。

2、LayoutParams

先来看两个例子:

     LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(
                LinearLayout.LayoutParams.MATCH_PARENT,
                LinearLayout.LayoutParams.MATCH_PARENT);
        layoutParams.bottomMargin = 20;
        LinearLayout linearLayout = new LinearLayout(this);


        Button button = new Button(this);
        button.setText("What");

        TextView tv = new TextView(this);
        tv.setText("What1");
        //调用Activity的addContentView函数
        linearLayout.addView(tv);
        linearLayout.addView(button);

        this.addContentView(linearLayout, layoutParams);
     // 第二个例子
        setContentView(R.layout.activity_main);
        LinearLayout linearLayout = (LinearLayout) findViewById(R.id.ll);
        Button button = new Button(this);
        button.setText("What");

        TextView tv = new TextView(this);
        tv.setText("What1");

        LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(
                LinearLayout.LayoutParams.MATCH_PARENT,
                LinearLayout.LayoutParams.WRAP_CONTENT);
        linearLayout.addView(tv, layoutParams);
        linearLayout.addView(button, layoutParams);

LayoutParams 类是用于child view(子视图) 向 parent view(父视图)传达自己的意愿的一个东西(孩子想变成什么样向其父亲说明)。其实子视图父视图可以简单理解成
一个LinearLayout 和 这个 LinearLayout 里边一个 TextView 的关系 TextView 就算 LinearLayout 的子视图 child view 。需要注意的是LayoutParams 只是 ViewGroup 的一个内部类这里边这个也就是ViewGroup里边这个 LayoutParams 类是 base class 基类实际上每个不同的ViewGroup 都有自己的 LayoutParams 子类。

因此我们可以看到都是将 LayoutParams 用到子 view 身上。

在继承BaseAdapter的时候,用getView返回View的时候,用代码控制布局,需要用到 View.setLayoutParams,但是报错了,报的是类型转换错误,经过研究,发现,这里不能使用ViewGroup.LayoutParams而必须使用对应父View的LayoutParams类型。如:某 View 被 LinearLayout 包含,则该View 的 setLayoutParams 参数类型必须是 LinearLayout.LayoutParams。

原因在于 LinearLayout(或其他继承自 ViewGroup 的layout,如:RelativeLayout )在进行递归布局的时候,LinearLayout 会获取子 View 的 LayoutParams,并强制转换成 LinearLayout.LayoutParams,如

1LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();

或者是如下定义:

1LayoutParams lp = (LayoutParams) child.getLayoutParams();

以转换成内部类型 LinearLayout.LayoutParams。

自己测试运行的时候报空指针,原因为 child.getLayoutParams(); 这里没有获得到子控件所在的布局,查看代码发现 parent.addView(child); 应该写在上面,之后 child 才能 getLayoutParams();

 在看一个例子:

//第一个参数为宽的设置,第二个参数为高的设置。  (用的时候注意修改LinearLayout前缀,其实很多时候可以不写它的)  
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(       
LinearLayout.LayoutParams.FILL_PARENT,       
LinearLayout.LayoutParams.WRAP_CONTENT       
);   
  
//设置居中显示:  
lp.gravity = Gravity.CENTER;  
//设置它的上下左右的margin:4个参数按顺序分别是左上右下  
lp.setMargins(10,10,10,10);  
  
//还可以这样添加规则:  
lp.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, RelativeLayout.TRUE);   
lp.addRule(RelativeLayout.ALIGN_PARENT_TOP, RelativeLayout.TRUE);   
  
//给某个View设置LayoutParams参数:  
btn1.setLayoutParams(lp);  
  
//最后将添加View到Layout中:(如果是在某个自定义的Layout布局中,前缀也可以省略)  
mLayout.addView(textView, lp);   
//其实不用LayoutParams也不会死啦  
mLayout.addView(textView, tvWidth, tvHeight);  

在看一个自定义 view 的例子

public class Card extends FrameLayout {  
  
    public Card(Context context) {  
        super(context);  
  
        LayoutParams lp = null;  
  
        background = new View(getContext());  
        lp = new LayoutParams(-1, -1);  
        lp.setMargins(10, 10, 0, 0);  
        background.setBackgroundColor(0x33ffffff);  
        addView(background, lp);  
  
        label = new TextView(getContext());  
        label.setTextSize(28);  
        label.setGravity(Gravity.CENTER);  
  
        lp = new LayoutParams(-1, -1);  
        lp.setMargins(10, 10, 0, 0);  
        addView(label, lp);  
  
        setNum(0);  
    }  
}  

3、paint 中获取文字的宽度

在使用Canvas绘制文字时,需要得到字符串的长度,Paint类内给了两个方法,measureText(),getTextBound();

可是对于同于字符串两个方法得出来的值有些差别:

getTextBounds() 得到的宽度总是比 measureText() 得到的宽度要小一点。

其中紫色的是 measureText(); 红色的标记是 getTextBounds。其实我们从方法名字也可以看出区别,一个是取宽度,一个是取字体边界。

 

4、文字基准定义和写字

  • 基准点是baseline 

  • ascent:是baseline之上至字符最高处的距离 

  • descent:是baseline之下至字符最低处的距离 

  • leading:是上一行字符的descent到下一行的ascent之间的距离,也就是相邻行间的空白距离 

  • top:是指的是最高字符到baseline的值,即ascent的最大值 

  • bottom:是指最低字符到baseline的值,即descent的最大值

注意 Y 坐标基准点是在 BaseLine 那个位置,而非 AscentLine 或 Top 那个位置。 还有就是此处的 height 指的是行高,而不是字体的高度。           

Paint的ascent和descent方法--算高度

这两个是native方法
//Return the distance above (negative) the baseline (ascent) based on the current typeface字体 and text size.
public native float ascent();
//Return the distance below (positive) the baseline (descent) based on the current typeface and text size.
public native float descent();
可以通过 mPaint.ascent()+ mPaint.descent() 获取文字高度
/** 
* text:要绘制的文字 
* x:绘制原点x坐标 
* y:绘制原点y坐标 
* paint:用来做画的画笔 
*/  
public void drawText(String text, float x, float y, Paint paint)  

上面这个构造函数是最常用的drawText方法,传进去一个String对象就能画出对应的文字。

但这里有两个参数需要非常注意,表示原点坐标的x和y.很多同学可能会认为,这里传进去的原点参数(x,y)是所在绘制文字所在矩形的左上角的点。但实际上并不是!比如,我们上面如果要画"harvic's blog"这几个字,这个原点坐标应当是下图中绿色小点的位置

在(x,y)中最让人捉急的是y坐标,一般而言,(x,y)所代表的位置是所画图形对应的矩形的左上角点。但在drawText中是非常例外的,y所代表的是基线的位置!

 

5、fresco 网上获取图片

/**
     * 获取服务端的图片
     * 为了避免图片的尺寸过大,我们要对其进行一定的大小控制,
     * 避免其过大撑破影响其他模板
     *//*
    private void prefetchImageTag(String url, final ImageView imageView) {
        ImageRequest request = ImageRequestBuilder.newBuilderWithSource(Uri.parse(url)).build();
        Fresco.getImagePipeline().fetchDecodedImage(request, FeedRuntime.getAppContext()).subscribe(
                new BaseBitmapDataSubscriber() {
                    @Override
                    protected void onNewResultImpl(@Nullable Bitmap bitmap) {
                        if (bitmap != null) {
                            imageView.setImageBitmap(bitmap);
                        }
                    }

                    @Override
                    protected void onFailureImpl(DataSource> dataSource) {
                    }
                }, UiThreadImmediateExecutorService.getInstance());
    }*/

 

6、Looper.loop()为什么不会阻塞主线程

Android是基于事件驱动的,即所有Activity的生命周期都是通过Handler事件驱动的。loop方法中会调用MessageQueue的next方法获取下一个message,当没有消息时,基于Linux pipe/epoll机制会阻塞在loop的queue.next()中的nativePollOnce()方法里,并不会消耗CPU。

7、IdleHandler

IdleHandler 是一个回调接口,可以通过MessageQueue的addIdleHandler添加实现类。当MessageQueue中的任务暂时处理完了(没有新任务或者下一个任务延时在之后),这个时候会回调这个接口,返回false,那么就会移除它,返回true就会在下次message处理完了的时候继续回调。

       Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
            @Override
            public boolean queueIdle() {
                //此处添加处理任务
                return false;
            }
        });

queueIdle()返回true表示可以反复执行该方法,即执行后还可以再次执行;返回false表示执行完该方法后会移除该IdleHandler,即只执行一次。

注意,在主线程中使用时queueIdle中不能执行太耗时的任务。

getWidth()方法和getMeasureWidth()区别 

首先getMeasureWidth()方法在measure()过程结束后就可以获取到了,而getWidth()方法要在layout()过程结束后才能获取到。另外,getMeasureWidth()方法中的值是通过setMeasuredDimension()方法来进行设置的,而getWidth()方法中的值则是通过视图右边的坐标减去左边的坐标计算出来的。

requestLayout,invalidate,postInvalidate区别与联系 

  • 相同点:三个方法都有刷新界面的效果。

  • 不同点:invalidate和postInvalidate只会调用onDraw()方法;requestLayout则会重新调用onMeasure、onLayout、onDraw。

调用了invalidate方法后,会为该View添加一个标记位,同时不断向父容器请求刷新,父容器通过计算得出自身需要重绘的区域,直到传递到ViewRootImpl中,最终触发performTraversals方法,进行开始View树重绘流程(只绘制需要重绘的视图)。
调用requestLayout方法,会标记当前View及父容器,同时逐层向上提交,直到ViewRootImpl处理该事件,ViewRootImpl会调用三大流程,从measure开始,对于每一个含有标记位的view及其子View都会进行测量onMeasure、布局onLayout、绘制onDraw。
Android View 深度分析requestLayout、invalidate与postInvalidate

Binder机制,共享内存实现原理

 为什么使用Binder?

概念
进程隔离
进程空间划分:用户空间(User Space)/内核空间(Kernel Space)
系统调用:用户态与内核态

原理
跨进程通信是需要内核空间做支持的。传统的 IPC 机制如管道、Socket 都是内核的一部分,因此通过内核支持来实现进程间通信自然是没问题的。但是 Binder 并不是 Linux 系统内核的一部分,那怎么办呢?这就得益于 Linux 的动态内核可加载模块(Loadable Kernel Module,LKM)的机制;模块是具有独立功能的程序,它可以被单独编译,但是不能独立运行。它在运行时被链接到内核作为内核的一部分运行。这样,Android 系统就可以通过动态添加一个内核模块运行在内核空间,用户进程之间通过这个内核模块作为桥梁来实现通信。

在 Android 系统中,这个运行在内核空间,负责各个用户进程通过 Binder 实现通信的内核模块就叫 Binder 驱动(Binder Dirver)。

那么在 Android 系统中用户进程之间是如何通过这个内核模块(Binder 驱动)来实现通信的呢?难道是和前面说的传统 IPC 机制一样,先将数据从发送方进程拷贝到内核缓存区,然后再将数据从内核缓存区拷贝到接收方进程,通过两次拷贝来实现吗?显然不是,否则也不会有开篇所说的 Binder 在性能方面的优势了。

这就不得不通道 Linux 下的另一个概念:内存映射。

Binder IPC 机制中涉及到的内存映射通过 mmap() 来实现,mmap() 是操作系统中一种内存映射的方法。内存映射简单的讲就是将用户空间的一块内存区域映射到内核空间。映射关系建立后,用户对这块内存区域的修改可以直接反应到内核空间;反之内核空间对这段区域的修改也能直接反应到用户空间。

一次完整的 Binder IPC 通信过程通常是这样:

  • 首先 Binder 驱动在内核空间创建一个数据接收缓存区;

  • 接着在内核空间开辟一块内核缓存区,建立内核缓存区和内核中数据接收缓存区之间的映射关系,以及内核中数据接收缓存区和接收进程用户空间地址的映射关系;

  • 发送方进程通过系统调用 copyfromuser() 将数据 copy 到内核中的内核缓存区,由于内核缓存区和接收进程的用户空间存在内存映射,因此也就相当于把数据发送到了接收进程的用户空间,这样便完成了一次进程间的通信。

Binder通讯模型
Binder是基于C/S架构的,其中定义了4个角色:Client、Server、Binder驱动和ServiceManager。

  • Binder驱动:类似网络通信中的路由器,负责将Client的请求转发到具体的Server中执行,并将Server返回的数据传回给Client。

  • ServiceManager:类似网络通信中的DNS服务器,负责将Client请求的Binder描述符转化为具体的Server地址,以便Binder驱动能够转发给具体的Server。Server如需提供Binder服务,需要向ServiceManager注册。
    具体的通讯过程

  • Server向ServiceManager注册。Server通过Binder驱动向ServiceManager注册,声明可以对外提供服务。ServiceManager中会保留一份映射表。

  • Client向ServiceManager请求Server的Binder引用。Client想要请求Server的数据时,需要先通过Binder驱动向ServiceManager请求Server的Binder引用(代理对象)。

  • 向具体的Server发送请求。Client拿到这个Binder代理对象后,就可以通过Binder驱动和Server进行通信了。

  • Server返回结果。Server响应请求后,需要再次通过Binder驱动将结果返回给Client。

ServiceManager是一个单独的进程,那么Server与ServiceManager通讯是靠什么呢?
当Android系统启动后,会创建一个名称为servicemanager的进程,这个进程通过一个约定的命令BINDERSETCONTEXT_MGR向Binder驱动注册,申请成为为ServiceManager,Binder驱动会自动为ServiceManager创建一个Binder实体。并且这个Binder实体的引用在所有的Client中都为0,也就说各个Client通过这个0号引用就可以和ServiceManager进行通信。Server通过0号引用向ServiceManager进行注册,Client通过0号引用就可以获取到要通信的Server的Binder引用。
写给 Android 应用工程师的 Binder 原理剖析
一篇文章了解相见恨晚的 Android Binder 进程间通讯机制

序列化的方式

Serializable是Java提供的一个序列化接口,是一个空接口,用于标示对象是否可以支持序列化,通过ObjectOutputStrean及ObjectInputStream实现序列化和反序列化的过程。注意可以为需要序列化的对象设置一个serialVersionUID,在反序列化的时候系统会检测文件中的serialVersionUID是否与当前类的值一致,如果不一致则说明类发生了修改,反序列化失败。因此对于可能会修改的类最好指定serialVersionUID的值。
Parcelable是Android特有的一个实现序列化的接口,在Parcel内部包装了可序列化的数据,可以在Binder中自由传输。序列化的功能由writeToParcel方法来完成,最终通过Parcel的一系列write方法完成。反序列化功能由CREAOR来完成,其内部标明了如何创建序列化对象和数组,并通过Parcel的一系列read方法来完成反序列化的过程。

Fragment的懒加载实现

Fragment可见状态改变时会被调用setUserVisibleHint()方法,可以通过复写该方法实现Fragment的懒加载,但需要注意该方法可能在onVIewCreated之前调用,需要确保界面已经初始化完成的情况下再去加载数据,避免空指针。

RecyclerView与ListView(缓存原理,区别联系,优缺点) 

缓存区别:

层级不同:
  • ListView有两级缓存,在屏幕与非屏幕内。

  • RecyclerView比ListView多两级缓存,支持多个离屏ItemView缓存(匹配pos获取目标位置的缓存,如果匹配则无需再次bindView),支持开发者自定义缓存处理逻辑,支持所有RecyclerView共用同一个RecyclerViewPool(缓存池)。缓存不同:

  • ListView缓存View。
  • RecyclerView缓存RecyclerView.ViewHolder,抽象可理解为:
  • View + ViewHolder(避免每次createView时调用findViewById) + flag(标识状态);

优点
RecylerView提供了局部刷新的接口,通过局部刷新,就能避免调用许多无用的bindView。
RecyclerView的扩展性更强大(LayoutManager、ItemDecoration等)。

Android两种虚拟机区别与联系 

Android中的Dalvik虚拟机相较于Java虚拟机针对手机的特点做了很多优化。

Dalvik基于寄存器,而JVM基于栈。在基于寄存器的虚拟机里,可以更为有效的减少冗余指令的分发和减少内存的读写访问。

Dalvik经过优化,允许在有限的内存中同时运行多个虚拟机的实例,并且每一个 Dalvik应用作为一个独立的Linux进程执行。

java虚拟机运行的是java字节码。(java类会被编译成一个或多个字节码.class文件,打包到.jar文件中,java虚拟机从相应的.class文件和.jar文件中获取相应的字节码)
Dalvik运行的是自定义的.dex字节码格式。(java类被编译成.class文件后,会通过一个dx工具将所有的.class文件转换成一个.dex文件,然后dalvik虚拟机会从其中读取指令和数据)
Android开发之浅谈java虚拟机和Dalvik虚拟机的区别

apk打包流程

  • aapt工具打包资源文件,生成R.java文件

  • aidl工具处理AIDL文件,生成对应的.java文件

  • javac工具编译Java文件,生成对应的.class文件

  • 把.class文件转化成Davik VM支持的.dex文件

  • apkbuilder工具打包生成未签名的.apk文件

  • jarsigner对未签名.apk文件进行签名

  • zipalign工具对签名后的.apk文件进行对齐处理

apk安装流程

  • 复制APK到/data/app目录下,解压并扫描安装包。

  • 资源管理器解析APK里的资源文件。

  • 解析AndroidManifest文件,并在/data/data/目录下创建对应的应用数据目录。

  • 然后对dex文件进行优化,并保存在dalvik-cache目录下。

  • 将AndroidManifest文件解析出的四大组件信息注册到PackageManagerService中。

  • 安装完成后,发送广播。

apk瘦身 

APK主要由以下几部分组成:

  • META-INF/ :包含了签名文件CERT.SF、CERT.RSA,以及 manifest 文件MANIFEST.MF。

  • assets/ : 存放资源文件,这些资源不会被编译成二进制。

  • lib/ :包含了一些引用的第三方库。

  • resources.arsc :包含res/values/中所有资源,例如strings,styles,以及其他未被包含在resources.arsc中的资源路径信息,例如layout 文件、图片等。

  • res/ :包含res中没有被存放到resources.arsc的资源。

  • classes.dex :经过dx编译能被android虚拟机理解的Java源码文件。

  • AndroidManifest.xml :清单文件

其中占据较大内存的是res资源、lib、class.dex,因此我们可以从下面的几个方面下手:

  • 代码方面可以通过代码混淆,这个一般都会去做。平时也可以删除一些没有使用类。

  • 去除无用资源。使用lint工具来检测没有使用到的资源,或者在gradle中配置shrinkResources来删除包括库中所有的无用的资源,需要配合proguard压缩代码使用。这里需要注意项目中是否存在使用getIdentifier方式获取资源,这种方式类似反射lint及shrinkResources无法检测情况。如果存在这种方式,则需要配置一个keep.xml来记录使用反射获取的资源。(https://developer.android.com/studio/build/shrink-code)[%5B)压缩代码和资源]

  • 去除无用国际化支持。对于一些第三库来说(如support),因为国际化的问题,它们可能会支持了几十种语言,但我们的应用可能只需要支持几种语言,可以通过配置resConfigs提出不要的语言支持。

  • 不同尺寸的图片支持。通常情况下只需要一套xxhpi的图片就可以支持大部分分辨率的要求了,因此,我们只需要保留一套图片。

  • 图片压缩。 png压缩或者使用webP图片,完美支持需要Android版本4.2.1+

  • 使用矢量图形。简单的图标可以使用矢量图片。

HTTP缓存机制

 

缓存的响应头:

  • Cache-control:标明缓存的最大存活时常;

  • Date:服务器告诉客户端,该资源的发送时间;

  • Expires:表示过期时间(该字段是1.0的东西,当cache-control和该字段同时存在的条件下,cache-control的优先级更高);

  • Last-Modified:服务器告诉客户端,资源的最后修改时间;

  • 还有一个字段,这个图没给出,就是E-Tag:当前资源在服务器的唯一标识,可用于判断资源的内容是否被修改了。

除以上响应头字段以外,还需了解两个相关的Request请求头:If-Modified-since、If-none-Match。这两个字段是和Last-Modified、E-Tag配合使用的。大致流程如下:
服务器收到请求时,会在200 OK中回送该资源的Last-Modified和ETag头(服务器支持缓存的情况下才会有这两个头哦),客户端将该资源保存在cache中,并记录这两个属性。当客户端需要发送相同的请求时,根据Date + Cache-control来判断是否缓存过期,如果过期了,会在请求中携带If-Modified-Since和If-None-Match两个头。两个头的值分别是响应中Last-Modified和ETag头的值。服务器通过这两个头判断本地资源未发生变化,客户端不需要重新下载,返回304响应。

okhttp原理

OkHttpClient通过newCall可以将一个Request构建成一个Call,Call表示准备被执行的请求。Call调用executed或enqueue会调用Dispatcher对应的方法在当前线程或者一步开始执行请求,经过RealInterceptorChain获得最终结果,RealInterceptorChain是一个拦截器链,其中依次包含以下拦截器:

  • 自定义的拦截器

  • retryAndFollowUpInterceptor 请求失败重试

  • BridgeInterceptor 为请求添加请求头,为响应添加响应头

  • CacheInterceptor 缓存get请求

  • ConnectInterceptor 连接相关的拦截器,分配一个Connection和HttpCodec为最终的请求做准备

  • CallServerInterceptor 该拦截器就是利用HttpCodec完成最终请求的发送

类的加载机制

程序在启动的时候,并不会一次性加载程序所要用的所有class文件,而是根据程序的需要,通过Java的类加载机制(ClassLoader)来动态加载某个class文件到内存当中的,从而只有class文件被载入到了内存之后,才能被其它class所引用。所以ClassLoader就是用来动态加载class文件到内存当中用的。
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中准备、验证、解析3个部分统称为连接(Linking)。

  • 加载:查找和导入Class文件;

  • 链接:把类的二进制数据合并到JRE中;
    (a) 验证:检查载入Class文件数据的正确性;
    (b) 准备:给类的静态变量分配存储空间;
    (c) 解析:将符号引用转成直接引用;

  • 初始化:对类的静态变量,静态代码块执行初始化操作

什么时候发生类初始化

  • 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候,读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。

  • 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。

  • 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。

  • 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。

  • 当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例左后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄锁对应的类没有进行过初始化时。

singleTask

singleTask保证了在栈中Activity的唯一性,如果被SingleTask标记的Activity处于栈底,站定的Activity发送Intent返回到栈底时,栈中间的Activity会被清除,这个时候被标记过SingleTask的生命周期会响应的做出调整。

onCreate是用来创建一个Activity也就是创建一个窗体,但一个Activty处于任务栈的顶端,若再次调用startActivity去创建它,则不会再次创建。

若你想利用已有的Acivity去处理别的Intent时,你就可以利用onNewIntent来处理。在 onNewIntent里面就会获得新的Intent.如果IntentActivity处于任务栈的顶端,也就是说之前打开过的Activity,现在处于onPause,onStop 状态的话其他应用再发送Intent的话,执行顺序为:

onNewIntent,onRestart,onStart,onResume

参考文章:

https://www.kancloud.cn/aslai/interview-guide/1126388