欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

iOS底层探索(十一)类的加载

程序员文章站 2024-03-24 14:13:04
...

iOS底层探索(十一)类的加载

通过上一篇文章iOS底层探索(十) 应用程序加载我们可以了解到应用程序加载的过程,那么类的加载是在什么时候进行的呢?

通过上一篇文章我们了解到dyld加载镜像文件时是通过(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());方法获取镜像文件的真实地址的。

那么sNotifyObjCInit指针地址又是什么时候传进来的呢?
通过搜索发现sNotifyObjCInit是在registerObjCNotifiers()方法中进行赋值的,而registerObjCNotifiers()方法又是在_dyld_objc_notify_register()方法中调用,通过传进来的init地址,回调函数,最终加载镜像文件
iOS底层探索(十一)类的加载
那么_dyld_objc_notify_register方法是在什么时候调用,给aNotifyObjCInit赋值的呢?

_objc_init 初始化方法

查看objc源码可知可查找到_objc_init方法,为类加载的入口。

/**
* _objc_init
* 引导程序初始化,用dyld注册我们的镜像文件通知程序
* 库初始化之前由libSystem调用
*/
void _objc_init(void) {
    // 判断是否已进行初始化
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    
    // fixme推迟初始化,直到找到objc使用的镜像文件
    
    // 读取影响运行时的环境变量。如果需要,还可以打印环境变量
    environ_init();
    // 关于线程key的绑定-比如线程数据的析构函数
    tls_init();
    // 运行C++静态构造函数。在dyld调用我们的静态构造函数之前,libc调用_objc_init()函数。
    static_init();
    // runtime运行时环境初始化,里面主要是unattachedCategories,allocatedClasses后面会分析
    runtime_init();
    // 初始化libobjc的异常处理系统,由map_images()调用
    exception_init();
    // 缓存条件初始化
    cache_init();
    // 启动回调机制。通常这不会做什么,因为所有的初始化都是惰性的,但是对于某些进程,我们会迫不及待的加载trampolines dylib
    _imp_implementationWithBlock_init();
    /// 注意:仅供objc运行时使用
    /// 当映射,取消映射和初始化objc镜像时将调用注册处理程序
    /// dyld将使用包含objc-image-info的镜像数组调用“映射”函数
    /// 那些是dilib的镜像将自动增加引用计数,因此objc不再需要在它们上调用dlopen()来防止它们被卸载。
    /// 在调用_dyld_objc_notify_register()期间,dyld还将调用"映射"函数。
    /// 在以后的任何dlopen()调用期间,dyld还将调用“映射”函数。
    /// 当dyld初始化镜像文件时,dyld将调用“init”函数,这是objc在镜像文件中调用任何 +load方法的时候
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);

#if __OBJC2__
    didCallDyldNotifyRegister = true;
#endif
}

由上面代码可知_objc_init中的工作流程

  • 判断是否进行过初始化
  • environ_init 初始化环境
  • tls_init
  • static_init C++静态构造函数
  • runtime_init runtime初始化
  • exception_init 异常系统初始化
  • cache_init 缓存系统初始化
  • _imp_implementationWithBlock_init
  • _dyld_objc_notify_register 镜像映射初始化
    接下来对方法进行一个一个的分析

environ_init

环境初始化,我们先来看一下简化后的代码结构

void emviron_init(void) {
    if (issetugid()) {...}
    
    bool PrintHelp = false;
    bool PrintOptions = false;
    bool maybeMallocDebugging = false;
    
    // Scan environ[] directly instead of calling getenv() a lot.
    // This optimizes the case where none are set.
    for (char **p = *_NSGetEnviron(); *p != nil; p++) {...}
    
    // Special case: enable some autorelease pool debugging 
    // when some malloc debugging is enabled 
    // and OBJC_DEBUG_POOL_ALLOCATION is not set to something other than NO.
    if (maybeMallocDebugging) {...}

    // Print OBJC_HELP and OBJC_PRINT_OPTIONS output.
    if (PrintHelp  ||  PrintOptions) {...}
}

然后我们利用断点进行查看该方法中有哪些代码块是执行了的,哪些没有执行,没有执行的代码可以忽略不看,只看执行的代码
运行后通过断点可知执行了for (char **p = *_NSGetEnviron(); *p != nil; p++)中的代码。

  • for (char **p = *_NSGetEnviron(); *p != nil; p++) 通过注释可知该代码在进行environ进行扫码,这样可以优化未设置的情况,源码如下
