前端魔法堂——调用栈,异常实例中的宝藏

前言

 在上一篇《前端魔法堂——异常不仅仅是try/catch》中我们描述出一副异常及如何捕获异常的画像,但仅仅如此而已。试想一下,我们穷尽一切捕获异常实例,然后仅仅为告诉用户,运维和开发人员页面报了一个哪个哪个类型的错误吗?答案是否定的。我们的目的是收集刚刚足够的现场证据,好让我们能马上重现问题,快速修复,提供更优质的用户体验。那么问题就落在“收集足够的现场证据”,那么我们又需要哪些现场证据呢?那就是异常信息调用栈栈帧局部状态。(异常信息我们已经获取了)
 本文将围绕上调用栈栈帧局部状态叙述,准开开车^_^

概要

 本篇将叙述如下内容:

  1. 什么是调用栈?
  2. 如何获取调用栈?
  3. 什么是栈帧局部状态?又如何获取呢?

一.什么是调用栈?

 既然我们要获取调用栈信息,那么起码要弄清楚什么是调用栈吧!下面我们分别从两个层次来理解~

印象派

 倘若主要工作内容为应用开发,那么我们对调用栈的印象如下就差不多了:

function funcA (a, b){
  return a + b
}

function funcB (a){
  let b = 3
  return funcA(a, b)
}

function main(){
  let a = 5
  funcB(a)
}

main()

 那么每次调用函数时就会生成一个栈帧,并压入调用栈,栈帧中存储对应函数的局部变量;当该函数执行完成后,其对应的栈帧就会弹出调用栈。
 因此调用main()时,调用栈如下

----------------<--栈顶
|function: main|
|let a = 5     |
|return void(0)|
----------------<--栈底

 调用funcB()时,调用栈如下

----------------<--栈顶
|function:funcB|
|let b = 3     |
|return funcA()|
----------------
|function: main|
|let a = 5     |
|return void(0)|
----------------<--栈底

 调用funcA()时,调用栈如下

----------------<--栈顶
|function:funcA|
|return a + b  |
----------------
|function:funcB|
|let b = 3     |
|return funcA()|
----------------
|function: main|
|let a = 5     |
|return void(0)|
----------------<--栈底

funcA()执行完成后,调用栈如下

----------------<--栈顶
|function:funcB|
|let b = 3     |
|return funcA()|
----------------
|function: main|
|let a = 5     |
|return void(0)|
----------------<--栈底

funcB()执行完成后,调用栈如下

----------------<--栈顶
|function: main|
|let a = 5     |
|return void(0)|
----------------<--栈底

main()执行完成后,调用栈如下

----------------<--栈顶
----------------<--栈底

 现在我们对调用栈有了大概的印象了,但大家有没有留意上面记录"栈帧中存储对应函数的局部变量",栈帧中仅仅存储对应函数的局部变量,那么入参呢?难道会作为局部变量吗?这个我们要从理论的层面才能得到解答呢。

理论派

 这里我们要引入一个简单的C程序,透过其对应的汇编指令来讲解了。我会尽我所能用通俗易懂的语言描述这一切的,若有错误请各位指正!!

前提知识

  1. Intel X86架构中调用栈的栈底位于高位地址,而栈顶位于低位地址。(和印象派中示意图的方向刚好相反)
  2. 调用栈涉及的寄存器有
ESP/RSP, 暂存栈顶地址
EBP/RBP, 暂存栈帧起始地址
EIP, 暂存下一个CPU指令的内存地址,当CPU执行完当前指令后,从EIP读取下一条指令的内存地址,然后继续执行
  1. 操作指令
PUSH <OPRD>,将ESP向低位地址移动操作数所需的空间,然后将操作数压入调用栈中
POP <OPRD>,从调用栈中读取数据暂存到操作数指定的寄存器或内存空间中,然后向高位地址移动操作数对应的空间字节数
MOV <SRC>,<DST>,数据传送指令。用于将一个数据从源地址传送到目标地址,且不破坏源地址的内容
ADD <OPRD1>,<OPRD2>,两数相加不带进位,然后将结果保存到目标地址上
RET,相当于POP EIP。就是从堆栈中出栈,然后将值保存到EIP寄存器中
LEAVE,相当于MOV EBP ESP,然后再POP EBP。就是将栈顶指向当前栈帧地址,然后将调用者的栈帧地址暂存到EBP中
  1. 每个函数调用前汇编器都会加入以下前言(Prolog),用于保存栈帧和返回地址
