philon的博客

一个菜鸟程序员的逆袭

App性能监控-newRelicSDK使用及拦截上报方案

随着应用的发展越来越大,产品的开发和测试逐渐开始关注app的在线运行性能。最近一直在关注这个方向,说一下自己的心得和体会。

所谓的性能监控,并不是做出一堆花哨的图表让人赏心悦目。其实目的只有两个,一个是能够实时发现线上的问题,通过报警机制通知到相关负责人;另外一个就是能够看到历史数据,供测试和开发进行性能优化。那么监控的关键是确定监控指标,在监控指标的指导下确定需要客户端打点收集的日志。

APP性能监控指标

最近调研了一下这个方向,行业内把这个方向称为APM(application performance management)。国内外都有不少公司在做这方面的产品,一些大公司为了防止应用监控数据的外泄,也会自行开发性能监控管理平台。国内做得比较好的如基调、博睿、云测、听云等,国外有New Relic、Blueware、OneAPM等。 各个平台的技术方案大同小异(这里主要说的是IOS平台),提供一个集成到应用的SDK(static framework 或者 .a 文件), 然后收集日志上报到其后台进行数据处理和纬度分析。这些平台的商业策略一般都是先免费提供试用,再进行租用收费的方式。

APP的性能指标主要分为两大类:InterActions(UI交互) 和 Network (网络连接)。在这两个大分类下,会有如下细分指标:

InterActions:(按时间-版本:+device类型,+os类型)

  • APP启动次数、APP启动平均时间;
  • Controller display次数、平均display时间;
  • Controller display过程中各个method(包括UI thread、worker thread中的method)的平均执行时间;

Network方面:(按时间-版本:+域名,+地区,+运营商, +连接网络类型)

  • 平均相应时间
  • rpm(一分钟请求次数)
  • 总耗时
  • 传输数据大小
  • 再具体到某个API接口:

(1) 平均http响应时间 (区分web Application(webview) 和 network); (2) rpm; (3) 平均数据传输大小(区分send和receive);

在两大指标上会增加如下方面的维度: 接入网络、运营商、设备、系统版本、地区、app版本、统计时间;

NewRelic SDK使用说明

我主要关注了国内的听云平台和国外的NewRelic平台。国内听云的统计平台注册只能看到非常基础的统计,一点细分分析维度,就提示你升级套餐,当然你可以申请14天免费试用,然后就是市场给你打电话过来。 其SDK中的framework也只提供一个头文件,想要对统计数据的定制基本上没有。于是果断切换到国外的newRelic平台,newRlic平台需要vpn访问才能快点,一注册所有的功能都能够试用,当然试用期也只有14天。

关注一下SDK的使用,SDK提供了众多可配置的头文件,让你能够更加清楚的了解sdk提供的功能;

(1)最基础的使用,当然就是通过Apptoken注册使用:(注意一下,newRelic集成在debug时会检查当前应用中hook的使用,需要将newrelic的注册使用放到所有hook之后,一般放到appdidfinishLaunch靠后的位置)

1
2
3
#import <NewRelicAgent/NewRelic.h>

[NewRelicAgent startWithApplicationToken:@"token"];

如果是最基础的用法,我们可以通过charles拦截其数据上报,可以发现request和response的数据都是通过ssl加密、https传输,看不到明文数据。如果需要看到明文数据可以使用非加密接口+ (void)startWithApplicationToken:(NSString*)appToken withoutSecurity:(BOOL)disableSSL

(2) SDK集成了各种指标收集,包括Interaction、SwiftInteraction、Crash、NSURLSession、HTTPResponse等。如下代码所示:

1
2
3
4
5
6
7
8
9
typedef NS_OPTIONS(unsigned long long, NRMAFeatureFlags){
    NRFeatureFlag_InteractionTracing                    = 1 << 1,
    NRFeatureFlag_SwiftInteractionTracing               = 1 << 2, //disabled by default 
    NRFeatureFlag_CrashReporting                        = 1 << 3,
    NRFeatureFlag_NSURLSessionInstrumentation           = 1 << 4,
    NRFeatureFlag_HttpResponseBodyCapture               = 1 << 5,
    NRFeatureFlag_ExperimentalNetworkingInstrumentation = 1 << 13, //disabled by default
    NRFeatureFlag_AllFeatures                           = ~0ULL //in 32-bit land the alignment is 4bytes
};

你可以按照自己的需要指定需要收集的指标数据,使用+ (void) enableFeatures:(NRMAFeatureFlags)featureFlags; 或 + (void) disableFeatures:(NRMAFeatureFlags)featureFlags;.

