Activity启动模式与任务栈(Task)全面深入记录(上)

2023-05-16

转载请注明出处(谢谢):
http://blog.csdn.net/javazejian/article/details/52071885

任务栈简单入门

  最近又把两本进阶书看了一遍,但总感觉好记性不如烂笔头,所以还是决定通过博客记录一下,我们将分两篇来全面深入地记录Activity 启动模式与任务栈的内容。

android任务栈简单了解

1. android任务栈又称为Task,它是一个栈结构,具有后进先出的特性,用于存放我们的Activity组件。
2. 我们每次打开一个新的Activity或者退出当前Activity都会在一个称为任务栈的结构中添加或者减少一个Activity组件,因此一个任务栈包含了一个activity的集合, android系统可以通过Task有序地管理每个activity,并决定哪个Activity与用户进行交互:只有在任务栈栈顶的activity才可以跟用户进行交互。
3. 在我们退出应用程序时,必须把所有的任务栈中所有的activity清除出栈时,任务栈才会被销毁。当然任务栈也可以移动到后台, 并且保留了每一个activity的状态. 可以有序的给用户列出它们的任务, 同时也不会丢失Activity的状态信息。
4. 需要注意的是,一个App中可能不止一个任务栈,某些特殊情况下,单独一个Actvity可以独享一个任务栈。还有一点就是一个Task中的Actvity可以来自不同的App,同一个App的Activity也可能不在一个Task中。

  嗯,目前android任务栈的概念我们就大概了解到这。下面我们主要还是来聊聊android的4种启动模式。

Activity的启动模式

为什么需要Activity的启动模式?

  我们在开发项目的过程中,一般都需要在本应用中多个Activity组件之间的跳转,也可能需要在本应用中打开其它应用的可复用的Activity。如我们可能需要跳转到原来某个Activity实例,此时我们更希望这个Activity可以被重用而不是创建一个新的 Activity,但根据Android系统的默认行为,确实每次都会为我们创建一个新的Activity并添加到Task中,这样android系统是不是很傻?还有一点就是在我们每开启一次页面加入到任务栈Task中后,一个Activity的数据和信息状态都将会被保留,这样会造成数据冗余, 重复数据太多, 最终还可能导致内存溢出的问题(OOM)。为了解决这些问题,android系统提供了一套Activity的启动模式来修改系统Activity的默认启动行为。目前启动模式有四种,分别是standard,singleTop,singTask和singleInstance,接下来我们将分别介绍这四种模式。

Activity的4种启动模式

  • Standard 模式

  又称为标准模式,也是系统的默认模式(可以不指定),在这样模式下,每启动一个Activity都会重新创建一个Activity的新实例,并且将其加入任务栈中,而且完全不会去考虑这个实例是否已存在。我们通过图解来更清晰地了解Standard模式:

  通过上图,我们可以发现,这个过程中,在standard模式下启动了三次MainActivity后,都生成了不同的新实例,并添加到同一个任务栈中。这个时候Activity的onCreate、onStart、onResume方法都会被调用。

  • singleTop 模式

  又称栈顶复用模式,顾名思义,在这种模式下,如果有新的Activity已经存在任务栈的栈顶,那么此Activity就不会被重新创建新实例,而是复用已存在任务栈栈顶的Activity。这里重点是位于栈顶,才会被复用,如果新的Activity的实例已存在但没有位于栈顶,那么新的Activity仍然会被重建。需要注意的是,Activity的onNewIntent方法会被调用,方法原型如下:

@Override
protected void onNewIntent(Intent intent) {
    super.onNewIntent(intent);
}

  通过此方法的参数,我们可以获取当前请求的相关信息,此时Activity的onCreate、onStart方法不会被调用,因为Activity并没有被重建。同理,我们通过图解来协助我们更清晰的理解singleTop模式:

  从上图我们可以看出,当需要新创建的MainActivity位于栈顶时,MainActivity并没有重新创建。下面我们再来看看新创建的MainActivity没有位于栈顶的情况。

  嗯,这就是singTop模式。这种模式通常比较适用于接收到消息后显示的界面,如qq接收到消息后弹出Activity界面,如果一次来10条消息,总不能一次弹10个Activity,是吧?再比如新闻客户端收到了100个推送,你每次点一下推送他都会进入某个activiy界面(显示新闻只用一个activity,只是内容不同而已),这时也比较适合使用singleTop模式。

  • singleTask 模式

   又称为栈内复用模式。这是一种单例模式,与singTop点类似,只不过singTop是检测栈顶元素是否有需要启动的Activity,而singTask则是检测整个栈中是否存在当前需要启动的Activity,如果存在就直接将该Activity置于栈顶,并将该Activity以上的Activity都从任务栈中移出销毁,同时也会回调onNewIntent方法。情况如下图:

   从图中可以看出,当我们再次启动MainActivity时,由于MainActivity位于栈中,所以系统直接将其置于栈顶,并移除其上方的所有Activity。当然如果所需要的MainActivity不存在栈中,则会创建新的Activity并添加到栈中。singleTask 模式比较适合应用的主界面activity(频繁使用的主架构),可以用于主架构的activity,(如新闻,侧滑,应用主界面等)里面有好多fragment,一般不会被销毁,它可以跳转其它的activity 界面再回主架构界面,此时其他Activity就销毁了。当然singTask还有一些比较特殊的场景这个我们后面会一一通过情景代码分析。

  • singleInstance 模式

  在singleInstance模式下,该Activity在整个android系统内存中有且只有一个实例,而且该实例单独尊享一个Task。换句话说,A应用需要启动的MainActivity 是singleInstance模式,当A启动后,系统会为它创建一个新的任务栈,然后A单独在这个新的任务栈中,如果此时B应用也要激活MainActivity,由于栈内复用的特性,则不会重新创建,而是两个应用共享一个Activity的实例。如下图所示:

  从图中我们可以看到最终AB应用都共享一个singleInstance模式的MainActivity,也没有去重新创建。到此Activity的四种启动模式我们都介绍完了,下面我们接着来聊聊怎么使用启动模式。

Activity启动模式的使用方式

  前面我们说了那么多,那么我们该如何给Activity指定启动模式呢?事实上共有如下两种方式:
1.通过AndroidMenifest.xml文件为Activity指定启动模式,代码如下:

<activity android:name=".ActivityC"android:launchMode="singleTask" />

2.通过在Intent中设置标志位(addFlags方法)来为Activity指定启动模式,示例代码如下:

Intent intent = new Intent();
intent.setClass(ActivityB.this,ActivityA.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);

那么标志位是是什么呢?接下来我们就来了解一些常用的标志位

Intent Flag 启动模式

  这里我们主要介绍一下一些常用的Activity的Flag,因为Activity的Flag比较多,我们知道一些常用的就够了,遇到比较特殊的还是查查官网文档吧。

  • Intent.FLAG_ACTIVITY_NEW_TASK

该标志位表示使用一个新的Task来启动一个Activity,相当于在清单文件中给Activity指定“singleTask”启动模式。通常我们在Service启动Activity时,由于Service中并没有Activity任务栈,所以必须使用该Flag来创建一个新的Task。我们来重现一下这个错误,创建一个Service服务,并在onCreate方法中启动Activity,代码如下:

public class ServiceT extends Service {
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        Intent i =new Intent(getApplicationContext(),ActivityD.class);
        startActivity(i);
    }
}

启动应用并启动Service服务,后报错如下:

从异常信息我们可以看出,提示我们添加Intent.FLAG_ACTIVITY_NEW_TASK标志位,所以我们代码必须改成如下:

public class ServiceT extends Service {
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }


    @Override
    public void onCreate() {
        super.onCreate();
        Intent i =new Intent(getApplicationContext(),ActivityD.class);
        i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        startActivity(i);
    }
}
  • Intent.FLAG_ACTIVITY_SINGLE_TOP
      该标志位表示使用singleTop模式来启动一个Activity,与在清单文件指定android:launchMode="singleTop"效果相同。

  • Intent.FLAG_ACTIVITY_CLEAR_TOP
    
  该标志位表示使用singleTask模式来启动一个Activity,与在清单文件指定android:launchMode="singleTask"效果相同。

  • Intent.FLAG_ACTIVITY_NO_HISTORY
      使用该模式来启动Activity,当该Activity启动其他Activity后,该Activity就被销毁了,不会保留在任务栈中。如A-B,B中以这种模式启动C,C再启动D,则任务栈只有ABD。

  • Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
      使用该标识位启动的Activity不添加到最近应用列表,也即我们从最近应用里面查看不到我们启动的这个activity。与属性android:excludeFromRecents="true"效果相同。