push   %rbp      ;将调用者的栈帧指针压入调用栈
mov    %rsp,%rbp ;现在栈顶指向刚入栈的RBP内容,要将其设置为栈帧的起始位置

 现在们结合实例来理解吧!
C语言

#include <stdio.h>

int add(int a, int b){
  return a + b;
}
int add2(int a){
  int sum = add(0, a);
  return sum + 2;
}

void main(){
  add2(2);
}

然后执行以下命令编译带调试信息的可执行文件,和dump文件

$ gcc -g -o main main.c
$ objdump -d main > main.dump

下面我们截取main、add2和add对应的汇编指令来讲解

main函数对应的汇编指令

0x40050f <main>                 push   %rbp
0x400510 <main+1>               mov    %rsp,%rbp
;将2暂存到寄存器EDI中
0x400513 <main+4>               mov    $0x2,%edi
;执行call指令前,EIP寄存器已经存储下一条指令的地址0x40051d了
;首先将EIP寄存器的值入栈,当函数返回时用于恢复之前的执行序列
;然后才是执行JUMP指令跳转到add2函数中开始执行其第一条指令
0x400518 <main+9>               callq  0x4004ea <add2>
;什么都不做
0x40051d <main+14>              nop
;设置RBP为指向main函数调用方的栈帧地址
0x40051e <main+15>              pop    %rbp
;设置EIP指向main函数返回后将要执行的指令的地址
0x40051f <main+16>              retq

下面是执行add2函数第一条指令前的调用栈快照

+++++++++++++++++ 高位地址
99 |   110    | -- 存放main函数调用方的栈帧地址 <-- EBP
+++++++++++++++++
98 | 0x40051d | -- EIP的值,存放add2返回后将执行的指令的地址 <-- ESP
+++++++++++++++++ 低位地址

add2函数对应的汇编指令

0x4004ea <add2>                 push   %rbp
0x4004eb <add2+1>               mov    %rsp,%rbp
0x4004ee <add2+4>               sub    $0x18,%rsp      ;栈顶向低位移动24个字节,为后续操作预留堆栈空间
0x4004f2 <add2+8>               mov    %edi,-0x14(%rbp);从EDI寄存器中读取参数,并存放到堆栈空间中
0x4004f5 <add2+11>              mov    -0x14(%rbp),%eax;从堆栈空间中读取参数,放进EAX寄存器中
0x4004f8 <add2+14>              mov    %eax,%esi       ;从EAX寄存器中读取参数,存放到ESI寄存器中
0x4004fa <add2+16>              mov    $0x0,%edi       ;将0存放到EDI寄存器中
;执行call指令前,EIP寄存器已经存储下一条指令的地址0x400504了
;首先将EIP寄存器的值入栈,当函数返回时用于恢复之前的执行序列
;然后才是执行JUMP指令跳转到add函数中开始执行其第一条指令
0x4004ff <add2+21>              callq  0x4004d6 <add>
0x400504 <add2+26>              mov    %eax,-0x4(%rbp) ;读取add的返回值(存储在EAX寄存器中),存放到堆栈空间中
0x400507 <add2+29>              mov    -0x4(%rbp),%eax ;又将add的返回值存放到EAX寄存器中(这是有多无聊啊~~)
0x40050a <add2+32>              add    $0x2,%eax       ;读取EAX寄存器的值与2相加,结果存放到EAX寄存器中
0x40050d <add2+35>              leaveq                 ;让栈顶指针指向main函数的栈帧地址,然后让EBP指向main函数的栈帧地址
0x40050e <add2+36>              retq                   ;让EIP指向add2返回后将执行的指令的地址

下面是执行完add2函数中mov %rsp,%rbp的调用栈快照

+++++++++++++++++ 高位地址
99 |    110   | -- 存放main函数调用方的栈帧地址
+++++++++++++++++
98 | 0x40051d | -- 存放EIP的值,add2返回后将执行的指令的地址
+++++++++++++++++
97 |    99    | -- 存放add2函数调用方(即main函数)的栈帧地址<-- ESP,EBP
+++++++++++++++++ 低位地址

下面是执行add函数第一条指令前的调用栈快照