for (char **p = *_NSGetEnviron(); *p != nil; p++) {
        if (0 == strncmp(*p, "Malloc", 6)  ||  0 == strncmp(*p, "DYLD", 4)  ||  
            0 == strncmp(*p, "NSZombiesEnabled", 16))
        {
            maybeMallocDebugging = true;
        }

        if (0 != strncmp(*p, "OBJC_", 5)) continue;
        
        if (0 == strncmp(*p, "OBJC_HELP=", 10)) {
            PrintHelp = true;
            continue;
        }
        if (0 == strncmp(*p, "OBJC_PRINT_OPTIONS=", 19)) {
            PrintOptions = true;
            continue;
        }
        
        const char *value = strchr(*p, '=');
        if (!*value) continue;
        value++;
        
        for (size_t i = 0; i < sizeof(Settings)/sizeof(Settings[0]); i++) {
            const option_t *opt = &Settings[i];
            if ((size_t)(value - *p) == 1+opt->envlen  &&  
                0 == strncmp(*p, opt->env, opt->envlen))
            {
            // 当设置环境变量并且设置的值为"YES"时执行
                *opt->var = (0 == strcmp(value, "YES"));
                break;
            }
        }            
    }
  • 强行打印环境变量,将if (PrintHelp || PrintOptions)内的打印方法进行强制打印,即将打印的代码移到必然执行的位置,代码如下
for (size_t i = 0; i < sizeof(Settings)/sizeof(Settings[0]); i++) {
    const option_t *opt = &Settings[i];            
    _objc_inform("%s: %s", opt->env, opt->help);
    _objc_inform("%s is set", opt->env);
}

运行,查看打印结果,输出的结果比较多,我们只截取一部分进行查看。
iOS底层探索(十一)类的加载
我鼠标选中的变量为优化后的isa,我们知道在isa中存在nonpointer字段,iOS底层探索(三) isa详解可详细查看,我们分别打印配置该环境变量与不配置环境变量,对象的区别。

  • 我们先进行环境变量的配置,在环境变量Environment Varibales中增加环境变量OBJC_DISABLE_NONPOINTER_ISA值为YES,在main函数中创建对象,并使用断点拦截。具体如下图
    iOS底层探索(十一)类的加载
    iOS底层探索(十一)类的加载
    重新运行后打印isa
    iOS底层探索(十一)类的加载
    由打印结果可知,当前isa的最后一位为0,即未做优化的isa
  • 将该环境变量删除,再重新运行并打印isa
    iOS底层探索(十一)类的加载
    由打印结果可知,当前isa的最后一位为1,即已做优化的isa
    总结:
  • environ_init函数用于读取运行时的环境变量,如果需要,还可以打印环境变量帮助

tls_init

关于线程key的绑定,比如线程数据的析构函数
暂不展开分析,有兴趣的自己去看看

static_init

运行C++静态构造函数。在dyld调用我们的静态构造函数之前,libc会调用_objc_init(),因此我们必须自己做
暂不展开分析,有兴趣的自己去看看

runtime_init

runtime运行时环境初始化,里面主要是unattachedCategories, allocatedClasses 后面会分析
暂不展开分析,有兴趣的自己去看看

exception_init

初始化libobjc的异常处理系统,方法源码如下:

void exception_init(void)
{
    old_terminate = std::set_terminate(&_objc_terminate);
}

说到异常就能引发出我们常遇到的问题crash,相信大家都遇到过相同的问题,比如数组越界、类型不匹配、方法不存在等等一些闪退问题。但是crash是什么呢?

异常捕获

在iOS中引发崩溃的代码本质上就两类

  • C/C++语言层面的错误。属于比较底层的错误,比如野指针、除零、内存访问异常等等,
  • 系统未捕获异常(Uncaught Exception),iOS下面最常见的就是OC的NSException,比如数组越界等等。
    因此我们可以了解到iOS系统的大致执行,为上层iOS代码以及底层C/C++代码。如下图所示。
    iOS底层探索(十一)类的加载
  • 在iOS系统中,上层代码与底层代码几乎同时运行。并且上层代码会编译底层代码进行运行
  • 当底层代码执行时发生Excaption异常时,底层代码会通过信号机制进行一个错误信号的抛出(signal或者是sigaction),并且阻断底层代码的运行,即crash.如果我们通过设定一个回调函数,当抛出异常时可以让我们自己去处理该错误。
    这个过程在objc源码中可以查看到。即_objc_terminate方法