启动模式中singleTask的特殊情景

  前面我们在分析singleTask模式时,提到过singleTask模式有些比较特殊的场景,现在我们就来了解了解它们。
特殊情景一:现在我们假设有如下两个Task栈,分别为前台任务栈和后台任务栈

  从图中我们看出前台任务栈分别为AB两个Activity,后台任务栈分别为CD两个任务栈,而且其启动模式均为singleTask,此时我们先启动CD,然后再启动AB,再有B启动D,此时后台任务栈便会被切换到前台,而且这个时候整个后退列表就变成了ABCD,请注意我们这里强调的是后退列表,而非栈合并。因此当用户点击back键时,列表中的Activity会依次按DCBA顺序出栈,如下图所示:

  这里我们通过两个应用ActivityTask和ActivityTask2来测试重现这个现象。因为两个是不同的应用所以启动时所在的栈也是不同。我们先启动ActivityTask2的应用,其ActivityC和ActivityD都是singleTask模式,然后再启动应用ActivityTask,此时ActivityC和ActivityD所在任务栈会被退居后台,而打开的ActivityA和ActivityB会在前台,而且都是默认模式。我们通过 adb shell dumpsys activity activities 命令查看此时栈的情况:

  我们可以看到由两个栈,分别为id=222且栈名为“com.cmcm.activitytask”的任务栈其包含ActivityA和ActivityB(下面简称AB,栈名一般默认和包名相同),另外一个任务栈,id=221,栈名为“com.cmcm.activitytask2”,其包含ActivityC和ActivityD(下面检测CD)。现在我们通过ActivityB去启动ActivityD,然后按back键回退。B调用D代码如下:

import android.app.Activity;
import android.content.ComponentName;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

/**
 * Created by zejian
 * Time 16/7/23.
 * Description:
 */
public class ActivityB extends Activity {
    private Button btn;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_b);
        btn= (Button) findViewById(R.id.main);
        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(Intent.ACTION_MAIN);
                intent.addCategory(Intent.CATEGORY_LAUNCHER);
                ComponentName cn = new ComponentName("com.cmcm.activitytask2", "com.cmcm.activitytask2.ActivityD");
                intent.setComponent(cn);
                startActivity(intent);

            }
        });
    }
}

运行结果如下:

  我们可以看到包含CD的任务栈被提前的,虽然CD隔开了,但是我们从id和栈名可以发现他们是同一个栈,而AB所在的栈则在CD所在栈的后面,所以此时我们按back回退时,退出顺序是这样的D->C->B->A,动态图如下:

  到这里我们就应该更加清晰的了解情景一的现象了。了解这点有什么用呢,这可以使用我们更好地去管理我们的任务栈,而不会导致栈混乱是进入一些用户本来就不需要界面,影响用户体验。

特殊情景二:
  如果上面B不是请求启动D而是请求启动C,那么又会是什么情况呢?其实这个时候任务栈退出列表变成C->B->A,其实原因很简单,singleTask模式的ActivityC切换到栈顶时会导致在他之上的栈内的Activity出栈。同样我们还是使用上面的代码,把B启动D改为B启动C,那么此时B未启动C时任务栈的情况如下:

  我们仍然可以看到两个任务栈,分别为id=242,栈名“com.cmcm.activitytask”的Task,包含ActivityA和ActivityB;id=241,栈名“com.cmcm.activitytask2”的Task,包含ActivityC和ActivityD。此时我们通过B启动C后栈的情况变成如下情况

因此,栈的退出列表就变成了C->B->A了,如下图所示:

