美文网首页
iOS-OC Aspects - 源码阅读

iOS-OC Aspects - 源码阅读

作者: 洧中苇_4187 | 来源:发表于2020-07-04 01:16 被阅读0次

一. Aspects 简介 翻译自github

  1. Aspects 这个框架 就是用来做方法交换的,它可以做到为每一个存在的类 或者实例对象 添加方法;你可以将自己的代码添加到方法的前面/后面/取代原有的方法,
  2. 它的原理就是通过动态创建一个子类继承你需要被hook的类,这个有点像KVO,严格地说,我不建议在生产代码中使用这些方法 ,我们在PSPDFKit中使用它进行部分测试模拟,PSPDFKit是一个与Dropbox或Evernote等应用一起发布的iOS PDF框架,它对于快速破解一些东西也非常有用。
  3. aspect使用_objc_msgForward,这会导致其他使用消息转发的代码出现问题。
  4. aspect使用Objective-C消息转发与消息挂钩。会有少量的额外开销。不要给调用很多的方法添加,是指那些每秒会调用1000次的视图/控制器代码

二. 什么是 OOP/AOP

OOP: OOP就是我们常说的 面向对象编程
AOP: AOP(Aspect Oriented Programming)是对OOP的补充和延续,中文翻译为 面向切面编程,可以通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能的一种技术.

简单来说AOP的主要使用场景就是日志记录,性能统计,安全控制,事务处理,异常处理,它的角色在OC中有点像runtime,动态添加方法的意思,

三 . Aspects 简单使用

使用非常简单,option有三个选项AspectPositionInstead,AspectPositionBefore,AspectPositionAfter,分别是替换方法,在方法前执行block,在方法后执行block

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.title = @"ViewController";
    
    [self aspect_hookSelector:@selector(testTimer) 
     withOptions:AspectPositionInstead 
     usingBlock:^{
        NSLog(@"%s",__func__);
    } error:nil];
    
}

- (void)testTimer{
    NSLog(@"%s",__func__);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [self testTimer];
}
@end

还可以监听手势的改变
[_singleTapGesture aspect_hookSelector:@selector(setState:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo) {
    NSLog(@"%@: %@", aspectInfo.instance, aspectInfo.arguments);
} error:NULL];

四. 进入正题,阅读源码,我们能学到什么?

  1. 当某个属性在当前方法没有用到,而想再调用的时候使用,为了不让编译器报错 使用 __unused消除警告
typedef struct _AspectBlock {
    __unused Class isa;
    XXXXXX
} *AspectBlockRef;

  1. respondsToSelectorinstancesRespondToSelector 的区别,举例说明
有一个类,结构是这样的,并实现了它自己的两个方法
@interface MJPerson : NSObject
- (void)instancePerson;
+ (void)ClassPerson;
@end
在控制器实现如下代码
- (void)viewDidLoad{
    [super viewDidLoad];
    MJPerson *person = [MJPerson new];
    if ([person respondsToSelector:@selector(instancePerson)]) {
         会响应
    }
    if ([person.class respondsToSelector:@selector(instancePerson)]) {
        不会响应
    }
    if ([person respondsToSelector:@selector(ClassPerson)]) {
       不会响应
    }
    if ([person.class respondsToSelector:@selector(ClassPerson)]) {
       会响应
    }

   if ([person.class instancesRespondToSelector:@selector(instancePerson)]) {
       会响应
    }
    if ([person.class instancesRespondToSelector:@selector(ClassPerson)]) {
        不会响应
    }
}

结论:
1>respondsToSelector是对象方法,由类的实例来调用,对象方法,类方法,如果都实现,则都可以响应
2>instancesRespondToSelector是类方法,由类调用,对象方法,类方法,如果都实现,只响应对象方法.

有个疑问,既然respondsToSelector实例方法,类方法是否响应都能判断,那为什么还要搞一个只能判断实例方法能否响应的它呢instancesRespondToSelector???,暂时我还不知道

验证的时候,我看到了这个,为什么MJPerson类能调用respondsToSelector

MJPerson这个类到底是调的哪个方法 难道类能调用实例方法??? 我想验证一下,于是这是我一开始的思路 ---- 方法交换
void (*ori_Response)(id, SEL);创建一个C方法


- (void)yp_respondsToSelector:(SEL)aSelector{
    NSLog(@"哈哈 %s",__func__);
    //怎么调回原来的方法 - respondsToSelector:

    if(ori_Response) {调用原来的实现,保证原来的逻辑不会受影响
        ori_Response([NSObject class], @selector(respondsToSelector:));
    }然而这段代码会崩会崩会崩会崩会崩
    
    [self yp_respondsToSelector:aSelector];
}

