
/***********************************************************************
* 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;
苹果的官方文档对 +load 的说明如下:
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.
二、运用 CaptainHook hook 类方法 +load
由于 +load 方法调用时机已经很早,早于 C++ static initializer 等,但晚于 framework(动态库),那就可以把 hook 的代码写到动态库中,也就可以做到在主程序的 loadable_classes 全局变量初始化之前就把 +load hook 掉。创建一个动态库,使用 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 方法可以通过 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 一个;并且 CaptainHook 方法需要先静态分析(使用 Hopper)来看到所有 +load 方法,或者使用 objc runtime 的方法获取所有包含 +load 方法的类名,非常麻烦,那么该怎么处理和改进呢?
三、Hook 所有 +load 方法(包括 Category)
① hook 目的
假设 App 包含两个自动链接的动态库,文件如下:
我们的目的就是 hook 这三个 MachO 文件中的所有 Objective C +load 方法,并统计出耗时,打印出来。
② 新增动态库
为了让 Hook 代码加载的比这两个动态库早,需要新增一个动态库 LoadRuler.dylib,链接的顺序很重要,要把 LoadRuler 第一个链接(App 启动时也就会第一个加载,以及第一个执行 macho 中的 +load 方法):
③ 获取 App 的所有 MachO
首先获取所有加载的 MachO 可以这样:
static void AppendAllImagePaths(std::vector<std::string> & image_paths){
uint32_t imageCount = _dyld_image_count();
for(uint32_t imageIndex = 0; imageIndex < imageCount; ++imageIndex){
const char * path = _dyld_get_image_name(imageIndex);
image_paths.push_back(std::string(path));
}
}
然后可以根据路径区分出 App 中的所有 MachO(动态库和可执行的主二进制文件):
static void AppendProductImagePaths(std::vector<std::string> & product_image_paths){
NSString *mainBundlePath = [NSBundle mainBundle].bundlePath;
std::vector<std::string> all_image_paths;
AppendAllImagePaths(all_image_paths);
for(auto path: all_image_paths){
NSString *imagePath = [NSString stringWithUTF8String:path.c_str()];
if([imagePath containsString:mainBundlePath] ||[imagePath containsString:@"Build/Products/"]){
product_image_paths.push_back(path);
}
}
}
其中 Build/Products/ 是为了适配开发模式,例如上图的工程配置下 FirstDylib 的目录是在:
/Users/everettjf/Library/Developer/Xcode/DerivedData/LoadCostSample-amfsvwltyimldeaxbquwejweulqd/Build/Products/Debug-iphonesimulator/FirstDylib.framework/FirstDylib
为了把这种情况过滤出来,这里简单的通过 Build/Products 匹配下(没有用 DerivedData 是考虑到 DerivedData 目录在 Xcode 的设置中是可修改的)。
④ 获取所有类
unsigned int classCount = 0;
const char ** classNames = objc_copyClassNamesForImage(path.c_str(),&classCount);
for(unsigned int classIndex = 0; classIndex < classCount; ++classIndex) {
NSString *className = [NSString stringWithUTF8String:classNames[classIndex]];
Class cls = object_getClass(NSClassFromString(className));
关键代码如下:
@interface LoadRuler : NSObject
@end
@implementation LoadRuler
+(void)LoadRulerSwizzledLoad0{
LoadRulerBegin;
[self LoadRulerSwizzledLoad0];
LoadRulerEnd;
}
+(void)LoadRulerSwizzledLoad1{
LoadRulerBegin;
[self LoadRulerSwizzledLoad1];
LoadRulerEnd;
}
+(void)LoadRulerSwizzledLoad2{
LoadRulerBegin;
[self LoadRulerSwizzledLoad2];
LoadRulerEnd;
}
+(void)LoadRulerSwizzledLoad3{
LoadRulerBegin;
[self LoadRulerSwizzledLoad3];
LoadRulerEnd;
}
+(void)LoadRulerSwizzledLoad4{
LoadRulerBegin;
[self LoadRulerSwizzledLoad4];
LoadRulerEnd;
}
+(void)load{
PrintAllImagePaths();
SEL originalSelector = @selector(load);
Class rulerClass = [LoadRuler class];
std::vector<std::string> product_image_paths;
AppendProductImagePaths(product_image_paths);
for(auto path : product_image_paths){
unsigned int classCount = 0;
const char ** classNames = objc_copyClassNamesForImage(path.c_str(),&classCount);
for(unsigned int classIndex = 0; classIndex < classCount; ++classIndex){
NSString *className = [NSString stringWithUTF8String:classNames[classIndex]];
Class cls = object_getClass(NSClassFromString(className));
// 不要把自己hook了
if(cls == [self class]){
continue;
}
unsigned int methodCount = 0;
Method * methods = class_copyMethodList(cls, &methodCount);
NSUInteger currentLoadIndex = 0;
for(unsigned int methodIndex = 0; methodIndex < methodCount; ++methodIndex){
Method method = methods[methodIndex];
std::string methodName(sel_getName(method_getName(method)));
if(methodName == "load"){
SEL swizzledSelector = NSSelectorFromString([NSString stringWithFormat:@"LoadRulerSwizzledLoad%@",@(currentLoadIndex)]);
Method originalMethod = method;
Method swizzledMethod = class_getClassMethod(rulerClass, swizzledSelector);
BOOL addSuccess = class_addMethod(cls, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
// 添加成功,则说明不存在load。但动态添加的load,不会被调用。与load的调用方式有关
if(!addSuccess){
// 已经存在,则添加新的selector
BOOL didAddSuccess = class_addMethod(cls, swizzledSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if(didAddSuccess){
// 然后交换
swizzledMethod = class_getClassMethod(cls, swizzledSelector);
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
++currentLoadIndex;
}
}
}
}
}
@end
⑤ Category 的处理
工程中 FirstLoader 的类及几个 Category 是如下这样:
@implementation FirstLoader
+ (void)load{
NSLog(@"first +load");
usleep(1000 * 15);
}
@end
@implementation FirstLoader (FirstCategory)
+(void)load{
NSLog(@"first category +load for FirstLoader");
usleep(1000 * 45);
}
@end
@implementation FirstLoader (SecondCategory)
+ (void)load{
NSLog(@"second category +load for FirstLoader");
usleep(1000 * 55);
}
@end
Hopper 中看到 Category 中的 +load,最终的符号没有体现出来:
为了把一个类及对应 Category 中的所有 load 都 hook,上面的代码使用了 class_copyMethodList 或许所有类方法,然后逐个替换。为了代码实现的简单,可以创建 LoadRulerSwizzledLoad0、 LoadRulerSwizzledLoad1、 LoadRulerSwizzledLoad2、 LoadRulerSwizzledLoad3 等这样的方法,适配 N 个 Category 的情况。
四、完整示例
Objective C之Hook所有+load方法简单示例。欢迎分享,转载请注明来源:内存溢出
微信扫一扫
支付宝扫一扫
评论列表(0条)