动态图如下:


  到此我们对SingleTask模式又有了更深入的理解,但是我们发现上面的例子使用的是两个应用,所以才会有不同的任务栈,那么我们能不能在一个应用中存在多个不同的任务栈呢(暂时不考虑singleInstance 模式)?答案当然是肯定的啦,这就需要通过taskAffinity属性来设置不同的任务栈名称,不过这点将放在下篇来记录,本篇就先到这里告一段落哈。

  • Android之Activity生命周期的浅析(一)
  • Android之Activity生命周期浅析(二)
  • Activity启动模式与任务栈(Task)全面深入记录(上)
  • Activity启动模式与任务栈(Task)全面深入记录(下)







    主要参考:
    《android开发艺术探索》
    google官网
    http://www.2cto.com/kf/201311/254450.html
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Activity启动模式与任务栈(Task)全面深入记录(上) 的相关文章

  • git 提交的时候报错:error: 'flutter_app/' does not have a commit checked out

    如果有朋友遇到这个问题 xff0c 请不用担心 xff0c 因为这个问题非常简单 xff01 出现的情况就是 xff0c 你在一个github仓库里面 xff0c 放进来一个文件夹 xff0c 但是文件夹里面还有文件夹 xff0c 而且还没
  • Fragment中使用viewLifecycleOwner/getActivity/this

    观察liveData使用生命周期 如图 xff1a 当liveData想在fragment里观察的时候 xff0c 可以使用getActivity this getViewLifecycleOwner getActivity不必说 xff0
  • Android的FileProvider使用解释

    前言 从Android7 0 xff08 N xff09 开始 xff0c 严格执行 StrictMode 模式 xff0c 也就是说 xff0c 将对安全做更严格的校验 不允许在 App 间 xff0c 使用 file 的方式 xff0c
  • STM32G0 串口编程

    利用USB转串口调试 xff0c 烧写单片机程序 1 打开STM32 CubeProgrammer软件设置启动配置 2 让程序从system Flash启动 xff08 1 xff09 勾选 DTR 勾选RTS 取消勾选RTS 2 取消勾选
  • Android使用SystemProperties基础了解

    安卓系统属性是以键值对的形式存在 xff0c 一般放在system prop xff0c build prop xff0c default prop等文件中 这些属性可能是进程状态 xff0c 资源使用情况 xff0c 系统特有属性等等 创
  • 使用Android Studio打包Module成jar包

    现在假设我们想打包一个module成jar包的形式给其它应用调用 xff1a vrservice 1 0 jar 步骤1 在Module项目的build gradle文件中做如下配置 xff1a 生成jar包的配置如下 xff1a def
  • C++无符号整型与有符号整型变量的运算-不简单

    示例分析 xff1a include lt iostream gt include lt stdio h gt struct Result char c char d unsigned char e Result getChar int x
  • C++虚继承下的类大小

    前言 带有虚函数的虚继承的对象模型比较复杂 xff0c 所以单独整理一下 其实关于计算类大小是C 43 43 的一大易错点之一 即便是我在这儿理了半天 xff0c 也不一定就是正确的 xff0c 如果大佬看到本文 xff0c 发现我很多错误
  • 解决android的跑马灯频繁刷新的问题

    先贴一下跑马灯效果的代码 xff0c 这里我是继承的TextView xff1a public class MarqueeTextView extends AppCompatTextView public MarqueeTextView C
  • 关于第一次将STM32与电脑连接情况

    安装了Keil xff08 ARM xff09 版本之后 xff0c 不管是自己编程 xff0c 还是配套的程序运行 我们都想把它下载到STM32芯片里面 xff0c 在板子上运行 这里介绍几种方法 1 用J LINK下载调试 这个工具 x
  • [java语言]——InetAddress类的getByName()方法

    InetAddress 表示互联网协议 xff08 IP xff09 地址 InetAddress getByName 34 www 163 com 34 在给定主机名的情况下确定主机的IP地址 如果参数为null 获得的是本机的IP地址
  • Java中Calendar.DAY_OF_WEEK、DAY_OF_MONTH需要减一的原因

    Java中对日期的处理需要用到Calendar类 xff0c 其中有几个方法在使用时需要新手注意 1 在获取月份时 xff0c Calendar MONTH 43 1 的原因 xff08 Java中Calendar MONTH返回的数值其实
  • C语言学习笔记(三)(头文件的结构顺序)(类型不完整的解决方法)

    目录 前言原则结构推荐结构图方便的结构 类型不完整的原因和解决方法 xff1a 定义了不定长数组结构体的定义放在了头文件里面解决方法1 xff08 当采用使用时包含特定头文件时 xff09 xff1a 解决方法2 xff08 当一个头文件包
  • S2

    int count 61 0 double lat 61 55 8241 double lng 61 137 8347 double radius 61 900 半径 double capHeight 61 2 S2 M PI radius
  • 从STM32F407到AT32F407(一)

    雅特力公司的MCU有着性能超群 xff0c 价格优越的巨大优势 xff0c 缺点是相关资料少一些 xff0c 我们可以充分利用ST的现有资源来开发它 我用雅特力的STM32F437开发板 xff0c 使用原子 stm32f407的开发板自带
  • vue-java分离

    import com google gson Gson import io renren common utils HttpContextUtils import io renren common utils R import org ap
  • java学习

    莫求全 有效努力 定时出结果 架构设计DDD 微服务介绍 https www kancloud cn qingshou aaa1 2667225 https www cnblogs com chencan p 16042197 html h
  • CentOS6.5添加虚拟IP(VIP)

    使用keepalived 实现Nginx高可用时 需要用到这项技术 虚拟ip在高可用中的作用后续再说 今天看看怎么给服务器配置虚拟IP xff0c 其实也就是多分配个IP地址 首先查看一下现有网卡的IP地址 xff0c 用root特权运行下
  • 微服务Spring Cloud例子

    Spring Cloud简介 Spring Cloud是一个基于Spring Boot实现的云应用开发工具 xff0c 为开发者提供了在分布式系统 xff08 配置管理 xff0c 服务发现 xff0c 熔断 xff0c 路由 xff0c
  • 美邦威集成呼吸墙饰

    http www mbwqs cn 湖北光大新型环保装饰材料有限公司 美邦威集成呼吸墙饰 生产销售中心 xff1a 湖北汉川经济开发区光大工业园 光大材料 210590 上海股交所挂牌

