iOS-琐碎知识点(二)

2,935 阅读12分钟

问题先行

1Runtime应用都有哪些?
2AOPOOP有什么区别?

Runtime

Objective-C是一个动态语言,这意味着它不仅需要一个编译器,也需要一个运行时系统来动态得创建类和对象、进行消息传递和转发,Runtime就是这个运行时系统,基本是用C汇编写的。

高级编程语言想要成为可执行文件需要先编译为汇编语言再汇编为机器语言,机器语言也是计算机能够识别的唯一语言,但是OC并不能直接编译为汇编语言,而是要先转写为纯C语言再进行编译和汇编的操作,从OCC语言的过渡就是由runtime来实现的。然而我们使用OC进行面向对象开发,而C语言更多的是面向过程开发,这就需要将面向对象的类转变为面向过程的结构体。

SEL(objc_selector)

Objc.h
/// An opaque type that represents a method selector.代表一个方法的不透明类型
typedef struct objc_selector *SEL;
A method selector is a C string that has been registered (or “mapped“) with the Objective-C runtime. Selectors generated by the compiler are automatically mapped by the runtime when the class is loaded.

其实selector就是个映射到方法的C字符串,你可以用 Objective-C 编译器命令@selector()或者 Runtime 系统的sel_registerName函数来获得一个 SEL 类型的方法选择器。
selector既然是一个string,我觉得应该是类似className+method的组合,命名规则有两条:同一个类,selector不能重复,不同的类,selector可以重复\

这也带来了一个弊端,我们在写C代码的时候,经常会用到函数重载,就是函数名相同,参数不同,但是这在Objective-C中是行不通的,因为selector只记了methodname,没有参数,所以没法区分不同的method

IMP

/// A pointer to the function of a method implementation. 指向一个方法实现的指针
typedef id (*IMP)(id, SEL, ...);

就是指向最终实现程序的内存地址的指针。
iOSRuntime中,Method通过selectorIMP两个属性,实现了快速查询方法及实现,相对提高了性能,又保持了灵活性。

Runtime应用

关联对象(Objective-C Associated Objects)给分类增加实例变量

下面实现一个UIViewCategory添加自定义属性defaultColor

#import "ViewController.h"
#import "objc/runtime.h"

@interface UIView (DefaultColor)

@property (nonatomic, strong) UIColor *defaultColor;

@end

@implementation UIView (DefaultColor)

@dynamic defaultColor;

static char kDefaultColorKey;

