问题先行
1、OC中调用方法的本质是?
2、OC中调用方法的流程是如何?
3、方法查找流程中方法的比对,会判断是类方法还是实例方法吗?
4、如下代码执行会crash吗?
@interface Son : NSObject
+ (void)test;
@end
@implementation Son
@end
@interface NSObject (Test)
@end
@implementation NSObject (Test)
- (void)test {
}
@end
/// 这个代码执行后会crash吗?
[Son test];
5、可以用C语言实现一个二分查找吗?
6、resolveInstanceMethod
、resolveClassMethod
为什么会在相应方法没有实现的时候调用两次?
资料准备
1、objc源码下载opensource.apple.com/
初步探索
通过clang命令,生成相关代码的C++文件,查看底层实现。
clang -rewrite-objc main.m -o main.cpp
实例
int main(int argc, char * argv[]) {
@autoreleasepool {
ASTest *test = [[ASTest alloc]init];
[test run];
return 0;
}
}
int main(int argc, char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
ASTest *test = ((ASTest *(*)(id, SEL))(void *)objc_msgSend)((id)((ASTest *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("ASTest"), sel_registerName("alloc")), sel_registerName("init"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)test, sel_registerName("run"));
return 0;
}
}
通过Xcode的符号断点可以看到如下流程
首先在方法出打一个断点,然后在设置一个objc_msgSend的符号断点,然后读取寄存器的值确认,就可以得到如下结果。
结论
1、通过以上可以看出OC方法调用底层的本质就是通过objc_msgSend
来发送消息
2、objc_msgSend
两个固定个参数:id
self
(消息接收者)、SEL
sel
(方法编号),不固定参数:id
(方法参数)
消息查找流程其实是通过上层的方法编号sel发送消息
objc_msgSend
找到具体实现imp
的过程
objc_msgSend
是用汇编写成的,为什么不用C而是用汇编写?
C语言不能通过写一个函数,保留未知的参数,跳转到任意的指针,而汇编有寄存器 对于一些调用频率太高的函数或操作,使用汇编来实现能够提高效率和性能,容易被机器来识别
objc_msgSend流程源码探索
开始查找
// 1️⃣ 消息发送 --- 汇编入口 ---
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
// 2️⃣ p0与空对比,判断接受者是否存在。p0为_objc_msgSend的第一个参数
cmp p0, #0 // nil check and tagged pointer check
// 3️⃣ --- le小于 --- taggedpointer(小对象类型)流程
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
b.eq LReturnZero
#endif
// 4️⃣ 根据对象取出isa,即从x0寄存器指向的地址取出isa,存入p13寄存器
ldr p13, [x0] // p13 = isa
// 5️⃣ 通过 p16(p13 & ISA_MASK)取到shiftcls信息,得到class信息。
GetClassFromIsa_p16 p13 // p16 = class
LGetIsaDone:
// calls imp or objc_msgSend_uncached
// 6️⃣ 如果有isa,走到CacheLookup,即缓存查找流程(sel-imp快速查找流程)
CacheLookup NORMAL, _objc_msgSend
1、开始objc_msgSend
2、判断消息接收者是否为空,为空直接返回
3、判断tagged_pointers
4、取得对象中的isa
存一份到p13中
5、根据isa
进行mask
地址偏移得到对应的上级对象(类、元类)
6、开始在缓存中查找imp
——开始了快速查找流程
快速查找流程
/********************************************************************
.......
//1️⃣ 此时x1是sel,x16是class
* Takes:
* x1 = selector
* x16 = class to be searched
.......
********************************************************************/
#define NORMAL 0
#define GETIMP 1
#define LOOKUP 2
.......
.macro CacheLookup
LLookupStart$1:
//2️⃣ 通过cache首地址平移16字节(因为在objc_class中,首地址距离cache正好16字节,即isa首地址 占8字节,superClass占8字节)获取cahce,cache中高16位存mask,低48位存buckets,即p11 = cache
// p1 = SEL, p16 = isa
ldr p11, [x16, #CACHE] // p11 = mask|buckets
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
//3️⃣ 从cache中分别取出buckets和mask,并由mask根据哈希算法计算出哈希下标
and p10, p11, #0x0000ffffffffffff // p10 = buckets
and p12, p1, p11, LSR #48 // x12 = _cmd & mask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
and p10, p11, #~0xf // p10 = buckets
and p11, p11, #0xf // p11 = maskShift
mov p12, #0xffff
lsr p11, p12, p11 // p11 = mask = 0xffff >> p11
and p12, p1, p11 // x12 = _cmd & mask
#else
#error Unsupported cache mask storage for ARM64.
#endif
//4️⃣ 根据所得的哈希下标index 和 buckets首地址,取出哈希下标对应的bucket
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
//5️⃣ 根据获取的bucket,取出其中的imp存入p17,即p17 = imp,取出sel存入p9,即p9 = sel
ldp p17, p9, [x12] // {imp, sel} = *bucket
//6️⃣ 第一次递归循环比较 sel、_cmd,CacheHit || CheckMiss
1: cmp p9, p1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
2: // not hit: p12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp p12, p10 // wrap if bucket == buckets
b.eq 3f
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b // loop
3: // wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
// p12 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
add p12, p12, p11, LSL #(1+PTRSHIFT)
// p12 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif
// Clone scanning loop to miss instead of hang when cache is corrupt.
// The slow path may detect any corruption and halt later.
ldp p17, p9, [x12] // {imp, sel} = *bucket
//7️⃣ 第二次递归循环比较 sel、_cmd,CacheHit || CheckMiss
1: cmp p9, p1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
2: // not hit: p12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp p12, p10 // wrap if bucket == buckets
b.eq 3f
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b // loop
LLookupEnd$1:
LLookupRecover$1:
3: // double wrap
JumpMiss $0
.endmacro
1、此时x1
是sel
,x16
是class
2、通过cache
首地址平移16字节(因为在objc_class
中,首地址距离cache
正好16字节,即isa
首地址 占8字节,superClass
占8字节),获取cahce
,cache
中高16位存mask
,低48位存buckets
,即p11 = cache
3、从cache
中分别取出buckets
和mask
,并由mask根据哈希算法计算出哈希下标
- 通过
cache
和掩码(即0x0000ffffffffffff
)的&运算,将高16位mask
抹零,得到buckets
指针地址,即p10
=buckets
- 将
cache
右移48位,得到mask
,即p11
=mask
- 将
objc_msgSend
的参数p1
(即第二个参数_cmd
)&msak
,通过哈希算法,得到需要查找存储sel-imp
的bucket
下标index
,即p12
=index
=_cmd
&mask
,为什么通过这种方式呢?因为在存储sel-imp时,也是通过同样哈希算法计算哈希下标进行存储,所以读取也需要通过同样的方式读取。 4、根据所得的哈希下标index
和buckets
首地址,取出哈希下标对应的bucket
- 其中
PTRSHIFT
等于3,左移4位(即2^4=16字节)的目的是计算出一个bucket
实际占用的大小,结构体bucket_t
中sel
占8字节,imp
占8字节 - 根据计算的哈希下标
index
乘以单个bucket
占用的内存大小,得到buckets
首地址在实际内存中的偏移量 - 通过首地址+实际偏移量,获取哈希下标
index
对应的bucket
5、根据获取的bucket
,取出其中的imp
存入p17,即p17=imp
,取出sel存入p9,即p9=sel
6、第一次递归循环 - 比较获取的
bucket
中sel
与objc_msgSend
的第二个参数的_cmd
(即p1)是否相等 - 如果相等,则直接跳转至
CacheHit
,即缓存命中,返回imp
- 如果不相等,有以下两种情况
- 如果一直都找不到,直接跳转至
CheckMiss
,因为$0是normal
,会跳转至__objc_msgSend_uncached
,即进入慢速查找流程 - 如果根据
index
获取的bucket
等于buckets
的第一个元素,则人为的将当前bucket
设置为buckets
的最后一个元素(通过buckets
首地址+mask
右移44位(等同于左移4位)直接定位到bucker
的最后一个元素),然后继续进行递归循环(第一个递归循环嵌套第二个递归循环),即⑥ - 如果当前
bucket
不等于buckets
的第一个元素,则继续向前查找,进入第一次递归循环 7、第二次递归循环:重复6的操作,与6中唯一区别是,如果当前的bucket
还是等于buckets
的第一个元素,则直接跳转至JumpMiss
,此时的$0是normal
,也是直接跳转至__objc_msgSend_uncached
,即进入慢速查找流程
8、在快速查找流程中,如果没有找到方法实现,无论是走到CheckMiss
还是JumpMiss
,最终都会走到__objc_msgSend_uncached
汇编函数,其中的核心是MethodTableLookup
(即查询方法列表),其中的核心是_lookUpImpOrForward
- 如果一直都找不到,直接跳转至
慢速查找流程
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();
/// 确保cls是初始化过的
if (slowpath(!cls->isInitialized())) {
behavior |= LOOKUP_NOCACHE;
}
/// 加锁保证读取的线程安全
runtimeLock.lock();
/// 判断当前是否是一个已知的类
checkIsKnownClass(cls);
/// 判断类是否实现,如果没有实现,需要先实现。
cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
// runtimeLock may have been dropped but is now locked again
runtimeLock.assertLocked();
curClass = cls;
/// 当前类对象中方法查找、若找到进行缓存
/// 沿着继承链查找父类的类对象中方法查找,若找到进行缓存
for (unsigned attempts = unreasonableClassCount();;) {
if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES
imp = cache_getImp(curClass, sel);
if (imp) goto done_unlock;
curClass = curClass->cache.preoptFallbackClass();
#endif
} else {
// curClass method list.
/// 查找(二分查找)方法,如果找到则返回,将方法缓存到cache中
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
imp = meth->imp(false);
goto done;
}
if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
// No implementation found, and method resolver didn't help.
// Use forwarding.
imp = forward_imp;
break;
}
}
// Halt if there is a cycle in the superclass chain.
if (slowpath(--attempts == 0)) {
_objc_fatal("Memory corruption in class list.");
}
// Superclass cache.
/// 父类的缓存
imp = cache_getImp(curClass, sel);
if (slowpath(imp == forward_imp)) {
// Found a forward:: entry in a superclass.
// Stop searching, but don't cache yet; call method
// resolver for this class first.
break;
}
if (fastpath(imp)) {
// Found the method in a superclass. Cache it in this class.
goto done;
}
}
// No implementation found. Try method resolver once.
/// 没有找到方法实现,尝试一次方法解析
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;
}
lookUpImpOrForward方法大体有以下功能
1、缓存检查,若存在直接获取IMP
2、确保cls是初始化过的
3、判断类是否实现,如果没有实现,需要先实现,路径为realizeAndInitializeIfNeeded_locked
->realizeClassMaybeSwiftAndLeaveLocked
->realizeClassMaybeSwiftMaybeRelock
->realizeClassWithoutSwift
(实现类)
4、当前类对象中方法查找、若找到进行缓存
5沿着继承链查找父类的类对象中方法查找,若找到进行缓存
6、没有找到方法实现,尝试一次方法解析
二分查找源码
template<class getNameFunc>
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName)
{
ASSERT(list);
auto first = list->begin();
auto base = first;
decltype(first) probe;
uintptr_t keyValue = (uintptr_t)key;
uint32_t count;
for (count = list->count; count != 0; count >>= 1) {
probe = base + (count >> 1);
uintptr_t probeValue = (uintptr_t)getName(probe);
if (keyValue == probeValue) {
// `probe` is a match.
// Rewind looking for the *first* occurrence of this value.
// This is required for correct category overrides.
while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {
probe--;
}
return &*probe;
}
if (keyValue > probeValue) {
base = probe + 1;
count--;
}
}
return nil;
}
- count >>= 1
- 右移一位相当于减半,例如:1000(8) 右移 0100(4);0111(7) 右移 0011(3)
- probe = base + (count >> 1);
- probe = 初始位置为 + 一半的位置
- 如果目标值小于一半位置的值,则初始位置不变,结尾位置再次右移一位减半
- 如果目标值大于一半位置的值,则初始位置变为一半位置加+1,count减1
总结
1、对于实例方法,即在类中查找,其慢速查找的父类链是:类--父类--根类(NSObjcet)--nil
2、对于类方法,即在元类中查找,其慢速查找的父类链是:元类--根元类--根类(NSObjcet)--nil
3、如果快速查找、慢速查找也没有找到方法实现,则尝试消息动态决议
4、如果消息动态决议仍然没有找到,则进行消息转发
objc_msgSendSuper
1、构造结构体
2、结构体中存在objc_super如下
3、消息响应载体依然是self
OBJC_EXPORT void
objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
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 */
};
类方法和对象方法
Method class_getInstanceMethod(Class cls, SEL sel)
{
if (!cls || !sel) return nil;
// This deliberately avoids +initialize because it historically did so.
// This implementation is a bit weird because it's the only place that
// wants a Method instead of an IMP.
#warning fixme build and search caches
// Search method lists, try method resolver, etc.
lookUpImpOrForward(nil, sel, cls, LOOKUP_RESOLVER);
#warning fixme build and search caches
return _class_getMethod(cls, sel);
}
Method class_getClassMethod(Class cls, SEL sel)
{
if (!cls || !sel) return nil;
return class_getInstanceMethod(cls->getMeta(), sel);
}
通过源码可以看出,类方法本质是获取元类的对象方法,元类的对象方法上层就是通过类的类方法体现。
消息日志打印
static void
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
if (slowpath(objcMsgLogEnabled && implementer)) {
bool cacheIt = logMessageSend(implementer->isMetaClass(),
cls->nameForLogging(),
implementer->nameForLogging(),
sel);
if (!cacheIt) return;
}
#endif
cache_fill(cls, sel, imp, receiver);
}
bool objcMsgLogEnabled = false;
static int objcMsgLogFD = -1;
bool logMessageSend(bool isClassMethod,
const char *objectsClass,
const char *implementingClass,
SEL selector)
{
char buf[ 1024 ];
// Create/open the log file
if (objcMsgLogFD == (-1))
{
snprintf (buf, sizeof(buf), "/tmp/msgSends-%d", (int) getpid ());
objcMsgLogFD = secure_open (buf, O_WRONLY | O_CREAT, geteuid());
if (objcMsgLogFD < 0) {
// no log file - disable logging
objcMsgLogEnabled = false;
objcMsgLogFD = -1;
return true;
}
}
// Make the log entry
snprintf(buf, sizeof(buf), "%c %s %s %s\n",
isClassMethod ? '+' : '-',
objectsClass,
implementingClass,
sel_getName(selector));
objcMsgLogLock.lock();
write (objcMsgLogFD, buf, strlen(buf));
objcMsgLogLock.unlock();
// Tell caller to not cache the method
return false;
}
通过log_and_fill_cache
方法可以看出,在符合条件的情况下会在logMessageSend
方法中进行相关日志本地输出。关键条件就是objcMsgLogEnabled
字段的值。
void instrumentObjcMessageSends(BOOL flag)
{
bool enable = flag;
// Shortcut NOP
if (objcMsgLogEnabled == enable)
return;
// If enabling, flush all method caches so we get some traces
if (enable)
_objc_flush_caches(Nil);
// Sync our log file
if (objcMsgLogFD != -1)
fsync (objcMsgLogFD);
objcMsgLogEnabled = enable;
}
通过上面方法即可设置objcMsgLogEnabled
字段的值。因此可以如下进行消息日志打印
#import <Foundation/Foundation.h>
#import "Sun.h"
extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
instrumentObjcMessageSends(YES);
Sun *objc2 = [Sun alloc];
[objc2 run];
instrumentObjcMessageSends(NO);
}
return 0;
}
消息动态决议流程
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
runtimeLock.assertLocked();
ASSERT(cls->isRealized());
runtimeLock.unlock();
/// 不是元类则调用对象的解析方法
if (! cls->isMetaClass()) {
// try [cls resolveInstanceMethod:sel]
resolveInstanceMethod(inst, sel, cls);
}
/// 元类调用类的解析方法
else {
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
resolveClassMethod(inst, sel, cls);
if (!lookUpImpOrNil(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);
}
}
// chances are that calling the resolver have populated the cache
// so attempt using it
return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}
其流程如下
总结
1、实例方法可以重写resolveInstanceMethod添加imp
2、类方法可以在本类重写resolveClassMethod往元类添加imp
3、消息动态决议只要在任意一步lookUpImpOrNil查找到imp就不会查找下去——即本类做了消息动态决议,不会走到NSObjct分类的动态方法解析
4、所有方法都可以通过在NSObject分类重写resolveInstanceMethod添加imp解决崩溃
所有崩溃都在NSObjct分类中处理是否合理?
1、统一处理起来耦合度高
2、逻辑判断多
3、SDK封装的时候需要给一个容错空间
消息转发流程
快速转发
/// 实例方法
- (id)forwardingTargetForSelector:(SEL)aSelector {
return [[Son alloc]init];
}
/// 类方法
+ (id)forwardingTargetForSelector:(SEL)aSelector {
return Son.class;
}
1、该方法的返回对象是执行sel的新对象,也就是自己处理不了会将消息转发给别的对象进行相关方法的处理,但是不能返回self,否则会一直找不到
2、该方法的效率较高,如果不实现,会走到forwardInvocation:方法进行处理
3、底层会调用objc_msgSend(forwardingTarget, sel, ...),来实现消息的发送
4、被转发消息的接受者参数、返回值等应和原方法相同 \
慢速转发
/// 实例方法
- (void)forwardInvocation:(NSInvocation *)anInvocation {
/// 转发处理
[anInvocation invokeWithTarget:[[Son alloc]init]];
/// 也可以不进行处理,只是错误上报,程序也不会崩溃
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
/// 类方法
+ (void)forwardInvocation:(NSInvocation *)anInvocation {
/// 转发处理
[anInvocation invokeWithTarget:Son.class];
/// 也可以不进行处理,只是错误上报,程序也不会崩溃
}
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
1、forwardInvocation和methodSignatureForSelector必须是同时存在的,底层会通过方法签名,生成一个NSInvocation,将其作为参数传递调用
2、查找可以响应NSInvocation中编码的消息的对象(对于所有消息,此对象不必相同)
3、使用anInvocation将消息发送到该对象,anInvocation将保存结果,运行时系统将提取结果并将其传递给原始发送者
4、无论在forwardInvocation
方法中是否处理invocation
事务,程序都不会崩溃。
5、类方法在NSObject
中找不到,只需要按照👆修改即可。
总结
1、【快速查找流程】首先,在类的缓存cache中查找指定方法的实现
2、【慢速查找流程】如果缓存中没有找到,则在类的方法列表中查找,如果还是没找到,则去父类链的缓存和方法列表中查找
3、【动态方法决议】如果慢速查找还是没有找到时,第一次补救机会就是尝试一次动态方法决议,即重写resolveInstanceMethod
/resolveClassMethod
方法
4、【消息转发】如果动态方法决议还是没有找到,则进行消息转发,消息转发中有两次补救机会:快速转发+慢速转发
5、如果转发之后也没有,则程序直接报错崩溃unrecognized selector sent to instance
问题先行解答
1、OC中调用方法的本质是?
objc_msgSend消息发送
2、OC中调用方法的流程是如何?
详细流程如上分析。
3、方法查找流程中方法的比对,会判断是类方法还是实例方法吗?
不会,见如下源码,仅仅是比对method_name和sel是否相等
/* These next three functions are the heart of ObjC method lookup.
* If the class is currently in use, methodListLock must be held by the caller.
*/
static inline old_method *_findMethodInList(old_method_list * mlist, SEL sel) {
int i;
if (!mlist) return nil;
for (i = 0; i < mlist->method_count; i++) {
old_method *m = &mlist->method_list[i];
if (m->method_name == sel) {
return m;
}
}
return nil;
}
4、如下代码执行会crash吗?
不会,对于类方法,即在元类中查找,其慢速查找的父类链是:元类 -> 元类的父类 -> 。。。 -> 根元类 -> NSObject -> nil
5、可以用C语言实现一个二个查找吗?
static int balance[9] = {1,2,4,6,8,11,34,67,88};
static int len = sizeof(balance) / sizeof(int);
int search(int keyValue) {
int base = 0;
int probe;
int count;
for (count = len; count != 0; count >>= 1) {
probe = base + (count >>= 1);
int probeVlaue = balance[probe];
if (keyValue == probeVlaue) {
return probe;
}
if (keyValue > probeVlaue) {
base = probe + 1;
count --;
}
}
return 0;
}
6、resolveInstanceMethod
、resolveClassMethod
为什么会在相应方法没有实现的时候调用两次?
待解决