随机推荐

  • activemq--MASTER SLAVE+BROKER CLUSTER 实践(二)

    鱼与熊掌兼得法 完美解决方案 我们知道 xff1a master slave模式下 xff0c 消息会被逐个复制而cluster模式下 xff0c 请求会被自动派发 那么可不可以把两者集成起来呢 xff1f 答案是有的 xff0c 网上所谓
  • Dubbo超时和重连机制

    dubbo启动时默认有重试机制和超时机制 超时机制的规则是如果在一定的时间内 xff0c provider没有返回 xff0c 则认为本次调用失败 xff0c 重试机制在出现调用失败时 xff0c 会再次调用 如果在配置的调用次数内都失败
  • Sharding-JDBC简介

    一般 xff0c 线上系统的业务量不是很大 xff0c 比如说单库的数据量在百万级别以下 xff0c 那么MySQL的单库即可完成任何增 删 改 查的业务操作 随着业务的发展 xff0c 单个DB中保存的数据量 xff08 用户 订单 计费
  • 1024

    听说今天发帖能有1024勋章 xff1f
  • 神奇!明明是 socket,被我玩成了 http!

    颓废青年 xff0c 快出来挨打 xff01 点击上方 Java极客技术 xff0c 选择 设为星标 后台回复 java xff0c 获取Java知识体系 面试必看资料 资料会持续更新 xff0c 已更新第四次 xff01 文章精品专栏 记
  • python画图程序

    usr bin python coding utf 8 import wx import wx lib buttons as buttons import wx adv as adv import wx lib colourselect a
  • 升级到tensorflow2.0,我整个人都不好了

    版本升级到 tensorflow 2 0 的悲惨经历 没事别升级 Tensorflow 2 0发布已经有一段时间了 xff0c 各种基于新API的教程看上去的确简单易用 xff0c 一个简单的mnist手写识别只需要下面不到20行代码就OK
  • 修改conda环境和缓存默认路径

    默认情况下 xff0c conda 创建的新环境 以及过往安装的模块缓存都存储在用户目录下 xff0c 这一点不会在 conda xff08 user specific xff09 配置文件 HOME condarc 中体现出来 xff0c
  • 融合人体姿态估计和目标检测的学生课堂行为识别

    融合人体姿态估计和目标检测的学生课堂行为识别 参考网 摘要 xff1a 在課堂教学中 xff0c 人工智能技术可以帮助实现学生行为分析自动化 xff0c 让教师能够高效且直观地掌握学生学习行为投入的情况 xff0c 为后续优化教学设计与实施
  • Python实例详解pdfplumber读取PDF写入Excel

    一 Python操作PDF 13大库对比 PDF xff08 Portable Document Format xff09 是一种便携文档格式 xff0c 便于跨操作系统传播文档 PDF文档遵循标准格式 xff0c 因此存在很多可以操作PD
  • 如何使用ChatGPT API训练自定义知识库AI聊天机器人

    原文 xff1a 如何使用ChatGPT API训练自定义知识库AI聊天机器人 闪电博 在我们之前的文章中 xff0c 我们演示了如何用ChatGPT API建立一个AI聊天机器人 xff0c 并指定一个角色来进行个性化处理 但如果你想在自
  • 哈工大团队开源医学智能问诊大模型 | 华佗: 基于中文医学知识的LLaMa指令微调模型

    原文 xff1a CVHub 门头沟学院AI视觉实验室御用公众号 学术 科研 就业 185篇原创内容 公众号 Title HuaTuo Tuning LLaMA Model with Chinese Medical Knowledge PD
  • 开源数字人Fay

    原文 xff1a 别再因AI焦虑 xff0c 这波年轻人已经用 中国版ChatGPT 创业成功了 数字人 AI 创业 新浪新闻 开源 xff1a GitHub TheRamU Fay Fay是一个完整的开源项目 xff0c 包含Fay控制器
  • 推荐 3 个令你惊艳的 GitHub 项目

    原文 xff1a 推荐 3 个令你惊艳的 GitHub 项目 昨日 GitHub Trending 上榜的开源项目 xff0c 基于 AI 技术提高你的生产力 借助 AI 你能搭建自己的数字人 搭建自己的法律助手 文档分析助手 本期推荐开源
  • AI 数字人制作(方案一):输入一张图片和一段文字即可生成数字人

    方案一 xff1a 原文 xff1a AI 数字人制作 xff08 方案一 xff09 哔哩哔哩 bilibili AI 文字和图片生成数字人 输入一张图片和一段文字即可生成数字人 用三个开源项目整合成可以商用的数字人项目 文本生成语音开源
  • 大量数据情况下单线程插入和多线程insert数据库的性能测试

    大量数据情况下单线程插入和多线程insert数据库的性能测试 之前一直没有遇到过大批量数据入库的场景 xff0c 所以一直没有思考过在大量数据的情况下单线程插入和多线程插入的性能情况 今天在看一个项目源代码的时候发现使用了多线程insert
  • 查看tensorflow 安装目录

    使用命令 xff1a pip show f tensorflow 图和张量源码 xff1a C Program Files Anaconda3 Lib site packages tensorflow python framework op
  • FP-growth算法,fpgrowth算法详解

    FP growth算法 xff0c fpgrowth算法详解 使用FP growth算法来高效发现频繁项集 前言 你用过搜索引擎挥发现这样一个功能 xff1a 输入一个单词或者单词的一部分 xff0c 搜索引擎酒会自动补全查询词项 xff0
  • 如何将一个矩阵化为行阶梯形矩阵

    2016 03 29 尾巴 线性代数 有同学反映上一课过于冷冰冰 xff0c 都是一些不带证明的公式 如果线性代数所有公式都要证明的话 xff0c 线性代数的难度会上好几个量级 xff0c 有的公式的证明是特别特别难的 还有一个 xff0c
  • Activity启动模式与任务栈(Task)全面深入记录(上)

    转载请注明出处 xff08 谢谢 xff09 xff1a http blog csdn net javazejian article details 52071885 任务栈简单入门 最近又把两本进阶书看了一遍 xff0c 但总感觉好记性不