+++++++++++++++++ 高位地址
99 |    110   | -- 存放main函数调用方的栈帧地址
+++++++++++++++++
98 | 0x40051d | -- 存放EIP的值,add2返回后将执行的指令的地址
+++++++++++++++++
97 |    99    | -- 存放add2函数调用方(即main函数)的栈帧地址<-- EBP
+++++++++++++++++
96 |   0xXX   |
+++++++++++++++++
.................
76 |   0x02   | -- 这是`mov %edi,-0x14(%rbp)`的执行结果
+++++++++++++++++
.................
+++++++++++++++++
73 |   0xXX   |
+++++++++++++++++
72 | 0x400504 | -- EIP的值,存放add返回后将执行的指令的地址 <-- ESP
+++++++++++++++++ 低位地址

add函数对应的汇编指令

0x4004d6 <add>                  push   %rbp
0x4004d7 <add+1>                mov    %rsp,%rbp
0x4004da <add+4>                mov    %edi,-0x4(%rbp)
0x4004dd <add+7>                mov    %esi,-0x8(%rbp)
0x4004e0 <add+10>               mov    -0x4(%rbp),%edx
0x4004e3 <add+13>               mov    -0x8(%rbp),%eax
0x4004e6 <add+16>               add    %edx,%eax
0x4004e8 <add+18>               pop    %rbp
0x4004e9 <add+19>               retq

下面是add函数执行完mov %rsp,%rbp的调用栈快照

+++++++++++++++++ 高位地址
99 |    110   | -- 存放main函数调用方的栈帧地址
+++++++++++++++++
98 | 0x40051d | -- 存放EIP的值,add2返回后将执行的指令的地址
+++++++++++++++++
97 |    99    | -- 存放add2函数调用方(即main函数)的栈帧地址
+++++++++++++++++
96 |   0xXX   |
+++++++++++++++++
.................
76 |   0x02   | -- 这是`mov %edi,-0x14(%rbp)`的执行结果
+++++++++++++++++
.................
+++++++++++++++++
73 |   0xXX   |
+++++++++++++++++
72 | 0x400504 | -- EIP的值,存放add返回后将执行的指令的地址
+++++++++++++++++
71 |    97    | -- 存放add函数调用方(即add函数)的栈帧地址<-- EBP,ESP
+++++++++++++++++ 低位地址

下面就是一系列弹出栈帧的过程了
当add函数执行完retq的调用栈快照

+++++++++++++++++ 高位地址
99 |    110   | -- 存放main函数调用方的栈帧地址
+++++++++++++++++
98 | 0x40051d | -- 存放EIP的值,add2返回后将执行的指令的地址
+++++++++++++++++
97 |    99    | -- 存放add2函数调用方(即main函数)的栈帧地址 <-- EBP
+++++++++++++++++
96 |   0xXX   |
+++++++++++++++++
.................
76 |   0x02   | -- 这是`mov %edi,-0x14(%rbp)`的执行结果
+++++++++++++++++
.................
+++++++++++++++++
73 |   0xXX   | <-- ESP
+++++++++++++++++ 低位地址

然后就不断弹出栈帧了~~~
 从上面看到函数入参是先存储到寄存器中,然后在函数体内读取到栈帧所在空间中(局部变量、临时变量)。那么从调用栈中我们能获取函数的调用流和入参信息,从而恢复案发现场^_^

插播:函数的调用方式

 其实函数入参的传递方式不止上述这种,还有以下3种

  1. cdecl调用约定
     调用方从右到左的顺序将参数压入栈中,在被调用方执行完成后,由调用方负责清理栈中的参数(也称为栈平衡)。
  2. stdcall调用约定
     巨硬自称的一种调用约定,并不是实际上的标准调用约定。调用方从右到左的顺序将参数压入栈中,在被调用方执行完成后,由被调用方负责清理栈中的参数(也称为栈平衡)。
  3. fastcall调用约定
     是stdcall的变体,调用方从右到左的顺序将参数压入栈中,最右边的两个参数则不压入栈中,而是分别存储在ECX和EDX寄存器中,在被调用方执行完成后,由被调用方负责清理栈中的参数(也称为栈平衡)。

 但不管哪种,最终还是会在函数体内读取到当前栈帧空间中。

二. 如何获取调用栈?

 上面写的这么多,可是我们现在写的是JavaScript哦,那到底怎么才能读取调用栈的信息呢?

抛个异常看看

 IE10+的Error实例中包含一个stack属性
