iOS平台使用LLDB调试JS逻辑之——对象属性
更多精彩内容,欢迎关注作者微信公众号:码工笔记
一、Native、JS混合调试的问题
随着基于RN或小程序的跨端框架越来越流行,调试Native和JS之间的数据交互也成了移动端开发者绕不开的技能需求。
以JavaScriptCore为例,JSC框架把Native和JS的数据交互封装得比较好,两个世界要交互的时候,两侧各自通过一些magic方法,就能将各自的读写顺利传递到对岸。
到具体调试问题的时候,JS侧是用Safari的Inspector,Native侧则用Xcode里的lldb,虽然经常需要在Xcode和Safari的Inspector之间频繁切换,但大部分情况下,各取所需,也能满足需求。
但这里面其实有一个潜藏的尴尬点,就是JS的调试与Native的调试本质上是割裂的,其割裂不仅反映在工具链的不同,更重要的是反映在数据上。
调试JS侧的代码时,在Safari Inspector里面只能看到JS侧的对象及其调用栈;而调试Native代码时在LLDB中只能看到Native侧的数据和调用栈。
而在真正复杂的交互流程中,JS和Native两边的对象就分得没那么开了。 JS侧会将包装了native对象的JS对象传来传去做处理,native侧也会将包装了JS对象的native对象传来传去做处理,有些情况下,是需要在单步调试的过程中同时查看native和js侧两边的数据的。
另外,大家应该也注意到过,在LLDB将Native侧程序断点停住的时候Safari Inspector也会失去响应(因为Native进程进止的情况下,无法响应WebKit Inspector Protocol的调试消息),因此这种情况下Inspector侧无法进行任何有意义的debug操作。
我们知道JS是运行在虚拟机中的,在Safari Inspector中是不可能看到Native侧内存的,那反过来,能不能在LLDB中直接查看JS对象的值呢?
二、使用LLDB分析JS对象
这一节咱们就来看看,如何在LLDB中分析JS对象。
Butterfly
下面是一个简单的例程:
//创建一个JSContext
self.jsContext = [[JSContext alloc] init];
//在JS侧创建一个数组 x 并设置其属性
[self.jsContext evaluateScript:@"x = [2,3,4]; x.abc = 5; x.def = 6;"];
我们在 LLDB 中看看x
是什么样子:
(lldb) p self.jsContext[@"x"]
(JSValue *) $1 = 0x000060000308f620 //对象x_native的地址
不出所料,是个JSValue *
类型,它的具体内容如何看呢?能从LLDB中看到[2,3,4]
和它的属性abc
、def
吗?
先看看这个地址里放了啥内容:
(lldb) x/2xg 0x000060000308f620 //对象x_native的地址
0x60000308f620: 0x0000000106094aa0 0x000000012a9af6e8
JSValue*是个OC对象,第一个8byte是isa,不管它。
第二个8 byte应该就是它的成员了。翻了一下JavaScriptCore的源码,JSValue
只有一个类型为JSValueRef
的成员变量m_value
,所以这里 m_value == 0x000000012a9af6e8。
JSValueRef
是个typedef:
typedef const struct OpaqueJSValue* JSValueRef;
搜了下源码,OpaqueJSValue *
基本都被强转转成了JSCell*
。JSCell 是个C++类,看看这个m_value里面放了啥:
(lldb) x/8xg 0x000000012a9af6e8 //JSCell
0x12a9af6e8: 0x010823050000a92f 0x000000012aba8028
0x12a9af6f8: 0x0000000000000000 0x000000012a9f9840
0x12a9af708: 0x0000000000000000 0x0000000000000000
0x12a9af718: 0x0000000000000000 0x0000000000000000
这是个C++对象,第一个8 byte是JSCell
的一些成员变量,记录了其StructureID
、cell类型等信息:
StructureID m_structureID; //uint32_t
IndexingType m_indexingTypeAndMisc; //uint8_t
JSType m_type; //uint8_t
TypeInfo::InlineTypeFlags m_flags; //uint8_t
CellState m_cellState; //uint8_t
其中0x0000a92f就是对象的StuctureID
。在JSC中,每个JS对象的prototype
对应一个Structure
,当一个对象的prototype
类型发生变化时,其StructureID
也会随之发生改变。
然后看看后面的第二个8 byte:
(lldb) x/8xg 0x000000012aba8028 //JSValue->m_value->ptr(JSCell *)
0x12aba8028: 0xfffe000000000002 0xfffe000000000003
0x12aba8038: 0xfffe000000000004 0x0000000000000000
0x12aba8048: 0x0000000000000000 0x0000000000000000
0x12aba8058: 0x0000000000000000 0x0000000000000000
这里就有点意思了,这里分明存了encode以后的[1,2,3,4],不过高位加了0xfffe
,翻一下相关文档,可知,JSC将64位的数据做了encoding,具体如下(详见webkit源码中的JSCJSValue.h
文件):
Pointer { 0000:PPPP:PPPP:PPPP
/ 0002:****:****:****
Double { ...
\ FFFC:****:****:****
Integer { FFFE:0000:IIII:IIII
False: 0x06
True: 0x07
Undefined: 0x0a
Null: 0x02
指针的前两个字节为0x0000
;32位整数的前两个字节为0xFFFE
;浮点数的前两个字节介于0x0002
与0xFFFC
之间。
对象x的数组成员找到了,那它的属性值放在哪里呢?
咱们把指针往前倒一倒看看,指针往前挪0x28看看:
(lldb) x/16xg 0x000000012aba8000
0x12aba8000: 0x0000000000000000 0x0000000000000000
0x12aba8010: 0xfffe000000000006 0xfffe000000000005
0x12aba8020: 0x0000000500000003 0xfffe000000000002
0x12aba8030: 0xfffe000000000003 0xfffe000000000004
0x12aba8040: 0x0000000000000000 0x0000000000000000
x的属性值5和6在这儿呢!
原来我们之前拿到的地址指向的并不是对象所占内存的起始地址,而是中间,其后面是数组元素,前面是属性值!这个结构在JSC里面就叫做Butterfly
。对象指针指向的是蝴蝶(对象内存)的肚子,左翅膀是属性值,右翅膀是数组元素。
普通对象的存储
是不是所有JS对象都是这么存的呢?我们看一个简单的JS对象(非Array)的情况:
//在JS侧创建一个简单对象,并设置其属性
a = {'prop1':10, 'prop2':'value2'};
还是跟之前一样,一步步看看它的内存数据:
//查看a_native的地址
(lldb) p self.jsContext[@"a"]
(JSValue *) $0 = 0x00006000004f6e20
//看JSValue的具体数据,m_value = 0x0000000114540000
(lldb) x/8xg 0x00006000004f6e20
0x6000004f6e20: 0x00007fff86e74c38 0x0000000114540000
0x6000004f6e30: 0x0000600000a899b0 0x0000000000000000
0x6000004f6e40: 0x00007fff86e74c38 0x00000001130421c8
0x6000004f6e50: 0x0000600000a899b0 0x0000000000000000
//查看JSCell的数据
(lldb) x/8xg 0x0000000114540000
0x114540000: 0x0100180000000790 0x0000000000000000
0x114540010: 0xfffe00000000000a 0x0000000114421820
0x114540020: 0x0000000000000000 0x0000000000000000
0x114540030: 0x0000000000000000 0x0000000000000000
可以看到在 0x114540010 地址处存了 0xfffe00000000000a,也就是prop1的值(整数10)。
它后面跟着的地址 0x0000000114421820 应该就是prop2的值了吧?我们确认一下:
(lldb) x/8xg 0x0000000114421820
0x114421820: 0x0118020000002d76 0x0000000113057720
0x114421830: 0x0000000000000000 0x0000000000000000
0x114421840: 0x0000000000000000 0x0000000000000000
0x114421850: 0x0000000000000000 0x0000000000000000
(lldb) x/8xg 0x0000000113057720
0x113057720: 0x0000000600000002 0x0000000113057734
0x113057730: 0x756c6176c6a4dd1c 0x0000000000003265
0x113057740: 0x947dfbfa94fb8887 0x000000fbf83010f9
0x113057750: 0x0000001511f92f00 0x0000000000000000
(lldb) x/s 0x113057734
0x113057734: "value2"
果然在这里。
JSC公开类型与内部类型对应关系
下面总结一些JSC中常见的公开类型与内部类型的对应关系,方便后续查看:
JSContextRef == OpaqueJSContext* == JSGlobalObject* == JSGlobalContextRef
JSValueRef == OpaueJSValue* == JSCell*
JSObjectRef == JSObject*
JSContextGroupRef == OpaqueJSContextGroup* == JSC::VM*
小结
本文介绍了使用LLDB分析JS对象的基本流程,同时介绍了Butterfly和普通对象数据的内存布局以及如何查找相应的属性值。至于如何分析Structure链,以及JS运行时调用栈等信息,且看下回。