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

目录

  • 前言
  • 准备工作
  • 页面的hook
  • 按钮的hook
  • 总结

前言

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

准备工作

  • 三方库

我们需要两个三方库:友盟和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。如下图

用户行为你看我就够了(hook)_第1张图片
桥接oc头文件.png

桥接文件还需要配置路径


用户行为你看我就够了(hook)_第2张图片
桥接文件路径.png

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


用户行为你看我就够了(hook)_第3张图片
桥接文件时必要配置的三方库.png

桥接代码:

#ifndef UserBehaviorSwift_Bridging_Header_h
#define UserBehaviorSwift_Bridging_Header_h

// 这个库不在 headSearch里面设置就找不到。单独加上
#import "Aspects.h"

//友盟不设置就能找到,应该是framework的缘故
#import 

#import 

#endif /* UserBehaviorSwift_Bridging_Header_h */
  • 代码准备

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


用户行为你看我就够了(hook)_第4张图片
需要的类.png

页面的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) 对其行为进行收集。

按钮的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如下图所示

用户行为你看我就够了(hook)_第5张图片
plist.png

效果图

最终效果.gif

总结

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

你可能感兴趣的:(用户行为你看我就够了(hook))