docs: Electron

This commit is contained in:
杭城小刘
2020-05-04 02:51:19 +08:00
parent 79f10acba2
commit 8dbcff87ed
42 changed files with 851 additions and 33 deletions

View File

@@ -644,7 +644,7 @@ RISC 架构要求软件来指定各个操作步骤。比如上面的乘法,指
#### 2. 获取线程信息
#### 2. 获取线程信息<a name="threadInfo"></a>
讲完了区别来讲下如何做 CPU 使用率的监控
- 开启定时器,按照设定的周期不断执行下面的逻辑
@@ -1857,11 +1857,11 @@ iOS 网络框架层级关系如下:
iOS 网络现状是由4层组成的最底层的 BSD Sockets、SecureTransport次级底层是 CFNetwork、NSURLSession、NSURLConnection、WebView 是用 Objective-C 实现的,且调用 CFNetwork应用层框架 AFNetworking 基于 NSURLSession、NSURLConnection 实现。
目前业界对于网络监控主要有2种一种是通过 NSURLProtocol 监控、一种是通过 Hook 来监控。
目前业界对于网络监控主要有2种一种是通过 NSURLProtocol 监控、一种是通过 Hook 来监控。下面介绍几种办法来监控网络请求,各有优缺点。
#### 2.1 NSURLProtocol 监控 App 网络请求<a name="network-2.1"></a>
#### 2.1 方案一:NSURLProtocol 监控 App 网络请求<a name="network-2.1"></a>
NSURLProtocol 作为上层接口,使用较为简单,但 NSURLProtocol 属于 URL Loading System 体系中。应用协议的支持程度有限,支持 FTP、HTTP、HTTPS 等几个应用层协议,对于其他的协议则无法监控,存在一定的局限性。如果监控底层网络库 CFNetwork 则没有这个限制。
@@ -2341,7 +2341,7 @@ API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0))
#### 2.2 骚操作篇 <a name="network-2.2"></a>
#### 2.2 方案二NSURLProtocol 监控 App 网络请求之黑魔法篇 <a name="network-2.2"></a>
文章上面 [2.1 ](#network-2.1)分析到了 NSURLSessionTaskMetrics 由于兼容性问题,对于网络监控来说似乎不太完美,但是自后在搜资料的时候看到了一篇[文章](https://www.jianshu.com/p/1c34147030d1)。文章在分析 WebView 的网络监控的时候分析 Webkit 源码的时候发现了下面代码
@@ -2433,7 +2433,11 @@ NSURLSession 在 iOS9 之前使用 `_setCollectsTimingData:` 就可以使用 Tim
#### 2.3 Hook
#### 2.3 方案三:Hook
iOS 中 hook 技术有2类一种是 NSProxy一种是 method swizzlingisa swizzling
##### 2.3.1 方法一
写 SDK 肯定不可能手动侵入业务代码(你没那个权限提交到线上代码 😂),所以不管是 APM 还是无痕埋点都是通过 Hook 的方式。
@@ -2548,6 +2552,42 @@ void printResponseData (CFDataRef responseData) {
我们知道 NSURLSession、NSURLConnection、CFNetwork 的使用都需要调用一堆方法进行设置然后需要设置代理对象,实现代理方法。所以针对这种情况进行监控首先想到的是使用 runtime hook 掉方法层级。但是针对设置的代理对象的代理方法没办法 hook因为不知道代理对象是哪个类。所以想办法可以 hook 设置代理对象这个步骤,将代理对象替换成我们设计好的某个类,然后让这个类去实现 NSURLConnection、NSURLSession、CFNetwork 相关的代理方法。然后在这些方法的内部都去调用一下原代理对象的方法实现。所以我们的需求得以满足,我们在相应的方法里面可以拿到监控数据,比如请求开始时间、结束时间、状态码、内容大小等。
NSURLSession、NSURLConnection hook 如下。
![NSURLSession Hook](./../assets/2020-04-13-NSURLSessionHook.jpeg)
![NSURLConnection Hook](./../assets/2020-04-13-NSURLConnectionHook.jpeg)
业界有 APM 针对 CFNetwork 的方案,整理描述下:
CFNetwork 是 c 语言实现的,要对 c 代码进行 hook 需要使用 Dynamic Loader Hook 库 - [fishhook](https://github.com/facebook/fishhook)。
> **Dynamic Loader**dyld通过更新 **Mach-O** 文件中保存的指针的方法来绑定符号。借用它可以在 **Runtime** 修改 **C** 函数调用的函数指针。**fishhook** 的实现原理:遍历 `__DATA segment` 里面 `__nl_symbol_ptr` 、`__la_symbol_ptr` 两个 section 里面的符号,通过 Indirect Symbol Table、Symbol Table 和 String Table 的配合,找到自己要替换的函数,达到 hook 的目的。
> /* Returns the number of bytes read, or -1 if an error occurs preventing any
>
> bytes from being read, or 0 if the stream's end was encountered.
>
> It is an error to try and read from a stream that hasn't been opened first.
>
> This call will block until at least one byte is available; it will NOT block
>
> until the entire buffer can be filled. To avoid blocking, either poll using
>
> CFReadStreamHasBytesAvailable() or use the run loop and listen for the
>
> kCFStreamEventHasBytesAvailable event for notification of data available. */
>
> CF_EXPORT
>
> CFIndex CFReadStreamRead(CFReadStreamRef **_Null_unspecified** stream, UInt8 * **_Null_unspecified** buffer, CFIndex bufferLength);
CFNetwork 使用 CFReadStreamRef 来传递数据,使用回调函数的形式来接受服务器的响应。当回调函数受到
具体步骤及其关键代码如下,以 NSURLConnection 举例
- 因为要 Hook 挺多地方,所以写一个 method swizzling 的工具类
@@ -2791,6 +2831,10 @@ void printResponseData (CFDataRef responseData) {
这样下来就是可以监控到网络信息了,然后将数据交给数据上报 SDK按照下发的数据上报策略去上报数据。
##### 2.3.2 方法二
其实,针对上述的需求还有另一种方法一样可以达到目的,那就是 **isa swizzling**。
顺道说一句,上面针对 NSURLConnection、NSURLSession、NSInputStream 代理对象的 hook 之后,利用 NSProxy 实现代理对象方法的转发,有另一种方法可以实现,那就是 **isa swizzling**。
- Method swizzling 原理
@@ -2835,13 +2879,93 @@ void printResponseData (CFDataRef responseData) {
![isa swizzling](./../assets/2020-04-13-isaSwizzling.png)
我们来分析一下为什么修改 `isa` 可以实现目的呢?
1. 写 APM 监控的人没办法确定业务代码
2. 不可能为了方便监控 APM写某些类让业务线开发者别使用系统 NSURLSession、NSURLConnection 类
想想 KVO 的实现原理?结合上面的图
- 创建监控对象子类
- 重写子类中属性的 getter、seeter
- 将监控对象的 isa 指针指向新创建的子类
- 在子类的 getter、setter 中拦截值的变化,通知监控对象值的变化
- 监控完之后将监控对象的 isa 还原回去
按照这个思路,我们也可以对 NSURLConnection、NSURLSession 的 load 方法中动态创建子类,在子类中重写方法,比如 `- (**nullable** **instancetype**)initWithRequest:(NSURLRequest *)request delegate:(**nullable** **id**)delegate startImmediately:(**BOOL**)startImmediately;` ,然后将 NSURLSession、NSURLConnection 的 isa 指向动态创建的子类。在这些方法处理完之后还原本身的 isa 指针。
不过 isa swizzling 针对的还是 method swizzling代理对象不确定还是需要 NSProxy 进行动态处理。
#### 2.4 监控 App 常见网络请求<a name="categoryNameRules"></a>
至于如何修改 isa我写一个简单的 Demo 来模拟 KVO
```objective-c
- (void)lbpKVO_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context {
//生成自定义的名称
NSString *className = NSStringFromClass(self.class);
NSString *currentClassName = [@"LBPKVONotifying_" stringByAppendingString:className];
//1. runtime 生成类
Class myclass = objc_allocateClassPair(self.class, [currentClassName UTF8String], 0);
// 生成后不能马上使用,必须先注册
objc_registerClassPair(myclass);
//2. 重写 setter 方法
class_addMethod(myclass,@selector(say) , (IMP)say, "v@:@");
// class_addMethod(myclass,@selector(setName:) , (IMP)setName, "v@:@");
//3. 修改 isa
object_setClass(self, myclass);
//4. 将观察者保存到当前对象里面
objc_setAssociatedObject(self, "observer", observer, OBJC_ASSOCIATION_ASSIGN);
//5. 将传递的上下文绑定到当前对象里面
objc_setAssociatedObject(self, "context", (__bridge id _Nullable)(context), OBJC_ASSOCIATION_RETAIN);
}
void say(id self, SEL _cmd)
{
// 调用父类方法一
struct objc_super superclass = {self, [self superclass]};
((void(*)(struct objc_super *,SEL))objc_msgSendSuper)(&superclass,@selector(say));
NSLog(@"%s", __func__);
// 调用父类方法二
// Class class = [self class];
// object_setClass(self, class_getSuperclass(class));
// objc_msgSend(self, @selector(say));
}
void setName (id self, SEL _cmd, NSString *name) {
NSLog(@"come here");
//先切换到当前类的父类,然后发送消息 setName然后切换当前子类
//1. 切换到父类
Class class = [self class];
object_setClass(self, class_getSuperclass(class));
//2. 调用父类的 setName 方法
objc_msgSend(self, @selector(setName:), name);
//3. 调用观察
id observer = objc_getAssociatedObject(self, "observer");
id context = objc_getAssociatedObject(self, "context");
if (observer) {
objc_msgSend(observer, @selector(observeValueForKeyPath:ofObject:change:context:), @"name", self, @{@"new": name, @"kind": @1 } , context);
}
//4. 改回子类
object_setClass(self, class);
}
@end
```
#### 2.4 方案四:监控 App 常见网络请求
本着成本的原因,由于现在大多数的项目的网络能力都是通过 [AFNetworking](https://github.com/AFNetworking/AFNetworking) 完成的,所以本文的网络监控可以快速完成。
@@ -2913,16 +3037,130 @@ self.didCompleteObserver = [[NSNotificationCenter defaultCenter] addObserverForN
#### 2.5 iOS 流量监控
## 六、 Crash 监控
简易版本。
## 六、 电量消耗
移动设备上电量一直是比较敏感的问题,如果用户在某款 App 的时候发现耗电量严重、手机发热严重,那么用户很大可能会马上卸载这款 App。所以需要在开发阶段关心耗电量问题。
一般来说遇到耗电量较大,我们立马会想到是不是使用了定位、是不是使用了频繁网络请求、是不是不断循环做某件事情?
开发阶段基本没啥问题,我们可以结合 `Instrucments` 里的 `Energy Log` 工具来定位问题。但是线上问题就需要代码去监控耗电量,可以作为 APM 的能力之一。
### 1. 如何获取电量
在 iOS 中,`IOKit` 是一个私有框架,用来获取硬件和设备的详细信息,也是硬件和内核服务通信的底层框架。所以我们可以通过 `IOKit `来获取硬件信息,从而获取到电量信息。步骤如下:
- 首先在苹果开放源代码 opensource 中找到 [IOPowerSources.h](https://opensource.apple.com/source/IOKitUser/IOKitUser-647.6/ps.subproj/IOPowerSources.h.auto.html)、[IOPSKeys.h](https://opensource.apple.com/source/IOKitUser/IOKitUser-647.6/ps.subproj/IOPSKeys.h)。在 Xcode 的 `Package Contents` 里面找到 `IOKit.framework`。 路径为 `/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks/IOKit.framework`
- 然后将 IOPowerSources.h、IOPSKeys.h、IOKit.framework 导入项目工程
- 设置 UIDevice 的 batteryMonitoringEnabled 为 true
- 获取到的耗电量精确度为 1%
```objective-c
- (double)fetchBatteryCostUsage
{
// returns a blob of power source information in an opaque CFTypeRef
CFTypeRef blob = IOPSCopyPowerSourcesInfo();
// returns a CFArray of power source handles, each of type CFTypeRef
CFArrayRef sources = IOPSCopyPowerSourcesList(blob);
CFDictionaryRef pSource = NULL;
const void *psValue;
// returns the number of values currently in an array
int numOfSources = CFArrayGetCount(sources);
// error in CFArrayGetCount
if (numOfSources == 0) {
NSLog(@"Error in CFArrayGetCount");
return -1.0f;
}
// calculating the remaining energy
for (int i=0; i<numOfSources; i++) {
// returns a CFDictionary with readable information about the specific power source
pSource = IOPSGetPowerSourceDescription(blob, CFArrayGetValueAtIndex(sources, i));
if (!pSource) {
NSLog(@"Error in IOPSGetPowerSourceDescription");
return -1.0f;
}
psValue = (CFStringRef) CFDictionaryGetValue(pSource, CFSTR(kIOPSNameKey));
int curCapacity = 0;
int maxCapacity = 0;
double percentage;
psValue = CFDictionaryGetValue(pSource, CFSTR(kIOPSCurrentCapacityKey));
CFNumberGetValue((CFNumberRef)psValue, kCFNumberSInt32Type, &curCapacity);
psValue = CFDictionaryGetValue(pSource, CFSTR(kIOPSMaxCapacityKey));
CFNumberGetValue((CFNumberRef)psValue, kCFNumberSInt32Type, &maxCapacity);
percentage = ((double) curCapacity / (double) maxCapacity * 100.0f);
NSLog(@"curCapacity : %d / maxCapacity: %d , percentage: %.1f ", curCapacity, maxCapacity, percentage);
return percentage;
}
return -1.0f;
}
```
### 2. 定位问题
通常我们通过 Instrucments 里的 Energy Log 解决了很多问题后App 上线了,线上的耗电量解决就需要使用 APM 来解决了。耗电地方可能是二方库、三方库,也可能是某个同事的代码。
思路是:在检测到耗电后,先找到有问题的线程,然后堆栈 dump还原案发现场。
在上面部分我们知道了线程信息的结构, `thread_basic_info` 中有个记录 CPU 使用率百分比的字段 `cpu_usage`。所以我们可以通过遍历当前线程,判断哪个线程的 CPU 使用率较高,从而找出有问题的线程。然后再 dump 堆栈,从而定位到发生耗电量的代码。详细请看 [3.2](#threadInfo) 部分。
### 3. 开发阶段针对电量消耗我们能做什么
CPU 密集运算是耗电量主要原因。所以我们对 CPU 的使用需要精打细算。尽量避免让 CPU 做无用功。对于大量数据的复杂运算可以借助服务器的能力、GPU 的能力。如果方案设计必须是在 CPU 上完成数据的运算,则可以利用 GCD 技术,使用 `dispatch_block_create_with_qos_class(<#dispatch_block_flags_t flags#>, dispatch_qos_class_t qos_class, <#int relative_priority#>, <#^(void)block#>)()` 并指定 队列的 qos 为 `QOS_CLASS_UTILITY`。将任务提交到这个队列的 block 中,在 QOS_CLASS_UTILITY 模式下,系统针对大量数据的计算,做了电量优化
除了 CPU 大量运算I/O 操作也是耗电主要原因。业界常见方案都是将「碎片化的数据写入磁盘存储」这个操作延后先在内存中聚合吗然后再进行磁盘存储。碎片化数据先聚合在内存中进行存储的机制iOS 提供 `NSCache` 这个对象。
NSCache 是线程安全的NSCache 会在达到达预设的缓存空间的条件时清理缓存,此时会触发 `- (**void**)cache:(NSCache *)cache willEvictObject:(**id**)obj;` 方法回调,在该方法内部对数据进行 I/O 操作,达到将聚合的数据 I/O 延后的目的。I/O 次数少了,对电量的消耗也就减少了。
NSCache 的使用可以查看 SDWebImage 这个图片加载框架。在图片读取缓存处理时没直接读取硬盘文件I/O而是使用系统的 NSCache。
```objective-c
- (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key {
return [self.memoryCache objectForKey:key];
}
- (nullable UIImage *)imageFromDiskCacheForKey:(nullable NSString *)key {
UIImage *diskImage = [self diskImageForKey:key];
if (diskImage && self.config.shouldCacheImagesInMemory) {
NSUInteger cost = diskImage.sd_memoryCost;
[self.memoryCache setObject:diskImage forKey:key cost:cost];
}
return diskImage;
}
```
可以看到主要逻辑是先从磁盘中读取图片,如果配置允许开启内存缓存,则将图片保存到 NSCache 中,使用的时候也是从 NSCache 中读取图片。NSCache 的 `totalCostLimit、countLimit` 属性,
`- (void)setObject:(ObjectType)obj forKey:(KeyType)key cost:(NSUInteger)g;` 方法用来设置缓存条件。所以我们写磁盘、内存的文件操作时可以借鉴该策略,以优化耗电量。
## 七、 Crash 监控
对于奔溃
*base +offset= 你imp
## 参考资料
@@ -2942,3 +3180,4 @@ self.didCompleteObserver = [[NSNotificationCenter defaultCenter] addObserverForN
- [iOS堆栈信息解析函数地址与符号关联](https://www.jianshu.com/p/df5b08330afd)
- [Apple-CFNetwork Programming Guide](https://developer.apple.com/library/archive/documentation/Networking/Conceptual/CFNetwork/Introduction/Introduction.html#//apple_ref/doc/uid/TP30001132-CH1-DontLinkElementID_30)