程序的加载原理
代码的编译过程
我们编写完成代码是需要通过编译器来完成编译后,才能变成可以执行的文件,也就是我们通常说的可执行文件。
那么编译过程是怎样的呢,下面我们来通过流程图为大家分析一下:
程序的执行,就是把可执行的文件加载到内存中进行执行。我们将可执行文件叫做Mach-O,Mach-O的运行需要依赖运行库(.a .lib .so),运行库分为动态库和静态库,这些库是可执行的二进制文件,能够被加载倒内存中。
静态库和动态库
静态库:例如.a和.framework。静态库链接时,会被完整地复制到可执行文件中,被使用到了多次时,就会被复制到内存中多次,这样就会产生拷贝冗余,造成内存浪费。动态库:例如.lib和.framework。动态库链接时,只会存在一份,不会复制多份。动态库在内存中是共享的,系统只加载一次,加载完成后谁用就会去引用这块内存,这样就既节省了时间又省了内存。- 静态库就是我们常说的值拷贝,而动态库就是我们常说的地址拷贝。
- 图解
- 动态链接库加载倒内存的过程就是由
dyld(dynamic link editor)动态链接器完成的。
dyld动态连接器
dyld的介绍:dyld是iOS操作系统的一个重要组成部分,再系统内核做好程序准备工作之后,会交由dyld负责余下的工作。dyld的作用:加载各个库,也就是image镜像文件,由dyld从内存中读到列表中,加载主程序,link链接各个动静态库,进行主程序的初始化工作。- dyld工作流程图如下,图中简单描述了
动态库的注册和动态库的加载过程,具体的分析还需要看底层的源码,此处暂时不做深入探究。
_objc_init与dyld
_objc_init源码
首先我们再objc源码中全局搜索_objc_init,在objc_os.mm文件下可以看到实现源码如下:
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;
// fixme defer initialization until an objc-using image is found?
// 读取影响运行时的环境变量
// 可以再Edite Scheme中的Environment Variable中进行配置,来打印环境变量帮助
environ_init();
// 关于线程key的绑定
tls_init();
// 运行C++静态构造函数 在dyld调用我们的静态构造函数之前,libc会调用objc_init,因此我们必须自己做
static_init();
// runtime运行时环境变量初始化
runtime_init();
// 初始化libobjc的异常处理方法
exception_init();
#if __OBJC2__
// 缓存条件初始化
cache_t::init();
#endif
// 启动回调机制
_imp_implementationWithBlock_init();
// 注册dyld
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
#if __OBJC2__
didCallDyldNotifyRegister = true;
#endif
}
根据源码所知,主要分为一下几部分:
environ_init:初始化一系列的环境变量,病读取影响运行的环境变量tls_init:关于线程key的绑定static_init:运行C++静态构造函数(只会运行系统级别的构造函数),在dyld调用静态析构函数之前,libc会调用_objc_initruntime_init:runtime运行时环境初始化,里面操作是unattachedCategories、allocatedClasses表的初始化exception_init:初始化libobjc的异常处理系统cache_t::init:cache缓存初始化_imp_implementationWithBlock_init:启动回调机制,通常这不会做什么,因为所有的初始化都是惰性的,但是对于某些进程,我们会迫不及待地加载trampolines dylib_dyld_objc_notify_register:dyld的注册-
- 仅供objc运行时使用。注册应用程序,以便再映射、取消映射和初始化objc镜像文件时使用,dyld将使用包含objc_image_info的镜像文件数组,回调mapped函数。
map_images:dyld将image镜像文件加载进内存时,会触发该函数
-
load_image:dyld初始化image会触发该函数unmap_image:dyld将image移除时会触发该函数
environ_init方法:环境变量初始化
environ_init源码如下,我截取了比较关键的部分,此处主要循环打印环境变量信息
void environ_init(void)
{
// ……省略
// Print OBJC_HELP and OBJC_PRINT_OPTIONS output.
if (PrintHelp || PrintOptions) {
// …… 省略
// 核心代码
for (size_t i = 0; i < sizeof(Settings)/sizeof(Settings[0]); i++) {
const option_t *opt = &Settings[i];
if (PrintHelp) _objc_inform("%s: %s", opt->env, opt->help);
if (PrintOptions && *opt->var) _objc_inform("%s is set", opt->env);
}
}
}
环境变量打印
方式一:修改源码的方式进行打印
- 首先我们移除掉内层和外层的if条件,只保留上述代码块;
- 运行我们的objc源码项目;
- 查看控制台打印结果,会打印出所有的环境变量信息
方式二:通过终端命令打印环境变量
需要再终端中输入export OBJC_HELP=1指令,打印环境变量
方式三:打印指定的环境变量信息
设置方式:
可以通过target -- Edit Scheme -- Run --Arguments -- Environment Variables配置,其中常用的环境变量主要有以下几个:
DYLD_PRINT_STATISTICS:设置DYLD_PRINT_STATISTICS为YES,控制台就会打印APP的加载时长,包括整体加载时长和动态库加载时长,即main函数之前dyld的处理时间,可以通过设置了解耗时部分,并对其进行启动优化。OBJC_DISABLE_NONPOINTER_ISA:杜绝生成相应的nonpointer isa制(nonpointer isa 优化后的isa,指针地址未优化的isa末尾为1,优化后的isa末尾为0),生成的都是普通的isaOBJC_PRINT_LOAD_METHODS:打印Class即Category的load方法的调用信息NSDoubleLocalizedStrings:项目国际化本地化的时候时一个耗时的工作,想要检测国际化翻译好的文字UI会变成什么样子,可以指定这个启动项,可以设置NSDoubleLocalizedStrings为YES。NSShowNonLocalizedStrings:在完成国际化的时候,偶尔会有一些字符串没有做本地化,这时就可以设置NSShowNonLocalizedStrings为YES,所有没有本地化的字符串全都会变成大写。
OBJC_DISABLE_NONPOINTER_ISA
以OBJC_DISABLE_NONPOINTER_ISA为例,将其设置为YES
未设置OBJC_DISABLE_NONPOINTER_ISA时打印objc对象,isa地址的二进制末尾为1
设置OBJC_DISABLE_NONPOINTER_ISA时打印机objc对象,isa地址的二进制末尾为0
所以OBJC_DISABLE_NONPOINTER_ISA可以控制isa开关,从而优化整个内存结构
tls_init:线程key的绑定
主要是本地线程池的初始化以及析构,源码:
void tls_init(void)
{
#if SUPPORT_DIRECT_THREAD_KEYS
pthread_key_init_np(TLS_DIRECT_KEY, &_objc_pthread_destroyspecific);
#else
_objc_pthread_key = tls_create(&_objc_pthread_destroyspecific);
#endif
}
static_init:运行系统级别的C++静态构造函数
主要是运行系统级别的C++静态构造函数,在dyld调用我们的静态构造函数之前,libc调用_objc_init方法,即系统级别的C++构造函数,自定义的C++构造函数运行,源码:
static void static_init()
{
size_t count;
auto inits = getLibobjcInitializers(&_mh_dylib_header, &count);
for (size_t i = 0; i < count; i++) {
inits[i]();
}
auto offsets = getLibobjcInitializerOffsets(&_mh_dylib_header, &count);
for (size_t i = 0; i < count; i++) {
UnsignedInitializer init(offsets[i]);
init();
}
}
runtime_init:运行时环境初始化
主要是运行时的初始化,主要分为两部分:分类初始化、类的表初始化,源码:
void runtime_init(void)
{
objc::unattachedCategories.init(32);
objc::allocatedClasses.init();
}
exception_init:初始化libobjc的异常处理系统
主要是初始化libobjc的异常处理系统,注册异常处理的回调,从而监控异常的处理,源码:
void exception_init(void)
{
old_terminate = std::set_terminate(&_objc_terminate);
}
- 当有
crash发生时,就会到old_terminate方法,最终走到uncaught_handler进行异常抛出 -
crash:是指系统发生了一下不被允许的指令,然后系统会发出的一个信号
old_terminate源码
static void (*old_terminate)(void) = nil;
static void _objc_terminate(void)
{
if (PrintExceptions) {
_objc_inform("EXCEPTIONS: terminating");
}
if (! __cxa_current_exception_type()) {
// No current exception.
(*old_terminate)();
}
else {
// There is a current exception. Check if it's an objc exception.
@try {
__cxa_rethrow();
} @catch (id e) {
// It's an objc object. Call Foundation's handler, if any.
(*uncaught_handler)((id)e);
(*old_terminate)();
} @catch (...) {
// It's not an objc object. Continue to C++ terminate.
(*old_terminate)();
}
}
}
- 搜索
uncaught_handler,我们发现是一个全局静态变量,是在应用层通过objc_setUncaughtExceptionHandler传入一个异常处理的回调方法,并在该方法内将异常处理方法赋值给uncaught_handler。
// uncaught_handler初始
static objc_uncaught_exception_handler uncaught_handler = _objc_default_uncaught_exception_handler;
// APP传入处理异常用的回调方法
objc_setUncaughtExceptionHandler(objc_uncaught_exception_handler fn)
{
objc_uncaught_exception_handler result = uncaught_handler;
uncaught_handler = fn;
return result;
}
cache_init:缓存初始化
主要是缓存初始化,源码:
void cache_t::init()
{
#if HAVE_TASK_RESTARTABLE_RANGES
mach_msg_type_number_t count = 0;
kern_return_t kr;
while (objc_restartableRanges[count].location) {
count++;
}
kr = task_restartable_ranges_register(mach_task_self(),
objc_restartableRanges, count);
if (kr == KERN_SUCCESS) return;
_objc_fatal("task_restartable_ranges_register failed (result 0x%x: %s)",
kr, mach_error_string(kr));
#endif // HAVE_TASK_RESTARTABLE_RANGES
}
_imp_implementationWithBlock_init:启动回调机制
该方法主要是启动回调机制,通常这不会做什么,因为所有的初始化都是惰性的,但是对于某些进程,我们会迫不及待地加载libobjc-trampolines.dylib,源码
_imp_implementationWithBlock_init(void)
{
#if TARGET_OS_OSX
// Eagerly load libobjc-trampolines.dylib in certain processes. Some
// programs (most notably QtWebEngineProcess used by older versions of
// embedded Chromium) enable a highly restrictive sandbox profile which
// blocks access to that dylib. If anything calls
// imp_implementationWithBlock (as AppKit has started doing) then we'll
// crash trying to load it. Loading it here sets it up before the sandbox
// profile is enabled and blocks it.
//
// This fixes EA Origin (rdar://problem/50813789)
// and Steam (rdar://problem/55286131)
if (__progname &&
(strcmp(__progname, "QtWebEngineProcess") == 0 ||
strcmp(__progname, "Steam Helper") == 0)) {
Trampolines.Initialize();
}
#endif
}
_dyld_objc_notify_register:dyld注册
_dyld_objc_notify_register方法
这个方法是dyld的注册方法,其源码是在dyld源码中,下面是_dyld_objc_notify_register的声明
//
// Note: only for use by objc runtime
// Register handlers to be called when objc images are mapped, unmapped, and initialized.
// Dyld will call back the "mapped" function with an array of images that contain an objc-image-info section.
// Those images that are dylibs will have the ref-counts automatically bumped, so objc will no longer need to
// call dlopen() on them to keep them from being unloaded. During the call to _dyld_objc_notify_register(),
// dyld will call the "mapped" function with already loaded objc images. During any later dlopen() call,
// dyld will also call the "mapped" function. Dyld will call the "init" function when dyld would be called
// initializers in that image. This is when objc calls any +load methods in that image.
//
void _dyld_objc_notify_register(_dyld_objc_notify_mapped mapped,
_dyld_objc_notify_init init,
_dyld_objc_notify_unmapped unmapped);
通过注释可以分析出:
- 仅运行
objc运行时使用 注册处理程序,以便在镜像映射和取消映射和初始化objc镜像是调用dyld将会通过一个包含objc-image-info的镜像文件的数组回调mapped函数
_dyld_objc_notify_register调用
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
方法中的三个参数含义:
&map_images:dyld将image镜像文件加载倒内存中,会触发该函数load_image:dyld初始化image会触发该函数unmap_image:dyld将image移除时,会触发该函数
load_image方法其实就是调用load方法,map_image方法,&map_image是指针传递,指向是同一块实现的地址,如果有什么变化就可以第一时间知道。
接下来我们就研究一下map_images
map_images
map_images源码
首先,我们先来看一下map_images的源码
void
map_images(unsigned count, const char * const paths[],
const struct mach_header * const mhdrs[])
{
mutex_locker_t lock(runtimeLock);
return map_images_nolock(count, paths, mhdrs);
}
我们可以看到map_images方法内调用了map_images_nolock方法,下面我们看看map_images_nolock源码
map_images_nolock源码
map_images_nolock(unsigned mhCount, const char * const mhPaths[],
const struct mach_header * const mhdrs[])
{
static bool firstTime = YES;
header_info *hList[mhCount];
uint32_t hCount;
size_t selrefCount = 0;
// …………省略
if (hCount > 0) {
_read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
}
firstTime = NO;
// Call image load funcs after everything is set up.
for (auto func : loadImageFuncs) {
for (uint32_t i = 0; i < mhCount; i++) {
func(mhdrs[i]);
}
}
}
map_images_nolock中的关键就在read_image方法,加下来我们就来完整的看一下read_image具体做了什么
read_image
read_image源码
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
header_info *hi;
uint32_t hIndex;
size_t count;
size_t i;
Class *resolvedFutureClasses = nil;
size_t resolvedFutureClassCount = 0;
static bool doneOnce;
bool launchTime = NO;
TimeLogger ts(PrintImageTimes);
runtimeLock.assertLocked();
hIndex = 0; \
hIndex < hCount && (hi = hList[hIndex]); \
hIndex++
// 条件控制进行一次加载
if (!doneOnce) {……}
// Fix up @selector references
// 修复预编译阶段的@selector的混乱问题
// 就是不同类中有相同方法 但是相同方法的地址是不一样的
static size_t UnfixedSelectors;
{……}
ts.log("IMAGE TIMES: fix up selector references");
// Discover classes. Fix up unresolved future classes. Mark bundle classes.
// 错误混乱的类处理
bool hasDyldRoots = dyld_shared_cache_some_image_overridden();
for (EACH_HEADER) {……}
ts.log("IMAGE TIMES: discover classes");
// Fix up remapped classes
// Class list and nonlazy class list remain unremapped.
// Class refs and super refs are remapped for message dispatching.
// 修复重映射一些没有被镜像文件加载进来的类
if (!noClassesRemapped()) {……}
ts.log("IMAGE TIMES: remap classes");
#if SUPPORT_FIXUP
// Fix up old objc_msgSend_fixup call sites
// 修复一些消息
for (EACH_HEADER) {……}
ts.log("IMAGE TIMES: fix up objc_msgSend_fixup");
#endif
// Discover protocols. Fix up protocol refs.
// 当类中有协议时 readProtocol
for (EACH_HEADER) {……}
ts.log("IMAGE TIMES: discover protocols");
// Fix up @protocol references
// Preoptimized images may have the right
// answer already but we don't know for sure.
// 修复没有被加载的协议
for (EACH_HEADER) {……}
ts.log("IMAGE TIMES: fix up @protocol references");
// Discover categories. Only do this after the initial category
// attachment has been done. For categories present at startup,
// discovery is deferred until the first load_images call after
// the call to _dyld_objc_notify_register completes. rdar://problem/53119145
// 分类的处理
if (didInitialAttachCategories) {
for (EACH_HEADER) {
load_categories_nolock(hi);
}
}
ts.log("IMAGE TIMES: discover categories");
// Category discovery MUST BE Late to avoid potential races
// when other threads call the new category code before
// this thread finishes its fixups.
// +load handled by prepare_load_methods()
// Realize non-lazy classes (for +load methods and static instances)
// 类的加载处理
for (EACH_HEADER) {……}
ts.log("IMAGE TIMES: realize non-lazy classes");
// Realize newly-resolved future classes, in case CF manipulates them
// 没有被处理的类 优化那些被侵犯的类
if (resolvedFutureClasses) {……}
ts.log("IMAGE TIMES: realize future classes");
#undef EACH_HEADER
}
从整体上可以看出read_image是对一些log日志的打印输出,主要如下:
- 条件控制运行一次加载
- 修复预编译阶段的
@selector的混乱的问题 - 错误混乱的类处理
- 修复重映射一些没有被镜像文件加载进来的类
- 修复一些消息
- 当类中有协议时
readProtocol - 修复没有被加载的协议
- 分类的处理
- 类的加载处理
- 没有被处理的类,优化那些被侵犯的类
下面我来重点分析几个比较重要的逻辑:
doneOnce
源码
if (!doneOnce) {
doneOnce = YES;
launchTime = YES;
// ……省略
// namedClasses
// Preoptimized classes don't go in this table.
// 4/3 is NXMapTable's load factor
int namedClassesSize =
(isPreoptimized() ? unoptimizedTotalClasses : totalClasses) * 4 / 3;
// 创建哈希表 存放所有类
gdb_objc_realized_classes =
NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);
ts.log("IMAGE TIMES: first time tasks");
}
加载一次下次就不会再次进入判断。只有第一次进来时创建表gdb_objc_realized_classes,表里存放所有的类(不管是实现的还是未实现的都存放在里面),是一张存放类的总表
UnfixedSelectors
源码
// Fix up @selector references
static size_t UnfixedSelectors;
{
mutex_locker_t lock(selLock);
for (EACH_HEADER) {
if (hi->hasPreoptimizedSelectors()) continue;
bool isBundle = hi->isBundle();
// 从Mach-O中获取方法列表
SEL *sels = _getObjc2SelectorRefs(hi, &count);
UnfixedSelectors += count;
for (i = 0; i < count; i++) {
const char *name = sel_cname(sels[i]);
// 从dyld中获取方法
SEL sel = sel_registerNameNoLock(name, isBundle);
if (sels[i] != sel) {
sels[i] = sel;
}
}
}
}
不同类中可能存在相同的方法,但是相同的方法地址是不同的,dyld的方法是准确的,所以此处需要对Mach-O的方法进行纠正。
下面我们通过debug断电来打印一下。
sels是通过_getObjc2SelectorRefs获取Mach-O中的方法信息,Mach-O有相对位移地址偏移地址。
sel是通过sel_registerNameNoLock获取的dyld的方法信息,dyld是链接整个程序的,所以dyld是最准确的。因为方法是存放在类中,没给类中的位置是不一样的,所以方法的地址也就不一样,那么久必须对那些混乱的方法进行修复处理。
总结:
Mach-O中存储的是对当前类的相对地址,dyld是加载过程中内存中的实际地址,所以需要将dyld的imp为准进行纠正。
错误混乱的类处理
源码
for (EACH_HEADER) {
if (! mustReadClasses(hi, hasDyldRoots)) {
// Image is sufficiently optimized that we need not call readClass()
continue;
}
// 从macho中读取类列表信息
classref_t const *classlist = _getObjc2ClassList(hi, &count);
bool headerIsBundle = hi->isBundle();
bool headerIsPreoptimized = hi->hasPreoptimizedClasses();
for (i = 0; i < count; i++) {
Class cls = (Class)classlist[i];
Class newCls = readClass(cls, headerIsBundle, headerIsPreoptimized);
// 类信息发生错乱,类运行时可能发生移动,但是没有被删除,就是我们常说的野指针
if (newCls != cls && newCls) {
// Class was moved but not deleted. Currently this occurs
// only when the new class resolved a future class.
// Non-lazily realize the class below.
resolvedFutureClasses = (Class *)
realloc(resolvedFutureClasses,
(resolvedFutureClassCount+1) * sizeof(Class));
resolvedFutureClasses[resolvedFutureClassCount++] = newCls;
}
}
}
cls:是我们从macho中读取出来的,指向的是一块内存地址。newCls:是我们通过readClass获取的,但是newCls有特殊性,在我们未调用readClass为其赋值时,系统会给newClas分配一块脏地址,一旦调用了readClass后我们会对其重新赋值,并且会为他设置名称,我们我对象的名称也就是在这里进行设置的。- 下面我们通过代码来调试一下
通过上述调试,也证实了readClass的作用就是把类名和地址关联起来。
readClass
源码
Class readClass(Class cls, bool headerIsBundle, bool headerIsPreoptimized)
{
// 获取类名
const char *mangledName = cls->nonlazyMangledName();
if (missingWeakSuperclass(cls)) {……}
cls->fixupBackwardDeployingStableSwift();
Class replacing = nil;
if (mangledName != nullptr) {……}
if (headerIsPreoptimized && !replacing) {……
} else {
if (mangledName) { //some Swift generic classes can lazily generate their names
// 将类名和地址关联起来
addNamedClass(cls, mangledName, replacing);
} else {
Class meta = cls->ISA();
const class_ro_t *metaRO = meta->bits.safe_ro();
ASSERT(metaRO->getNonMetaclass() && "Metaclass with lazy name must have a pointer to the corresponding nonmetaclass.");
ASSERT(metaRO->getNonMetaclass() == cls && "Metaclass nonmetaclass pointer must equal the original class.");
}
// 将关联好的类插入到另一张哈希表中 初始化完成的类列表
addClassTableEntry(cls);
}
// for future reference: shared cache never contains MH_BUNDLEs
if (headerIsBundle) {……}
return cls;
}
关键方法:
nonlazyMangledName获取类名addNamedClass将类名和地址关联起来addClassTableEntry将关联好的类插入到另一张哈希表中,这张表中都是已经初始化完成的类
下面我们通过对cls进行过滤,研究我们自己创建的Person类
此过程通过nonlazyMangledName获取到了类的名称
nonlazyMangledName源码:
const char *nonlazyMangledName() const {
return bits.safe_ro()->getName();
}
safe_ro源码:
const class_ro_t *safe_ro() const {
class_rw_t *maybe_rw = data();
if (maybe_rw->flags & RW_REALIZED) {
// maybe_rw is rw
return maybe_rw->ro();
} else {
// maybe_rw is actually ro
return (class_ro_t *)maybe_rw;
}
}
addNamedClass将类名和地址关联
static void addNamedClass(Class cls, const char *name, Class replacing = nil)
{
runtimeLock.assertLocked();
Class old;
if ((old = getClassExceptSomeSwift(name)) && old != replacing) {
inform_duplicate(name, old, cls);
// getMaybeUnrealizedNonMetaClass uses name lookups.
// Classes not found by name lookup must be in the
// secondary meta->nonmeta table.
addNonMetaClass(cls);
} else {
// 更新存储所有类的哈希表 key是name value是cls
NXMapInsert(gdb_objc_realized_classes, name, cls);
}
ASSERT(!(cls->data()->flags & RO_META));
// wrong: constructed classes are already realized when they get here
// ASSERT(!cls->isRealized());
}
通过NXMapInsert方法更新gdb_objc_realized_classes哈希表,key是name,value是cls
NXMapInsert源码:
void *NXMapInsert(NXMapTable *table, const void *key, const void *value) {
MapPair *pairs = (MapPair *)table->buckets;
unsigned index = bucketOf(table, key);
MapPair *pair = pairs + index;
if (key == NX_MAPNOTAKEY) {
_objc_inform("*** NXMapInsert: invalid key: -1\n");
return NULL;
}
unsigned numBuckets = table->nbBucketsMinusOne + 1;
if (pair->key == NX_MAPNOTAKEY) {
pair->key = key; pair->value = value;
table->count++;
if (table->count * 4 > numBuckets * 3) _NXMapRehash(table);
return NULL;
}
// …………省略
}
addClassTableEntry源码
static void
addClassTableEntry(Class cls, bool addMeta = true)
{
runtimeLock.assertLocked();
// This class is allowed to be a known class via the shared cache or via
// data segments, but it is not allowed to be in the dynamic table already.
// allocatedClasses就是_objc_alloc runtime_init中初始化的
auto &set = objc::allocatedClasses.get();
ASSERT(set.find(cls) == set.end());
if (!isKnownClass(cls))
set.insert(cls);
if (addMeta)
addClassTableEntry(cls->ISA(), false);
}
allocatedClasses在_objc_init中进行初始化,主要是unattachedCategories(分类)和allocatedClasses(类)两张表,此时addClassTableEntry的操作是插入到allocatedClasses表中。同时还对元类进行了相应的处理。
通过对源码的分析和断电调试,我们发现rw和ro的获取和赋值并不是在readClass里面,那么重新回到_read_image继续向下看。
最终我们断电继续向下走,走到了类加载的区域发现,调用了realizeClassWithoutSwift方法,先透露一下,rw和ro的查找和赋值就是在这个方法中完成的,这个方法我们下节进行探讨