/*
* 未捕获的异常回调实现
* 1. 检查是否存在活动异常
* 2. 如果是,检查是否是Objective-C异常
* 3. 如果是,请使用该对象调用我们注册的回调函数
* 4. 最后,调用设置的回调函数
**/
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) {
            // 这是一个objc对象,如果设置了,则进行调用
            (*uncaught_handler)((id)e);
            (*old_terminate)();
        } @catch (...) {
            // It's not an objc object. Continue to C++ terminate.
            (*old_terminate)();
        }
    }
}

由上面的代码可知,使用uncaught_handler方法进行异常信号进行回调。,查看uncaught_handler方法,并搜索相关信息。、

// 给uncaught_handler设置默认方法
static objc_uncaught_exception_handler uncaught_handler = _objc_default_uncaught_exception_handler;

// 提供设置uncaught_handler方法的入口
objc_uncaught_exception_handler 
objc_setUncaughtExceptionHandler(objc_uncaught_exception_handler fn)
{
    objc_uncaught_exception_handler result = uncaught_handler;
    uncaught_handler = fn;
    return result;
}

未捕获异常处理

  • 创建未捕获异常的类并创建捕获方法,代码如下,其中NSSetUncaughtExceptionHandler等于objc_setUncaughtExceptionHandler方法
// 创建捕获异常的类方法,并注册方法
+ (void)installUncaughtSignalExceptionHandler{
    // objc_setUncaughtExceptionHandler(objc_uncaught_exception_handler fn)
NSSetUncaughtExceptionHandler(&LGExceptionHandlers);
}

/// Exception,异常处理方法
void LGExceptionHandlers(NSException *exception) {
    NSLog(@"%s",__func__);
}
  • AppDelegate中的didFinishLaunchingWithOptions方法中进行方法注册
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    [LGUncaughtExceptionHandle installUncaughtSignalExceptionHandler];
    return YES;
}
  • ViewController中手动做一个数组越界的异常。
- (IBAction)exceptionAction:(id)sender {
    self.dataArray = @[];
    NSLog(@"%@",self.dataArray[100]);
}
  • 运行代码后执行,我们可以看到,数组越界的异常出现在我们设置的方法LGExceptionHandlers里了。
    iOS底层探索(十一)类的加载
    这么做有什么好处呢?
    设置了该方法后,所有的未捕获异常都会执行到该方法中,我们可以利用这个机制进行线上应用的异常收集,方便我们的crash的监测、修改

常见的Exception Type & Exception Code

  • Exception Type
    • EXC_BAD_ACCESS,此类型是我们最常碰到的crash,通常用于访问了不可访问的内存导致。一般EXC_BAD_ACCESS后面的()内还会带有补充信息。
      • SIGSEGV:通常由于重复释放对象导致,这种类型在切换了ARC模式后已经很少见到了。
      • SIGABRT:收到Abort信号退出,通常Foundation库中的容器为了保护状态正常会做一些检测,例如插入nil到数组中等会遇到此类错误
      • SEGV:(Segmentation Violation),代表无效内存地址,比如空指针、未初始化指针、栈溢出等。
      • SIGBUS:总线错误,与SIGSEGV不同的是,SIGSEGV访问的是无效地址,而SIGBUS访问的是有效地址,但总线访问异常,如地址对齐问题
      • SIGILL:尝试执行非法的指令,可能不被识别或者没有权限。
    • EXC_BAD_INSTRUCTION:此类异常通常由于线程执行非法指令导致
    • EXC_ARITHMETIC: 除零错误会抛出此类异常
  • Exception Code
    • 0xbaaaaaad: 此种类型的log意味着该Crash log并非一个真正的Crash,它仅仅只是包含了整个系统某一时刻的运行状态。通常可以通过同时按Home键和音量键,可能由于用户不小心触发.
    • 0xbad22222: 当VOIP程序后台太过频繁的**时,系统可能会终止此类程序.
    • 0x8badf00d: 程序启动或者恢复时间过长被watch dog终止
    • 0xc00010ff: 程序执行大量耗费CPU和GPU的运算,导致设备过热,触发系统过热保护被系统终止.
    • 0xdead10cc: 程序退到后台时还占用系统资源,如通讯录被系统终止.
    • 0xdeadfa11: 程序无响应用户强制关闭.

cache_init

缓存条件初始化
暂不展开分析

_imp_implementationWithBlock_init

启动回调机制。通常这不会做什么,因为所有的初始化都是惰性的,但是对于某些进程,我们会迫不及待的加载trampolines dylib.
暂不展开分析

_dyld_objc_notify_register

注册dyld通知回调。该方法是dyld方法,源码如下

void _dyld_objc_notify_register(_dyld_objc_notify_mapped    mapped, _dyld_objc_notify_init      init, _dyld_objc_notify_unmapped  unmapped) {
	dyld::registerObjCNotifiers(mapped, init, unmapped);
}

_dyld_objc_notify_register方法可知,该方法中只有一个registerObjCNotifiers方法,查看该方法。

void registerObjCNotifiers(_dyld_objc_notify_mapped mapped, _dyld_objc_notify_init init, _dyld_objc_notify_unmapped unmapped)
{
	// record functions to call
	sNotifyObjCMapped	= mapped;
	sNotifyObjCInit		= init;
	sNotifyObjCUnmapped = unmapped;

	// call 'mapped' function with all images mapped so far
	try {
		notifyBatchPartial(dyld_image_state_bound, true, NULL, false, true);
	}
	catch (const char* msg) {
		// ignore request to abort during registration
	}

	// <rdar://problem/32209809> call 'init' function on all images already init'ed (below libSystem)
	for (std::vector<ImageLoader*>::iterator it=sAllImages.begin(); it != sAllImages.end(); it++) {
		ImageLoader* image = *it;
		if ( (image->getState() == dyld_image_state_initialized) && image->notifyObjC() ) {
			dyld3::ScopedTimer timer(DBG_DYLD_TIMING_OBJC_INIT, (uint64_t)image->machHeader(), 0, 0);
			(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
		}
	}
}

由上方代码可知

  • sNotifyObjCMapped即为_objc_init方法中_dyld_objc_notify_register中的第一个参数&map_images,映射镜像文件
  • sNotifyObjCInit即为第二个参数load_images,加载镜像文件。
  • sNotifyObjCUnmapped即为第三个参数unmap_image
    dyld中的内容我们在iOS底层探索 应用程序加载已经了解过相应的内容,而现在需要做的是需要弄懂map_imagesload_imagesunmap_image三个方法即可

map_images

管理文件和动态库中所有的符号,如classProtocolselectorcategory
源码如下:

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_nolock方法,并通过断点方方式,将不执行的代码屏蔽掉,简化源码如下

void 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 (firstTime) {...}
    if (PrintImages) {...}
    
    // 查找所有带有Objective-C元数据的镜像文件
    hCount = 0;
    // 数级,根据总数改变大小
    int totalClasses = 0;
    int unoptimizedTotalClasses = 0;
    // 代码块,作用域
    {...}
    
    if (firstTime) {...}
    
    if (hCount > 0) {
        _read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
    }
    
    firstTime = NO;
    
    // 一切设置完成后,调用镜像加载功能
    for (auto func : loadImageFuncs) {...}
}

根据上部分的简化源码可知,firstTime变量的改变中间位置是该方法的核心,而_read_images方法为firstTime变量改变区间的核心。那么接下来的重点为_read_images方法

_read_images

进入到该方法,查看源码,先整体分析,简化源码如下

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();
#define EACH_HEADER \
    hIndex = 0;         \
    hIndex < hCount && (hi = hList[hIndex]); \
    hIndex++
    // 1: 
    if (!doneOnce) {...}
    
    // 2: Fix up @selector references
    static size_t UnfixedSelectors;
    {...}
    
    ts.log("IMAGE TIMES: fix up selector references");
    
    // 3: Discover classes. Fix up unresolved future classes. Mark bundle classes.
    bool hasDyldRoots = dyld_shared_cache_some_image_overridden();
    for (EACH_HEADER) {...}
    // 4: 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
    // 5: Fix up old objc_msgSend_fixup call sites
    for (EACH_HEADER) {...}
    
    ts.log("IMAGE TIMES: fix up objc_msgSend_fixup");
#endif
    bool cacheSupportsProtocolRoots = sharedCacheSupportsProtocolRoots();

    // 6: Discover protocols. Fix up protocol refs.
    for (EACH_HEADER) {...}
    
    ts.log("IMAGE TIMES: discover protocols");
    
    // 7: 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");

    // 8: 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) {...}
    
    ts.log("IMAGE TIMES: discover categories");

    // 9: 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");

    // 10: Realize newly-resolved future classes, in case CF manipulates them
    if (resolvedFutureClasses) {...}
    
    ts.log("IMAGE TIMES: realize future classes");

    if (DebugNonFragileIvars) {...}
    
    // Print preoptimization statistics
    if (PrintPreopt) {...}
    
#undef EACH_HEADER
}

上部流程分析

  • 1:条件控制进行一次的加载
  • 2:修复预编译阶段的@selector的混乱问题
  • 3:错误混乱的类处理
  • 4:修复重映射一些没有被镜像文件加载进来额类
  • 5:修复一些消息
  • 6:当我们类里面有协议的时候:readProtocol
  • 7:修复没有被加载的协议
  • 8:分类处理
  • 9:类的加载处理
  • 10:没有被处理的类,优化那些被侵犯的类

接下来通过断点,仅将执行的代码进行细节分析
2:中的代码块,源码如下:

// Fix up @selector references
    static size_t UnfixedSelectors;
    {
        mutex_locker_t lock(selLock);
        for (EACH_HEADER) {
            if (hi->hasPreoptimizedSelectors()) continue;

            bool isBundle = hi->isBundle();
            SEL *sels = _getObjc2SelectorRefs(hi, &count);
            UnfixedSelectors += count;
            for (i = 0; i < count; i++) {
                const char *name = sel_cname(sels[i]);
                SEL sel = sel_registerNameNoLock(name, isBundle);
                if (sels[i] != sel) {
                    sels[i] = sel;
                }
            }
        }
    }

将代码运行后,断点分析,运行结果如下:
iOS底层探索(十一)类的加载
按理来说,两个方法名相同,那么两个方法的地址应该是相同,但是为什么会出现这种情况呢?
iOS底层探索(十一)类的加载
如上图所示,在我们整个系统中会有多个框架如FoundationCoreFoundation等,当每个框架都有一个class方法时,在执行该方法时,需要将方法平移到程序出口的位置进行执行,那么在Foundation框架中的class方法,则为0, 在CoreFoundation框架中的class方法则为0 + Foundation大小。因此,地址不同,方法需要进行平移调整。

3: 中的代码块,源码如下

// Discover classes. Fix up unresolved future classes. Mark bundle classes.
    bool hasDyldRoots = dyld_shared_cache_some_image_overridden();

    for (EACH_HEADER) {
        if (! mustReadClasses(hi, hasDyldRoots)) {
            // Image is sufficiently optimized that we need not call readClass()
            continue;
        }

        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) {...}
        }
    }

该代码中将进行readClass的读取。在readClass的前后进行断点打印,结果如下:
iOS底层探索(十一)类的加载
发现该方法是对类进行名字赋值。
查看readClass方法源码,根据断点进行简化,源码如下:

Class readClass(Class cls, bool headerIsBundle, bool headerIsPreoptimized)
{
    const char *mangledName = cls->mangledName();
    
    if (missingWeakSuperclass(cls)) {...}
    
    cls->fixupBackwardDeployingStableSwift();

    Class replacing = nil;
    if (Class newCls = popFutureNamedClass(mangledName)) {...}
    
    if (headerIsPreoptimized  &&  !replacing) {...} else {
    // 添加类名,
        addNamedClass(cls, mangledName, replacing);
        addClassTableEntry(cls);
    }

    // for future reference: shared cache never contains MH_BUNDLEs
    if (headerIsBundle) {
        cls->data()->flags |= RO_FROM_BUNDLE;
        cls->ISA()->data()->flags |= RO_FROM_BUNDLE;
    }
    
    return cls;
}

查看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 {
        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());
}

该方法为将类地址与名字进行映射,并插入内存。
查看mangledName方法源码,如下:

const char *mangledName() { 
        // fixme can't assert locks here
        ASSERT(this);

        if (isRealized()  ||  isFuture()) {
            return data()->ro()->name;
        } else {
            return ((const class_ro_t *)data())->name;
        }
    }

该方法为读取类的名字