一种 hook objective c +load 的方法

iOS有以下四种方法可方便的在premain阶段执行代码:

1. Objective C类的+load方法
2. C++ static initializer
3. C/C++ __attribute__(constructor) functions 
4. 动态库中的上面三种方法

所有类的+load方法是在main函数之前、在主线程,以串行方式调用。 因此,任何一个+load方法的耗时大小将直接影响到App的启动耗时。

先看Objective C Runtime

/***********************************************************************
* call_class_loads
* Call all pending class +load methods.
* If new classes become loadable, +load is NOT called for them.
*
* Called only by call_load_methods().
**********************************************************************/
static void call_class_loads(void)
{
    int i;
    
    // Detach current loadable list.
    struct loadable_class *classes = loadable_classes;
    int used = loadable_classes_used;
    loadable_classes = nil;
    loadable_classes_allocated = 0;
    loadable_classes_used = 0;
    
    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
        Class cls = classes[i].cls;
        load_method_t load_method = (load_method_t)classes[i].method;
        if (!cls) continue; 

        if (PrintLoading) {
            _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
        }
        (*load_method)(cls, SEL_load);
    }
    
    // Destroy the detached list.
    if (classes) free(classes);
}


直接通过遍历loadable_classes全局变量,逐个调用。

全局变量的定义如下:


// List of classes that need +load called (pending superclass +load)
// This list always has superclasses first because of the way it is constructed
static struct loadable_class *loadable_classes = nil;
static int loadable_classes_used = 0;
static int loadable_classes_allocated = 0;

再看下文档

The order of initialization is as follows:
- All initializers in any framework you link to.
- All +load methods in your image.
- All C++ static initializers and C/C++ __attribute__(constructor) functions in your image.
- All initializers in frameworks that link to you.

如何hook

由于+load方法调用时机已经很早,早于 C++ static initializer等,但晚于framework(动态库),那我们就可以把hook的代码写到动态库中,也就可以做到在主程序的 loadable_classes 全局变量初始化之前就把+load hook掉。

代码

创建一个动态库,使用CaptainHook (https://github.com/rpetrich/CaptainHook ,只有一个头文件,使用也很简单)。

#import "CaptainHook.h"


CHDeclareClass(MyClass);
CHClassMethod0(void, MyClass, load){
    CFTimeInterval start = CFAbsoluteTimeGetCurrent();
    
    CHSuper0(MyClass,load);
    
    CFTimeInterval end = CFAbsoluteTimeGetCurrent();
    // output: end - start
}

__attribute__((constructor)) static void entry(){
    NSLog(@"dylib loaded");
    
    CHLoadLateClass(MyClass);
    CHHook0(MyClass, load);
}

这样,把这个动态库链接到App主程序,就可以hook主程序中的 MyClass类的+load方法了。

如何列出程序所有+load方法

知道了如何Hook,但如何列出所有+load方法呢,代码中搜索太麻烦,那就通过Runtime获取:


int numClasses;
Class * classes = NULL;
    
classes = NULL;
numClasses = objc_getClassList(NULL, 0);
    
if (numClasses > 0 )
{
   classes = (Class*)malloc(sizeof(Class) * numClasses);
   numClasses = objc_getClassList(classes, numClasses);
   
   for(int idx = 0; idx < numClasses; ++idx){
       Class cls = *(classes + idx);
       
       const char *className = object_getClassName(cls);
       Class metaCls = objc_getMetaClass(className);
       
       BOOL hasLoad = NO;
       unsigned int methodCount = 0;
       Method *methods = class_copyMethodList(metaCls, & methodCount);
       if(methods){
           for(int j = 0; j < methodCount; ++j){
               Method method = *(methods + j);
               SEL name = method_getName(method);
               NSString *methodName = NSStringFromSelector(name);
               if([methodName isEqualToString:@"load"]){
                   hasLoad = YES;
                   break;
               }
           }
       }
       
       if(hasLoad){
           NSLog(@"has load : %@", NSStringFromClass(cls));
       }else{
//                NSLog(@"not has load : %@", NSStringFromClass(cls));
       }
   }
   
   free(classes);
}

遗漏

经过测试发现,如果一个类存在Category,则上面的方法只能hook Category中的+load,多个Category也只能hook一个。还需要研究下如何hook所有的。

TimeProfiler

通过TimeProfiler我们也可以进行分析,但经验告诉我们,日常使用中,用户启动App时,耗时经常存在“浮动”,如何把这些“浮动”的代码找出来,就可以用这个方法了。(当然这种hook本身也对性能有影响,个人或者小范围使用,肯定不要发布的。)

总结

这是一种逐个hook较麻烦的方法,一定有更简单的方法,抽时间研究。