iOS底层-16:应用程序的加载

今天我们研究的是应用程序的加载过程,先做一下准备工作。
新建一个iphone工程,添加下面代码:

  • ViewController.m
+ (void)load{
    NSLog(@"%s",__func__);
}
  • main.m
int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    
    NSLog(@"333333");
    
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

__attribute__((constructor)) void lyFunc(){
    printf("来了 : %s \n",__func__);
}

运行查看这三个打印的顺序:

+[ViewController load]
来了 : lyFunc 
333333

程序加载过程这些方法的调用顺序是:
load ——> C++ ——> main
main函数作为程序的入口,为什确实最后执行的,我们需要研究的是在main函数之前,程序到底做了什么?

程序编译过程

编译过程

预编译:预编译又称为预处理,主要做些代码文本的替换工作,处理#开头的指令。比如拷贝#include、#import包含的文件代码,#define宏定义的替换,条件编译等,就是为编译做预备工作的阶段,主要处理#开始的预编译指令。生成.i文件

编译:将高级语言转换为机器能识别的汇编语言。生成.s文件

汇编:将汇编文件转换成机器码文件。生成.o文件

链接:对.o文件中引用的其他的库进行引入,生成可执行文件。

动态库 和 静态库

动静态库链接
  • 静态库:在链接阶段,将可汇编生成目标程序与它所引用的库一起链接打包到可执行文件中。此时静态库就不会改变了,静态库是直接拷贝,复制到目标程序。

    • 优点:编译完成,库文件就没有作用了,运行时可以直接使用
    • 缺点:不同的静态库中引用了相同的文件,如上图中的B、D,他就会拷贝两份相同的库文件,导致目标程序体积增大,对性能、内存会有一定的影响。
  • 动态库:编译时相同的库并不会拷贝到目标程序,而是在目标程序载入的时候,把那些相同的库用一份共享实例加载进来。

    • 优点
      • 减少打包的体积:共享内存,节约资源
      • 更新动态库可以直接更新程序:由于运行时才载入的特性,因此可以随时对下层库进行更换,而我们的代码却不用改变
    • 缺点:动态载入会带来一部分性能损失,如果当前环境缺少动态库,或者版本不正确,会导致程序无法运行

dyld加载流程

dyld是苹果的动态连接器,加载到程序中的动态库,主要靠dyld来链接管理。

下面我们通过源码分析dyld的加载流程,首先下载一份dyld源码,这篇文章使用的是750.6

拿到了源码,那么我们从哪里开始分析呢?借助一下上面的工程,在load方法处加上断点,打印堆栈信息:

  • bt

image.png

我们可以看到堆栈的第一个信息是_dyld_start,也可以直接在左边窗口查看。

  • 打开dyld源码,搜索_dyld_start找到__arm64__环境下的源码

往下查找发现它调起了dyldbootstrap::start方法

  • 搜索dyldbootstrap找到start方法
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);
}

其核心就是dyld::_main,返回调用了dyldmain函数。从左边的堆栈,也可以清晰的看到。其中macho_header就是Mach-o的头部。

  • 点击跳转到dyld::_main函数
    我们发现这个main函数有600多行代码,在这里就不详细解释,感兴趣的同学可以自行研究。_main函数主要做了以下事情:

1.环境变量配置
2.共享缓存:(UIKit、CoreFoundation等)
3.主程序的初始化
4.加入动态库
5.link主程序
6.link动态库
7.绑定弱引用
8.initialize初始化
9.main()

-环境变量配置

  • 共享缓存

  • 主程序初始化

  • 加载动态库

  • link主程序

  • link动态库

  • 弱引用绑定主程序

  • run initialize初始化方法

    image.png

  • 进入main()函数

    image.png

initialize

我们主要分析第八步,看看run all initializers里面做了什么?

  • 点击进入initializeMainExecutable源码

  • 点击runInitializers

    image.png

    主要是其中的processInitializers方法

  • 点击进入processInitializers

  • 进入recursiveInitialization源码


    重点查看notifySingledoInitialization方法