- (void)setDefaultColor:(UIColor *)defaultColor {
    objc_setAssociatedObject(self, &kDefaultColorKey, defaultColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (id)defaultColor {
    return objc_getAssociatedObject(self, &kDefaultColorKey);
}

@end

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    UIView *test = [UIView new];
    test.defaultColor = [UIColor blackColor];
    NSLog(@"%@", test.defaultColor);
}

@end

///
打印结果: 2018-04-01 15:41:44.977732+0800 ocram[2053:63739] UIExtendedGrayColorSpace 0 1

方法魔法(Method Swizzling)方法添加和替换和KVO实现

下面实现一个替换ViewControllerviewDidLoad方法的例子。

@implementation ViewController

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        //原有方法
        SEL originalSelector = @selector(viewDidLoad);
        //替换原有方法的新方法
        SEL swizzledSelector = @selector(jkviewDidLoad);
        
        Method originalMethod = class_getInstanceMethod(class,originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class,swizzledSelector);
        
        //先尝试給源SEL添加IMP,这里是为了避免源SEL没有实现IMP的情况
        BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
        if (didAddMethod) {
            //添加成功:说明源SEL没有实现IMP,将源SEL的IMP替换到交换SEL的IMP
            class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        }
        else {
            //添加失败:说明源SEL已经有IMP,直接将两个SEL的IMP交换即可
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

- (void)jkviewDidLoad {
    NSLog(@"替换的方法");
    
    [self jkviewDidLoad];
}

- (void)viewDidLoad {
    NSLog(@"自带的方法");
    
    [super viewDidLoad];
}

@end

swizzling应该只在+load中完成。 在Objective-C的运行时中,每个类有两个方法都会自动调用。+load是在一个类被初始装载时调用,+initialize是在应用第一次调用该类的类方法或实例方法前调用的。两个方法都是可选的,并且只有在方法被实现的情况下才会被调用。

swizzling应该只在dispatch_once 中完成,由于swizzling改变了全局的状态,所以我们需要确保每个预防措施在运行时都是可用的。原子操作就是这样一个用于确保代码只会被执行一次的预防措施,就算是在不同的线程中也能确保代码只执行一次。Grand Central Dispatch的 dispatch_once满足了所需要的需求,并且应该被当做使用swizzling的初始化单例方法的标准。
实现图解如下图。

image.png KVO实现

全称是Key-value observing,翻译成键值观察。提供了一种当其它对象属性被修改的时候能通知当前对象的机制。在MVC大行其道的Cocoa中,KVO机制很适合实现model和controller类之间的通讯。

KVO的实现依赖于Objective-C强大的Runtime,当观察某对象A时,KVO机制动态创建一个对象A当前类的子类,并为这个新的子类重写了被观察属性keyPathsetter方法。setter方法随后负责通知观察对象属性的改变状况。

Apple使用了isa-swizzling来实现KVO。当观察对象A时,KVO机制动态创建一个新的名为:NSKVONotifying_A的新类,该类继承自对象A的本类,且KVONSKVONotifying_A重写观察属性的setter方法,setter方法会负责在调用原setter方法之前和之后,通知所有观察对象属性值的更改情况。

  • NSKVONotifying_A类剖析
NSLog(@"self->isa:%@",self->isa);
NSLog(@"self class:%@",[self class]);

在建立KVO监听前,打印结果为:

self->isa:A
self class:A

在建立KVO监听之后,打印结果为:

self->isa:NSKVONotifying_A
self class:A

在这个过程,被观察对象的isa指针从指向原来的A类,被KVO机制修改为指向系统新创建的子类NSKVONotifying_A类,来实现当前类属性值改变的监听;所以当我们从应用层面上看来,完全没有意识到有新的类出现,这是系统“隐瞒”了对KVO的底层实现过程,让我们误以为还是原来的类。但是此时如果我们创建一个新的名为“NSKVONotifying_A”的类,就会发现系统运行到注册KVO的那段代码时程序就崩溃,因为系统在注册监听的时候动态创建了名为NSKVONotifying_A的中间类,并指向这个中间类了。

  • 子类setter方法剖析 KVO的键值观察通知依赖于NSObject的两个方法:willChangeValueForKey:didChangeValueForKey:,在存取数值的前后分别调用2个方法:被观察属性发生改变之前,willChangeValueForKey:被调用,通知系统该keyPath的属性值即将变更;当改变发生后, didChangeValueForKey: 被调用,通知系统该keyPath的属性值已经变更;之后, observeValueForKey:ofObject:change:context:也会被调用。且重写观察属性的setter 方法这种继承方式的注入是在运行时而不是编译时实现的。

KVO 为子类的观察者属性重写调用存取方法的工作原理在代码中相当于:

- (void)setName:(NSString *)newName { 
      [self willChangeValueForKey:@"name"];    //KVO 在调用存取方法之前总调用 
      [super setValue:newName forKey:@"name"]; //调用父类的存取方法 
      [self didChangeValueForKey:@"name"];     //KVO 在调用存取方法之后总调用
}

消息转发(热更新)解决Bug(JSPatch)

JSPatch 是一个 iOS动态更新框架,只需在项目中引入极小的引擎,就可以使用JavaScript调用任何Objective-C原生接口,获得脚本语言的优势:为项目动态添加模块,或替换项目原生代码动态修复bug。

关于消息转发,前面已经讲到过了,消息转发分为三级,我们可以在每级实现替换功能,实现消息转发,从而不会造成崩溃。JSPatch不仅能够实现消息转发,还可以实现方法添加、替换能一系列功能。

实现NSCoding的自动归档和自动解档

原理描述:用runtime提供的函数遍历Model自身所有属性,并对属性进行encodedecode操作。 核心方法:在Model的基类中重写方法:

- (id)initWithCoder:(NSCoder *)aDecoder {
    if (self = [super init]) {
        unsigned int outCount;
        Ivar * ivars = class_copyIvarList([self class], &outCount);
        for (int i = 0; i < outCount; i ++) {
            Ivar ivar = ivars[i];
            NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
            [self setValue:[aDecoder decodeObjectForKey:key] forKey:key];
        }
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder {
    unsigned int outCount;
    Ivar * ivars = class_copyIvarList([self class], &outCount);
    for (int i = 0; i < outCount; i ++) {
        Ivar ivar = ivars[i];
        NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
        [aCoder encodeObject:[self valueForKey:key] forKey:key];
    }
}

实现字典和模型的自动转换(MJExtension)

原理描述:用runtime提供的函数遍历Model自身所有属性,如果属性在json中有对应的值,则将其赋值。核心方法:在NSObject的分类中添加方法

- (instancetype)initWithDict:(NSDictionary *)dict {

    if (self = [self init]) {
        //(1)获取类的属性及属性对应的类型
        NSMutableArray * keys = [NSMutableArray array];
        NSMutableArray * attributes = [NSMutableArray array];
        /*
         * 例子
         * name = value3 attribute = T@"NSString",C,N,V_value3
         * name = value4 attribute = T^i,N,V_value4
         */
        unsigned int outCount;
        objc_property_t * properties = class_copyPropertyList([self class], &outCount);
        for (int i = 0; i < outCount; i ++) {
            objc_property_t property = properties[i];
            //通过property_getName函数获得属性的名字
            NSString * propertyName = [NSString stringWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
            [keys addObject:propertyName];
            //通过property_getAttributes函数可以获得属性的名字和@encode编码
            NSString * propertyAttribute = [NSString stringWithCString:property_getAttributes(property) encoding:NSUTF8StringEncoding];
            [attributes addObject:propertyAttribute];
        }
        //立即释放properties指向的内存
        free(properties);

        //(2)根据类型给属性赋值
        for (NSString * key in keys) {
            if ([dict valueForKey:key] == nil) continue;
            [self setValue:[dict valueForKey:key] forKey:key];
        }
    }
    return self;

}

AOPOOP

OOP:(面向对象编程)针对业务处理过程的实体及其属性行为进行抽象封装,以获得更加清晰高效的逻辑单元划分。
AOPAspect Oriented Programming,译为面向切面编程,是可以通过预编译的方式和运行期动态实现,在不修改源代码的情况下,给程序动态统一添加功能的技术。

AOP首先想到的应该就是Method Swizzle,下面来看一些主流AOP框架。

AspectsiOS上的一个轻量级AOP库。它利用Method Swizzling技术为已有的类或者实例方法添加额外的代码,使用起来是很方便:

/// Adds a block of code before/instead/after the current `selector` for a specific class.
///
/// @param block Aspects replicates the type signature of the method being hooked.
/// The first parameter will be `id<AspectInfo>`, followed by all parameters of the method.
/// These parameters are optional and will be filled to match the block signature.
/// You can even use an empty block, or one that simple gets `id<AspectInfo>`.
///
/// @note Hooking static methods is not supported.
/// @return A token which allows to later deregister the aspect.
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error;

/// Adds a block of code before/instead/after the current `selector` for a specific instance.
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error;

/// Deregister an aspect.
/// @return YES if deregistration is successful, otherwise NO.
id<AspectToken> aspect = ...;
[aspect remove];

Aspects提供了2个AOP方法,一个用于类,一个用于实例。在确定hook的方法之后,Aspects允许我们选择hook的时机是在方法执行之前,还是方法执行之后,甚至可以直接替换掉方法的实现。 image.png

官方强烈不推荐在生产环境中使用,这个是重点

ReactiveCocoaRxSwift利用KVO的运行时ISA-swizzle原理,动态创建子类、并重写相关方法,并且添加我们想要的方法,然后在这个方法中调用原来的方法,从而达到hook的目的。

ReactiveCocoa为例源码

internal func swizzle(_ pairs: (Selector, Any)..., key hasSwizzledKey: AssociationKey<Bool>) {

        // 动态创建子类
		let subclass: AnyClass = swizzleClass(self)

		ReactiveCocoa.synchronized(subclass) {
			let subclassAssociations = Associations(subclass as AnyObject)

			if !subclassAssociations.value(forKey: hasSwizzledKey) {
				subclassAssociations.setValue(true, forKey: hasSwizzledKey)

				for (selector, body) in pairs {
					let method = class_getInstanceMethod(subclass, selector)!
					let typeEncoding = method_getTypeEncoding(method)!

					if method_getImplementation(method) == _rac_objc_msgForward {
						let succeeds = class_addMethod(subclass, selector.interopAlias, imp_implementationWithBlock(body), typeEncoding)
						precondition(succeeds, "RAC attempts to swizzle a selector that has message forwarding enabled with a runtime injected implementation. This is unsupported in the current version.")
					} else {    
                        // 通过 block 生成一个新的 IMP,为生成的子类添加该方法实现。
						let succeeds = class_addMethod(subclass, selector, imp_implementationWithBlock(body), typeEncoding)
						precondition(succeeds, "RAC attempts to swizzle a selector that has already a runtime injected implementation. This is unsupported in the current version.")
					}
				}
			}
		}
	}

internal func swizzleClass(_ instance: NSObject) -> AnyClass {
	if let knownSubclass = instance.associations.value(forKey: knownRuntimeSubclassKey) {
		return knownSubclass
	}

	let perceivedClass: AnyClass = instance.objcClass
	let realClass: AnyClass = object_getClass(instance)!
	let realClassAssociations = Associations(realClass as AnyObject)

	if perceivedClass != realClass {
		// If the class is already lying about what it is, it's probably a KVO
		// dynamic subclass or something else that we shouldn't subclass at runtime.
		synchronized(realClass) {
			let isSwizzled = realClassAssociations.value(forKey: runtimeSubclassedKey)
			if !isSwizzled {
                // 重写类的 -class 和 +class 方法,隐藏真实的子类类型
				replaceGetClass(in: realClass, decoy: perceivedClass)
				realClassAssociations.setValue(true, forKey: runtimeSubclassedKey)
			}
		}

		return realClass
	} else {
		let name = subclassName(of: perceivedClass)
		let subclass: AnyClass = name.withCString { cString in
			if let existingClass = objc_getClass(cString) as! AnyClass? {
				return existingClass
			} else {
				let subclass: AnyClass = objc_allocateClassPair(perceivedClass, cString, 0)!
                // 重写类的 -class 和 +class 方法,隐藏真实的子类类型
				replaceGetClass(in: subclass, decoy: perceivedClass)
				objc_registerClassPair(subclass)
				return subclass
			}
		}

		object_setClass(instance, subclass)
		instance.associations.setValue(subclass, forKey: knownRuntimeSubclassKey)
		return subclass
	}
}

fishhook利用了MachO的动态绑定机制,苹果的共享缓存库不会被编译进我们的MachO文件,而是在动态链接(依靠动态连接器dyld)时才去重新绑定。苹果采用了PIC(Position-independent code)技术成功让C的底层也能有动态的表现:

  • 编译时在Mach-O文件_DATA段的符号表中为每一个被引用的系统C函数建立一个指针(8字节的数据,放的全是0),这个指针用于动态绑定时重定位到共享库中的函数实现。
  • 在运行时当系统C函数被第一次调用时会动态绑定一次,然后将Mach-O中的_DATA段符号表中对应的指针,指向外部函数(其在共享库中的实际内存地址)。 fishhook正是利用了PIC技术做了两个操作:
  • 将指向系统方法(外部函数)的指针重新进行绑定指向内部函数/自定义C函数。
  • 将内部函数的指针在动态链接时指向系统方法的地址。

总结
OOP实际上是对对象的属性和行为的封装,而AOP是处理某个步骤和阶段的,从中进行切面的提取。例如日志记录功能,如果我们单纯使用OOP,在每个方法里的开始、结束、异常的地方手动添加日志?如果使用AOP就可以借助代理完成这些重复的操作,就能够在逻辑过程中,降低各部分之间的耦合了。二者扬长补短,互相结合最好。

weak底层原理

创建
我们通过打开汇编Always Show Disassembly分析底层实现。 image.png image.png 入口为objc_initWeak

/** 
 * Initialize a fresh weak pointer to some object location. 
 * It would be used for code like: 
 *
 * (The nil case) 
 * __weak id weakPtr;
 * (The non-nil case) 
 * NSObject *o = ...;
 * __weak id weakPtr = o;
 * 
 * This function IS NOT thread-safe with respect to concurrent 
 * modifications to the weak variable. (Concurrent weak clear is safe.)
 *
 * @param location Address of __weak ptr. 
 * @param newObj Object ptr. 
 */
id
objc_initWeak(id *location, id newObj)
{
    if (!newObj) {
        *location = nil;
        return nil;
    }
    return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
        (location, (objc_object*)newObj);
}

注释描述的挺清楚的

  • location:表示__weak指针的地址
  • newObj:所引用的对象
static id 
storeWeak(id *location, objc_object *newObj)
{
    ASSERT(haveOld  ||  haveNew);
    if (!haveNew) ASSERT(newObj == nil);

    Class previouslyInitializedClass = nil;
    id oldObj;
    SideTable *oldTable;
    SideTable *newTable;

    /// 代码省略......

    // Clean up old value, if any.
    /// weak指针是否已经指向一个弱引用
    if (haveOld) {
        weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
    }

    // Assign new value, if any.
    /// weak是否需要指向一个新引用
    if (haveNew) {
        newObj = (objc_object *)
        /// 获取到的是新的散列表SideTable来进行插入
            weak_register_no_lock(&newTable->weak_table, (id)newObj, location, 
                                  crashIfDeallocating ? CrashIfDeallocating : ReturnNilIfDeallocating);
        // weak_register_no_lock returns nil if weak store should be rejected

        // Set is-weakly-referenced bit in refcount table.
        if (!newObj->isTaggedPointerOrNil()) {
            newObj->setWeaklyReferenced_nolock();
        }
        // Do not set *location anywhere else. That would introduce a race.
        *location = (id)newObj;
    }
    else {
        // No new value. The storage is not changed.
    }

    /// 代码省略......

    return (id)newObj;
}

storeWeak最主要的逻辑点

  • HaveOldweak指针之前是否已经指向了一个弱引用
  • HaveNewweak指针是否需要指向一个新引用
  • CrashIfDeallocating:如果被弱引用的对象正在析构,此时再弱引用该对象,是否应该crash
/** 
 * Registers a new (object, weak pointer) pair. Creates a new weak
 * object entry if it does not exist.
 * 
 * @param weak_table The global weak table.
 * @param referent The object pointed to by the weak reference.
 * @param referrer The weak pointer address.
 */
id 
weak_register_no_lock(weak_table_t *weak_table, id referent_id, 
                      id *referrer_id, WeakRegisterDeallocatingOptions deallocatingOptions)
{
    /// 取得弱引用对象
    objc_object *referent = (objc_object *)referent_id;
    objc_object **referrer = (objc_object **)referrer_id;
    /// 对象为空或者小对象直接返回
    if (referent->isTaggedPointerOrNil()) return referent_id;

    // ensure that the referenced object is viable
    if (deallocatingOptions == ReturnNilIfDeallocating ||
        deallocatingOptions == CrashIfDeallocating) {
        bool deallocating;
        if (!referent->ISA()->hasCustomRR()) {
            deallocating = referent->rootIsDeallocating();
        }
        else {
            // Use lookUpImpOrForward so we can avoid the assert in
            // class_getInstanceMethod, since we intentionally make this
            // callout with the lock held.
            auto allowsWeakReference = (BOOL(*)(objc_object *, SEL))
            lookUpImpOrForwardTryCache((id)referent, @selector(allowsWeakReference),
                                       referent->getIsa());
            if ((IMP)allowsWeakReference == _objc_msgForward) {
                return nil;
            }
            deallocating =
            ! (*allowsWeakReference)(referent, @selector(allowsWeakReference));
        }
        /// 如果正在析构或者释放,则不能被弱引用
        if (deallocating) {
            if (deallocatingOptions == CrashIfDeallocating) {
                _objc_fatal("Cannot form weak reference to instance (%p) of "
                            "class %s. It is possible that this object was "
                            "over-released, or is in the process of deallocation.",
                            (void*)referent, object_getClassName((id)referent));
            } else {
                return nil;
            }
        }
    }

    // now remember it and where it is being stored
    /// 在weak_table中找到被弱引用对象referrer对应的weak_entry_t
    weak_entry_t *entry;
    if ((entry = weak_entry_for_referent(weak_table, referent))) {
        /// 如果能找到entry就将referrer插入到weak_entry_t中
        append_referrer(entry, referrer);
    } 
    else {
        /// 如果找不到就新建一个插入
        weak_entry_t new_entry(referent, referrer);
        weak_grow_maybe(weak_table);
        weak_entry_insert(weak_table, &new_entry);
    }

    // Do not set *referrer. objc_storeWeak() requires that the 
    // value not change.

    return referent_id;
}
  • 主要进行isTaggedPointerdeallocating条件判断
  • 将被弱引用对象所在的weak_table中的weak_entry_t哈希数组中取出对应的weak_entry_t
  • 如果weak_entry_t不存在,则会新建一个并插入
  • 如果存在就将指向被弱引用对象地址的指针referrer通过函数append_referrer插入到对应的weak_entry_t引用数组

其创建流程如下 image.png

销毁
iOS-琐碎知识点(一)中的dealloc分析

问题先行解答

1Runtime应用都有哪些?
见👆Runtime->Runtime应用
2AOPOOP有什么区别?
见👆AOPOOP