+ (void)load{

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method myMethod = class_getInstanceMethod([ViewController class], @selector(yp_respondsToSelector:));
        Method originMethod = class_getInstanceMethod([NSObject class], @selector(respondsToSelector:));

        //保存方法的原有实现
        ori_Response = (void(*)(id, SEL))method_getImplementation(originMethod);

//        Method originMethod1 = class_getClassMethod([NSProxy class], @selector(respondsToSelector:));

        method_exchangeImplementations(myMethod, originMethod);
    });
}

既然这个不行,那就换一个,直接打印方法的内存地址不就好了吗

(lldb) po [MJPerson respondsToSelector:@selector(ClassPerson)]
YES

(lldb) p class_getInstanceMethod([ViewController class], @selector(respondsToSelector:))
(Method) $1 = 0x00007fff87840788
(lldb) p class_getInstanceMethod([NSObject class], @selector(respondsToSelector:))
(Method) $2 = 0x00007fff87840788
(lldb) p class_getInstanceMethod([NSProxy class], @selector(respondsToSelector:))
(Method) $3 = 0x00007fff809d2ee0
(lldb) p class_getClassMethod([NSObject class], @selector(respondsToSelector:))
(Method) $4 = 0x00007fff8783ffe0
(lldb) p class_getClassMethod([NSProxy class], @selector(respondsToSelector:))
(Method) $7 = 0x00007fff809d3350

所以结论是,它最终调用了实例方法,其实我也有猜到会是这样,记得我这篇文章有说,为什么类能调用对象方法,结果就在这根线↓↓↓

当查找类方法找到NSObject元类这里还是没有,那么最后就会从这根superclass指针,到自己的类对象里去找,在这里找到的肯定是NSObject的对象方法了
  1. NSAssert/NSCAssert 和 NSParameterAssert / NSCparameterAssert 的区别是前者是对条件断言, 后者只是对参数是否存在的断言.

  2. 关于属性 和 方法编码objc Type Encodings,这块内容比较多,先贴出苹果关于属性编码的 官方文档, 官方文档1我在 MJExtension源码分析里面也有提及过,首先我们先来看看属性是怎么编码的,

@interface ViewController ()
@property(nonatomic,copy)NSString *name;
@property(nonatomic,strong)NSString *name1;

@property (nonatomic, copy) NSMutableArray *mArr;
@property(nonatomic,copy)NSMutableArray *arrayM;
@property(nonatomic,strong)NSTimer *timer;
@property (nonatomic, strong) NSString *str;

@property(nonatomic,strong)MJPerson *person;
@property(nonatomic,copy)id (^testBlock)(NSString *tmpStr,BOOL isSuccess);
@end

- (void)viewDidLoad{
    unsigned int count = 0;
    objc_property_t *properties = class_copyPropertyList([self class], &count);
    
    for (int i = 0; i < count; i++) {
        const char *attr = property_getAttributes(properties[I]);
        NSLog(@"%s",attr);
    }
    free(properties);
}

打印结果
T@"NSString",C,N,V_name
T@"NSString",&,N,V_name1
T@"NSMutableArray",C,N,V_mArr
T@"NSMutableArray",C,N,V_arrayM
T@"NSTimer",&,N,V_timer
T@"NSString",&,N,V_str
T@"MJPerson",&,N,V_person
T@?,C,N,V_testBlock

@ :对象(无论是静态类型 还是 id 类型)
C :An unsigned char
N : nonatomic,非原子属性

值得注意的是block的编码,
? : 任意返回值,

再来看看方法是怎么编码的,无关代码省略

- (void)viewDidLoad{
    [super viewDidLoad];
     unsigned int outCount = 0;
     Method *methods = class_copyMethodList([self class], &outCount);
     for (int i = 0; i < outCount; i++) {
          SEL name = method_getName(methods[I]);
          const char *attr = method_getTypeEncoding(methods[I]);
          NSLog(@"%s-%s",name,attr);
     }
    free(methods);
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
}
- (void)testArray{
}
- (void)testString{
}
- (void)test{
}
- (void)testABC:(NSString *)string arg2:(int)arg2{
}
- (void)testABC:(UIView *)view{
}
- (void)testABC{
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
}
+(void)testClassClass{
    NSLog(@"self.arrayM :");
}

注意的一点是:打印的是[self class]的方法,所以类方法testClassClass 不会打印,因为类方法列表在类对象里.

打印结果(代码太多,举例几个比较有代表性的):
viewDidLoad-v16@0:8
observeValueForKeyPath:ofObject:change:context:-v48@0:8@16@24@32^v40
setTestBlock:-v24@0:8@?16
testBlock-@?16@0:8