notifySingle
  • 进入notifySingle源码
    根据传入参数dyld_image_state_dependents_initializeddyld_image_state_initialized找到下段代码

  • sNotifyObjCInit
    直接搜索sNotifyObjCInit没有找到相应的源码,只找到一段赋值代码。我们继续跟registerObjCNotifiers流程

  • 搜索_dyld_objc_notify_register
    只找到一处调用的地方,接着我们搜索查看_dyld_objc_notify_register,并没有发现调用的地方。_dyld_objc_notify_register实际上是在objc源码中调用的.

  • objc-781源码中搜索_dyld_objc_notify_register


    _objc_init中找到,从这可知赋给sNotifyObjCInit的值其实就是load_images

  • 接着查看load_images源码


    主要放大是call_load_methods,调用所有+load方法

  • 点击call_load_methods

void call_load_methods(void)
{
    static bool loading = NO;
    bool more_categories;

    loadMethodLock.assertLocked();

    // Re-entrant calls do nothing; the outermost call will finish the job.
    if (loading) return;
    loading = YES;

    void *pool = objc_autoreleasePoolPush();

    do {
        //遍历所有的类,调用 + load 方法
        // 1. Repeatedly call class +loads until there aren't any more
        while (loadable_classes_used > 0) {
            call_class_loads();
        }
        //加载所有的category
        // 2. Call category +loads ONCE
        more_categories = call_category_loads();

        // 3. Run more +loads if there are classes OR more untried categories
    } while (loadable_classes_used > 0  ||  more_categories);

    objc_autoreleasePoolPop(pool);

    loading = NO;
}

看到这里想必大家都已经明白了,sNotifyObjCInit实际上调用的是libobjc里的load_images方法,作用是调用所有类的+load方法和加载category

doInitialization
  • 点击进入doInitialization

这里的doImageInitdoModInitFunctions这两个方法是关键。我们先看doImageInit

  • 点击进入doImageInit


    这里主要进行动态库的初始化工作,值得注意的是libSystem.dylib必须第一个初始化。

  • 点击进入doModInitFunctions


    这里主要是加载C++方法,我们可以在上面的demo中打一个断点进行验证。

    C++方法调用之前,确实调用了doModInitFunctions

至此我们整个的runInitializers执行完毕了。大致流程是initializeMainExecutable ——> runInitializers ——> processInitializers ——> processInitializers ——> recursiveInitialization ——> notifySingle ——> load_images ——> call_load_methods ——> call_class_loads ——> call_category_loads ——> doInitialization ——> doImageInit ——> doModInitFunctions ——> main()

不知道大家有没有注意到notifySingle调用load_images时,必须先由_objc_init ——> _dyld_objc_notify_register ——> registerObjCNotifiers中才给了notifySingle赋值load_images

这个赋值必须要在notifySingle调用之前,那么_objc_init在什么时候调用了呢?这个我们并没有找到。

探索_objc_init调用时机

我们在objc-781源码中_objc_init方法打上断点。查看堆栈情况


我们发现在_objc_init之前,调用了libSystem_initializerlibdispatch_initializer方法。


接下来我们要分别查看libSystemlibdispatch的源码。开源库地址我下载的是Libsystem-1281.100.1libdispatch-1173.100.2

libSystem

  • 打开libSystem源码,搜索libSystem_initializer
static void
libSystem_initializer(int argc,
              const char* argv[],
              const char* envp[],
              const char* apple[],
              const struct ProgramVars* vars)
{

    _libSystem_ktrace0(ARIADNE_LIFECYCLE_libsystem_init | DBG_FUNC_START);

    __libkernel_init(&libkernel_funcs, envp, apple, vars);
    _libSystem_ktrace_init_func(KERNEL);

    __libplatform_init(NULL, envp, apple, vars);
    _libSystem_ktrace_init_func(PLATFORM);

    __pthread_init(&libpthread_funcs, envp, apple, vars);
    _libSystem_ktrace_init_func(PTHREAD);

    _libc_initializer(&libc_funcs, envp, apple, vars);
    _libSystem_ktrace_init_func(LIBC);

    // TODO: Move __malloc_init before __libc_init after breaking malloc's upward link to Libc
    __malloc_init(apple);
    _libSystem_ktrace_init_func(MALLOC);

#if TARGET_OS_OSX
    /* <rdar://problem/9664631> */
    __keymgr_initializer();
    _libSystem_ktrace_init_func(KEYMGR);
#endif

    // No ASan interceptors are invoked before this point. ASan is normally initialized via the malloc interceptor:
    // _dyld_initializer() -> tlv_load_notification -> wrap_malloc -> ASanInitInternal

    _dyld_initializer();
    _libSystem_ktrace_init_func(DYLD);

    libdispatch_init();
    _libSystem_ktrace_init_func(LIBDISPATCH);

       ....已省略部分代码.....
}

