在上一篇cache_t探索文章中,我们探索了cache_t的作用,是去进行方法缓存,其目的就是当方法再次调用时能更快的进行响应.,接下来我们探究一下如何从cache_t中读取方法.
我们在源码中 obcj-cache.m中会找到这样一个注释
* Cache readers (PC-checked by collecting_in_critical())
* objc_msgSend*
* cache_getImp
这说明cache读取时通过objc_msgSend和cache_getImp这两个函数进行读取.
我们都知道OC是一门动态的语言,动态语言就是指我们的程序在运行的过程中可以对于我们的类、对象、属性、方法、变量进行修改,可以改变他们的数据结构,可以添加或者删除一些函数,可以去改变一些变量的值等等的操作.我们经常提到的runtime就是可以实现语言动态的一组api,runtime所有都是围绕两个核心
1:类的各方面的动态配置(使用runtime的api动态的修改我们的类或者对象的信息,为类添加属性方法,修改成员变量的值)
2:消息传递(消息的发送和消息的转发,消息的发送就是runtime通过sel找imp,然后实现对应的方法)
我们的消息发送在编译的时候,编译器就会把这个方法转换成为objc_msgSend这么一个函数,为了验证这件事情,我们在main中加入熟悉的person
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
WTPerson *p = [WTPerson alloc];
[p say:@"hello"];
[p run];
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
然后在终端中输入 xcrun -sdk iphonesimulator clang -arch x86_64 -rewrite-objc main.m,我们获取到.cpp文件,在.cpp文件中我们可以找到相应的编译器编译后的代码
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
WTPerson *p = ((WTPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("WTPerson"), sel_registerName("alloc"));
((void (*)(id, SEL, NSString * _Nonnull))(void *)objc_msgSend)((id)p, sel_registerName("say:"), (NSString *)&__NSConstantStringImpl__var_folders_x6_75ftxbbd4t97nl_2t2sh7kww0000gn_T_main_bd8849_mi_0);
((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("run"));
}
return UIApplicationMain(argc, argv, __null, appDelegateClassName);
}
我们发现,alloc这种类方法和say、run这种对象方法都被包装成了((void (*)(id, SEL))(void *)objc_msgSend)(receiver, sel)格式.
当我们的方法都没有参数,可转换成objc_msgSend后都会带有两个默认的参数消息的接收者receiver、消息的方法名sel.类方法的接收者是类对象,他就会通过类对象的isa指针找到我们的元类,从元类中找对应的方法;实例对象方法的接收者是实例对象,他就会通过实例对象的isa指针找到我们的类对象,从类对象中找对应的方法.
当我们的方法有参数时,我们的参数会放在objc_msgSend的第三个参数及以后中,第一个和第二个参数是不变的,也就是说,objc_msgSend的参数是两个默认的参数加上方法本身的参数.
下面我们试一下直接调用objc_msgSend函数
发现同样调用类say方法,也就是说我们可以直接手动去调用
objc_msgSend函数.
需要注意:手动调用需要导入头文件#import <objc/message.h>
在.cpp文件中,我们可以看到objc_msgSend其实是有很多种类的
__OBJC_RW_DLLIMPORT void objc_msgSend(void); //
__OBJC_RW_DLLIMPORT void objc_msgSendSuper(void); //给父类发消息
__OBJC_RW_DLLIMPORT void objc_msgSend_stret(void); //返回值是结构体
__OBJC_RW_DLLIMPORT void objc_msgSendSuper_stret(void); //
__OBJC_RW_DLLIMPORT void objc_msgSend_fpret(void); //返回值是浮点类型
接下来我们详细了解一下objc_msgSendSuper和objc_msgSend的区别
objc_msgSendSuper
我们创建一个WTStudent类继承WTPerson,重写其init方法
- (instancetype)init {
if (self = [super init]) {
NSLog(@"%@", [self class]);
NSLog(@"%@", [super class]);
}
return self;
}
面试的摧残让我们都知道两个打印的都是WTStudent,但是为什么呢?让我们来研究一下self和super这两个关键字有什么区别:
我们用clang命令编译WTStudent.m,得到如下编译代码
static instancetype _I_WTStudent_init(WTStudent * self, SEL _cmd) {
if (self = ((WTStudent *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("WTStudent"))}, sel_registerName("init"))) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_x6_75ftxbbd4t97nl_2t2sh7kww0000gn_T_WTStudent_8d6cff_mi_0, ((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class")));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_x6_75ftxbbd4t97nl_2t2sh7kww0000gn_T_WTStudent_8d6cff_mi_1, ((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("WTStudent"))}, sel_registerName("class")));
}
return self;
}
我们发现self编译后是objc_msgSend,super编译后是objc_msgSendSuper,objc_msgSendSuper的第一个参数不再是self,而是__rw_objc_super的结构体指针,包括消息的接收者:self和开始搜索方法实现的超类super:(id)class_getSuperclass(objc_getClass("WTStudent")).
我们看到objc_msgSendSuper的消息的接收者仍然是self,所以[super class]打印出来的一样是self这个对象所指的类,也就是WTStudent.
通过这里,我们可以得出结论:objc_msgSend和objc_msgSendSuper唯一的区别只有他们查找方法的出发点不同,objc_msgSendSuper是从方法实现的超类super也就是类的父类开始查找,objc_msgSend是从本身开始查找.我们可以直接实现super关键字
struct objc_super {
/// Specifies an instance of a class.
__unsafe_unretained _Nonnull id receiver;
/// Specifies the particular superclass of the instance to message.
#if !defined(__cplusplus) && !__OBJC2__
/* For compatibility with old objc-runtime.h header */
__unsafe_unretained _Nonnull Class class;
#else
__unsafe_unretained _Nonnull Class super_class;
#endif
/* super_class is the first class to search */
};
我们可以看到,我们成功的调用了
super的run方法,如果super_class改成WTStudent,调用的方法是study方法的话,不会因为super没有study方法崩溃,而会因为一直调用[WTStudent study]死循环崩溃.如果super_class改成改成NSObject则会因为NSObject没有run方法崩溃.
objc_msgSend
接下来我们来进行探索一下objc_msgSend的流程,在源码我们找到了各种系统的实现,最后我们锁定到了objc-msg-arm64.s文件中,以.s结尾的都是汇编写的文件,而arm64是我们的真机架构,objc_msgSend为什么使用汇编实现,因为汇编比C语言更快,可以免去局部变量的copy操作,参数直接被存放寄存器中,可以直接使用
//进入objc_msgSend流程
ENTRY _objc_msgSend
//流程开始,无需frame
UNWIND _objc_msgSend, NoFrame
//判断p0(消息接受者)是否存在,不存在则重新开始执行objc_msgSend
cmp p0, #0 // nil check and tagged pointer check
//如果支持小对象类型。返回小对象或空
#if SUPPORT_TAGGED_POINTERS
//b是进行跳转,b.le是小于判断,也就是小于的时候LNilOrTagged
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
//等于,如果不支持小对象,就LReturnZero
b.eq LReturnZero
#endif
//通过p13取isa
ldr p13, [x0] // p13 = isa
//通过isa取class并保存到p16寄存器中
GetClassFromIsa_p16 p13, 1, x0 // p16 = class
//LGetIsaDone是一个入口
LGetIsaDone:
// calls imp or objc_msgSend_uncached
//进入到缓存查找或者没有缓存查找方法的流程
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
// nil check判空处理,直接退出
b.eq LReturnZero // nil check
GetTaggedClass
b LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif
LReturnZero:
// x0 is already zero
mov x1, #0
movi d0, #0
movi d1, #0
movi d2, #0
movi d3, #0
ret
END_ENTRY _objc_msgSend
这一部分代码,实际上就是先检测我们调用_objc_msgSend的调用方,是否为空,如果为空,则直接去进行方法的调用,直接返回。否则就将调用方的isa指针存到p13寄存器里面去,其中比较重要的一个方法是GetClassFromIsa_p16他是通过isa来获取class,下面我们来重点看下他
.macro GetClassFromIsa_p16 src, needs_auth, auth_address /* note: auth_address is not required if !needs_auth */
#if SUPPORT_INDEXED_ISA
// Indexed isa
mov p16, \src // optimistically set dst = src
tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f // done if not non-pointer isa
// isa in p16 is indexed
adrp x10, _objc_indexed_classes@PAGE
add x10, x10, _objc_indexed_classes@PAGEOFF
ubfx p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS // extract index
ldr p16, [x10, p16, UXTP #PTRSHIFT] // load class from array
1:
// **因为是真机环境,所以走这里**
#elif __LP64__
//如果needs_auth参数等于0,暂时不用管他的含义,在objc_msgSend中,这里传入的是1
.if \needs_auth == 0 // _cache_getImp takes an authed class already
// ** 将src,也就是我们的入参isa指针,存放到寄存器P16中**
mov p16, \src
.else
// 64-bit packed isa
//**调用ExtractISA**
ExtractISA p16, \src, \auth_address
.endif
#else
// 32-bit raw isa
mov p16, \src
#endif
.endmacro
//**这个方法有两个实现,一个是针对A12芯片以上的手机,我们这里看A12以下的**
.macro ExtractISA
//**实际上就是将传入的参数,对象的isa与isa_mask按位与,也就是得到Class**
and $0, $1, #ISA_MASK
.endmacro
这一段内容,简单来说,就是将对象的isa传入GetClassFromIsa_p16然后,这个方法针对不同的isa类型做了不同的处理,最终得到了类对象Class。在下面就是CacheLookup方法了,我们继续往下看
//在cache中通过sel查找imp的核心流程
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
//从x16中取出class移到x15中
mov x15, x16 // stash the original isa
//开始查找
LLookupStart\Function:
// p1 = SEL, p16 = isa
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
//ldr表示将一个值存入到p10寄存器中
//x16表示p16寄存器存储的值,当前是Class
//#数值表示一个值,这里的CACHE经过全局搜索发现是2倍的指针地址,也就是16个字节
//#define CACHE (2 * __SIZEOF_POINTER__)
//经计算,p10就是cache
ldr p10, [x16, #CACHE] // p10 = mask|buckets
lsr p11, p10, #48 // p11 = mask
and p10, p10, #0xffffffffffff // p10 = buckets
and w12, w1, w11 // x12 = _cmd & mask
//真机64位看这个
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
//CACHE 16字节,也就是通过isa内存平移获取cache,然后cache的首地址就是 (bucket_t *)
ldr p11, [x16, #CACHE] // p11 = mask|buckets
#if CONFIG_USE_PREOPT_CACHES
//获取buckets
#if __has_feature(ptrauth_calls)
tbnz p11, #0, LLookupPreopt\Function
and p10, p11, #0x0000ffffffffffff // p10 = buckets
#else
//and表示与运算,将与上mask后的buckets值保存到p10寄存器
and p10, p11, #0x0000fffffffffffe // p10 = buckets
//p11与#0比较,如果p11不存在,就走Function,如果存在走LLookupPreopt
tbnz p11, #0, LLookupPreopt\Function
#endif
//按位右移7个单位,存到p12里面,p0是对象,p1是_cmd
eor p12, p1, p1, LSR #7
and p12, p12, p11, LSR #48 // x12 = (_cmd ^ (_cmd >> 7)) & mask
#else
and p10, p11, #0x0000ffffffffffff // p10 = buckets
//LSR表示逻辑向右偏移
//p11, LSR #48表示cache偏移48位,拿到前16位,也就是得到mask
//这个是哈希算法,p12存储的就是搜索下标(哈希地址)
//整句表示_cmd & mask并保存到p12
and p12, p1, p11, LSR #48 // x12 = _cmd & mask
#endif // CONFIG_USE_PREOPT_CACHES
这一段主要是获取到我们缓存也就是之前讲过的cache_t的首地址,并且获取到buckets和mask。接着继续往下走
//去除掩码后bucket的内存平移
//PTRSHIFT经全局搜索发现是3
//LSL #(1+PTRSHIFT)表示逻辑左移4位,也就是*16
//通过bucket的首地址进行左平移下标的16倍数并与p12相与得到bucket,并存入到p13中
add p13, p10, p12, LSL #(1+PTRSHIFT)
// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
// do {
//ldp表示出栈,取出bucket中的imp和sel分别存放到p17和p9
1: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
//cmp表示比较,对比p9和p1,如果相同就找到了对应的方法,返回对应imp,走CacheHit
cmp p9, p1 // if (sel != _cmd) {
//b.ne表示如果不相同则跳转到2f
b.ne 3f // scan more
// } else {
2: CacheHit \Mode // hit: call or return imp
// }
//向前查找下一个bucket,一直循环直到找到对应的方法,循环完都没有找到就调用_objc_msgSend_uncached
3: cbz p9, \MissLabelDynamic // if (sel == 0) goto Miss;
//通过p13和p10来判断是否是第一个bucket
cmp p13, p10 // } while (bucket >= buckets)
b.hs 1b
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
add p13, p10, w11, UXTW #(1+PTRSHIFT)
// p13 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
add p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
// p13 = buckets + (mask << 1+PTRSHIFT)
// see comment about maskZeroBits
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
add p13, p10, p11, LSL #(1+PTRSHIFT)
// p13 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = first probed bucket
// do {
4: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
cmp p9, p1 // if (sel == _cmd)
b.eq 2b // goto hit
cmp p9, #0 // } while (sel != 0 &&
ccmp p13, p12, #0, ne // bucket > first_probed)
b.hi 4b
这一段是查找缓存中最核心的代码,通过循环来查找方法在缓存中的位置,如果找到了则调用CacheHit \Mode,否则调用MissLabelDynamic。接下来我们看看CacheHit的实现
// CacheHit: x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa
.macro CacheHit
//**objs_msgSend中$0==NORMAL**
.if $0 == NORMAL
//调用找到的方法
TailCallCachedImp x17, x10, x1, x16 // authenticate and call imp
.elseif $0 == GETIMP
mov p0, p17
cbz p0, 9f // don't ptrauth a nil imp
AuthAndResignAsIMP x0, x10, x1, x16 // authenticate imp and re-sign as IMP
9: ret // return IMP
.elseif $0 == LOOKUP
// No nil check for ptrauth: the caller would crash anyway when they
// jump to a nil IMP. We don't care if that jump also fails ptrauth.
AuthAndResignAsIMP x17, x10, x1, x16 // authenticate imp and re-sign as IMP
cmp x16, x15
cinc x16, x16, ne // x16 += 1 when x15 != x16 (for instrumentation ; fallback to the parent class)
ret // return imp via x17
.else
.abort oops
.endif
.endmacro
//**依旧有两个实现,我们看A12以下的**
.macro TailCallCachedImp
// $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa
//**$0(imp) ^ $3(isa)**
//**实际上就是一个解码的过程**
eor $0, $0, $3
//**跳转到$0(imp)的地址,就是调用IMP**
br $0
.endmacro
比较简单的实现,就是去调用查找到的SEL对应的IMP,实现方法的调用。到此,objc_msgSend调用方法,在缓存中查找方法的流程就全部结束了,这个流程我们也称之为方法的快速查找流程,和我们之前的找到cache_t中方法的流程是一样的,只是我们之前用的是lldb进行的查找,objc_msgSend直接使用编译器方法进行的查找
_lookUpImpOrForward
快速查找未找到会跳转__objc_msgSend_uncached,在其中会跳转到MethodTableLookup,在MethodTableLookup中执行了_lookUpImpOrForward,这里进行慢速查找流程
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves
MethodTableLookup
TailCallFunctionPointer x17
END_ENTRY __objc_msgSend_uncached
STATIC_ENTRY __objc_msgLookup_uncached
UNWIND __objc_msgLookup_uncached, FrameWithNoSaves
MethodTableLookup
ret
END_ENTRY __objc_msgLookup_uncached
STATIC_ENTRY _cache_getImp
GetClassFromIsa_p16 p0, 0
CacheLookup GETIMP, _cache_getImp, LGetImpMissDynamic, LGetImpMissConstant
.macro MethodTableLookup
SAVE_REGS MSGSEND
// lookUpImpOrForward(obj, sel, cls, LOOKUP_RESOLVER)
// receiver and selector already in x0 and x1
mov x2, x16
mov x3, #3
bl _lookUpImpOrForward
// IMP in x0
mov x17, x0
RESTORE_REGS MSGSEND
在源码objc-runtime-new.m中,我们找到了他的实现
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior){
// 定义消息转发的imp
const IMP forward_imp = (IMP)_objc_msgForward_impcache;
IMP imp = nil;
Class curClass;
runtimeLock.assertUnlocked();
// 判断类是否初始化
if (slowpath(!cls->isInitialized())) {
behavior |= LOOKUP_NOCACHE;
}
runtimeLock.lock();
// 是否是已知的类,是否已经被加载过
checkIsKnownClass(cls);
// 确定父类继承链的关系
cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
runtimeLock.assertLocked();
curClass = cls;
// 死循环,只有达到条件才会退出循环,
for (unsigned attempts = unreasonableClassCount();;) {
if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
// 再一次从cache中找imp
// 目的: 防止多线程操作时,刚好调用函数,此时缓存进来了
#if CONFIG_USE_PREOPT_CACHES
imp = cache_getImp(curClass, sel);
if (imp) goto done_unlock;
curClass = curClass->cache.preoptFallbackClass();
#endif
} else {
method_t *meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
imp = meth->imp(false);
goto done;
}
if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
imp = forward_imp;
break;
}
}
if (slowpath(--attempts == 0)) {
_objc_fatal("Memory corruption in class list.");
}
imp = cache_getImp(curClass, sel);
if (slowpath(imp == forward_imp)) {
break;
}
if (fastpath(imp)) {
goto done;
}
}
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}
done:
if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES
while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
cls = cls->cache.preoptFallbackClass();
}
#endif
log_and_fill_cache(cls, imp, sel, inst, curClass);
}
done_unlock:
runtimeLock.unlock();
if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
return nil;
}
return imp;
}
static method_t *getMethodNoSuper_nolock(Class cls, SEL sel){
runtimeLock.assertLocked();
auto const methods = cls->data()->methods();
for (auto mlists = methods.beginLists(),
end = methods.endLists();
mlists != end;
++mlists)
{
method_t *m = search_method_list_inline(*mlists, sel);
if (m) return m;
}
return nil;
}
findMethodInSortedMethodList(SEL key, const method_list_t *list){
if (list->isSmallList()) {
if (CONFIG_SHARED_CACHE_RELATIVE_DIRECT_SELECTORS && objc::inSharedCache((uintptr_t)list)) {
return findMethodInSortedMethodList(key, list, [](method_t &m) { return m.getSmallNameAsSEL(); });
} else {
return findMethodInSortedMethodList(key, list, [](method_t &m) { return m.getSmallNameAsSELRef(); });
}
} else {
return findMethodInSortedMethodList(key, list, [](method_t &m) { return m.big().name; });
}
}
其实现就是从先判断是否缓存过,缓存过再次查找cache,未缓存就从当前类的方法列表中查找方法的实现,最后根据findMethodInSortedMethodList这个二分查找流程查找方法实现.
static method_t * findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName){
ASSERT(list);
auto first = list->begin();
auto base = first; // 0
decltype(first) probe;
uintptr_t keyValue = (uintptr_t)key;
uint32_t count;
// count 16
// 小了的情况
// 第一次 probe = 0 + count >> 1 = 8 base = 9 count-- = 15
// 第二次 probe = 12 base = 13 count >>= 1 = 7 count >> 1 = 3 count-- = 6
// 第三次 probe = 14 count >>=1 = 3 count >> 1 = 1
// 大了的情况
// 第一次 probe = 0 + count >> 1 = 8 base = 0 count = 16
// 第二次 probe = 4 base = 0 count >>= 1 = 8 count >> 1 = 4
// 第三次 probe = 2 count >>=1 = 2 count >> 1 = 0
for (count = list->count; count != 0; count >>= 1) {
probe = base + (count >> 1);
uintptr_t probeValue = (uintptr_t)getName(probe);
if (keyValue == probeValue) {
while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) { // 查找第一次出现的地方,为了调用分类的方法
probe--;
}
return &*probe;
}
if (keyValue > probeValue) {
base = probe + 1;
count--;
}
}
return nil;
}
当我们在慢速查找找到方法时,会调用log_and_fill_cache,其内部调用cls->cache.insert(sel, imp, receiver);将方法存入cache中,下次调用会进行快速查找流程,子类调用父类方法,缓存仍然会缓存到子类,因为在for循环中,如果子类没找到时curClass = curClass->getSuperclass()会将curClass更新成父类,log_and_fill_cache的写入与curClass无关,而是写入cls的cache,cls一直是不变的.
以上是找到了imp,当未找到imp,则会将最初定义消息转发的imp赋值给当前imp,进行消息转发流程
总结
objc_msgSend其具体实现如下:
1.receiver是否存在
2.reciver - isa - class
3.class - 内存偏移 - cache
4.cache - buckets - 对应sel
5.buckets 有对应的sel - cacheHit - 调用imp - 方法的快速查找流程
6.buckets 没有对应的sel - __objc_msgSend_uncached - 方法的慢速查找流程
7._lookUpImpOrForward - 先找当前类的methodlist - 再找父类的cache - 父类的methodlist - 父类为nil - forward的imp