├── .gitignore ├── LICENSE ├── README.md └── iOS 无痕埋点方案 ├── iOS 无痕埋点方案.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist └── iOS 无痕埋点方案 ├── AppDelegate.h ├── AppDelegate.m ├── Assets.xcassets ├── AppIcon.appiconset │ └── Contents.json └── Contents.json ├── Base.lproj └── LaunchScreen.storyboard ├── CrashAvoid ├── CrashAvoidConst.h ├── CrashAvoidManager.h ├── CrashAvoidManager.m ├── NSArray+safe.h ├── NSArray+safe.m ├── NSObject+avoid.h └── NSObject+avoid.m ├── HookClass ├── UICollectionView+Analysis.h ├── UICollectionView+Analysis.m ├── UIControl+Analysis.h ├── UIControl+Analysis.m ├── UIGestureRecognizer+Analysis.h ├── UIGestureRecognizer+Analysis.m ├── UITableView+Analysis.h ├── UITableView+Analysis.m ├── UIViewController+Analysis.h └── UIViewController+Analysis.m ├── Info.plist ├── Models ├── LikeModel.h ├── LikeModel.m ├── TestModel.h ├── TestModel.m ├── ThirdModel.h └── ThirdModel.m ├── MyCell.h ├── MyCell.m ├── PrefixHeader.pch ├── RootViewController.h ├── RootViewController.m ├── SecondViewController.h ├── SecondViewController.m ├── Source └── 20180719090759.json ├── TestTableview.h ├── TestTableview.m ├── Tools ├── CaptureTool.h ├── CaptureTool.m ├── DataContainer.h ├── DataContainer.m ├── MethodSwizzingTool.h ├── MethodSwizzingTool.m ├── UIGestureRecognizer+gesture.h └── UIGestureRecognizer+gesture.m ├── ViewController.h ├── ViewController.m └── main.m /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | # CocoaPods 32 | # 33 | # We recommend against adding the Pods directory to your .gitignore. However 34 | # you should judge for yourself, the pros and cons are mentioned at: 35 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 36 | # 37 | # Pods/ 38 | 39 | # Carthage 40 | # 41 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 42 | # Carthage/Checkouts 43 | 44 | Carthage/Build 45 | 46 | # fastlane 47 | # 48 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 49 | # screenshots whenever they are needed. 50 | # For more information about the recommended setup visit: 51 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 52 | 53 | fastlane/report.xml 54 | fastlane/Preview.html 55 | fastlane/screenshots/**/*.png 56 | fastlane/test_output 57 | 58 | # Code Injection 59 | # 60 | # After new code Injection tools there's a generated folder /iOSInjectionProject 61 | # https://github.com/johnno1962/injectionforxcode 62 | 63 | iOSInjectionProject/ 64 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #前言 2 | > 当前互联网行业的竞争已经是非常激烈了, “功能驱动”的时代已经过去了, 现在更加注重软件的细节, 以及用户的体验问题。 说到用户体验,就不得不提到用户的操作行为。 在我们的软件中,我们会到处进行埋点, 以便提取到我们想要的数据,进而分析用户的行为习惯。 通过这些数据,我们也可以更好的分析出用户的操作趋势,从而在用户体验上把我们的app做的更好。 3 | 4 | 随着公司业务的发展,数据的重要性日益体现出来。 数据埋点的全面性和准确性尤为重要。 只有拿到精准并详细的数据, 后面的分析才有意义。 然后随着业务的不断变化, 埋点的动态性也越来越重要。为了解决这些问题, 很多公司都提出自己的解决方案, 各中解决方案中,大体分为以下三种: 5 | 1. **代码埋点** 6 | 由开发人员在触发事件的具体方法里,植入多行代码把需要上传的参数上报至服务端。 7 | 8 | 2. **可视化埋点** 9 | 根据标识来识别每一个事件, 针对指定的事件进行取参埋点。而事件的标识与参数信息都写在配置表中,通过动态下发配置表来实现埋点统计。 10 | 11 | 3. **无埋点** 12 | 无埋点并不是不需要埋点,更准确的说应该是“全埋”, 前端的任意一个事件都被绑定一个标识,所有的事件都别记录下来。 通过定期上传记录文件,配合文件解析,解析出来我们想要的数据, 并生成可视化报告供专业人员分析 , 因此实现“无埋点”统计。 13 | 14 | --- 15 | 由于考虑到“无埋点”的方案成本较高,并且后期解析也比较复杂,加上view_path的不确定性(具体可以参考: [网易HubbleData无埋点SDK在iOS端的设计与实现](https://neyoufan.github.io/2017/04/19/ios/%E7%BD%91%E6%98%93HubbleData%E6%97%A0%E5%9F%8B%E7%82%B9SDK%E5%9C%A8iOS%E7%AB%AF%E7%9A%84%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0/))。所以本文重点分享一个 **可视化埋点** 的简单实现方式。 16 | 17 | #可视化埋点 18 | 首先,可视化埋点并非完全抛弃了代码埋点,而是在代码埋点的上层封装的一套逻辑来代替手工埋点,大体上架构如下图: 19 | ![image](http://upload-images.jianshu.io/upload_images/3104472-15d0364de7f22ecd?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 20 | 21 | 不过要实现可视化埋点也有很多问题需要解决,比如事件唯一标识的确定,业务参数的获取,有逻辑判断的埋点配置项信息等等。接下来我会重点围绕唯一标识以及业务参数获取这两个问题给出自己的一个解决方案。 22 | 23 | ##唯一标识问题 24 | 唯一标识的组成方式主要是又 **target + action** 来确定, 即任何一个事件都存在一个target与action。 在此引入AOP编程,AOP(Aspect-Oriented-Programming)即面向切面编程的思想,基于 Runtime 的 Method Swizzling能力,来 hook 相应的方法,从而在hook方法中进行统一的埋点处理。例如所有的按钮被点击时,都会触发UIApplication的sendAction方法,我们hook这个方法,即可拦截所有按钮的点击事件。 25 | ![image](http://upload-images.jianshu.io/upload_images/3104472-3b1942be410c1e02?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 26 | 27 | 这里主要分为两个部分 : 28 | * 事件的锁定 29 | 事件的锁定主要是靠 “事件唯一标识符”来锁定,而事件的唯一标识是由我们写入配置表中的。 30 | 31 | * 埋点数据的上报。 32 | 埋点数据的数据又分为两种类型: **固定数据**与**可变的业务数据**, 而固定数据我们可以直接写到配置表中, 通过唯一标识来获取。而对于业务数据,我是这么理解的: 数据是有持有者的, 例如我们Controller的一个属性值, 又或者数据再Model的某一个层级。 这么的话我们就可以通过KVC的的方式来递归获取该属性的值来取到业务数据, 代码后面会有介绍。 33 | 34 | ## 整体代码示例 35 | 由于iOS中的事件场景是多样的, 在此我以UIControl, UITablview(collectionView与tableView基本相同), UITapGesture, UIViewController的PV统计 为例,介绍一下具体思路。 36 | 37 | 1. UIViewController PV统计 38 | 页面的统计较为简单,利用Method Swizzing hook 系统的viewDidLoad, 直接通过页面名称即可锁定页面的展示代码如下: 39 | ``` 40 | @implementation UIViewController (Analysis) 41 | 42 | +(void)load 43 | { 44 | static dispatch_once_t onceToken; 45 | dispatch_once(&onceToken, ^{ 46 | 47 | SEL originalDidLoadSelector = @selector(viewDidLoad); 48 | SEL swizzingDidLoadSelector = @selector(user_viewDidLoad); 49 | [MethodSwizzingTool swizzingForClass:[self class] originalSel:originalDidLoadSelector swizzingSel:swizzingDidLoadSelector]; 50 | 51 | }); 52 | } 53 | 54 | -(void)user_viewDidLoad 55 | { 56 | [self user_viewDidLoad]; 57 | 58 | //从配置表中取参数的过程 1 固定参数 2 业务参数(此处参数被target持有) 59 | NSString * identifier = [NSString stringWithFormat:@"%@", [self class]]; 60 | NSDictionary * dic = [[[DataContainer dataInstance].data objectForKey:@"PAGEPV"] objectForKey:identifier]; 61 | if (dic) { 62 | NSString * pageid = dic[@"userDefined"][@"pageid"]; 63 | NSString * pagename = dic[@"userDefined"][@"pagename"]; 64 | NSDictionary * pagePara = dic[@"pagePara"]; 65 | 66 | __block NSMutableDictionary * uploadDic = [NSMutableDictionary dictionaryWithCapacity:0]; 67 | [pagePara enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { 68 | 69 | id value = [CaptureTool captureVarforInstance:self withPara:obj]; 70 | if (value && key) { 71 | [uploadDic setObject:value forKey:key]; 72 | } 73 | }]; 74 | 75 | NSLog(@"\n 事件唯一标识为:%@ \n pageid === %@,\n pagename === %@,\n pagepara === %@ \n", [self class], pageid, pagename, uploadDic); 76 | } 77 | } 78 | ``` 79 | 80 | 81 | 2. UIControl 点击统计。 82 | 主要通过hook **sendAction:to:forEvent:** 来实现, 其唯一标识符我们用 targetname/selector/tag来标记,具体代码如下: 83 | ~~~ 84 | @implementation UIControl (Analysis) 85 | 86 | +(void)load 87 | { 88 | static dispatch_once_t onceToken; 89 | dispatch_once(&onceToken, ^{ 90 | SEL originalSelector = @selector(sendAction:to:forEvent:); 91 | SEL swizzingSelector = @selector(user_sendAction:to:forEvent:); 92 | [MethodSwizzingTool swizzingForClass:[self class] originalSel:originalSelector swizzingSel:swizzingSelector]; 93 | }); 94 | } 95 | 96 | 97 | -(void)user_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event 98 | { 99 | [self user_sendAction:action to:target forEvent:event]; 100 | 101 | NSString * identifier = [NSString stringWithFormat:@"%@/%@/%ld", [target class], NSStringFromSelector(action),self.tag]; 102 | NSDictionary * dic = [[[DataContainer dataInstance].data objectForKey:@"ACTION"] objectForKey:identifier]; 103 | if (dic) { 104 | 105 | NSString * eventid = dic[@"userDefined"][@"eventid"]; 106 | NSString * targetname = dic[@"userDefined"][@"target"]; 107 | NSString * pageid = dic[@"userDefined"][@"pageid"]; 108 | NSString * pagename = dic[@"userDefined"][@"pagename"]; 109 | NSDictionary * pagePara = dic[@"pagePara"]; 110 | __block NSMutableDictionary * uploadDic = [NSMutableDictionary dictionaryWithCapacity:0]; 111 | [pagePara enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { 112 | 113 | id value = [CaptureTool captureVarforInstance:target withPara:obj]; 114 | if (value && key) { 115 | [uploadDic setObject:value forKey:key]; 116 | } 117 | }]; 118 | 119 | 120 | NSLog(@" \n 唯一标识符为 : %@, \n event id === %@,\n target === %@, \n pageid === %@,\n pagename === %@,\n pagepara === %@ \n", identifier, eventid, targetname, pageid, pagename, uploadDic); 121 | } 122 | } 123 | ~~~ 124 | 125 | 3. TableView (CollectionView) 的点击统计。 126 | tablview的唯一标识, 我们使用 delegate.class/tableview.class/tableview.tag的组合来唯一锁定。 主要是通过hook **setDelegate** 方法, 在设置代理的时候再去交互 **didSelect** 方法来实现, 具体的原理是 具体代码如下: 127 | 128 | ~~~ 129 | @implementation UITableView (Analysis) 130 | 131 | +(void)load 132 | { 133 | static dispatch_once_t onceToken; 134 | dispatch_once(&onceToken, ^{ 135 | 136 | SEL originalAppearSelector = @selector(setDelegate:); 137 | SEL swizzingAppearSelector = @selector(user_setDelegate:); 138 | [MethodSwizzingTool swizzingForClass:[self class] originalSel:originalAppearSelector swizzingSel:swizzingAppearSelector]; 139 | }); 140 | } 141 | 142 | 143 | 144 | -(void)user_setDelegate:(id)delegate 145 | { 146 | [self user_setDelegate:delegate]; 147 | 148 | SEL sel = @selector(tableView:didSelectRowAtIndexPath:); 149 | 150 | SEL sel_ = NSSelectorFromString([NSString stringWithFormat:@"%@/%@/%ld", NSStringFromClass([delegate class]), NSStringFromClass([self class]),self.tag]); 151 | 152 | 153 | //因为 tableView:didSelectRowAtIndexPath:方法是optional的,所以没有实现的时候直接return 154 | if (![self isContainSel:sel inClass:[delegate class]]) { 155 | 156 | return; 157 | } 158 | 159 | 160 | BOOL addsuccess = class_addMethod([delegate class], 161 | sel_, 162 | method_getImplementation(class_getInstanceMethod([self class], @selector(user_tableView:didSelectRowAtIndexPath:))), 163 | nil); 164 | 165 | //如果添加成功了就直接交换实现, 如果没有添加成功,说明之前已经添加过并交换过实现了 166 | if (addsuccess) { 167 | Method selMethod = class_getInstanceMethod([delegate class], sel); 168 | Method sel_Method = class_getInstanceMethod([delegate class], sel_); 169 | method_exchangeImplementations(selMethod, sel_Method); 170 | } 171 | } 172 | 173 | 174 | //判断页面是否实现了某个sel 175 | - (BOOL)isContainSel:(SEL)sel inClass:(Class)class { 176 | unsigned int count; 177 | 178 | Method *methodList = class_copyMethodList(class,&count); 179 | for (int i = 0; i < count; i++) { 180 | Method method = methodList[i]; 181 | NSString *tempMethodString = [NSString stringWithUTF8String:sel_getName(method_getName(method))]; 182 | if ([tempMethodString isEqualToString:NSStringFromSelector(sel)]) { 183 | return YES; 184 | } 185 | } 186 | return NO; 187 | } 188 | 189 | 190 | // 由于我们交换了方法, 所以在tableview的 didselected 被调用的时候, 实质调用的是以下方法: 191 | -(void)user_tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath 192 | { 193 | 194 | SEL sel = NSSelectorFromString([NSString stringWithFormat:@"%@/%@/%ld", NSStringFromClass([self class]), NSStringFromClass([tableView class]), tableView.tag]); 195 | if ([self respondsToSelector:sel]) { 196 | IMP imp = [self methodForSelector:sel]; 197 | void (*func)(id, SEL,id,id) = (void *)imp; 198 | func(self, sel,tableView,indexPath); 199 | } 200 | 201 | 202 | NSString * identifier = [NSString stringWithFormat:@"%@/%@/%ld", [self class],[tableView class], tableView.tag]; 203 | NSDictionary * dic = [[[DataContainer dataInstance].data objectForKey:@"TABLEVIEW"] objectForKey:identifier]; 204 | if (dic) { 205 | 206 | NSString * eventid = dic[@"userDefined"][@"eventid"]; 207 | NSString * targetname = dic[@"userDefined"][@"target"]; 208 | NSString * pageid = dic[@"userDefined"][@"pageid"]; 209 | NSString * pagename = dic[@"userDefined"][@"pagename"]; 210 | NSDictionary * pagePara = dic[@"pagePara"]; 211 | 212 | 213 | 214 | UITableViewCell * cell = [tableView cellForRowAtIndexPath:indexPath]; 215 | __block NSMutableDictionary * uploadDic = [NSMutableDictionary dictionaryWithCapacity:0]; 216 | [pagePara enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { 217 | NSInteger containIn = [obj[@"containIn"] integerValue]; 218 | id instance = containIn == 0 ? self : cell; 219 | id value = [CaptureTool captureVarforInstance:instance withPara:obj]; 220 | if (value && key) { 221 | [uploadDic setObject:value forKey:key]; 222 | } 223 | }]; 224 | 225 | NSLog(@"\n event id === %@,\n target === %@, \n pageid === %@,\n pagename === %@,\n pagepara === %@ \n", eventid, targetname, pageid, pagename, uploadDic); 226 | } 227 | 228 | } 229 | 230 | @end 231 | 232 | ~~~ 233 | 234 | 235 | 4. gesture方式添加的的点击统计。 236 | gesture的事件,是通过 hook **initWithTarget:action:**方法来实现的, 事件的唯一标识依然是target.class/actionname来锁定的, 代码如下: 237 | ~~~ 238 | 239 | @implementation UIGestureRecognizer (Analysis) 240 | 241 | + (void)load 242 | { 243 | static dispatch_once_t onceToken; 244 | dispatch_once(&onceToken, ^{ 245 | 246 | [MethodSwizzingTool swizzingForClass:[self class] originalSel:@selector(initWithTarget:action:) swizzingSel:@selector(vi_initWithTarget:action:)]; 247 | }); 248 | } 249 | 250 | - (instancetype)vi_initWithTarget:(nullable id)target action:(nullable SEL)action 251 | { 252 | UIGestureRecognizer *selfGestureRecognizer = [self vi_initWithTarget:target action:action]; 253 | 254 | if (!target || !action) { 255 | return selfGestureRecognizer; 256 | } 257 | 258 | if ([target isKindOfClass:[UIScrollView class]]) { 259 | return selfGestureRecognizer; 260 | } 261 | 262 | Class class = [target class]; 263 | 264 | 265 | SEL originalSEL = action; 266 | 267 | NSString * sel_name = [NSString stringWithFormat:@"%s/%@", class_getName([target class]),NSStringFromSelector(action)]; 268 | SEL swizzledSEL = NSSelectorFromString(sel_name); 269 | 270 | //给原对象添加一共名字为 “sel_name”的方法,并将方法的实现指向本类中的 responseUser_gesture:方法的实现 271 | BOOL isAddMethod = class_addMethod(class, 272 | swizzledSEL, 273 | method_getImplementation(class_getInstanceMethod([self class], @selector(responseUser_gesture:))), 274 | nil); 275 | 276 | if (isAddMethod) { 277 | [MethodSwizzingTool swizzingForClass:class originalSel:originalSEL swizzingSel:swizzledSEL]; 278 | } 279 | 280 | //将gesture的对应的sel存储到 methodName属性中,主要是方便 responseUser_gesture: 方法中取出来 281 | self.methodName = NSStringFromSelector(action); 282 | return selfGestureRecognizer; 283 | } 284 | 285 | 286 | -(void)responseUser_gesture:(UIGestureRecognizer *)gesture 287 | { 288 | 289 | NSString * identifier = [NSString stringWithFormat:@"%s/%@", class_getName([self class]),gesture.methodName]; 290 | 291 | //调用原方法 292 | SEL sel = NSSelectorFromString(identifier); 293 | if ([self respondsToSelector:sel]) { 294 | IMP imp = [self methodForSelector:sel]; 295 | void (*func)(id, SEL,id) = (void *)imp; 296 | func(self, sel,gesture); 297 | } 298 | 299 | //处理业务,上报埋点 300 | NSDictionary * dic = [[[DataContainer dataInstance].data objectForKey:@"GESTURE"] objectForKey:identifier]; 301 | if (dic) { 302 | 303 | NSString * eventid = dic[@"userDefined"][@"eventid"]; 304 | NSString * targetname = dic[@"userDefined"][@"target"]; 305 | NSString * pageid = dic[@"userDefined"][@"pageid"]; 306 | NSString * pagename = dic[@"userDefined"][@"pagename"]; 307 | NSDictionary * pagePara = dic[@"pagePara"]; 308 | 309 | __block NSMutableDictionary * uploadDic = [NSMutableDictionary dictionaryWithCapacity:0]; 310 | [pagePara enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { 311 | id value = [CaptureTool captureVarforInstance:self withPara:obj]; 312 | if (value && key) { 313 | [uploadDic setObject:value forKey:key]; 314 | } 315 | }]; 316 | 317 | NSLog(@"\n event id === %@,\n target === %@, \n pageid === %@,\n pagename === %@,\n pagepara === %@ \n", eventid, targetname, pageid, pagename, uploadDic); 318 | 319 | } 320 | } 321 | 322 | 323 | ~~~ 324 | 325 | ## 配置表结构 326 | 首先那, 配置表是一个json数据。 针对不同的场景 (UIControl , 页面PV, Tabeview, Gesture)都做了区分, 用不同的key区别。 对于 "固定参数" , 我们之间写到配置表中,而对于业务参数, 我们之间写清楚参数在业务内的名字, 以及上传时的 keyName, 参数的持有者。 通过Runtime + KVC来取值。 配置表可以是这个样子:(仅供参考) 327 | 328 | > 说明: json最外层有四个Key, 分别为 ACTION PAGEPV TABLEVIEW GESTURE, 分别对应 UIControl的点击, 页面PV, tableview cell点击, Gesture 单击事件的参数。 每个key对应的value为json格式,Json中的keys, 即为唯一标识符。 标识符下的json有两个key : userDefine指的 固定数据, 即直接取值进行上报。 而pagePara为业务参数。 pagePara对应的value也是一个json, json的keys, 即上报的keys, value内的json包含三个参数: propertyName 为属性名字, containIn 参数只有0 ,1 两种情况, 其实这个参数主要是为tabview cell的点击取参做区别的,因为点击cell的时候, 上报的参数可能是被target持有,又或者是被cell本身持有 。 当containIn = 0的时候, 取参数时就从target中取值,= 1的时候就从cell中取值。 propertyPath 是一般备选项, 因为有时候从instace内递归取值的时候,可能会出现在不同的层级有相同的属性名字, 此时 propertyPath就派上用处了。 例如有属性 self.age 和 self.person.age , 其实如果需要self.person.age, 就把 propertyPath的值设为 person/age, 接着在取值的时候就会按照指定路径进行取值。 329 | ~~~ 330 | { 331 | "ACTION": { 332 | "ViewController/jumpSecond": { 333 | "userDefined": { 334 | "eventid": "201803074|93", 335 | "target": "", 336 | "pageid": "234", 337 | "pagename": "button点击,跳转至下一个页面" 338 | }, 339 | "pagePara": { 340 | "testKey9": { 341 | "propertyName": "testPara", 342 | "propertyPath":"", 343 | "containIn": "0" 344 | } 345 | } 346 | } 347 | }, 348 | 349 | "PAGEPV": { 350 | "ViewController": { 351 | "userDefined": { 352 | "pageid": "234", 353 | "pagename": "XXX 页面展示了" 354 | }, 355 | "pagePara": { 356 | "testKey10": { 357 | "propertyName": "testPara", 358 | "propertyPath":"", 359 | "containIn": "0" 360 | } 361 | } 362 | } 363 | }, 364 | "TABLEVIEW": { 365 | "ViewController/UITableView/0":{ 366 | "userDefined": { 367 | "eventid": "201803074|93", 368 | "target": "", 369 | "pageid": "234", 370 | "pagename": "tableview 被点击" 371 | }, 372 | "pagePara": { 373 | "user_grade": { 374 | "propertyName": "grade", 375 | "propertyPath":"", 376 | "containIn": "1" 377 | } 378 | } 379 | } 380 | }, 381 | 382 | "GESTURE": { 383 | "ViewController/controllerclicked:":{ 384 | "userDefined": { 385 | "eventid": "201803074|93", 386 | "target": "", 387 | "pageid": "123", 388 | "pagename": "手势响应" 389 | }, 390 | "pagePara": { 391 | "testKey1": { 392 | "propertyName": "testPara", 393 | "propertyPath":"", 394 | "containIn": "0" 395 | } 396 | } 397 | } 398 | } 399 | } 400 | 401 | ~~~ 402 | 403 | ## 取参方法 404 | ~~~ 405 | @implementation CaptureTool 406 | 407 | +(id)captureVarforInstance:(id)instance varName:(NSString *)varName 408 | { 409 | id value = [instance valueForKey:varName]; 410 | 411 | unsigned int count; 412 | objc_property_t *properties = class_copyPropertyList([instance class], &count); 413 | 414 | if (!value) { 415 | NSMutableArray * varNameArray = [NSMutableArray arrayWithCapacity:0]; 416 | for (int i = 0; i < count; i++) { 417 | objc_property_t property = properties[i]; 418 | NSString* propertyAttributes = [NSString stringWithUTF8String:property_getAttributes(property)]; 419 | NSArray* splitPropertyAttributes = [propertyAttributes componentsSeparatedByString:@"\""]; 420 | if (splitPropertyAttributes.count < 2) { 421 | continue; 422 | } 423 | NSString * className = [splitPropertyAttributes objectAtIndex:1]; 424 | Class cls = NSClassFromString(className); 425 | NSBundle *bundle2 = [NSBundle bundleForClass:cls]; 426 | if (bundle2 == [NSBundle mainBundle]) { 427 | // NSLog(@"自定义的类----- %@", className); 428 | const char * name = property_getName(property); 429 | NSString * varname = [[NSString alloc] initWithCString:name encoding:NSUTF8StringEncoding]; 430 | [varNameArray addObject:varname]; 431 | } else { 432 | // NSLog(@"系统的类"); 433 | } 434 | } 435 | 436 | for (NSString * name in varNameArray) { 437 | id newValue = [instance valueForKey:name]; 438 | if (newValue) { 439 | value = [newValue valueForKey:varName]; 440 | if (value) { 441 | return value; 442 | }else{ 443 | value = [[self class] captureVarforInstance:newValue varName:varName]; 444 | } 445 | } 446 | } 447 | } 448 | return value; 449 | } 450 | 451 | 452 | +(id)captureVarforInstance:(id)instance withPara:(NSDictionary *)para 453 | { 454 | NSString * properyName = para[@"propertyName"]; 455 | NSString * propertyPath = para[@"propertyPath"]; 456 | if (propertyPath.length > 0) { 457 | NSArray * keysArray = [propertyPath componentsSeparatedByString:@"/"]; 458 | 459 | return [[self class] captureVarforInstance:instance withKeys:keysArray]; 460 | } 461 | return [[self class] captureVarforInstance:instance varName:properyName]; 462 | } 463 | 464 | +(id)captureVarforInstance:(id)instance withKeys:(NSArray *)keyArray 465 | { 466 | id result = [instance valueForKey:keyArray[0]]; 467 | 468 | if (keyArray.count > 1 && result) { 469 | int i = 1; 470 | while (i < keyArray.count && result) { 471 | result = [result valueForKey:keyArray[i]]; 472 | i++; 473 | } 474 | } 475 | return result; 476 | } 477 | @end 478 | 479 | ~~~ 480 | 481 | --- 482 | # 结尾 483 | >以上是自己的一些想法与实践, 感觉目前的无痕埋点方案都还是不是很成熟, 不同的公司会有不同的方案, 但是可能大部分还是用的代码埋点的方式。 代码埋点的侵入性,维护性成本比较大, 尤其是当埋点特别多的时候, 有时候自己几个月前写的埋点代码,突然需要改,自己都要找半天才能找到。 并且代码埋点很致命的一个问题是无法动态更新, 即每次修改埋点,必须重新上线, 有时候上线后产品经理突然跑过来问:为什么埋点数据不太正常那, 此时你突然发现有一句埋点代码写错了, 这个时候你要么承认错误,承诺下次加上。要么赶快紧急上线解决。 通过以上方式,可以实现埋点的动态追加。 配置表可以通过服务端下载, 每次下载后就存在本地, 如果配置表有更新,只需要重新更新配置表就可以解决 。 方案中可能很多细节还需要完善,例如selector方法中存在业务逻辑判断,即一个标识符无法唯一的锁定一个埋点。 这种情况目前用配置表解决的成本较大, 并且业务是灵活的不好控制。 所以以上方案也只是涵盖了大部分场景, 并非所有场景都适用,具体大家可以根据业务情况来决定使用范围。 484 | -------------------------------------------------------------------------------- /iOS 无痕埋点方案/iOS 无痕埋点方案.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 872801182103168800C28D16 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 872801172103168800C28D16 /* AppDelegate.m */; }; 11 | 872801202103168900C28D16 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8728011F2103168900C28D16 /* Assets.xcassets */; }; 12 | 872801232103168900C28D16 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 872801212103168900C28D16 /* LaunchScreen.storyboard */; }; 13 | 872801262103168900C28D16 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 872801252103168900C28D16 /* main.m */; }; 14 | 87280131210316DC00C28D16 /* 20180719090759.json in Resources */ = {isa = PBXBuildFile; fileRef = 87280130210316DC00C28D16 /* 20180719090759.json */; }; 15 | 87280139210316F200C28D16 /* CrashAvoidManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 87280132210316F100C28D16 /* CrashAvoidManager.m */; }; 16 | 8728013A210316F200C28D16 /* NSArray+safe.m in Sources */ = {isa = PBXBuildFile; fileRef = 87280133210316F100C28D16 /* NSArray+safe.m */; }; 17 | 8728013B210316F200C28D16 /* NSObject+avoid.m in Sources */ = {isa = PBXBuildFile; fileRef = 87280138210316F200C28D16 /* NSObject+avoid.m */; }; 18 | 87280146210316FC00C28D16 /* UIGestureRecognizer+Analysis.m in Sources */ = {isa = PBXBuildFile; fileRef = 8728013C210316FC00C28D16 /* UIGestureRecognizer+Analysis.m */; }; 19 | 87280147210316FC00C28D16 /* UITableView+Analysis.m in Sources */ = {isa = PBXBuildFile; fileRef = 8728013F210316FC00C28D16 /* UITableView+Analysis.m */; }; 20 | 87280148210316FC00C28D16 /* UICollectionView+Analysis.m in Sources */ = {isa = PBXBuildFile; fileRef = 87280141210316FC00C28D16 /* UICollectionView+Analysis.m */; }; 21 | 87280149210316FC00C28D16 /* UIControl+Analysis.m in Sources */ = {isa = PBXBuildFile; fileRef = 87280142210316FC00C28D16 /* UIControl+Analysis.m */; }; 22 | 8728014A210316FC00C28D16 /* UIViewController+Analysis.m in Sources */ = {isa = PBXBuildFile; fileRef = 87280145210316FC00C28D16 /* UIViewController+Analysis.m */; }; 23 | 872801512103171000C28D16 /* CaptureTool.m in Sources */ = {isa = PBXBuildFile; fileRef = 8728014B2103170F00C28D16 /* CaptureTool.m */; }; 24 | 872801522103171000C28D16 /* DataContainer.m in Sources */ = {isa = PBXBuildFile; fileRef = 8728014D2103171000C28D16 /* DataContainer.m */; }; 25 | 872801532103171000C28D16 /* MethodSwizzingTool.m in Sources */ = {isa = PBXBuildFile; fileRef = 8728014E2103171000C28D16 /* MethodSwizzingTool.m */; }; 26 | 872801562103171800C28D16 /* SecondViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 872801542103171800C28D16 /* SecondViewController.m */; }; 27 | 872801592103172700C28D16 /* MyCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 872801572103172600C28D16 /* MyCell.m */; }; 28 | 872801612103174100C28D16 /* TestModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 8728015B2103174000C28D16 /* TestModel.m */; }; 29 | 872801622103174100C28D16 /* LikeModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 8728015C2103174000C28D16 /* LikeModel.m */; }; 30 | 872801632103174100C28D16 /* ThirdModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 8728015D2103174100C28D16 /* ThirdModel.m */; }; 31 | 872801662103175600C28D16 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 872801652103175600C28D16 /* ViewController.m */; }; 32 | 872EACAC21BB94D4000766B7 /* RootViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 872EACA921BB94D4000766B7 /* RootViewController.m */; }; 33 | 872EACAD21BB94D4000766B7 /* TestTableview.m in Sources */ = {isa = PBXBuildFile; fileRef = 872EACAA21BB94D4000766B7 /* TestTableview.m */; }; 34 | 872EACB021BB9512000766B7 /* UIGestureRecognizer+gesture.m in Sources */ = {isa = PBXBuildFile; fileRef = 872EACAE21BB9512000766B7 /* UIGestureRecognizer+gesture.m */; }; 35 | /* End PBXBuildFile section */ 36 | 37 | /* Begin PBXFileReference section */ 38 | 872801132103168800C28D16 /* iOS 无痕埋点方案.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "iOS 无痕埋点方案.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 39 | 872801162103168800C28D16 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 40 | 872801172103168800C28D16 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 41 | 8728011F2103168900C28D16 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 42 | 872801222103168900C28D16 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 43 | 872801242103168900C28D16 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 44 | 872801252103168900C28D16 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 45 | 87280130210316DC00C28D16 /* 20180719090759.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = 20180719090759.json; sourceTree = ""; }; 46 | 87280132210316F100C28D16 /* CrashAvoidManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CrashAvoidManager.m; sourceTree = ""; }; 47 | 87280133210316F100C28D16 /* NSArray+safe.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSArray+safe.m"; sourceTree = ""; }; 48 | 87280134210316F100C28D16 /* CrashAvoidConst.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CrashAvoidConst.h; sourceTree = ""; }; 49 | 87280135210316F100C28D16 /* CrashAvoidManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CrashAvoidManager.h; sourceTree = ""; }; 50 | 87280136210316F200C28D16 /* NSArray+safe.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSArray+safe.h"; sourceTree = ""; }; 51 | 87280137210316F200C28D16 /* NSObject+avoid.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSObject+avoid.h"; sourceTree = ""; }; 52 | 87280138210316F200C28D16 /* NSObject+avoid.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSObject+avoid.m"; sourceTree = ""; }; 53 | 8728013C210316FC00C28D16 /* UIGestureRecognizer+Analysis.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIGestureRecognizer+Analysis.m"; sourceTree = ""; }; 54 | 8728013D210316FC00C28D16 /* UIViewController+Analysis.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIViewController+Analysis.h"; sourceTree = ""; }; 55 | 8728013E210316FC00C28D16 /* UITableView+Analysis.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UITableView+Analysis.h"; sourceTree = ""; }; 56 | 8728013F210316FC00C28D16 /* UITableView+Analysis.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UITableView+Analysis.m"; sourceTree = ""; }; 57 | 87280140210316FC00C28D16 /* UICollectionView+Analysis.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UICollectionView+Analysis.h"; sourceTree = ""; }; 58 | 87280141210316FC00C28D16 /* UICollectionView+Analysis.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UICollectionView+Analysis.m"; sourceTree = ""; }; 59 | 87280142210316FC00C28D16 /* UIControl+Analysis.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIControl+Analysis.m"; sourceTree = ""; }; 60 | 87280143210316FC00C28D16 /* UIControl+Analysis.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIControl+Analysis.h"; sourceTree = ""; }; 61 | 87280144210316FC00C28D16 /* UIGestureRecognizer+Analysis.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIGestureRecognizer+Analysis.h"; sourceTree = ""; }; 62 | 87280145210316FC00C28D16 /* UIViewController+Analysis.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIViewController+Analysis.m"; sourceTree = ""; }; 63 | 8728014B2103170F00C28D16 /* CaptureTool.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CaptureTool.m; sourceTree = ""; }; 64 | 8728014C2103171000C28D16 /* DataContainer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DataContainer.h; sourceTree = ""; }; 65 | 8728014D2103171000C28D16 /* DataContainer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DataContainer.m; sourceTree = ""; }; 66 | 8728014E2103171000C28D16 /* MethodSwizzingTool.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MethodSwizzingTool.m; sourceTree = ""; }; 67 | 8728014F2103171000C28D16 /* MethodSwizzingTool.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MethodSwizzingTool.h; sourceTree = ""; }; 68 | 872801502103171000C28D16 /* CaptureTool.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CaptureTool.h; sourceTree = ""; }; 69 | 872801542103171800C28D16 /* SecondViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SecondViewController.m; sourceTree = ""; }; 70 | 872801552103171800C28D16 /* SecondViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SecondViewController.h; sourceTree = ""; }; 71 | 872801572103172600C28D16 /* MyCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MyCell.m; sourceTree = ""; }; 72 | 872801582103172600C28D16 /* MyCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MyCell.h; sourceTree = ""; }; 73 | 8728015B2103174000C28D16 /* TestModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TestModel.m; sourceTree = ""; }; 74 | 8728015C2103174000C28D16 /* LikeModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LikeModel.m; sourceTree = ""; }; 75 | 8728015D2103174100C28D16 /* ThirdModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThirdModel.m; sourceTree = ""; }; 76 | 8728015E2103174100C28D16 /* LikeModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LikeModel.h; sourceTree = ""; }; 77 | 8728015F2103174100C28D16 /* TestModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TestModel.h; sourceTree = ""; }; 78 | 872801602103174100C28D16 /* ThirdModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ThirdModel.h; sourceTree = ""; }; 79 | 872801642103175600C28D16 /* ViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; 80 | 872801652103175600C28D16 /* ViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; 81 | 872801672103176B00C28D16 /* PrefixHeader.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PrefixHeader.pch; sourceTree = ""; }; 82 | 872EACA821BB94D4000766B7 /* TestTableview.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TestTableview.h; sourceTree = ""; }; 83 | 872EACA921BB94D4000766B7 /* RootViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RootViewController.m; sourceTree = ""; }; 84 | 872EACAA21BB94D4000766B7 /* TestTableview.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TestTableview.m; sourceTree = ""; }; 85 | 872EACAB21BB94D4000766B7 /* RootViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RootViewController.h; sourceTree = ""; }; 86 | 872EACAE21BB9512000766B7 /* UIGestureRecognizer+gesture.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIGestureRecognizer+gesture.m"; sourceTree = ""; }; 87 | 872EACAF21BB9512000766B7 /* UIGestureRecognizer+gesture.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIGestureRecognizer+gesture.h"; sourceTree = ""; }; 88 | /* End PBXFileReference section */ 89 | 90 | /* Begin PBXFrameworksBuildPhase section */ 91 | 872801102103168800C28D16 /* Frameworks */ = { 92 | isa = PBXFrameworksBuildPhase; 93 | buildActionMask = 2147483647; 94 | files = ( 95 | ); 96 | runOnlyForDeploymentPostprocessing = 0; 97 | }; 98 | /* End PBXFrameworksBuildPhase section */ 99 | 100 | /* Begin PBXGroup section */ 101 | 8728010A2103168800C28D16 = { 102 | isa = PBXGroup; 103 | children = ( 104 | 872801152103168800C28D16 /* iOS 无痕埋点方案 */, 105 | 872801142103168800C28D16 /* Products */, 106 | ); 107 | sourceTree = ""; 108 | }; 109 | 872801142103168800C28D16 /* Products */ = { 110 | isa = PBXGroup; 111 | children = ( 112 | 872801132103168800C28D16 /* iOS 无痕埋点方案.app */, 113 | ); 114 | name = Products; 115 | sourceTree = ""; 116 | }; 117 | 872801152103168800C28D16 /* iOS 无痕埋点方案 */ = { 118 | isa = PBXGroup; 119 | children = ( 120 | 8728012F210316D000C28D16 /* Tools */, 121 | 8728012E210316C900C28D16 /* HookClass */, 122 | 8728012D210316C100C28D16 /* CrashAvoid */, 123 | 8728012C210316B200C28D16 /* Source */, 124 | 8728015A2103173100C28D16 /* Models */, 125 | 872801162103168800C28D16 /* AppDelegate.h */, 126 | 872801172103168800C28D16 /* AppDelegate.m */, 127 | 872801642103175600C28D16 /* ViewController.h */, 128 | 872801652103175600C28D16 /* ViewController.m */, 129 | 872801552103171800C28D16 /* SecondViewController.h */, 130 | 872801542103171800C28D16 /* SecondViewController.m */, 131 | 872EACAB21BB94D4000766B7 /* RootViewController.h */, 132 | 872EACA921BB94D4000766B7 /* RootViewController.m */, 133 | 872EACA821BB94D4000766B7 /* TestTableview.h */, 134 | 872EACAA21BB94D4000766B7 /* TestTableview.m */, 135 | 872801582103172600C28D16 /* MyCell.h */, 136 | 872801572103172600C28D16 /* MyCell.m */, 137 | 872801672103176B00C28D16 /* PrefixHeader.pch */, 138 | 8728011F2103168900C28D16 /* Assets.xcassets */, 139 | 872801212103168900C28D16 /* LaunchScreen.storyboard */, 140 | 872801242103168900C28D16 /* Info.plist */, 141 | 872801252103168900C28D16 /* main.m */, 142 | ); 143 | path = "iOS 无痕埋点方案"; 144 | sourceTree = ""; 145 | }; 146 | 8728012C210316B200C28D16 /* Source */ = { 147 | isa = PBXGroup; 148 | children = ( 149 | 87280130210316DC00C28D16 /* 20180719090759.json */, 150 | ); 151 | path = Source; 152 | sourceTree = ""; 153 | }; 154 | 8728012D210316C100C28D16 /* CrashAvoid */ = { 155 | isa = PBXGroup; 156 | children = ( 157 | 87280134210316F100C28D16 /* CrashAvoidConst.h */, 158 | 87280135210316F100C28D16 /* CrashAvoidManager.h */, 159 | 87280132210316F100C28D16 /* CrashAvoidManager.m */, 160 | 87280136210316F200C28D16 /* NSArray+safe.h */, 161 | 87280133210316F100C28D16 /* NSArray+safe.m */, 162 | 87280137210316F200C28D16 /* NSObject+avoid.h */, 163 | 87280138210316F200C28D16 /* NSObject+avoid.m */, 164 | ); 165 | path = CrashAvoid; 166 | sourceTree = ""; 167 | }; 168 | 8728012E210316C900C28D16 /* HookClass */ = { 169 | isa = PBXGroup; 170 | children = ( 171 | 87280140210316FC00C28D16 /* UICollectionView+Analysis.h */, 172 | 87280141210316FC00C28D16 /* UICollectionView+Analysis.m */, 173 | 87280143210316FC00C28D16 /* UIControl+Analysis.h */, 174 | 87280142210316FC00C28D16 /* UIControl+Analysis.m */, 175 | 87280144210316FC00C28D16 /* UIGestureRecognizer+Analysis.h */, 176 | 8728013C210316FC00C28D16 /* UIGestureRecognizer+Analysis.m */, 177 | 8728013E210316FC00C28D16 /* UITableView+Analysis.h */, 178 | 8728013F210316FC00C28D16 /* UITableView+Analysis.m */, 179 | 8728013D210316FC00C28D16 /* UIViewController+Analysis.h */, 180 | 87280145210316FC00C28D16 /* UIViewController+Analysis.m */, 181 | ); 182 | path = HookClass; 183 | sourceTree = ""; 184 | }; 185 | 8728012F210316D000C28D16 /* Tools */ = { 186 | isa = PBXGroup; 187 | children = ( 188 | 872EACAF21BB9512000766B7 /* UIGestureRecognizer+gesture.h */, 189 | 872EACAE21BB9512000766B7 /* UIGestureRecognizer+gesture.m */, 190 | 872801502103171000C28D16 /* CaptureTool.h */, 191 | 8728014B2103170F00C28D16 /* CaptureTool.m */, 192 | 8728014C2103171000C28D16 /* DataContainer.h */, 193 | 8728014D2103171000C28D16 /* DataContainer.m */, 194 | 8728014F2103171000C28D16 /* MethodSwizzingTool.h */, 195 | 8728014E2103171000C28D16 /* MethodSwizzingTool.m */, 196 | ); 197 | path = Tools; 198 | sourceTree = ""; 199 | }; 200 | 8728015A2103173100C28D16 /* Models */ = { 201 | isa = PBXGroup; 202 | children = ( 203 | 8728015E2103174100C28D16 /* LikeModel.h */, 204 | 8728015C2103174000C28D16 /* LikeModel.m */, 205 | 8728015F2103174100C28D16 /* TestModel.h */, 206 | 8728015B2103174000C28D16 /* TestModel.m */, 207 | 872801602103174100C28D16 /* ThirdModel.h */, 208 | 8728015D2103174100C28D16 /* ThirdModel.m */, 209 | ); 210 | path = Models; 211 | sourceTree = ""; 212 | }; 213 | /* End PBXGroup section */ 214 | 215 | /* Begin PBXNativeTarget section */ 216 | 872801122103168800C28D16 /* iOS 无痕埋点方案 */ = { 217 | isa = PBXNativeTarget; 218 | buildConfigurationList = 872801292103168900C28D16 /* Build configuration list for PBXNativeTarget "iOS 无痕埋点方案" */; 219 | buildPhases = ( 220 | 8728010F2103168800C28D16 /* Sources */, 221 | 872801102103168800C28D16 /* Frameworks */, 222 | 872801112103168800C28D16 /* Resources */, 223 | ); 224 | buildRules = ( 225 | ); 226 | dependencies = ( 227 | ); 228 | name = "iOS 无痕埋点方案"; 229 | productName = "iOS 无痕埋点方案"; 230 | productReference = 872801132103168800C28D16 /* iOS 无痕埋点方案.app */; 231 | productType = "com.apple.product-type.application"; 232 | }; 233 | /* End PBXNativeTarget section */ 234 | 235 | /* Begin PBXProject section */ 236 | 8728010B2103168800C28D16 /* Project object */ = { 237 | isa = PBXProject; 238 | attributes = { 239 | LastUpgradeCheck = 0930; 240 | ORGANIZATIONNAME = SandyLoo; 241 | TargetAttributes = { 242 | 872801122103168800C28D16 = { 243 | CreatedOnToolsVersion = 9.3.1; 244 | }; 245 | }; 246 | }; 247 | buildConfigurationList = 8728010E2103168800C28D16 /* Build configuration list for PBXProject "iOS 无痕埋点方案" */; 248 | compatibilityVersion = "Xcode 9.3"; 249 | developmentRegion = en; 250 | hasScannedForEncodings = 0; 251 | knownRegions = ( 252 | en, 253 | Base, 254 | ); 255 | mainGroup = 8728010A2103168800C28D16; 256 | productRefGroup = 872801142103168800C28D16 /* Products */; 257 | projectDirPath = ""; 258 | projectRoot = ""; 259 | targets = ( 260 | 872801122103168800C28D16 /* iOS 无痕埋点方案 */, 261 | ); 262 | }; 263 | /* End PBXProject section */ 264 | 265 | /* Begin PBXResourcesBuildPhase section */ 266 | 872801112103168800C28D16 /* Resources */ = { 267 | isa = PBXResourcesBuildPhase; 268 | buildActionMask = 2147483647; 269 | files = ( 270 | 872801232103168900C28D16 /* LaunchScreen.storyboard in Resources */, 271 | 87280131210316DC00C28D16 /* 20180719090759.json in Resources */, 272 | 872801202103168900C28D16 /* Assets.xcassets in Resources */, 273 | ); 274 | runOnlyForDeploymentPostprocessing = 0; 275 | }; 276 | /* End PBXResourcesBuildPhase section */ 277 | 278 | /* Begin PBXSourcesBuildPhase section */ 279 | 8728010F2103168800C28D16 /* Sources */ = { 280 | isa = PBXSourcesBuildPhase; 281 | buildActionMask = 2147483647; 282 | files = ( 283 | 872801622103174100C28D16 /* LikeModel.m in Sources */, 284 | 872801562103171800C28D16 /* SecondViewController.m in Sources */, 285 | 87280148210316FC00C28D16 /* UICollectionView+Analysis.m in Sources */, 286 | 872801632103174100C28D16 /* ThirdModel.m in Sources */, 287 | 87280147210316FC00C28D16 /* UITableView+Analysis.m in Sources */, 288 | 8728014A210316FC00C28D16 /* UIViewController+Analysis.m in Sources */, 289 | 8728013A210316F200C28D16 /* NSArray+safe.m in Sources */, 290 | 872801532103171000C28D16 /* MethodSwizzingTool.m in Sources */, 291 | 872EACAD21BB94D4000766B7 /* TestTableview.m in Sources */, 292 | 872801662103175600C28D16 /* ViewController.m in Sources */, 293 | 87280139210316F200C28D16 /* CrashAvoidManager.m in Sources */, 294 | 872EACB021BB9512000766B7 /* UIGestureRecognizer+gesture.m in Sources */, 295 | 872801522103171000C28D16 /* DataContainer.m in Sources */, 296 | 872801592103172700C28D16 /* MyCell.m in Sources */, 297 | 872801512103171000C28D16 /* CaptureTool.m in Sources */, 298 | 87280149210316FC00C28D16 /* UIControl+Analysis.m in Sources */, 299 | 872EACAC21BB94D4000766B7 /* RootViewController.m in Sources */, 300 | 872801262103168900C28D16 /* main.m in Sources */, 301 | 872801612103174100C28D16 /* TestModel.m in Sources */, 302 | 872801182103168800C28D16 /* AppDelegate.m in Sources */, 303 | 87280146210316FC00C28D16 /* UIGestureRecognizer+Analysis.m in Sources */, 304 | 8728013B210316F200C28D16 /* NSObject+avoid.m in Sources */, 305 | ); 306 | runOnlyForDeploymentPostprocessing = 0; 307 | }; 308 | /* End PBXSourcesBuildPhase section */ 309 | 310 | /* Begin PBXVariantGroup section */ 311 | 872801212103168900C28D16 /* LaunchScreen.storyboard */ = { 312 | isa = PBXVariantGroup; 313 | children = ( 314 | 872801222103168900C28D16 /* Base */, 315 | ); 316 | name = LaunchScreen.storyboard; 317 | sourceTree = ""; 318 | }; 319 | /* End PBXVariantGroup section */ 320 | 321 | /* Begin XCBuildConfiguration section */ 322 | 872801272103168900C28D16 /* Debug */ = { 323 | isa = XCBuildConfiguration; 324 | buildSettings = { 325 | ALWAYS_SEARCH_USER_PATHS = NO; 326 | CLANG_ANALYZER_NONNULL = YES; 327 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 328 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 329 | CLANG_CXX_LIBRARY = "libc++"; 330 | CLANG_ENABLE_MODULES = YES; 331 | CLANG_ENABLE_OBJC_ARC = YES; 332 | CLANG_ENABLE_OBJC_WEAK = YES; 333 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 334 | CLANG_WARN_BOOL_CONVERSION = YES; 335 | CLANG_WARN_COMMA = YES; 336 | CLANG_WARN_CONSTANT_CONVERSION = YES; 337 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 338 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 339 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 340 | CLANG_WARN_EMPTY_BODY = YES; 341 | CLANG_WARN_ENUM_CONVERSION = YES; 342 | CLANG_WARN_INFINITE_RECURSION = YES; 343 | CLANG_WARN_INT_CONVERSION = YES; 344 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 345 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 346 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 347 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 348 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 349 | CLANG_WARN_STRICT_PROTOTYPES = YES; 350 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 351 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 352 | CLANG_WARN_UNREACHABLE_CODE = YES; 353 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 354 | CODE_SIGN_IDENTITY = "iPhone Developer"; 355 | COPY_PHASE_STRIP = NO; 356 | DEBUG_INFORMATION_FORMAT = dwarf; 357 | ENABLE_STRICT_OBJC_MSGSEND = YES; 358 | ENABLE_TESTABILITY = YES; 359 | GCC_C_LANGUAGE_STANDARD = gnu11; 360 | GCC_DYNAMIC_NO_PIC = NO; 361 | GCC_NO_COMMON_BLOCKS = YES; 362 | GCC_OPTIMIZATION_LEVEL = 0; 363 | GCC_PREPROCESSOR_DEFINITIONS = ( 364 | "DEBUG=1", 365 | "$(inherited)", 366 | ); 367 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 368 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 369 | GCC_WARN_UNDECLARED_SELECTOR = YES; 370 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 371 | GCC_WARN_UNUSED_FUNCTION = YES; 372 | GCC_WARN_UNUSED_VARIABLE = YES; 373 | IPHONEOS_DEPLOYMENT_TARGET = 11.3; 374 | MTL_ENABLE_DEBUG_INFO = YES; 375 | ONLY_ACTIVE_ARCH = YES; 376 | SDKROOT = iphoneos; 377 | }; 378 | name = Debug; 379 | }; 380 | 872801282103168900C28D16 /* Release */ = { 381 | isa = XCBuildConfiguration; 382 | buildSettings = { 383 | ALWAYS_SEARCH_USER_PATHS = NO; 384 | CLANG_ANALYZER_NONNULL = YES; 385 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 386 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 387 | CLANG_CXX_LIBRARY = "libc++"; 388 | CLANG_ENABLE_MODULES = YES; 389 | CLANG_ENABLE_OBJC_ARC = YES; 390 | CLANG_ENABLE_OBJC_WEAK = YES; 391 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 392 | CLANG_WARN_BOOL_CONVERSION = YES; 393 | CLANG_WARN_COMMA = YES; 394 | CLANG_WARN_CONSTANT_CONVERSION = YES; 395 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 396 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 397 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 398 | CLANG_WARN_EMPTY_BODY = YES; 399 | CLANG_WARN_ENUM_CONVERSION = YES; 400 | CLANG_WARN_INFINITE_RECURSION = YES; 401 | CLANG_WARN_INT_CONVERSION = YES; 402 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 403 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 404 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 405 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 406 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 407 | CLANG_WARN_STRICT_PROTOTYPES = YES; 408 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 409 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 410 | CLANG_WARN_UNREACHABLE_CODE = YES; 411 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 412 | CODE_SIGN_IDENTITY = "iPhone Developer"; 413 | COPY_PHASE_STRIP = NO; 414 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 415 | ENABLE_NS_ASSERTIONS = NO; 416 | ENABLE_STRICT_OBJC_MSGSEND = YES; 417 | GCC_C_LANGUAGE_STANDARD = gnu11; 418 | GCC_NO_COMMON_BLOCKS = YES; 419 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 420 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 421 | GCC_WARN_UNDECLARED_SELECTOR = YES; 422 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 423 | GCC_WARN_UNUSED_FUNCTION = YES; 424 | GCC_WARN_UNUSED_VARIABLE = YES; 425 | IPHONEOS_DEPLOYMENT_TARGET = 11.3; 426 | MTL_ENABLE_DEBUG_INFO = NO; 427 | SDKROOT = iphoneos; 428 | VALIDATE_PRODUCT = YES; 429 | }; 430 | name = Release; 431 | }; 432 | 8728012A2103168900C28D16 /* Debug */ = { 433 | isa = XCBuildConfiguration; 434 | buildSettings = { 435 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 436 | CODE_SIGN_STYLE = Automatic; 437 | GCC_PREFIX_HEADER = "$(SRCROOT)/iOS 无痕埋点方案/PrefixHeader.pch"; 438 | INFOPLIST_FILE = "iOS 无痕埋点方案/Info.plist"; 439 | LD_RUNPATH_SEARCH_PATHS = ( 440 | "$(inherited)", 441 | "@executable_path/Frameworks", 442 | ); 443 | PRODUCT_BUNDLE_IDENTIFIER = "sandyLoo.iOS-------"; 444 | PRODUCT_NAME = "$(TARGET_NAME)"; 445 | TARGETED_DEVICE_FAMILY = "1,2"; 446 | }; 447 | name = Debug; 448 | }; 449 | 8728012B2103168900C28D16 /* Release */ = { 450 | isa = XCBuildConfiguration; 451 | buildSettings = { 452 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 453 | CODE_SIGN_STYLE = Automatic; 454 | GCC_PREFIX_HEADER = "$(SRCROOT)/iOS 无痕埋点方案/PrefixHeader.pch"; 455 | INFOPLIST_FILE = "iOS 无痕埋点方案/Info.plist"; 456 | LD_RUNPATH_SEARCH_PATHS = ( 457 | "$(inherited)", 458 | "@executable_path/Frameworks", 459 | ); 460 | PRODUCT_BUNDLE_IDENTIFIER = "sandyLoo.iOS-------"; 461 | PRODUCT_NAME = "$(TARGET_NAME)"; 462 | TARGETED_DEVICE_FAMILY = "1,2"; 463 | }; 464 | name = Release; 465 | }; 466 | /* End XCBuildConfiguration section */ 467 | 468 | /* Begin XCConfigurationList section */ 469 | 8728010E2103168800C28D16 /* Build configuration list for PBXProject "iOS 无痕埋点方案" */ = { 470 | isa = XCConfigurationList; 471 | buildConfigurations = ( 472 | 872801272103168900C28D16 /* Debug */, 473 | 872801282103168900C28D16 /* Release */, 474 | ); 475 | defaultConfigurationIsVisible = 0; 476 | defaultConfigurationName = Release; 477 | }; 478 | 872801292103168900C28D16 /* Build configuration list for PBXNativeTarget "iOS 无痕埋点方案" */ = { 479 | isa = XCConfigurationList; 480 | buildConfigurations = ( 481 | 8728012A2103168900C28D16 /* Debug */, 482 | 8728012B2103168900C28D16 /* Release */, 483 | ); 484 | defaultConfigurationIsVisible = 0; 485 | defaultConfigurationName = Release; 486 | }; 487 | /* End XCConfigurationList section */ 488 | }; 489 | rootObject = 8728010B2103168800C28D16 /* Project object */; 490 | } 491 | -------------------------------------------------------------------------------- /iOS 无痕埋点方案/iOS 无痕埋点方案.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /iOS 无痕埋点方案/iOS 无痕埋点方案.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /iOS 无痕埋点方案/iOS 无痕埋点方案/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // iOS 无痕埋点方案 4 | // 5 | // Created by lushuangqiang on 2018/7/21. 6 | // Copyright © 2018年 SandyLoo. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface AppDelegate : UIResponder 12 | 13 | @property (strong, nonatomic) UIWindow *window; 14 | 15 | 16 | @end 17 | 18 | -------------------------------------------------------------------------------- /iOS 无痕埋点方案/iOS 无痕埋点方案/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // iOS 无痕埋点方案 4 | // 5 | // Created by lushuangqiang on 2018/7/21. 6 | // Copyright © 2018年 SandyLoo. All rights reserved. 7 | // 8 | 9 | #import "AppDelegate.h" 10 | #import "RootViewController.h" 11 | 12 | 13 | @interface AppDelegate () 14 | 15 | @end 16 | 17 | @implementation AppDelegate 18 | 19 | 20 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 21 | // Override point for customization after application launch. 22 | RootViewController * rootVC = [[RootViewController alloc]init]; 23 | UINavigationController * nav = [[UINavigationController alloc]initWithRootViewController:rootVC]; 24 | _window.bounds = [UIScreen mainScreen].bounds; 25 | _window.rootViewController = nav; 26 | [_window makeKeyAndVisible]; 27 | return YES; 28 | } 29 | 30 | 31 | - (void)applicationWillResignActive:(UIApplication *)application { 32 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 33 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 34 | } 35 | 36 | 37 | - (void)applicationDidEnterBackground:(UIApplication *)application { 38 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 39 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 40 | } 41 | 42 | 43 | - (void)applicationWillEnterForeground:(UIApplication *)application { 44 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 45 | } 46 | 47 | 48 | - (void)applicationDidBecomeActive:(UIApplication *)application { 49 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 50 | } 51 | 52 | 53 | - (void)applicationWillTerminate:(UIApplication *)application { 54 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 55 | } 56 | 57 | 58 | @end 59 | -------------------------------------------------------------------------------- /iOS 无痕埋点方案/iOS 无痕埋点方案/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /iOS 无痕埋点方案/iOS 无痕埋点方案/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /iOS 无痕埋点方案/iOS 无痕埋点方案/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /iOS 无痕埋点方案/iOS 无痕埋点方案/CrashAvoid/CrashAvoidConst.h: -------------------------------------------------------------------------------- 1 | // 2 | // CrashAvoidConst.h 3 | // Test 4 | // 5 | // Created by sandy on 2018/7/9. 6 | // Copyright © 2018年 sandy. All rights reserved. 7 | // 8 | 9 | 10 | #ifndef CrashAvoidConst_h 11 | #define CrashAvoidConst_h 12 | 13 | #import "CrashAvoidManager.h" 14 | #import 15 | 16 | 17 | static NSString * const AvoidCrashDefaultReturnNil = @"AvoidCrash default is to return nil to avoid crash."; 18 | static NSString * const AvoidCrashDefaultIgnore = @"AvoidCrash default is to ignore this operation to avoid crash."; 19 | static NSString * const AvoidCrashSeparator = @"================================================================"; 20 | static NSString * const AvoidCrashSeparatorWithFlag = @"========================CrashAvoid Log=========================="; 21 | 22 | static NSString * const key_errorName = @"errorName"; 23 | static NSString * const key_errorReason = @"errorReason"; 24 | static NSString * const key_errorPlace = @"errorPlace"; 25 | static NSString * const key_defaultToDo = @"defaultToDo"; 26 | static NSString * const key_callStackSymbols = @"callStackSymbols"; 27 | static NSString * const key_exception = @"exception"; 28 | 29 | #endif /* CrashAvoidConst_h */ 30 | -------------------------------------------------------------------------------- /iOS 无痕埋点方案/iOS 无痕埋点方案/CrashAvoid/CrashAvoidManager.h: -------------------------------------------------------------------------------- 1 | // 2 | // CrashAvoidManager.h 3 | // Test 4 | // 5 | // Created by sandy on 2018/7/9. 6 | // Copyright © 2018年 sandy. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface CrashAvoidManager : NSObject 12 | 13 | /** 14 | 交换类方法 15 | 16 | @param anClass 类 17 | @param method1Sel 原方法 18 | @param method2Sel 交换后的方法 19 | */ 20 | + (void)exchangeClassMethod:(Class)anClass method1Sel:(SEL)method1Sel method2Sel:(SEL)method2Sel; 21 | 22 | /** 23 | 交换对象方法 24 | 25 | @param anClass 类 26 | @param method1Sel 原方法 27 | @param method2Sel 交换后的方法 28 | */ 29 | + (void)exchangeInstanceMethod:(Class)anClass method1Sel:(SEL)method1Sel method2Sel:(SEL)method2Sel; 30 | 31 | 32 | 33 | /** 34 | 当检测到error时会调用此方法, 方法内对exception进行解析,并获取堆栈信息上传到bugly 35 | 36 | @param exception 异常信息 37 | @param defaultToDo 默认的异常处理做法 38 | */ 39 | + (void)noteErrorWithException:(NSException *)exception defaultToDo:(NSString *)defaultToDo; 40 | 41 | 42 | @end 43 | -------------------------------------------------------------------------------- /iOS 无痕埋点方案/iOS 无痕埋点方案/CrashAvoid/CrashAvoidManager.m: -------------------------------------------------------------------------------- 1 | // 2 | // CrashAvoidManager.m 3 | // Test 4 | // 5 | // Created by sandy on 2018/7/9. 6 | // Copyright © 2018年 sandy. All rights reserved. 7 | // 8 | 9 | #import "CrashAvoidManager.h" 10 | #import "CrashAvoidConst.h" 11 | 12 | 13 | @implementation CrashAvoidManager 14 | 15 | 16 | /** 17 | * 类方法的交换 18 | * 19 | * @param anClass 哪个类 20 | * @param method1Sel 方法1 21 | * @param method2Sel 方法2 22 | */ 23 | + (void)exchangeClassMethod:(Class)anClass method1Sel:(SEL)method1Sel method2Sel:(SEL)method2Sel { 24 | Method method1 = class_getClassMethod(anClass, method1Sel); 25 | Method method2 = class_getClassMethod(anClass, method2Sel); 26 | method_exchangeImplementations(method1, method2); 27 | } 28 | 29 | 30 | /** 31 | * 对象方法的交换 32 | * 33 | * @param anClass 哪个类 34 | * @param method1Sel 方法1(原本的方法) 35 | * @param method2Sel 方法2(要替换成的方法) 36 | */ 37 | + (void)exchangeInstanceMethod:(Class)anClass method1Sel:(SEL)method1Sel method2Sel:(SEL)method2Sel { 38 | 39 | 40 | Method originalMethod = class_getInstanceMethod(anClass, method1Sel); 41 | Method swizzledMethod = class_getInstanceMethod(anClass, method2Sel); 42 | 43 | BOOL didAddMethod = 44 | class_addMethod(anClass, 45 | method1Sel, 46 | method_getImplementation(swizzledMethod), 47 | method_getTypeEncoding(swizzledMethod)); 48 | 49 | if (didAddMethod) { 50 | class_replaceMethod(anClass, 51 | method2Sel, 52 | method_getImplementation(originalMethod), 53 | method_getTypeEncoding(originalMethod)); 54 | } 55 | 56 | else { 57 | method_exchangeImplementations(originalMethod, swizzledMethod); 58 | } 59 | 60 | } 61 | 62 | 63 | /** 64 | * 提示崩溃的信息(控制台输出、通知) 65 | * 66 | * @param exception 捕获到的异常 67 | * @param defaultToDo 这个框架里默认的做法 68 | */ 69 | + (void)noteErrorWithException:(NSException *)exception defaultToDo:(NSString *)defaultToDo 70 | { 71 | 72 | //堆栈数据 73 | NSArray *callStackSymbolsArr = [NSThread callStackSymbols]; 74 | 75 | //获取在哪个类的哪个方法中实例化的数组 字符串格式 -[类名 方法名] 或者 +[类名 方法名] 76 | NSString *mainCallStackSymbolMsg = [CrashAvoidManager getMainCallStackSymbolMessageWithCallStackSymbols:callStackSymbolsArr]; 77 | 78 | if (mainCallStackSymbolMsg == nil) { 79 | 80 | mainCallStackSymbolMsg = @"崩溃方法定位失败,请您查看函数调用栈来排查错误原因"; 81 | } 82 | 83 | NSString *errorName = exception.name; 84 | NSString *errorReason = exception.reason; 85 | //errorReason 可能为 -[__NSCFConstantString avoidCrashCharacterAtIndex:]: Range or index out of bounds 86 | //将avoidCrash去掉 87 | errorReason = [errorReason stringByReplacingOccurrencesOfString:@"avoidCrash" withString:@""]; 88 | 89 | NSString *errorPlace = [NSString stringWithFormat:@"Error Place:%@",mainCallStackSymbolMsg]; 90 | 91 | NSString *logErrorMessage = [NSString stringWithFormat:@"\n\n%@\n\n%@\n%@\n%@\n%@",AvoidCrashSeparatorWithFlag, errorName, errorReason, errorPlace, defaultToDo]; 92 | 93 | logErrorMessage = [NSString stringWithFormat:@"%@\n\n%@\n\n",logErrorMessage,AvoidCrashSeparator]; 94 | 95 | 96 | //请忽略下面的赋值,目的只是为了能顺利上传到cocoapods 97 | logErrorMessage = logErrorMessage; 98 | 99 | NSDictionary *errorInfoDic = @{ 100 | key_errorName : errorName, 101 | key_errorReason : errorReason, 102 | key_errorPlace : errorPlace, 103 | key_defaultToDo : defaultToDo, 104 | key_exception : exception, 105 | key_callStackSymbols : callStackSymbolsArr 106 | }; 107 | 108 | 109 | // NSLog(@"error == %@",errorInfoDic); 110 | } 111 | 112 | 113 | /** 114 | * 获取堆栈主要崩溃精简化的信息<根据正则表达式匹配出来> 115 | * 116 | * @param callStackSymbols 堆栈主要崩溃信息 117 | * 118 | * @return 堆栈主要崩溃精简化的信息 119 | */ 120 | 121 | + (NSString *)getMainCallStackSymbolMessageWithCallStackSymbols:(NSArray *)callStackSymbols { 122 | 123 | //mainCallStackSymbolMsg的格式为 +[类名 方法名] 或者 -[类名 方法名] 124 | __block NSString *mainCallStackSymbolMsg = nil; 125 | 126 | //匹配出来的格式为 +[类名 方法名] 或者 -[类名 方法名] 127 | NSString *regularExpStr = @"[-\\+]\\[.+\\]"; 128 | 129 | 130 | NSRegularExpression *regularExp = [[NSRegularExpression alloc] initWithPattern:regularExpStr options:NSRegularExpressionCaseInsensitive error:nil]; 131 | 132 | 133 | for (int index = 2; index < callStackSymbols.count; index++) { 134 | NSString *callStackSymbol = callStackSymbols[index]; 135 | 136 | [regularExp enumerateMatchesInString:callStackSymbol options:NSMatchingReportProgress range:NSMakeRange(0, callStackSymbol.length) usingBlock:^(NSTextCheckingResult * _Nullable result, NSMatchingFlags flags, BOOL * _Nonnull stop) { 137 | if (result) { 138 | NSString* tempCallStackSymbolMsg = [callStackSymbol substringWithRange:result.range]; 139 | 140 | //get className 141 | NSString *className = [tempCallStackSymbolMsg componentsSeparatedByString:@" "].firstObject; 142 | className = [className componentsSeparatedByString:@"["].lastObject; 143 | 144 | NSBundle *bundle = [NSBundle bundleForClass:NSClassFromString(className)]; 145 | 146 | //filter category and system class 147 | if (![className hasSuffix:@")"] && bundle == [NSBundle mainBundle]) { 148 | mainCallStackSymbolMsg = tempCallStackSymbolMsg; 149 | 150 | } 151 | *stop = YES; 152 | } 153 | }]; 154 | 155 | if (mainCallStackSymbolMsg.length) { 156 | break; 157 | } 158 | } 159 | 160 | return mainCallStackSymbolMsg; 161 | } 162 | @end 163 | -------------------------------------------------------------------------------- /iOS 无痕埋点方案/iOS 无痕埋点方案/CrashAvoid/NSArray+safe.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSArray+safe.h 3 | // OneKeyAnalysis 4 | // 5 | // Created by sandy on 2018/7/12. 6 | // Copyright © 2018年 sandy. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface NSArray (safe) 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /iOS 无痕埋点方案/iOS 无痕埋点方案/CrashAvoid/NSArray+safe.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSArray+safe.m 3 | // OneKeyAnalysis 4 | // 5 | // Created by sandy on 2018/7/12. 6 | // Copyright © 2018年 sandy. All rights reserved. 7 | // 8 | 9 | #import "NSArray+safe.h" 10 | #import "CrashAvoidConst.h" 11 | 12 | @implementation NSArray (safe) 13 | 14 | +(void)load 15 | { 16 | static dispatch_once_t onceToken; 17 | dispatch_once(&onceToken, ^{ 18 | 19 | Class __NSArray = NSClassFromString(@"NSArray"); 20 | Class __NSArrayI = NSClassFromString(@"__NSArrayI"); 21 | Class __NSSingleObjectArrayI = NSClassFromString(@"__NSSingleObjectArrayI"); 22 | Class __NSArray0 = NSClassFromString(@"__NSArray0"); 23 | Class __NSPlaceholderArray = NSClassFromString(@"__NSPlaceholderArray"); 24 | 25 | //objectAtIndex: 26 | 27 | [CrashAvoidManager exchangeInstanceMethod:__NSArrayI method1Sel:@selector(objectAtIndex:) method2Sel:@selector(__NSArrayIAvoidCrashObjectAtIndex:)]; 28 | 29 | [CrashAvoidManager exchangeInstanceMethod:__NSSingleObjectArrayI method1Sel:@selector(objectAtIndex:) method2Sel:@selector(__NSSingleObjectArrayIAvoidCrashObjectAtIndex:)]; 30 | 31 | [CrashAvoidManager exchangeInstanceMethod:__NSArray0 method1Sel:@selector(objectAtIndex:) method2Sel:@selector(__NSArray0AvoidCrashObjectAtIndex:)]; 32 | 33 | [CrashAvoidManager exchangeInstanceMethod:__NSPlaceholderArray method1Sel:@selector(objectAtIndex:) method2Sel:@selector(__NSPlaceholderArrayAvoidCrashObjectAtIndex:)]; 34 | 35 | 36 | }); 37 | } 38 | 39 | 40 | 41 | //================================================================= 42 | // objectAtIndex: 43 | //================================================================= 44 | #pragma mark - objectAtIndex: 45 | 46 | //__NSArrayI objectAtIndex: 47 | - (id)__NSArrayIAvoidCrashObjectAtIndex:(NSUInteger)index { 48 | id object = nil; 49 | 50 | @try { 51 | object = [self __NSArrayIAvoidCrashObjectAtIndex:index]; 52 | } 53 | @catch (NSException *exception) { 54 | NSString *defaultToDo = AvoidCrashDefaultReturnNil; 55 | [CrashAvoidManager noteErrorWithException:exception defaultToDo:defaultToDo]; 56 | } 57 | @finally { 58 | return object; 59 | } 60 | } 61 | 62 | 63 | 64 | //__NSSingleObjectArrayI objectAtIndex: 65 | - (id)__NSSingleObjectArrayIAvoidCrashObjectAtIndex:(NSUInteger)index { 66 | id object = nil; 67 | 68 | @try { 69 | object = [self __NSSingleObjectArrayIAvoidCrashObjectAtIndex:index]; 70 | } 71 | @catch (NSException *exception) { 72 | NSString *defaultToDo = AvoidCrashDefaultReturnNil; 73 | [CrashAvoidManager noteErrorWithException:exception defaultToDo:defaultToDo]; 74 | } 75 | @finally { 76 | return object; 77 | } 78 | } 79 | 80 | //__NSArray0 objectAtIndex: 81 | - (id)__NSArray0AvoidCrashObjectAtIndex:(NSUInteger)index { 82 | id object = nil; 83 | 84 | @try { 85 | object = [self __NSArray0AvoidCrashObjectAtIndex:index]; 86 | } 87 | @catch (NSException *exception) { 88 | NSString *defaultToDo = AvoidCrashDefaultReturnNil; 89 | [CrashAvoidManager noteErrorWithException:exception defaultToDo:defaultToDo]; 90 | } 91 | @finally { 92 | return object; 93 | } 94 | } 95 | 96 | //__NSPlaceholderArray objectAtIndex: 97 | - (id)__NSPlaceholderArrayAvoidCrashObjectAtIndex:(NSUInteger)index { 98 | id object = nil; 99 | 100 | @try { 101 | object = [self __NSPlaceholderArrayAvoidCrashObjectAtIndex:index]; 102 | } 103 | @catch (NSException *exception) { 104 | NSString *defaultToDo = AvoidCrashDefaultReturnNil; 105 | [CrashAvoidManager noteErrorWithException:exception defaultToDo:defaultToDo]; 106 | } 107 | @finally { 108 | return object; 109 | } 110 | } 111 | 112 | 113 | @end 114 | -------------------------------------------------------------------------------- /iOS 无痕埋点方案/iOS 无痕埋点方案/CrashAvoid/NSObject+avoid.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSObject+avoid.h 3 | // OneKeyAnalysis 4 | // 5 | // Created by sandy on 2018/7/12. 6 | // Copyright © 2018年 sandy. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface NSObject (avoid) 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /iOS 无痕埋点方案/iOS 无痕埋点方案/CrashAvoid/NSObject+avoid.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSObject+avoid.m 3 | // OneKeyAnalysis 4 | // 5 | // Created by sandy on 2018/7/12. 6 | // Copyright © 2018年 sandy. All rights reserved. 7 | // 8 | 9 | #import "NSObject+avoid.h" 10 | #import "CrashAvoidConst.h" 11 | 12 | @implementation NSObject (avoid) 13 | +(void)load 14 | { 15 | static dispatch_once_t onceToken; 16 | dispatch_once(&onceToken, ^{ 17 | 18 | //valueForKey: 19 | Class anClass = [self class]; 20 | SEL method1Sel = @selector(valueForKey:); 21 | SEL method2Sel = @selector(safe_valueForKey:); 22 | 23 | [CrashAvoidManager exchangeInstanceMethod:anClass method1Sel:method1Sel method2Sel:method2Sel]; 24 | }); 25 | } 26 | 27 | 28 | -(id)safe_valueForKey:(NSString *)key 29 | { 30 | id object = nil; 31 | 32 | @try { 33 | object = [self safe_valueForKey:key]; 34 | } 35 | @catch (NSException *exception) { 36 | 37 | } 38 | @finally { 39 | return object; 40 | } 41 | } 42 | @end 43 | -------------------------------------------------------------------------------- /iOS 无痕埋点方案/iOS 无痕埋点方案/HookClass/UICollectionView+Analysis.h: -------------------------------------------------------------------------------- 1 | // 2 | // UICollectionView+Analysis.h 3 | // OneKeyAnalysis 4 | // 5 | // Created by sandy on 2018/7/12. 6 | // Copyright © 2018年 sandy. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface UICollectionView (Analysis) 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /iOS 无痕埋点方案/iOS 无痕埋点方案/HookClass/UICollectionView+Analysis.m: -------------------------------------------------------------------------------- 1 | // 2 | // UICollectionView+Analysis.m 3 | // OneKeyAnalysis 4 | // 5 | // Created by sandy on 2018/7/12. 6 | // Copyright © 2018年 sandy. All rights reserved. 7 | // 8 | 9 | #import "UICollectionView+Analysis.h" 10 | #import "MethodSwizzingTool.h" 11 | #import 12 | 13 | @implementation UICollectionView (Analysis) 14 | 15 | +(void)load 16 | { 17 | static dispatch_once_t onceToken; 18 | dispatch_once(&onceToken, ^{ 19 | 20 | SEL originalAppearSelector = @selector(setDelegate:); 21 | SEL swizzingAppearSelector = @selector(user_setDelegate:); 22 | [MethodSwizzingTool swizzingForClass:[self class] originalSel:originalAppearSelector swizzingSel:swizzingAppearSelector]; 23 | }); 24 | } 25 | 26 | 27 | -(void)user_setDelegate:(id)delegate 28 | { 29 | [self user_setDelegate:delegate]; 30 | 31 | SEL sel = @selector(collectionView:didSelectItemAtIndexPath:); 32 | 33 | SEL sel_ = NSSelectorFromString(@"userDefined_collectionView_didselected"); 34 | 35 | class_addMethod([delegate class], 36 | sel_, 37 | method_getImplementation(class_getInstanceMethod([self class], @selector(user_collectionView:didSelectItemAtIndexPath:))), 38 | nil); 39 | 40 | 41 | //判断是否有实现,没有的话添加一个实现 42 | if (![self isContainSel:sel inClass:[delegate class]]) { 43 | IMP imp = method_getImplementation(class_getInstanceMethod([delegate class], sel)); 44 | class_addMethod([delegate class], sel, imp, nil); 45 | } 46 | 47 | 48 | // 将swizzle delegate method 和 origin delegate method 交换 49 | [MethodSwizzingTool swizzingForClass:[delegate class] originalSel:sel swizzingSel:sel_]; 50 | } 51 | 52 | 53 | //判断页面是否实现了某个sel 54 | - (BOOL)isContainSel:(SEL)sel inClass:(Class)class { 55 | unsigned int count; 56 | 57 | Method *methodList = class_copyMethodList(class,&count); 58 | for (int i = 0; i < count; i++) { 59 | Method method = methodList[i]; 60 | NSString *tempMethodString = [NSString stringWithUTF8String:sel_getName(method_getName(method))]; 61 | if ([tempMethodString isEqualToString:NSStringFromSelector(sel)]) { 62 | return YES; 63 | } 64 | } 65 | return NO; 66 | } 67 | 68 | 69 | 70 | - (void)user_collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath; 71 | { 72 | SEL sel = NSSelectorFromString(@"userDefined_collectionView_didselected"); 73 | if ([self respondsToSelector:sel]) { 74 | IMP imp = [self methodForSelector:sel]; 75 | void (*func)(id, SEL,id,id) = (void *)imp; 76 | func(self, sel,collectionView,indexPath); 77 | } 78 | } 79 | 80 | @end 81 | -------------------------------------------------------------------------------- /iOS 无痕埋点方案/iOS 无痕埋点方案/HookClass/UIControl+Analysis.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIControl+Analysis.h 3 | // OneKeyAnalysis 4 | // 5 | // Created by sandy on 2018/7/4. 6 | // Copyright © 2018年 sandy. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface UIControl (Analysis) 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /iOS 无痕埋点方案/iOS 无痕埋点方案/HookClass/UIControl+Analysis.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIControl+Analysis.m 3 | // OneKeyAnalysis 4 | // 5 | // Created by sandy on 2018/7/4. 6 | // Copyright © 2018年 sandy. All rights reserved. 7 | // 8 | 9 | #import "UIControl+Analysis.h" 10 | #import "MethodSwizzingTool.h" 11 | 12 | @implementation UIControl (Analysis) 13 | 14 | +(void)load 15 | { 16 | static dispatch_once_t onceToken; 17 | dispatch_once(&onceToken, ^{ 18 | SEL originalSelector = @selector(sendAction:to:forEvent:); 19 | SEL swizzingSelector = @selector(user_sendAction:to:forEvent:); 20 | [MethodSwizzingTool swizzingForClass:[self class] originalSel:originalSelector swizzingSel:swizzingSelector]; 21 | }); 22 | } 23 | 24 | 25 | -(void)user_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event 26 | { 27 | [self user_sendAction:action to:target forEvent:event]; 28 | 29 | NSString * identifier = [NSString stringWithFormat:@"%@/%@/%ld", [target class], NSStringFromSelector(action),self.tag]; 30 | NSDictionary * dic = [[[DataContainer dataInstance].data objectForKey:@"ACTION"] objectForKey:identifier]; 31 | if (dic) { 32 | 33 | NSString * eventid = dic[@"userDefined"][@"eventid"]; 34 | NSString * targetname = dic[@"userDefined"][@"target"]; 35 | NSString * pageid = dic[@"userDefined"][@"pageid"]; 36 | NSString * pagename = dic[@"userDefined"][@"pagename"]; 37 | NSDictionary * pagePara = dic[@"pagePara"]; 38 | __block NSMutableDictionary * uploadDic = [NSMutableDictionary dictionaryWithCapacity:0]; 39 | [pagePara enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { 40 | 41 | id value = [CaptureTool captureVarforInstance:target withPara:obj]; 42 | if (value && key) { 43 | [uploadDic setObject:value forKey:key]; 44 | } 45 | }]; 46 | 47 | 48 | NSLog(@"\n event id === %@,\n target === %@, \n pageid === %@,\n pagename === %@,\n pagepara === %@ \n", eventid, targetname, pageid, pagename, uploadDic); 49 | 50 | } 51 | } 52 | 53 | @end 54 | -------------------------------------------------------------------------------- /iOS 无痕埋点方案/iOS 无痕埋点方案/HookClass/UIGestureRecognizer+Analysis.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIGestureRecognizer+Analysis.h 3 | // OneKeyAnalysis 4 | // 5 | // Created by sandy on 2018/7/12. 6 | // Copyright © 2018年 sandy. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface UIGestureRecognizer (Analysis) 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /iOS 无痕埋点方案/iOS 无痕埋点方案/HookClass/UIGestureRecognizer+Analysis.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIGestureRecognizer+Analysis.m 3 | // OneKeyAnalysis 4 | // 5 | // Created by sandy on 2018/7/12. 6 | // Copyright © 2018年 sandy. All rights reserved. 7 | // 8 | 9 | #import "UIGestureRecognizer+Analysis.h" 10 | #import "MethodSwizzingTool.h" 11 | #import 12 | #import "UIGestureRecognizer+gesture.h" 13 | 14 | 15 | @implementation UIGestureRecognizer (Analysis) 16 | 17 | + (void)load 18 | { 19 | static dispatch_once_t onceToken; 20 | dispatch_once(&onceToken, ^{ 21 | 22 | [MethodSwizzingTool swizzingForClass:[self class] originalSel:@selector(initWithTarget:action:) swizzingSel:@selector(vi_initWithTarget:action:)]; 23 | }); 24 | } 25 | 26 | - (instancetype)vi_initWithTarget:(nullable id)target action:(nullable SEL)action 27 | { 28 | UIGestureRecognizer *selfGestureRecognizer = [self vi_initWithTarget:target action:action]; 29 | 30 | if (!target && !action) { 31 | return selfGestureRecognizer; 32 | } 33 | 34 | if ([target isKindOfClass:[UIScrollView class]]) { 35 | return selfGestureRecognizer; 36 | } 37 | 38 | Class class = [target class]; 39 | 40 | 41 | SEL sel = action; 42 | 43 | NSString * sel_name = [NSString stringWithFormat:@"%s/%@", class_getName([target class]),NSStringFromSelector(action)]; 44 | SEL sel_ = NSSelectorFromString(sel_name); 45 | 46 | BOOL isAddMethod = class_addMethod(class, 47 | sel_, 48 | method_getImplementation(class_getInstanceMethod([self class], @selector(responseUser_gesture:))), 49 | nil); 50 | 51 | self.methodName = NSStringFromSelector(action); 52 | if (isAddMethod) { 53 | Method selMethod = class_getInstanceMethod(class, sel); 54 | Method sel_Method = class_getInstanceMethod(class, sel_); 55 | method_exchangeImplementations(selMethod, sel_Method); 56 | } 57 | 58 | return selfGestureRecognizer; 59 | } 60 | 61 | 62 | -(void)responseUser_gesture:(UIGestureRecognizer *)gesture 63 | { 64 | 65 | NSString * identifier = [NSString stringWithFormat:@"%s/%@", class_getName([self class]),gesture.methodName]; 66 | 67 | SEL sel = NSSelectorFromString(identifier); 68 | if ([self respondsToSelector:sel]) { 69 | IMP imp = [self methodForSelector:sel]; 70 | void (*func)(id, SEL,id) = (void *)imp; 71 | func(self, sel,gesture); 72 | } 73 | 74 | 75 | NSDictionary * dic = [[[DataContainer dataInstance].data objectForKey:@"GESTURE"] objectForKey:identifier]; 76 | if (dic) { 77 | 78 | NSString * eventid = dic[@"userDefined"][@"eventid"]; 79 | NSString * targetname = dic[@"userDefined"][@"target"]; 80 | NSString * pageid = dic[@"userDefined"][@"pageid"]; 81 | NSString * pagename = dic[@"userDefined"][@"pagename"]; 82 | NSDictionary * pagePara = dic[@"pagePara"]; 83 | 84 | __block NSMutableDictionary * uploadDic = [NSMutableDictionary dictionaryWithCapacity:0]; 85 | [pagePara enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { 86 | id value = [CaptureTool captureVarforInstance:self withPara:obj]; 87 | if (value && key) { 88 | [uploadDic setObject:value forKey:key]; 89 | } 90 | }]; 91 | 92 | NSLog(@"\n event id === %@,\n target === %@, \n pageid === %@,\n pagename === %@,\n pagepara === %@ \n", eventid, targetname, pageid, pagename, uploadDic); 93 | 94 | } 95 | } 96 | 97 | @end 98 | -------------------------------------------------------------------------------- /iOS 无痕埋点方案/iOS 无痕埋点方案/HookClass/UITableView+Analysis.h: -------------------------------------------------------------------------------- 1 | // 2 | // UITableView+Analysis.h 3 | // OneKeyAnalysis 4 | // 5 | // Created by sandy on 2018/7/11. 6 | // Copyright © 2018年 sandy. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface UITableView (Analysis) 12 | 13 | 14 | 15 | 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /iOS 无痕埋点方案/iOS 无痕埋点方案/HookClass/UITableView+Analysis.m: -------------------------------------------------------------------------------- 1 | // 2 | // UITableView+Analysis.m 3 | // OneKeyAnalysis 4 | // 5 | // Created by sandy on 2018/7/11. 6 | // Copyright © 2018年 sandy. All rights reserved. 7 | // 8 | 9 | #import "UITableView+Analysis.h" 10 | #import "MethodSwizzingTool.h" 11 | #import 12 | #import "CaptureTool.h" 13 | 14 | @implementation UITableView (Analysis) 15 | 16 | +(void)load 17 | { 18 | static dispatch_once_t onceToken; 19 | dispatch_once(&onceToken, ^{ 20 | 21 | SEL originalAppearSelector = @selector(setDelegate:); 22 | SEL swizzingAppearSelector = @selector(user_setDelegate:); 23 | [MethodSwizzingTool swizzingForClass:[self class] originalSel:originalAppearSelector swizzingSel:swizzingAppearSelector]; 24 | }); 25 | } 26 | 27 | 28 | 29 | -(void)user_setDelegate:(id)delegate 30 | { 31 | [self user_setDelegate:delegate]; 32 | 33 | SEL sel = @selector(tableView:didSelectRowAtIndexPath:); 34 | 35 | SEL sel_ = NSSelectorFromString([NSString stringWithFormat:@"%@/%@/%ld", NSStringFromClass([delegate class]), NSStringFromClass([self class]),self.tag]); 36 | 37 | 38 | //因为 tableView:didSelectRowAtIndexPath:方法是optional的,所以没有实现的时候直接return 39 | if (![self isContainSel:sel inClass:[delegate class]]) { 40 | 41 | return; 42 | } 43 | 44 | 45 | BOOL addsuccess = class_addMethod([delegate class], 46 | sel_, 47 | method_getImplementation(class_getInstanceMethod([self class], @selector(user_tableView:didSelectRowAtIndexPath:))), 48 | nil); 49 | 50 | //如果添加成功了就直接交换实现, 如果没有添加成功,说明之前已经添加过并交换过实现了 51 | if (addsuccess) { 52 | Method selMethod = class_getInstanceMethod([delegate class], sel); 53 | Method sel_Method = class_getInstanceMethod([delegate class], sel_); 54 | method_exchangeImplementations(selMethod, sel_Method); 55 | } 56 | } 57 | 58 | 59 | //判断页面是否实现了某个sel 60 | - (BOOL)isContainSel:(SEL)sel inClass:(Class)class { 61 | unsigned int count; 62 | 63 | Method *methodList = class_copyMethodList(class,&count); 64 | for (int i = 0; i < count; i++) { 65 | Method method = methodList[i]; 66 | NSString *tempMethodString = [NSString stringWithUTF8String:sel_getName(method_getName(method))]; 67 | if ([tempMethodString isEqualToString:NSStringFromSelector(sel)]) { 68 | return YES; 69 | } 70 | } 71 | return NO; 72 | } 73 | 74 | 75 | // 由于我们交换了方法, 所以在tableview的 didselected 被调用的时候, 实质调用的是以下方法: 76 | -(void)user_tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath 77 | { 78 | 79 | SEL sel = NSSelectorFromString([NSString stringWithFormat:@"%@/%@/%ld", NSStringFromClass([self class]), NSStringFromClass([tableView class]), tableView.tag]); 80 | if ([self respondsToSelector:sel]) { 81 | IMP imp = [self methodForSelector:sel]; 82 | void (*func)(id, SEL,id,id) = (void *)imp; 83 | func(self, sel,tableView,indexPath); 84 | } 85 | 86 | 87 | NSString * identifier = [NSString stringWithFormat:@"%@/%@/%ld", [self class],[tableView class], tableView.tag]; 88 | NSDictionary * dic = [[[DataContainer dataInstance].data objectForKey:@"TABLEVIEW"] objectForKey:identifier]; 89 | if (dic) { 90 | 91 | NSString * eventid = dic[@"userDefined"][@"eventid"]; 92 | NSString * targetname = dic[@"userDefined"][@"target"]; 93 | NSString * pageid = dic[@"userDefined"][@"pageid"]; 94 | NSString * pagename = dic[@"userDefined"][@"pagename"]; 95 | NSDictionary * pagePara = dic[@"pagePara"]; 96 | 97 | 98 | 99 | UITableViewCell * cell = [tableView cellForRowAtIndexPath:indexPath]; 100 | __block NSMutableDictionary * uploadDic = [NSMutableDictionary dictionaryWithCapacity:0]; 101 | [pagePara enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { 102 | NSInteger containIn = [obj[@"containIn"] integerValue]; 103 | id instance = containIn == 0 ? self : cell; 104 | id value = [CaptureTool captureVarforInstance:instance withPara:obj]; 105 | if (value && key) { 106 | [uploadDic setObject:value forKey:key]; 107 | } 108 | }]; 109 | 110 | NSLog(@"\n event id === %@,\n target === %@, \n pageid === %@,\n pagename === %@,\n pagepara === %@ \n", eventid, targetname, pageid, pagename, uploadDic); 111 | } 112 | 113 | } 114 | 115 | @end 116 | -------------------------------------------------------------------------------- /iOS 无痕埋点方案/iOS 无痕埋点方案/HookClass/UIViewController+Analysis.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+Analysis.h 3 | // OneKeyAnalysis 4 | // 5 | // Created by sandy on 2018/7/4. 6 | // Copyright © 2018年 sandy. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface UIViewController (Analysis) 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /iOS 无痕埋点方案/iOS 无痕埋点方案/HookClass/UIViewController+Analysis.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+Analysis.m 3 | // OneKeyAnalysis 4 | // 5 | // Created by sandy on 2018/7/4. 6 | // Copyright © 2018年 sandy. All rights reserved. 7 | // 8 | 9 | #import "UIViewController+Analysis.h" 10 | #import "MethodSwizzingTool.h" 11 | 12 | 13 | @implementation UIViewController (Analysis) 14 | 15 | +(void)load 16 | { 17 | static dispatch_once_t onceToken; 18 | dispatch_once(&onceToken, ^{ 19 | SEL originalAppearSelector = @selector(viewWillAppear:); 20 | SEL swizzingAppearSelector = @selector(user_viewWillAppear:); 21 | [MethodSwizzingTool swizzingForClass:[self class] originalSel:originalAppearSelector swizzingSel:swizzingAppearSelector]; 22 | 23 | SEL originalDisappearSelector = @selector(viewWillDisappear:); 24 | SEL swizzingDisappearSelector = @selector(user_viewWillDisappear:); 25 | [MethodSwizzingTool swizzingForClass:[self class] originalSel:originalDisappearSelector swizzingSel:swizzingDisappearSelector]; 26 | 27 | SEL originalDidLoadSelector = @selector(viewDidLoad); 28 | SEL swizzingDidLoadSelector = @selector(user_viewDidLoad); 29 | [MethodSwizzingTool swizzingForClass:[self class] originalSel:originalDidLoadSelector swizzingSel:swizzingDidLoadSelector]; 30 | 31 | }); 32 | } 33 | 34 | 35 | -(void)user_viewWillAppear:(BOOL)animated 36 | { 37 | [self user_viewWillAppear:animated]; 38 | } 39 | 40 | 41 | -(void)user_viewWillDisappear:(BOOL)animated 42 | { 43 | [self user_viewWillDisappear:animated]; 44 | 45 | } 46 | 47 | -(void)user_viewDidLoad 48 | { 49 | 50 | [self user_viewDidLoad]; 51 | 52 | 53 | NSString * identifier = [NSString stringWithFormat:@"%@", [self class]]; 54 | NSDictionary * dic = [[[DataContainer dataInstance].data objectForKey:@"PAGEPV"] objectForKey:identifier]; 55 | if (dic) { 56 | NSString * pageid = dic[@"userDefined"][@"pageid"]; 57 | NSString * pagename = dic[@"userDefined"][@"pagename"]; 58 | NSDictionary * pagePara = dic[@"pagePara"]; 59 | 60 | __block NSMutableDictionary * uploadDic = [NSMutableDictionary dictionaryWithCapacity:0]; 61 | [pagePara enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { 62 | 63 | id value = [CaptureTool captureVarforInstance:self withPara:obj]; 64 | if (value && key) { 65 | [uploadDic setObject:value forKey:key]; 66 | } 67 | }]; 68 | 69 | 70 | NSLog(@"\n pageid === %@,\n pagename === %@,\n pagepara === %@ \n", pageid, pagename, uploadDic); 71 | } 72 | } 73 | 74 | 75 | @end 76 | -------------------------------------------------------------------------------- /iOS 无痕埋点方案/iOS 无痕埋点方案/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | LaunchScreen 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /iOS 无痕埋点方案/iOS 无痕埋点方案/Models/LikeModel.h: -------------------------------------------------------------------------------- 1 | // 2 | // LikeModel.h 3 | // OneKeyAnalysis 4 | // 5 | // Created by sandy on 2018/7/12. 6 | // Copyright © 2018年 sandy. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import "ThirdModel.h" 12 | 13 | @interface LikeModel : NSObject 14 | 15 | @property(nonatomic,strong)NSString * fly; 16 | 17 | @property(nonatomic,strong)NSString * goods; 18 | 19 | @property(nonatomic,strong)ThirdModel * model3; 20 | 21 | @end 22 | -------------------------------------------------------------------------------- /iOS 无痕埋点方案/iOS 无痕埋点方案/Models/LikeModel.m: -------------------------------------------------------------------------------- 1 | // 2 | // LikeModel.m 3 | // OneKeyAnalysis 4 | // 5 | // Created by sandy on 2018/7/12. 6 | // Copyright © 2018年 sandy. All rights reserved. 7 | // 8 | 9 | #import "LikeModel.h" 10 | 11 | @implementation LikeModel 12 | 13 | 14 | 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /iOS 无痕埋点方案/iOS 无痕埋点方案/Models/TestModel.h: -------------------------------------------------------------------------------- 1 | // 2 | // TestModel.h 3 | // OneKeyAnalysis 4 | // 5 | // Created by sandy on 2018/7/11. 6 | // Copyright © 2018年 sandy. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "LikeModel.h" 11 | @interface TestModel : NSObject 12 | 13 | @property(nonatomic,assign)NSInteger age; 14 | 15 | 16 | @property(nonatomic,strong)NSString * name; 17 | 18 | 19 | @property(nonatomic,strong)NSString * sex; 20 | 21 | 22 | @property(nonatomic,strong)NSString * genDer; 23 | 24 | 25 | @property(nonatomic,strong)LikeModel * secondModel; 26 | 27 | 28 | @end 29 | -------------------------------------------------------------------------------- /iOS 无痕埋点方案/iOS 无痕埋点方案/Models/TestModel.m: -------------------------------------------------------------------------------- 1 | // 2 | // TestModel.m 3 | // OneKeyAnalysis 4 | // 5 | // Created by sandy on 2018/7/11. 6 | // Copyright © 2018年 sandy. All rights reserved. 7 | // 8 | 9 | #import "TestModel.h" 10 | 11 | @implementation TestModel 12 | 13 | 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /iOS 无痕埋点方案/iOS 无痕埋点方案/Models/ThirdModel.h: -------------------------------------------------------------------------------- 1 | // 2 | // ThirdModel.h 3 | // OneKeyAnalysis 4 | // 5 | // Created by sandy on 2018/7/12. 6 | // Copyright © 2018年 sandy. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface ThirdModel : NSObject 12 | 13 | 14 | @property(nonatomic,assign)NSInteger grade; 15 | 16 | @property(nonatomic,strong)NSString * sex1; 17 | 18 | 19 | @end 20 | -------------------------------------------------------------------------------- /iOS 无痕埋点方案/iOS 无痕埋点方案/Models/ThirdModel.m: -------------------------------------------------------------------------------- 1 | // 2 | // ThirdModel.m 3 | // OneKeyAnalysis 4 | // 5 | // Created by sandy on 2018/7/12. 6 | // Copyright © 2018年 sandy. All rights reserved. 7 | // 8 | 9 | #import "ThirdModel.h" 10 | 11 | @implementation ThirdModel 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /iOS 无痕埋点方案/iOS 无痕埋点方案/MyCell.h: -------------------------------------------------------------------------------- 1 | // 2 | // MyCell.h 3 | // OneKeyAnalysis 4 | // 5 | // Created by sandy on 2018/7/11. 6 | // Copyright © 2018年 sandy. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "TestModel.h" 11 | 12 | @interface MyCell : UITableViewCell 13 | 14 | @property(nonatomic,strong)TestModel * model; 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /iOS 无痕埋点方案/iOS 无痕埋点方案/MyCell.m: -------------------------------------------------------------------------------- 1 | // 2 | // MyCell.m 3 | // OneKeyAnalysis 4 | // 5 | // Created by sandy on 2018/7/11. 6 | // Copyright © 2018年 sandy. All rights reserved. 7 | // 8 | 9 | #import "MyCell.h" 10 | 11 | @implementation MyCell 12 | 13 | - (void)awakeFromNib { 14 | [super awakeFromNib]; 15 | 16 | } 17 | 18 | -(void)setModel:(TestModel *)model 19 | { 20 | _model = model; 21 | 22 | self.textLabel.text = model.name; 23 | } 24 | 25 | @end 26 | -------------------------------------------------------------------------------- /iOS 无痕埋点方案/iOS 无痕埋点方案/PrefixHeader.pch: -------------------------------------------------------------------------------- 1 | // 2 | // PrefixHeader.pch 3 | // OneKeyAnalysis 4 | // 5 | // Created by sandy on 2018/7/19. 6 | // Copyright © 2018年 sandy. All rights reserved. 7 | // 8 | 9 | #ifndef PrefixHeader_pch 10 | #define PrefixHeader_pch 11 | 12 | // Include any system framework and library headers here that should be included in all compilation units. 13 | // You will also need to set the Prefix Header build setting of one or more of your targets to reference this file. 14 | 15 | #import "CaptureTool.h" 16 | #import "DataContainer.h" 17 | 18 | #endif /* PrefixHeader_pch */ 19 | -------------------------------------------------------------------------------- /iOS 无痕埋点方案/iOS 无痕埋点方案/RootViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // RootViewController.h 3 | // OneKeyAnalysis 4 | // 5 | // Created by lushuangqiang on 2018/12/8. 6 | // Copyright © 2018 lushuangqiang. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface RootViewController : UIViewController 14 | 15 | @end 16 | 17 | NS_ASSUME_NONNULL_END 18 | -------------------------------------------------------------------------------- /iOS 无痕埋点方案/iOS 无痕埋点方案/RootViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // RootViewController.m 3 | // OneKeyAnalysis 4 | // 5 | // Created by lushuangqiang on 2018/12/8. 6 | // Copyright © 2018 lushuangqiang. All rights reserved. 7 | // 8 | 9 | #import "RootViewController.h" 10 | #import "ViewController.h" 11 | 12 | @interface RootViewController () 13 | 14 | @end 15 | 16 | @implementation RootViewController 17 | 18 | - (void)viewDidLoad { 19 | [super viewDidLoad]; 20 | // Do any additional setup after loading the view. 21 | self.view.backgroundColor = [UIColor grayColor]; 22 | } 23 | 24 | 25 | -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event 26 | { 27 | ViewController * vc = [[ViewController alloc]init]; 28 | [self.navigationController pushViewController:vc animated:YES]; 29 | } 30 | 31 | @end 32 | -------------------------------------------------------------------------------- /iOS 无痕埋点方案/iOS 无痕埋点方案/SecondViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // SecondViewController.h 3 | // OneKeyAnalysis 4 | // 5 | // Created by sandy on 2018/7/12. 6 | // Copyright © 2018年 sandy. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface SecondViewController : UIViewController 12 | 13 | @property(nonatomic,assign)NSInteger age; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /iOS 无痕埋点方案/iOS 无痕埋点方案/SecondViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // SecondViewController.m 3 | // OneKeyAnalysis 4 | // 5 | // Created by sandy on 2018/7/12. 6 | // Copyright © 2018年 sandy. All rights reserved. 7 | // 8 | 9 | #import "SecondViewController.h" 10 | 11 | @interface SecondViewController () 12 | 13 | 14 | @end 15 | 16 | @implementation SecondViewController 17 | 18 | - (void)viewDidLoad { 19 | [super viewDidLoad]; 20 | // Do any additional setup after loading the view. 21 | 22 | self.view.backgroundColor = [UIColor whiteColor]; 23 | 24 | 25 | //手势3 26 | UILabel * tapLabel3 = [[UILabel alloc]init]; 27 | tapLabel3.frame = CGRectMake(0,0, 200, 50); 28 | tapLabel3.text = @"点击触发手势埋点"; 29 | tapLabel3.textAlignment = NSTextAlignmentCenter; 30 | tapLabel3.textColor = [UIColor whiteColor]; 31 | tapLabel3.backgroundColor = [UIColor grayColor]; 32 | tapLabel3.userInteractionEnabled = YES; 33 | tapLabel3.center = self.view.center; 34 | [self.view addSubview:tapLabel3]; 35 | 36 | UITapGestureRecognizer *tap1 = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(gesture3clicked:)]; 37 | [tapLabel3 addGestureRecognizer:tap1]; 38 | 39 | 40 | UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(gesture3clicked:)]; 41 | [tapLabel3 addGestureRecognizer:tap]; 42 | 43 | 44 | UIButton * jumpButton = [UIButton buttonWithType:UIButtonTypeCustom]; 45 | jumpButton.frame = CGRectMake(100, 100, 100, 50); 46 | jumpButton.backgroundColor = [UIColor grayColor]; 47 | [jumpButton setTitle:@"返回" forState:UIControlStateNormal]; 48 | [jumpButton addTarget:self action:@selector(back) forControlEvents:UIControlEventTouchUpInside]; 49 | [self.view addSubview:jumpButton]; 50 | } 51 | 52 | -(void)back 53 | { 54 | [self dismissViewControllerAnimated:YES completion:nil]; 55 | } 56 | 57 | -(void)gesture3clicked:(UIGestureRecognizer *)ges 58 | { 59 | 60 | 61 | } 62 | 63 | 64 | 65 | 66 | 67 | @end 68 | -------------------------------------------------------------------------------- /iOS 无痕埋点方案/iOS 无痕埋点方案/Source/20180719090759.json: -------------------------------------------------------------------------------- 1 | { 2 | "ACTION": { 3 | "ViewController/jumpSecond": { 4 | "userDefined": { 5 | "eventid": "201803074|93", 6 | "target": "", 7 | "pageid": "234", 8 | "pagename": "button点击,跳转至下一个页面" 9 | }, 10 | "pagePara": { 11 | "testKey9": { 12 | "propertyName": "testPara", 13 | "propertyPath":"", 14 | "containIn": "0" 15 | } 16 | } 17 | }, 18 | 19 | "SecondViewController/back": { 20 | "userDefined": { 21 | "eventid": "201803074|965", 22 | "target": "second", 23 | "pageid": "234", 24 | "pagename": "button点击,返回" 25 | }, 26 | "pagePara": { 27 | "testKey9": { 28 | "propertyName": "testPara", 29 | "propertyPath":"", 30 | "containIn": "0" 31 | } 32 | } 33 | } 34 | }, 35 | 36 | "PAGEPV": { 37 | "ViewController": { 38 | "userDefined": { 39 | "pageid": "234", 40 | "pagename": "XXX 页面展示了" 41 | }, 42 | "pagePara": { 43 | "testKey10": { 44 | "propertyName": "testPara", 45 | "propertyPath":"", 46 | "containIn": "0" 47 | } 48 | } 49 | }, 50 | "SecondViewController": { 51 | "userDefined": { 52 | "pageid": "234", 53 | "pagename": "XXX页面展示" 54 | }, 55 | "pagePara": { 56 | "testKey0": { 57 | "propertyName": "age", 58 | "propertyPath":"", 59 | "containIn": "0" 60 | } 61 | } 62 | } 63 | }, 64 | "TABLEVIEW": { 65 | "ViewController/TestTableview/0":{ 66 | "userDefined": { 67 | "eventid": "201803074|93", 68 | "target": "", 69 | "pageid": "234", 70 | "pagename": "tableview 被点击" 71 | }, 72 | "pagePara": { 73 | "user_grade": { 74 | "propertyName": "grade", 75 | "propertyPath":"", 76 | "containIn": "1" 77 | } 78 | } 79 | } 80 | }, 81 | 82 | "GESTURE": { 83 | "ViewController/gesture1clicked:":{ 84 | "userDefined": { 85 | "eventid": "201803074|93", 86 | "target": "", 87 | "pageid": "手势1对应的id", 88 | "pagename": "手势1对应的page name" 89 | }, 90 | "pagePara": { 91 | "testKey1": { 92 | "propertyName": "testPara", 93 | "propertyPath":"", 94 | "containIn": "0" 95 | } 96 | 97 | } 98 | }, 99 | "ViewController/gesture2clicked:":{ 100 | "userDefined": { 101 | "eventid": "201803074|93", 102 | "target": "", 103 | "pageid": "手势2对应的id", 104 | "pagename": "手势2对应的page name" 105 | }, 106 | "pagePara": { 107 | "testKey2": { 108 | "propertyName": "testPara", 109 | "propertyPath":"", 110 | "containIn": "0" 111 | } 112 | 113 | } 114 | }, 115 | 116 | "SecondViewController/gesture3clicked:":{ 117 | "userDefined": { 118 | "eventid": "201803074|98", 119 | "target": "", 120 | "pageid": "gesture3clicked", 121 | "pagename": "手势3对应的page name" 122 | }, 123 | "pagePara": { 124 | "user_age": { 125 | 126 | } 127 | 128 | } 129 | } 130 | } 131 | } 132 | 133 | -------------------------------------------------------------------------------- /iOS 无痕埋点方案/iOS 无痕埋点方案/TestTableview.h: -------------------------------------------------------------------------------- 1 | // 2 | // TestTableview.h 3 | // OneKeyAnalysis 4 | // 5 | // Created by lushuangqiang on 2018/12/8. 6 | // Copyright © 2018 lushuangqiang. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface TestTableview : UITableView 14 | 15 | @end 16 | 17 | NS_ASSUME_NONNULL_END 18 | -------------------------------------------------------------------------------- /iOS 无痕埋点方案/iOS 无痕埋点方案/TestTableview.m: -------------------------------------------------------------------------------- 1 | // 2 | // TestTableview.m 3 | // OneKeyAnalysis 4 | // 5 | // Created by lushuangqiang on 2018/12/8. 6 | // Copyright © 2018 lushuangqiang. All rights reserved. 7 | // 8 | 9 | #import "TestTableview.h" 10 | 11 | @implementation TestTableview 12 | 13 | -(void)dealloc 14 | { 15 | NSLog(@"tableview dealloc"); 16 | } 17 | 18 | /* 19 | // Only override drawRect: if you perform custom drawing. 20 | // An empty implementation adversely affects performance during animation. 21 | - (void)drawRect:(CGRect)rect { 22 | // Drawing code 23 | } 24 | */ 25 | 26 | @end 27 | -------------------------------------------------------------------------------- /iOS 无痕埋点方案/iOS 无痕埋点方案/Tools/CaptureTool.h: -------------------------------------------------------------------------------- 1 | // 2 | // VarCaptureTool.h 3 | // OneKeyAnalysis 4 | // 5 | // Created by sandy on 2018/7/12. 6 | // Copyright © 2018年 sandy. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface CaptureTool : NSObject 12 | 13 | /** 14 | 根据属性名获取某个对象的对应属性的值 15 | 16 | @param instance 持有属性的对象 17 | @param varName 属性的名字 18 | @return 属性对应的value 19 | */ 20 | +(id)captureVarforInstance:(id)instance varName:(NSString *)varName; 21 | 22 | 23 | 24 | /** 25 | 利用配置表中的para参数,从指定实例取值 26 | 27 | @param instance 参数的持有者 28 | @param para 配置表中的pagePara值 29 | @return 取到的值 30 | */ 31 | +(id)captureVarforInstance:(id)instance withPara:(NSDictionary *)para; 32 | 33 | 34 | @end 35 | -------------------------------------------------------------------------------- /iOS 无痕埋点方案/iOS 无痕埋点方案/Tools/CaptureTool.m: -------------------------------------------------------------------------------- 1 | // 2 | // VarCaptureTool.m 3 | // OneKeyAnalysis 4 | // 5 | // Created by sandy on 2018/7/12. 6 | // Copyright © 2018年 sandy. All rights reserved. 7 | // 8 | 9 | #import "CaptureTool.h" 10 | #import 11 | 12 | @implementation CaptureTool 13 | 14 | +(id)captureVarforInstance:(id)instance varName:(NSString *)varName 15 | { 16 | id value = [instance valueForKey:varName]; 17 | 18 | unsigned int count; 19 | objc_property_t *properties = class_copyPropertyList([instance class], &count); 20 | 21 | if (!value) { 22 | NSMutableArray * varNameArray = [NSMutableArray arrayWithCapacity:0]; 23 | for (int i = 0; i < count; i++) { 24 | objc_property_t property = properties[i]; 25 | NSString* propertyAttributes = [NSString stringWithUTF8String:property_getAttributes(property)]; 26 | NSArray* splitPropertyAttributes = [propertyAttributes componentsSeparatedByString:@"\""]; 27 | if (splitPropertyAttributes.count < 2) { 28 | continue; 29 | } 30 | NSString * className = [splitPropertyAttributes objectAtIndex:1]; 31 | Class cls = NSClassFromString(className); 32 | NSBundle *bundle2 = [NSBundle bundleForClass:cls]; 33 | if (bundle2 == [NSBundle mainBundle]) { 34 | // NSLog(@"自定义的类----- %@", className); 35 | const char * name = property_getName(property); 36 | NSString * varname = [[NSString alloc] initWithCString:name encoding:NSUTF8StringEncoding]; 37 | [varNameArray addObject:varname]; 38 | } else { 39 | // NSLog(@"系统的类"); 40 | } 41 | } 42 | 43 | for (NSString * name in varNameArray) { 44 | id newValue = [instance valueForKey:name]; 45 | if (newValue) { 46 | value = [newValue valueForKey:varName]; 47 | if (value) { 48 | return value; 49 | }else{ 50 | value = [[self class] captureVarforInstance:newValue varName:varName]; 51 | } 52 | } 53 | } 54 | } 55 | return value; 56 | } 57 | 58 | 59 | +(id)captureVarforInstance:(id)instance withPara:(NSDictionary *)para 60 | { 61 | NSString * properyName = para[@"propertyName"]; 62 | NSString * propertyPath = para[@"propertyPath"]; 63 | if (propertyPath.length > 0) { 64 | NSArray * keysArray = [propertyPath componentsSeparatedByString:@"/"]; 65 | 66 | return [[self class] captureVarforInstance:instance withKeys:keysArray]; 67 | } 68 | return [[self class] captureVarforInstance:instance varName:properyName]; 69 | } 70 | 71 | +(id)captureVarforInstance:(id)instance withKeys:(NSArray *)keyArray 72 | { 73 | id result = [instance valueForKey:keyArray[0]]; 74 | 75 | if (keyArray.count > 1 && result) { 76 | int i = 1; 77 | while (i < keyArray.count && result) { 78 | result = [result valueForKey:keyArray[i]]; 79 | i++; 80 | } 81 | } 82 | return result; 83 | } 84 | 85 | 86 | 87 | 88 | 89 | 90 | @end 91 | -------------------------------------------------------------------------------- /iOS 无痕埋点方案/iOS 无痕埋点方案/Tools/DataContainer.h: -------------------------------------------------------------------------------- 1 | // 2 | // DataContainer.h 3 | // OneKeyAnalysis 4 | // 5 | // Created by sandy on 2018/7/19. 6 | // Copyright © 2018年 sandy. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface DataContainer : NSObject 12 | 13 | @property(nonatomic,strong)NSDictionary * data; 14 | 15 | 16 | +(instancetype)dataInstance; 17 | 18 | @end 19 | -------------------------------------------------------------------------------- /iOS 无痕埋点方案/iOS 无痕埋点方案/Tools/DataContainer.m: -------------------------------------------------------------------------------- 1 | // 2 | // DataContainer.m 3 | // OneKeyAnalysis 4 | // 5 | // Created by sandy on 2018/7/19. 6 | // Copyright © 2018年 sandy. All rights reserved. 7 | // 8 | 9 | #import "DataContainer.h" 10 | 11 | @implementation DataContainer 12 | 13 | 14 | +(instancetype)dataInstance 15 | { 16 | static DataContainer * instacne = nil; 17 | static dispatch_once_t onceToken; 18 | dispatch_once(&onceToken, ^{ 19 | instacne = [DataContainer new]; 20 | 21 | NSString * path = [[NSBundle mainBundle] pathForResource:@"20180719090759" ofType:@"json"]; 22 | NSData * JSONData = [NSData dataWithContentsOfFile:path]; 23 | instacne.data = [NSJSONSerialization JSONObjectWithData:JSONData options:NSJSONReadingAllowFragments error:nil]; 24 | }); 25 | return instacne; 26 | } 27 | 28 | 29 | 30 | 31 | 32 | @end 33 | -------------------------------------------------------------------------------- /iOS 无痕埋点方案/iOS 无痕埋点方案/Tools/MethodSwizzingTool.h: -------------------------------------------------------------------------------- 1 | // 2 | // MethodSwizzingTool.h 3 | // OneKeyAnalysis 4 | // 5 | // Created by sandy on 2018/7/4. 6 | // Copyright © 2018年 sandy. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface MethodSwizzingTool : NSObject 12 | 13 | 14 | +(void)swizzingForClass:(Class)cls originalSel:(SEL)originalSelector swizzingSel:(SEL)swizzingSelector; 15 | 16 | 17 | +(NSDictionary *)getConfig; 18 | 19 | @end 20 | -------------------------------------------------------------------------------- /iOS 无痕埋点方案/iOS 无痕埋点方案/Tools/MethodSwizzingTool.m: -------------------------------------------------------------------------------- 1 | // 2 | // MethodSwizzingTool.m 3 | // OneKeyAnalysis 4 | // 5 | // Created by sandy on 2018/7/4. 6 | // Copyright © 2018年 sandy. All rights reserved. 7 | // 8 | 9 | #import "MethodSwizzingTool.h" 10 | #import 11 | 12 | @implementation MethodSwizzingTool 13 | 14 | 15 | +(void)swizzingForClass:(Class)cls originalSel:(SEL)originalSelector swizzingSel:(SEL)swizzingSelector 16 | { 17 | Class class = cls; 18 | Method originalMethod = class_getInstanceMethod(class, originalSelector); 19 | Method swizzingMethod = class_getInstanceMethod(class, swizzingSelector); 20 | 21 | BOOL addMethod = class_addMethod(class, 22 | originalSelector, 23 | method_getImplementation(swizzingMethod), 24 | method_getTypeEncoding(swizzingMethod)); 25 | 26 | if (addMethod) { 27 | class_replaceMethod(class, 28 | swizzingSelector, 29 | method_getImplementation(originalMethod), 30 | method_getTypeEncoding(originalMethod)); 31 | }else{ 32 | 33 | method_exchangeImplementations(originalMethod, swizzingMethod); 34 | } 35 | } 36 | 37 | +(NSDictionary *)getConfig 38 | { 39 | NSString * filePath = [[NSBundle mainBundle] pathForResource:@"Analysis" ofType:@"plist"]; 40 | NSDictionary * dic = [NSDictionary dictionaryWithContentsOfFile:filePath]; 41 | return dic; 42 | } 43 | @end 44 | -------------------------------------------------------------------------------- /iOS 无痕埋点方案/iOS 无痕埋点方案/Tools/UIGestureRecognizer+gesture.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIGestureRecognizer+gesture.h 3 | // OneKeyAnalysis 4 | // 5 | // Created by lushuangqiang on 2018/12/8. 6 | // Copyright © 2018 lushuangqiang. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface UIGestureRecognizer (gesture) 14 | 15 | @property(nonatomic,copy)NSString * methodName; 16 | 17 | @end 18 | 19 | NS_ASSUME_NONNULL_END 20 | -------------------------------------------------------------------------------- /iOS 无痕埋点方案/iOS 无痕埋点方案/Tools/UIGestureRecognizer+gesture.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIGestureRecognizer+gesture.m 3 | // OneKeyAnalysis 4 | // 5 | // Created by sandy on 2018/12/8. 6 | // Copyright © 2018 sandy. All rights reserved. 7 | // 8 | 9 | #import "UIGestureRecognizer+gesture.h" 10 | #import 11 | 12 | 13 | @implementation UIGestureRecognizer (gesture) 14 | 15 | 16 | -(void)setMethodName:(NSString *)methodName 17 | { 18 | objc_setAssociatedObject(self, @"methodName", methodName, OBJC_ASSOCIATION_COPY_NONATOMIC); 19 | } 20 | 21 | 22 | -(NSString *)methodName 23 | { 24 | return objc_getAssociatedObject(self, @"methodName"); 25 | } 26 | @end 27 | -------------------------------------------------------------------------------- /iOS 无痕埋点方案/iOS 无痕埋点方案/ViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.h 3 | // OneKeyAnalysis 4 | // 5 | // Created by sandy on 2018/7/4. 6 | // Copyright © 2018年 sandy. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface ViewController : UIViewController 12 | 13 | 14 | @end 15 | 16 | -------------------------------------------------------------------------------- /iOS 无痕埋点方案/iOS 无痕埋点方案/ViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.m 3 | // OneKeyAnalysis 4 | // 5 | // Created by sandy on 2018/7/4. 6 | // Copyright © 2018年 sandy. All rights reserved. 7 | // 8 | 9 | #import "ViewController.h" 10 | #import "TestModel.h" 11 | #import "MyCell.h" 12 | #import 13 | #import "CaptureTool.h" 14 | #import "LikeModel.h" 15 | #import "ThirdModel.h" 16 | #import "SecondViewController.h" 17 | #import "DataContainer.h" 18 | #import "TestTableview.h" 19 | 20 | @interface ViewController () 21 | 22 | @property(nonatomic,strong)TestTableview * tableView; 23 | 24 | @property(nonatomic,strong)NSMutableArray * dataArray; 25 | 26 | @property(nonatomic,strong)NSString * testSTR; 27 | 28 | @property(nonatomic,strong)NSString * testPara; 29 | 30 | 31 | @end 32 | 33 | @implementation ViewController 34 | 35 | -(void)dealloc 36 | { 37 | NSLog(@"ViewController 释放"); 38 | } 39 | 40 | - (void)viewDidLoad { 41 | [super viewDidLoad]; 42 | 43 | self.view.backgroundColor = [UIColor whiteColor]; 44 | self.testPara = @"para in page"; 45 | 46 | 47 | self.tableView = [[TestTableview alloc]init]; 48 | self.tableView.frame = CGRectMake(0, 250, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height); 49 | self.tableView.delegate = self; 50 | self.tableView.dataSource = self; 51 | [self.tableView registerClass:[MyCell class] forCellReuseIdentifier:@"cell"]; 52 | [self.view addSubview:self.tableView]; 53 | 54 | 55 | //点击 56 | UIButton * jumpButton = [UIButton buttonWithType:UIButtonTypeCustom]; 57 | jumpButton.frame = CGRectMake(30, 80, 200, 50); 58 | [jumpButton setTitle:@"点击跳转,已做埋点" forState:UIControlStateNormal]; 59 | jumpButton.backgroundColor = [UIColor grayColor]; 60 | [jumpButton addTarget:self action:@selector(jumpSecond) forControlEvents:UIControlEventTouchUpInside]; 61 | [self.view addSubview:jumpButton]; 62 | 63 | 64 | //手势1 65 | UILabel * tapLabel1 = [[UILabel alloc]init]; 66 | tapLabel1.frame = CGRectMake(30,140, 200, 50); 67 | tapLabel1.text = @"点击触发手势埋点 -1"; 68 | tapLabel1.textAlignment = NSTextAlignmentCenter; 69 | tapLabel1.textColor = [UIColor whiteColor]; 70 | tapLabel1.backgroundColor = [UIColor grayColor]; 71 | tapLabel1.userInteractionEnabled = YES; 72 | [self.view addSubview:tapLabel1]; 73 | 74 | UITapGestureRecognizer *tap1 = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(gesture1clicked:)]; 75 | [tapLabel1 addGestureRecognizer:tap1]; 76 | 77 | 78 | //手势2 79 | UILabel * tapLabel2 = [[UILabel alloc]init]; 80 | tapLabel2.frame = CGRectMake(30,200, 200, 50); 81 | tapLabel2.text = @"点击触发手势埋点 2"; 82 | tapLabel2.textAlignment = NSTextAlignmentCenter; 83 | tapLabel2.textColor = [UIColor whiteColor]; 84 | tapLabel2.backgroundColor = [UIColor grayColor]; 85 | tapLabel2.userInteractionEnabled = YES; 86 | [self.view addSubview:tapLabel2]; 87 | 88 | UITapGestureRecognizer *tap2 = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(gesture2clicked:)]; 89 | [tapLabel2 addGestureRecognizer:tap2]; 90 | 91 | } 92 | 93 | -(void)gesture1clicked:(UIGestureRecognizer *)ges 94 | { 95 | NSLog(@"手势1触发"); 96 | } 97 | 98 | -(void)gesture2clicked:(UIGestureRecognizer *)ges 99 | { 100 | NSLog(@"手势2触发"); 101 | } 102 | 103 | 104 | -(void)jumpSecond 105 | { 106 | SecondViewController * second = [[SecondViewController alloc]init]; 107 | second.age = 118; 108 | [self presentViewController:second animated:YES completion:nil]; 109 | } 110 | 111 | 112 | 113 | 114 | -(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section 115 | { 116 | return self.dataArray.count; 117 | } 118 | 119 | 120 | -(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 121 | { 122 | MyCell * cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath]; 123 | 124 | TestModel * model = self.dataArray[indexPath.row]; 125 | 126 | cell.model = model; 127 | return cell; 128 | } 129 | 130 | -(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath 131 | { 132 | TestModel * model = self.dataArray[indexPath.row]; 133 | NSLog(@"name == %@", model.name); 134 | } 135 | 136 | 137 | 138 | -(NSMutableArray *)dataArray 139 | { 140 | if (!_dataArray) { 141 | _dataArray = [NSMutableArray array]; 142 | 143 | for (int i = 0 ; i < 30; i++) { 144 | TestModel * model = [[TestModel alloc]init]; 145 | model.age = i; 146 | model.name = [NSString stringWithFormat:@"zhangsan - %d", i]; 147 | model.sex = @"male"; 148 | 149 | LikeModel * model2 = [LikeModel new]; 150 | model2.goods = @"abcd"; 151 | 152 | ThirdModel * model3 = [[ThirdModel alloc]init]; 153 | model3.grade = [[NSString stringWithFormat:@"%d", 100 + i] integerValue]; 154 | model3.sex1 = @"male"; 155 | 156 | model.secondModel = model2; 157 | model.secondModel.model3 = model3; 158 | 159 | 160 | [_dataArray addObject:model]; 161 | } 162 | } 163 | return _dataArray; 164 | } 165 | 166 | 167 | @end 168 | 169 | -------------------------------------------------------------------------------- /iOS 无痕埋点方案/iOS 无痕埋点方案/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // iOS 无痕埋点方案 4 | // 5 | // Created by lushuangqiang on 2018/7/21. 6 | // Copyright © 2018年 SandyLoo. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "AppDelegate.h" 11 | 12 | int main(int argc, char * argv[]) { 13 | @autoreleasepool { 14 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 15 | } 16 | } 17 | --------------------------------------------------------------------------------