iOS - 探索dyld

作者: Sheisone | 来源:发表于2020-10-14 22:26 被阅读0次

    一、背景知识

    1.静态库和动态库

    1.1.库

    首先来看什么是库,库(Library)说白了就是一段编译好的二进制代码,加上头文件就可以供别人使用。
    什么时候我们会用到库呢?

    • 一种情况是某些代码需要给别人使用,但是我们不希望别人看到源码,就需要以库的形式进行封装,只暴露出头文件。 (一些引入的第三方库)
    • 对于某些不会进行大的改动的代码,我们想减少编译的时间,就可以把它打包成库,因为库是已经编译好的二进制了,编译的时候只需要 Link 一下,不会浪费编译时间。 (一些比较稳定的工具类,或者很稳定的功能模块)

    1.2.framework

    framework并不是库,它只是一种打包方式,它既可以是动态库也可以是静态库。将库的二进制文件,头文件和有关的资源文件打包到一起,方便管理和分发,和静态库、动态库的本质是没有什么关系。

    1.3.静态库

    静态库 (静态链接库) 以.a.framework结尾 。之所以称之为【静态库】,是因为在链接阶段,会将汇编生成的目标文件.o与 引用的库一起链接到可执行文件中。对应的链接方式称为 静态链接。 静态库在编译的时候会被直接拷贝一份,复制到目标程序里,这段代码在程序里就不会在改变。
    举个🌰:

    静态库.png

    静态库优点:

    • 编译完成之后,库文件没有作用了,目标没有外部依赖,直接可以运行
    • 静态库对函数库的链接是在编译期完成的。执行期间代码装载速度快。

    静态库缺点:

    • 使可执行文件变大,浪费空间和资源(占空间)
    • 维护成本高,某一个静态库更新了,所有使用它的应用程序都需要重新编译、发布给用户。

    1.4.动态库

    动态库(动态链接库) 以 .dylib或者.framework后缀结尾。与静态库相反,动态库在编译时并不会被拷贝到目标程序中,目标程序中只会存储指向动态库的引用。等到程序运行时,动态库才会被真正加载进来。不同的应用程序如果调用相同的库,那么在内存中只需要有一份该共享库的实例,避免了空间浪费问题。同时也解决了静态库对程序的更新的依赖,用户只需更新动态库即可。
    再举个🌰:

    动态库.png

    动态库优点:

    • 不会影响目标程序的体积,而且同一份库可以被多个程序使用
    • 动态性,运行时才载入的特性,也可以让我们随时对库进行替换,而不需要重新编译代码,
    • 动态库把对一些库函数的链接载入推迟到程序运行时期

    动态库缺点:

    • 动态载入会带来一部分性能损失,使用动态库也会使得程序依赖于外部环境。缺少动态库或者库的版本不正确,就会导致程序无法运行
    • 维护成本高,某一个静态库更新了,所有使用它的应用程序都需要重新编译、发布给用户。

    2.虚拟内存与物理内存

    2.1.物理内存

    在早期的计算机中 , 并没有虚拟内存的概念 , 任何应用被从磁盘中加载到运行内存中时 , 都是完整加载和按序排列的 .
    那么因此 , 就会出现两个问题 :

    使用物理内存时遗留的问题

    • 安全问题 : 由于在内存条中使用的都是真实物理地址 , 而且内存条中各个应用进程都是按顺序依次排列的 . 那么在 进程1 中通过地址偏移就可以访问到 其他进程 的内存 .
    • 效率问题 : 随着软件的发展 , 一个软件运行时需要占用的内存越来越多 , 但往往用户并不会用到这个应用的所有功能 , 造成很大的内存浪费 , 而后面打开的进程往往需要排队等待 .
      为了解决上述两个问题 , 虚拟内存应运而生 .

    2.2.虚拟内存

    引用了虚拟内存后 , 在我们进程中认为自己有一大片连续的内存空间实际上是虚拟的 , 也就是说从0x000000 ~ 0xffffff我们是都可以访问的 . 但是实际上这个内存地址只是一个虚拟地址 , 而这个虚拟地址通过一张映射表映射后才可以获取到真实的物理地址 .

    什么意思呢 ?
    实际上我们可以理解为 , 系统对真实物理内存访问做了一层限制 , 只有被写到映射表中的地址才是被认可可以访问的 .
    例如 , 虚拟地址 0x000000 ~ 0xffffff这个范围内的任意地址我们都可以访问 , 但是这个虚拟地址对应的实际物理地址是计算机来随机分配到内存页上的 .
    这里提到了实际物理内存分页的概念 , 下面会详细讲述 .
    可能大家也有注意到 , 我们在一个工程中获取的地址 , 同时在另一个工程中去访问 , 并不能访问到数据 , 其原理就是虚拟内存 .

    3.dyld

    dyldthe dynamic link editor),【动态链接器】是苹果操作系统一个重要部分,在 iOS / macOS 系统中,仅有很少的进程只需内核就可以完成加载,基本上所有的进程都是动态链接的,所以 Mach-O 镜像文件中会有很多对外部的库和符号的引用,但是这些引用并不能直接用,在启动时还必须要通过这些引用进行内容填充,这个填充的工作就是由 dyld 来完成的。
    【动态链接加载器】在系统中以一个用户态的可执行文件形式存在,一般应用程序会在Mach-O文件部分指定一个 LC_LOAD_DYLINKER 的加载命令,此加载命令指定了dyld的路径,通常它的默认值是“/usr/lib/dyld”。系统内核在加载Mach-O文件时,会使用该路径指定的程序作为动态库的加载器来加载dylib。

    4.共享缓存

    dyld加载时,为了优化程序启动,启用了共享缓存(shared cache)技术。共享缓存会在进程启动时被dyld映射到内存中,之后,当任何Mach-O镜像加载时,dyld首先会检查该Mach-O镜像与所需的动态库是否在共享缓存中,如果存在,则直接将它在共享内存中的内存地址映射到进程的内存地址空间。在程序依赖的系统动态库很多的情况下,这种做法对程序启动性能是有明显提升的。

    二、dyld流程

    本文中会涉及到dyld的源码,所以需要的同学可以点击下载dyld 源码
    首先创建一个工程,并且在main函数前打上断点:

    image.png

    运行工程,来到断点处,打开汇编模式:


    image.png

    我们会发现,在main之前,调用了start函数,这个start是来自于libdyld.dylib库执行的。

    我们再在Viewcontroller中重写load方法,并在load方法前打上断点:

    image.png
    运行:
    image.png

    我们会发现:

    • load的断点比main的断点先执行,那说明+load方法是在main之前执行的。
    • load之前,dyld还执行了很多流程

    接下来我们需要来到dyld源码中,从_dyld_start开始探索:

    1._dyld_start

    在源码中搜索_dyld_start并找到下图所示的地方:

    image.png

    会发现_dyld_start是汇编编写,可能汇编我们看不懂,但是没关系,我们可以看下注释。
    尽管在不同架构下有所区别,但都是会调用dyldbootstrap 命名空间下的start方法,这和上面的堆栈顺序也是相同的。

    image.png

    1.dyldbootstrap::start:

    继续搜索start,发现很多不好找,我们再次搜索start(,结果如下:

    image.png
    start函数源码如下:
    //  This is code to bootstrap dyld.  This work in normally done for a program by dyld and crt.
    //  In dyld we have to do this manually.
    //
    uintptr_t start(const dyld3::MachOLoaded* appsMachHeader, int argc, const char* argv[],
                    const dyld3::MachOLoaded* dyldsMachHeader, uintptr_t* startGlue)
    {
    
        // Emit kdebug tracepoint to indicate dyld bootstrap has started <rdar://46878536>
        dyld3::kdebug_trace_dyld_marker(DBG_DYLD_TIMING_BOOTSTRAP_START, 0, 0, 0, 0);
    
        // if kernel had to slide dyld, we need to fix up load sensitive locations
        // we have to do this before using any global variables
        rebaseDyld(dyldsMachHeader);
    
        // kernel sets up env pointer to be just past end of agv array
        const char** envp = &argv[argc+1];
        
        // kernel sets up apple pointer to be just past end of envp array
        const char** apple = envp;
        while(*apple != NULL) { ++apple; }
        ++apple;
    
        // set up random value for stack canary
        __guard_setup(apple);
    
    #if DYLD_INITIALIZER_SUPPORT
        // run all C++ initializers inside dyld
        runDyldInitializers(argc, argv, envp, apple);
    #endif
    
        // now that we are done bootstrapping dyld, call dyld's main
        uintptr_t appsSlide = appsMachHeader->getSlide();
        return dyld::_main((macho_header*)appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
    }
    

    看了下源码,发现只有一句关键代码,就是return dyld::_main((macho_header*)appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);,调用了_main函数,所以我们接着看_main

    3、dyld::_main

    直接command进去,看到_main的源码,这里面源码非常多,将重要的代码提炼出来:

    uintptr_t
    _main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide, 
            int argc, const char* argv[], const char* envp[], const char* apple[], 
            uintptr_t* startGlue)
    {
        ......
    
        // 设置运行环境,可执行文件准备工作
        ......
    
        // load shared cache   加载共享缓存
        mapSharedCache();
        ......
    ///加载所有的库,image就是库
    reloadAllImages:
    
        ......
        // instantiate ImageLoader for main executable 加载可执行文件并生成一个ImageLoader实例对象
        sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
    
        ......
    
        // load any inserted libraries   加载插入的动态库
        if  ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
            for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib) 
                loadInsertedDylib(*lib);
        }
            
        // link main executable  链接主程序
        link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
    
        ......
        // link any inserted libraries   链接所有插入的动态库
        if ( sInsertedDylibCount > 0 ) {
            for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
                ImageLoader* image = sAllImages[i+1];
                link(image, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
                image->setNeverUnloadRecursive();
            }
            if ( gLinkContext.allowInterposing ) {
                // only INSERTED libraries can interpose
                // register interposing info after all inserted libraries are bound so chaining works
                for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
                    ImageLoader* image = sAllImages[i+1];
                    // 注册符号插入
                    image->registerInterposing(gLinkContext);
                }
            }
        }
    
        ......
        //弱符号绑定
        sMainExecutable->weakBind(gLinkContext);
            
        sMainExecutable->recursiveMakeDataReadOnly(gLinkContext);
    
        ......
        // run all initializers   执行初始化方法
        initializeMainExecutable(); 
    
        // notify any montoring proccesses that this process is about to enter main()
        notifyMonitoringDyldMain();
    
        return result;
    }
    
    
    

    _main中做了非常多的时候事情,我们可以对照我们前面的短线堆栈图来观察:

    重要函数解释:

    3.1 sMainExecutable = instantiateFromLoadedImage(....)loadInsertedDylib(...)

    这一步 dyld将我们可执行文件以及插入的 lib 加载进内存,生成对应的image
    sMainExecutable 对应着我们的可执行文件,里面包含了我们项目中所有新建的类。
    InsertDylib 一些插入的库,他们配置在全局的环境变量 sEnv 中,我们可以在项目中设置环境变量 DYLD_PRINT_ENV 为1来打印该 sEnv 的值。

    static ImageLoaderMachO* instantiateFromLoadedImage(const macho_header* mh, uintptr_t slide, const char* path)
    {
        // try mach-o loader
        if ( isCompatibleMachO((const uint8_t*)mh, path) ) {
            ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);
            addImage(image);
            return (ImageLoaderMachO*)image;
        }
        
        throw "main executable not a known format";
    }
    

    isCompatibleMachO 是检查Mach-Osubtype是否是当前cpu可以支持;
    内核会映射到主可执行文件中,我们需要为映射到主可执行文件的文件,创建ImageLoader
    instantiateMainExecutable 就是实例化可执行文件, 这个期间会解析LoadCommand, 这个之后会发送 dyld_image_state_mapped 通知; 在此方法中,读取image,然后addImage()到镜像列表。

    3.2:link(sMainExecutable,...)link(image,....)

    对上面生成的 Image 进行链接。这个过程就是将加载进来的二进制变为可用状态的过程。其主要做的事有对image进行 load(加载),rebase(基地址复位),bind(外部符号绑定),我们可以查看源码:

    void ImageLoader::link(const LinkContext& context, bool forceLazysBound, bool preflightOnly, bool neverUnload, const RPathChain& loaderRPaths, const char* imagePath)
    {
        ......
        this->recursiveLoadLibraries(context, preflightOnly, loaderRPaths, imagePath);  
        ......
        this->recursiveRebaseWithAccounting(context);
        ......
        this->recursiveBindWithAccounting(context, forceLazysBound, neverUnload);
    }
    
    3.3: recursiveLoadLibraries(context, preflightOnly, loaderRPaths)

    递归加载所有依赖库进内存。

    3.4:recursiveRebase(context)

    递归对自己以及依赖库进行rebase操作。在以前,程序每次加载其在内存中的堆栈基地址都是一样的,这意味着你的方法,变量等地址每次都一样的,这使得程序很不安全,后面就出现ASLR(Address space layout randomization,地址空间布局随机化),程序每次启动后地址都会随机变化,这样程序里所有的代码地址都是错的,需要重新对代码地址进行计算修复才能正常访问。

    3.5:recursiveBindWithAccounting(context, forceLazysBound, neverUnload);

    对库中所有nolazy的符号进行bind,一般的情况下多数符号都是lazybind的,他们在第一次使用的时候才进行bind

    3.6 initializeMainExecutable()
    void initializeMainExecutable()
    {
        // record that we've reached this step
        gLinkContext.startedInitializingMainExecutable = true;
    
        // run initialzers for any inserted dylibs
        ImageLoader::InitializerTimingList initializerTimes[allImagesCount()];
        initializerTimes[0].count = 0;
        const size_t rootCount = sImageRoots.size();
        if ( rootCount > 1 ) {
            for(size_t i=1; i < rootCount; ++i) {
                sImageRoots[i]->runInitializers(gLinkContext, initializerTimes[0]);
            }
        }
        
        // run initializers for main executable and everything it brings up 
        sMainExecutable->runInitializers(gLinkContext, initializerTimes[0]);
        
        // register cxa_atexit() handler to run static terminators in all loaded images when this process exits
        if ( gLibSystemHelpers != NULL ) 
            (*gLibSystemHelpers->cxa_atexit)(&runAllStaticTerminators, NULL, NULL);
    
        // dump info if requested
        if ( sEnv.DYLD_PRINT_STATISTICS )
            ImageLoader::printStatistics((unsigned int)allImagesCount(), initializerTimes[0]);
        if ( sEnv.DYLD_PRINT_STATISTICS_DETAILS )
            ImageLoaderMachO::printStatisticsDetails((unsigned int)allImagesCount(), initializerTimes[0]);
    }
    

    这一步主要是调用所有imageInitalizer方法进行初始化。先为所有插入并链接完成的动态库执行初始化操作
    sImageRoots[i]->runInitializers(gLinkContext, initializerTimes[0]);

    再为主程序可执行文件执行初始化操作
    sMainExecutable->runInitializers(gLinkContext, initializerTimes[0]);

    具体流程为:ImageLoader::runInitializers -->ImageLoader::processInitializers --> ImageLoader::recursiveInitialization
    详细代码如下:

    3.7 ImageLoader::runInitializers
    void ImageLoader::runInitializers(const LinkContext& context, InitializerTimingList& timingInfo)
    {
        uint64_t t1 = mach_absolute_time();
        mach_port_t thisThread = mach_thread_self();
        ImageLoader::UninitedUpwards up;
        up.count = 1;
        up.imagesAndPaths[0] = { this, this->getPath() };
          // 重点
        processInitializers(context, thisThread, timingInfo, up);
        context.notifyBatch(dyld_image_state_initialized, false);
        mach_port_deallocate(mach_task_self(), thisThread);
        uint64_t t2 = mach_absolute_time();
        fgTotalInitTime += (t2 - t1);
    }
    

    调用processInitializers

    3.7 ImageLoader::processInitializers
    void ImageLoader::processInitializers(const LinkContext& context, mach_port_t thisThread,
                                         InitializerTimingList& timingInfo, ImageLoader::UninitedUpwards& images)
    {
        uint32_t maxImageCount = context.imageCount()+2;
        ImageLoader::UninitedUpwards upsBuffer[maxImageCount];
        ImageLoader::UninitedUpwards& ups = upsBuffer[0];
        ups.count = 0;
        // Calling recursive init on all images in images list, building a new list of
        // uninitialized upward dependencies.
        for (uintptr_t i=0; i < images.count; ++i) {
          // 重点
            images.imagesAndPaths[i].first->recursiveInitialization(context, thisThread, images.imagesAndPaths[i].second, timingInfo, ups);
        }
        // If any upward dependencies remain, init them.
        if ( ups.count > 0 )
            processInitializers(context, thisThread, timingInfo, ups);
    }
    

    在这里,对镜像表中的所有镜像执行recursiveInitialization ,创建一个未初始化的向上依赖新表。如果依赖中未初始化完毕,则继续执行processInitializers,直到全部初始化完毕。

    3.8 ImageLoader::recursiveInitialization
    void ImageLoader::recursiveInitialization(const LinkContext& context, mach_port_t this_thread, const char* pathToInitialize,
                                              InitializerTimingList& timingInfo, UninitedUpwards& uninitUps)
    {
        recursive_lock lock_info(this_thread);
        recursiveSpinLock(lock_info);
    
        if ( fState < dyld_image_state_dependents_initialized-1 ) {
            uint8_t oldState = fState;
            // break cycles
            fState = dyld_image_state_dependents_initialized-1;
            try {
                // initialize lower level libraries first
                for(unsigned int i=0; i < libraryCount(); ++i) {
                    ImageLoader* dependentImage = libImage(i);
                    if ( dependentImage != NULL ) {
                        // don't try to initialize stuff "above" me yet
                        if ( libIsUpward(i) ) {
                            uninitUps.imagesAndPaths[uninitUps.count] = { dependentImage, libPath(i) };
                            uninitUps.count++;
                        }
                        else if ( dependentImage->fDepth >= fDepth ) {
                            dependentImage->recursiveInitialization(context, this_thread, libPath(i), timingInfo, uninitUps);
                        }
                    }
                }
                
                // record termination order
                if ( this->needsTermination() )
                    context.terminationRecorder(this);
    
                // 重点 1: let objc know we are about to initialize this image
                uint64_t t1 = mach_absolute_time();
                fState = dyld_image_state_dependents_initialized;
                oldState = fState;
                context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);
                
                // 重点 2: initialize this image   
                bool hasInitializers = this->doInitialization(context);
    
                // 重点 3: let anyone know we finished initializing this image
                fState = dyld_image_state_initialized;
                oldState = fState;
                context.notifySingle(dyld_image_state_initialized, this, NULL);
                
                if ( hasInitializers ) {
                    uint64_t t2 = mach_absolute_time();
                    timingInfo.addTime(this->getShortName(), t2-t1);
                }
            }
            catch (const char* msg) {
                // this image is not initialized
                fState = oldState;
                recursiveSpinUnLock();
                throw;
            }
        }
        
        recursiveSpinUnLock();
    }
    

    recursiveInitialization 函数中,我们重点关注

    context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);,
    doInitialization(context)
    context.notifySingle(dyld_image_state_initialized, this, NULL);
    
    context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);
    

    通知objc我们要初始化这个镜像,这里 通过 notifySingle函数对sNotifyObjCInit进行函数调用。

    3.9 context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo)
    static void notifySingle(dyld_image_states state, const ImageLoader* image, ImageLoader::InitializerTimingList* timingInfo)
    {
        ......
    
        if ( (state == dyld_image_state_dependents_initialized) && (sNotifyObjCInit != NULL) && image->notifyObjC() ) {
            uint64_t t0 = mach_absolute_time();
            
            (*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
            
        }
        ......  
    }
    

    获取镜像文件的真实地址 【*sNotifyObjCInit)(image->getRealPath(), image->machHeader()】,而sNotifyObjCInit 是通过 registerObjCNotifiers 中传递的参数(_dyld_objc_notify_init)进行赋值的。

    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;
        ......
    }
    

    继而找到,registerObjCNotifiers的 拉起函数 _dyld_objc_notify_register .

    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 函数是供 objc runtime 使用的,当objc镜像被映射,取消映射,和初始化时 被调用的注册处理器。我们可以在libobjc.A.dylib库里,_objc_init函数中找到其调用。

    /***********************************************************************
    * _objc_init
    * Bootstrap initialization. Registers our image notifier with dyld.
    * Called by libSystem BEFORE library initialization time
    **********************************************************************/
    void _objc_init(void)
    {
        static bool initialized = false;
        if (initialized) return;
        initialized = true;
        
        // fixme defer initialization until an objc-using image is found?
        environ_init(); // 环境变量
        tls_init();
        static_init(); // C++
        runtime_init(); // runtime 初始化
        exception_init(); // 异常初始化
        cache_init(); // 缓存初始化
        _imp_implementationWithBlock_init(); //
    
        _dyld_objc_notify_register(&map_images, load_images, unmap_image);
    
    #if __OBJC2__
        didCallDyldNotifyRegister = true;
    #endif
    }
    

    runtime初始化后,在_objc_init中注册了几个通知,从dyld这里接手了几个活,其中包括负责初始化相应依赖库里的类结构,调用依赖库里所有的load方法等。
    就拿sMainExcuatable来说,它的initializer方法是最后调用的,当initializer方法被调用前dyld会通知runtime进行类结构初始化,然后再通知调用load方法,这些目前还发生在main函数前,但由于lazy bind机制,依赖库多数都是在使用时才进行bind,所以这些依赖库的类结构初始化都是发生在程序里第一次使用到该依赖库时才进行的。
    当所有的依赖库的lnitializer都调用完后,dyld::main 函数会返回程序的main()函数地址,main函数被调用,从而代码来到了我们熟悉的程序入口。
    那么_objc_init 又是如何被调用的呢?

    还记得之前ImageLoader::recursiveInitialization中的重点2吗?我们command进去:

    bool ImageLoaderMachO::doInitialization(const LinkContext& context)
    {
        CRSetCrashLogMessage2(this->getPath());
    
        // mach-o has -init and static initializers
        doImageInit(context);
        doModInitFunctions(context);
        
        CRSetCrashLogMessage2(NULL);
        
        return (fHasDashInit || fHasInitializers);
    }
    
    

    这里调用了doModInitFunctions,在 doModInitFunctions之后 会 先执行 libSystem_initializer,保证系统库优先初始化完毕,在这里初始化 libdispatch_init,进而在_os_object_init中 调用_objc_init
    由于runtimedyld 绑定了回调,当image加载到内存后,dyld会通知 runtime 进行处理
    runtime 接手后调用 map_images 做解析和处理,接下来load_images中调用 call_load_methods方法,遍历所有加载进来的 Class,按继承层级依次调用Class+load方法和其 Category+load方法。
    至此,可执行文件和动态库中所有的符号(Class,Protocol,Selector,IMP,…)都已经按格式成功加载到内存中,被runtime所管理,在这之后,runtime的那些方法(动态添加 Classswizzle 等等才能生效)

    dyld流程图:

    image.png

    相关文章

      网友评论

        本文标题:iOS - 探索dyld

        本文链接:https://www.haomeiwen.com/subject/rtcbpktx.html