viewDidLoad  返回值为void,所以第一个参数是v,方法都有两个隐式参数self,_cmd,
第0个self,占8个字节,@ -->OC对象
第8位开始是 _cmd占 8位, : -->方法选择器SEL
总共16位 
所以 viewDidLoad的编码格式 是 v16@0:8

observeValueForKeyPath:ofObject:change:context:
最后一个参数 context是指针类型,它的编码格式是  ^v
testBlock  ? --->返回值是id类型
上面编码的事情说这么多,就是为了这个做准备,Aspect重写了block结构体,源码先贴在这,我还没有完全理解 ---今天在公司的项目里看到了类似的用法哈哈哈...
// Block internals.
typedef NS_OPTIONS(int, AspectBlockFlags) {
    AspectBlockFlagsHasCopyDisposeHelpers = (1 << 25),保存block是否有析构函数的标志位
    AspectBlockFlagsHasSignature          = (1 << 30)保存block是否有方法签名的标志位
};
typedef struct _AspectBlock {//真实block的内部结构,
    __unused Class isa;
    AspectBlockFlags flags;
    __unused int reserved;
    void (__unused *invoke)(struct _AspectBlock *block, ...);
    struct {
        unsigned long int reserved;保留字段
        unsigned long int size; block的大小
        // requires AspectBlockFlagsHasCopyDisposeHelpers
        void (*copy)(void *dst, const void *src); copy函数
        void (*dispose)(const void *); 析构函数
        // requires AspectBlockFlagsHasSignature
        const char *signature; 方法签名
        const char *layout; 布局
    } *descriptor;
    // imported variables
} *AspectBlockRef;

static NSMethodSignature *aspect_blockMethodSignature(id block, NSError **error) {
    AspectBlockRef layout = (__bridge void *)block;
    if (!(layout->flags & AspectBlockFlagsHasSignature)) {
        NSString *description = [NSString stringWithFormat:@"The block %@ doesn't contain a type signature.", block];
        AspectError(AspectErrorMissingBlockSignature, description);
        return nil;
    }
    void *desc = layout->descriptor;
    desc += 2 * sizeof(unsigned long int); 将指针移动到析构函数的位置
    if (layout->flags & AspectBlockFlagsHasCopyDisposeHelpers) {判断是否有析构函数标志位
        desc += 2 * sizeof(void *);//有析构函数,则将指针的值 +2个指针的大小
    }
    if (!desc) {
        NSString *description = [NSString stringWithFormat:@"The block %@ doesn't has a type signature.", block];
        AspectError(AspectErrorMissingBlockSignature, description);
        return nil;
    }
    NSLog(@"yp_desc:   %@",desc);
    const char *signature = (*(const char **)desc);
    return [NSMethodSignature signatureWithObjCTypes:signature];
}

补充

我们知道block的结构大致如下

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

static struct __ViewController__viewDidLoad_block_desc_0 {
  size_t reserved;
  size_t Block_size;
}