(3) 关于Interaction的主要方案就是hook controller中的各个生命周期方法,NewRelicSDK中主要拦截了如下方法:

UIViewController

  1. viewDidLoad
  2. viewWillAppear:
  3. viewDidAppear:
  4. viewWillDisappear:
  5. viewDidDisappear:
  6. viewWillLayoutSubviews
  7. viewDidLayoutSubviews

UIImage:

  1. imageNamed:
  2. imageWithContentsOfFile:
  3. imageWithData:
  4. imageWithData:scale:
  5. initWithContentsOfFile:
  6. initWithData:
  7. initWithData:scale:

NSJSONSerialization:

  1. JSONObjectWithData:options:error:
  2. JSONObjectWithStream:options:error:
  3. dataWithJSONObject:options:error:
  4. writeJSONObject:toStream:options:error:

NSManagedObjectContext

  1. executeFetchRequest:error:
  2. processPendingChanges

(4)在ARC模式下,你也可以使用如下宏拦截你想统计的应用中关键方法:

1
2
3
4
5
6
7
8
- (void)myMethod
{
  NR_TRACE_METHOD_START(0);

  // … existing code

  NR_TRACE_METHOD_STOP;
}

更多的使用请关注其SDK文档说明:https://docs.newrelic.com/docs/mobile-monitoring/mobile-sdk-api/new-relic-mobile-sdk-api/work-ios-sdk-api

newRelic SDK的数据拦截上报

newRelic SDK已经做得非常精致,但是如果我们不想把数据上报到第三方平台管理,那应该怎么办呢,自己去研发一套,耗时费力,而且还费力不讨好。那何不做一个数据拦截,把数据转发到我们自己的后台平台呢?

static framework public的头文件并不包括上传class的头文件,但是当网络不好或者没有网的时候,我们可以从控制台打印上报数据失败的日志看到一些端倪,我们可以找到上传日志的方法所在的类为“NRMAHarvesterConnection”,如果是ssl上传,日志为send方法,如果是非ssl上传,日志中显示为sendData方法;

为了更好的知道类中方法的分布,我们可以用class-dump-z工具解析出SDK中所有的头文件。 class-dump-z是不能直接dump framework中的头文件,解析出来的头文件中会有一个source 为null的提示。 一开始觉得可能是因为framework做了加密处理,找了一个越狱机器在cydia中安装了openssh-how-to工具,通过sftp把framework和Clutch上传到越狱机器中,开始用Clutch进行解密,发现Clutch并不能直接解析,也不能解析debug安装的调试app。

最后终于将集成了newRelic SDK的的app 通过archive打包,导出一个ipa。解压出app目录之后,再用class-dump-z工具对app目录的二进制文件进行头文件导出,这时候我们就能够看到sdk中的所有头文件了;

我找到“NRMAHarvesterConnection”文件,如下所示,可以看到其中的sendData方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#import <XXUnknownSuperclass.h> // Unknown library

@class NRMAConnectInformation, NSString;

@interface NRMAHarvesterConnection : XXUnknownSuperclass {
  BOOL _useSSL;
  NRMAConnectInformation* _connectionInformation;
  NSString* _collectorHost;
  NSString* _applicationToken;
  NSString* _crossProcessID;
  long long _serverTimestamp;
}
@property(assign) BOOL useSSL;
@property(retain) NSString* crossProcessID;
@property(assign) long long serverTimestamp;
@property(retain) NSString* applicationToken;
@property(retain) NSString* collectorHost;
@property(retain) NRMAConnectInformation* connectionInformation;
-(void).cxx_destruct;
-(id)collectorHostURL:(id)url;
-(id)collectorHostDataURL;
-(id)collectorConnectURL;
-(id)createDataPost:(id)post;
-(id)createConnectPost:(id)post;
-(id)sendData:(id)data;
-(id)sendConnect;
-(id)send:(id)send;
-(id)createPostWithURI:(id)uri message:(id)message;
-(id)init;
@end

为了更好的了解方法的输入和输出参数,我使用了阿里最近开源的ali-wax的lua调试工具进行了sendData的方法的输入输出了解, 我们可以看到输入参数data的class类型,是一个“NRMAHarvestData”,我们同样可以找到其头文件,查询到里面的JSONObject方法,可以把data转化成一个json对象。 得到这个对象了我们就可以将数据转发到我们自己的平台,并伪造一个http上传成功或失败的对象给sendData方法进行内存数据删除处理即可。