发现在其中调用了__malloc_init、 _dyld_initializer、 libdispatch_init

libdispatch

  • 打开libdispatch源码,搜索libdispatch_init
void
libdispatch_init(void)
{
      ...已省略部分代码....
    dispatch_assert(sizeof(struct dispatch_apply_s) <=
            DISPATCH_CONTINUATION_SIZE);

    if (_dispatch_getenv_bool("LIBDISPATCH_STRICT", false)) {
        _dispatch_mode |= DISPATCH_MODE_STRICT;
    }

#if DISPATCH_USE_THREAD_LOCAL_STORAGE
    _dispatch_thread_key_create(&__dispatch_tsd_key, _libdispatch_tsd_cleanup);
#else
    _dispatch_thread_key_create(&dispatch_priority_key, NULL);
    _dispatch_thread_key_create(&dispatch_r2k_key, NULL);
    _dispatch_thread_key_create(&dispatch_queue_key, _dispatch_queue_cleanup);
    _dispatch_thread_key_create(&dispatch_frame_key, _dispatch_frame_cleanup);
    _dispatch_thread_key_create(&dispatch_cache_key, _dispatch_cache_cleanup);
    _dispatch_thread_key_create(&dispatch_context_key, _dispatch_context_cleanup);
    _dispatch_thread_key_create(&dispatch_pthread_root_queue_observer_hooks_key,
            NULL);
    _dispatch_thread_key_create(&dispatch_basepri_key, NULL);
#if DISPATCH_INTROSPECTION
    _dispatch_thread_key_create(&dispatch_introspection_key , NULL);
#elif DISPATCH_PERF_MON
    _dispatch_thread_key_create(&dispatch_bcounter_key, NULL);
#endif
    _dispatch_thread_key_create(&dispatch_wlh_key, _dispatch_wlh_cleanup);
    _dispatch_thread_key_create(&dispatch_voucher_key, _voucher_thread_cleanup);
    _dispatch_thread_key_create(&dispatch_deferred_items_key,
            _dispatch_deferred_items_cleanup);
#endif

#if DISPATCH_USE_RESOLVERS // rdar://problem/8541707
    _dispatch_main_q.do_targetq = _dispatch_get_default_queue(true);
#endif

    _dispatch_queue_set_current(&_dispatch_main_q);
    _dispatch_queue_set_bound_thread(&_dispatch_main_q);

#if DISPATCH_USE_PTHREAD_ATFORK
    (void)dispatch_assume_zero(pthread_atfork(dispatch_atfork_prepare,
            dispatch_atfork_parent, dispatch_atfork_child));
#endif
    _dispatch_hw_config_init();
    _dispatch_time_init();
    _dispatch_vtable_init();
    _os_object_init();
    _voucher_init();
    _dispatch_introspection_init();
}

最后我们看到libdispatch调用了_os_object_init

  • 继续搜索_os_object_init

    果然_objc_init就在其中,所有的猜想都得到了验证。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容

  • 本文的目的主要是分析dyld的加载流程 首先我们先运行个代码 来引入我们今天的主题~~ 运行结果: 运行程序,查看...
    北京_小海阅读 549评论 1 3
  • 前言 我们都知道,每个项目都会有一个入口 main() 函数,是不是应用程序加载时第一个调用的就是 main 函数...
    远方竹叶阅读 663评论 0 6
  • 首先我们来看一个案例,viewController添加load方法,main.m文件添加一个c++函数,思考一下l...
    HardCabbage阅读 521评论 0 0
  • 1. 理论基础速成 1.1 静态库与动态库 库是已写好的、供使用的 可复用代码,每个程序都要依赖很多基础的底层库。...
    卖馍工程师阅读 678评论 0 5
  • 表情是什么,我认为表情就是表现出来的情绪。表情可以传达很多信息。高兴了当然就笑了,难过就哭了。两者是相互影响密不可...
    Persistenc_6aea阅读 124,195评论 2 7