面试项目-父母相亲

本文尝试通过 STAR 原则拆解成父母相亲这个项目。

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

# 业务介绍

相亲是一个线上的相亲业务,目的是帮助老人在线上帮助孩子找对象。目前全平台日活差不多 xx 万左右,具体在 iOS 上大概是 xx 万左右。

主要的功能就是

  • 首页推荐卡片流&各种运营引导入口Banner
  • 收藏看过以及对应IM功能
  • 会员专区的H5页面
  • 相关设置页面

# 个人职责

iOS 端总共两个人,我负责的内容

  • 基础框架搭建,包括视图框架,整体架构分层。
  • 日常开发涉及到的 UI 功能模块儿。比如 IM 界面以及推荐相关界面的开发。
  • 应用内嵌 H5 页面的开发,基于 Vue 框架。
  • 自动化打包/装包流程的搭建。

# 项目中用到的技术点

# 推送

使用个推进行推送配置。

# 内购

使用集团提供的SDK完成内购相关逻辑。

内购遇到的问题是「掉单」。 用户在某个时间点下单之后,未等到苹果状态支付完成就杀死进程退出应用,然后在之后的某个时间点启动应用后,支付 SDK 收到回调后,将凭据传给平台侧,但是购买订单已经关闭了,导致我们后台认为用户购买失败。解决方案是将订单关闭时长延时到一个小时同时根据平台侧的支付结果来确认对应用户是否购买成功,给用户补单。

# 即时消息(IM)

主要实现:HTTP轮询+手撸聊天界面。

为什么使用 HTTP 轮询?背景是集团 SDK 没有提供小程序版本,而消息未读数量功能是通过 SDK 实现的,即服务端没有保存未读消息数量。所以没办法使用集团的 SDK。

本地通过消息过滤的方式实现撤回删除消息等等功能。

# H5/Native 交互

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

# 项目中遇到的难点

父母相亲算是业务性的 App,确实没有那么难技术上的点,比如做直播那种,不过业务还是有一定复杂性,主要体现在购买逻辑和弹窗逻辑上,除此之外也做了一些启动优化相关的工作。

# 购买逻辑的抽象

应用提供了不同的商品,比如用户可以购买联系次数和购买会员;以及后端会针对不同用户下发不同的购买策略。用户点击购买之后需要先获取购买规则,规则里面包含了对应这个用户的购买策略以及用户是否需要真实付费(因为如果购买会员后就不需要再进行付费)等信息。

我们预先初始化好了一个策略工厂保存了各个策略的实例,然后根据服务端下发好的策略去匹配对应的策略实例,策略实例都统一实现了预先定义好的触发购买行为协议,相当于是抽象了一个通用的购买行为的入口,然后在各个实例里面去做对应的具体的购买行为。

//购买行为协议
protocol BuyWayInterface {
    func showBuyPopView(childInfo:Child, payRuleInfo:PayRuleInfo, showGuide:Bool, goChat:Bool, trackInfo:BuyWayTrackInfo)
    
}
//策略工厂
class BuyWayFactory: NSObject {
    private static var map:[String:BuyWayInterface] = {
        var tmap = [String:BuyWayInterface]()
        tmap[PayMode.card.rawValue] = CardStrategy.init()
	      ...
        return tmap
    }()
    //获取具体策略行为
    public static func getStrategy(_ mode:PayMode) -> BuyWayInterface {
	     return map[mode.rawValue]
    }
}
//策略具体实现
class VIPStrategy: BuyWayInterface {   
    func showBuyPopView(childInfo: Child, payRuleInfo: PayRuleInfo,showGuide:Bool, goChat:Bool, trackInfo:BuyWayTrackInfo) {
        ...
    }
}
//具体使用
let stragegory = BuyWayFactory.getStrategy(payMode)
stragegory.showBuyPopView(childInfo: childInfo, payRuleInfo: payRuleInfo, showGuide: showGuide, goChat: goChat, trackInfo: buyTrackInfo)

# 弹窗逻辑的抽象

最开始的时候弹窗逻辑是客户端依次向服务端发请求判定是否满足某个条件然后去弹窗,还有一些本地的弹窗逻辑的优先级穿插在服务器返回的弹窗优先级里。这时候用的是责任链的模式去做的优化,统一封装了弹窗的逻辑(不够具体,怎么统一封装的?)