1
2
3
4
5
6
7
8
waxClass{"NRMAHarvesterConnection"}

--如果需要上报,拦截这个方法即可
function sendData(self,data)
  print(data:class())
  jsonObject = data:JSONObject()
  self:ORIGsendData(data)
end

注:这里我只提供一种方案,公司产品并没有采用这种方法,因为我们自己的性能监控虽然数据粒度不够,已经足够日常查询问题使用了。更多是分享一下取巧的思路,以及纪录一下破解的过程。

Ali-Wax使用和源码解析系列-Wax的集成和调试环境搭建

那些多余的话

在ios平台的基础框架中,代码的直接动态部署一直是一个无法解决的问题,所以我们只能退而求其次,通过建立脚本语言和OC的bridge来实现动态更新的目标。在这个方向上,wax-lua框架是第一个解决方案,但是随着wax-lua作者的放弃维护,wax框架逐渐被降级为做补丁修复的工具。之后随着objc语言的完善和SDK中JavaScriptCore(webkit)framework api的开放,另外一种脚本语言js以一种更为被苹果认可的姿态进入了这个方向,先有国外FaceBook发布的ReactNative框架,让更多的前端开发者可以通过js语言完成可以毗邻native效果的app,后有国内腾讯的bang开源JSPatch,专注于ios平台Patch修复。

本人最近一直在维护公司内部的的wax框架,也一直在探索ios热更新方向上的新技术。先是wax框架的兼容64位维护,wax框架集成到产品后多个组件交叉使用runtime的bug修复;再是探索Tianium跨平台JS框架;再到FaceBook出品开源的ReactNative框架;再到最后的JSPatch… 这些框架的源码和Demo我都有涉及,从希望到失望…首先是自己维护的wax框架稳健性不够,特别是通过AOP+forward兼容64位后bug频现,用作patch热更新尚且能够应付,但是想用来做组件动态部署成熟度还不够。其次Tianium框架,关于jscore部分没有开源不敢用,另外其bind oc api部分需要时刻跟进新版本的SDK是一个low点。 再其次是ReactNative框架,源码我没有怎么看,但是我写了一个稍微复杂的页面,当图片在同一个页面加载较多的时候,卡顿的现象特别明显,让我对js的效率产生了深深的怀疑。 JSPatch同样是利用了forward消息转发机制完成了js和oc的bind,但是社区和框架成熟度仍然不够。

最近阿里开源了增强版的wax框架,我连续看了两天源码,对其增强部分的实现有了一个大致的了解,也让我对lua-bind的方案有了很大的信心。之前受Andfix android平台热更新框架思路的影响,自己很想在这条路上再继续折腾一番:如果能够把OC代码通过一个转换器转化成lua代码,ios平台的准直接动态部署应该是可以实现的。 所有的开发人员仍然用自己最熟悉的objc开发需求,测试和发版,比较版本代码的不同可以让低版本的产品用户使用高版本的功能。我们可以不按照版本发布,转而按照模块开发,所有的新需求和新运营方案都可以在不发版的情况推送给用户。

Ali-Wax简介

代码地址:https://github.com/alibaba/wax.git

wax-lua 的语言优势:

  • (1)自动垃圾回收:再也不用使用alloc,retain和release;
  • (2)代码少:没有头文件,没有static类型、array常量、dictionary常量;
  • (3)能够使用任何一个framework,例如cocoa、UITouch、Fondation等,任何用oc写的framework,wax自动将其暴露给lua使用;
  • (4)超级简单的http请求: 和REST的web service一起交互使用;
  • (5)lua也有函数闭包,也就是所谓的blocks;
  • (6)lua有内置的Regex-like 模式匹配library;

相比原Wax框架:

  • 64位支持;
  • 线程安全;
  • 其他一些特性:
  1. lua function 转化成 oc block,
  2. 在lua中调用oc block,
  3. getting/setting 私有成员变量,
  4. 内置通用的C函数,
  5. 支持lua代码debug;

在接下来的博客系列中,我将结合Ali—Wax源码,介绍如何实现这些增强特性的,敬请期待。下面我们先来看看如何使用和集成wax框架和其debug环境。

Ali-Wax的Podfile集成

Wax框架只是一个热更新实现方案,我们用于具体的产品进行补丁修复或者功能组件发布还需要实现一个版本管理方案。在实现版本方案的时候,肯定涉及到在wax框架上的功能定制,比如说补丁现场恢复功能等(这部分功能我会开源到我的fork代码中)。另外我们现在用CocoaPods管理我们的代码组件版本,为了方便各自的产品修复bug,发放版本。 我建议大家fork一份代码到自己的github中,进行版本管理。