示例

function add(a, b){
  let sum = a + b
  throw Error("Capture Call Stack!")
  return sum
}

function add2(a){
  return 2 + add(0, a)
}

function main(){
  add2(2)
}

try{
  main()
} catch (e){
  console.log(e.stack)
}

Chrome回显

Error: Capture Call Stack!
    at add (index.html:16)
    at add2 (index.html:21)
    at main (index.html:25)
    at index.html:29

FireFox回显

add@file:///home/john/index.html:16:9
add2@file:///home/john/index.html:21:14
main@file:///home/john/index.html:25:3
@file:///home/john/index.html:29:3

V8的Error.captureStackTrace函数

 V8引擎向JavaScript提供了其Stack Trace API中的captureStackTrace函数,用于获取调用Error.captureStackTrace时的调用栈快照。函数签名如下

@static
@method captureStackTrace(targetObject, constructorOpt)
@param {Object} targetObject - 为targetObject添加.stack属性,该属性保存调用Error.captureStackTrace时的调用栈快照
@param {Function} constructorOpt= - 调用栈快照不断作出栈操作,直到constructorOpt所指向的函数刚好出栈为止,然后保存到targetObject的stack属性中
@return {undefined}

示例

function add(a, b){
  let sum = a + b
  let targetObj = {}

  Error.captureStackTrace(targetObj)
  console.log(targetObj.stack)

  Error.captureStackTrace(targetObj, add)
  console.log(targetObj.stack)

  return sum
}

function add2(a){
  return 2 + add(0, a)
}

function main(){
  add2(2)
}

main()

Chrome回显

Error
    at add (index.html:18)
    at add2 (index.html:28)
    at main (index.html:32)
    at index.html:35
Error
    at add2 (index.html:28)
    at main (index.html:32)
    at index.html:35

控制台的console.trace函数

 还有最后一招console.trace,不过实际用处不大
示例

function add(a, b){
  let sum = a + b
  console.trace()

  return sum
}

function add2(a){
  return 2 + add(0, a)
}

function main(){
  add2(2)
}

main()

Chrome回显

add    @    index.html:16
add2    @    index.html:22
main    @    index.html:26
(anonymous)    @    index.html:29

 上述三种方式(实际就两种可用啦)都只能获取函数调用流,函数入参、局部变量等信息全都灰飞烟灭了?上面不是说好这些信息调用栈都有嘛,干嘛不给我呢?其实想想都知道调用栈中有这么多信息,其实我们只需一小部分,全盘托出并不是什么好设计。其实我们只要再获取栈帧局部状态就好了。

三. 什么是栈帧局部状态?又如何获取呢?

 所谓栈帧局部状态其实就是函数入参和局部变量,试想如果我们得到add函数调用时的入参是a=0b=2sum=2,那么不就得到完整案发现场了吗?那问题就是如何获得了。要不我们做个Monkey Patch

  1. 自定义一个异常类来承载栈帧局部状态
function StackTraceError(e, env){
  if (this instanceof StackTraceError);else return new StackTraceError(e, env)
  this.e = e
  this.env = env
}
let proto = StackTraceError.prototype = Object.create(Error.prototype)
proto.name = "StackTraceError"
proto.message = "Internal error."
proto.constructor = StackTraceError
proto.valueOf = proto.toString = function(){
  let curr = this, q = [], files = []
  do {
    if (curr.stack){
      let stack = String(curr.stack)
      let segs = stack.split('\n').map(seg => seg.trim())
      files = segs.filter(seg => seg != "Error")
    }
    else{
      q.unshift({name: curr.name,
                 msg: curr.message,
                 env: curr.env})
    }
  } while (curr = curr.e)

  let frames = []
  let c = files.length, i = 0
  while (i < c){
    let file = files[i]
    let e = q[i]
    let frame = {
      name: e && e.name,
      msg: e && e.msg,
      env: e && e.env,
      file: file
    }
    frames.push(JSON.stringify(frame))
    i += 1
  }
  return frames.join("\n")
}
  1. 每个函数定义都通过try/catch捕获栈帧局部状态
function add(a, b){
  try{
    var sum = a + b
    throw Error()
  }
  catch(e){
    throw StackTraceError(e, ["a:", a, "b", b, "sum", sum].join("::"))
  }
  return sum
}

