背景
遠(yuǎn)在iOS 11時(shí)期(2017年),蘋果就發(fā)公告要求所有需要上架的應(yīng)用都必須支持64位 。32位應(yīng)用不再支持上架與運(yùn)行 。
升級(jí)64位應(yīng)用有什么好處呢?(以下內(nèi)容純摘抄 , 客官可以直接跳過)
寄存器更多,減少內(nèi)存讀寫,加快執(zhí)行速度
這里我們要注意的是:虛擬內(nèi)存確實(shí)比純32位多了,但是App到底能用多少,是否跟宣傳一樣接近16EB?下面將會(huì)展開聊聊,我們先來看一個(gè)Crash 。
一個(gè)長(zhǎng)期存在的幽靈
我們先來看下面的一個(gè)內(nèi)存導(dǎo)致的崩潰,JSC在使用嘗試進(jìn)行內(nèi)存分配時(shí),提示OOM導(dǎo)致了 。
Last Exception :0JavaScriptCore0x000000018b777570 _pas_panic_on_out_of_memory_error1JavaScriptCore0x000000018b72e918 _bmalloc_try_iso_allocate_impl_impl_slow2JavaScriptCore0x000000018b73d3d8 _bmalloc_heap_config_specialized_local_allocator_try_allocate_small_segregated_slow +59523JavaScriptCore0x000000018b7276f8 _bmalloc_allocate_impl_casual_case +8004JavaScriptCore0x000000018c60d494 JSC::PropertyTable::create(JSC::VM&, unsigned int) +2445JavaScriptCore0x000000018c66ba74 JSC::Structure::materializePropertyTable(JSC::VM&, bool) +3246JavaScriptCore0x000000018c66dfac JSC::Structure::changePrototypeTransition(JSC::VM&, JSC::Structure*, JSC::JSValue, JSC::DeferredStructureTransitionWatchpointFire&) +6127JavaScriptCore0x000000018c559930 JSC::JSObject::setPrototypeDirect(JSC::VM&, JSC::JSValue) +1928JavaScriptCore0x000000018c559e40 JSC::JSObject::setPrototypeWithCycleCheck(JSC::VM&, JSC::JSGlobalObject*, JSC::JSValue, bool) +3169JavaScriptCore0x000000018c4f580c JSC::globalFuncProtoSetter(JSC::JSGlobalObject*, JSC::CallFrame*) +19210 JavaScriptCore0x000000018ba1f7a8 _vmEntryToNative +28011 JavaScriptCore0x000000018c1b0cd0 JSC::Interpreter::executeCall(JSC::JSGlobalObject*, JSC::JSObject*, JSC::CallData const&, JSC::JSValue, JSC::ArgList const&) +61612 JavaScriptCore0x000000018c474ecc JSC::GetterSetter::callSetter(JSC::JSGlobalObject*, JSC::JSValue, JSC::JSValue, bool) +21213 JavaScriptCore0x000000018c5b6264 JSC::JSGenericTypedArrayView::put(JSC::JSCell*, JSC::JSGlobalObject*, JSC::PropertyName, JSC::JSValue, JSC::PutPropertySlot&) +61214 JavaScriptCore0x000000018c2c2ecc _llint_slow_path_put_by_id +3244// 忽略多余重復(fù)堆棧37 JavaScriptCore0x000000018ba1f5fc _vmEntryToJavaScript +26438 JavaScriptCore0x000000018c1b0c7c JSC::Interpreter::executeCall(JSC::JSGlobalObject*, JSC::JSObject*, JSC::CallData const&, JSC::JSValue, JSC::ArgList const&) +53239 JavaScriptCore0x000000018bac7ae4 _JSObjectCallAsFunction +56840 mttlite0x0000000102a54914 hippy::napi::JSCCtx::CallFunction(std::__1::shared_ptr const&, unsigned long, std::__1::shared_ptr const*) (js_native_api_value_jsc.cc:406)41 mttlite0x0000000102a664e0 _ZNSt3__110__function6__funcIZN11TimerModule5StartERKN5hippy4napi12CallbackInfoEbE3$_4NS_9allocatorIS8_EEFvvEEclEv (memory:3237)42 mttlite0x0000000102a63018 hippy::base::TaskRunner::Run() (memory:3237)43 mttlite0x0000000102a64974 ThreadEntry (thread.cc:0)44 libsystem_pthread.dylib0x00000001dc129348 __pthread_start +116------Exception Type: SIGTRAP Exception Codes: fault addr: 0x000000018b777570Crashed Thread: 48 hippy.js這個(gè)OOM問題 , 與iOS上常見的OOM不一樣 。按照常規(guī)的理解 , 當(dāng)App內(nèi)存不足的時(shí)候,正常會(huì)觸發(fā)系統(tǒng)的機(jī)制殺死App 。在系統(tǒng)日志中會(huì)留下相關(guān)日志,理論上不會(huì)在Bugly等異常上報(bào)中發(fā)現(xiàn) 。但這一類崩潰卻一直在產(chǎn)生上報(bào)軟件運(yùn)行出錯(cuò)誤代碼1,并且低內(nèi)存的崩潰堆棧表現(xiàn)形式有很多種 。
以上的JSC崩潰問題已經(jīng)存在很長(zhǎng)一段時(shí)間了(至少2年),而且崩潰堆棧都集中在JSC執(zhí)行JS代碼的過程中,長(zhǎng)期缺乏JS相關(guān)的監(jiān)控與Debug工具導(dǎo)致該問題一直無法解決 。
雖然堆棧上有明確的原因說明是OOM,但我們觀察到有不少用戶實(shí)際上物理內(nèi)存空間還是足夠的:
兩年前,沖浪的時(shí)候偶然看來了來自微視同學(xué)的Case總結(jié):《OOM與內(nèi)存》
當(dāng)時(shí)跟hippy SDK的同事也討論過是否存在類似的內(nèi)存不足情況 。但由于大家對(duì)JSC黑盒都不熟悉,而且崩潰的JS堆棧也不確切 。當(dāng)時(shí)的建議是:少在后臺(tái)加載JSC 。最終也并沒有解決該問題 。
兩年后,當(dāng)瀏覽器集成,類似的JS崩潰直接翻倍(21H2 0.08% -> 22H1 0.16%) 。沒辦法,還是要看類似JSC和Dart VM的內(nèi)存分配機(jī)制是怎樣的,再挖掘一下是否存在解(緩)決(解)方案 。
JSC、的虛擬內(nèi)存分配
翻閱相關(guān)虛擬機(jī)的內(nèi)存管理相關(guān)代碼 , 可以找到底層的內(nèi)存分配基本實(shí)現(xiàn)都是基于mmap處理的 。
// WebKit bmalloc VMAllocateinline void* tryVMAllocate(size_t vmSize, VMTag usage = VMTag::Malloc){vmValidate(vmSize);void* result = mmap(0, vmSize, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON | BMALLOC_NORESERVE, static_cast(usage), 0);if (result == MAP_FAILED)return nullptr;return result;}// Dart VM的虛擬內(nèi)存VirtualMemory* VirtualMemory::Allocate(intptr_t size,bool is_executable,const char* name) {ASSERT(Utils::IsAligned(size, PageSize()));const int prot = PROT_READ | PROT_WRITE | (is_executable ? PROT_EXEC : 0);int map_flags = MAP_PRIVATE | MAP_ANONYMOUS;#if (defined(DART_HOST_OS_MACOS) && !defined(DART_HOST_OS_IOS))if (is_executable && IsAtLeastOS10_14()) {map_flags |= MAP_JIT;}#endif// defined(DART_HOST_OS_MACOS)// Some 64-bit microarchitectures store only the low 32-bits of targets as// part of indirect branch prediction, predicting that the target's upper bits// will be same as the call instruction's address. This leads to misprediction// for indirect calls crossing a 4GB boundary. We ask mmap to place our// generated code near the VM binary to avoid this.void* hint = is_executable ? reinterpret_cast(&Allocate) : nullptr;void* address = mmap(hint, size, prot, map_flags, -1, 0);if (address == MAP_FAILED) {return nullptr;}return new VirtualMemory(address, size);}VirtualMemory::~VirtualMemory() {if (address_ != nullptr) {if (munmap(address_, size_) != 0) {int error = errno;const int kBufferSize = 1024;char error_buf[kBufferSize];FATAL("munmap error: %d (%s)", error,Utils::StrError(error, error_buf, kBufferSize));}}}當(dāng)包含時(shí) , 并且fd傳入-1時(shí) , mmap將直接使用虛擬內(nèi)存進(jìn)行分配,不需要依賴文件描述符 。
mmap在xnu上的實(shí)現(xiàn)
/* * mmap stub, with preemptory failures due to extra parameter checking * mandated for conformance. * * This is for UNIX03 only. */void *mmap(void *addr, size_t len, int prot, int flags, int fildes, off_t off){/** Preemptory failures:** ooff is not a multiple of the page size* oflags does not contain either MAP_PRIVATE or MAP_SHARED* olen is zero*/extern void cerror_nocancel(int);if ((off & PAGE_MASK) ||(((flags & MAP_PRIVATE) != MAP_PRIVATE) &&((flags & MAP_SHARED) != MAP_SHARED)) ||(len == 0)) {cerror_nocancel(EINVAL);return(MAP_FAILED);}void *ptr = __mmap(addr, len, prot, flags, fildes, off);if (__syscall_logger) {int stackLoggingFlags = stack_logging_type_vm_allocate;if (flags & MAP_ANON) {stackLoggingFlags |= (fildes & VM_FLAGS_ALIAS_MASK);} else {stackLoggingFlags |= stack_logging_type_mapped_file_or_shared_mem;}__syscall_logger(stackLoggingFlags, (uintptr_t)mach_task_self(), (uintptr_t)len, 0, (uintptr_t)ptr, 0);}return ptr;}上面的調(diào)用會(huì)傳遞到內(nèi)核.c的實(shí)現(xiàn)函數(shù)mmap( p, *uap, *)
/* * XXX Internally, we use VM_PROT_* somewhat interchangeably, but the correct * XXX usage is PROT_* from an interface perspective.Thus the values of * XXX VM_PROT_* and PROT_* need to correspond. */intmmap(proc_t p, struct mmap_args *uap, user_addr_t *retval){/** 上面忽略了一部分代碼*/result = vm_map_enter_mem_object(user_map,&user_addr, user_size,0, alloc_flags, vmk_flags,tag,IPC_PORT_NULL, 0, FALSE,prot, maxprot,(flags & MAP_SHARED) ?VM_INHERIT_SHARE :VM_INHERIT_DEFAULT);/* If a non-binding address was specified for this anonymous* mapping, retry the mapping with a zero base* in the event the mapping operation failed due to* lack of space between the address and the map's maximum.*/if ((result == KERN_NO_SPACE) && ((flags & MAP_FIXED) == 0) && user_addr && (num_retries++ == 0)) {user_addr = vm_map_page_size(user_map);goto map_anon_retry;}/** 下面忽略了一部分代碼*/}其中又會(huì)調(diào)用.c內(nèi)部的ect,而該方法最終會(huì)在中依據(jù)對(duì)象進(jìn)行內(nèi)存分配:
// 下面這個(gè)只截了個(gè)頭 , 大概帶一下,我也沒調(diào)過代碼~/* *Routine:vm_map_enter * *Description: *Allocate a range in the specified virtual address map. *The resulting range will refer to memory defined by *the given memory object and offset into that object. * *Arguments are as defined in the vm_map call. */kern_return_tvm_map_enter(vm_map_tmap,vm_map_offset_t*address,/* IN/OUT */vm_map_size_tsize,vm_map_offset_tmask,intflags,vm_map_kernel_flags_tvmk_flags,vm_tag_talias,vm_object_tobject,vm_object_offset_toffset,boolean_tneeds_copy,vm_prot_tcur_protection,vm_prot_tmax_protection,vm_inherit_tinheritance)其中在分配過程中會(huì)對(duì)→作判斷,即最大的可分配空間 。
xnu上虛擬內(nèi)存的分配范圍
本來我只是觀察到蘋果在iOS15上增加了com.apple…–limit的能力聲明 。本著死馬當(dāng)活馬醫(yī)的想法,嘗試在新版本上添加該聲明以緩解一部分問題 。
結(jié)果偶然看到部分開發(fā)者提問:該能力可配合com.apple…–使用 ??吹胶笪乙幌伦臃磻?yīng)過來 , 順手搜到了今年二月國(guó)外有大佬做了相關(guān)的探索:

