利用Objective-C Runtimee的动态绑定特性,将一个方法的实现与另一个方法的实现进行交换。交换两个方法的实现一般写在分类的load方法里面,因为load方法会在程序运行前加载一次,而initialize方法会在类或者子类在 第一次使用的时候调用,当有分类的时候会调用多次。
应用场景:数据采集、生命周期、响应事件埋点。
load 的加载比main 还要早,所以如果我们再load方法里面做了耗时的操作,那么一定会影响程序的启动时间,所以
在load里面一定不要写耗时的代码
。 不要在load里面取加载对象,因为我们再load调用的时候根本就不确定我们的对象是否已经初始化了,所以不要去做对象的初始化
在Objective-C的Runtime中,一个类是用一个名为objc_class的结构体表示的,它的定义如下
https://developer.apple.com/documentation/objectivec/objective-c_runtime?language=objc
objc_class { Class _Nonnull isa OBJC_ISA_AVAILABILITY;#if !__OBJC2__ Class _Nullable super_class OBJC2_UNAVAILABLE; const char * _Nonnull name OBJC2_UNAVAILABLE; long version OBJC2_UNAVAILABLE; long info OBJC2_UNAVAILABLE; long instance_size OBJC2_UNAVAILABLE; struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE; struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE; struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE; struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;#endif} OBJC2_UNAVAILABLE;
从上述结构体中可以发现一个objc_method_list
指针,它保存着当前类的所有方法列表。同时,objc_method_list也是一个结构体,它的定义如下:
struct objc_method_list { struct objc_method_list * _Nullable obsolete OBJC2_UNAVAILABLE; int method_count OBJC2_UNAVAILABLE;#ifdef __LP64__ int space OBJC2_UNAVAILABLE;#endif /* variable length structure */ struct objc_method method_list[1] OBJC2_UNAVAILABLE;}
从上面的结构体中发现一个objc_method字段,它的定义如下:
struct objc_method { SEL _Nonnull method_name OBJC2_UNAVAILABLE; char * _Nullable method_types OBJC2_UNAVAILABLE; IMP _Nonnull method_imp OBJC2_UNAVAILABLE;}
从上面的结构体中还发现,一个方法由如下三部分组成:
method_name:方法名。
method_types:方法类型。
method_imp:方法实现。
使用Method Swizzling交换方法,其实就是修改了objc_method结构体中的mthod_imp,即改变了method_name和method_imp的映射关系
在这里插入图片描述
字符串驻留的优化技术:把一个不可变字符串对象的值拷贝给各个不同的指针。
Objective-C 选择器的名字也是作为驻留字符串储存在一个共享的字符串池当中的。
NSString *stra = @"Hello";
NSString *strb = @"Hello";
BOOL wt = (stra == strb); // YES
Selector(typedef struct objc_selector *SEL)
:在运行时 Selectors 用来代表一个方法的名字。Selector 是一个在运行时被注册(或映射)的C类型字符串,由编译器产生并且在类被加载进内存时由runtime自动进行名字和实现的映射。
选择器的比较
OBJC_EXPORT BOOL
class_respondsToSelector(Class _Nullable cls, SEL _Nonnull sel)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
- (BOOL)respondsToSelector:(SEL)aSelector;
if([device respondsToSelector:@selector(setSmoothAutoFocusEnabled:)]){
}
实现原理:在运行时改变 selector
在消息分发列表中的映射,通过交换 selector 来改变函数指针的引用(名字和实现的映射)。
在Objective-C中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。因此我们可以在运行时偷换selector对应的方法实现,达到给方法挂钩的目的。
在运行时,类(Class)维护了一个消息分发列表来保证消息的正确发送,每一个消息列表的入口是一个方法(Method),这个方法映射了一对键值对,其中键是这个方法的名字 selector(SEL),值是指向这个方法实现的函数指针 implementation(IMP)。
Method swizzling 修改了类的消息分发列表使得已经存在的 selector 映射到另一个实现 implementation,同时重命名了原生方法的实现对应一个新的 selector。
#import "NSObject+Swizzle.h"
#include "objc/runtime.h"
BOOL class_swizzleMethodAndStore(Class class, SEL original, IMP replacement, IMPPointer store);
BOOL class_swizzleMethodAndStore(Class class, SEL original, IMP replacement, IMPPointer store) {
IMP imp = NULL;
Method method = class_getInstanceMethod(class, original);
if (method) {
const char *type = method_getTypeEncoding(method);
imp = class_replaceMethod(class, original, replacement, type);
if (!imp) {
imp = method_getImplementation(method);
}
}
if (imp && store) {
*store = imp;
}
return (imp != NULL);
}
@implementation NSObject (Swizzle)
+ (BOOL)swizzle:(SEL)original with:(IMP)replacement store:(out IMPPointer)store {
return class_swizzleMethodAndStore(self, original, replacement, store);
}
@end
每个类都有一个方法列表,存放着selector的名字和方法实现的映射关系。IMP有点类似函数指针,指向具体的Method实现。
如果使用恰当,Method swizzling 还是很安全的,一个简单安全的方法是,仅在load中swizzle ,在 dispatch_once 中完成
。
+ (void)load {
Method originAddObserverMethod = class_getInstanceMethod(self, @selector(presentViewController:animated:completion:));
Method swizzledAddObserverMethod = class_getInstanceMethod(self, @selector(K_presentViewController:animated:completion:));
method_exchangeImplementations(originAddObserverMethod, swizzledAddObserverMethod);
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSArray *selStringsArray = @[@"shouldAutorotate",@"supportedInterfaceOrientations",@"preferredInterfaceOrientationForPresentation"];
[selStringsArray enumerateObjectsUsingBlock:^(NSString *selString, NSUInteger idx, BOOL *stop) {
NSString *mySelString = [@"sd_" stringByAppendingString:selString];
Method originalMethod = class_getInstanceMethod(self, NSSelectorFromString(selString));
Method myMethod = class_getInstanceMethod(self, NSSelectorFromString(mySelString));
method_exchangeImplementations(originalMethod, myMethod);
}];
});
}
因为load 方法调用在main之前,并且不需要我们初始化,+load`方法是在类或者类别被加载到Objective-C时执行。如果在+load类方法中实现MethodSwizzling,替换的方法会在应用程序运行的整个生命周期中生效,这也是我们期望的结果。 ps :
initialize 会在类第一次接收到消息的时候调用
有继承关系的对象swizzle时,先从父对象开始。这样才能保证子类方法拿到父类中的被swizzle的实现。因此在+(void)load中swizzle不会出错,就是因为load类方法会默认从父类开始调用。
+initialize本质为objc/_msgSend,如果子类没有实现initialize则会去父类查找,如果分类中实现,那么会覆盖主类,和runtime消息转发逻辑一样
initialize 会在类第一次接收到消息的时候调用
Since SELs are guaranteed to be unique and constant, you can use _cmd as the key for objc_setAssociatedObject().
https://blog.csdn.net/z929118967/article/details/78033027
/**
* Returns a specified instance method for a given class.
*
* @param cls The class you want to inspect.
* @param name The selector of the method you want to retrieve.
*
* @return The method that corresponds to the implementation of the selector specified by
* \e name for the class specified by \e cls, or \c NULL if the specified class or its
* superclasses do not contain an instance method with the specified selector.
*
* @note This function searches superclasses for implementations, whereas \c class_copyMethodList does not.
*/
OBJC_EXPORT Method _Nullable
class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
// 通过方法名获取方法指针
Method method1 = class_getInstanceMethod(cls, selector1);
OBJC_EXPORT BOOL
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp,
const char * _Nullable types)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
// 往类中添加 originalSEL 方法,如果已经存在会添加失败,并返回 NO
if (class_addMethod(self, originalSEL, originalIMP, originalMethodType)) {
// 如果添加成功了,重新获取 originalSEL 实例方法
originalMethod = class_getInstanceMethod(self, originalSEL);
}
#import "NSArray+Swizzle.h"
@implementation NSArray (Swizzle)
- (id)myLastObject
{
id ret = [self myLastObject]; //method_exchangeImplementations 之后,执行到这里将是调用LastObject 方法
NSLog(@"********** myLastObject *********** ");
return ret;
}
@end
Method ori_Method = class_getInstanceMethod([NSArray class], @selector(lastObject));
Method my_Method = class_getInstanceMethod([NSArray class], @selector(myLastObject));
method_exchangeImplementations(ori_Method, my_Method);
NSArray *array = @[@"0",@"1",@"2",@"3"];
NSString *string = [array lastObject];
NSLog(@"TEST RESULT : %@",string);
Method Swizzling 的封装 --------------------
/**
让所有继承自NSObject的子类,都具有Method Swizzling的能力。
*/
@interface NSObject (SASwizzler)
/**
交换方法名为 originalSEL 和方法名为 alternateSEL 两个方法的实现
@param originalSEL 原始方法名
@param alternateSEL 要交换的方法名称
*/
+ (BOOL)sensorsdata_swizzleMethod:(SEL)originalSEL withMethod:(SEL)alternateSEL;
/**
方式二
*/
+ (BOOL)swizzle:(SEL)original with:(IMP)replacement store:(out IMPPointer)store;
实现页面浏览事件全埋
利用Method Swizzling来交换UIViewController的-viewDidAppear:方法,然后在交换的方法中触发$AppViewScreen事件,以实现页面浏览事件的全埋点。
/**
模态只处理13以上的
*/
@interface UIViewController (ERPPresent13)
/**
Whether or not to set ModelPresentationStyle automatically for instance, Default is [Class K_automaticallySetModalPresentationStyle].
@return BOOL
*/
@property (nonatomic, assign) BOOL K_automaticallySetModalPresentationStyle;
/**
Whether or not to set ModelPresentationStyle automatically, Default is YES, but UIImagePickerController/UIAlertController is NO.
@return BOOL
*/
+ (BOOL)K_automaticallySetModalPresentationStyle;
@implementation UIViewController (ERPPresent13)
+ (void)load {
Method originAddObserverMethod = class_getInstanceMethod(self, @selector(presentViewController:animated:completion:));
Method swizzledAddObserverMethod = class_getInstanceMethod(self, @selector(K_presentViewController:animated:completion:));
method_exchangeImplementations(originAddObserverMethod, swizzledAddObserverMethod);
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSArray *selStringsArray = @[@"shouldAutorotate",@"supportedInterfaceOrientations",@"preferredInterfaceOrientationForPresentation"];
[selStringsArray enumerateObjectsUsingBlock:^(NSString *selString, NSUInteger idx, BOOL *stop) {
NSString *mySelString = [@"sd_" stringByAppendingString:selString];
Method originalMethod = class_getInstanceMethod(self, NSSelectorFromString(selString));
Method myMethod = class_getInstanceMethod(self, NSSelectorFromString(mySelString));
method_exchangeImplementations(originalMethod, myMethod);
}];
});
}
- (void)setK_automaticallySetModalPresentationStyle:(BOOL)K_automaticallySetModalPresentationStyle {
objc_setAssociatedObject(self, K_automaticallySetModalPresentationStyleKey, @(K_automaticallySetModalPresentationStyle), OBJC_ASSOCIATION_ASSIGN);
}
- (BOOL)K_automaticallySetModalPresentationStyle {
id obj = objc_getAssociatedObject(self, K_automaticallySetModalPresentationStyleKey);
if (obj) {
return [obj boolValue];
}
return [self.class K_automaticallySetModalPresentationStyle];
}
+ (BOOL)K_automaticallySetModalPresentationStyle {
if ([self isKindOfClass:[UIImagePickerController class]] || [self isKindOfClass:[UIAlertController class]]) {
return NO;
}
return YES;
}
- (void)K_presentViewController:(UIViewController *)viewControllerToPresent animated:(BOOL)flag completion:(void (^)(void))completion {
if (@available(iOS 13.0, *)) {
if (viewControllerToPresent.K_automaticallySetModalPresentationStyle) {
viewControllerToPresent.modalPresentationStyle = [QCTSession getModalPresentationStyleWith:viewControllerToPresent];
// viewControllerToPresent.modalPresentationStyle = UIModalPresentationOverFullScreen;// 1、后果:被怼了很惨,因为项目里有很多模态出来的VC是半透明,结果现在变成完全不透明,背景为黑色。2、 修复拜访记录日期控件PGDatePicker蒙版的问题:点击时间输入框,弹窗蒙版变成了空白
// viewControllerToPresent.modalPresentationStyle = UIModalPresentationAutomatic;// 1、后果:被怼了很惨,因为项目里有很多模态出来的VC是半透明,结果现在变成完全不透明,背景为黑色。2、 修复拜访记录日期控件PGDatePicker蒙版的问题:点击时间输入框,弹窗蒙版变成了空白
// 2、后遗症:因为把显示效果修改为:UIModalPresentationOverFullScreen;半透明,全屏覆盖的问题都得到完美解决。但是会引发一个新的问题:前一个页面的viewWillAppear:、viewDidAppear:也无法触发。例如:A Present B, 有时因为业务逻辑需要,必须在viewWillAppear, viewDidAppear里写一些代码,当B 调用dismiss方法的时候, A的这个两个方法不会触发,因此会有一些安全隐患。因此如果要求B 调用dismiss方法,A要执行viewWillAppear:、viewDidAppear:这个两个方法,这个时候要把B的modalPresentationStyle设置为:UIModalPresentationFullScreen;
// 3、 其他:如果要求 B既要半透明,dismiss时,A还要调用viewWillAppear:、viewDidAppear:。我的想法是在B 写一个block,在B调用dismiss之前,利用block回调A相关的业务逻辑代码。如果有其他更好的方法请告诉我。万分感谢!!
}
[self K_presentViewController:viewControllerToPresent animated:flag completion:completion];
} else {
// Fallback on earlier versions
[self K_presentViewController:viewControllerToPresent animated:flag completion:completion];
}
}
#pragma mark - ******** 2、【如果用户有打开手机的自动旋转功能 除了签名界面的页面,其余的都是竖屏】:
- (BOOL)sd_shouldAutorotate{
return YES;
}
- (UIInterfaceOrientationMask)sd_supportedInterfaceOrientations {
return UIInterfaceOrientationMaskPortrait;
}
-(UIInterfaceOrientation)sd_preferredInterfaceOrientationForPresentation{
return UIInterfaceOrientationPortrait;
}
- (BOOL)shouldAutorotate {
//CRMSignatureViewController4EditMerchantInfo
//判断类型,签名的上一个界面需要自动旋转回来
return YES;
}
- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
return UIInterfaceOrientationMaskPortrait;
}
-(UIInterfaceOrientation)preferredInterfaceOrientationForPresentation{
return UIInterfaceOrientationPortrait;
}
//
#小程序:iOS逆向