fork Ali-Wax: https://github.com/philonpang/wax.git

另外Ali-Wax框架中集成了lua的debug方案:mobdebug,位于wax/tools/mobdebug处,作者配置了Podspec,可以直接pod到你的调试工程中。 这部分代码也不会有什么变化,已建议作者单独列一个git工程,目前我放到了自己的git空间里,大家可以共享。

mobdebug: https://github.com/philonpang/mobdebug.git

这样我们就可以在项目工程中通过Pofile直接集成wax框架和代码调试环境了(当然你也可以通过path指向wax clone项目的本地);podifle如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
source 'https://github.com/CocoaPods/Specs.git'

platform :ios, '6.0'
inhibit_all_warnings!

workspace 'WLDWaxService'
xcodeproj 'LDWaxService'

target :LDWaxService do
    pod 'wax', :git => 'https://github.com/philonpang/wax.git'
    pod 'mobdebug', :git  => 'https://github.com/philonpang/mobdebug.git'
    link_with 'LDWaxService'
end

Ali-Wax的luaDebug

作者通过编译luasocket源代码支持了lua代码的ios环境调试。作者刚发布的时候的调试环境也折腾了我很久,我这里说一下我踩过的那些坑和注意事项。 官方调试安装步骤见https://github.com/alibaba/wax/tree/master/examples/LuaCodeDebug

  • download ZeroBraneStudio (lua代码调试IDE,直接git clone到本地)
  • run ZeroBraneStudio: 进入ZeroBraneStudio/zbstudio文件夹,直接双击ZeroBraneStudio.app文件即可运行(将app拷贝到application中不能运行)
  • import lua code: click the 6th buttonSmaller icon, choose your lua code’s root directory
  • start debug server: click Project->Start Debugger Server. (每次打开ZeroBraneStudio的时候,记得打开这个选项)
  • run this code before you enter debug
1
2
3
wax_start(nil, nil);//must start before debug
extern void luaopen_mobdebug_scripts(void* L);
luaopen_mobdebug_scripts(wax_currentLuaState());
  • addrequire('mobdebug').start('YOUR_MAC_IP_ADDRESS')to your lua code. if you use simulator 'YOUR_MAC_IP_ADDRESS' can be empty
  • launch your app,when require('mobdebug').start() is invoked, ZeroBraneStudio’s dock will become active, then you should add breakpoint. (这种情况下,当你在模拟器中运行app后,会自动跳在ZeroBraneStudio中启动lua文件中断点下来)

坑1:import luaCode 的tip

官方截图上看到的截图是整个xcode工程的目录,如果你import了整个工程,在目录和文件较多的情况下,ZeroBraneStudio中的断点是无效的。 其实你只需要import存放lua文件的目录即可。

坑2:xcode工程中的lua代码文件

xcode添加文件有两种方式:Create groups 和 Create folder reference两种。 按照前一种方式,你引入目录的所有文件在运行时会被拷贝到“XX.app/”目录下,而后面一种方式目录会以引用方式加入,运行时文件仍然放在“XX.app/拷贝目录/”下,而且当引入目录中有新文件添加的时候,不用再执行一遍添加操作。

所以我们在本地调试代码的时候,建议以第二种方式引入目录,这样我们可以随时在ZeroBraneStudio中添加文件(而不用在xcode工程中再进行一次add操作)进行调试。 但是执行lua代码中的require代码行时,会提示找不到对应的require文件。这其实是lua 默认search 文件的路径没有设置照成的,所以我们需要设置lua的search路径环境变量。 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#import "wax.h"
#import "wax_http.h"
#import "wax_json.h"
#import "wax_xml.h"


    //设置lua的search路径环境变量
    NSString *patchPath = [[NSBundle mainBundle] pathForResource:@"patchDemo" ofType:nil];
    NSString *pp = [NSString stringWithFormat:@"%@/?.lua;", patchPath];
    setenv(LUA_PATH, [pp UTF8String], 1);

    //启动wax框架和debug环境
    wax_start(nil, luaopen_wax_http, luaopen_wax_json, luaopen_wax_xml, nil);//must start
    extern void luaopen_mobdebug_scripts(void* L);
    void * p = wax_currentLuaState();
    luaopen_mobdebug_scripts(p);

    //运行启动文件
    wax_runLuaString("require('patch')");

完成以上步骤之后,你就可以在你的项目工程中开心的调试lua代码了。如下图所示: