跳过正文
  1. Ps/

iOS "即插即用"的服务类

·2687 字·6 分钟· loading · loading · ·
目录

在项目的开发过程中免不了需要集成第三方 SDK ,而这个过程中最头疼的事就是在 AppDelegate 中重写各种相关的系统回调方法,比如 application:openURL:options:application:didReceiveRemoteNotification:fetchCompletionHandler: 等等,而每个 SDK 对这些方法中拿到的数据处理方式又各不相同,并且随着集成的第三方 SDK 越来越多,AppDelegate 也变得越来越臃肿,每一个回调方法中充斥着各种第三方 SDK 的处理代码,AppDelegate 亟待瘦身。

另外,当一个公司内部有多个项目都需要集成一系列 SDK(如分享、推送、支付等) 的时候,将这个重复的集成步骤在公共组件中统一处理是一个非常有必要的事情,这样既能够减少很大一部分重复开发工作,也能够将冗余的代码从 AppDelegate 中剥离出去。而对于各个项目的开发同学来说,使用公共组件中的这些组件能力的时候,更关心的应该是如何在业务中使用这些能力,而不是关心第三方 SDK 需要从哪些系统回调中监听数据,或者第三方 SDK 的生命周期管理,例如:使用支付功能的时候,业务需要使用的能力只有发起支付动作和支付成功或失败的回调,而具体发起支付前需要的参数配置和支付结果的回调应该在哪里监听(通过 url scheme 还是 SDK 内置的 H5 回调),这些是业务方不需要考虑的。

于是我们集成第三方 SDK 时有了这几个痛点:

  • 自动完成 SDK 参数配置
  • 监听需要的系统回调

简单来说就是我们需要一个机制来达到"即插即用"的目的,所谓"即插即用",即拖进项目就可以使用,不需要额外的初始化、响应回调等步骤,要做到这些,我们实现一个 iOS 中的"服务"机制,这个机制需要:

  • 感知 APP 生命周期
  • 响应 APP 关键回调
  • 自动发现、自动初始化

下面来看看如何实现这几个特性。

获取已注册的类
#

要做到服务类的自动发现,首先要能够获取到项目中所有的类,这一点不难做到,runtime 给我们提供了这样一个方法:

OBJC_EXPORT int objc_getClassList(Class *buffer, int bufferLen);

这个方法有两个参数,第一个参数 buffer 是分配好内存空间的数组,用来存储这个方法获取到的所有类的指针,当这个参数是 NULL 时,这个方法只会返回所有已注册类的个数;第二个参数 bufferLen 是第一个参数 buffer 的大小,告诉这个方法 buffer 所能装载的指针数量,当 bufferLen 小于所有注册类的数量时,这个方法会随机填充 bufferLen 数量的已注册类到 buffer 中,注意是随机的,意味着每一次调用得到的结果都不一样。

我们可以在 runtime 源码的使用示例中理解这两个参数的作用:

int numClasses = 0, newNumClasses = objc_getClassList(NULL, 0);
Class *classes = NULL;
while (numClasses < newNumClasses) {
     numClasses = newNumClasses;
     classes = realloc(classes, sizeof(Class) * numClasses);
     newNumClasses = objc_getClassList(classes, numClasses);
}
// now, can use the classes list; if NULL, there are no classes
free(classes);

除此以外,还有另外一个方法可以获取到所有已注册类:

Class * objc_copyClassList(unsigned int *outCount);

这个方法比上面的方法使用起来更加简单,这个方法会直接创建并返回所有已注册类指针的数组,参数 outCount 是已注册类的数量,使用方法:

unsigned int outCount;
Class *classes = objc_copyClassList(&outCount);
// now, can use the classes list; if NULL, there are no classes
free(classes);

使用这个方法跑起来并把获取到的类名打印出来你可能就震惊了,已注册类居然有上万个之多:

_CNCombineLatestObservable
_CNDistinctObservable
_CNScheduledObservable
CNBehaviorSubject
CNVirtualSchedulerJob
CNTracedLog
...

这里面包括了很多完全没有见过的类,其中也包括了非 NSObject 的子类,我们使用 NSObject 方法对这些非 NSObject 子类进行操作的时候是非常危险的,会导致应用 crash,而事实上我们要关心的只有 NSObject 的子类,所以还需要将获取到的列表中的类做一次过滤,runtime 中提供了一个方法来判断类中是否实现了某个特定方法:

OBJC_EXPORT BOOL class_respondsToSelector(Class _Nullable cls, SEL _Nonnull sel) 

而要过滤出 NSObject 的子类,只需要判断类是否包含 NSObject 中一定会存在的 doesNotRecognizeSelector: 方法即可:

class_respondsToSelector(class, @selector(doesNotRecognizeSelector:))

这样我们就得到了所有已注册的 NSObject 子类,于是我们就可以很方便地通过 NSObject 中的 isSubclassOfClass: 方法来从中获取到项目中我们指定类的子类了。