function add2(a){
  try{
    return 2 + add(0, a)
  }
  catch(e){
    throw StackTraceError(e, ["a", a].join("::"))
  }
}

function main(){
  try{
    add2(2)
  }
  catch(e){
    throw StackTraceError(e, "")
  }
}

try{
  main()
} catch(e){
  console.log(e+'')
}

chrome下

{"name":"StackTraceError","msg":"Internal error.","env":"a::0::b::2::sum::2","file":"at add (file:///home/john/index.html:57:11)"}
{"name":"StackTraceError","msg":"Internal error.","env":"a:;2","file":"at add2 (file:///home/john/index.html:67:16)"}
{"name":"StackTraceError","msg":"Internal error.","env":"","file":"at main (file:///home/john/index.html:76:5)"}
{"file":"at file:///home/john/index.html:84:3"}

 上面这种做法有三个问题

  1. V8引擎不会对包含try/catch的函数进行优化,如果每个函数都包含try/catch那会严重影响执行效率。
  2. 这种方式显然不能让每个开发人员手写,必须通过预编译器来静态织入,开发难度有点大哦。
  3. sum这种临时变量其实并不用记录,因为它可以被运算出来,只要记录ab即可。

 假如我们写的全是纯函数(就是相同入参必定得到相同的返回值,函数内部不依赖外部状态,如加法一样,1+1永远等于2),那么我们只需捕获入口/公用函数的入参即可恢复整个案发现场了。

function add(a, b){
  var sum = a + b
  throw Error()
  return sum
}

function add2(a){
  try{
    return 2 + add(0, a)
  }
  catch(e){
    throw {error:e, env:["a:", a].join("::")})
  }
}

function main(){
  add2(2)
}

try{
  main()
} catch(e){
  console.log(e+'')
}

 然后我们就可以拿着报错信息从add2逐步调试到add中了。假如用ClojureScript我们还可以定义个macro简化一下

;; 私有函数
(defn- add [a b]
  (let [sum (+ a b)]
    (throw (Error.))
    sum))
;; 入口/公用函数
(defn-pub add2 [a]
  (+ 2 (add 0 a)))

(defn main []
  (add2 2))

(try
 (main)
 (catch e
  (println e)))

defn-pub macro的定义

