แก้ ITMS-90338 Non-public API usage ใน Expo
โดย Mehdi Azizi
เผยแพร่เมื่อ , 9 นาที
ITMS-90338 คือ build ของคุณอ้างถึงเมธอด ฟังก์ชัน หรือคลาสที่ Apple ถือว่าเป็นของภายใน ให้หา dependency ต้นเหตุ อัปเดต แล้ว build ใหม่
ITMS-90338 คือการตรวจสอบอัตโนมัติที่ Apple ทำกับ build ของคุณ ข้อความนี้บอกว่าไบนารีของคุณอ้างถึง API ที่ Apple ถือว่าเป็น API ภายใน (private) ได้แก่ เมธอด Objective-C ฟังก์ชัน หรือคลาส วิธีแก้คือ หาไฟล์ที่อีเมลระบุ ไล่ย้อนไปหา dependency ที่มีชื่อนั้นอยู่ อัปเดตมันหรือเปลี่ยนชื่อเมธอดของคุณ แล้ว build ใหม่
ถ้าใช้ Expo ชื่อที่ถูกอ้างถึงแทบทุกครั้งมาจากไลบรารี native น้อยครั้งมากที่จะมาจาก JavaScript ของคุณ เมื่อวันที่ 15 ตุลาคม 2026 ผมรันคำสั่งวิเคราะห์ในคู่มือนี้กับ build สำหรับ simulator ของ Solys แอปนาฬิกาปลุกที่มีมินิเกมของผม (Expo SDK 57) และกับ iOS SDK ของ Xcode 26.2 ต่อไปนี้คือวิธีอ่านอีเมล หาตัวการ และแยกแยะ false positive
ข้อผิดพลาด ITMS-90338 หมายความว่าอะไร?
ข้อความมีหัวเรื่องว่า "ITMS-90338: Non-public API usage" และมีสามแบบ ขึ้นอยู่กับสิ่งที่ Apple เจอในไบนารีของคุณ:
- "references non-public selectors": ชื่อเมธอด Objective-C เช่น
boldFontOfSize: - "references non-public symbols": ฟังก์ชัน ซึ่งเขียนโดยมีขีดล่างนำหน้า เช่น
__dyld_get_shared_cache_range - "contains or inherits from non-public classes": ชื่อคลาส
แต่ละแบบจะให้ path มา ตามด้วยรายชื่อ ข้อความยังให้คำแนะนำสองข้อ คือ เปลี่ยนชื่อเมธอดของคุณถ้าชื่อตรงกับ API ภายใน และเอา static library ที่มีชื่อเหล่านั้นออก อีเมลฉบับหนึ่งเมื่อกันยายน 2026 ยังแนะนำ xcrun swift-demangle เพื่อแปลงชื่อ Swift ที่ถูกเข้ารหัสให้อ่านออก
กฎเบื้องหลังการตรวจนี้คือ guideline 2.5.1: แอปใช้ได้เฉพาะ API สาธารณะเท่านั้น สัญญาอนุญาตของ Apple Developer Program ก็ย้ำไว้ในข้อ 3.3.1 ว่าห้องเรียก API ภายในใดๆ การรีวิวโดยคนตามกฎนี้มีอธิบายในคู่มือ guideline 2.5.1 ส่วนบทความนี้จะพูดถึงข้อผิดพลาดตอนส่ง build
เหมือนข้อผิดพลาด ITMS อื่นๆ คือไม่มีใครเปิดแอปของคุณเลย Apple วิเคราะห์ไบนารีระหว่างประมวลผล build Apple ไม่ได้เผยแพร่รายชื่อ API ภายในใดๆ ชื่อในอีเมลของคุณคือรายชื่อเดียวที่เชื่อถือได้
อ่าน path: เป็น SDK หรือไฟล์ executable ของคุณ?
path ที่เขียนไว้ก่อนรายชื่อบอกคุณว่าต้องค้นในไฟล์ไหน
Frameworks/Name.framework/Name: ชื่อนั้นอยู่ใน dynamic framework ที่ฝังมากับแอป ก็คือใน SDK ตัวใดตัวหนึ่ง ใน build ของ Solys โฟลเดอร์นี้มี 14 framework ได้แก่ Hermes, React Native, โมดูล Expo ที่ส่งมาแบบคอมไพล์แล้ว และไลบรารีรูปภาพ SDWebImage- ชื่อแอปของคุณเพียงอย่างเดียว: ชื่อนั้นอยู่ใน executable หลัก กับ Expo ไฟล์นี้รวมโค้ด native ของคุณและ pod ที่คอมไพล์จากซอร์ส ซึ่งลิงก์แบบ static เป็นค่าเริ่มต้น ไลบรารี native ตัวไหนก็อาจเป็นต้นเหตุได้
แผนผัง: จากชื่อที่ถูกอ้างถึงไปจนถึงการแก้
ไล่ดูทีละแถวตามลำดับ แถวแรกที่ตรงกับกรณีของคุณคือแนวทาง
| สิ่งที่คุณเห็น | ค้นที่ไหน | วิธีแก้ |
|---|---|---|
ชื่อขึ้นต้น $s หรือ _$s | xcrun swift-demangle | เริ่มแผนผังใหม่ |
| หาไม่เจอใน IPA ของคุณ | grep ทั้ง bundle | ส่ง build ใหม่ |
| ชื่อสาธารณะบน iOS | header ของ iOS SDK | ส่ง build ใหม่ |
API_UNAVAILABLE(ios) | header ของ iOS SDK | อัปเดตแพ็กเกจ |
อยู่ใน Frameworks/X | SDK ที่ให้ X มา | อัปเดต SDK |
| selector ในแอปของคุณ | pod และโมดูล native | ใส่ prefix หรืออัปเดต |
symbol _name ในแอปของคุณ | nm -m แล้วดู pod | เปลี่ยนการเรียกใช้ |
| คลาสในแอปของคุณ | คลาสของคุณและ pod | ใส่ prefix ให้คลาส |
แถว "ส่ง build ใหม่" หมายถึง false positive ซึ่งจะอธิบายด้านล่าง
ขั้นที่ 1: เปิด build ที่ส่งไป
ทำงานกับไฟล์ที่ส่งให้ Apple จริงๆ ไม่ใช่กับโปรเจกต์ของคุณ ดาวน์โหลด IPA จากหน้า build บน expo.dev แล้วรันบน Mac:
unzip -q MyApp.ipa -d ipa
ls ipa/Payload/MyApp.app/Frameworks
otool -L ipa/Payload/MyApp.app/MyApp | grep PrivateFrameworks
บรรทัดที่สองแสดงรายการ framework ที่ฝังมา บรรทัดที่สามค้นหาลิงก์ตรงไปยัง framework ภายในของ Apple ซึ่งต้องไม่แสดงอะไรออกมา สำหรับ Solys otool -L ไม่เจอ framework ภายในเลย ทั้งใน executable และใน 14 framework
ขั้นที่ 2: หาไฟล์ที่มีชื่อนั้น
เริ่มแบบกว้างๆ ค้นชื่อที่ถูกอ้างถึง โดยตัดขีดล่างข้างหน้าออก ในทั้ง bundle:
grep -rl "dyld_get_shared_cache_range" ipa/Payload/MyApp.app
คำสั่งนี้แสดงไฟล์ที่มีข้อความนั้น จากนั้นค้นให้ละเอียดขึ้นตามประเภทชื่อ (selector, symbol, คลาส) ในไฟล์ที่เจอ ซึ่งเขียนแทนด้วย Binary:
strings -a Binary | grep -x "boldFontOfSize:"
nm -m Binary | grep "_dyld_get_shared_cache_range"
nm -m Binary | grep '_OBJC_CLASS_$_ClassName'
สำหรับ selector ให้ใช้ strings ไม่ใช่ nm ใน framework React ของ Solys nm แสดงเมธอดพร้อมคลาส เช่น -[RCTRootView initWithBridge:moduleName:initialProperties:] แต่ไม่เคยแสดง selector initWithBridge: เดี่ยวๆ ส่วน strings หาเจอ
สำหรับ symbol ให้อ่านท้ายบรรทัด ใน Solys nm -m แสดงเช่น (undefined) external __dyld_get_image_name (from libSystem) หมายความว่าไบนารีเรียกฟังก์ชันนี้โดยไม่ได้มีมันอยู่ข้างใน และ libSystem เป็นตัวให้มา ฟังก์ชันตัวนี้เป็นสาธารณะ ตามที่ขั้นถัดไปจะแสดง
ขั้นที่ 3: ตรวจว่าชื่อนั้นเป็น API ภายในจริงไหม
header ของ iOS SDK อธิบาย API สาธารณะไว้ การค้นไม่กี่วินาทีจะบอกได้ว่าชื่อนั้นอยู่ในนั้นไหม และใช้ได้บนแพลตฟอร์มไหน สำหรับ symbol ให้ตัดขีดล่างที่เพิ่มมาตอนคอมไพล์ออกก่อน: _SCDynamicStoreCreate จะกลายเป็น SCDynamicStoreCreate
SDK=$(xcrun --sdk iphoneos --show-sdk-path)
grep -rn "SCDynamicStoreCreate" "$SDK/usr/include" "$SDK"/System/Library/Frameworks/*/Headers
เมื่อวันที่ 15 ตุลาคม 2026 กับ iOS SDK 26.2 นี่คือผลของสี่ชื่อที่มาจากอีเมล ITMS-90338 จริง:
| ชื่อที่ถูกอ้างถึง | ใน iOS SDK | การตีความ |
|---|---|---|
| _dyld_get_shared_cache_range | ไม่มี | API ภายใน |
| boldFontOfSize: | ไม่มี | API ภายในหรือชื่อชนกัน |
| SCDynamicStoreCreate | API_UNAVAILABLE(ios) | macOS เท่านั้น |
| isMainFrame | property ของ WebKit | สาธารณะ |
ชื่อที่มีอยู่และใช้ได้บน iOS ชี้ไปที่ false positive ชื่อที่ไม่มีชี้ไปที่ API ภายในหรือชื่อชนกัน มีข้อจำกัดสองข้อ: API สาธารณะที่มีเฉพาะใน Swift จะไม่ปรากฏใน header เหล่านี้ และสำหรับ accessor อย่าง setDelegate: ให้ค้น property delegate แทน
ขั้นที่ 4: ไล่ย้อนไปหาแพ็กเกจ
ถ้า path ระบุ framework ชื่อของมันจะพาไปหา pod แล้วไปหาแพ็กเกจ npm:
grep -n "FrameworkName" ios/Podfile.lock
npm explain package-name
npm explain บอกว่า dependency ตัวไหนติดตั้งแพ็กเกจนั้นมา ถ้าคุณไม่เคยเพิ่มมันเอง
ถ้า path ระบุ executable ให้ค้นชื่อในซอร์ส native แล้วค้นใน pod:
grep -rl --include='*.m' --include='*.mm' --include='*.swift' --include='*.h' \
"boldFontOfSize" node_modules modules
grep -rl "boldFontOfSize" ios/Pods
ปรับรายชื่อโฟลเดอร์ตามโปรเจกต์ modules คือโฟลเดอร์ของโมดูล Expo แบบ local โฟลเดอร์ ios/Pods จะปรากฏหลังรัน npx expo prebuild -p ios ซึ่งติดตั้ง pod บน Mac ให้ด้วย ระวัง: คำสั่งนี้อาจสร้างโฟลเดอร์ ios ใหม่ ถ้าคุณแก้มันด้วยมือไว้ ให้ทำงานกับสำเนา
การค้นใน ios/Pods สำคัญพอๆ กับการค้นในซอร์ส ใน Solys โฟลเดอร์นี้มี 15 framework ที่ส่งมาแบบคอมไพล์แล้วในรูปแบบ .xcframework ได้แก่ โมดูล Expo, Hermes, React Native และ SDK ของบุคคลที่สาม การค้นแค่ไฟล์ซอร์สจะพลาดโค้ดของพวกนี้
สาเหตุที่พบบ่อย
SDK ของบุคคลที่สามที่เก่าเกินไปหรือใหม่เกินไป เมื่อกันยายน 2026 นักพัฒนา Flutter คนหนึ่งรายงานว่าโดนปฏิเสธเพราะ SDK ด้านความปลอดภัย: เวอร์ชัน 8.2.3 ของมันเรียก __dyld_get_shared_cache_range เวอร์ชัน 8.2.2 ผ่าน และผู้พัฒนา SDK แนะนำให้ย้อนกลับไปใช้ระหว่างรอการแก้ไข
โมดูล native ที่เรียก API ที่ไม่มีเอกสาร โมดูลของคุณเอง ปลั๊กอิน หรือไลบรารีเล็กๆ อาจใช้เมธอดภายในของ UIKit ที่เจอในฟอรัม การตรวจจะจับได้จากชื่อ
ชื่อชนกัน เมธอดหรือคลาสของคุณบังเอิญมีชื่อเหมือน API ภายในของ Apple ข้อความได้คาดกรณีนี้ไว้แล้วและขอให้คุณเปลี่ยนชื่อ Apple แนะนำให้ใส่ prefix ให้เมธอดที่เพิ่มเข้าไปในคลาสของ Apple เช่น ตัวพิมพ์เล็กสามตัวตามด้วยขีดล่าง
API ที่เป็นสาธารณะบน macOS แต่ไม่ใช่บน iOS ในปี 2024 แอป .NET MAUI ตัวหนึ่งโดนปฏิเสธเพราะ _SCDynamicStoreCreate Apple มีเอกสารของฟังก์ชันนี้จริง แต่สำหรับ macOS เท่านั้น และ header ของ iOS SDK ทำเครื่องหมายไว้ว่า API_UNAVAILABLE(ios, tvos, watchos)
แก้โดยไม่หาทางเลี่ยง
- อัปเดต dependency สำหรับแพ็กเกจที่ Expo ดูแล
npx expo install --checkจะแจ้งตัวที่ไม่ตรงกับ SDK ของคุณ ตามเอกสารของ Expo CLI สำหรับตัวอื่น ให้อ่าน changelog - ย้อนกลับไปเวอร์ชันก่อนหน้า ถ้าเวอร์ชันใหม่เป็นตัวที่ทำให้ชื่อนั้นโผล่มา และแจ้งผู้พัฒนาด้วย
- เปลี่ยนหรือเอาออก ไลบรารีที่ถูกทิ้งร้างแล้ว
- เปลี่ยนชื่อ เมธอดหรือคลาสของคุณ โดยใส่ prefix
อย่าซ่อนชื่อ การหั่น string หรือเรียกเมธอดผ่านทางอ้อมเป็นแค่การซ่อนการใช้งาน ไม่ใช่การแก้ มันยังผิดกฎ 2.5.1 และสัญญาอยู่ดี และบัญชีของคุณจะเป็นฝ่ายเสี่ยง
จากนั้น build ใหม่พร้อมหมายเลข build ใหม่ อัปเดตแบบ remote ไม่พอ เพราะชื่ออยู่ในโค้ด native ที่คอมไพล์แล้ว ก่อนส่ง ให้รันขั้นที่ 2 ซ้ำกับ IPA ใหม่ ชื่อนั้นต้องไม่ปรากฏอีก
เมื่อเป็น false positive
การตรวจของ Apple ก็ผิดพลาดได้บ้าง เมื่อวันที่ 6 มิถุนายน 2022 แอป React Native และ Flutter ได้รับ ITMS-90338 จากชื่อสาธารณะของ WebKit อย่าง setNavigationDelegate: หรือ isMainFrame แม้แต่ในอัปเดตที่แทบไม่เปลี่ยนอะไร วันต่อมา วิศวกรของ Apple เขียนว่าปัญหาฝั่งการตรวจสอบได้รับการแก้แล้ว และให้ส่ง build ใหม่ เธรดนี้ยังเก็บไว้ในรีโพ GitHub ของ Expo
ถ้าเจอสามสัญญาณนี้พร้อมกัน ก็น่าจะเป็น false positive:
- build คล้ายกับ build ล่าสุดที่ผ่าน
- ขั้นที่ 2 หาชื่อไม่เจอ หรือขั้นที่ 3 แสดงว่าเป็นชื่อสาธารณะบน iOS
- นักพัฒนาคนอื่นรายงานข้อผิดพลาดเดียวกันในวันเดียวกันบนฟอรัมของ Apple
ในกรณีนี้ ให้ดูสถานะบริการของ Apple แล้วส่ง build ใหม่พร้อมหมายเลขใหม่ ถ้ายังเจออยู่ ให้เขียนถึงฝ่ายสนับสนุนนักพัฒนาของ Apple พร้อมอีเมลฉบับเต็ม ส่วนชื่อชนกันนั้น Apple ไม่ถือว่าเป็น false positive: ให้เปลี่ยนชื่อ
ก่อนส่งครั้งถัดไป
ข้อผิดพลาด ITMS อื่นๆ ก็มีคู่มือเช่นกัน: ITMS-90725 เรื่องเวอร์ชัน Xcode, ITMS-91053 เรื่อง privacy manifest และ ITMS-90034 เรื่องลายเซ็น เมื่อ build ผ่านแล้ว ให้ติดตั้งผ่าน TestFlight ก่อนเข้ารีวิว
การโดน reviewer ปฏิเสธต้องใช้มุมมองที่กว้างกว่านี้: ขั้นตอนอยู่ในหน้าแอปโดน Apple ปฏิเสธ และเหตุผลที่พบบ่อยที่สุดอยู่ในทำไม Apple ถึงปฏิเสธแอป
ถ้าอยากตรวจความพร้อมก่อนส่ง Review Kit รวม checklist ของผมและ skill ของ Claude Code ที่ช่วยตรวจโปรเจกต์ของคุณ ถ้าการส่งติดขัดและคุณอยากให้คนช่วยดู จองเวลาหนึ่งชั่วโมงกับผม เราจะดูโปรเจกต์ของคุณด้วยกันผ่านวิดีโอคอลบนหน้าจอของคุณ และถ้าคุณกำลังเตรียมปล่อยแอปแรก ทำการวินิจฉัยฟรี: มันจะเรียงขั้นตอนให้เป็นลำดับ และส่ง checklist กันโดน Apple ปฏิเสธของผมให้คุณ
คำถามที่พบบ่อย
ITMS-90338 คือการโดน App Review ปฏิเสธหรือไม่?
ไม่ใช่ครับ มันคือการตรวจอัตโนมัติระหว่างประมวลผล build ก่อนที่คนจะรีวิว build ที่โดนปฏิเสธจะไม่เข้า TestFlight และไม่เข้ารีวิว ส่วน API ภายในที่ reviewer เจอทีหลังจะอยู่ภายใต้ guideline 2.5.1
แก้ ITMS-90338 ด้วยการอัปเดตแบบ remote ได้ไหม?
ไม่ได้ครับ ชื่อที่ถูกอ้างถึงอยู่ในโค้ด native ที่คอมไพล์อยู่ในไบนารี การอัปเดตแบบ remote เปลี่ยนได้แค่ JavaScript และ resource แก้ dependency แล้ว build ใหม่ จากนั้นส่งอีกครั้ง
ทำไม Apple ถึงอ้างถึงเมธอดที่ผมไม่เคยเขียน?
เพราะมันมาจากไลบรารีที่ฝังมา หรือมาจากชื่อชนกัน คือ dependency ตัวหนึ่งประกาศเมธอดที่มีชื่อเหมือน API ภายใน Apple อ่านชื่อที่อยู่ในไบนารี ไม่ได้อ่านซอร์สโค้ดของคุณ ไล่ย้อนไปหาแพ็กเกจด้วย grep และ npm explain
จะรู้ได้อย่างไรว่า API เป็นสาธารณะบน iOS?
ค้นชื่อของมันใน header ของ iOS SDK ซึ่ง xcrun --sdk iphoneos --show-sdk-path จะบอก path ให้ แล้วดูว่าใช้ได้บนแพลตฟอร์มไหน หน้าเอกสารของ Apple ก็ระบุไว้เช่นกัน ฟังก์ชันที่มีเอกสารสำหรับ macOS เท่านั้นไม่ถือว่าเป็นสาธารณะบน iPhone
ต้องเปลี่ยนหมายเลข build หลังแก้ไหม?
ต้องครับ ให้ EAS เพิ่มเลขให้อัตโนมัติ เพราะหมายเลขที่เคยส่งไปแล้วสำหรับเวอร์ชันเดียวกันอาจโดนปฏิเสธด้วย ITMS-90189

