iOS:动态库的加载顺序

在写 《iOS:load方法能不能被hook?》《iOS启动优化:App启动耗时在线监控与AppDelegate管控》 两篇文章时都提到了动态库的加载,由于主题的原因,没有详细介绍,有同学对这个比较感兴趣,今天我们就来研究下在iOS中动态库的加载顺序是什么样子的。

1.实验篇

我们先通过demo看下几种Case:

  • 没有依赖关系:

    情形一

    我们制作dylibAdylibBdylibB这三个动态库(不了解动态库的制作的请问度娘...),且它们没有依赖关系,同时我们在每个库中添加一个Class,暂且以Class的load方法的调用顺序当做是动态库的加载顺序(先挖一个坑),比如dylibA

    @implementation ClassA
    + (void)load {
        NSLog(@"dylibA loaded");
    }
    @end
    

    将这三个库加到demo工程中,并且保证Build Phases-Link Binary With Libraries中的顺序:dylibA > dylibB > dylibC,运行结果:

    Demo[53199:17384949] dylibA loaded
    Demo[53199:17384949] dylibB loaded
    Demo[53199:17384949] dylibC loaded
    

    我们调整下Link Binary With Libraries中的顺序:dylibC > dylibB > dylibA,运行结果:

    Demo[53265:17397552] dylibC loaded
    Demo[53265:17397552] dylibB loaded
    Demo[53265:17397552] dylibA loaded
    

    通过实验我们知道:在没有依赖关系的情况下,动态库的加载顺序由Link Binary With Libraries中的顺序决定,当然我们可以通过Link Binary With Libraries来控制动态库的加载顺序。

  • 单一依赖关系

    情形二

    dylibA依赖dylibBdylibB依赖dylibC,我们简单改造下这三个库,如在dylibB中import下dylibC的头文件,dylibA中同理:

    #import "ClassB.h"
    #import <dylibC/dylibC.h>
    
    @implementation ClassB
    + (void)load {
        NSLog(@"dylibB loaded");
    }
    @end
    

    Link Binary With Libraries中的顺序:dylibA > dylibB > dylibC,运行结果:

    Demo[53570:17450857] dylibC loaded
    Demo[53570:17450857] dylibB loaded
    Demo[53570:17450857] dylibA loaded
    

    这次我们发现三个库的加载顺序是反的,我们修改下顺序:dylibC > dylibA > dylibB,运行结果不变。由实验结果可知:动态库的加载顺序还受依赖关系影响,被依赖的子节点优先加载。

  • 组合依赖关系


    组合关系1

    其中,dylibAdylibBdylibB 没有依赖关系,dylibA 依赖了dylibDdylibEdylibF

    Demo[97898:19286936] dylibD loaded
    Demo[97898:19286936] dylibF loaded
    Demo[97898:19286936] dylibE loaded
    Demo[97898:19286936] dylibA loaded
    Demo[97898:19286936] dylibB loaded
    Demo[97898:19286936] dylibC loaded
    

    通过修改dylibA-Link Binary With LibrariesdylibDdylibE的顺序调整为:

    组合关系2

    Demo[97982:19305235] dylibF loaded
    Demo[97982:19305235] dylibE loaded
    Demo[97982:19305235] dylibD loaded
    Demo[97982:19305235] dylibA loaded
    Demo[97982:19305235] dylibB loaded
    Demo[97982:19305235] dylibC loaded
    

    通过上面的尝试,我们可以看出动态库的加载顺序为:先根据配置的链接顺序加载,如有依赖的先递归式加载依赖。

2.源码篇

上面我们通过几个实验对动态库的加载有个大概的影响,下面我们通过源码进一步了解动态库的加载(dyld源码,本文使用版本:dyld-635.2,篇幅问题,只展示部分代码)。
为了便于的过程,我们在上面demo中的位于子节点的dylibF库的load方法添加一个符号断点,获取下这个load方法的调用栈:

  #0    +[ClassF load]
  #1    load_images ()
  #2    dyld::notifySingle(dyld_image_states, ImageLoader const*, ImageLoader::InitializerTimingList*) ()
  #3    ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) ()
  #4    ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) ()
  #5    ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) ()
  #6    ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) ()
  #7    ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) ()
  #8    ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) ()
  #9    dyld::initializeMainExecutable() 
  #10   dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) ()
  #11   dyldbootstrap::start(macho_header const*, int, char const**, long, macho_header const*, unsigned long*) ()
  #12   _dyld_start ()

