diff --git a/Chapter1 - iOS/1.74.md b/Chapter1 - iOS/1.74.md index e34dd5a..13947d3 100644 --- a/Chapter1 - iOS/1.74.md +++ b/Chapter1 - iOS/1.74.md @@ -125,7 +125,7 @@ if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) { Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR); // 通知 Observers: RunLoop 的线程即将进入休眠(sleep) if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting); - __CFRunLoopSetSleeping(rl); + __CFRunLoopSetSleeping(rl); ``` 第五步:进入休眠后,会等待 mach_port 消息,以便再次唤醒。只有以下 4 种情况才可以被再次唤醒。 @@ -218,12 +218,12 @@ if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObserve CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort); if (rls) { #if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI - mach_msg_header_t *reply = NULL; - sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop; - if (NULL != reply) { - (void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL); - CFAllocatorDeallocate(kCFAllocatorSystemDefault, reply); - } + mach_msg_header_t *reply = NULL; + sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop; + if (NULL != reply) { + (void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL); + CFAllocatorDeallocate(kCFAllocatorSystemDefault, reply); + } #elif DEPLOYMENT_TARGET_WINDOWS sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls) || sourceHandledThisLoop; #endif @@ -257,7 +257,6 @@ if (sourceHandledThisLoop && stopAfterHandle) { RunLoop 6 个状态 ```Objective-C - typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { kCFRunLoopEntry , // 进入 loop kCFRunLoopBeforeTimers , // 触发 Timer 回调 @@ -609,15 +608,15 @@ RISC 架构要求软件来指定各个操作步骤。比如上面的乘法,指 ```Objective-c struct thread_basic_info { - time_value_t user_time; /* user run time(用户运行时长) */ - time_value_t system_time; /* system run time(系统运行时长) */ - integer_t cpu_usage; /* scaled cpu usage percentage(CPU使用率,上限1000) */ - policy_t policy; /* scheduling policy in effect(有效调度策略) */ - integer_t run_state; /* run state (运行状态,见下) */ - integer_t flags; /* various flags (各种各样的标记) */ - integer_t suspend_count; /* suspend count for thread(线程挂起次数) */ - integer_t sleep_time; /* number of seconds that thread - * has been sleeping(休眠时间) */ + time_value_t user_time; /* user run time(用户运行时长) */ + time_value_t system_time; /* system run time(系统运行时长) */ + integer_t cpu_usage; /* scaled cpu usage percentage(CPU使用率,上限1000) */ + policy_t policy; /* scheduling policy in effect(有效调度策略) */ + integer_t run_state; /* run state (运行状态,见下) */ + integer_t flags; /* various flags (各种各样的标记) */ + integer_t suspend_count; /* suspend count for thread(线程挂起次数) */ + integer_t sleep_time; /* number of seconds that thread + * has been sleeping(休眠时间) */ }; ``` @@ -730,23 +729,23 @@ Memory page\*\* 是内存管理中的最小单位,是系统分配的,可能 - Clean Memory Clean memory 包括 3 类:可以 `page out` 的内存、内存映射文件、App 使用到的 framework(每个 framework 都有 \_DATA_CONST 段,通常都是 clean 状态,但使用 runtime swizling,那么变为 dirty)。 - + 一开始分配的 page 都是干净的(堆里面的对象分配除外),我们 App 数据写入时候变为 dirty。从硬盘读进内存的文件,也是只读的、clean page。 - + ![Clean memory](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-28-iOSMemoryTypeClean.png) - Dirty Memory - + Dirty memory 包括 4 类:被 App 写入过数据的内存、所有堆区分配的对象、图像解码缓冲区、framework(framework 都有 \_DATA 段和 \_DATA_DIRTY 段,它们的内存都是 dirty)。 - + 在使用 framework 的过程中会产生 Dirty memory,使用单例或者全局初始化方法有助于帮助减少 Dirty memory(因为单例一旦创建就不销毁,一直在内存中,系统不认为是 Dirty memory)。 - + ![Dirty memory](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-28-iOSMemoryTypeDirty.png) - Compressed Memory - + 由于闪存容量和读写限制,iOS 没有交换空间机制,而是在 iOS7 引入了 **memory compressor**。它是在内存紧张时候能够将最近一段时间未使用过的内存对象,内存压缩器会把对象压缩,释放出更多的 page。在需要时内存压缩器对其解压复用。在节省内存的同时提高了响应速度。 - + 比如 App 使用某 Framework,内部有个 NSDictionary 属性存储数据,使用了 3 pages 内存,在近期未被访问的时候 memory compressor 将其压缩为 1 page,再次使用的时候还原为 3 pages。 App 运行内存 = pageNumbers \* pageSize。因为 Compressed Memory 属于 Dirty memory。所以 Memory footprint = dirtySize + CompressedSize @@ -1002,12 +1001,12 @@ memstat_bucket_t memstat_bucket[MEMSTAT_BUCKET_COUNT]; #define JETSAM_PRIORITY_IDLE_HEAD -2 /* The value -1 is an alias to JETSAM_PRIORITY_DEFAULT */ #define JETSAM_PRIORITY_IDLE 0 -#define JETSAM_PRIORITY_IDLE_DEFERRED 1 /* Keeping this around till all xnu_quick_tests can be moved away from it.*/ -#define JETSAM_PRIORITY_AGING_BAND1 JETSAM_PRIORITY_IDLE_DEFERRED +#define JETSAM_PRIORITY_IDLE_DEFERRED 1 /* Keeping this around till all xnu_quick_tests can be moved away from it.*/ +#define JETSAM_PRIORITY_AGING_BAND1 JETSAM_PRIORITY_IDLE_DEFERRED #define JETSAM_PRIORITY_BACKGROUND_OPPORTUNISTIC 2 -#define JETSAM_PRIORITY_AGING_BAND2 JETSAM_PRIORITY_BACKGROUND_OPPORTUNISTIC +#define JETSAM_PRIORITY_AGING_BAND2 JETSAM_PRIORITY_BACKGROUND_OPPORTUNISTIC #define JETSAM_PRIORITY_BACKGROUND 3 -#define JETSAM_PRIORITY_ELEVATED_INACTIVE JETSAM_PRIORITY_BACKGROUND +#define JETSAM_PRIORITY_ELEVATED_INACTIVE JETSAM_PRIORITY_BACKGROUND #define JETSAM_PRIORITY_MAIL 4 #define JETSAM_PRIORITY_PHONE 5 #define JETSAM_PRIORITY_UI_SUPPORT 8 @@ -1032,19 +1031,19 @@ memstat_bucket_t memstat_bucket[MEMSTAT_BUCKET_COUNT]; ```shell /* For logging clarity */ static const char *memorystatus_kill_cause_name[] = { - "" , /* kMemorystatusInvalid */ - "jettisoned" , /* kMemorystatusKilled */ - "highwater" , /* kMemorystatusKilledHiwat */ - "vnode-limit" , /* kMemorystatusKilledVnodes */ - "vm-pageshortage" , /* kMemorystatusKilledVMPageShortage */ - "proc-thrashing" , /* kMemorystatusKilledProcThrashing */ - "fc-thrashing" , /* kMemorystatusKilledFCThrashing */ - "per-process-limit" , /* kMemorystatusKilledPerProcessLimit */ - "disk-space-shortage" , /* kMemorystatusKilledDiskSpaceShortage */ - "idle-exit" , /* kMemorystatusKilledIdleExit */ - "zone-map-exhaustion" , /* kMemorystatusKilledZoneMapExhaustion */ - "vm-compressor-thrashing" , /* kMemorystatusKilledVMCompressorThrashing */ - "vm-compressor-space-shortage" , /* kMemorystatusKilledVMCompressorSpaceShortage */ + "" , /* kMemorystatusInvalid */ + "jettisoned" , /* kMemorystatusKilled */ + "highwater" , /* kMemorystatusKilledHiwat */ + "vnode-limit" , /* kMemorystatusKilledVnodes */ + "vm-pageshortage" , /* kMemorystatusKilledVMPageShortage */ + "proc-thrashing" , /* kMemorystatusKilledProcThrashing */ + "fc-thrashing" , /* kMemorystatusKilledFCThrashing */ + "per-process-limit" , /* kMemorystatusKilledPerProcessLimit */ + "disk-space-shortage" , /* kMemorystatusKilledDiskSpaceShortage */ + "idle-exit" , /* kMemorystatusKilledIdleExit */ + "zone-map-exhaustion" , /* kMemorystatusKilledZoneMapExhaustion */ + "vm-compressor-thrashing" , /* kMemorystatusKilledVMCompressorThrashing */ + "vm-compressor-space-shortage" , /* kMemorystatusKilledVMCompressorSpaceShortage */ }; ``` @@ -1054,80 +1053,80 @@ static const char *memorystatus_kill_cause_name[] = { __private_extern__ void memorystatus_init(void) { - // ... + // ... /* Initialize the jetsam_threads state array */ - jetsam_threads = kalloc(sizeof(struct jetsam_thread_state) * max_jetsam_threads); + jetsam_threads = kalloc(sizeof(struct jetsam_thread_state) * max_jetsam_threads); - /* Initialize all the jetsam threads */ - for (i = 0; i < max_jetsam_threads; i++) { + /* Initialize all the jetsam threads */ + for (i = 0; i < max_jetsam_threads; i++) { - result = kernel_thread_start_priority(memorystatus_thread, NULL, 95 /* MAXPRI_KERNEL */, &jetsam_threads[i].thread); - if (result == KERN_SUCCESS) { - jetsam_threads[i].inited = FALSE; - jetsam_threads[i].index = i; - thread_deallocate(jetsam_threads[i].thread); - } else { - panic("Could not create memorystatus_thread %d", i); - } - } + result = kernel_thread_start_priority(memorystatus_thread, NULL, 95 /* MAXPRI_KERNEL */, &jetsam_threads[i].thread); + if (result == KERN_SUCCESS) { + jetsam_threads[i].inited = FALSE; + jetsam_threads[i].index = i; + thread_deallocate(jetsam_threads[i].thread); + } else { + panic("Could not create memorystatus_thread %d", i); + } + } } ``` ```shell /* - * High-level priority assignments + * High-level priority assignments * ************************************************************************* - * 127 Reserved (real-time) - * A - * + - * (32 levels) - * + - * V - * 96 Reserved (real-time) - * 95 Kernel mode only - * A - * + - * (16 levels) - * + - * V - * 80 Kernel mode only - * 79 System high priority - * A - * + - * (16 levels) - * + - * V - * 64 System high priority - * 63 Elevated priorities - * A - * + - * (12 levels) - * + - * V - * 52 Elevated priorities - * 51 Elevated priorities (incl. BSD +nice) - * A - * + - * (20 levels) - * + - * V - * 32 Elevated priorities (incl. BSD +nice) - * 31 Default (default base for threads) - * 30 Lowered priorities (incl. BSD -nice) - * A - * + - * (20 levels) - * + - * V - * 11 Lowered priorities (incl. BSD -nice) - * 10 Lowered priorities (aged pri's) - * A - * + - * (11 levels) - * + - * V - * 0 Lowered priorities (aged pri's / idle) + * 127 Reserved (real-time) + * A + * + + * (32 levels) + * + + * V + * 96 Reserved (real-time) + * 95 Kernel mode only + * A + * + + * (16 levels) + * + + * V + * 80 Kernel mode only + * 79 System high priority + * A + * + + * (16 levels) + * + + * V + * 64 System high priority + * 63 Elevated priorities + * A + * + + * (12 levels) + * + + * V + * 52 Elevated priorities + * 51 Elevated priorities (incl. BSD +nice) + * A + * + + * (20 levels) + * + + * V + * 32 Elevated priorities (incl. BSD +nice) + * 31 Default (default base for threads) + * 30 Lowered priorities (incl. BSD -nice) + * A + * + + * (20 levels) + * + + * V + * 11 Lowered priorities (incl. BSD -nice) + * 10 Lowered priorities (aged pri's) + * A + * + + * (11 levels) + * + + * V + * 0 Lowered priorities (aged pri's / idle) ************************************************************************* */ ``` @@ -1144,112 +1143,112 @@ memorystatus_thread(void *param __unused, wait_result_t wr __unused) { //... while (memorystatus_action_needed()) { - boolean_t killed; - int32_t priority; - uint32_t cause; - uint64_t jetsam_reason_code = JETSAM_REASON_INVALID; - os_reason_t jetsam_reason = OS_REASON_NULL; + boolean_t killed; + int32_t priority; + uint32_t cause; + uint64_t jetsam_reason_code = JETSAM_REASON_INVALID; + os_reason_t jetsam_reason = OS_REASON_NULL; - cause = kill_under_pressure_cause; - switch (cause) { - case kMemorystatusKilledFCThrashing: - jetsam_reason_code = JETSAM_REASON_MEMORY_FCTHRASHING; - break; - case kMemorystatusKilledVMCompressorThrashing: - jetsam_reason_code = JETSAM_REASON_MEMORY_VMCOMPRESSOR_THRASHING; - break; - case kMemorystatusKilledVMCompressorSpaceShortage: - jetsam_reason_code = JETSAM_REASON_MEMORY_VMCOMPRESSOR_SPACE_SHORTAGE; - break; - case kMemorystatusKilledZoneMapExhaustion: - jetsam_reason_code = JETSAM_REASON_ZONE_MAP_EXHAUSTION; - break; - case kMemorystatusKilledVMPageShortage: - /* falls through */ - default: - jetsam_reason_code = JETSAM_REASON_MEMORY_VMPAGESHORTAGE; - cause = kMemorystatusKilledVMPageShortage; - break; - } + cause = kill_under_pressure_cause; + switch (cause) { + case kMemorystatusKilledFCThrashing: + jetsam_reason_code = JETSAM_REASON_MEMORY_FCTHRASHING; + break; + case kMemorystatusKilledVMCompressorThrashing: + jetsam_reason_code = JETSAM_REASON_MEMORY_VMCOMPRESSOR_THRASHING; + break; + case kMemorystatusKilledVMCompressorSpaceShortage: + jetsam_reason_code = JETSAM_REASON_MEMORY_VMCOMPRESSOR_SPACE_SHORTAGE; + break; + case kMemorystatusKilledZoneMapExhaustion: + jetsam_reason_code = JETSAM_REASON_ZONE_MAP_EXHAUSTION; + break; + case kMemorystatusKilledVMPageShortage: + /* falls through */ + default: + jetsam_reason_code = JETSAM_REASON_MEMORY_VMPAGESHORTAGE; + cause = kMemorystatusKilledVMPageShortage; + break; + } - /* Highwater */ - boolean_t is_critical = TRUE; - if (memorystatus_act_on_hiwat_processes(&errors, &hwm_kill, &post_snapshot, &is_critical)) { - if (is_critical == FALSE) { - /* - * For now, don't kill any other processes. - */ - break; - } else { - goto done; - } - } + /* Highwater */ + boolean_t is_critical = TRUE; + if (memorystatus_act_on_hiwat_processes(&errors, &hwm_kill, &post_snapshot, &is_critical)) { + if (is_critical == FALSE) { + /* + * For now, don't kill any other processes. + */ + break; + } else { + goto done; + } + } - jetsam_reason = os_reason_create(OS_REASON_JETSAM, jetsam_reason_code); - if (jetsam_reason == OS_REASON_NULL) { - printf("memorystatus_thread: failed to allocate jetsam reason\n"); - } + jetsam_reason = os_reason_create(OS_REASON_JETSAM, jetsam_reason_code); + if (jetsam_reason == OS_REASON_NULL) { + printf("memorystatus_thread: failed to allocate jetsam reason\n"); + } - if (memorystatus_act_aggressive(cause, jetsam_reason, &jld_idle_kills, &corpse_list_purged, &post_snapshot)) { - goto done; - } + if (memorystatus_act_aggressive(cause, jetsam_reason, &jld_idle_kills, &corpse_list_purged, &post_snapshot)) { + goto done; + } - /* - * memorystatus_kill_top_process() drops a reference, - * so take another one so we can continue to use this exit reason - * even after it returns - */ - os_reason_ref(jetsam_reason); + /* + * memorystatus_kill_top_process() drops a reference, + * so take another one so we can continue to use this exit reason + * even after it returns + */ + os_reason_ref(jetsam_reason); - /* LRU */ - killed = memorystatus_kill_top_process(TRUE, sort_flag, cause, jetsam_reason, &priority, &errors); - sort_flag = FALSE; + /* LRU */ + killed = memorystatus_kill_top_process(TRUE, sort_flag, cause, jetsam_reason, &priority, &errors); + sort_flag = FALSE; - if (killed) { - if (memorystatus_post_snapshot(priority, cause) == TRUE) { + if (killed) { + if (memorystatus_post_snapshot(priority, cause) == TRUE) { - post_snapshot = TRUE; - } + post_snapshot = TRUE; + } - /* Jetsam Loop Detection */ - if (memorystatus_jld_enabled == TRUE) { - if ((priority == JETSAM_PRIORITY_IDLE) || (priority == system_procs_aging_band) || (priority == applications_aging_band)) { - jld_idle_kills++; - } else { - /* - * We've reached into bands beyond idle deferred. - * We make no attempt to monitor them - */ - } - } + /* Jetsam Loop Detection */ + if (memorystatus_jld_enabled == TRUE) { + if ((priority == JETSAM_PRIORITY_IDLE) || (priority == system_procs_aging_band) || (priority == applications_aging_band)) { + jld_idle_kills++; + } else { + /* + * We've reached into bands beyond idle deferred. + * We make no attempt to monitor them + */ + } + } - if ((priority >= JETSAM_PRIORITY_UI_SUPPORT) && (total_corpses_count() > 0) && (corpse_list_purged == FALSE)) { - /* - * If we have jetsammed a process in or above JETSAM_PRIORITY_UI_SUPPORT - * then we attempt to relieve pressure by purging corpse memory. - */ - task_purge_all_corpses(); - corpse_list_purged = TRUE; - } - goto done; - } + if ((priority >= JETSAM_PRIORITY_UI_SUPPORT) && (total_corpses_count() > 0) && (corpse_list_purged == FALSE)) { + /* + * If we have jetsammed a process in or above JETSAM_PRIORITY_UI_SUPPORT + * then we attempt to relieve pressure by purging corpse memory. + */ + task_purge_all_corpses(); + corpse_list_purged = TRUE; + } + goto done; + } - if (memorystatus_avail_pages_below_critical()) { - /* - * Still under pressure and unable to kill a process - purge corpse memory - */ - if (total_corpses_count() > 0) { - task_purge_all_corpses(); - corpse_list_purged = TRUE; - } + if (memorystatus_avail_pages_below_critical()) { + /* + * Still under pressure and unable to kill a process - purge corpse memory + */ + if (total_corpses_count() > 0) { + task_purge_all_corpses(); + corpse_list_purged = TRUE; + } - if (memorystatus_avail_pages_below_critical()) { - /* - * Still under pressure and unable to kill a process - panic - */ - panic("memorystatus_jetsam_thread: no victim! available pages:%llu\n", (uint64_t)memorystatus_available_pages); - } - } + if (memorystatus_avail_pages_below_critical()) { + /* + * Still under pressure and unable to kill a process - panic + */ + panic("memorystatus_jetsam_thread: no victim! available pages:%llu\n", (uint64_t)memorystatus_available_pages); + } + } done: @@ -1263,12 +1262,12 @@ static boolean_t memorystatus_action_needed(void) { #if CONFIG_EMBEDDED - return (is_reason_thrashing(kill_under_pressure_cause) || - is_reason_zone_map_exhaustion(kill_under_pressure_cause) || - memorystatus_available_pages <= memorystatus_available_pages_pressure); + return (is_reason_thrashing(kill_under_pressure_cause) || + is_reason_zone_map_exhaustion(kill_under_pressure_cause) || + memorystatus_available_pages <= memorystatus_available_pages_pressure); #else /* CONFIG_EMBEDDED */ - return (is_reason_thrashing(kill_under_pressure_cause) || - is_reason_zone_map_exhaustion(kill_under_pressure_cause)); + return (is_reason_thrashing(kill_under_pressure_cause) || + is_reason_zone_map_exhaustion(kill_under_pressure_cause)); #endif /* CONFIG_EMBEDDED */ } ``` @@ -1283,19 +1282,19 @@ memorystatus_action_needed(void) static boolean_t memorystatus_act_aggressive(uint32_t cause, os_reason_t jetsam_reason, int *jld_idle_kills, boolean_t *corpse_list_purged, boolean_t *post_snapshot) { - // ... + // ... if ( (jld_bucket_count == 0) || - (jld_now_msecs > (jld_timestamp_msecs + memorystatus_jld_eval_period_msecs))) { + (jld_now_msecs > (jld_timestamp_msecs + memorystatus_jld_eval_period_msecs))) { - /* - * Refresh evaluation parameters - */ - jld_timestamp_msecs = jld_now_msecs; - jld_idle_kill_candidates = jld_bucket_count; - *jld_idle_kills = 0; - jld_eval_aggressive_count = 0; - jld_priority_band_max = JETSAM_PRIORITY_UI_SUPPORT; - } + /* + * Refresh evaluation parameters + */ + jld_timestamp_msecs = jld_now_msecs; + jld_idle_kill_candidates = jld_bucket_count; + *jld_idle_kills = 0; + jld_eval_aggressive_count = 0; + jld_priority_band_max = JETSAM_PRIORITY_UI_SUPPORT; + } //... } ``` @@ -1305,11 +1304,11 @@ memorystatus_act_aggressive(uint32_t cause, os_reason_t jetsam_reason, int *jld_ ```C /* Jetsam Loop Detection */ if (max_mem <= (512 * 1024 * 1024)) { - /* 512 MB devices */ -memorystatus_jld_eval_period_msecs = 8000; /* 8000 msecs == 8 second window */ + /* 512 MB devices */ +memorystatus_jld_eval_period_msecs = 8000; /* 8000 msecs == 8 second window */ } else { - /* 1GB and larger devices */ -memorystatus_jld_eval_period_msecs = 6000; /* 6000 msecs == 6 second window */ + /* 1GB and larger devices */ +memorystatus_jld_eval_period_msecs = 6000; /* 6000 msecs == 6 second window */ } ``` @@ -1319,40 +1318,40 @@ 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% | -| iPad4(iOS 8.1) | 585 | 1024 | 57% | -| Pad Mini 1st Generation | 297 | 512 | 58% | -| iPad Mini retina(iOS 7.1) | 696 | 1024 | 68% | -| iPad Air | 697 | 1024 | 68% | -| iPad Air 2(iOS 10.2.1) | 1383 | 2048 | 68% | -| iPad Pro 9.7"(iOS 10.0.2 (14A456)) | 1395 | 1971 | 71% | -| iPad Pro 10.5”(iOS 11 beta4) | 3057 | 4000 | 76% | -| iPad Pro 12.9” (2015)(iOS 11.2.1) | 3058 | 3999 | 76% | -| iPad 10.2(iOS 13.2.3) | 1844 | 2998 | 62% | -| iPod touch 4th gen(iOS 6.1.1) | 130 | 256 | 51% | -| iPod touch 5th gen | 286 | 512 | 56% | -| iPhone4 | 325 | 512 | 63% | -| iPhone4s | 286 | 512 | 56% | -| iPhone5 | 645 | 1024 | 62% | -| iPhone5s | 646 | 1024 | 63% | -| iPhone6(iOS 8.x) | 645 | 1024 | 62% | -| iPhone6 Plus(iOS 8.x) | 645 | 1024 | 62% | -| iPhone6s(iOS 9.2) | 1396 | 2048 | 68% | -| iPhone6s Plus(iOS 10.2.1) | 1396 | 2048 | 68% | -| iPhoneSE(iOS 9.3) | 1395 | 2048 | 68% | -| iPhone7(iOS 10.2) | 1395 | 2048 | 68% | -| iPhone7 Plus(iOS 10.2.1) | 2040 | 3072 | 66% | -| iPhone8(iOS 12.1) | 1364 | 1990 | 70% | -| iPhoneX(iOS 11.2.1) | 1392 | 2785 | 50% | -| iPhoneXS(iOS 12.1) | 2040 | 3754 | 54% | -| iPhoneXS Max(iOS 12.1) | 2039 | 3735 | 55% | -| iPhoneXR(iOS 12.1) | 1792 | 2813 | 63% | -| iPhone11(iOS 13.1.3) | 2068 | 3844 | 54% | -| iPhone11 Pro Max(iOS 13.2.3) | 2067 | 3740 | 55% | +| device | crash amount:MB | total amount:MB | percentage of total | +|:----------------------------------:|:---------------:|:---------------:|:-------------------:| +| iPad1 | 127 | 256 | 49% | +| iPad2 | 275 | 512 | 53% | +| iPad3 | 645 | 1024 | 62% | +| iPad4(iOS 8.1) | 585 | 1024 | 57% | +| Pad Mini 1st Generation | 297 | 512 | 58% | +| iPad Mini retina(iOS 7.1) | 696 | 1024 | 68% | +| iPad Air | 697 | 1024 | 68% | +| iPad Air 2(iOS 10.2.1) | 1383 | 2048 | 68% | +| iPad Pro 9.7"(iOS 10.0.2 (14A456)) | 1395 | 1971 | 71% | +| iPad Pro 10.5”(iOS 11 beta4) | 3057 | 4000 | 76% | +| iPad Pro 12.9” (2015)(iOS 11.2.1) | 3058 | 3999 | 76% | +| iPad 10.2(iOS 13.2.3) | 1844 | 2998 | 62% | +| iPod touch 4th gen(iOS 6.1.1) | 130 | 256 | 51% | +| iPod touch 5th gen | 286 | 512 | 56% | +| iPhone4 | 325 | 512 | 63% | +| iPhone4s | 286 | 512 | 56% | +| iPhone5 | 645 | 1024 | 62% | +| iPhone5s | 646 | 1024 | 63% | +| iPhone6(iOS 8.x) | 645 | 1024 | 62% | +| iPhone6 Plus(iOS 8.x) | 645 | 1024 | 62% | +| iPhone6s(iOS 9.2) | 1396 | 2048 | 68% | +| iPhone6s Plus(iOS 10.2.1) | 1396 | 2048 | 68% | +| iPhoneSE(iOS 9.3) | 1395 | 2048 | 68% | +| iPhone7(iOS 10.2) | 1395 | 2048 | 68% | +| iPhone7 Plus(iOS 10.2.1) | 2040 | 3072 | 66% | +| iPhone8(iOS 12.1) | 1364 | 1990 | 70% | +| iPhoneX(iOS 11.2.1) | 1392 | 2785 | 50% | +| iPhoneXS(iOS 12.1) | 2040 | 3754 | 54% | +| iPhoneXS Max(iOS 12.1) | 2039 | 3735 | 55% | +| iPhoneXR(iOS 12.1) | 1792 | 2813 | 63% | +| iPhone11(iOS 13.1.3) | 2068 | 3844 | 54% | +| iPhone11 Pro Max(iOS 13.2.3) | 2067 | 3740 | 55% | #### 3.3 触发当前 App 的 high water mark @@ -1390,22 +1389,22 @@ timer = [NSTimer scheduledTimerWithTimeInterval:0.01 target:self selector:@selec iOS13 开始 中 `size_t os_proc_available_memory(void); ` 可以查看当前可用内存。 > Return Value -> +> > The number of bytes that the app may allocate before it hits its memory limit. If the calling process isn't an app, or if the process has already exceeded its memory limit, this function returns `0`. -> +> > Discussion -> +> > Call this function to determine the amount of memory available to your app. The returned value corresponds to the current memory limit minus the memory footprint of your app at the time of the function call. Your app's memory footprint consists of the data that you allocated in RAM, and that must stay in RAM (or the equivalent) at all times. Memory limits can change during the app life cycle and don't necessarily correspond to the amount of physical memory available on the device. -> +> > Use the returned value as advisory information only and don't cache it. The precise value changes when your app does any work that affects memory, which can happen frequently. -> +> > Although this function lets you determine the amount of memory your app may safely consume, don't use it to maximize your app's memory usage. Significant memory use, even when under the current memory limit, affects system performance. For example, when your app consumes all of its available memory, the system may need to terminate other apps and system processes to accommodate your app's requests. Instead, always consume the smallest amount of memory you need to be responsive to the user's needs. -> +> > If you need more detailed information about the available memory resources, you can call [`task_info`](apple-reference-documentation://hcPGvbcfam). However, be aware that `task_info` is an expensive call, whereas this function is much more efficient. ```objective-c if (@available(iOS 13.0, *)) { - return os_proc_available_memory() / 1024.0 / 1024.0; + return os_proc_available_memory() / 1024.0 / 1024.0; } ``` @@ -1485,17 +1484,17 @@ typedef struct memorystatus_priority_entry { #define MEMORYSTATUS_CMD_SET_PRIORITY_PROPERTIES 2 #define MEMORYSTATUS_CMD_GET_JETSAM_SNAPSHOT 3 #define MEMORYSTATUS_CMD_GET_PRESSURE_STATUS 4 -#define MEMORYSTATUS_CMD_SET_JETSAM_HIGH_WATER_MARK 5 /* Set active memory limit = inactive memory limit, both non-fatal */ -#define MEMORYSTATUS_CMD_SET_JETSAM_TASK_LIMIT 6 /* Set active memory limit = inactive memory limit, both fatal */ -#define MEMORYSTATUS_CMD_SET_MEMLIMIT_PROPERTIES 7 /* Set memory limits plus attributes independently */ -#define MEMORYSTATUS_CMD_GET_MEMLIMIT_PROPERTIES 8 /* Get memory limits plus attributes */ +#define MEMORYSTATUS_CMD_SET_JETSAM_HIGH_WATER_MARK 5 /* Set active memory limit = inactive memory limit, both non-fatal */ +#define MEMORYSTATUS_CMD_SET_JETSAM_TASK_LIMIT 6 /* Set active memory limit = inactive memory limit, both fatal */ +#define MEMORYSTATUS_CMD_SET_MEMLIMIT_PROPERTIES 7 /* Set memory limits plus attributes independently */ +#define MEMORYSTATUS_CMD_GET_MEMLIMIT_PROPERTIES 8 /* Get memory limits plus attributes */ #define MEMORYSTATUS_CMD_PRIVILEGED_LISTENER_ENABLE 9 /* Set the task's status as a privileged listener w.r.t memory notifications */ #define MEMORYSTATUS_CMD_PRIVILEGED_LISTENER_DISABLE 10 /* Reset the task's status as a privileged listener w.r.t memory notifications */ #define MEMORYSTATUS_CMD_AGGRESSIVE_JETSAM_LENIENT_MODE_ENABLE 11 /* Enable the 'lenient' mode for aggressive jetsam. See comments in kern_memorystatus.c near the top. */ #define MEMORYSTATUS_CMD_AGGRESSIVE_JETSAM_LENIENT_MODE_DISABLE 12 /* Disable the 'lenient' mode for aggressive jetsam. */ #define MEMORYSTATUS_CMD_GET_MEMLIMIT_EXCESS 13 /* Compute how much a process's phys_footprint exceeds inactive memory limit */ -#define MEMORYSTATUS_CMD_ELEVATED_INACTIVEJETSAMPRIORITY_ENABLE 14 /* Set the inactive jetsam band for a process to JETSAM_PRIORITY_ELEVATED_INACTIVE */ -#define MEMORYSTATUS_CMD_ELEVATED_INACTIVEJETSAMPRIORITY_DISABLE 15 /* Reset the inactive jetsam band for a process to the default band (0)*/ +#define MEMORYSTATUS_CMD_ELEVATED_INACTIVEJETSAMPRIORITY_ENABLE 14 /* Set the inactive jetsam band for a process to JETSAM_PRIORITY_ELEVATED_INACTIVE */ +#define MEMORYSTATUS_CMD_ELEVATED_INACTIVEJETSAMPRIORITY_DISABLE 15 /* Reset the inactive jetsam band for a process to the default band (0)*/ #define MEMORYSTATUS_CMD_SET_PROCESS_IS_MANAGED 16 /* (Re-)Set state on a process that marks it as (un-)managed by a system entity e.g. assertiond */ #define MEMORYSTATUS_CMD_GET_PROCESS_IS_MANAGED 17 /* Return the 'managed' status of a process */ #define MEMORYSTATUS_CMD_SET_PROCESS_IS_FREEZABLE 18 /* Is the process eligible for freezing? Apps and extensions can pass in FALSE to opt out of freezing, i.e., @@ -1509,7 +1508,7 @@ size_t count = sizeof(struct memorystatus_priority_entry) * NUM_ENTRIES; int kernResult = memorystatus_control(MEMORYSTATUS_CMD_GET_PRIORITY_LIST, 0, 0, memStatus, count); if (rc < 0) { NSLog(@"memorystatus_control"); - return ; + return ; } int entry = 0; @@ -1593,23 +1592,23 @@ for (NSInteger index = 0; index < 10000000; index++) { void * malloc(size_t size) { - void *retval; - retval = malloc_zone_malloc(default_zone, size); - if (retval == NULL) { - errno = ENOMEM; - } - return retval; + void *retval; + retval = malloc_zone_malloc(default_zone, size); + if (retval == NULL) { + errno = ENOMEM; + } + return retval; } void * calloc(size_t num_items, size_t size) { - void *retval; - retval = malloc_zone_calloc(default_zone, num_items, size); - if (retval == NULL) { - errno = ENOMEM; - } - return retval; + void *retval; + retval = malloc_zone_calloc(default_zone, num_items, size); + if (retval == NULL) { + errno = ENOMEM; + } + return retval; } ``` @@ -1617,31 +1616,31 @@ calloc(size_t num_items, size_t size) ```c++ typedef struct { - malloc_zone_t malloc_zone; - uint8_t pad[PAGE_MAX_SIZE - sizeof(malloc_zone_t)]; + malloc_zone_t malloc_zone; + uint8_t pad[PAGE_MAX_SIZE - sizeof(malloc_zone_t)]; } virtual_default_zone_t; static virtual_default_zone_t virtual_default_zone __attribute__((section("__DATA,__v_zone"))) __attribute__((aligned(PAGE_MAX_SIZE))) = { - NULL, - NULL, - default_zone_size, - default_zone_malloc, - default_zone_calloc, - default_zone_valloc, - default_zone_free, - default_zone_realloc, - default_zone_destroy, - DEFAULT_MALLOC_ZONE_STRING, - default_zone_batch_malloc, - default_zone_batch_free, - &default_zone_introspect, - 10, - default_zone_memalign, - default_zone_free_definite_size, - default_zone_pressure_relief, - default_zone_malloc_claimed_address, + NULL, + NULL, + default_zone_size, + default_zone_malloc, + default_zone_calloc, + default_zone_valloc, + default_zone_free, + default_zone_realloc, + default_zone_destroy, + DEFAULT_MALLOC_ZONE_STRING, + default_zone_batch_malloc, + default_zone_batch_free, + &default_zone_introspect, + 10, + default_zone_memalign, + default_zone_free_definite_size, + default_zone_pressure_relief, + default_zone_malloc_claimed_address, }; static malloc_zone_t *default_zone = &virtual_default_zone.malloc_zone; @@ -1649,16 +1648,16 @@ static malloc_zone_t *default_zone = &virtual_default_zone.malloc_zone; static void * default_zone_malloc(malloc_zone_t *zone, size_t size) { - zone = runtime_default_zone(); + zone = runtime_default_zone(); - return zone->malloc(zone, size); + return zone->malloc(zone, size); } MALLOC_ALWAYS_INLINE static inline malloc_zone_t * runtime_default_zone() { - return (lite_zone) ? lite_zone : inline_malloc_default_zone(); + return (lite_zone) ? lite_zone : inline_malloc_default_zone(); } ``` @@ -1668,9 +1667,9 @@ runtime_default_zone() { static inline malloc_zone_t * inline_malloc_default_zone(void) { - _malloc_initialize_once(); - // malloc_report(ASL_LEVEL_INFO, "In inline_malloc_default_zone with %d %d\n", malloc_num_zones, malloc_has_debug_zone); - return malloc_zones[0]; + _malloc_initialize_once(); + // malloc_report(ASL_LEVEL_INFO, "In inline_malloc_default_zone with %d %d\n", malloc_num_zones, malloc_has_debug_zone); + return malloc_zones[0]; } ``` @@ -1680,7 +1679,7 @@ inline_malloc_default_zone(void) ```c++ malloc_zone_t * create_scalable_zone(size_t initial_size, unsigned debug_flags) { - return (malloc_zone_t *) create_scalable_szone(initial_size, debug_flags); + return (malloc_zone_t *) create_scalable_szone(initial_size, debug_flags); } ``` @@ -1762,21 +1761,21 @@ ASLR: `slide` 函数虚拟地址加载到进程内存的随机偏移量,每个 ### 6. 开发阶段针对内存我们能做些什么 1. 图片缩放 - + WWDC 2018 Session 416 - iOS Memory Deep Dive,处理图片缩放的时候直接使用 UIImage 会在解码时读取文件而占用一部分内存,还会生成中间位图 bitmap 消耗大量内存。而 **ImageIO** 不存在上述 2 种弊端,只会占用最终图片大小的内存 - + 做了 2 组对比实验:给 App 显示一张图片 - + ```objective-c // 方法1: 19.6M UIImage *imageResult = [self scaleImage:[UIImage imageNamed:@"test"] newSize:CGSizeMake(self.view.frame.size.width, self.view.frame.size.height)]; self.imageView.image = imageResult; - + // 方法2: 14M NSData *data = UIImagePNGRepresentation([UIImage imageNamed:@"test"]); - UIImage *imageResult = [self scaledImageWithData:data withSize:CGSizeMake(self.view.frame.size.width, self.view.frame.size.height) scale:3 orientation:UIImageOrientationUp]; + UIImage *imageResult = [self scaledImageWithData:data withSize:CGSizeMake(self.view.frame.size.width, self.view.frame.size.height) scale:3 orientation:UIImageOrientationUp]; self.imageView.image = imageResult; - + - (UIImage *)scaleImage:(UIImage *)image newSize:(CGSize)newSize { UIGraphicsBeginImageContextWithOptions(newSize, NO, 0); @@ -1785,7 +1784,7 @@ ASLR: `slide` 函数虚拟地址加载到进程内存的随机偏移量,每个 UIGraphicsEndImageContext(); return newImage; } - + - (UIImage *)scaledImageWithData:(NSData *)data withSize:(CGSize)size scale:(CGFloat)scale orientation:(UIImageOrientation)orientation { CGFloat maxPixelSize = MAX(size.width, size.height); @@ -1799,7 +1798,7 @@ ASLR: `slide` 函数虚拟地址加载到进程内存的随机偏移量,每个 return resultImage; } ``` - + 可以看出使用 ImageIO 比使用 UIImage 直接缩放占用内存更低。 2. 合理使用 autoreleasepool @@ -1835,7 +1834,7 @@ for (NSInteger index = 0; index < 10000000; index++) { 4. 不管是打开网页,还是执行 js,都应该使用 WKWebView。UIWebView 会占用大量内存,从而导致 App 发生 OOM 的几率增加,而 WKWebView 是一个多进程组件,Network Loading 以及 UI Rendering 在其它进程中执行,比 UIWebView 占用更低的内存开销。 5. 在做 SDK 或者 App,如果场景是缓存相关,尽量使用 NSCache 而不是 NSMutableDictionary。它是系统提供的专门处理缓存的类,NSCache 分配的内存是 `Purgeable Memory`,可以由系统自动释放。NSCache 与 NSPureableData 的结合使用可以让系统根据情况回收内存,也可以在内存清理时移除对象。 - + 其他的开发习惯就不一一描述了,良好的开发习惯和代码意识是需要平时注意修炼的。 ### 7. 现状及其改进 @@ -1865,36 +1864,36 @@ for (NSInteger index = 0; index < 10000000; index++) { App 发送一次网络请求一般会经历下面几个关键步骤: - DNS 解析 - + Domain Name system,网络域名名称系统,本质上就是将`域名`和`IP 地址` 相互映射的一个分布式数据库,使人们更方便的访问互联网。首先会查询本地的 DNS 缓存,查找失败就去 DNS 服务器查询,这其中可能会经过非常多的节点,涉及到**递归查询和迭代查询**的过程。运营商可能不干人事:一种情况就是出现运营商劫持的现象,表现为你在 App 内访问某个网页的时候会看到和内容不相关的广告;另一种可能的情况就是把你的请求丢给非常远的基站去做 DNS 解析,导致我们 App 的 DNS 解析时间较长,App 网络效率低。一般做 HTTPDNS 方案去自行解决 DNS 的问题。 - TCP 3 次握手 - + 关于 TCP 握手过程中为什么是 3 次握手而不是 2 次、4 次,可以查看这篇[文章](https://draveness.me/whys-the-design-tcp-three-way-handshake/)。 - TLS 握手 - + 对于 HTTPS 请求还需要做 TLS 握手,也就是密钥协商的过程。 - 发送请求 - + 连接建立好之后就可以发送 request,此时可以记录下 request start 时间 - 等待回应 - + 等待服务器返回响应。这个时间主要取决于资源大小,也是网络请求过程中最为耗时的一个阶段。 - 返回响应 - + 服务端返回响应给客户端,根据 HTTP header 信息中的状态码判断本次请求是否成功、是否走缓存、是否需要重定向。 ### 2. 监控原理 -| 名称 | 说明 | -| :-------------: | :---------------------: | -| NSURLConnection | 已经被废弃。用法简单 | -| NSURLSession | iOS7.0 推出,功能更强大 | -| CFNetwork | NSURL 的底层,纯 C 实现 | +| 名称 | 说明 | +|:---------------:|:----------------:| +| NSURLConnection | 已经被废弃。用法简单 | +| NSURLSession | iOS7.0 推出,功能更强大 | +| CFNetwork | NSURL 的底层,纯 C 实现 | iOS 网络框架层级关系如下: @@ -2070,7 +2069,7 @@ API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0)) /* * This property is set to YES if a proxy connection was used to fetch the resource. - 该连接是否使用了代理 + 该连接是否使用了代理 */ @property (assign, readonly, getter=isProxyConnection) BOOL proxyConnection; @@ -2292,11 +2291,11 @@ API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0)) //使用NSURLSessionDataTask请求网络 - (void)startLoading { NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; - NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration + NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil]; NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil]; - self.sessionDelegateQueue = [[NSOperationQueue alloc] init]; + self.sessionDelegateQueue = [[NSOperationQueue alloc] init]; self.sessionDelegateQueue.maxConcurrentOperationCount = 1; self.sessionDelegateQueue.name = @"com.networkMonitor.session.queue"; self.dataTask = [session dataTaskWithRequest:self.request]; @@ -2375,7 +2374,7 @@ API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0)) self.dataModel.useProxy = obj.isProxyConnection; } }]; - // 上传 Network 数据到数据上报组件,数据上报会在 [打造功能强大、灵活可配置的数据上报组件](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.80.md) 讲 + // 上传 Network 数据到数据上报组件,数据上报会在 [打造功能强大、灵活可配置的数据上报组件](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.80.md) 讲 } } ``` @@ -2598,21 +2597,21 @@ CFNetwork 是 c 语言实现的,要对 c 代码进行 hook 需要使用 Dynami > **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 来传递数据,使用回调函数的形式来接受服务器的响应。当回调函数受到 @@ -2620,51 +2619,51 @@ CFNetwork 使用 CFReadStreamRef 来传递数据,使用回调函数的形式 具体步骤及其关键代码如下,以 NSURLConnection 举例 - 因为要 Hook 挺多地方,所以写一个 method swizzling 的工具类 - + ```objective-c #import - + NS_ASSUME_NONNULL_BEGIN - + @interface NSObject (hook) - + /** hook对象方法 - + @param originalSelector 需要hook的原始对象方法 @param swizzledSelector 需要替换的对象方法 */ + (void)apm_swizzleMethod:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector; - + /** hook类方法 - + @param originalSelector 需要hook的原始类方法 @param swizzledSelector 需要替换的类方法 */ + (void)apm_swizzleClassMethod:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector; - + @end - + NS_ASSUME_NONNULL_END - + + (void)apm_swizzleMethod:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector { class_swizzleInstanceMethod(self, originalSelector, swizzledSelector); } - + + (void)apm_swizzleClassMethod:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector { //类方法实际上是储存在类对象的类(即元类)中,即类方法相当于元类的实例方法,所以只需要把元类传入,其他逻辑和交互实例方法一样。 Class class2 = object_getClass(self); class_swizzleInstanceMethod(class2, originalSelector, swizzledSelector); } - + void class_swizzleInstanceMethod(Class class, SEL originalSEL, SEL replacementSEL) { Method originMethod = class_getInstanceMethod(class, originalSEL); Method replaceMethod = class_getInstanceMethod(class, replacementSEL); - + if(class_addMethod(class, originalSEL, method_getImplementation(replaceMethod),method_getTypeEncoding(replaceMethod))) { class_replaceMethod(class,replacementSEL, method_getImplementation(originMethod), method_getTypeEncoding(originMethod)); @@ -2675,7 +2674,7 @@ CFNetwork 使用 CFReadStreamRef 来传递数据,使用回调函数的形式 ``` - 建立一个继承自 NSProxy 抽象类的类,实现相应方法。 - + ```objective-c #import @@ -2699,93 +2698,97 @@ CFNetwork 使用 CFReadStreamRef 来传递数据,使用回调函数的形式 @end ``` - @implementation NetworkDelegateProxy #pragma mark - life cycle - + (instancetype)sharedInstance { - static NetworkDelegateProxy *_sharedInstance = nil; - - static dispatch_once_t onceToken; - - dispatch_once(&onceToken, ^{ - _sharedInstance = [NetworkDelegateProxy alloc]; - }); - - return _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 - { - NetworkDelegateProxy *instance = [NetworkDelegateProxy sharedInstance]; - instance->_originalTarget = originalTarget; - instance->_NewDelegate = newDelegate; - return instance; - } - - - (void)forwardInvocation:(NSInvocation *)invocation - { - if ([_originalTarget respondsToSelector:invocation.selector]) { - [invocation invokeWithTarget:_originalTarget]; - [((NSURLSessionAndConnectionImplementor *)_NewDelegate) invoke:invocation]; - } - } - - - (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel - { - return [_originalTarget methodSignatureForSelector:sel]; ++ (instancetype)setProxyForObject:(id)originalTarget withNewDelegate:(id)newDelegate + { + NetworkDelegateProxy *instance = [NetworkDelegateProxy sharedInstance]; + instance->_originalTarget = originalTarget; + instance->_NewDelegate = newDelegate; + return instance; + } +- (void)forwardInvocation:(NSInvocation *)invocation + { + if ([_originalTarget respondsToSelector:invocation.selector]) { + + [invocation invokeWithTarget:_originalTarget]; + [((NSURLSessionAndConnectionImplementor *)_NewDelegate) invoke:invocation]; + } + } +- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel + { + return [_originalTarget methodSignatureForSelector:sel]; + } + @end + + ``` + ``` - 创建一个对象,实现 NSURLConnection、NSURLSession、NSIuputStream 代理方法 - + ```objective-c // NetworkImplementor.m - + #pragma mark-NSURLConnectionDelegate - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { NSLog(@"%s", __func__); } - + - (nullable NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(nullable NSURLResponse *)response { NSLog(@"%s", __func__); return request; } - + #pragma mark-NSURLConnectionDataDelegate - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { NSLog(@"%s", __func__); } - + - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { NSLog(@"%s", __func__); } - + - (void)connection:(NSURLConnection *)connection didSendBodyData:(NSInteger)bytesWritten totalBytesWritten:(NSInteger)totalBytesWritten totalBytesExpectedToWrite:(NSInteger)totalBytesExpectedToWrite { NSLog(@"%s", __func__); } - + - (void)connectionDidFinishLoading:(NSURLConnection *)connection { NSLog(@"%s", __func__); } - + #pragma mark-NSURLConnectionDownloadDelegate - (void)connection:(NSURLConnection *)connection didWriteData:(long long)bytesWritten totalBytesWritten:(long long)totalBytesWritten expectedTotalBytes:(long long) expectedTotalBytes { NSLog(@"%s", __func__); } - + - (void)connectionDidResumeDownloading:(NSURLConnection *)connection totalBytesWritten:(long long)totalBytesWritten expectedTotalBytes:(long long) expectedTotalBytes { NSLog(@"%s", __func__); } - + - (void)connectionDidFinishDownloading:(NSURLConnection *)connection destinationURL:(NSURL *) destinationURL { NSLog(@"%s", __func__); } @@ -2793,7 +2796,7 @@ CFNetwork 使用 CFReadStreamRef 来传递数据,使用回调函数的形式 ``` - 给 NSURLConnection 添加 Category,专门设置 hook 代理对象、hook NSURLConnection 对象方法 - + ```objective-c // NSURLConnection+Monitor.m @implementation NSURLConnection (Monitor) @@ -2866,7 +2869,7 @@ CFNetwork 使用 CFReadStreamRef 来传递数据,使用回调函数的形式 顺道说一句,上面针对 NSURLConnection、NSURLSession、NSInputStream 代理对象的 hook 之后,利用 NSProxy 实现代理对象方法的转发,有另一种方法可以实现,那就是 **isa swizzling**。 - Method swizzling 原理 - + ```objective-c struct old_method { SEL method_name; @@ -2874,11 +2877,11 @@ CFNetwork 使用 CFReadStreamRef 来传递数据,使用回调函数的形式 IMP method_imp; }; ``` - + ![method swizzling](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-04-09-methodSwizzling.png) - + method swizzling 改进版如下 - + ```objective-c Method originalMethod = class_getInstanceMethod(aClass, aSEL); IMP originalIMP = method_getImplementation(originalMethod); @@ -2891,7 +2894,7 @@ CFNetwork 使用 CFReadStreamRef 来传递数据,使用回调函数的形式 ``` - isa swizzling - + ```objective-c /// Represents an instance of a class. struct objc_object { @@ -2900,9 +2903,8 @@ CFNetwork 使用 CFReadStreamRef 来传递数据,使用回调函数的形式 /// A pointer to an instance of a class. typedef struct objc_object *id; - ``` - + ![isa swizzling](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-04-13-isaSwizzling.png) 我们来分析一下为什么修改 `isa` 可以实现目的呢? @@ -3103,21 +3105,21 @@ HTTP 请求报文结构 ##### 2.5.2 问题 1. Request 和 Response 不一定成对存在 - + 比如网络断开、App 突然 Crash 等,所以 Request 和 Response 监控后不应该记录在一条记录里 2. 请求流量计算方式不精确 - + 主要原因有: - + - 监控技术方案忽略了请求头和请求行部分的数据大小 - 监控技术方案忽略了 Cookie 部分的数据大小 - 监控技术方案在对请求体大小计算的时候直接使用 `HTTPBody.length`,导致不够精确 3. 响应流量计算方式不精确 - + 主要原因有: - + - 监控技术方案忽略了响应头和响应行部分的数据大小 - 监控技术方案在对 body 部分的字节大小计算,因采用 `exceptedContentLength` 导致不够准确 - 监控技术方案忽略了响应体使用 gzip 压缩。真正的网络通信过程中,客户端在发起请求的请求头中 `Accept-Encoding` 字段代表客户端支持的数据压缩方式(表明客户端可以正常使用数据时支持的压缩方法),同样服务端根据客户端想要的压缩方式、服务端当前支持的压缩方式,最后处理数据,在响应头中`Content-Encoding` 字段表示当前服务器采用了什么压缩方式。 @@ -3131,14 +3133,14 @@ HTTP 请求报文结构 1. 先利用网络监控方案将 NSURLProtocol 管理 App 的各种网络请求 2. 在各个方法内部记录各项所需参数(NSURLProtocol 不能分析请求握手、挥手等数据大小和时间消耗,不过对于正常情况的接口流量分析足够了,最底层需要 Socket 层) - + ```objective-c @property(nonatomic, strong) NSURLConnection *internalConnection; @property(nonatomic, strong) NSURLResponse *internalResponse; @property(nonatomic, strong) NSMutableData *responseData; @property (nonatomic, strong) NSURLRequest *internalRequest; ``` - + ```objective-c - (void)startLoading { @@ -3146,13 +3148,13 @@ HTTP 请求报文结构 self.internalConnection = [[NSURLConnection alloc] initWithRequest:mutableRequest delegate:self]; self.internalRequest = self.request; } - + - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; self.internalResponse = response; } - + - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { [self.responseData appendData:data]; @@ -3224,7 +3226,7 @@ typedef CFHTTPMessageRef (*APMURLResponseFetchHTTPResponse)(CFURLRef response); ``` 4. 将获取到的 Status Line 转换为 NSData,再计算大小 - + ```objective-c - (NSUInteger)apm_getLineLength { NSString *statusLineString = @""; @@ -3238,11 +3240,11 @@ typedef CFHTTPMessageRef (*APMURLResponseFetchHTTPResponse)(CFURLRef response); ``` 5. Header 部分 - + `allHeaderFields` 获取到 NSDictionary,然后按照 `key: value` 拼接成字符串,然后转换成 NSData 计算大小 - + 注意:`key: value` key 后是有空格的,curl 或者 chrome Network 面板可以查看印证下。 - + ```objective-c - (NSUInteger)apm_getHeadersLength { @@ -3267,47 +3269,47 @@ typedef CFHTTPMessageRef (*APMURLResponseFetchHTTPResponse)(CFURLRef response); ``` 6. Body 部分 - + Body 大小的计算不能直接使用 excepectedContentLength,官方文档说明了其不准确性,只可以作为参考。或者 `allHeaderFields` 中的 `Content-Length` 值也是不够准确的。 - + > /\*! - > + > > **@abstract** Returns the expected content length of the receiver. - > + > > **@discussion** Some protocol implementations report a content length - > + > > as part of delivering load metadata, but not all protocols - > + > > guarantee the amount of data that will be delivered in actuality. - > + > > Hence, this method returns an expected amount. Clients should use - > + > > this value as an advisory, and should be prepared to deal with - > + > > either more or less data. - > + > > **@result** The expected content length of the receiver, or -1 if - > + > > there is no expectation that can be arrived at regarding expected - > + > > content length. - > + > > \*/ - > + > > **@property** (**readonly**) **long** **long** expectedContentLength; - + - HTTP 1.1 版本规定,如果存在 `Transfer-Encoding: chunked`,则在 header 中不能有 `Content-Length`,有也会被忽视。 - 在 HTTP 1.0 及之前版本中,`content-length` 字段可有可无 - 在 HTTP 1.1 及之后版本。如果是 `keep alive`,则 `Content-Length` 和 `chunked` 必然是二选一。若是非`keep alive`,则和 HTTP 1.0 一样。`Content-Length` 可有可无。 - + 什么是 `Transfer-Encoding: chunked` - + 数据以一系列分块的形式进行发送 `Content-Length` 首部在这种情况下不被发送. 在每一个分块的开头需要添加当前分块的长度, 以十六进制的形式表示,后面紧跟着 `\r\n` , 之后是分块本身, 后面也是 `\r\n` ,终止块是一个常规的分块, 不同之处在于其长度为 0. - + 我们之前拿 NSMutableData 记录了数据,所以我们可以在 `stopLoading `方法中计算出 Body 大小。步骤如下: - + - 在 `didReceiveData` 中不断添加 data - + ```objective-c - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { @@ -3315,11 +3317,11 @@ typedef CFHTTPMessageRef (*APMURLResponseFetchHTTPResponse)(CFURLRef response); [self.client URLProtocol:self didLoadData:data]; } ``` - + - 在 `stopLoading` 方法中拿到 `allHeaderFields` 字典,获取 `Content-Encoding` key 的值,如果是 **gzip**,则在 `stopLoading` 中将 NSData 处理为 gzip 压缩后的数据,再计算大小。(gzip 相关功能可以使用这个[工具](https://github.com/nicklockwood/GZIP)) - + 需要额外计算一个空白行的长度 - + ```objective-c - (void)stopLoadi { @@ -3351,14 +3353,14 @@ typedef CFHTTPMessageRef (*APMURLResponseFetchHTTPResponse)(CFURLRef response); 1. 先利用网络监控方案将 NSURLProtocol 管理 App 的各种网络请求 2. 在各个方法内部记录各项所需参数(NSURLProtocol 不能分析请求握手、挥手等数据大小和时间消耗,不过对于正常情况的接口流量分析足够了,最底层需要 Socket 层) - + ```objective-c @property(nonatomic, strong) NSURLConnection *internalConnection; @property(nonatomic, strong) NSURLResponse *internalResponse; @property(nonatomic, strong) NSMutableData *responseData; @property (nonatomic, strong) NSURLRequest *internalRequest; ``` - + ```objective-c - (void)startLoading { @@ -3366,13 +3368,13 @@ typedef CFHTTPMessageRef (*APMURLResponseFetchHTTPResponse)(CFURLRef response); self.internalConnection = [[NSURLConnection alloc] initWithRequest:mutableRequest delegate:self]; self.internalRequest = self.request; } - + - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; self.internalResponse = response; } - + - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { [self.responseData appendData:data]; @@ -3381,11 +3383,11 @@ typedef CFHTTPMessageRef (*APMURLResponseFetchHTTPResponse)(CFURLRef response); ``` 3. Status Line 部分 - + 对于 NSURLRequest 没有像 NSURLResponse 一样的方法找到 StatusLine。所以兜底方案是自己根据 Status Line 的结构,自己手动构造一个。结构为:`协议版本号+空格+状态码+空格+状态文本+换行` - + 为 NSURLRequest 添加一个专门获取 Status Line 的分类。 - + ```objective-c // NSURLResquest+apm_FetchStatusLineFromCFNetwork.m - (NSUInteger)apm_fetchStatusLineLength @@ -3397,30 +3399,30 @@ typedef CFHTTPMessageRef (*APMURLResponseFetchHTTPResponse)(CFURLRef response); ``` 4. Header 部分 - + 一个 HTTP 请求会先构建判断是否存在缓存,然后进行 DNS 域名解析以获取请求域名的服务器 IP 地址。如果请求协议是 HTTPS,那么还需要建立 TLS 连接。接下来就是利用 IP 地址和服务器建立 TCP 连接。连接建立之后,浏览器端会构建请求行、请求头等信息,并把和该域名相关的 Cookie 等数据附加到请求头中,然后向服务器发送构建的请求信息。 - + 所以一个网络监控不考虑 cookie 😂,借用王多鱼的一句话「那不完犊子了吗」。 - + 看过一些文章说 NSURLRequest 不能完整获取到请求头信息。其实问题不大, 几个信息获取不完全也没办法。衡量监控方案本身就是看接口在不同版本或者某些情况下数据消耗是否异常,WebView 资源请求是否过大,类似于控制变量法的思想。 - + 所以获取到 NSURLRequest 的 `allHeaderFields` 后,加上 cookie 信息,计算完整的 Header 大小 - + ```objective-c // NSURLResquest+apm_FetchHeaderWithCookies.m - (NSUInteger)apm_fetchHeaderLengthWithCookie { NSDictionary *headerFields = self.allHTTPHeaderFields; NSDictionary *cookiesHeader = [self apm_fetchCookies]; - + if (cookiesHeader.count) { NSMutableDictionary *headerDictionaryWithCookies = [NSMutableDictionary dictionaryWithDictionary:headerFields]; [headerDictionaryWithCookies addEntriesFromDictionary:cookiesHeader]; headerFields = [headerDictionaryWithCookies copy]; } - + NSString *headerString = @""; - + for (NSString *key in headerFields.allKeys) { headerString = [headerString stringByAppendingString:key]; headerString = [headerString stringByAppendingString:@": "]; @@ -3433,7 +3435,7 @@ typedef CFHTTPMessageRef (*APMURLResponseFetchHTTPResponse)(CFURLRef response); headersLength = headerData.length; return headerString; } - + - (NSDictionary *)apm_fetchCookies { NSDictionary *cookiesHeaderDictionary; @@ -3447,15 +3449,15 @@ typedef CFHTTPMessageRef (*APMURLResponseFetchHTTPResponse)(CFURLRef response); ``` 5. Body 部分 - + NSURLConnection 的 `HTTPBody` 有可能获取不到,问题类似于 WebView 上 ajax 等情况。所以可以通过 `HTTPBodyStream` 读取 stream 来计算 body 大小. - + ```objective-c - (NSUInteger)apm_fetchRequestBody { NSDictionary *headerFields = self.allHTTPHeaderFields; NSUInteger bodyLength = [self.HTTPBody length]; - + if ([headerFields objectForKey:@"Content-Encoding"]) { NSData *bodyData; if (self.HTTPBody == nil) { @@ -3481,7 +3483,7 @@ typedef CFHTTPMessageRef (*APMURLResponseFetchHTTPResponse)(CFURLRef response); ``` 6. 在 `- (NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)response` 方法中将数据上报会在 [打造功能强大、灵活可配置的数据上报组件](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.80.md) 讲 - + ```objective-c -(NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)response { @@ -4151,55 +4153,55 @@ static void uninstallSignalHandler(void) 说明: 1. 先从堆上分配一块内存区域,被称为“可替换信号栈”,目的是将信号处理函数的栈干掉,用堆上的内存区域代替,而不和进程共用一块栈区。 - + 为什么这么做?一个进程可能有 n 个线程,每个线程都有自己的任务,假如某个线程执行出错,这样就会导致整个进程的崩溃。所以为了信号处理函数正常运行,需要为信号处理函数设置单独的运行空间。另一种情况是递归函数将系统默认的栈空间用尽了,但是信号处理函数使用的栈是它实现在堆中分配的空间,而不是系统默认的栈,所以它仍旧可以正常工作。 2. `int sigaltstack(const stack_t * __restrict, stack_t * __restrict)` 函数的二个参数都是 `stack_t` 结构的指针,存储了可替换信号栈的信息(栈的起始地址、栈的长度、状态)。第 1 个参数该结构存储了一个“可替换信号栈” 的位置及属性信息。第 2 个参数用来返回上一次建立的“可替换信号栈”的信息(如果有的话)。 - + ```c _STRUCT_SIGALTSTACK { - void *ss_sp; /* signal stack base */ - __darwin_size_t ss_size; /* signal stack length */ - int ss_flags; /* SA_DISABLE and/or SA_ONSTACK */ + void *ss_sp; /* signal stack base */ + __darwin_size_t ss_size; /* signal stack length */ + int ss_flags; /* SA_DISABLE and/or SA_ONSTACK */ }; typedef _STRUCT_SIGALTSTACK stack_t; /* [???] signal stack */ ``` - + 新创建的可替换信号栈,`ss_flags` 必须设置为 0。系统定义了 `SIGSTKSZ` 常量,可满足绝大多可替换信号栈的需求。 - + ```c /* * Structure used in sigaltstack call. */ - + #define SS_ONSTACK 0x0001 /* take signal on signal stack */ #define SS_DISABLE 0x0004 /* disable taking signals on alternate stack */ #define MINSIGSTKSZ 32768 /* (32K)minimum allowable stack */ #define SIGSTKSZ 131072 /* (128K)recommended stack size */ ``` - + `sigaltstack` 系统调用通知内核“可替换信号栈”已经建立。 - + `ss_flags` 为 `SS_ONSTACK` 时,表示进程当前正在“可替换信号栈”中执行,如果此时试图去建立一个新的“可替换信号栈”,那么会遇到 `EPERM` (禁止该动作) 的错误;为 `SS_DISABLE` 说明当前没有已建立的“可替换信号栈”,禁止建立“可替换信号栈”。 3. `int sigaction(int, const struct sigaction * __restrict, struct sigaction * __restrict);` - + 第一个函数表示需要处理的信号值,但不能是 `SIGKILL` 和 `SIGSTOP` ,这两个信号的处理函数不允许用户重写,因为它们给超级用户提供了终止程序的方法( `SIGKILL` and `SIGSTOP` cannot be caught, blocked, or ignored); - + 第二个和第三个参数是一个 `sigaction` 结构体。如果第二个参数不为空则代表将其指向信号处理函数,第三个参数不为空,则将之前的信号处理函数保存到该指针中。如果第二个参数为空,第三个参数不为空,则可以获取当前的信号处理函数。 - + ```c /* * Signal vector "template" used in sigaction call. */ struct sigaction { - union __sigaction_u __sigaction_u; /* signal handler */ - sigset_t sa_mask; /* signal mask to apply */ - int sa_flags; /* see signal options below */ + union __sigaction_u __sigaction_u; /* signal handler */ + sigset_t sa_mask; /* signal mask to apply */ + int sa_flags; /* see signal options below */ }; ``` - + `sigaction` 函数的 `sa_flags` 参数需要设置 `SA_ONSTACK` 标志,告诉内核信号处理函数的栈帧就在“可替换信号栈”上建立。 #### 2.3. C++ 异常处理 @@ -4375,7 +4377,7 @@ static void setEnabled(bool isEnabled) - 创建一个线程,在线程运行方法中用 `do...while...` 循环处理逻辑,加了 autorelease 避免内存过高 - 有一个 `awaitingResponse` 属性和 `watchdogPulse` 方法。watchdogPulse 主要逻辑为设置 `awaitingResponse` 为 YES,切换到主线程中,设置 `awaitingResponse` 为 NO, - + ```objective-c - (void) watchdogPulse { @@ -4389,7 +4391,7 @@ static void setEnabled(bool isEnabled) ``` - 线程的执行方法里面不断循环,等待设置的 `g_watchdogInterval` 后判断 `awaitingResponse` 的属性值是不是初始状态的值,否则判断为死锁 - + ```objective-c - (void) runMonitor { @@ -4467,7 +4469,6 @@ static void setEnabled(bool isEnabled) ![caller](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-03-KSCrashCaller.png) ```c - /** Start general exception processing. * * @oaram context Contextual information about the exception. @@ -4623,7 +4624,7 @@ bool ksfu_openBufferedWriter(KSBufferedWriter* writer, const char* const path, c void kscrashreport_writeStandardReport(const struct KSCrash_MonitorContext* const monitorContext, const char* path) { - KSLOG_INFO("Writing crash report to %s", path); + KSLOG_INFO("Writing crash report to %s", path); char writeBuffer[1024]; KSBufferedWriter bufferedWriter; @@ -5022,7 +5023,7 @@ Tips:RN 项目打 Release 包 - 在项目根目录下创建文件夹( release_iOS),作为资源的输出文件夹 - 在终端切换到工程目录,然后执行下面的代码 - + ```shell react-native bundle --entry-file index.js --platform ios --dev false --bundle-output release_ios/main.jsbundle --assets-dest release_iOS --sourcemap-output release_ios/index.ios.map; ``` @@ -5078,24 +5079,24 @@ value@(null):(null) ' *** First throw call stack: ( - 0 CoreFoundation 0x00007fff23e3cf0e __exceptionPreprocess + 350 - 1 libobjc.A.dylib 0x00007fff50ba89b2 objc_exception_throw + 48 - 2 todos 0x00000001017b0510 RCTFormatError + 0 - 3 todos 0x000000010182d8ca -[RCTExceptionsManager reportFatal:stack:exceptionId:suppressRedBox:] + 503 - 4 todos 0x000000010182e34e -[RCTExceptionsManager reportException:] + 1658 - 5 CoreFoundation 0x00007fff23e43e8c __invoking___ + 140 - 6 CoreFoundation 0x00007fff23e41071 -[NSInvocation invoke] + 321 - 7 CoreFoundation 0x00007fff23e41344 -[NSInvocation invokeWithTarget:] + 68 - 8 todos 0x00000001017e07fa -[RCTModuleMethod invokeWithBridge:module:arguments:] + 578 - 9 todos 0x00000001017e2a84 _ZN8facebook5reactL11invokeInnerEP9RCTBridgeP13RCTModuleDatajRKN5folly7dynamicE + 246 - 10 todos 0x00000001017e280c ___ZN8facebook5react15RCTNativeModule6invokeEjON5folly7dynamicEi_block_invoke + 78 - 11 libdispatch.dylib 0x00000001025b5f11 _dispatch_call_block_and_release + 12 - 12 libdispatch.dylib 0x00000001025b6e8e _dispatch_client_callout + 8 - 13 libdispatch.dylib 0x00000001025bd6fd _dispatch_lane_serial_drain + 788 - 14 libdispatch.dylib 0x00000001025be28f _dispatch_lane_invoke + 422 - 15 libdispatch.dylib 0x00000001025c9b65 _dispatch_workloop_worker_thread + 719 - 16 libsystem_pthread.dylib 0x00007fff51c08a3d _pthread_wqthread + 290 - 17 libsystem_pthread.dylib 0x00007fff51c07b77 start_wqthread + 15 + 0 CoreFoundation 0x00007fff23e3cf0e __exceptionPreprocess + 350 + 1 libobjc.A.dylib 0x00007fff50ba89b2 objc_exception_throw + 48 + 2 todos 0x00000001017b0510 RCTFormatError + 0 + 3 todos 0x000000010182d8ca -[RCTExceptionsManager reportFatal:stack:exceptionId:suppressRedBox:] + 503 + 4 todos 0x000000010182e34e -[RCTExceptionsManager reportException:] + 1658 + 5 CoreFoundation 0x00007fff23e43e8c __invoking___ + 140 + 6 CoreFoundation 0x00007fff23e41071 -[NSInvocation invoke] + 321 + 7 CoreFoundation 0x00007fff23e41344 -[NSInvocation invokeWithTarget:] + 68 + 8 todos 0x00000001017e07fa -[RCTModuleMethod invokeWithBridge:module:arguments:] + 578 + 9 todos 0x00000001017e2a84 _ZN8facebook5reactL11invokeInnerEP9RCTBridgeP13RCTModuleDatajRKN5folly7dynamicE + 246 + 10 todos 0x00000001017e280c ___ZN8facebook5react15RCTNativeModule6invokeEjON5folly7dynamicEi_block_invoke + 78 + 11 libdispatch.dylib 0x00000001025b5f11 _dispatch_call_block_and_release + 12 + 12 libdispatch.dylib 0x00000001025b6e8e _dispatch_client_callout + 8 + 13 libdispatch.dylib 0x00000001025bd6fd _dispatch_lane_serial_drain + 788 + 14 libdispatch.dylib 0x00000001025be28f _dispatch_lane_invoke + 422 + 15 libdispatch.dylib 0x00000001025c9b65 _dispatch_workloop_worker_thread + 719 + 16 libsystem_pthread.dylib 0x00007fff51c08a3d _pthread_wqthread + 290 + 17 libsystem_pthread.dylib 0x00007fff51c07b77 start_wqthread + 15 ) libc++abi.dylib: terminating with uncaught exception of type NSException (lldb) @@ -5276,9 +5277,9 @@ global.ErrorUtils.setGlobalHandler(e => { 其实对于 RN 的 crash 处理还有个需要注意的就是 **React Error Boundaries**。[详细资料](https://zh-hans.reactjs.org/docs/error-boundaries.html) > 过去,组件内的 JavaScript 错误会导致 React 的内部状态被破坏,并且在下一次渲染时 [产生](https://github.com/facebook/react/issues/4026) [可能无法追踪的](https://github.com/facebook/react/issues/6895) [错误](https://github.com/facebook/react/issues/8579)。这些错误基本上是由较早的其他代码(非 React 组件代码)错误引起的,但 React 并没有提供一种在组件中优雅处理这些错误的方式,也无法从错误中恢复。 -> +> > 部分 UI 的 JavaScript 错误不应该导致整个应用崩溃,为了解决这个问题,React 16 引入了一个新的概念 —— 错误边界。 -> +> > 错误边界是一种 React 组件,这种组件**可以捕获并打印发生在其子组件树任何位置的 JavaScript 错误,并且,它会渲染出备用 UI**,而不是渲染那些崩溃了的子组件树。错误边界在渲染期间、生命周期方法和整个组件树的构造函数中捕获错误。 它能捕获子组件生命周期函数中的异常,包括构造函数(constructor)和 render 函数 @@ -5335,7 +5336,7 @@ parseJSError(line, column); 接下来做个实验,还是上述的 todos 项目。 1. 在 Text 的点击事件上模拟 crash - + ```jsx #import "APMCrashReporterSink.h" - + @implementation APMCrashInstallation - + + (instancetype)sharedInstance { static APMCrashInstallation *sharedInstance = nil; static dispatch_once_t onceToken; @@ -5414,35 +5415,35 @@ parseJSError(line, column); }); return sharedInstance; } - + - (id)init { return [super initWithRequiredProperties: nil]; } - + - (id)sink { APMCrashReporterSink *sink = [[APMCrashReporterSink alloc] init]; return [sink defaultCrashReportFilterSetAppleFmt]; } - + @end ``` - `sink` 方法内部的 `APMCrashReporterSink` 类,遵循了 **KSCrashReportFilter** 协议,声明了公有方法 `defaultCrashReportFilterSetAppleFmt` - + ```objective-c // .h #import #import - + @interface APMCrashReporterSink : NSObject - + - (id ) defaultCrashReportFilterSetAppleFmt; - + @end - + // .m #pragma mark - public Method - + - (id ) defaultCrashReportFilterSetAppleFmt { return [KSCrashReportFilterPipeline filterWithFilters: @@ -5451,11 +5452,11 @@ parseJSError(line, column); nil]; } ``` - + 其中 `defaultCrashReportFilterSetAppleFmt` 方法内部返回了一个 `KSCrashReportFilterPipeline` 类方法 `filterWithFilters` 的结果。 - + `APMCrashReportFilterAppleFmt` 是一个继承自 `KSCrashReportFilterAppleFmt` 的类,遵循了 `KSCrashReportFilter` 协议。协议方法允许开发者处理 Crash 的数据格式。 - + ```objective-c /** Filter the specified reports. * @@ -5465,14 +5466,14 @@ parseJSError(line, column); - (void) filterReports:(NSArray*) reports onCompletion:(KSCrashReportFilterCompletion) onCompletion; ``` - + ```objective-c #import - + @interface APMCrashReportFilterAppleFmt : KSCrashReportFilterAppleFmt - + @end - + // .m - (void) filterReports:(NSArray*)reports onCompletion:(KSCrashReportFilterCompletion)onCompletion { @@ -5487,7 +5488,7 @@ parseJSError(line, column); } kscrash_callCompletion(onCompletion, filteredReports, YES, nil); } - + /** @brief 获取Crash JSON中的crash时间、mach name、signal name和apple report */ @@ -5496,17 +5497,17 @@ parseJSError(line, column); NSDictionary *infoReport = [crashReport objectForKey:@"report"]; // ... id appleReport = [self toAppleFormat:crashReport]; - + NSMutableDictionary *info = [NSMutableDictionary dictionary]; [info setValue:crashTime forKey:@"crashTime"]; [info setValue:appleReport forKey:@"appleReport"]; [info setValue:userException forKey:@"userException"]; [info setValue:userInfo forKey:@"custom"]; - + return [info copy]; } ``` - + ```objective-c /** * A pipeline of filters. Reports get passed through each subfilter in order. @@ -5518,7 +5519,7 @@ parseJSError(line, column); ``` - APM 能力中为 Crash 模块设置一个启动器。启动器内部设置 KSCrash 的初始化工作,以及触发 Crash 时候监控所需数据的组装。比如:SESSION_ID、App 启动时间、App 名称、崩溃时间、App 版本号、当前页面信息等基础信息。 - + ```objective-c /** C Function to call during a crash report to give the callee an opportunity to * add to the report. NULL = ignore. @@ -5528,7 +5529,7 @@ parseJSError(line, column); */ @property(atomic,readwrite,assign) KSReportWriteCallback onCrash; ``` - + ```objective-c + (instancetype)sharedInstance { @@ -5541,158 +5542,187 @@ parseJSError(line, column); } ``` - #pragma mark - public Method - - (void)startMonitor - { - APMMLog(@"crash monitor started"); - +- (void)startMonitor + { + APMMLog(@"crash monitor started"); + #ifdef DEBUG - BOOL _trackingCrashOnDebug = [APMMonitorConfig sharedInstance].trackingCrashOnDebug; - if (_trackingCrashOnDebug) { - [self installKSCrash]; - } + BOOL _trackingCrashOnDebug = [APMMonitorConfig sharedInstance].trackingCrashOnDebug; + if (_trackingCrashOnDebug) { + + [self installKSCrash]; + + } #else - [self installKSCrash]; + [self installKSCrash]; #endif } - + #pragma mark - private method - + static void onCrash(const KSCrashReportWriter* writer) { - NSString *sessionId = [NSString stringWithFormat:@"\"%@\"", ***]]; - writer->addJSONElement(writer, "SESSION_ID", [sessionId UTF8String], true); - - NSString *appLaunchTime = ***; - writer->addJSONElement(writer, "USER_APP_START_DATE", [[NSString stringWithFormat:@"\"%@\"", appLaunchTime] UTF8String], true); - // ... + NSString *sessionId = [NSString stringWithFormat:@"\"%@\"", ***]]; + writer->addJSONElement(writer, "SESSION_ID", [sessionId UTF8String], true); + + NSString *appLaunchTime = ***; + writer->addJSONElement(writer, "USER_APP_START_DATE", [[NSString stringWithFormat:@"\"%@\"", appLaunchTime] UTF8String], true); + // ... } - - (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; - }); - } +- (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; + + }); + } ``` - + 在 `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 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]; - - KSCrash* handler = [KSCrash sharedInstance]; - handler.sink = sink; - [handler sendAllReportsWithCompletion:onCompletion]; + NSError* error = [self validateProperties]; + if(error != nil) + { + + if(onCompletion != nil) + { + onCompletion(nil, NO, error); + } + return; + + } + + id 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]; + + KSCrash* handler = [KSCrash sharedInstance]; + handler.sink = sink; + [handler sendAllReportsWithCompletion:onCompletion]; } ``` - + 方法内部将 `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) - { - KSLOG_ERROR(@"Failed to send reports: %@", error); - } - if((self.deleteBehaviorAfterSendAll == KSCDeleteOnSucess && completed) || - self.deleteBehaviorAfterSendAll == KSCDeleteAlways) - { - kscrash_deleteAllReports(); - } - kscrash_callCompletion(onCompletion, filteredReports, completed, error); - }]; + 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) + { + KSLOG_ERROR(@"Failed to send reports: %@", error); + } + if((self.deleteBehaviorAfterSendAll == KSCDeleteOnSucess && completed) || + self.deleteBehaviorAfterSendAll == KSCDeleteAlways) + { + kscrash_deleteAllReports(); + } + kscrash_callCompletion(onCompletion, filteredReports, completed, error); + + }]; } ``` - + 该方法内部调用了对象方法 `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); - }]; + 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); + + }]; } ``` - + 方法内部的 `[self.sink filterReports: onCompletion: ]` 实现其实就是 `APMCrashInstallation` 中设置的 `sink` getter 方法,内部返回了 `APMCrashReporterSink` 对象的 `defaultCrashReportFilterSetAppleFmt` 方法的返回值。内部实现如下 - + ```objective-c - - (id ) defaultCrashReportFilterSetAppleFmt + +- (id ) defaultCrashReportFilterSetAppleFmt { - return [KSCrashReportFilterPipeline filterWithFilters: - [APMCrashReportFilterAppleFmt filterWithReportStyle:KSAppleReportStyleSymbolicatedSideBySide], - self, - nil]; + 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 - { - for (NSDictionary *report in reports) { - // 处理 Crash 数据,将数据交给统一的数据上报组件处理... - } - kscrash_callCompletion(onCompletion, reports, YES, nil); - } - ``` +- (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. 符号化 @@ -5724,37 +5754,37 @@ DWARF 是可执行程序与源代码关系的一个紧凑表示。 DWARF 文件中的数据如下: -| 数据列 | 信息说明 | -| --------------- | -------------------------------------- | +| 数据列 | 信息说明 | +| --------------- | --------------------------- | | .debug_loc | 在 DW_AT_location 属性中使用的位置列表 | -| .debug_macinfo | 宏信息 | +| .debug_macinfo | 宏信息 | | .debug_pubnames | 全局对象和函数的查找表 | -| .debug_pubtypes | 全局类型的查找表 | +| .debug_pubtypes | 全局类型的查找表 | | .debug_ranges | 在 DW_AT_ranges 属性中使用的地址范围 | -| .debug_str | 在 .debug_info 中使用的字符串表 | -| .debug_types | 类型描述 | +| .debug_str | 在 .debug_info 中使用的字符串表 | +| .debug_types | 类型描述 | 常用的标记与属性如下: -| 数据列 | 信息说明 | -| --------------------------- | ----------------------------- | +| 数据列 | 信息说明 | +| --------------------------- | ------------------- | | DW_TAG_class_type | 表示类名称和类型信息 | -| DW_TAG_structure_type | 表示结构名称和类型信息 | -| DW_TAG_union_type | 表示联合名称和类型信息 | -| DW_TAG_enumeration_type | 表示枚举名称和类型信息 | +| DW_TAG_structure_type | 表示结构名称和类型信息 | +| DW_TAG_union_type | 表示联合名称和类型信息 | +| DW_TAG_enumeration_type | 表示枚举名称和类型信息 | | DW_TAG_typedef | 表示 typedef 的名称和类型信息 | -| DW_TAG_array_type | 表示数组名称和类型信息 | -| DW_TAG_subrange_type | 表示数组的大小信息 | -| DW_TAG_inheritance | 表示继承的类名称和类型信息 | -| DW_TAG_member | 表示类的成员 | -| DW_TAG_subprogram | 表示函数的名称信息 | -| DW_TAG_formal_parameter | 表示函数的参数信息 | -| DW_TAG_name | 表示名称字符串 | -| DW_TAG_type | 表示类型信息 | -| DW_TAG_artifical | 在创建时由编译程序设置 | -| DW_TAG_sibling | 表示兄弟位置信息 | -| DW_TAG_data_memver_location | 表示位置信息 | -| DW_TAG_virtuality | 在虚拟时设置 | +| DW_TAG_array_type | 表示数组名称和类型信息 | +| DW_TAG_subrange_type | 表示数组的大小信息 | +| DW_TAG_inheritance | 表示继承的类名称和类型信息 | +| DW_TAG_member | 表示类的成员 | +| DW_TAG_subprogram | 表示函数的名称信息 | +| DW_TAG_formal_parameter | 表示函数的参数信息 | +| DW_TAG_name | 表示名称字符串 | +| DW_TAG_type | 表示类型信息 | +| DW_TAG_artifical | 在创建时由编译程序设置 | +| DW_TAG_sibling | 表示兄弟位置信息 | +| DW_TAG_data_memver_location | 表示位置信息 | +| DW_TAG_virtuality | 在虚拟时设置 | 简单看一个 DWARF 的例子:将测试工程的 `.DSYM` 文件夹下的 DWARF 文件用下面命令解析 @@ -5765,128 +5795,128 @@ dwarfdump -F --debug-info Test.app.DSYM/Contents/Resources/DWARF/Test > debug-in 打开如下 ```shell -Test.app.DSYM/Contents/Resources/DWARF/Test: file format Mach-O arm64 +Test.app.DSYM/Contents/Resources/DWARF/Test: file format Mach-O arm64 .debug_info contents: 0x00000000: Compile Unit: length = 0x0000004f version = 0x0004 abbr_offset = 0x0000 addr_size = 0x08 (next unit at 0x00000053) 0x0000000b: DW_TAG_compile_unit - DW_AT_producer [DW_FORM_strp] ("Apple clang version 11.0.3 (clang-1103.0.32.62)") - DW_AT_language [DW_FORM_data2] (DW_LANG_ObjC) - DW_AT_name [DW_FORM_strp] ("_Builtin_stddef_max_align_t") - DW_AT_stmt_list [DW_FORM_sec_offset] (0x00000000) - DW_AT_comp_dir [DW_FORM_strp] ("/Users/lbp/Desktop/Test") - DW_AT_APPLE_major_runtime_vers [DW_FORM_data1] (0x02) - DW_AT_GNU_dwo_id [DW_FORM_data8] (0x392b5344d415340c) + DW_AT_producer [DW_FORM_strp] ("Apple clang version 11.0.3 (clang-1103.0.32.62)") + DW_AT_language [DW_FORM_data2] (DW_LANG_ObjC) + DW_AT_name [DW_FORM_strp] ("_Builtin_stddef_max_align_t") + DW_AT_stmt_list [DW_FORM_sec_offset] (0x00000000) + DW_AT_comp_dir [DW_FORM_strp] ("/Users/lbp/Desktop/Test") + DW_AT_APPLE_major_runtime_vers [DW_FORM_data1] (0x02) + DW_AT_GNU_dwo_id [DW_FORM_data8] (0x392b5344d415340c) 0x00000027: DW_TAG_module - DW_AT_name [DW_FORM_strp] ("_Builtin_stddef_max_align_t") - DW_AT_LLVM_config_macros [DW_FORM_strp] ("\"-DDEBUG=1\" \"-DOBJC_OLD_DISPATCH_PROTOTYPES=1\"") - DW_AT_LLVM_include_path [DW_FORM_strp] ("/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/11.0.3/include") - DW_AT_LLVM_isysroot [DW_FORM_strp] ("/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.5.sdk") + DW_AT_name [DW_FORM_strp] ("_Builtin_stddef_max_align_t") + DW_AT_LLVM_config_macros [DW_FORM_strp] ("\"-DDEBUG=1\" \"-DOBJC_OLD_DISPATCH_PROTOTYPES=1\"") + DW_AT_LLVM_include_path [DW_FORM_strp] ("/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/11.0.3/include") + DW_AT_LLVM_isysroot [DW_FORM_strp] ("/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.5.sdk") 0x00000038: DW_TAG_typedef - DW_AT_type [DW_FORM_ref4] (0x0000004b "long double") - DW_AT_name [DW_FORM_strp] ("max_align_t") - DW_AT_decl_file [DW_FORM_data1] ("/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/11.0.3/include/__stddef_max_align_t.h") - DW_AT_decl_line [DW_FORM_data1] (16) + DW_AT_type [DW_FORM_ref4] (0x0000004b "long double") + DW_AT_name [DW_FORM_strp] ("max_align_t") + DW_AT_decl_file [DW_FORM_data1] ("/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/11.0.3/include/__stddef_max_align_t.h") + DW_AT_decl_line [DW_FORM_data1] (16) 0x00000043: DW_TAG_imported_declaration - DW_AT_decl_file [DW_FORM_data1] ("/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/11.0.3/include/__stddef_max_align_t.h") - DW_AT_decl_line [DW_FORM_data1] (27) - DW_AT_import [DW_FORM_ref_addr] (0x0000000000000027) + DW_AT_decl_file [DW_FORM_data1] ("/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/11.0.3/include/__stddef_max_align_t.h") + DW_AT_decl_line [DW_FORM_data1] (27) + DW_AT_import [DW_FORM_ref_addr] (0x0000000000000027) 0x0000004a: NULL 0x0000004b: DW_TAG_base_type - DW_AT_name [DW_FORM_strp] ("long double") - DW_AT_encoding [DW_FORM_data1] (DW_ATE_float) - DW_AT_byte_size [DW_FORM_data1] (0x08) + DW_AT_name [DW_FORM_strp] ("long double") + DW_AT_encoding [DW_FORM_data1] (DW_ATE_float) + DW_AT_byte_size [DW_FORM_data1] (0x08) 0x00000052: NULL 0x00000053: Compile Unit: length = 0x000183dc version = 0x0004 abbr_offset = 0x0000 addr_size = 0x08 (next unit at 0x00018433) 0x0000005e: DW_TAG_compile_unit - DW_AT_producer [DW_FORM_strp] ("Apple clang version 11.0.3 (clang-1103.0.32.62)") - DW_AT_language [DW_FORM_data2] (DW_LANG_ObjC) - DW_AT_name [DW_FORM_strp] ("Darwin") - DW_AT_stmt_list [DW_FORM_sec_offset] (0x000000a7) - DW_AT_comp_dir [DW_FORM_strp] ("/Users/lbp/Desktop/Test") - DW_AT_APPLE_major_runtime_vers [DW_FORM_data1] (0x02) - DW_AT_GNU_dwo_id [DW_FORM_data8] (0xa4a1d339379e18a5) + DW_AT_producer [DW_FORM_strp] ("Apple clang version 11.0.3 (clang-1103.0.32.62)") + DW_AT_language [DW_FORM_data2] (DW_LANG_ObjC) + DW_AT_name [DW_FORM_strp] ("Darwin") + DW_AT_stmt_list [DW_FORM_sec_offset] (0x000000a7) + DW_AT_comp_dir [DW_FORM_strp] ("/Users/lbp/Desktop/Test") + DW_AT_APPLE_major_runtime_vers [DW_FORM_data1] (0x02) + DW_AT_GNU_dwo_id [DW_FORM_data8] (0xa4a1d339379e18a5) 0x0000007a: DW_TAG_module - DW_AT_name [DW_FORM_strp] ("Darwin") - DW_AT_LLVM_config_macros [DW_FORM_strp] ("\"-DDEBUG=1\" \"-DOBJC_OLD_DISPATCH_PROTOTYPES=1\"") - DW_AT_LLVM_include_path [DW_FORM_strp] ("/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.5.sdk/usr/include") - DW_AT_LLVM_isysroot [DW_FORM_strp] ("/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.5.sdk") + DW_AT_name [DW_FORM_strp] ("Darwin") + DW_AT_LLVM_config_macros [DW_FORM_strp] ("\"-DDEBUG=1\" \"-DOBJC_OLD_DISPATCH_PROTOTYPES=1\"") + DW_AT_LLVM_include_path [DW_FORM_strp] ("/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.5.sdk/usr/include") + DW_AT_LLVM_isysroot [DW_FORM_strp] ("/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.5.sdk") 0x0000008b: DW_TAG_module - DW_AT_name [DW_FORM_strp] ("C") - DW_AT_LLVM_config_macros [DW_FORM_strp] ("\"-DDEBUG=1\" \"-DOBJC_OLD_DISPATCH_PROTOTYPES=1\"") - DW_AT_LLVM_include_path [DW_FORM_strp] ("/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.5.sdk/usr/include") - DW_AT_LLVM_isysroot [DW_FORM_strp] ("/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.5.sdk") + DW_AT_name [DW_FORM_strp] ("C") + DW_AT_LLVM_config_macros [DW_FORM_strp] ("\"-DDEBUG=1\" \"-DOBJC_OLD_DISPATCH_PROTOTYPES=1\"") + DW_AT_LLVM_include_path [DW_FORM_strp] ("/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.5.sdk/usr/include") + DW_AT_LLVM_isysroot [DW_FORM_strp] ("/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.5.sdk") 0x0000009c: DW_TAG_module - DW_AT_name [DW_FORM_strp] ("fenv") - DW_AT_LLVM_config_macros [DW_FORM_strp] ("\"-DDEBUG=1\" \"-DOBJC_OLD_DISPATCH_PROTOTYPES=1\"") - DW_AT_LLVM_include_path [DW_FORM_strp] ("/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.5.sdk/usr/include") - DW_AT_LLVM_isysroot [DW_FORM_strp] ("/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.5.sdk") + DW_AT_name [DW_FORM_strp] ("fenv") + DW_AT_LLVM_config_macros [DW_FORM_strp] ("\"-DDEBUG=1\" \"-DOBJC_OLD_DISPATCH_PROTOTYPES=1\"") + DW_AT_LLVM_include_path [DW_FORM_strp] ("/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.5.sdk/usr/include") + DW_AT_LLVM_isysroot [DW_FORM_strp] ("/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.5.sdk") 0x000000ad: DW_TAG_enumeration_type - DW_AT_type [DW_FORM_ref4] (0x00017276 "unsigned int") - DW_AT_byte_size [DW_FORM_data1] (0x04) - DW_AT_decl_file [DW_FORM_data1] ("/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.5.sdk/usr/include/fenv.h") - DW_AT_decl_line [DW_FORM_data1] (154) + DW_AT_type [DW_FORM_ref4] (0x00017276 "unsigned int") + DW_AT_byte_size [DW_FORM_data1] (0x04) + DW_AT_decl_file [DW_FORM_data1] ("/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.5.sdk/usr/include/fenv.h") + DW_AT_decl_line [DW_FORM_data1] (154) 0x000000b5: DW_TAG_enumerator - DW_AT_name [DW_FORM_strp] ("__fpcr_trap_invalid") - DW_AT_const_value [DW_FORM_udata] (256) + DW_AT_name [DW_FORM_strp] ("__fpcr_trap_invalid") + DW_AT_const_value [DW_FORM_udata] (256) 0x000000bc: DW_TAG_enumerator - DW_AT_name [DW_FORM_strp] ("__fpcr_trap_divbyzero") - DW_AT_const_value [DW_FORM_udata] (512) + DW_AT_name [DW_FORM_strp] ("__fpcr_trap_divbyzero") + DW_AT_const_value [DW_FORM_udata] (512) 0x000000c3: DW_TAG_enumerator - DW_AT_name [DW_FORM_strp] ("__fpcr_trap_overflow") - DW_AT_const_value [DW_FORM_udata] (1024) + DW_AT_name [DW_FORM_strp] ("__fpcr_trap_overflow") + DW_AT_const_value [DW_FORM_udata] (1024) 0x000000ca: DW_TAG_enumerator - DW_AT_name [DW_FORM_strp] ("__fpcr_trap_underflow") + DW_AT_name [DW_FORM_strp] ("__fpcr_trap_underflow") // ...... 0x000466ee: DW_TAG_subprogram - DW_AT_name [DW_FORM_strp] ("CFBridgingRetain") - DW_AT_decl_file [DW_FORM_data1] ("/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.5.sdk/System/Library/Frameworks/Foundation.framework/Headers/NSObject.h") - DW_AT_decl_line [DW_FORM_data1] (105) - DW_AT_prototyped [DW_FORM_flag_present] (true) - DW_AT_type [DW_FORM_ref_addr] (0x0000000000019155 "CFTypeRef") - DW_AT_inline [DW_FORM_data1] (DW_INL_inlined) + DW_AT_name [DW_FORM_strp] ("CFBridgingRetain") + DW_AT_decl_file [DW_FORM_data1] ("/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.5.sdk/System/Library/Frameworks/Foundation.framework/Headers/NSObject.h") + DW_AT_decl_line [DW_FORM_data1] (105) + DW_AT_prototyped [DW_FORM_flag_present] (true) + DW_AT_type [DW_FORM_ref_addr] (0x0000000000019155 "CFTypeRef") + DW_AT_inline [DW_FORM_data1] (DW_INL_inlined) 0x000466fa: DW_TAG_formal_parameter - DW_AT_name [DW_FORM_strp] ("X") - DW_AT_decl_file [DW_FORM_data1] ("/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.5.sdk/System/Library/Frameworks/Foundation.framework/Headers/NSObject.h") - DW_AT_decl_line [DW_FORM_data1] (105) - DW_AT_type [DW_FORM_ref4] (0x00046706 "id") + DW_AT_name [DW_FORM_strp] ("X") + DW_AT_decl_file [DW_FORM_data1] ("/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.5.sdk/System/Library/Frameworks/Foundation.framework/Headers/NSObject.h") + DW_AT_decl_line [DW_FORM_data1] (105) + DW_AT_type [DW_FORM_ref4] (0x00046706 "id") 0x00046705: NULL 0x00046706: DW_TAG_typedef - DW_AT_type [DW_FORM_ref4] (0x00046711 "objc_object*") - DW_AT_name [DW_FORM_strp] ("id") - DW_AT_decl_file [DW_FORM_data1] ("/Users/lbp/Desktop/Test/Test/NetworkAPM/NSURLResponse+apm_FetchStatusLineFromCFNetwork.m") - DW_AT_decl_line [DW_FORM_data1] (44) + DW_AT_type [DW_FORM_ref4] (0x00046711 "objc_object*") + DW_AT_name [DW_FORM_strp] ("id") + DW_AT_decl_file [DW_FORM_data1] ("/Users/lbp/Desktop/Test/Test/NetworkAPM/NSURLResponse+apm_FetchStatusLineFromCFNetwork.m") + DW_AT_decl_line [DW_FORM_data1] (44) 0x00046711: DW_TAG_pointer_type - DW_AT_type [DW_FORM_ref4] (0x00046716 "objc_object") + DW_AT_type [DW_FORM_ref4] (0x00046716 "objc_object") 0x00046716: DW_TAG_structure_type - DW_AT_name [DW_FORM_strp] ("objc_object") - DW_AT_byte_size [DW_FORM_data1] (0x00) + DW_AT_name [DW_FORM_strp] ("objc_object") + DW_AT_byte_size [DW_FORM_data1] (0x00) 0x0004671c: DW_TAG_member - DW_AT_name [DW_FORM_strp] ("isa") - DW_AT_type [DW_FORM_ref4] (0x00046727 "objc_class*") - DW_AT_data_member_location [DW_FORM_data1] (0x00) + DW_AT_name [DW_FORM_strp] ("isa") + DW_AT_type [DW_FORM_ref4] (0x00046727 "objc_class*") + DW_AT_data_member_location [DW_FORM_data1] (0x00) // ...... ``` @@ -5901,7 +5931,7 @@ dwarfdump -F --debug-line Test.app.DSYM/Contents/Resources/DWARF/Test > debug-in 贴部分信息 ```shell -Test.app.DSYM/Contents/Resources/DWARF/Test: file format Mach-O arm64 +Test.app.DSYM/Contents/Resources/DWARF/Test: file format Mach-O arm64 .debug_line contents: debug_line[0x00000000] @@ -6114,7 +6144,7 @@ crash 日志中的信息 ```shell Last Exception Backtrace: // ... -5 Test 0x102fe592c -[ViewController testMonitorCrash] + 22828 (ViewController.mm:58) +5 Test 0x102fe592c -[ViewController testMonitorCrash] + 22828 (ViewController.mm:58) ``` ```sh @@ -6133,11 +6163,11 @@ atos -o Test.app.DSYM/Contents/Resources/DWARF/Test-arch arm64 -l 0x102fe0000 0x #### 4.5 UUID - crash 文件的 UUID - + ```shell grep --after-context=2 "Binary Images:" *.crash ``` - + ```shell Test 5-28-20, 7-47 PM.crash:Binary Images: Test 5-28-20, 7-47 PM.crash-0x102fe0000 - 0x102ff3fff Test arm64 <37eaa57df2523d95969e47a9a1d69ce5> /var/containers/Bundle/Application/643F0DFE-A710-4136-A278-A89D780B7208/Test.app/Test @@ -6147,29 +6177,29 @@ atos -o Test.app.DSYM/Contents/Resources/DWARF/Test-arch arm64 -l 0x102fe0000 0x Test.crash-0x102fe0000 - 0x102ff3fff Test arm64 <37eaa57df2523d95969e47a9a1d69ce5> /var/containers/Bundle/Application/643F0DFE-A710-4136-A278-A89D780B7208/Test.app/Test Test.crash-0x1030e0000 - 0x1030ebfff libobjc-trampolines.dylib arm64 <181f3aa866d93165ac54344385ac6e1d> /usr/lib/libobjc-trampolines.dylib ``` - + Test App 的 UUID 为 `37eaa57df2523d95969e47a9a1d69ce5`. - .DSYM 文件的 UUID - + ```shell dwarfdump --uuid Test.app.DSYM ``` - + 结果为 - + ```shell UUID: 37EAA57D-F252-3D95-969E-47A9A1D69CE5 (arm64) Test.app.DSYM/Contents/Resources/DWARF/Test ``` - app 的 UUID - + ```shell dwarfdump --uuid Test.app/Test ``` - + 结果为 - + ```shell UUID: 37EAA57D-F252-3D95-969E-47A9A1D69CE5 (arm64) Test.app/Test ``` @@ -6187,45 +6217,45 @@ app 和 .DSYM 文件可以通过打包的产物得到,路径为 `~/Library/Dev 解析方法一般有 2 种: - 使用 **symbolicatecrash** - + symbolicatecrash 是 Xcode 自带的 crash 日志分析工具,先确定所在路径,在终端执行下面的命令 - + ```shell find /Applications/Xcode.app -name symbolicatecrash -type f ``` - + 会返回几个路径,找到 `iPhoneSimulator.platform` 所在那一行 - + ``` /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Library/PrivateFrameworks/DVTFoundation.framework/symbolicatecrash ``` - + 将 symbolicatecrash 拷贝到指定文件夹下(保存了 app、DSYM、crash 文件的文件夹) - + 执行命令 - + ```shell ./symbolicatecrash Test.crash Test.DSYM > Test.crash ``` - + 第一次做这事儿应该会报错 `Error: "DEVELOPER_DIR" is not defined at ./symbolicatecrash line 69.`,解决方案:在终端执行下面命令 - + ```shell export DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer ``` - 使用 atos - + 区别于 symbolicatecrash,atos 较为灵活,只要 `.crash` 和 `.DSYM` 或者 `.crash` 和 `.app` 文件对应即可。 - + 用法如下,-l 最后跟得是符号地址 - + ```shell xcrun atos -o Test.app.DSYM/Contents/Resources/DWARF/Test -arch armv7 -l 0x1023c592c ``` - + 也可以解析 .app 文件(不存在 .DSYM 文件),其中 xxx 为段地址,xx 为偏移地址 - + ```shell atos -arch architecture -o binary -l xxx xx ``` @@ -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 有个能力就是调用打包系统的打包构建能力,会根据项目的特点,选择合适的打包机(打包平台是维护了多个打包任务,不同任务根据特点被派发到不同的打包机上,任务详情页可以看到依赖的下载、编译、运行过程等,打包好的产物包括二进制包、下载二维码等等) ![符号化流程图](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) -## 八、 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 本身是一个闭环,监控了之后需符号化解析、数据整理,进行产品化开发、最后需要监控大盘展示等 @@ -6313,11 +6386,11 @@ Crash log 统一入库 Kibana 时是没有符号化的,所以需要符号化 4. 监控数据需要做内存到文件的写入处理,需要注意策略。监控数据需要存储数据库,数据库大小、设计规则等。存入数据库后如何上报,上报机制等会在另一篇文章讲:[打造一个通用、可配置的数据上报 SDK](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.80.md) 5. 尽量在技术评审后,将各端的技术实现写进文档中,同步给相关人员。比如 ANR 的实现 - + ```objective-c /* android 端 - + 根据设备分级,一般超过 300ms 视为一次卡顿 hook 系统 loop,在消息处理前后插桩,用以计算每条消息的时长 开启另外线程 dump 堆栈,处理结束后关闭 @@ -6334,10 +6407,10 @@ Crash log 统一入库 Kibana 时是没有符号化的,所以需要符号化 } } }) - + /* iOS 端 - + 子线程通过 ping 主线程来确认主线程当前是否卡顿。 卡顿阈值设置为 300ms,超过阈值时认为卡顿。 卡顿时获取主线程的堆栈,并存储上传。 @@ -6359,11 +6432,11 @@ Crash log 统一入库 Kibana 时是没有符号化的,所以需要符号化 ``` 6. 整个 APM 的架构图如下 - + ![APM Structure](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-17-APMStructure.jpg) - + 说明: - + - 埋点 SDK,通过 sessionId 来关联日志数据 7. APM 技术方案本身是随着技术手段、分析需求不断调整升级的。上图的几个结构示意图是早期几个版本的,目前使用的是在此基础上进行了升级和结构调整,提几个关键词:Hermes、Flink SQL、InfluxDB。 diff --git a/Chapter1 - iOS/1.90.md b/Chapter1 - iOS/1.90.md index d535614..fec5db7 100644 --- a/Chapter1 - iOS/1.90.md +++ b/Chapter1 - iOS/1.90.md @@ -26,4 +26,28 @@ _imageView.image = image; pthread_mutex_lock -pthread_mutex_unlock \ No newline at end of file +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的图片,显然非常浪费 +( 字节的图片库也支持自动降采样操作 ) \ No newline at end of file diff --git a/Chapter1 - iOS/1.99.md b/Chapter1 - iOS/1.99.md index b8dd4df..c35907e 100644 --- a/Chapter1 - iOS/1.99.md +++ b/Chapter1 - iOS/1.99.md @@ -1,9 +1,10 @@ # App 质量把控 > 笔者结合中台经验,本文重点谈谈 App 的质量稳定性该如何做。业务作为 App 的核心服务之一,业务异常监控当然也很重要,这不是本文重点。 -## 质量问题的现状 -对于质量问题,直接以小故事的形式展开。 +## 质量问题的现状 + +对于质量问题,直接以小故事的形式展开。下面是移动中台年度针对质量复盘的一些思考 1. 技术方案阶体现测试用例 对于业务项目来说,会存在测试资源、冒烟用例、精准测试、QA 新业务的业务回归、核心业务的 UI 自动化、高铁阶段的 QA 人工回归等。这里简单讲讲这些词语,对于新的业务项目,一定会有测试资源,简单说就是 QA,新项目在经过 PRD、MRD、需求讨论会、Kick-off 之后,技术方案评审后,会经过测试用例评审,产出的结果就是用例指南,到时候 QA 会在用例平台指配给对应的开发。 diff --git a/Chapter7 - Geek Talk/7.3.md b/Chapter7 - Geek Talk/7.3.md index 1a556fe..9953789 100644 --- a/Chapter7 - Geek Talk/7.3.md +++ b/Chapter7 - Geek Talk/7.3.md @@ -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 ``` diff --git a/Chapter9 - Ragdoll/9.6.md b/Chapter9 - Ragdoll/9.6.md index b22f293..56f77e7 100644 --- a/Chapter9 - Ragdoll/9.6.md +++ b/Chapter9 - Ragdoll/9.6.md @@ -118,7 +118,7 @@ Pro 版:可以喝新鲜羊奶,淘宝有,不是那种劣质的羊奶粉。 ## 六、 结语 -小时候学完老舍的《猫》这篇文章就挺喜欢猫咪的,上班族的我们,下班后有猫咪在家陪伴或者在学习看书累了后,给猫咪喂食物、梳毛发等都是一种解压的行为。猫咪也是一门复杂的学问,需要主人的耐心和一席位经济基础(比如不要给猫咪吃30元1斤以下的猫咪,这样的猫粮钱省了,可以猫咪生病就是好几千,比较费猫 😂) +小时候学完老舍的《猫》这篇文章就挺喜欢猫咪的,上班族的我们,下班后有猫咪在家陪伴或者在学习看书累了后,给猫咪喂食物、梳毛发等都是一种解压的行为。猫咪也是一门复杂的学问,需要主人的耐心和一定的经济基础(比如不要给猫咪吃30元1斤以下的猫咪,这样的猫粮钱省了,可以猫咪生病就是好几千,比较费猫 😂) 耐心观察他、善待他,猫咪和主人互相成就,他让你不再孤单、无聊、那么萌、可爱,可以成为你朋友圈的主角。你给了他一个温暖、舒适、安全、衣食无忧的家,所以,Enjoy yourself。 diff --git a/assets/2022-0204-SubThreadUIMonitor@2x.PNG b/assets/2022-0204-SubThreadUIMonitor@2x.PNG new file mode 100644 index 0000000..63493de Binary files /dev/null and b/assets/2022-0204-SubThreadUIMonitor@2x.PNG differ diff --git a/assets/2022-0204-SubThreadUISymbolBreakpoints@2x.png b/assets/2022-0204-SubThreadUISymbolBreakpoints@2x.png new file mode 100644 index 0000000..5a9a55a Binary files /dev/null and b/assets/2022-0204-SubThreadUISymbolBreakpoints@2x.png differ diff --git a/assets/2022-0204-SubThreadUIXcode1@2x.png b/assets/2022-0204-SubThreadUIXcode1@2x.png new file mode 100644 index 0000000..29264e5 Binary files /dev/null and b/assets/2022-0204-SubThreadUIXcode1@2x.png differ diff --git a/assets/2022-0204-SubThreadUIXcodeBreakingPointsHappened@2x.png b/assets/2022-0204-SubThreadUIXcodeBreakingPointsHappened@2x.png new file mode 100644 index 0000000..0397da0 Binary files /dev/null and b/assets/2022-0204-SubThreadUIXcodeBreakingPointsHappened@2x.png differ