面试项目-Susu

本文尝试通过 STAR 原则拆解成Susu这个项目。

  • Situation 情景
  • Task 任务
  • Action 行动
  • Result 结果

# 业务介绍

Susu是一个线上的陌生人社交业务。目前全平台日活差不多 xx 万左右,具体在 iOS 上大概是 xx 万左右。

# 个人职责

团队之前是四个人,我的职责

  • IM SDK 部分的接入以及界面定制
  • 视频 SDK 的接入
  • 相关页面的开发

# 项目中用到的技术点

# 推送

使用云信进行推送配置。

# 即时消息(IM)

云信 SDK。

# 视频播放

阿里云视频。

# H5/Native 交互

主要是通过 JSBridge 的方式进行交互

# 项目中遇到的难点

# 动态库延迟加载

问题背景,因为我们之前有个需求就是新装的 App 需要点击进入欢迎页的「进入 Susu」按钮 的时候才弹窗进行网络权限提示,但是通过 cocoapod 引入 facebook 库后,facebook 库实现了 +load() 方法,并且在 +load() 方法里进行了网络请求,这样就等不到用户点击进入 Susu 就弹出了网络权限弹窗,所以现状就是不符合产品的需求。

解决方案就是 将 facebook 从 cocoapod 里面移出来,然后将其作为二进制包嵌入到工程里面。这里面嵌入的 framework 是需要经过签名的,不签名的话会在导出包的时候报错「 code signing FBSDKCoreKit.framework failed 」,所以需要先通过 cocoapod 方式打包成 IPA 方式之后再从里面提取出对应的 framework 文件,可以通过 codesign 命令查看签名信息

> codesign -dvvv /Frameworks/FBSDKCoreKit 
Authority=Apple Distribution: xxx
Authority=Apple Worldwide Developer Relations Certification Authority
Authority=Apple Root CA

启动后在合适的时机,比如在用户已经点击过「进入Susu」之后,我们再去通过动态加载 bundle 的方式去加载 facebook 库。

+ (void)loadFBSDKBundle {
    static BOOL loaded = NO;
    if (loaded) { return; }
    NSString *path = [[NSBundle mainBundle].bundlePath stringByAppendingString:@"/Frameworks/FBSDKCoreKit.framework”];
    NSBundle *bundle = [NSBundle bundleWithPath:path];
    if (!bundle.isLoaded) {
        loaded = [bundle load];
    }
}

不光要加载 facebook 库还要执行库中对应的方法,因为这种加载方式是没有办法直接引入头文件,正常 Pod 引入的方式是会在 Build SettingSearch Paths 下自动对应库的 header 文件的。

我们只能通过调用原始的消息发送的机制去发送消息,通过运行时API,拿到 Facebook 库初始化用到的主类,然后通过 objc_msgSend 方法去进行对应的消息发送。

+ (BOOL)openUrl:(NSURL *)url withOptions:(NSDictionary *)options {
    Class FBSDKApplicationDelegateClass = (Class)objc_getClass("FBSDKApplicationDelegate");
    FBSDKApplicationDelegate *instance = ((FBSDKApplicationDelegate *(*)(id,SEL))objc_msgSend)(FBSDKApplicationDelegateClass,@selector(sharedInstance));
    UIApplication *app = UIApplication.sharedApplication;
    BOOL (*objc_msgSendWithSingleType)(id self, SEL _cmd,UIApplication *app,NSURL *url, NSDictionary* launchOption) = (void*)objc_msgSend;
    return objc_msgSendWithSingleType(instance, @selector(application:openURL:options:), app, url, options);
}

# 项目中的亮点

# 防 SPAM 抓包

应用中有很多机器人用户来骚扰正常用户,我们判断这些机器人可能是被批量操控的,猜测这些机器人是被放置在某个架子上的,所以这些机器人的陀螺仪不会产生波动的数据,于是业务上设计了,通过客户端定时获取陀螺仪数据,上传到服务端,然后根据服务端对陀螺仪的数据分析,对相关用户执行封禁的操作。

客户端定时获取陀螺仪数据的实现是通过 Runloop 保活线程,然后再 Runloop 上添加定时器的方式来执行对应的任务。

主要代码如下

class TimerPermanentThread: NSObject {
    private static var timerPermanentThread:Thread? = nil
    @discardableResult
    static func buildTimerPermanentThread(_ timeInterval:TimeInterval, _ task:@escaping vv_t) -> Thread {
        objc_sync_enter(TimerPermanentThread.self)
        defer { objc_sync_exit(TimerPermanentThread.self) }
        if timerPermanentThread != nil {
            return timerPermanentThread!
        }
        timerPermanentThread = Thread.init {
            Thread.current.name = "com.xx.permanent-thread"
            let timer = Timer.init(timeInterval: timeInterval, repeats: true) { _ in
                NSLog("TimerPermanentThread")
                task()
            }
            let runloop = RunLoop.current
            runloop.add(timer, forMode: RunLoop.Mode.default)
            runloop.run()
        }
        timerPermanentThread?.start()
        return timerPermanentThread!
    }   
}