_dyld_startdyldbootstrap::start使用汇编实现,我们重点看下dyld::_main开始的实现。

  • dyld::_main,动态链接器的入口函数,代码较多,只展示部分代码:

    //
    // Entry point for dyld.  The kernel loads dyld and jumps to __dyld_start which
    // sets up some registers and call this function.
    //
    // Returns address of main() in target program which __dyld_start jumps to
    //
    uintptr_t
    _main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide, 
            int argc, const char* argv[], const char* envp[], const char* apple[], 
            uintptr_t* startGlue)  {
        //1.配置环境
        ...很多代码
        //2.加载共享缓存
        checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);
        //3.实例化主程序
        sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
        gLinkContext.mainExecutable = sMainExecutable;
        gLinkContext.mainExecutableCodeSigned = hasCodeSignatureLoadCommand(mainExecutableMH);
        //4.插入动态库(使用动态库注入代码就是通过这个实现的)
        if ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
              for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib) 
                  loadInsertedDylib(*lib);
          }
        //5.链接主程序
        gLinkContext.linkingMainExecutable = true;
        link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
        //6.链接插入的动态库
        ...很多代码
        //7.初始化主程序
        initializeMainExecutable(); 
        //...快结束了 
    }
    

    和我们今天主题相关的为link()initializeMainExecutable()两个过程,分别负责链接(符号rebase,binding)和初始化(调用load__attribute__((constructor)) 修饰的c函数等)的工作。

  • link()

    void link(ImageLoader* image, bool forceLazysBound, bool neverUnload, const 
      ImageLoader::RPathChain& loaderRPaths, unsigned cacheIndex)
    {
      // add to list of known images.  This did not happen at creation time for bundles
      if ( image->isBundle() && !image->isLinked() )
          addImage(image);
    
      // we detect root images as those not linked in yet 
      if ( !image->isLinked() )
          addRootImage(image);
      
      // process images
      try {
          const char* path = image->getPath();
    #if SUPPORT_ACCELERATE_TABLES
          if ( image == sAllCacheImagesProxy )
              path = sAllCacheImagesProxy->getIndexedPath(cacheIndex);
    #endif
          image->link(gLinkContext, forceLazysBound, false, neverUnload, loaderRPaths, path);
      }
      catch (const char* msg) {
          garbageCollectImages();
          throw;
      }
    }
    

    link()中又调用ImageLoader::link方法,ImageLoader是一个基类负责加载image文件(主程序,动态库)每个image对应一个ImageLoader子类的实例,我们继续看下它的实现:

  • ImageLoader::link

     void ImageLoader::link(const LinkContext& context, bool forceLazysBound, bool preflightOnly, bool neverUnload, const RPathChain& loaderRPaths, const char* imagePath)
     {
      //dyld::log("ImageLoader::link(%s) refCount=%d, neverUnload=%d\n", imagePath, fDlopenReferenceCount, fNeverUnload);
      
      // clear error strings
      (*context.setErrorStrings)(0, NULL, NULL, NULL);
    
      uint64_t t0 = mach_absolute_time();
      this->recursiveLoadLibraries(context, preflightOnly, loaderRPaths, imagePath);
      context.notifyBatch(dyld_image_state_dependents_mapped, preflightOnly);
    
      // we only do the loading step for preflights
      if ( preflightOnly )
          return;
    
      uint64_t t1 = mach_absolute_time();
      context.clearAllDepths();
      this->recursiveUpdateDepth(context.imageCount());
    
      //省略符号rebase 和binding过程...
    }
    

    ImageLoader::link中使用ImageLoader::recursiveLoadLibraries加载动态库,截取部分代码:

  • ImageLoader::recursiveLoadLibraries

     void ImageLoader::recursiveLoadLibraries(const LinkContext& context, bool preflightOnly, const RPathChain& loaderRPaths, const char* loadPath)
    {
        //...省略很多代码
        // 获取当前image依赖的动态库
        DependentLibraryInfo libraryInfos[fLibraryCount]; 
        this->doGetDependentLibraries(libraryInfos);
        //...省略很多代码
    
        for(unsigned int i=0; i < fLibraryCount; ++i){ 
           //加载动态库
           DependentLibraryInfo& requiredLibInfo = libraryInfos[i];
           dependentLib = context.loadLibrary(requiredLibInfo.name, true, this->getPath(), &thisRPaths, enforceIOSMac, cacheIndex);
           //...省略很多代码
        }
        
        //保存加载完成的动态库
        setLibImage(i, dependentLib, depLibReExported, requiredLibInfo.upward);
    
       // 告诉此image依赖的动态库们递归的加载各自依赖的动态库
       for(unsigned int i=0; i < libraryCount(); ++i) {
          ImageLoader* dependentImage = libImage(i);
          if ( dependentImage != NULL ) { 
              dependentImage->recursiveLoadLibraries(context, preflightOnly, thisRPaths, libraryInfos[i].name);
          }
       }
       //...省略很多代码
    }
    

    此方法也比较长,它主要做的事情是:1)获取当前image依赖的的动态库;2)循环加载当前image依赖的动态库;3)告诉当前image依赖的动态库们递归的加载各自依赖的动态库;
    具体的加载方法在dyld.cppImageLoader* load(const char* path, const LoadContext& context, unsigned& cacheIndex),我们不多做介绍。从上面的代码可以知道,dyld通过doGetDependentLibraries获取了image依赖的动态库信息,然后循环加载,这个函数的实现会影响动态库的加载顺序,我们看下doGetDependentLibraries的实现(doGetDependentLibraries是个虚函数,只有在ImageLoaderMachO.cpp中找到了它的实现):

  • ImageLoaderMachO::doGetDependentLibraries

    void ImageLoaderMachO::doGetDependentLibraries(DependentLibraryInfo libs[])
    {
      if ( needsAddedLibSystemDepency(libraryCount(), (macho_header*)fMachOData) ) {
          DependentLibraryInfo* lib = &libs[0];
          lib->name = LIBSYSTEM_DYLIB_PATH;
          lib->info.checksum = 0;
          lib->info.minVersion = 0;
          lib->info.maxVersion = 0;
          lib->required = false;
          lib->reExported = false;
          lib->upward = false;
      }
      else {
          uint32_t index = 0;
          const uint32_t cmd_count = ((macho_header*)fMachOData)->ncmds;
          const struct load_command* const cmds = (struct load_command*)&fMachOData[sizeof(macho_header)];
          const struct load_command* cmd = cmds;
          for (uint32_t i = 0; i < cmd_count; ++i) {
              switch (cmd->cmd) {
                  case LC_LOAD_DYLIB:
                  case LC_LOAD_WEAK_DYLIB:
                  case LC_REEXPORT_DYLIB:
                  case LC_LOAD_UPWARD_DYLIB:
                  {
                      const struct dylib_command* dylib = (struct dylib_command*)cmd;
                      DependentLibraryInfo* lib = &libs[index++];
                      lib->name = (char*)cmd + dylib->dylib.name.offset;
                      //lib->name = strdup((char*)cmd + dylib->dylib.name.offset);
                      lib->info.checksum = dylib->dylib.timestamp;
                      lib->info.minVersion = dylib->dylib.compatibility_version;
                      lib->info.maxVersion = dylib->dylib.current_version;
                      lib->required = (cmd->cmd != LC_LOAD_WEAK_DYLIB);
                      lib->reExported = (cmd->cmd == LC_REEXPORT_DYLIB);
                      lib->upward = (cmd->cmd == LC_LOAD_UPWARD_DYLIB);
                  }
                  break;
              }
              cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
          }
      }
    }
    

    dyld通过读取当前Mach-O文件的Load Commonds段来获取到它依赖的动态库,涉及到的字段有LC_LOAD_DYLIBLC_LOAD_WEAK_DYLIBLC_REEXPORT_DYLIBLC_LOAD_UPWARD_DYLIB,遍历时通过&libs[]记录。而影响Load Commonds中动态库顺序的是Xcode中Link Binary With Libraries的顺序。下图是上文Demo工程的Mach-O中Load Commonds的示例截图:

    Load Commonds