文章插圖
Size : An of on iOS
文章闡述了iOS的內(nèi)存管理機(jī)制和虛擬內(nèi)存空間分配在不同的機(jī)型上存在上限 , 代碼如下:
#define ARM64_MIN_MAX_ADDRESS (SHARED_REGION_BASE_ARM64 + SHARED_REGION_SIZE_ARM64 + 0x20000000) // end of shared region + 512MB for various purposesconst vm_map_offset_t min_max_offset = ARM64_MIN_MAX_ADDRESS; // end of shared region + 512MB for various purposesif (arm64_pmap_max_offset_default) {max_offset_ret = arm64_pmap_max_offset_default;} else if (max_mem > 0xC0000000) {max_offset_ret = min_max_offset + 0x138000000; // Max offset is 13.375GB for devices with > 3GB of memory} else if (max_mem > 0x40000000) {max_offset_ret = min_max_offset + 0x38000000;// Max offset is 9.375GB for devices with > 1GB and <= 3GB of memory} else {max_offset_ret = min_max_offset;}并且總結(jié)了一個(gè)上限值與機(jī)型表格:RAM
Space
> 3 GiB
15.375 GiB
7.375 GiB
– XS – 13
– iPad Air (4th )
– iPad Pro (12.9-inch), (10.5-inch), (11-inch)
> 1 GiB
11.375 GiB
3.375 GiB
– 6s – X, SE, XR
– iPad (5th ) – iPad (8th )
– iPad Air 2, iPad Air (3rd )
– iPad mini 4, iPad mini (5th )
– iPad Pro (9.7-inch)
(64位系統(tǒng)下1008字節(jié),32位系統(tǒng)下496)
內(nèi)存擴(kuò)展前失敗閾值約 * 1009 = 6.63 GB
內(nèi)存擴(kuò)展后失敗閾值約 * 1009 = 53.33 GB
當(dāng)然,在xnu的單元測(cè)試代碼中,也可找到j(luò)umbo mode相關(guān)的測(cè)試代碼 , 與上面的測(cè)試結(jié)果完全一致,即最多可分配53GB的空間 。
#define GB (1ULL * 1024 * 1024 * 1024)/* * This test expects the entitlement to be the enabling factor for a process to * allocate at least this many GB of VA space. i.e. with the entitlement, n GB * must be allocatable; whereas without it, it must be less. * This value was determined experimentally to fit on applicable devices and to * be clearly distinguishable from the default VA limit. */#define ALLOC_TEST_GB 53T_DECL(TESTNAME,"Verify that a required entitlement is present in order to be granted an extra-large ""VA space on arm64",T_META_NAMESPACE("xnu.vm"),T_META_CHECK_LEAKS(false)){int i;void*res;if (!dt_64_bit_kernel()) {T_SKIP("This test is only applicable to arm64");}T_LOG("Attemping to allocate VA space in 1 GB chunks.");for (i = 0; i < (ALLOC_TEST_GB * 2); i++) {res = mmap(NULL, 1 * GB, PROT_NONE, MAP_PRIVATE | MAP_ANON, 0, 0);if (res == MAP_FAILED) {if (errno != ENOMEM) {T_WITH_ERRNO;T_LOG("mmap failed: stopped at %d of %d GB allocated", i, ALLOC_TEST_GB);}break;} else {T_LOG("%d: %pn", i, res);}}#if defined(ENTITLED)T_EXPECT_GE_INT(i, ALLOC_TEST_GB, "Allocate at least %d GB of VA space", ALLOC_TEST_GB);#elseT_EXPECT_LT_INT(i, ALLOC_TEST_GB, "Not permitted to allocate %d GB of VA space", ALLOC_TEST_GB);#endif}可見,當(dāng)開啟com.apple…–時(shí),內(nèi)核的可分配空間確實(shí)有明顯提升 。上線效果與結(jié)論
從QQ瀏覽器的上線效果來看,JS相關(guān)的內(nèi)存分配Crash在14.0以上系統(tǒng)幾乎全部消失 。上線第一天App崩潰率環(huán)比下降接近50%,效果顯著 。
簡(jiǎn)單總結(jié):
蘋果很少在公開文檔中說明64位App在虛擬內(nèi)存使用上存在限制 。而且很多App也并沒有像瀏覽器內(nèi)一樣,為業(yè)務(wù)靈活性而選擇將hippy、等技術(shù)進(jìn)行大規(guī)模的組合使用,所以可能很多App其實(shí)并不會(huì)遇到虛擬內(nèi)存不足的情況 。上線效果也說明瀏覽器在混合開發(fā)的場(chǎng)景下,內(nèi)存優(yōu)化仍然存在很大的空間 。因?yàn)?僅能緩解虛擬內(nèi)存不足的情況,并不意味著App的物理內(nèi)存也得到增加,對(duì)FOOM的治理仍然需要持續(xù) 。鑒于司內(nèi)有不少的著名組件都會(huì)使用mmap機(jī)制進(jìn)行內(nèi)存管理軟件運(yùn)行出錯(cuò)誤代碼1,建議在使用相關(guān)組件時(shí) , 控制好mmap的大小 。如果有需要在 12 Pro、M1 iPad、M1上運(yùn)行應(yīng)用,并希望解放更多的物理內(nèi)存,建議增加com.apple…–limit的能力聲明,實(shí)測(cè)在 13 Pro下可以增加1GB的可用物理內(nèi)存 。和類似框架在項(xiàng)目中使用較多的,建議需要考慮多個(gè)的復(fù)用,減少創(chuàng)建重復(fù)內(nèi)容,司內(nèi)外都有實(shí)踐證明該措施十分有效 。對(duì)于一類的內(nèi)存優(yōu)化 , 可翻閱的相關(guān)代碼 。vm在創(chuàng)建時(shí)允許外部傳參控制vm行為,包括:old heap size、leak vm等 。合適的參數(shù)可比較有效控制內(nèi)存占用 。
以上源碼相關(guān)的內(nèi)容僅個(gè)人閱讀理解,如有錯(cuò)誤請(qǐng)指出 。
【一鍵釋放iOS 64位App潛力】本文到此結(jié)束,希望對(duì)大家有所幫助 。
- iOS16不能忍受的五大槽點(diǎn)匯總
- iOS17什么時(shí)候發(fā)布,支持什么機(jī)型
- iOS重大更新發(fā)布 ?prores視頻拍攝是什么意思
- 16 個(gè) iOS 16 隱藏功能盤點(diǎn)
- iPhone 12怎么截圖?
- 4個(gè)iOS使用小技巧
- 怎么找回iOS設(shè)備序列號(hào)和IMEI,趕緊看!
- 華為一鍵測(cè)速App值得擁有:免費(fèi)下載無廣告 安卓用戶獨(dú)享
- 總結(jié)了excel小技能之技巧篇
- 糖果媽媽:原創(chuàng)寶寶釋放了這些信號(hào),可能是進(jìn)入了“肛欲期”,家長(zhǎng)還應(yīng)認(rèn)真對(duì)待