什么时候触发自动加载?
#

知道了如何获取所有的类,并且能够从中获取到指定类的子类,我们就实现了自动发现服务类的第一步,那么应该什么时候触发这个获取动作呢?加到 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 方法中?之前提到了服务类很重要的一点——“即插即用”,对项目有侵入代码的行为就不能称为即插即用了,所以这个方案不行。通过 hook?这是一个好办法,实际上我们的服务类的确在 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 方法中挂上了 hook ,用于实现 APP 类加载完毕之后服务类进行一些业务配置相关的初始化动作,比如相关 SDK 的初始化。但是假如某个服务需要实现在 didFinishLaunchingWithOptions: 方法之前执行一些操作(就比如 hook didFinishLaunchingWithOptions: 方法)怎么办?

了解 runtime 的同学肯定很快就想到了 + (void)load 方法,一起来回顾一下这个方法。先看一下苹果对这个方法的解释:

Invoked whenever a class or category is added to the Objective-C runtime; implement this method to perform class-specific behavior upon loading.

在类或者分类被添加到 runtime 时执行,实现这个方法可以在加载的时候执行一些类相关的行为。具体的运行机制可以看下 dyld 的源码,下次再开篇文章一起来梳理一下。

由于这个方法在所有类被加载时就会调用,显然调用时机在 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 方法之前。需要注意的是,原则上在这个方法中只能做一些诸如方法交换等类相关的操作,因为 APP 在启动时执行类加载等资源初始化动作是耗费性能巨大的流程,在这个时候执行一些计算量大的操作会严重影响 APP 启动速度。

我们新建一个类,实现 + (void)load 方法。接下来的事情就很明确了,我们可以获取到所有已注册的类,并且能够从中获取到指定类的子类,现在只需要实现一个服务类的基类,所有服务类都继承于这个基类,然后在 + (void)load 中获取这个基类的所有子类就能够达到 APP 启动时自动加载项目中所有服务类的目的:

+ (void)load {
    // 加载 services
    NSArray *allServices = [MYRuntime allSubClassesOf:[MYBasicService class]];
    ...
    // 调用 allServices 中的生命周期方法
    // hook
    // Notification
    ...
}

allSubClassesOf: 方法就是上面提到的获取到指定类子类的实现。

接着在服务基类的这个方法中,通过 method swizzling 给 AppDelegate 中的下面的方法挂上钩子,以便服务类可以在这些方法调用时提供相应处理能力:

@selector(application:didFinishLaunchingWithOptions:)
@selector(application:handleOpenURL:)
@selector(application:openURL:options:)
@selector(application:didRegisterForRemoteNotificationsWithDeviceToken:)
@selector(application:didReceiveRemoteNotification:fetchCompletionHandler:)
@selector(application:continueUserActivity:restorationHandler:)

除此以外,响应下面的系统通知,以便服务类能感知到 APP 的生命周期:

UIApplicationDidFinishLaunchingNotification
UIApplicationDidEnterBackgroundNotification
UIApplicationWillEnterForegroundNotification
UIApplicationWillTerminateNotification

以上 hook 和通知的响应都调用下列服务基类的模板方法,以实现服务类的生命周期机制:

- (void)load;                // + (void)load 时调用
- (void)unload;              // + (void)dealloc 时调用
- (void)launch;              // UIApplicationDidFinishLaunchingNotification 时调用
- (void)terminate;           // UIApplicationWillTerminateNotification 时调用
- (void)serviceWillActive;   // UIApplicationWillEnterForegroundNotification 时调用
- (void)serviceDidDeactived; // UIApplicationDidEnterBackgroundNotification 时调用

实现服务类
#

继承于服务基类的服务类通过重写服务基类中提供的生命周期模板方法,来对 APP 相应的生命周期作出响应,除此以外,由于我们 hook 了 openURL 相关的方法,我们能够在服务类中很方便的对第三方 APP 跳转回来的动作作出响应,这在将分享 SDK 或支付 SDK 封装成服务的时候非常有用,对于第三方 APP 通过 url scheme 跳转回来的动作,直接在服务类中就能完成处理,不需要对 AppDelegate 中的方法作出任何改动。推送类 SDK 的封装也是一样。

封装好后的服务类,给业务方提供几个很简单的输入/输出方法或者 block,就可以将第三方 SDK 的能力提供给业务方调用,业务方不需要再考虑集成 SDK 所需要在诸如 AppDelegate 中重写的回调方法了,真正实现将服务类"拖入"项目就可以使用第三方 SDK 的能力,达到"即插即用"的目的。

注意
#

服务类中的 - (void)load 方法与 NSObject 的 + (void)load 方法调用时机一致,所以在实现服务类的时候,这个方法也仅限于做一些类相关的操作,其他耗费资源的操作如 SDK 的初始化一律在 - (void)launch 中调用,避免影响 APP 启动速度。

小乜
作者
小乜
这里是我的数字分身。

相关文章