struct __ViewController__viewDidLoad_block_impl_0 {
  struct __block_impl impl;
  struct __ViewController__viewDidLoad_block_desc_0* Desc;
  __ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

要理解Aspects上述对block的重写就要说清楚一个知识点,其实block会根据自己的类型,结构里会增加一些结构体,

enum {
    BLOCK_DEALLOCATING =      (0x0001),  // runtime  正在 dealloc
    BLOCK_REFCOUNT_MASK =     (0xfffe),  // runtime  引用计数掩码,即从第 1 ~ 15 位是用来存引用计数的,第 0 位上面已经被用了
    BLOCK_NEEDS_FREE =        (1 << 24), // runtime  需要释放,即它现在在堆上
    BLOCK_HAS_COPY_DISPOSE =  (1 << 25), // compiler 是否有 copy / dispose 函数,copy 和 dispose 在 desc 中
    BLOCK_HAS_CTOR =          (1 << 26), // compiler: helpers have C++ code block 有 C++ 的构造器
    BLOCK_IS_GC =             (1 << 27), // runtime  用了 GC,这个不用管,GC 已经被淘汰
    BLOCK_IS_GLOBAL =         (1 << 28), // compiler 是否处于全局区
    BLOCK_USE_STRET =         (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
                                         //    返回值是否在栈上,如果没有签名,则它一定是 0
    BLOCK_HAS_SIGNATURE  =    (1 << 30), // compiler 是否有签名,签名是描述 block 的参数和返回值的一个字符串
    BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31)  // compiler 是否有扩展布局
};

这些定义标记block的各种特性,其中有对应三个结构体,代表的意义如下
BLOCK_DESCRIPTOR_1 block的大小,
BLOCK_DESCRIPTOR_2 copy功能 和 析构函数
BLOCK_DESCRIPTOR_3 方法签名 和 扩展布局
Block的图形结构如下:BLOCK_DESCRIPTOR_2 , BLOCK_DESCRIPTOR_3这两个结构体可能没有,具体要看block的类型

image.png

Aspect将这几个struct写在了block里面,flags&对应的枚举定义值,就能得到相应的属性值,例如__block_impl->Flags & BLOCK_HAS_SIGNATURE如果 & 出来的结果为真,那就证明这个block是有方法签名的;那我怎么去拿方法签名呢???

一般来说, BLOCK_DESCRIPTOR_1 都是有值的,所以我们直接判断BLOCK_DESCRIPTOR_2是否有值,
int32_t _pointer = BLOCK_DESCRIPTOR_1; 指向block_size这个结构体的首地址
 _pointer = _pointer +2 * sizeOf(int32_t)
if(block->Flags & BLOCK_HAS_COPY_DISPOSE){
  _pointer = _pointer +2 * sizeOf(int32_t) 让指针下移两个单位,因为这个里面包含两个指针的地址空间,
}

if(block->Flags & BLOCK_HAS_SIGNATURE){此时_pointer的指针就指向了signature
  const char signature = **_pointer;这个就是签名
}

对应到Aspect中的源码就是这段 image.png
#define BLOCK_DESCRIPTOR_1 1
struct Block_descriptor_1 { // 和 TestBlock 中 desc 的结构是一样的
 uintptr_t reserved;
 uintptr_t size; // block 的大小,这个大小应该是包括 Block_layout、Block_descriptor_1、Block_descriptor_2、Block_descriptor_3 的总大小
};

#define BLOCK_DESCRIPTOR_2 1
struct Block_descriptor_2 {
 // requires BLOCK_HAS_COPY_DISPOSE 必须有 BLOCK_HAS_COPY_DISPOSE
 // copy helper 和 dispose helper
 // copy helper 用于帮助拷贝成员变量;dispose helper 用于帮助释放成员变量
 void (*copy)(void *dst, const void *src);
 void (*dispose)(const void *);
};

#define BLOCK_DESCRIPTOR_3 1
struct Block_descriptor_3 {
 // requires BLOCK_HAS_SIGNATURE
 const char *signature;  // 签名,是个 char * 字符串,描述 block 的参数和返回值
 const char *layout;     // 扩展布局。contents depend on BLOCK_HAS_EXTENDED_LAYOUT
                         // #疑问:这个变量很让人疑惑,按 _Block_layout() 函数的意思,它有可能存的是 GC layout,
                         //       扩展布局和 GC layout 是什么关系呢??
};

1.关于block的知识点 可以访问者两个博客,写的很不错 CSDN博客,
2.这篇简书是我看过关于block最详细的
3.block实现的 官方源码 但是下载不了
4.这是我git上找到的 block底层源码

自己写了个 Demo 看官可以down下来看看.
一个以前没用过的API
NSUInteger index = [array indexOfObjectIdenticalTo:@"abc6"];//一次性返回给你index

相关文章

  • iOS-OC Aspects - 源码阅读

    一. Aspects 简介 翻译自github [https://github.com/steipete/Aspe...

  • Aspects 源码阅读

    Aspects 作为Object-C最受欢迎的AOP(面向切片编程)框架,提供在原方法执行之前(before)或者...

  • Aspects源码浅析

    Aspects源码浅析 同步发布到博客地址Aspects源码浅析 Aspects 可以很方便的让我们 hook 要...

  • Aspects源码解析

    Aspects 源码:https://github.com/steipete/Aspects 基本概述,此框架提供...

  • iOS源码阅读 - Aspects

    什么是Aspects Aspects是一个开源的的库,面向切面编程,它能允许你在每一个类和每一个实例中存在的方法里...

  • Aspects 源码解读

    Aspects 源码解读 1.Aspects简介 Aspects是一种面向切面编程,相对于继承而已,无需改动目标源...

  • Aspects改进尝试

    背景 一个库:Aspects两篇文章:面向切面编程之 Aspects 源码解析及应用消息转发机制与Aspects源...

  • AOP 面向切向编程(Aspect)

    转载:从 Aspects 源码中我学到了什么?

  • iOS 引入 Aspects 吧,迟早会用到!

    引入 Aspects 吧,迟早会用到! 关于 AOP 在 iOS 中的应用,以及 Aspects 的源码分析文章...

  • 值得一读的源码解析

    搜集了一些值得阅读的第三方源码,供大家一起学习哒~持续更新 ...... Aspects

网友评论

      本文标题:iOS-OC Aspects - 源码阅读

      本文链接:https://www.haomeiwen.com/subject/mohcqktx.html