iOS程序啟動流程概覽
什么是Dyld? 它跟程序的啟動有什么關(guān)系?
Dyld是動態(tài)庫鏈接器。在程序啟動過程中負(fù)責(zé)加載所有庫和可執(zhí)行文件。在此過程中完成對這些庫和可執(zhí)行文件的符號重定向(Rebase)和符號綁定(Binding)等操作。接下來我們可以通過配置xcode的環(huán)境變量大致查看App啟動流程。
打印iOS程序啟動流程
iOS APP的啟動流程可以通過在xcode的Edit Scheme ->Run->Arguments -> Environment Variables中加入環(huán)境變量DYLD_PRINT_STATISTICS打印出來:
DYLD_PRINT_STATISTICS.png
運(yùn)行APP,打印結(jié)果如下:
Total pre-main time: 31.70 milliseconds (100.0%)
dylib loading time: 27.29 milliseconds (86.0%)
rebase/binding time: 411015771.5 seconds (61552300.3%)
ObjC setup time: 78.12 milliseconds (246.4%)
initializer time: 74.97 milliseconds (236.4%)
slowest intializers :
libSystem.B.dylib : 6.10 milliseconds (19.2%)
libBacktraceRecording.dylib : 7.03 milliseconds (22.1%)
libobjc.A.dylib : 1.69 milliseconds (5.3%)
libMainThreadChecker.dylib : 57.82 milliseconds (182.3%)
從結(jié)果可以看出四個階段的名稱以及它們對應(yīng)的耗時。
dylib loading 階段是動態(tài)鏈接庫dylib加載動態(tài)庫的階段,包括系統(tǒng)動態(tài)庫和我們自己的動態(tài)庫。
rebase/binding這個階段實(shí)際就是兩個操作rebase和binding。rebase就是內(nèi)部符號偏移修正,binding是外部符號綁定。
ObjC setup這一階段主要是OC類相關(guān)的事務(wù),比如類的注冊,category、protocol的讀取等等。
intializers 程序的初始化,包括所依賴的動態(tài)庫的初始化。在這期間會調(diào)用 Objc 類的 + load 函數(shù),調(diào)用 C++ 中帶有constructor 標(biāo)記的函數(shù)等。
上面打印出的這四個階段還是比較粗略的,實(shí)際上dyld在啟動過程中是比較復(fù)雜的。如果將上面的DYLD_PRINT_STATISTICS換成DYLD_PRINT_STATISTICS_DETAILS,打印得會更加詳細(xì):
total time: 1.3 seconds (100.0%)
total images loaded: 398 (392 from dyld shared cache)
total segments mapped: 21, into 415 pages
total images loading time: 944.18 milliseconds (68.5%)
total load time in ObjC: 80.88 milliseconds (5.8%)
total debugger pause time: 801.87 milliseconds (58.2%)
total dtrace DOF registration time: 0.00 milliseconds (0.0%)
total rebase fixups: 16,279
total rebase fixups time: 7.47 milliseconds (0.5%)
total binding fixups: 567,346
total binding fixups time: 275.57 milliseconds (20.0%)
total weak binding fixups time: 0.03 milliseconds (0.0%)
total redo shared cached bindings time: 459.30 milliseconds (33.3%)
total bindings lazily fixed up: 0 of 0
total time in initializers and ObjC +load: 68.67 milliseconds (4.9%)
libSystem.B.dylib : 6.04 milliseconds (0.4%)
libBacktraceRecording.dylib : 7.01 milliseconds (0.5%)
libMainThreadChecker.dylib : 52.05 milliseconds (3.7%)
total symbol trie searches: 1378030
total symbol table binary searches: 0
total images defining weak symbols: 50
total images using weak symbols: 110
從打印結(jié)果可以看出,實(shí)際上也是上面四個階段的擴(kuò)展。比如說dyld loading包含了images loaded(共享緩存)和images loading等。這幾個階段都是通過dyld來完成的。那dyld到底具體做些什么呢?接下來就通過dyld源碼來分析app啟動過程dyld到底做了哪些事情。
斷點(diǎn)查看啟動時dyld的函數(shù)調(diào)用棧,查找源碼位置
首先我們在+load方法打一個斷點(diǎn)。然后通過函數(shù)調(diào)用棧找出啟動過程中dyld都執(zhí)行了那些函數(shù):
dyld_start.jpg
可以看到APP啟動時dyld中調(diào)用函數(shù)的流程是_dyld_start -> bootstrap::start -> dyld:: _main, 順著流程分別找它們的實(shí)現(xiàn):
- _dyld_start
_dyld_start.jpg
這里面都是匯編代碼,根據(jù)注釋大概意思是一些app相關(guān)的準(zhǔn)備工作.根據(jù)注釋可知會調(diào)用bootstrap類的start方法,跟調(diào)用棧是一致的,因此可以直接進(jìn)入到bootstrap::start中。
- bootstrap::start
bootstrap_start.png
圖中紅圈1:這個方法的rebaseDyld是dyld完成自身重定位的方法。首先dyld本身也是一個動態(tài)庫。對于普通動態(tài)庫,符號重定位可以由dyld來加載鏈接來完成,但是dyld的重定位誰來做?只能是它自身完成。這就是為什么會有rebaseDyld的原因,它其實(shí)是在對自身進(jìn)行重定位,只有完成了自身的重定位它才能使用全局變量和靜態(tài)變量。
圖中紅圈2:部分就是進(jìn)入了 dyld:: _main函數(shù)了。dyld在app啟動中做的主要工作基本上都是在這里完成的。
-
dyld:: _main
App啟動過程中Dyld的主要工作都是在這個dyld:: _main函數(shù)中完成的。函數(shù)中代碼比較多,現(xiàn)在大概整理出如下流程:
1、環(huán)境變量配置
2、加載共享緩存
3、實(shí)例化主程序,并添加到allImages列表中;
4、加載動態(tài)庫插入的動態(tài)庫,并添加到allImages列表中;
5、鏈接主程序(完成Rebase/Binding/weakBind)
6、主程序初始化(ObjC setup/intializers是在這一過程進(jìn)行的)
7、查找并返回主程序的main函數(shù)地址
APP啟動流程詳解
1、環(huán)境變量配置
環(huán)境變量是存在于系統(tǒng)中的一些共用數(shù)據(jù),任何程序都可以訪問。通常來說,環(huán)境變量存儲的都是一些系統(tǒng)的公共信息,例如系統(tǒng)搜索路徑,當(dāng)前OS版本等。
// 主程序文件頭部信息
sMainExecutableMachHeader = mainExecutableMH;
// 隨機(jī)偏移值A(chǔ)SLR
sMainExecutableSlide = mainExecutableSlide;
setContext(mainExecutableMH, argc, argv, envp, apple);
2、共享緩存
當(dāng)我們構(gòu)建一個APP的時候,會鏈接各種各樣的庫。這些庫同樣又會依賴其他的一些框架和動態(tài)鏈接庫。于是要加載的動態(tài)鏈接庫會非常多。同樣非獨(dú)立的符號也非常多。這里就會有成千上萬的符號要確定。這個工作將會話費(fèi)很多時間——幾秒鐘。 為了優(yōu)化這個過程,OS X和iOS上動態(tài)鏈接器使用了一個共享緩存,在/var/db/dyld/。對于每一種架構(gòu),操作系統(tǒng)有一個單獨(dú)的文件包含了絕大多數(shù)的動態(tài)鏈接庫,這些庫已經(jīng)互相連接并且符號都已經(jīng)確定。當(dāng)一個Mach-o文件被加載的時候,動態(tài)鏈接器會首先檢查共享緩存,如果存在相應(yīng)的庫就用。每一個進(jìn)程都把這個共享緩存映射到了自己的地址空間中。這個方法優(yōu)化了OS X和iOS上程序的加載時間。以下是讀取緩存的代碼:
// load shared cache
checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);
if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion ) {
mapSharedCache();
}
3、實(shí)例化主程序
初始化一個ImageLoader類型的主程序sMainExecutable。后面的主程序相關(guān)的操作包括初始化都是通過這個對象來完成的。mainExecutableMH是mach-o文件頭部信息,mainExecutableSlide主程序隨機(jī)偏移值,sExecPath程序的路徑。初始化之后檢查應(yīng)用簽名。
//
// instantiate ImageLoader for main executable
// instantiate ImageLoader for main executable
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
gLinkContext.mainExecutable = sMainExecutable;
gLinkContext.mainExecutableCodeSigned = hasCodeSignatureLoadCommand(mainExecutableMH);
4、加載插入的動態(tài)庫
這里會遍歷所有插入的動態(tài)庫(dylib),然后加載。
// load any inserted libraries
if ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib)
loadInsertedDylib(*lib);
}
5、鏈接主程序
這一步主要實(shí)現(xiàn)過程如下:
5.1、主程序及其依賴的動態(tài)庫的Rebase
5.2、插入動態(tài)庫的Rebase
5.3、主程序及其依賴的動態(tài)庫的Binding
5.4、插入動態(tài)庫的Binding
5.5、弱綁定weakBind
備注:這里的recursiveBind和recursiveRebase都是遞歸的,所以會自底向上地分別調(diào)用 doRebase() 和 doBind() 方法,這樣被依賴的動態(tài)庫總是先于依賴它的動態(tài)庫執(zhí)行 rebase 和 binding。因為只有所依賴的子動態(tài)庫rebase和binding之后,符號地址才能被確定,這時候主動態(tài)庫再進(jìn)行binding就可以直接映射到相應(yīng)的子動態(tài)庫的符號地址。
-
5.1、主程序及其依賴的動態(tài)庫的Rebase
主程序調(diào)用link函數(shù)進(jìn)行鏈接。其中重定向rebase就是在這一步完成的,過程是link->recursiveRebaseWithAccounting->recursiveRebase。recursiveRebase函數(shù)會遞歸對主程序及其所依賴的庫和執(zhí)行文件進(jìn)行符號重定向(Rebase)。
// link main executable
gLinkContext.linkingMainExecutable = true;
link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
這其中雖然調(diào)用了link函數(shù),但是這里主程序link函數(shù)中并沒有調(diào)用recursiveBindWithAccounting函數(shù)。因為link函數(shù)內(nèi)部判斷l(xiāng)inkingMainExecutable為false才執(zhí)行recursiveBindWithAccounting,而此時linkingMainExecutable為true。以下是link函數(shù)的主要實(shí)現(xiàn):
t2 = mach_absolute_time();
this->recursiveRebaseWithAccounting(context);
context.notifyBatch(dyld_image_state_rebased, false);
t3 = mach_absolute_time();
if ( !context.linkingMainExecutable )
this->recursiveBindWithAccounting(context, forceLazysBound, neverUnload);
t4 = mach_absolute_time();
if ( !context.linkingMainExecutable ) this ->weakBind(context);
t5 = mach_absolute_time();
-
5.2、插入動態(tài)庫的Rebase
鏈接插入的動態(tài)庫,跟上一步一樣都是調(diào)用link函數(shù)。
// link any inserted libraries
// do this after linking main executable so that any dylibs pulled in by inserted
// dylibs (e.g. libSystem) will not be in front of dylibs the program uses
if ( sInsertedDylibCount > 0 ) {
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
link(image, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
image->setNeverUnloadRecursive();
}
if ( gLinkContext.allowInterposing ) {
// only INSERTED libraries can interpose
// register interposing info after all inserted libraries are bound so chaining works
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
image->registerInterposing(gLinkContext);
}
}
}
-
5.3、主程序及其依賴的動態(tài)庫的Binding
調(diào)用函數(shù)recursiveBindWithAccounting會遞歸對主程序及其所依賴的動態(tài)庫進(jìn)行符號綁定(Binding)。
/ Bind and notify for the main executable now that interposing has been registered
uint64_t bindMainExecutableStartTime = mach_absolute_time();
sMainExecutable->recursiveBindWithAccounting(gLinkContext, sEnv.DYLD_BIND_AT_LAUNCH, true);
- 5.4、插入動態(tài)庫的Binding
// Bind and notify for the inserted images now interposing has been registered
if ( sInsertedDylibCount > 0 ) {
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
image->recursiveBind(gLinkContext, sEnv.DYLD_BIND_AT_LAUNCH, true);
}
}
-
5.5、弱綁定weakBind
插入的動態(tài)庫鏈接完成后就執(zhí)行主程序的弱綁定,之后就會把狀態(tài)linkingMainExecutable設(shè)置為false,表示主程序鏈接完成。
// <rdar://problem/12186933> do weak binding only after all inserted images linked
sMainExecutable->weakBind(gLinkContext);
gLinkContext.linkingMainExecutable = false;
6、主程序初始化
完成了rebase/binding之后就開始了初始化工作。通過調(diào)用函數(shù) initializeMainExecutable進(jìn)行初始化:
// run all initializers
initializeMainExecutable();
主程序初始化初始是app啟動很重要的一個過程,在主程序初始化的過程中會同時初始化其所依賴的動態(tài)庫,這一過程完成了包括ObjC的初始化等工作。+load和C++構(gòu)造函數(shù)也是在這一過程中完成的。
initializeMainExecutable函數(shù)的初始流程大概整理如下:
dyld_sim`ImageLoader::runInitializers
dyld_sim`ImageLoader::processInitializers
dyld_sim`ImageLoader::recursiveInitialization
dyld_sim`dyld::notifySingle
libobjc.A.dylib`load_images
libobjc.A.dylib`call_load_methods
libobjc.A.dylib`call_class_loads
+ load
libobjc.A.dylib`call_category_loads
+ load
/*備注:這里面可以看到,先調(diào)用所有class的+load再調(diào)用category的。因此我們本類的+load方法總是先于category被調(diào)用。*/
/**下面是調(diào)用C++構(gòu)造函數(shù)的流程*/
dyld_sim`ImageLoaderMachO::doInitialization
dyld_sim`ImageLoaderMachO::doModInitFunctions
__attribute__((constructor)) void func(void)
主程序在初始化時是先遞歸調(diào)用其所依賴的動態(tài)庫完成初始化然后在完成自身初始化。在上面的函數(shù)流程中l(wèi)oad_images函數(shù)是在libobjc.A.dylib動態(tài)庫里面的,它是在程序初始化之前被注冊到dyld里面,等到初始化的時候執(zhí)行,完成libobjc的映射,非懶加載類就是在這一過程中被加載并調(diào)用+load方法的。以下是initializeMainExecutable源碼如下:
void initializeMainExecutable()
{
// record that we've reached this step
gLinkContext.startedInitializingMainExecutable = true;
// run initialzers for any inserted dylibs
ImageLoader::InitializerTimingList initializerTimes[allImagesCount()];
initializerTimes[0].count = 0;
const size_t rootCount = sImageRoots.size();
if ( rootCount > 1 ) {
for(size_t i=1; i < rootCount; ++i) {
sImageRoots[i]->runInitializers(gLinkContext, initializerTimes[0]);
}
}
// run initializers for main executable and everything it brings up
sMainExecutable->runInitializers(gLinkContext, initializerTimes[0]);
// register cxa_atexit() handler to run static terminators in all loaded images when this process exits
if ( gLibSystemHelpers != NULL )
(*gLibSystemHelpers->cxa_atexit)(&runAllStaticTerminators, NULL, NULL);
// dump info if requested
if ( sEnv.DYLD_PRINT_STATISTICS )
ImageLoader::printStatistics((unsigned int)allImagesCount(), initializerTimes[0]);
if ( sEnv.DYLD_PRINT_STATISTICS_DETAILS )
ImageLoaderMachO::printStatisticsDetails((unsigned int)allImagesCount(), initializerTimes[0]);
}
通過源碼分析:首先是遍歷插入的動態(tài)庫進(jìn)行初始化,然后就是主程序的初始化。到這里程序的初始化就算完成了,這一階段其實(shí)就是完成了我們之前打印的ObjC setUp 和initializers兩個階段。主程序初始化完成之后就進(jìn)入,dyld就通知各方主程序準(zhǔn)備要調(diào)用main函數(shù)了,然后開始尋找主程序的main函數(shù)入口地址返回。
7、找到主程序的main函數(shù)地址并返回
完成主程序初始化之后,dyld就可以從主程序找到入口main函數(shù),然后返回。
// main executable uses LC_UNIXTHREAD, dyld needs to let "start" in program set up for main()
result = (uintptr_t)sMainExecutable->getEntryFromLC_UNIXTHREAD();
另外程序啟動過程中一些重要函數(shù)的解析
通過load_images作為符號斷點(diǎn)查看調(diào)用棧,可以得到更詳細(xì)的調(diào)用棧:
load_image斷點(diǎn).png
load_images調(diào)用棧.png
這個過程中涉及到幾個重要的動態(tài)庫和方法,主要有
動態(tài)庫:
libSystem.B.dylib,系統(tǒng)基礎(chǔ)動態(tài)庫,其他動態(tài)庫的初始化必須在他之后。其初始化函數(shù)是libSystem_initializer;
libdispatch.dylib,GCD所在的庫,其初始化函數(shù)是libdispatch_init;
libobjc.dylib,OC代碼庫,其初始化函數(shù)是_objc_init 。
幾個重要的函數(shù):
_objc_init;
_dyld_objc_notify_register;
map_images;
load_images;
_objc_init
_objc_init是libobjc.A.dylib的初始化函數(shù),其源碼如下:
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;
// fixme defer initialization until an objc-using image is found?
environ_init();//環(huán)境變量相關(guān)初始化,逼比如我們之前添加的打印程序啟動的環(huán)境變量就是在這里面處理的
tls_init();
static_init();// static_init() Run C++ static constructor functions. libc calls _objc_init() before dyld would call our static constructors, so we have to do it ourselves.
runtime_init(); // 類表allocatedClasses.init()和unattachedCategories.init(32)看似跟category相關(guān)的表
exception_init();//異常處理回調(diào)函數(shù)初始化
cache_init();
_imp_implementationWithBlock_init();
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
#if __OBJC2__
didCallDyldNotifyRegister = true;
#endif
}
總結(jié)如下:
environ_init()讀取影響運(yùn)行時的環(huán)境變量。如果需要,還可以打印環(huán)境變量幫助。
tls_init()關(guān)于線程key的綁定 - 比如每條線程數(shù)據(jù)的析構(gòu)函數(shù)。
static_init()有注釋可知運(yùn)行C++靜態(tài)析構(gòu)函數(shù)。在dyld調(diào)用我們的靜態(tài)構(gòu)造函數(shù)之前,libc會調(diào)用_objc_init(), 因此我們必須自己調(diào)用。
runtime_init()類表allocatedClasses.init()和unattachedCategories.init(32)看似跟category相關(guān)的表,這個在后面類的
imp_implementationWithBlock_init(void)啟動回調(diào)機(jī)制。通常不會這么做。因為所有的初始話都是惰性的,但是對于某些進(jìn)程,我們會迫不及待地加載trampolines dylib。
_dyld_objc_notify_register向dyld注冊監(jiān)聽函數(shù)。以便在鏡像文件映射、取消映射和初始化objc時調(diào)用。Dyld將使用包含objc-image-info的鏡像文件的數(shù)組回調(diào)給map_images函數(shù)。
runtime_init()運(yùn)行時環(huán)境初始化,里面主要是類表allocatedClasses 和unattachedCategories
exception_init()初始化ibobjc的異常處理系統(tǒng)
_dyld_objc_notify_register
函數(shù)_dyld_objc_notify_register是負(fù)責(zé)向dyld注冊相關(guān)的回調(diào)函數(shù),這些函數(shù)會在合適的時機(jī)dyld會通知調(diào)用,這函數(shù)就是它的三個參數(shù)load_images、map_images和unmap_image。其實(shí)現(xiàn)是在dlyd中的:
void _dyld_objc_notify_register(_dyld_objc_notify_mapped mapped,
_dyld_objc_notify_init init,
_dyld_objc_notify_unmapped unmapped)
{
dyld::registerObjCNotifiers(mapped, init, unmapped);
}
void registerObjCNotifiers(_dyld_objc_notify_mapped mapped, _dyld_objc_notify_init init, _dyld_objc_notify_unmapped unmapped)
{
// record functions to call
sNotifyObjCMapped = mapped;
sNotifyObjCInit = init;
sNotifyObjCUnmapped = unmapped;
// call 'mapped' function with all images mapped so far
try {
notifyBatchPartial(dyld_image_state_bound, true, NULL, false, true);
}
catch (const char* msg) {
// ignore request to abort during registration
}
// <rdar://problem/32209809> call 'init' function on all images already init'ed (below libSystem)
for (std::vector<ImageLoader*>::iterator it=sAllImages.begin(); it != sAllImages.end(); it++) {
ImageLoader* image = *it;
if ( (image->getState() == dyld_image_state_initialized) && image->notifyObjC() ) {
dyld3::ScopedTimer timer(DBG_DYLD_TIMING_OBJC_INIT, (uint64_t)image->machHeader(), 0, 0);
(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
}
}
}
可以看到dyld會把這三個函數(shù)緩存起來,以方便在合適的時機(jī)調(diào)用它們。
map_images(ObjC setUp)
函數(shù)map_images是在libobjc庫中實(shí)現(xiàn)的,是在libobjc初始化的時候通過_dyld_objc_notify_register向dlyd注冊,用于監(jiān)聽鏡像文件的映射。OC相關(guān)的鏡像文件在完成rebase/binding之后,就會發(fā)出一個通知去調(diào)用map_images。但是有些時候鏡像文件是在libobjc初始化之前完成rebase/binding,這時候還沒有map_images這個函數(shù)。所以在libobjc初始化并調(diào)用 _dyld_objc_notify_register向dyld注冊的map_images時候,它會去把已經(jīng)完成rebase/binding的鏡像文件通過調(diào)用map_images進(jìn)行OC代碼相關(guān)的映射映射。所以map_images的調(diào)用是在rebase/binding之后,在動態(tài)庫(鏡像文件)調(diào)用initializer初始化之前的。函數(shù)map_images主要完成動態(tài)庫中的類的映射、初始化等操作。也就是我們前面打印的ObjC setUp階段。map_images通過鏡像文件的讀取類相關(guān)的信息進(jìn)行初始化,然后保存在類表中。
在完成類的映射,并完成非懶加載類的加載之后就可以調(diào)用OC相關(guān)的動態(tài)庫的初始化方法initializer了。initializer完成之后就會調(diào)用load_images函數(shù)。
load_images
load_images函數(shù)是在OC相關(guān)動態(tài)庫完成初始化之后調(diào)用的,在這期間,這里首先會加載非懶加載類的category,然后調(diào)用所有已經(jīng)加載的class和category的+load方法(點(diǎn)擊了解類的加載流程):
void
load_images(const char *path __unused, const struct mach_header *mh)
{
if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
didInitialAttachCategories = true;
loadAllCategories();
}
// Return without taking locks if there are no +load methods here.
if (!hasLoadMethods((const headerType *)mh)) return;
recursive_mutex_locker_t lock(loadMethodLock);
// Discover load methods
{
mutex_locker_t lock2(runtimeLock);
prepare_load_methods((const headerType *)mh);
}
// Call +load methods (without runtimeLock - re-entrant)
call_load_methods();
}
void call_load_methods(void)
{
static bool loading = NO;
bool more_categories;
loadMethodLock.assertLocked();
// Re-entrant calls do nothing; the outermost call will finish the job.
if (loading) return;
loading = YES;
void *pool = objc_autoreleasePoolPush();
do {
// 1. Repeatedly call class +loads until there aren't any more
while (loadable_classes_used > 0) {
call_class_loads();
}
// 2. Call category +loads ONCE
more_categories = call_category_loads();
// 3. Run more +loads if there are classes OR more untried categories
} while (loadable_classes_used > 0 || more_categories);
objc_autoreleasePoolPop(pool);
loading = NO;
}





