mirror of
https://github.com/NohamR/knowledge-kit.git
synced 2026-05-24 20:00:37 +00:00
docs: 增加系统 UI API 子线程监控能力,并输出堆栈
This commit is contained in:
@@ -257,7 +257,6 @@ if (sourceHandledThisLoop && stopAfterHandle) {
|
||||
RunLoop 6 个状态
|
||||
|
||||
```Objective-C
|
||||
|
||||
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
|
||||
kCFRunLoopEntry , // 进入 loop
|
||||
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 临界值
|
||||
|
||||
| device | crash amount:MB | total amount:MB | percentage of total |
|
||||
| :--------------------------------: | :-------------: | :-------------: | :-----------------: |
|
||||
|:----------------------------------:|:---------------:|:---------------:|:-------------------:|
|
||||
| iPad1 | 127 | 256 | 49% |
|
||||
| iPad2 | 275 | 512 | 53% |
|
||||
| iPad3 | 645 | 1024 | 62% |
|
||||
@@ -1891,7 +1890,7 @@ App 发送一次网络请求一般会经历下面几个关键步骤:
|
||||
### 2. 监控原理
|
||||
|
||||
| 名称 | 说明 |
|
||||
| :-------------: | :---------------------: |
|
||||
|:---------------:|:----------------:|
|
||||
| NSURLConnection | 已经被废弃。用法简单 |
|
||||
| NSURLSession | iOS7.0 推出,功能更强大 |
|
||||
| CFNetwork | NSURL 的底层,纯 C 实现 |
|
||||
@@ -2699,48 +2698,52 @@ CFNetwork 使用 CFReadStreamRef 来传递数据,使用回调函数的形式
|
||||
@end
|
||||
```
|
||||
|
||||
|
||||
@implementation NetworkDelegateProxy
|
||||
|
||||
#pragma mark - life cycle
|
||||
|
||||
+ (instancetype)sharedInstance {
|
||||
+ (instancetype)sharedInstance {
|
||||
static NetworkDelegateProxy *_sharedInstance = nil;
|
||||
|
||||
static dispatch_once_t onceToken;
|
||||
|
||||
dispatch_once(&onceToken, ^{
|
||||
|
||||
_sharedInstance = [NetworkDelegateProxy alloc];
|
||||
|
||||
});
|
||||
|
||||
return _sharedInstance;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - public Method
|
||||
|
||||
+ (instancetype)setProxyForObject:(id)originalTarget withNewDelegate:(id)newDelegate
|
||||
+ (instancetype)setProxyForObject:(id)originalTarget withNewDelegate:(id)newDelegate
|
||||
{
|
||||
NetworkDelegateProxy *instance = [NetworkDelegateProxy sharedInstance];
|
||||
instance->_originalTarget = originalTarget;
|
||||
instance->_NewDelegate = newDelegate;
|
||||
return instance;
|
||||
}
|
||||
|
||||
- (void)forwardInvocation:(NSInvocation *)invocation
|
||||
- (void)forwardInvocation:(NSInvocation *)invocation
|
||||
{
|
||||
if ([_originalTarget respondsToSelector:invocation.selector]) {
|
||||
|
||||
[invocation invokeWithTarget:_originalTarget];
|
||||
[((NSURLSessionAndConnectionImplementor *)_NewDelegate) invoke:invocation];
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel
|
||||
- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel
|
||||
{
|
||||
return [_originalTarget methodSignatureForSelector:sel];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
|
||||
- 创建一个对象,实现 NSURLConnection、NSURLSession、NSIuputStream 代理方法
|
||||
@@ -2900,7 +2903,6 @@ CFNetwork 使用 CFReadStreamRef 来传递数据,使用回调函数的形式
|
||||
|
||||
/// A pointer to an instance of a class.
|
||||
typedef struct objc_object *id;
|
||||
|
||||
```
|
||||
|
||||

|
||||
@@ -4467,7 +4469,6 @@ static void setEnabled(bool isEnabled)
|
||||

|
||||
|
||||
```c
|
||||
|
||||
/** Start general exception processing.
|
||||
*
|
||||
* @oaram context Contextual information about the exception.
|
||||
@@ -5541,17 +5542,18 @@ parseJSError(line, column);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
#pragma mark - public Method
|
||||
|
||||
- (void)startMonitor
|
||||
- (void)startMonitor
|
||||
{
|
||||
APMMLog(@"crash monitor started");
|
||||
|
||||
#ifdef DEBUG
|
||||
BOOL _trackingCrashOnDebug = [APMMonitorConfig sharedInstance].trackingCrashOnDebug;
|
||||
if (_trackingCrashOnDebug) {
|
||||
|
||||
[self installKSCrash];
|
||||
|
||||
}
|
||||
#else
|
||||
[self installKSCrash];
|
||||
@@ -5570,13 +5572,15 @@ parseJSError(line, column);
|
||||
// ...
|
||||
}
|
||||
|
||||
- (void)installKSCrash
|
||||
- (void)installKSCrash
|
||||
{
|
||||
[[APMCrashInstallation sharedInstance] install];
|
||||
[[APMCrashInstallation sharedInstance] sendAllReportsWithCompletion:nil];
|
||||
[APMCrashInstallation sharedInstance].onCrash = onCrash;
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
|
||||
_isCanAddCrashCount = NO;
|
||||
|
||||
});
|
||||
}
|
||||
```
|
||||
@@ -5584,25 +5588,30 @@ parseJSError(line, column);
|
||||
在 `installKSCrash` 方法中调用了 `[[APMCrashInstallation sharedInstance] sendAllReportsWithCompletion: nil]`,内部实现如下
|
||||
|
||||
```objective-c
|
||||
- (void) sendAllReportsWithCompletion:(KSCrashReportFilterCompletion) onCompletion
|
||||
|
||||
- (void) sendAllReportsWithCompletion:(KSCrashReportFilterCompletion) onCompletion
|
||||
{
|
||||
NSError* error = [self validateProperties];
|
||||
if(error != nil)
|
||||
{
|
||||
|
||||
if(onCompletion != nil)
|
||||
{
|
||||
onCompletion(nil, NO, error);
|
||||
}
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
id<KSCrashReportFilter> sink = [self sink];
|
||||
if(sink == nil)
|
||||
{
|
||||
|
||||
onCompletion(nil, NO, [NSError errorWithDomain:[[self class] description]
|
||||
code:0
|
||||
description:@"Sink was nil (subclasses must implement method \"sink\")"]);
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
sink = [KSCrashReportFilterPipeline filterWithFilters:self.prependedFilters, sink, nil];
|
||||
@@ -5616,15 +5625,19 @@ parseJSError(line, column);
|
||||
方法内部将 `KSCrashInstallation` 的 `sink` 赋值给 `KSCrash` 对象。 内部还是调用了 `KSCrash` 的 `sendAllReportsWithCompletion` 方法,实现如下
|
||||
|
||||
```objective-c
|
||||
- (void) sendAllReportsWithCompletion:(KSCrashReportFilterCompletion) onCompletion
|
||||
|
||||
- (void) sendAllReportsWithCompletion:(KSCrashReportFilterCompletion) onCompletion
|
||||
{
|
||||
NSArray* reports = [self allReports];
|
||||
|
||||
KSLOG_INFO(@"Sending %d crash reports", [reports count]);
|
||||
|
||||
[self sendReports:reports
|
||||
|
||||
onCompletion:^(NSArray* filteredReports, BOOL completed, NSError* error)
|
||||
|
||||
{
|
||||
|
||||
KSLOG_DEBUG(@"Process finished with completion: %d", completed);
|
||||
if(error != nil)
|
||||
{
|
||||
@@ -5636,6 +5649,7 @@ parseJSError(line, column);
|
||||
kscrash_deleteAllReports();
|
||||
}
|
||||
kscrash_callCompletion(onCompletion, filteredReports, completed, error);
|
||||
|
||||
}];
|
||||
}
|
||||
```
|
||||
@@ -5643,27 +5657,36 @@ parseJSError(line, column);
|
||||
该方法内部调用了对象方法 `sendReports: onCompletion:`,如下所示
|
||||
|
||||
```objective-c
|
||||
- (void) sendReports:(NSArray*) reports onCompletion:(KSCrashReportFilterCompletion) onCompletion
|
||||
|
||||
- (void) sendReports:(NSArray*) reports onCompletion:(KSCrashReportFilterCompletion) onCompletion
|
||||
{
|
||||
if([reports count] == 0)
|
||||
{
|
||||
|
||||
kscrash_callCompletion(onCompletion, reports, YES, nil);
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
if(self.sink == nil)
|
||||
{
|
||||
|
||||
kscrash_callCompletion(onCompletion, reports, NO,
|
||||
[NSError errorWithDomain:[[self class] description]
|
||||
code:0
|
||||
description:@"No sink set. Crash reports not sent."]);
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
[self.sink filterReports:reports
|
||||
|
||||
onCompletion:^(NSArray* filteredReports, BOOL completed, NSError* error)
|
||||
|
||||
{
|
||||
|
||||
kscrash_callCompletion(onCompletion, filteredReports, completed, error);
|
||||
|
||||
}];
|
||||
}
|
||||
```
|
||||
@@ -5671,28 +5694,35 @@ parseJSError(line, column);
|
||||
方法内部的 `[self.sink filterReports: onCompletion: ]` 实现其实就是 `APMCrashInstallation` 中设置的 `sink` getter 方法,内部返回了 `APMCrashReporterSink` 对象的 `defaultCrashReportFilterSetAppleFmt` 方法的返回值。内部实现如下
|
||||
|
||||
```objective-c
|
||||
- (id <KSCrashReportFilter>) defaultCrashReportFilterSetAppleFmt
|
||||
|
||||
- (id <KSCrashReportFilter>) defaultCrashReportFilterSetAppleFmt
|
||||
{
|
||||
return [KSCrashReportFilterPipeline filterWithFilters:
|
||||
|
||||
[APMCrashReportFilterAppleFmt filterWithReportStyle:KSAppleReportStyleSymbolicatedSideBySide],
|
||||
self,
|
||||
nil];
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
可以看到这个函数内部设置了多个 **filters**,其中一个就是 **self**,也就是 `APMCrashReporterSink` 对象,所以上面的 ` [self.sink filterReports: onCompletion:]` ,也就是调用 `APMCrashReporterSink` 内的数据处理方法。完了之后通过 `kscrash_callCompletion(onCompletion, reports, YES, nil);` 告诉 `KSCrash` 本地保存的 Crash 日志已经处理完毕,可以删除了。
|
||||
|
||||
```objective-c
|
||||
- (void)filterReports:(NSArray *)reports onCompletion:(KSCrashReportFilterCompletion)onCompletion
|
||||
|
||||
- (void)filterReports:(NSArray *)reports onCompletion:(KSCrashReportFilterCompletion)onCompletion
|
||||
{
|
||||
for (NSDictionary *report in reports) {
|
||||
|
||||
// 处理 Crash 数据,将数据交给统一的数据上报组件处理...
|
||||
|
||||
}
|
||||
kscrash_callCompletion(onCompletion, reports, YES, nil);
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
至此,概括下 KSCrash 做的事情,提供各种 crash 的监控能力,在 crash 后将进程信息、基本信息、异常信息、线程信息等用 c 高效转换为 json 写入文件,App 下次启动后读取本地的 crash 文件夹中的 crash 日志,让开发者可以自定义 key、value 然后去上报日志到 APM 系统,然后删除本地 crash 文件夹中的日志。
|
||||
```
|
||||
|
||||
### 4. 符号化
|
||||
|
||||
@@ -5725,7 +5755,7 @@ DWARF 是可执行程序与源代码关系的一个紧凑表示。
|
||||
DWARF 文件中的数据如下:
|
||||
|
||||
| 数据列 | 信息说明 |
|
||||
| --------------- | -------------------------------------- |
|
||||
| --------------- | --------------------------- |
|
||||
| .debug_loc | 在 DW_AT_location 属性中使用的位置列表 |
|
||||
| .debug_macinfo | 宏信息 |
|
||||
| .debug_pubnames | 全局对象和函数的查找表 |
|
||||
@@ -5737,7 +5767,7 @@ DWARF 文件中的数据如下:
|
||||
常用的标记与属性如下:
|
||||
|
||||
| 数据列 | 信息说明 |
|
||||
| --------------------------- | ----------------------------- |
|
||||
| --------------------------- | ------------------- |
|
||||
| DW_TAG_class_type | 表示类名称和类型信息 |
|
||||
| DW_TAG_structure_type | 表示结构名称和类型信息 |
|
||||
| DW_TAG_union_type | 表示联合名称和类型信息 |
|
||||
@@ -6285,7 +6315,9 @@ Crash log 统一入库 Kibana 时是没有符号化的,所以需要符号化
|
||||
- Symbolication Service 作为整个监控系统的一个组成部分,是专注于 crash report 符号化的微服务。
|
||||
|
||||
- 接收来自任务调度框架的包含预处理过的 crash report 和 DSYM index 的请求,从七牛拉取对应的 DSYM,对 crash report 做符号化解析,计算 hash,并将 hash 响应给「数据处理和任务调度框架」。
|
||||
|
||||
- 接收来自 APM 管理系统的包含原始 crash report 和 DSYM index 的请求,从七牛拉取对应的 DSYM,对 crash report 做符号化解析,并将符号化的 crash report 响应给 APM 管理系统。
|
||||
|
||||
- 脚手架 cli 有个能力就是调用打包系统的打包构建能力,会根据项目的特点,选择合适的打包机(打包平台是维护了多个打包任务,不同任务根据特点被派发到不同的打包机上,任务详情页可以看到依赖的下载、编译、运行过程等,打包好的产物包括二进制包、下载二维码等等)
|
||||
|
||||

|
||||
@@ -6302,7 +6334,48 @@ Crash log 统一入库 Kibana 时是没有符号化的,所以需要符号化
|
||||
|
||||

|
||||
|
||||
## 八、 APM 小结
|
||||
## 八、子线程 UI 监控
|
||||
|
||||
### 1. 背景介绍
|
||||
|
||||
可能有些人一直没有遇到过因为在子线程操作 UI,导致在开发阶段 Xcode console 输出了一堆日志,大体如下
|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
其实我们可以给 Xcode 打个 `Runtime Issue Breakpoint` ,type 选择 `Main Thread Checker`, 在发生子线程操作 UI 的时候就会被系统检测到并触发断点,同时可以看到堆栈情况
|
||||
|
||||

|
||||
|
||||
效果如下
|
||||
|
||||

|
||||
|
||||
### 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` 来调用该方法,以达到对自定义类和方法的主线程调用监测。
|
||||

|
||||
|
||||
另外该功能可以在线下 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 本身是一个闭环,监控了之后需符号化解析、数据整理,进行产品化开发、最后需要监控大盘展示等
|
||||
|
||||
|
||||
@@ -27,3 +27,27 @@ _imageView.image = image;
|
||||
pthread_mutex_lock
|
||||
|
||||
pthread_mutex_unlock
|
||||
|
||||
|
||||
## 内存方面
|
||||
图片相关小建议
|
||||
目前对于大图片没有特别好的方法进行内存峰值的控制
|
||||
内存峰值:解码一瞬间带来的内存压力,无关最终图片可能缩放的大小
|
||||
1. 下发图片不要太大,字节内部大多数直接接入 ImageX 可以直接控制,
|
||||
图片下发尽可能不要超大类型的图片,这样对客户端下载和解码带来的压力都不低
|
||||
( 字节ImageX 外部也可接入服务 )
|
||||
2. 大部分主流图片库支持 Force Redraw 概念,默认都是开启的
|
||||
ForceRedraw 开启:3倍内存峰值,提升FPS
|
||||
ForceRedraw 关闭:2倍内存峰值,直到需要图片才进行渲染,降低 FPS
|
||||
( 字节图片库可以指定是否开启 )
|
||||
3. 如果是用户上传的图片,可以选择显示图片大小有一定范围,比如最大不超过 1920x1080,
|
||||
否则本地进行图片缩小后上传
|
||||
4. 部分低端机可以增加更多的解码限制,举例:
|
||||
iPhone6 内存大小为 1GB,iPhone6s 内存大小为 2GB
|
||||
对于 iPhone6 而言,一张 4000x6000,或着三张 1920x1080同时解码,就是压力的极限
|
||||
对于 iPhone6s 而言,两张 4000x6000,或着五张 1920x1080同时解码,就是压力的极限
|
||||
( 字节图片库支持提前获取图片大小,然后让选择是否解码图片的功能 )
|
||||
5. 图片解码后可以进行降采样
|
||||
虽然无法控制解码当时的内存峰值,但是对于解码后的图库可以进行缩放,
|
||||
例如在一个 100x100 的UIImageView 里展示一张 4000x6000的图片,显然非常浪费
|
||||
( 字节的图片库也支持自动降采样操作 )
|
||||
@@ -1,9 +1,10 @@
|
||||
# App 质量把控
|
||||
|
||||
> 笔者结合中台经验,本文重点谈谈 App 的质量稳定性该如何做。业务作为 App 的核心服务之一,业务异常监控当然也很重要,这不是本文重点。
|
||||
## 质量问题的现状
|
||||
对于质量问题,直接以小故事的形式展开。
|
||||
|
||||
## 质量问题的现状
|
||||
|
||||
对于质量问题,直接以小故事的形式展开。下面是移动中台年度针对质量复盘的一些思考
|
||||
|
||||
1. 技术方案阶体现测试用例
|
||||
对于业务项目来说,会存在测试资源、冒烟用例、精准测试、QA 新业务的业务回归、核心业务的 UI 自动化、高铁阶段的 QA 人工回归等。这里简单讲讲这些词语,对于新的业务项目,一定会有测试资源,简单说就是 QA,新项目在经过 PRD、MRD、需求讨论会、Kick-off 之后,技术方案评审后,会经过测试用例评审,产出的结果就是用例指南,到时候 QA 会在用例平台指配给对应的开发。
|
||||
|
||||
@@ -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
|
||||
5. git push origin 本地分支:远程分支
|
||||
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
|
||||
git commit -m '.gitattributes'
|
||||
//9
|
||||
git push origin master
|
||||
git push origin main
|
||||
//10
|
||||
git add /Users/liubinpeng/Desktop/Github/Company-Website-Pro/video/Company-Website-Pro.mov
|
||||
//11
|
||||
git commit -m 'video'
|
||||
//12
|
||||
git push origin master
|
||||
git push origin main
|
||||
|
||||
```
|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@ Pro 版:可以喝新鲜羊奶,淘宝有,不是那种劣质的羊奶粉。
|
||||
|
||||
## 六、 结语
|
||||
|
||||
小时候学完老舍的《猫》这篇文章就挺喜欢猫咪的,上班族的我们,下班后有猫咪在家陪伴或者在学习看书累了后,给猫咪喂食物、梳毛发等都是一种解压的行为。猫咪也是一门复杂的学问,需要主人的耐心和一席位经济基础(比如不要给猫咪吃30元1斤以下的猫咪,这样的猫粮钱省了,可以猫咪生病就是好几千,比较费猫 😂)
|
||||
小时候学完老舍的《猫》这篇文章就挺喜欢猫咪的,上班族的我们,下班后有猫咪在家陪伴或者在学习看书累了后,给猫咪喂食物、梳毛发等都是一种解压的行为。猫咪也是一门复杂的学问,需要主人的耐心和一定的经济基础(比如不要给猫咪吃30元1斤以下的猫咪,这样的猫粮钱省了,可以猫咪生病就是好几千,比较费猫 😂)
|
||||
|
||||
耐心观察他、善待他,猫咪和主人互相成就,他让你不再孤单、无聊、那么萌、可爱,可以成为你朋友圈的主角。你给了他一个温暖、舒适、安全、衣食无忧的家,所以,Enjoy yourself。
|
||||
|
||||
|
||||
BIN
assets/2022-0204-SubThreadUIMonitor@2x.PNG
Normal file
BIN
assets/2022-0204-SubThreadUIMonitor@2x.PNG
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 MiB |
BIN
assets/2022-0204-SubThreadUISymbolBreakpoints@2x.png
Normal file
BIN
assets/2022-0204-SubThreadUISymbolBreakpoints@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 326 KiB |
BIN
assets/2022-0204-SubThreadUIXcode1@2x.png
Normal file
BIN
assets/2022-0204-SubThreadUIXcode1@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 780 KiB |
BIN
assets/2022-0204-SubThreadUIXcodeBreakingPointsHappened@2x.png
Normal file
BIN
assets/2022-0204-SubThreadUIXcodeBreakingPointsHappened@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
Reference in New Issue
Block a user