好了,我们通过实验和源码的方式了解到了动态库的加载顺序:1)先从当前image文件(可执行文件/动态库)的Load Commonds中获取动态库的信息(包括顺序,名字,路径等);2)然后循环加载它依赖的动态库;3)加载完后,再根据当前image依赖的动态库列表递归加载各自依赖的动态库。

3.填坑篇

文章开始我们就挖了一个坑:实验的方式是建立在“load方法的调用顺序当做是动态库的加载顺序”这一假设上的,那么这一假设成立吗?
从上文中+[ClassF load]的调用栈可知:load 方法的调用是在initializeMainExecutable()中,在link()之后,initializeMainExecutable()后又调用ImageLoader::runInitializers()ImageLoader::processInitializers(),我们看下代码:

  • ImageLoader::processInitializers中调用了ImageLoader::recursiveInitialization,从名字看进行了递归方式的初始化。
    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.images[i]->recursiveInitialization(context, thisThread, images.images[i]->getPath(), timingInfo, ups);
      }
      // If any upward dependencies remain, init them.
      if ( ups.count > 0 )
          processInitializers(context, thisThread, timingInfo, ups);
     }
    
  • ImageLoader::recursiveInitialization,截取部分代码:
    void ImageLoader::recursiveInitialization(const LinkContext& context, mach_port_t this_thread, const char* pathToInitialize,
                                            InitializerTimingList& timingInfo, UninitedUpwards& uninitUps)
    {
      
      //优先初始化fDepth深的库
      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.images[uninitUps.count] = dependentImage;
                  uninitUps.count++;
              }
              else if ( dependentImage->fDepth >= fDepth ) {
                  dependentImage->recursiveInitialization(context, this_thread, libPath(i), timingInfo, uninitUps);
              }
          }
      }
    
    
      // 告诉objc我们准备初始化image了
      uint64_t t1 = mach_absolute_time();
      fState = dyld_image_state_dependents_initialized;
      oldState = fState;
      context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);
    
      // 初始化image
      bool hasInitializers = this->doInitialization(context);
    
      // 告诉objc初始化完成
      fState = dyld_image_state_initialized;
      oldState = fState;
      context.notifySingle(dyld_image_state_initialized, this, NULL);
    
     //...
    }
    
    从上面的代码可以看出,初始化的动态库是从libImage()中获取,而libImage()的数据是在加载动态库的ImageLoader::recursiveLoadLibraries中的setLibImage()进行保存的。通过比较fDepth,优先初始化fDepth大的库,我们再看下fDepth的赋值:
  • ImageLoader::recursiveUpdateDepth
    unsigned int ImageLoader::recursiveUpdateDepth(unsigned int maxDepth)
    {
      // the purpose of this phase is to make the images sortable such that 
      // in a sort list of images, every image that an image depends on
      // occurs in the list before it.
      if ( fDepth == 0 ) {
          // break cycles
          fDepth = maxDepth;
          
          // get depth of dependents
          unsigned int minDependentDepth = maxDepth;
          for(unsigned int i=0; i < libraryCount(); ++i) {
              ImageLoader* dependentImage = libImage(i);
              if ( (dependentImage != NULL) && !libIsUpward(i) ) {
                  unsigned int d = dependentImage->recursiveUpdateDepth(maxDepth);
                  if ( d < minDependentDepth )
                      minDependentDepth = d;
              }
          }
      
          // make me less deep then all my dependents
          fDepth = minDependentDepth - 1;
      }
      
      return fDepth;
    }
    
    在整个依赖树的关系中,层级越深,fDepth的值越大,ImageLoader::recursiveUpdateDepth的调用在ImageLoader::recursiveLoadLibraries之后,通过上面的分析过程我们知道:在加载动态库时将动态库的信息通过setLibImage()保存,加载完成后dyld根据依赖关系递归地给各个库都计算了依赖深度:fDepth,在初始化的时候递归地根据fDepth进行初始化,每初始化一个image都会使用 context.notifySingle()通知Objc通过load_images()调用load方法。从分析结果看:不同库中的load方法的调用顺序可以当做是动态库的加载顺序。(dyld过程比较复杂,本人水平有限,如有错误敬请指出~)
dyld_main.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349