(defmacro defn-pub [name args & body]
  (let [e (gensym)
        arg-names (mapv str args)]
    `(def ~name
       (fn ~args
         (try ~@body
           (catch js/Object ~e
             (throw (clj->js {:e ~e, :env (zipmap ~arg-names ~args)}))))))))

总结

 写到这里其实也没有一个很好的方式去捕获案发现场证据,在入口/公用函数中加入try/catch是我现阶段能想到比较可行的方式,请各位多多指点。
尊重原创,转载请注明转自:http://www.cnblogs.com/fsjohnhuang/p/7729527.html ^_^肥仔John

参考

http://www.cnblogs.com/exiahan/p/4310010.html
http://blog.csdn.net/qiu265843468/article/details/17844419
http://lucasfcosta.com/2017/02/17/JavaScript-Errors-and-Stack-Traces.html
http://blog.shaochuancs.com/about-error-capturestacktrace/
https://github.com/v8/v8/wiki/Stack-Trace-API

时间: 2024-10-27 17:15:50

前端魔法堂——调用栈,异常实例中的宝藏的相关文章

前端魔法堂——异常不仅仅是try/catch

前言  编程时我们往往拿到的是业务流程正确的业务说明文档或规范,但实际开发中却布满荆棘和例外情况,而这些例外中包含业务用例的例外,也包含技术上的例外.对于业务用例的例外我们别无它法,必须要求实施人员与用户共同提供合理的解决方案:而技术上的例外,则必须由我们码农们手刃之,而这也是我想记录的内容.  我打算分成<前端魔法堂--异常不仅仅是try/catch>和<前端魔法堂--调用栈,异常实例中的宝藏>两篇分别叙述内置/自定义异常类,捕获运行时异常/语法异常/网络请求异常/Promise

《C语言程序设计进阶教程》一2.7 在DDD(命令行调试程序)上检测调用栈

2.7 在DDD(命令行调试程序)上检测调用栈 本文讲的是C语言程序设计进阶教程一2.7 在DDD(命令行调试程序)上检测调用栈,在编辑器中输入下面的程序,并把名字存为p1.c不要担心还不能完全理解main函数中的argv,这将会在之后讨论.在Linux终端下使用下面的命令创建可执行文件:这里使用gcc将C程序(p1.c)的源文件转化为计算机可以理解的可执行文件.添加-g启用调试,这样我们可以检验调用栈.添加-Wall和-Wshadow启用警告信息.影子变量将会在4.1节中进行讲解.警告信息有时

在C/C++程序里打印调用栈信息

我们知道,GDB的backtrace命令可以查看堆栈信息.但很多时候,GDB根本用不上.比如说,在线上环境中可能没有GDB,即使有,也不太可能让我们直接在上面调试.如果能让程序自己输出调用栈,那是最好不过了.本文介绍和调用椎栈相关的几个函数.   NAME       backtrace, backtrace_symbols, backtrace_symbols_fd - support for application self-debugging SYNOPSIS       #include

小览CallStack(调用栈)(三)-用调试器脚本查看调用栈信息

在这一系列之前的两篇文章中,我介绍了如何在windbg中查看调用栈的相关 信息(详见小览call stack(调用栈)(一)),以及调用约定(详见小览call stack(调用栈) (二)--调用约定).今天的这篇博客在二者的基础 之上,介绍如何使用调式器脚本程序来观察调用栈.对CallStack感兴趣的朋友 可以在此基础上开发更加详尽的脚本来观察CallStack的信息:对调试感兴趣的 朋友则可以看一下DScript的用处. 我们先来看一个例子,下面的程序并不是一个优美的程序片段,但是它能够

小览call stack(调用栈) (二)——调用约定

在上一篇博客中小览call stack(调用栈) (一)中,我展示了如何在windbg中 观察调用栈的相关信息:函数的返回地址,参数,返回值.这些信息都按照一定 的规则存储在固定的地方.这个规则就是调用约定(calling convention). 调用约定在计算机界不是什么新鲜的概念,已经有许多相关的文献给予详细 的介绍.比较全面的介绍可以参见wikipedia上的相关页面.然而,如果你和我 一样,在第一次接触调用约定的时候,觉得这个概念是个高深神秘的冬冬,那么 就请跟随我一起,在这篇博客中看

小览call stack(调用栈) (一)

栈在计算机领域中是个经常提到的名词,数据结构中有栈:网络传输中有协 议栈.今天我们讨论的调用栈(call stack),指的是在程序的执行过程中存储函 数调用信息的动态数据结构. 这个定义可能太抽象了一些,在给出具体的例子之前,请大家先思考一个问 题,哪些信息是函数调用过程中所需要的?或者这么问,一个编译器,在面对一 个函数的调用指令时,该生成哪些代码? 首先,函数的返回地址要保存下来.就好像你和你的小狗玩仍飞碟游戏,每 一个函数调用好比扔一个飞碟,当你的狗狗哼兹哼兹的捡来飞碟,函数完执行的 时

关于winform中多线程调用AX控件中的方法会阻塞UI线程的问题

问题描述 本人新手...winform中子线程调用AX控件中的某个耗时的方法(3.4秒)会阻塞UI线程,请问有没有办法在子线程中创建AX控件,让子线程去执行AX控件中的方法呢? 解决方案 解决方案二:你可以尝试直接在新的线程里执行该方法,不行的话只能由控件编写者提供异步的方法解决方案三:winform中子线程调用AX控件中的某个耗时的方法(3.4秒)会阻塞UI线程既然你是在子线程里调用,为什么会阻塞UI线程?放出你的代码来解决方案四:控件是vc写的,拖到form上的,SCM_Card_Chb是控

Jquery调用iframe父页面中的元素及方法_javascript技巧

一.在iframe中查找父页面元素的方法: $('#id', window.parent.document) 二.在iframe中调用父页面中定义的方法和变量: parent.method parent.value 三.实例 父页面: <%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="IframeDemo._De

Android群英传笔记——第八章:Activity与Activity调用栈分析

Android群英传笔记--第八章:Activity与Activity调用栈分析 开篇,我们陈述一下Activity,Activity是整个应用用户交互的核心组件,了解Activity的工作模式,生命周期和管理方式,是了解Android的基础,本节主讲 Activity的生命周期与工作模式 Activity调用栈管理 一.Activity Activity作为四大组建出现平率最高的组件,我们在哪里都能看到他,就让我们一起先来了解一下他的生命周期 1.起源 Activity是用户交互的第一接口,他