Hook所有+load方法(包括Category)
本文仅供学习参考,但介绍的方法性能不好。推荐使用 https://github.com/huakucha/TTAnalyzeLoadTime ,这个性能会更好。
大概两年前刚开始做性能优化工作,为了Hook所有+load方法,是用Hopper先列出所有+load,然后使用CaptainHook在动态库中逐个指定类名来Hook每一个+load方法。写了一篇文章,先后发表到了内网(ATA)和博客,博客文章地址是: https://everettjf.github.io/2017/01/06/a-method-of-hook-objective-c-load/ 。
这个方法有两个缺点:
- 需要先静态分析(使用Hopper)来看到所有+load方法,或者使用objc runtime的方法获取所有包含+load方法的类名。很麻烦。
- 对Category中的+load方法,没有办法Hook。
当时也意识到了这两个缺点,但迫于时间也凑合够用了。
时隔两年,想来了结这个心愿。这篇文章就把这两个缺点弥补上,实现:Hook所有的+load方法,包括Category中实现的+load方法。
目的
假设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 1 2 3 4这样的方法,适配N个Category的情况。
工程代码
代码初期编写时参考了佳乐同学的代码 https://github.com/joy0304/Joy-Demo/blob/master/HookLoad/LDAPMLoadMonitor.m,但佳乐同学的代码没有处理Category的情况。
总结
两年,眨眼而过,而是是非非,却历历在目。一念起万水千山,一念灭沧海桑田。勇往直前,少年。
欢迎关注订阅号「客户端技术评论」:
补充
同事告知Hook所有+load有性能更好的方法,但不便于公开了了。哈。