docs: 增加系统 UI API 子线程监控能力,并输出堆栈

This commit is contained in:
LiuBinPeng
2022-02-07 10:08:32 +08:00
parent 418ee550cc
commit ab1e0b274a
9 changed files with 880 additions and 782 deletions

View File

@@ -257,7 +257,6 @@ if (sourceHandledThisLoop && stopAfterHandle) {
RunLoop 6 个状态 RunLoop 6 个状态
```Objective-C ```Objective-C
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry , // 进入 loop kCFRunLoopEntry , // 进入 loop
kCFRunLoopBeforeTimers , // 触发 Timer 回调 kCFRunLoopBeforeTimers , // 触发 Timer 回调
@@ -1320,7 +1319,7 @@ memorystatus_jld_eval_period_msecs = 6000; /* 6000 msecs == 6 second window */
[stackoverflow](https://stackoverflow.com/questions/5887248/ios-app-maximum-memory-budget/15200855#15200855) 上有一份数据,整理了各种设备的 OOM 临界值 [stackoverflow](https://stackoverflow.com/questions/5887248/ios-app-maximum-memory-budget/15200855#15200855) 上有一份数据,整理了各种设备的 OOM 临界值
| device | crash amount:MB | total amount:MB | percentage of total | | device | crash amount:MB | total amount:MB | percentage of total |
| :--------------------------------: | :-------------: | :-------------: | :-----------------: | |:----------------------------------:|:---------------:|:---------------:|:-------------------:|
| iPad1 | 127 | 256 | 49% | | iPad1 | 127 | 256 | 49% |
| iPad2 | 275 | 512 | 53% | | iPad2 | 275 | 512 | 53% |
| iPad3 | 645 | 1024 | 62% | | iPad3 | 645 | 1024 | 62% |
@@ -1891,7 +1890,7 @@ App 发送一次网络请求一般会经历下面几个关键步骤:
### 2. 监控原理 ### 2. 监控原理
| 名称 | 说明 | | 名称 | 说明 |
| :-------------: | :---------------------: | |:---------------:|:----------------:|
| NSURLConnection | 已经被废弃。用法简单 | | NSURLConnection | 已经被废弃。用法简单 |
| NSURLSession | iOS7.0 推出,功能更强大 | | NSURLSession | iOS7.0 推出,功能更强大 |
| CFNetwork | NSURL 的底层,纯 C 实现 | | CFNetwork | NSURL 的底层,纯 C 实现 |
@@ -2699,7 +2698,6 @@ CFNetwork 使用 CFReadStreamRef 来传递数据,使用回调函数的形式
@end @end
``` ```
@implementation NetworkDelegateProxy @implementation NetworkDelegateProxy
#pragma mark - life cycle #pragma mark - life cycle
@@ -2710,13 +2708,14 @@ CFNetwork 使用 CFReadStreamRef 来传递数据,使用回调函数的形式
static dispatch_once_t onceToken; static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{ dispatch_once(&onceToken, ^{
_sharedInstance = [NetworkDelegateProxy alloc]; _sharedInstance = [NetworkDelegateProxy alloc];
}); });
return _sharedInstance; return _sharedInstance;
} }
#pragma mark - public Method #pragma mark - public Method
+ (instancetype)setProxyForObject:(id)originalTarget withNewDelegate:(id)newDelegate + (instancetype)setProxyForObject:(id)originalTarget withNewDelegate:(id)newDelegate
@@ -2726,12 +2725,13 @@ CFNetwork 使用 CFReadStreamRef 来传递数据,使用回调函数的形式
instance->_NewDelegate = newDelegate; instance->_NewDelegate = newDelegate;
return instance; return instance;
} }
- (void)forwardInvocation:(NSInvocation *)invocation - (void)forwardInvocation:(NSInvocation *)invocation
{ {
if ([_originalTarget respondsToSelector:invocation.selector]) { if ([_originalTarget respondsToSelector:invocation.selector]) {
[invocation invokeWithTarget:_originalTarget]; [invocation invokeWithTarget:_originalTarget];
[((NSURLSessionAndConnectionImplementor *)_NewDelegate) invoke:invocation]; [((NSURLSessionAndConnectionImplementor *)_NewDelegate) invoke:invocation];
} }
} }
@@ -2741,6 +2741,9 @@ CFNetwork 使用 CFReadStreamRef 来传递数据,使用回调函数的形式
} }
@end @end
```
``` ```
- 创建一个对象,实现 NSURLConnection、NSURLSession、NSIuputStream 代理方法 - 创建一个对象,实现 NSURLConnection、NSURLSession、NSIuputStream 代理方法
@@ -2900,7 +2903,6 @@ CFNetwork 使用 CFReadStreamRef 来传递数据,使用回调函数的形式
/// A pointer to an instance of a class. /// A pointer to an instance of a class.
typedef struct objc_object *id; typedef struct objc_object *id;
``` ```
![isa swizzling](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-04-13-isaSwizzling.png) ![isa swizzling](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-04-13-isaSwizzling.png)
@@ -4467,7 +4469,6 @@ static void setEnabled(bool isEnabled)
![caller](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-03-KSCrashCaller.png) ![caller](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-03-KSCrashCaller.png)
```c ```c
/** Start general exception processing. /** Start general exception processing.
* *
* @oaram context Contextual information about the exception. * @oaram context Contextual information about the exception.
@@ -5541,7 +5542,6 @@ parseJSError(line, column);
} }
``` ```
#pragma mark - public Method #pragma mark - public Method
- (void)startMonitor - (void)startMonitor
@@ -5551,7 +5551,9 @@ parseJSError(line, column);
#ifdef DEBUG #ifdef DEBUG
BOOL _trackingCrashOnDebug = [APMMonitorConfig sharedInstance].trackingCrashOnDebug; BOOL _trackingCrashOnDebug = [APMMonitorConfig sharedInstance].trackingCrashOnDebug;
if (_trackingCrashOnDebug) { if (_trackingCrashOnDebug) {
[self installKSCrash]; [self installKSCrash];
} }
#else #else
[self installKSCrash]; [self installKSCrash];
@@ -5576,7 +5578,9 @@ parseJSError(line, column);
[[APMCrashInstallation sharedInstance] sendAllReportsWithCompletion:nil]; [[APMCrashInstallation sharedInstance] sendAllReportsWithCompletion:nil];
[APMCrashInstallation sharedInstance].onCrash = onCrash; [APMCrashInstallation sharedInstance].onCrash = onCrash;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
_isCanAddCrashCount = NO; _isCanAddCrashCount = NO;
}); });
} }
``` ```
@@ -5584,25 +5588,30 @@ parseJSError(line, column);
在 `installKSCrash` 方法中调用了 `[[APMCrashInstallation sharedInstance] sendAllReportsWithCompletion: nil]`,内部实现如下 在 `installKSCrash` 方法中调用了 `[[APMCrashInstallation sharedInstance] sendAllReportsWithCompletion: nil]`,内部实现如下
```objective-c ```objective-c
- (void) sendAllReportsWithCompletion:(KSCrashReportFilterCompletion) onCompletion - (void) sendAllReportsWithCompletion:(KSCrashReportFilterCompletion) onCompletion
{ {
NSError* error = [self validateProperties]; NSError* error = [self validateProperties];
if(error != nil) if(error != nil)
{ {
if(onCompletion != nil) if(onCompletion != nil)
{ {
onCompletion(nil, NO, error); onCompletion(nil, NO, error);
} }
return; return;
} }
id<KSCrashReportFilter> sink = [self sink]; id<KSCrashReportFilter> sink = [self sink];
if(sink == nil) if(sink == nil)
{ {
onCompletion(nil, NO, [NSError errorWithDomain:[[self class] description] onCompletion(nil, NO, [NSError errorWithDomain:[[self class] description]
code:0 code:0
description:@"Sink was nil (subclasses must implement method \"sink\")"]); description:@"Sink was nil (subclasses must implement method \"sink\")"]);
return; return;
} }
sink = [KSCrashReportFilterPipeline filterWithFilters:self.prependedFilters, sink, nil]; sink = [KSCrashReportFilterPipeline filterWithFilters:self.prependedFilters, sink, nil];
@@ -5616,6 +5625,7 @@ parseJSError(line, column);
方法内部将 `KSCrashInstallation` 的 `sink` 赋值给 `KSCrash` 对象。 内部还是调用了 `KSCrash` 的 `sendAllReportsWithCompletion` 方法,实现如下 方法内部将 `KSCrashInstallation` 的 `sink` 赋值给 `KSCrash` 对象。 内部还是调用了 `KSCrash` 的 `sendAllReportsWithCompletion` 方法,实现如下
```objective-c ```objective-c
- (void) sendAllReportsWithCompletion:(KSCrashReportFilterCompletion) onCompletion - (void) sendAllReportsWithCompletion:(KSCrashReportFilterCompletion) onCompletion
{ {
NSArray* reports = [self allReports]; NSArray* reports = [self allReports];
@@ -5623,8 +5633,11 @@ parseJSError(line, column);
KSLOG_INFO(@"Sending %d crash reports", [reports count]); KSLOG_INFO(@"Sending %d crash reports", [reports count]);
[self sendReports:reports [self sendReports:reports
onCompletion:^(NSArray* filteredReports, BOOL completed, NSError* error) onCompletion:^(NSArray* filteredReports, BOOL completed, NSError* error)
{ {
KSLOG_DEBUG(@"Process finished with completion: %d", completed); KSLOG_DEBUG(@"Process finished with completion: %d", completed);
if(error != nil) if(error != nil)
{ {
@@ -5636,6 +5649,7 @@ parseJSError(line, column);
kscrash_deleteAllReports(); kscrash_deleteAllReports();
} }
kscrash_callCompletion(onCompletion, filteredReports, completed, error); kscrash_callCompletion(onCompletion, filteredReports, completed, error);
}]; }];
} }
``` ```
@@ -5643,27 +5657,36 @@ parseJSError(line, column);
该方法内部调用了对象方法 `sendReports: onCompletion:`,如下所示 该方法内部调用了对象方法 `sendReports: onCompletion:`,如下所示
```objective-c ```objective-c
- (void) sendReports:(NSArray*) reports onCompletion:(KSCrashReportFilterCompletion) onCompletion - (void) sendReports:(NSArray*) reports onCompletion:(KSCrashReportFilterCompletion) onCompletion
{ {
if([reports count] == 0) if([reports count] == 0)
{ {
kscrash_callCompletion(onCompletion, reports, YES, nil); kscrash_callCompletion(onCompletion, reports, YES, nil);
return; return;
} }
if(self.sink == nil) if(self.sink == nil)
{ {
kscrash_callCompletion(onCompletion, reports, NO, kscrash_callCompletion(onCompletion, reports, NO,
[NSError errorWithDomain:[[self class] description] [NSError errorWithDomain:[[self class] description]
code:0 code:0
description:@"No sink set. Crash reports not sent."]); description:@"No sink set. Crash reports not sent."]);
return; return;
} }
[self.sink filterReports:reports [self.sink filterReports:reports
onCompletion:^(NSArray* filteredReports, BOOL completed, NSError* error) onCompletion:^(NSArray* filteredReports, BOOL completed, NSError* error)
{ {
kscrash_callCompletion(onCompletion, filteredReports, completed, error); kscrash_callCompletion(onCompletion, filteredReports, completed, error);
}]; }];
} }
``` ```
@@ -5671,28 +5694,35 @@ parseJSError(line, column);
方法内部的 `[self.sink filterReports: onCompletion: ]` 实现其实就是 `APMCrashInstallation` 中设置的 `sink` getter 方法,内部返回了 `APMCrashReporterSink` 对象的 `defaultCrashReportFilterSetAppleFmt` 方法的返回值。内部实现如下 方法内部的 `[self.sink filterReports: onCompletion: ]` 实现其实就是 `APMCrashInstallation` 中设置的 `sink` getter 方法,内部返回了 `APMCrashReporterSink` 对象的 `defaultCrashReportFilterSetAppleFmt` 方法的返回值。内部实现如下
```objective-c ```objective-c
- (id <KSCrashReportFilter>) defaultCrashReportFilterSetAppleFmt - (id <KSCrashReportFilter>) defaultCrashReportFilterSetAppleFmt
{ {
return [KSCrashReportFilterPipeline filterWithFilters: return [KSCrashReportFilterPipeline filterWithFilters:
[APMCrashReportFilterAppleFmt filterWithReportStyle:KSAppleReportStyleSymbolicatedSideBySide], [APMCrashReportFilterAppleFmt filterWithReportStyle:KSAppleReportStyleSymbolicatedSideBySide],
self, self,
nil]; nil];
} }
``` ```
可以看到这个函数内部设置了多个 **filters**,其中一个就是 **self**,也就是 `APMCrashReporterSink` 对象,所以上面的 ` [self.sink filterReports: onCompletion:]` ,也就是调用 `APMCrashReporterSink` 内的数据处理方法。完了之后通过 `kscrash_callCompletion(onCompletion, reports, YES, nil);` 告诉 `KSCrash` 本地保存的 Crash 日志已经处理完毕,可以删除了。 可以看到这个函数内部设置了多个 **filters**,其中一个就是 **self**,也就是 `APMCrashReporterSink` 对象,所以上面的 ` [self.sink filterReports: onCompletion:]` ,也就是调用 `APMCrashReporterSink` 内的数据处理方法。完了之后通过 `kscrash_callCompletion(onCompletion, reports, YES, nil);` 告诉 `KSCrash` 本地保存的 Crash 日志已经处理完毕,可以删除了。
```objective-c ```objective-c
- (void)filterReports:(NSArray *)reports onCompletion:(KSCrashReportFilterCompletion)onCompletion - (void)filterReports:(NSArray *)reports onCompletion:(KSCrashReportFilterCompletion)onCompletion
{ {
for (NSDictionary *report in reports) { for (NSDictionary *report in reports) {
// 处理 Crash 数据,将数据交给统一的数据上报组件处理... // 处理 Crash 数据,将数据交给统一的数据上报组件处理...
} }
kscrash_callCompletion(onCompletion, reports, YES, nil); kscrash_callCompletion(onCompletion, reports, YES, nil);
} }
```
```
至此,概括下 KSCrash 做的事情,提供各种 crash 的监控能力,在 crash 后将进程信息、基本信息、异常信息、线程信息等用 c 高效转换为 json 写入文件App 下次启动后读取本地的 crash 文件夹中的 crash 日志,让开发者可以自定义 key、value 然后去上报日志到 APM 系统,然后删除本地 crash 文件夹中的日志。 至此,概括下 KSCrash 做的事情,提供各种 crash 的监控能力,在 crash 后将进程信息、基本信息、异常信息、线程信息等用 c 高效转换为 json 写入文件App 下次启动后读取本地的 crash 文件夹中的 crash 日志,让开发者可以自定义 key、value 然后去上报日志到 APM 系统,然后删除本地 crash 文件夹中的日志。
```
### 4. 符号化 ### 4. 符号化
@@ -5725,7 +5755,7 @@ DWARF 是可执行程序与源代码关系的一个紧凑表示。
DWARF 文件中的数据如下: DWARF 文件中的数据如下:
| 数据列 | 信息说明 | | 数据列 | 信息说明 |
| --------------- | -------------------------------------- | | --------------- | --------------------------- |
| .debug_loc | 在 DW_AT_location 属性中使用的位置列表 | | .debug_loc | 在 DW_AT_location 属性中使用的位置列表 |
| .debug_macinfo | 宏信息 | | .debug_macinfo | 宏信息 |
| .debug_pubnames | 全局对象和函数的查找表 | | .debug_pubnames | 全局对象和函数的查找表 |
@@ -5737,7 +5767,7 @@ DWARF 文件中的数据如下:
常用的标记与属性如下: 常用的标记与属性如下:
| 数据列 | 信息说明 | | 数据列 | 信息说明 |
| --------------------------- | ----------------------------- | | --------------------------- | ------------------- |
| DW_TAG_class_type | 表示类名称和类型信息 | | DW_TAG_class_type | 表示类名称和类型信息 |
| DW_TAG_structure_type | 表示结构名称和类型信息 | | DW_TAG_structure_type | 表示结构名称和类型信息 |
| DW_TAG_union_type | 表示联合名称和类型信息 | | DW_TAG_union_type | 表示联合名称和类型信息 |
@@ -6285,7 +6315,9 @@ Crash log 统一入库 Kibana 时是没有符号化的,所以需要符号化
- Symbolication Service 作为整个监控系统的一个组成部分,是专注于 crash report 符号化的微服务。 - Symbolication Service 作为整个监控系统的一个组成部分,是专注于 crash report 符号化的微服务。
- 接收来自任务调度框架的包含预处理过的 crash report 和 DSYM index 的请求,从七牛拉取对应的 DSYM对 crash report 做符号化解析,计算 hash并将 hash 响应给「数据处理和任务调度框架」。 - 接收来自任务调度框架的包含预处理过的 crash report 和 DSYM index 的请求,从七牛拉取对应的 DSYM对 crash report 做符号化解析,计算 hash并将 hash 响应给「数据处理和任务调度框架」。
- 接收来自 APM 管理系统的包含原始 crash report 和 DSYM index 的请求,从七牛拉取对应的 DSYM对 crash report 做符号化解析,并将符号化的 crash report 响应给 APM 管理系统。 - 接收来自 APM 管理系统的包含原始 crash report 和 DSYM index 的请求,从七牛拉取对应的 DSYM对 crash report 做符号化解析,并将符号化的 crash report 响应给 APM 管理系统。
- 脚手架 cli 有个能力就是调用打包系统的打包构建能力,会根据项目的特点,选择合适的打包机(打包平台是维护了多个打包任务,不同任务根据特点被派发到不同的打包机上,任务详情页可以看到依赖的下载、编译、运行过程等,打包好的产物包括二进制包、下载二维码等等) - 脚手架 cli 有个能力就是调用打包系统的打包构建能力,会根据项目的特点,选择合适的打包机(打包平台是维护了多个打包任务,不同任务根据特点被派发到不同的打包机上,任务详情页可以看到依赖的下载、编译、运行过程等,打包好的产物包括二进制包、下载二维码等等)
![符号化流程图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-17-symolication_flow.png) ![符号化流程图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-17-symolication_flow.png)
@@ -6302,7 +6334,48 @@ Crash log 统一入库 Kibana 时是没有符号化的,所以需要符号化
![符号化服务架构图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-17-SymbolicateServerArch.png) ![符号化服务架构图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-17-SymbolicateServerArch.png)
## 八、 APM 小结 ## 八、子线程 UI 监控
### 1. 背景介绍
可能有些人一直没有遇到过因为在子线程操作 UI导致在开发阶段 Xcode console 输出了一堆日志,大体如下
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2022-0204-SubThreadUIXcode1@2x.png)
其实我们可以给 Xcode 打个 `Runtime Issue Breakpoint` type 选择 `Main Thread Checker` 在发生子线程操作 UI 的时候就会被系统检测到并触发断点,同时可以看到堆栈情况
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2022-0204-SubThreadUISymbolBreakpoints@2x.png)
效果如下
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2022-0204-SubThreadUIXcodeBreakingPointsHappened@2x.png)
### 2. 问题及解决方案
上述的功能是在 Xcode 自带的,连接 Xcode 做调试才具备的功能,线上包无法检测到。
经过探索 Xcode 实现该功能是依赖于设备上的` libMainThreadChecker.dylib` 库,我们可以通过 `dlopen` 方法强制加载该库让非 Xcode 环境下也拥有监测功能。
另外在监控到子线程调用 UI 调用时,在 Xcode 环境下,会将调用栈输出到控制台,经过测试,`libMainThreadChecker.dylib` 使用的是进行输出的,由于 NSLog 是将信息输出到 `STDERR`中,我们可以通过 `NSPipe` 与 `dup2` 将 `STDERR` 输出拦截,通过对信息的文案的判断,进而获取监测到的 UI 调用,最后可以通过堆栈打印出来,就可以帮助定位到具体问题。
`libMainThreadChecker.dylib` 库具有局限性,仅仅对系统提供的一些特定类的特定 API 在子线程调用会被监控到(例如 UIKit 框架中 UIView 类)。
但是某些类有些 API 我们也不希望在子线程被调用,这时候 `libMainThreadChecker.dylib`是无法满足的。
对 `libMainThreadChecker.dylib` 库的汇编代码研究,发现 `libMainThreadChecker.dylib` 是通过内部 `__main_thread_add_check_for_selector` 这个方法来进行类和方法的注册的。所以如果我们同样可以通过 `dlsym` 来调用该方法,以达到对自定义类和方法的主线程调用监测。
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2022-0204-SubThreadUIMonitor@2x.PNG)
另外该功能可以在线下 debug 阶段开启,判断是否是在 Xcode debug 状态,可以通过苹果提供的[官方判断方法](https://developer.apple.com/library/archive/qa/qa1361/_index.html#//apple_ref/doc/uid/DTS10003368)实现。
对 [dlopen](https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/dlopen.3.html)、[dlsym](https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/dlsym.3.html) 陌生的小伙伴可以直接看 Apple 官方文档,这里不做展开。
## 九、 APM 小结
1. 通常来说各个端的监控能力是不太一致的,技术实现细节也不统一。所以在技术方案评审的时候需要将监控能力对齐统一。每个能力在各个端的数据字段必须对齐(字段个数、名称、数据类型和精度),因为 APM 本身是一个闭环,监控了之后需符号化解析、数据整理,进行产品化开发、最后需要监控大盘展示等 1. 通常来说各个端的监控能力是不太一致的,技术实现细节也不统一。所以在技术方案评审的时候需要将监控能力对齐统一。每个能力在各个端的数据字段必须对齐(字段个数、名称、数据类型和精度),因为 APM 本身是一个闭环,监控了之后需符号化解析、数据整理,进行产品化开发、最后需要监控大盘展示等

View File

@@ -27,3 +27,27 @@ _imageView.image = image;
pthread_mutex_lock pthread_mutex_lock
pthread_mutex_unlock pthread_mutex_unlock
## 内存方面
图片相关小建议
目前对于大图片没有特别好的方法进行内存峰值的控制
内存峰值:解码一瞬间带来的内存压力,无关最终图片可能缩放的大小
1. 下发图片不要太大,字节内部大多数直接接入 ImageX 可以直接控制,
图片下发尽可能不要超大类型的图片,这样对客户端下载和解码带来的压力都不低
( 字节ImageX 外部也可接入服务 )
2. 大部分主流图片库支持 Force Redraw 概念,默认都是开启的
ForceRedraw 开启3倍内存峰值提升FPS
ForceRedraw 关闭2倍内存峰值直到需要图片才进行渲染降低 FPS
( 字节图片库可以指定是否开启 )
3. 如果是用户上传的图片,可以选择显示图片大小有一定范围,比如最大不超过 1920x1080
否则本地进行图片缩小后上传
4. 部分低端机可以增加更多的解码限制,举例:
iPhone6 内存大小为 1GBiPhone6s 内存大小为 2GB
对于 iPhone6 而言,一张 4000x6000或着三张 1920x1080同时解码就是压力的极限
对于 iPhone6s 而言,两张 4000x6000或着五张 1920x1080同时解码就是压力的极限
( 字节图片库支持提前获取图片大小,然后让选择是否解码图片的功能 )
5. 图片解码后可以进行降采样
虽然无法控制解码当时的内存峰值,但是对于解码后的图库可以进行缩放,
例如在一个 100x100 的UIImageView 里展示一张 4000x6000的图片显然非常浪费
( 字节的图片库也支持自动降采样操作 )

View File

@@ -1,9 +1,10 @@
# App 质量把控 # App 质量把控
> 笔者结合中台经验,本文重点谈谈 App 的质量稳定性该如何做。业务作为 App 的核心服务之一,业务异常监控当然也很重要,这不是本文重点。 > 笔者结合中台经验,本文重点谈谈 App 的质量稳定性该如何做。业务作为 App 的核心服务之一,业务异常监控当然也很重要,这不是本文重点。
## 质量问题的现状
对于质量问题,直接以小故事的形式展开。
## 质量问题的现状
对于质量问题,直接以小故事的形式展开。下面是移动中台年度针对质量复盘的一些思考
1. 技术方案阶体现测试用例 1. 技术方案阶体现测试用例
对于业务项目来说会存在测试资源、冒烟用例、精准测试、QA 新业务的业务回归、核心业务的 UI 自动化、高铁阶段的 QA 人工回归等。这里简单讲讲这些词语,对于新的业务项目,一定会有测试资源,简单说就是 QA新项目在经过 PRD、MRD、需求讨论会、Kick-off 之后,技术方案评审后,会经过测试用例评审,产出的结果就是用例指南,到时候 QA 会在用例平台指配给对应的开发。 对于业务项目来说会存在测试资源、冒烟用例、精准测试、QA 新业务的业务回归、核心业务的 UI 自动化、高铁阶段的 QA 人工回归等。这里简单讲讲这些词语,对于新的业务项目,一定会有测试资源,简单说就是 QA新项目在经过 PRD、MRD、需求讨论会、Kick-off 之后,技术方案评审后,会经过测试用例评审,产出的结果就是用例指南,到时候 QA 会在用例平台指配给对应的开发。

View File

@@ -10,7 +10,7 @@ http://blog.csdn.net/wirelessqa/article/details/20153689
4. git remote add origin git@xx.xx.xx.xx:repos/xxx/xxx/xxx.git 4. git remote add origin git@xx.xx.xx.xx:repos/xxx/xxx/xxx.git
5. git push origin 本地分支:远程分支 5. git push origin 本地分支:远程分支
fatal: refusing to merge unrelated histories fatal: refusing to merge unrelated histories
决办法git pull origin master --allow-unrelated-histories 决办法git pull origin main --allow-unrelated-histories
@@ -51,13 +51,13 @@ git add .gitattributes
//8 //8
git commit -m '.gitattributes' git commit -m '.gitattributes'
//9 //9
git push origin master git push origin main
//10 //10
git add /Users/liubinpeng/Desktop/Github/Company-Website-Pro/video/Company-Website-Pro.mov git add /Users/liubinpeng/Desktop/Github/Company-Website-Pro/video/Company-Website-Pro.mov
//11 //11
git commit -m 'video' git commit -m 'video'
//12 //12
git push origin master git push origin main
``` ```

View File

@@ -118,7 +118,7 @@ Pro 版:可以喝新鲜羊奶,淘宝有,不是那种劣质的羊奶粉。
## 六、 结语 ## 六、 结语
小时候学完老舍的《猫》这篇文章就挺喜欢猫咪的,上班族的我们,下班后有猫咪在家陪伴或者在学习看书累了后,给猫咪喂食物、梳毛发等都是一种解压的行为。猫咪也是一门复杂的学问,需要主人的耐心和一席位经济基础比如不要给猫咪吃30元1斤以下的猫咪这样的猫粮钱省了可以猫咪生病就是好几千比较费猫 😂) 小时候学完老舍的《猫》这篇文章就挺喜欢猫咪的,上班族的我们,下班后有猫咪在家陪伴或者在学习看书累了后,给猫咪喂食物、梳毛发等都是一种解压的行为。猫咪也是一门复杂的学问,需要主人的耐心和一定的经济基础比如不要给猫咪吃30元1斤以下的猫咪这样的猫粮钱省了可以猫咪生病就是好几千比较费猫 😂)
耐心观察他、善待他猫咪和主人互相成就他让你不再孤单、无聊、那么萌、可爱可以成为你朋友圈的主角。你给了他一个温暖、舒适、安全、衣食无忧的家所以Enjoy yourself。 耐心观察他、善待他猫咪和主人互相成就他让你不再孤单、无聊、那么萌、可爱可以成为你朋友圈的主角。你给了他一个温暖、舒适、安全、衣食无忧的家所以Enjoy yourself。

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 780 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB