前言 用户行为的统计...">
美文网首页工具iOS Developer程序员
用户行为你看我就够了(hook)

用户行为你看我就够了(hook)

作者: 灯泡虫 | 来源:发表于2017-03-02 23:18 被阅读401次

    目录

    <a name="前言"></a>前言

    用户行为的统计可以帮助我们更好的了解用户的各方面信息。现在比较主流的有两款三方统计库:友盟(国内)和flurry(国外)。但是用户行为收集的代码往往分散在各个类中,难以维护也不雅观,今天交流一种比较好用的用户行为统计方法。本文将全部采用swift来解析。

    <a name="准备工作"></a>准备工作

    • 三方库

    我们需要两个三方库:友盟和Aspects.
    * 友盟用来做最终的统计收集
    * Aspects 是本文的关键点,它hook住了我们想要的方法,高度集中。

    我们采用cocoapods 来管理三方库

    platform :ios, '8.0'
    use_frameworks!
    
    target 'UserBehaviorSwift' do
        #hook
        pod 'Aspects', '~> 1.4.1'
        #友盟统计(错误分析,事件统计)
        pod 'UMengAnalytics-NO-IDFA', '~> 4.0.5'
    end
    
    
    • 桥接(如果是oc项目可以跳过该项)

    因这两个库都是oc写的,故我们需要新建一个bribing文件。命名格式最好按照官方建议的 xxx-Bridging-Header.h。记得勾选上targets。如下图

    桥接oc头文件.png

    桥接文件还需要配置路径


    桥接文件路径.png

    有些三方库的head search(不是所有的库都需要,今天这两个库中Aspects需要配置)


    桥接文件时必要配置的三方库.png

    桥接代码:

    #ifndef UserBehaviorSwift_Bridging_Header_h
    #define UserBehaviorSwift_Bridging_Header_h
    
    // 这个库不在 headSearch里面设置就找不到。单独加上
    #import "Aspects.h"
    
    //友盟不设置就能找到,应该是framework的缘故
    #import <UMMobClick/MobClick.h>
    
    #import <UMMobClick/MobClickSocialAnalytics.h>
    
    #endif /* UserBehaviorSwift_Bridging_Header_h */
    
    • 代码准备

    我们需要一个RootVC类和两个继承与RootVC 的A 和 B;一个RootButton.整个项目的代码都已经传到我的github,地址在文章的结尾。


    需要的类.png

    <a name="页面的hook"></a>页面的hook

    • 一般的用户行为收集的写法都是直接在对应的类中写业务。类越多,就写的越多。太过麻烦,难以维护。
        override func viewWillAppear(_ animated: Bool) {
            MobClick.beginLogPageView("falgA")
        }
        
        override func viewWillDisappear(_ animated: Bool) {
            MobClick.endLogPageView("falgA")
        }
    
    • 我们怎么写?可以用Aspect 巧妙的hook住vc 的生命周期函数。拿viewWillAppear来举例。
              //进入页面
            let viewWillAppearBlock:@convention(block) (AspectInfo)-> Void = { aspectInfo in
                
                //获得实例
                let instance = aspectInfo.instance() as? RootViewController
                guard instance != nil else {
                    return
                }
                
                //实例转 格式化 string
                let className = String.init(reflecting: instance)
                let arr = className.components(separatedBy: ".")
                guard arr.count > 1 else {
                    return
                }
                
                //最终类名
                let finalClassName = arr[1].components(separatedBy: ":").first! as String
                
                //标题
                let title = instance?.title != nil ? instance?.title : "unknown"
                
                
                //最终使用 : type + 类名 + 标题 ,用“/”来分割
                let logFlag = "pageIn/" + finalClassName + "/" + title!
                MobClick.beginLogPageView(logFlag)
                print(logFlag)
            }
            
            //转换
            let viewWillAppearBlockWrapped: AnyObject = unsafeBitCast(viewWillAppearBlock, to: AnyObject.self)
            
            //最终hook住对应的函数,这里设置了AspectOptions.positionBefore模式,会在viewWillAppear 即将被调用前调用
            do {
                try RootViewController.aspect_hook(#selector(RootViewController.viewWillAppear(_:)), with: AspectOptions.positionBefore, usingBlock: viewWillAppearBlockWrapped)
            }
            catch {
                print(error)
            }
    

    解析

    用RootVC hook 住viewWillAppear,所有继承与RootVC的类在每次的viewWillAppear被调用之前都会调用我么已经准备好的block。因是swift 调用oc 故用convention修饰,这里如果直接使用闭包会 crash。block里会返还一个当前调用者的实例,用实例我们可以获得 类名、标题。我们设定了格式来确保这个flag的唯一性质 :进入页面("pageIn/" + finalClassName + "/" + title),离开页面("pageOut/" + finalClassName + "/" + title)。最后我们使用 MobClick.beginLogPageView(logFlag) 对其行为进行收集。

    <a name="按钮的hook"></a>按钮的hook

    按钮的hook相对于页面来说会复杂一点,多了一些步骤。

              //按钮
            let touchesBeganBlock:@convention(block) (AspectInfo)-> Void = { aspectInfo in
                let instance = aspectInfo.instance() as? RootButton
                
                guard instance != nil else {
                    return
                }
                
                let className = String.init(reflecting: instance?.allTargets.first)
                let arr = className.components(separatedBy: ".")
                guard arr.count > 1 else {
                    return
                }
                
                let finalClassName = arr[1].components(separatedBy: ":").first! as String
                
                let title = instance?.titleLabel?.text != nil ? instance?.titleLabel?.text : "unknown"
    
                let logFlag = "event/" + finalClassName + "/" + title!
                
                let path = Bundle.main.path(forResource: "UserBehavior", ofType: "plist")
                let dict = NSDictionary.init(contentsOfFile: path!)
                let id = dict?.object(forKey: logFlag) as? String
                guard id != nil else {
                    return
                }
                
                MobClick.event(id, label: logFlag)
    
                print(logFlag)
            }
    
    

    解析

    相对于类来说,按钮需要通过alltargets 的一系列格式化才能得到类名。最终格式为("event/" + finalClassName + "/" + title)。因MobClick.event 事件一般都需要产品经理 给你一套他们定制的id,假如你反驳不了的话,那么就好好的享受吧!这里用了plist进行管理,把所有的按钮和对应的id都写进去,用的时候再取出来。

    注意: 我么在解析的时候可能碰到一些没有标题或者其他的情况,所有要严格的进行校验,宁愿少记录一条都碍事,也要避免crash.

    plist如下图所示

    plist.png

    效果图

    最终效果.gif

    <a name="总结"></a>总结

    实际开发中可能不仅仅需要到 页面和按钮这两种,但都是一样的道理,大多都可以通过这种方法来写,个别写不了的就直接用原始方法写也是无伤大碍。所有的代码都已经传到Github

    相关文章

      网友评论

        本文标题:用户行为你看我就够了(hook)

        本文链接:https://www.haomeiwen.com/subject/fsxlgttx.html