将可能得弹窗的情况封装成类,并实现对应的协议方法,同时抽象了一个弹窗管理服务,弹窗管理服务将这些弹窗处理类封装成对应数组,依次从数组里面取出对应弹窗处理类来处理,A 处理不了,就继续让 B 处理,依次往下直到有能弹窗的情况出现。

代码如下

//弹窗协议
protocol HandlePopup {
    func handle(next:UIDubyPopupService, transInfo:TransInfo)
}
//弹窗服务
class UIDubyPopupService: NSObject {    
    var showingHomePopup = false
    var popoupArray:[HandlePopup]
    fileprivate let updateAppGuideHandler = UIUpdateAppGuideHandler()           //升级app弹窗
    fileprivate let redpacketPopupHandler = UIRedpacketPopupHandler()           //红包
    static func handlePopup() {
        ServiceManager.uiPopupService.process(next: ServiceManager.uiPopupService, transInfo: transInfo)
    }
    override init() {
        popoupArray = [
            updateAppGuideHandler,
            redpacketPopupHandler,
        ]
    }
    var handleIndex = 0
    func process(next: UIDubyPopupService, transInfo: TransInfo) {
        let handler = popoupArray[handleIndex]
        handleIndex = handleIndex + 1
        handler.handle(next: self, transInfo: transInfo)
    }
}
//更新引导弹窗处理逻辑
fileprivate class UIUpdateAppGuideHandler: HandlePopup {
    func handle(next: UIDubyPopupService, transInfo: TransInfo) {
        ServiceManager.launchSvr.getGeneralConfig(timeInterval: 3) { response in
            if condition {
                UpdateGuideView.show(vUpgradeStrategy)
            } else {
                next.process(next: next, transInfo: transInfo)
            }
        } failBlk: { raw in
            //
        }
    }
}
//红包弹窗处理逻辑
fileprivate class UIRedpacketPopupHandler: HandlePopup {
    func handle(next: UIDubyPopupService, transInfo: TransInfo) {
        ServiceManager.payNetSvr.fetchPaymentRuleWithRedPacket { response in
            next.process(next: next, transInfo: transInfo)
        } failBlk: { raw in
            //
		}
    }
}

# 启动优化

背景是小程序启动太慢了,所以配合小程序一起做了启动优化。

  1. 首页列表网络请求合并

    随着业务迭代,首页的网络请求越来越多,从业务刚开始的一个获取推荐卡片 list 的接口,到现在获取各种优惠券的接口,以及底部的贴片banner,还有首页列表插入的很多个区位的信息也是单独的网络请求。

    有的时候产品是希望界面能全部展示出来,所以用 dispatch_group 来实现等待让接口数据全部回来,但是有可能有的接口出点问题就会造成整体的展示变慢。当然这些都是快速迭代时候埋下的坑,但是有问题也得继续改,所以就是和后端同事一起梳理了首页的所有网络请求,最后合并到只有一两个的样子,首页展示就轻快不少。

  2. Cocoapods 动态库转静态库

    cocoapods 通过在 podfile 中配置 use_frameworks! :linkage => :static 使用静态链接,大概是 18 年左右加入的支持。

  3. 二进制重排。

    静态插桩技术,通过减少缺页异常的方式加速程序启动。

总体优化后的启动时间从 2.0s 降低到 1.5s 左右。线上监控的方式是 Xcode 的 Metrics 和 Firebase。

# 项目中的亮点

# 动态修改 HTTP 代理

在项目中做了一些调试效率提升的工作。我们之前配置 Charles 抓包的时候需要跳转到设置的 WIFI 下去修改 HTTP 代理和端口。我尝试在应用内直接修改 HTTP 代理和端口,而且本地也会保存之前配置过的代理和端口信息,方便下次更改配置。

最终的效果就是调试的时候就不用再跑到设置里面去修改了,直接在应用基于 FLEX 做了一个调试页面,在里面修改地址和端口信息,或者关闭代理。这个特性还是很提升调试效率的,最后还申请了公司的专利。