MST

星途 面试题库

面试题:Objective-C运行时在iOS开发优化中的应用

深入阐述Objective-C运行时(Runtime)机制,结合iOS开发场景,说明如何利用运行时机制优化应用性能,比如动态方法解析、消息转发等。假设在一个大型iOS项目中,如何通过运行时机制实现对项目中所有ViewController的生命周期方法进行统一埋点统计?
32.3万 热度难度
编程语言Objective-C

知识考点

AI 面试

面试题答案

一键面试

一、Objective-C运行时(Runtime)机制深入阐述

  1. 运行时本质
    • Objective - C是一门动态语言,其运行时(Runtime)是一个用C和汇编语言编写的库。它提供了一种在运行时动态加载类、方法,动态关联对象和方法等功能的机制。
    • 在编译阶段,编译器会将Objective - C的方法调用转化为objc_msgSend函数调用,这个函数会在运行时根据对象的类信息找到对应的方法实现并执行。
  2. 类结构
    • 每个类在运行时都有一个objc_class结构体实例,它包含了类的元数据,如类名、超类指针、类的属性列表、方法列表、协议列表等。例如:
struct 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;
  1. 方法查找与消息发送
    • 当向一个对象发送消息时,首先会在对象的类的方法缓存(objc_cache)中查找,如果找到则直接调用。
    • 如果缓存中未找到,会在类的方法列表中查找。若类的方法列表中没有找到,会沿着继承体系向超类的方法列表中查找。
    • 如果一直到根类NSObject都没找到,就会进入动态方法解析阶段。

二、利用运行时机制优化应用性能

  1. 动态方法解析
    • 原理:当运行时在方法列表中找不到对应的方法实现时,会给类一次动态添加方法实现的机会。在+ (BOOL)resolveInstanceMethod:(SEL)sel(实例方法)或+ (BOOL)resolveClassMethod:(SEL)sel(类方法)方法中,可以通过class_addMethod函数动态添加方法实现。
    • iOS开发场景应用:在一些情况下,某些方法可能在编译时不确定是否需要实现,但在运行时根据具体条件才确定。比如在一个地图应用中,可能根据用户当前所在地区决定是否需要加载特定的地图图层绘制方法,通过动态方法解析可以避免在编译时就加载不必要的代码,从而减少应用的启动时间和内存占用。
  2. 消息转发
    • 备用接收者:如果动态方法解析没有处理消息,运行时会询问对象是否有其他对象能处理该消息,通过- (id)forwardingTargetForSelector:(SEL)aSelector方法返回一个备用接收者,如果返回非nil对象,则会向该对象发送消息。
    • 完整转发:如果没有备用接收者,运行时会进入完整转发阶段。首先会调用- (void)forwardInvocation:(NSInvocation *)anInvocation方法,在这个方法中可以手动设置消息的接收者、方法等信息来处理消息。如果此方法也未处理,最终会调用- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector来获取方法签名,如果获取不到签名,就会抛出unrecognized selector sent to instance异常。
    • iOS开发场景应用:在开发框架时,可能希望对一些未实现的方法进行统一处理,避免应用崩溃。比如在一个基础库中,对某些可能在不同业务场景下有不同实现的方法,通过消息转发机制可以在运行时灵活处理,提高框架的通用性和健壮性。

三、通过运行时机制实现对项目中所有ViewController的生命周期方法进行统一埋点统计

  1. 方法交换
    • 利用method_exchangeImplementations函数来交换两个方法的实现。例如,要统计UIViewControllerviewDidLoad方法调用次数,可以先创建一个自定义的统计方法,然后与viewDidLoad方法交换实现。
    • 代码示例
#import <objc/runtime.h>

@implementation UIViewController (LifeCycleHook)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        SEL originalSelector = @selector(viewDidLoad);
        SEL swizzledSelector = @selector(swizzled_viewDidLoad);
        
        Method originalMethod = class_getInstanceMethod(self, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(self, swizzledSelector);
        
        BOOL didAddMethod =
        class_addMethod(self,
                        originalSelector,
                        method_getImplementation(swizzledMethod),
                        method_getTypeEncoding(swizzledMethod));
        
        if (didAddMethod) {
            class_replaceMethod(self,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

- (void)swizzled_viewDidLoad {
    // 埋点统计逻辑,例如记录调用时间等
    NSLog(@"viewDidLoad called at %@", [NSDate date]);
    [self swizzled_viewDidLoad];
}

@end
  1. 遍历所有ViewController
    • 可以通过UIWindowrootViewController来递归遍历所有的UIViewController,在UIApplicationDidFinishLaunching通知中启动遍历,这样可以确保在应用启动后对所有的ViewController进行埋点统计的初始化。
    • 代码示例
#import "AppDelegate.h"
#import <objc/runtime.h>

@interface UIViewController (LifeCycleHook)
@end

@implementation UIViewController (LifeCycleHook)

+ (void)load {
    // 方法交换代码同上述
}

- (void)swizzled_viewDidLoad {
    // 埋点统计逻辑
    NSLog(@"viewDidLoad called at %@", [NSDate date]);
    [self swizzled_viewDidLoad];
}

@end

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    [self traverseViewControllers:self.window.rootViewController];
    return YES;
}

- (void)traverseViewControllers:(UIViewController *)viewController {
    if (viewController) {
        // 这里可以对viewController进行埋点统计相关的初始化操作
        NSLog(@"Traversing view controller: %@", viewController);
        if ([viewController isKindOfClass:[UINavigationController class]]) {
            UINavigationController *navController = (UINavigationController *)viewController;
            for (UIViewController *childVC in navController.viewControllers) {
                [self traverseViewControllers:childVC];
            }
        } else if ([viewController isKindOfClass:[UITabBarController class]]) {
            UITabBarController *tabController = (UITabBarController *)viewController;
            for (UIViewController *childVC in tabController.viewControllers) {
                [self traverseViewControllers:childVC];
            }
        } else {
            for (UIViewController *childVC in viewController.childViewControllers) {
                [self traverseViewControllers:childVC];
            }
        }
    }
}

@end

通过上述方法交换和遍历ViewController的方式,就可以在大型iOS项目中利用运行时机制对所有ViewController的生命周期方法进行统一埋点统计。