www.eoeandroid.com首發,作者:Tigertang2@gmail.com 大家都知道啟動速度慢是智能作業系統的一個通病,Android也不例外,啟動速度大概在1分鐘左右,雖然日本有一個叫quick boot的一秒啟動android的產品,但是畢竟是旁門左道。所以從常規來提高android的啟動速度成了大家研究的重點,也是痛點。下面將初步研究的一下經驗跟大家分享一下。 首先看一下android系統的啟動流程: bootloader 引導程式 kernel 核心 init init初始化(這個大家都比較熟悉了,不要多說) loads several daemons and services, including zygote see /init.rc and init..rc
zygote
這個是佔用時間最多的,重點修理對象 preloads classes 裝載了一千多個類,媽呀!!! starts package manager 掃描package(下面詳細介紹)service manager start services (啟動多個服務)從實際的測試資料來看,有兩個地方時最耗時間的,一個是zygote的裝載一千多個類和初始化堆棧的過程,用了20秒左右。另一個是掃描 /system/app, /system/framework, /data/app, /data/app-private. 這幾個目錄下面的package用了大概10秒,所以我們重點能夠修理的就是這兩個老大的。 一、首先是調試工具的使用,可以測試哪些類和那些過程佔用了多少時間, 主要工具為 stopwatch Message loggers grabserial 參考http://elinux.org/Grabserial printk times 參考http://elinux.org/Printk_Times logcat Android內建
bootchart 參考http://elinux.org/Bootchart 和 http://elinux.org/Bootchart strace AOSP的一部分(Eclair及以上版本)
使用例子 在init.rc中為了調試zygote service zygote /system/bin/app_process -Xzygote /system/bin --zygote --start-system-server改為 service zygote /system/xbin/strace -tt -o/data/boot.strace /system/bin/app_process -Xzygote /system/bin --zygote --start-system-server
method tracer* ftrace* 詳細使用可看提供的文檔和網頁介紹 上面的工具如果不用詳細的分析不一定都用到,也可以使用logcat就可以,在代碼中加一點計算時間和一些類的調試資訊也可以達到很好效果。 二、zygote 裝載1千多個類 首先,我們可以添加一點調試資訊,以獲得具體轉載情況。 diff --git a/core/java/com/android/internal/os/ZygoteInit.java b/core/java/com/android/internal/os/ZygoteInit.java index 404c513..f2b573c 100644 --- a/core/java/com/android/internal/os/ZygoteInit.java +++ b/core/java/com/android/internal/os/ZygoteInit.java @@ -259,6 +259,8 @@ public class ZygoteInit { } else { Log.i(TAG, Preloading classes...); long startTime = SystemClock.uptimeMillis(); + long lastTime = SystemClock.uptimeMillis(); + long nextTime = SystemClock.uptimeMillis();
// Drop root perms while running static initializers. setEffectiveGroup(UNPRIVILEGED_GID); @@ -292,12 +294,24 @@ public class ZygoteInit { if (Config.LOGV) { Log.v(TAG, Preloading + line + ...); } + //if (count%5==0) { + // Log.v(TAG, Preloading + line + ...); + //} + Log.v(TAG, Preloading + line + ...); Class.forName(line); + nextTime = SystemClock.uptimeMillis(); + if (nextTime-lastTime >50) { + Log.i(TAG, Preloading + line + ... took + (nextTime-lastTime) + ms.); + } + lastTime = nextTime; + if (Debug.getGlobalAllocSize() > PRELOAD_GC_THRESHOLD) { if (Config.LOGV) { Log.v(TAG, GC at + Debug.getGlobalAllocSize()); } + Log.i(TAG, + GC at + Debug.getGlobalAllocSize()); runtime.gcSoftReferences(); runtime.runFinalizationSync(); Debug.resetGlobalAllocSize(); 上面+代表添加的代碼,這樣就可以很容易的得到在裝載類的過程中具體裝載了哪些類,耗費了多久。具體裝載的類在檔案platform/frameworks/base/ preloaded-classes 內容類別似: android.R$styleable android.accounts.AccountMonitor android.accounts.AccountMonitor$AccountUpdater android.app.Activity android.app.ActivityGroup android.app.ActivityManager$MemoryInfo$1 android.app.ActivityManagerNative android.app.ActivityManagerProxy android.app.ActivityThread android.app.ActivityThread$ActivityRecord android.app.ActivityThread$AppBindData android.app.ActivityThread$ApplicationThread android.app.ActivityThread$ContextCleanupInfo android.app.ActivityThread$GcIdler android.app.ActivityThread$H android.app.ActivityThread$Idler 而這個檔案是由檔案WritePreloadedClassFile.java中的WritePreloadedClassFile類自動產生 /** * Writes /frameworks/base/preloaded-classes. Also updates
* {@link LoadedClass#preloaded} fields and writes over compiled log file.
*/
public class WritePreloadedClassFile /** * Preload any class that take longer to load than MIN_LOAD_TIME_MICROS us. */ static final int MIN_LOAD_TIME_MICROS = 1250;//這個代表了裝載時間小於1250us即1.25ms的類將不予裝載,也許可以改這個參數減少一下類的裝載 //這裡可以看到什麼樣的類會被裝載
A:啟動必須裝載的類,比如系統級的類
B:剛才說的裝載時間大於1.25ms的類
C:被使用一次以上或被應用裝載的類
仔細看看篩選類的具體實現,可以協助我們認識哪些類比較重要,哪些可以去掉。 篩選規則是 第一 isPreloadable, /**Reports if the given class should be preloaded. */ public static boolean isPreloadable(LoadedClass clazz) { return clazz.systemClass && !EXCLUDED_CLASSES.contains(clazz.name); } 意思是指除了EXCLUDED_CLASSES包含的類之外的所有系統裝載的類。 EXCLUDED_CLASSES包含 /** * Classes which we shouldn't load from the Zygote. */ private static final Set EXCLUDED_CLASSES = new HashSet(Arrays.asList( // Binders android.app.AlarmManager, android.app.SearchManager, android.os.FileObserver, com.android.server.PackageManagerService$AppDirObserver, // Threads android.os.AsyncTask, android.pim.ContactsAsyncHelper, java.lang.ProcessManager )); 目前是跟Binders跟Threads有關的不會被預裝載。
第二 clazz.medianTimeMicros() > MIN_LOAD_TIME_MICROS裝載時間大於1.25ms。 第三 names.size() > 1 ,既是被processes一次以上的。 上面的都是指的system class,另外還有一些application class需要被裝載 規則是fromZygote而且不是服務 proc.fromZygote() && !Policy.isService(proc.name)
fromZygote指的除了com.android.development的zygote類 public boolean fromZygote() { return parent != null && parent.name.equals(zygote) && !name.equals(com.android.development); }
/除了常駐記憶體的服務 /** * Long running services. These are restricted in their contribution to the * preloader because their launch time is less critical. */ // TODO: Generate this automatically from package manager. private static final Set SERVICES = new HashSet(Arrays.asList( system_server, com.google.process.content, android.process.media, com.android.bluetooth, com.android.calendar, com.android.inputmethod.latin, com.android.phone, com.google.android.apps.maps.FriendService, // pre froyo com.google.android.apps.maps:FriendService, // froyo com.google.android.apps.maps.LocationFriendService, com.google.android.deskclock, com.google.process.gapps, android.tts ));
好了。要轉載的就是這些類了。雖然preloaded-classes是在下載源碼的時候已經確定了的,也就是對我們來說WritePreloadedClassFile類是沒用到的,我們可以做的就是在preloaded-classes檔案中,把不預裝載的類去掉,試了把所有類去掉,啟動確實很快跳過那個地方,但是啟動HOME的時候就會很慢了。所以最好的方法就是只去掉那些沒怎麼用到的,不過要小心處理。至於該去掉哪些,還在摸索,稍後跟大家分享。有興趣的朋友可以先把preloaded-classes這個檔案裡面全部清空,啟動快了很多,但在啟動apk的時候會慢了點。當然了,也可以把android相關的類全部去掉,剩下java的類,試過了也是可以提高速度。
三,系統服務初始化和package 掃描 在啟動系統服務的init2()時會啟動應用程式層(Java層)的所有服務。 public static void main(String[] args) { System.loadLibrary(android_servers); init1(args); //init1 初始化,完成之後會回調init2() }
在init2()中會啟動一個線程來啟動所有服務 public static final void init2() { Log.i(TAG, Entered the Android system server!); Thread thr = new ServerThread(); thr.setName(android.server.ServerThread); thr.start(); }
class ServerThread extends Thread { 。。。 public void run() { 。。。 關鍵服務: ServiceManager.addService(entropy, new EntropyService()); ServiceManager.addService(Context.POWER_SERVICE, power); context = ActivityManagerService.main(factoryTest); ServiceManager.addService(telephony.registry, new TelephonyRegistry(context));
PackageManagerService.main(context, factoryTest != SystemServer.FACTORY_TEST_OFF);//apk掃描的服務 ServiceManager.addService(Context.ACCOUNT_SERVICE, new AccountManagerService(context)); ContentService.main(context, factoryTest == SystemServer.FACTORY_TEST_LOW_LEVEL); battery = new BatteryService(context); ServiceManager.addService(battery, battery);
hardware = new HardwareService(context); ServiceManager.addService(hardware, hardware); AlarmManagerService alarm = new AlarmManagerService(context); ServiceManager.addService(Context.ALARM_SERVICE, alarm); ServiceManager.addService(Context.SENSOR_SERVICE, new SensorService(context));
WindowManagerService.main(context, power, factoryTest != SystemServer.FACTORY_TEST_LOW_LEVEL); ServiceManager.addService(Context.WINDOW_SERVICE, wm);
上面這些都是關鍵服務,不建議進行裁剪。
下面的這些不是很關鍵,可以進行裁剪,當是必須相應的修改framework部分的代碼,工作量比較大和複雜。我去掉了20個服務,大概需要相應修改大概20多個檔案。
statusBar = new StatusBarService(context); ServiceManager.addService(statusbar, statusBar);
ServiceManager.addService(clipboard, new ClipboardService(context));
imm = new InputMethodManagerService(context, statusBar); ServiceManager.addService(Context.INPUT_METHOD_SERVICE, imm);
ServiceManager.addService(netstat, new NetStatService(context));
connectivity = ConnectivityService.getInstance(context);
ServiceManager.addService(Context.CONNECTIVITY_SERVICE, connectivity); ServiceManager.addService(Context.ACCESSIBILITY_SERVICE, new AccessibilityManagerService(context));
notification = new NotificationManagerService(context, statusBar, hardware); ServiceManager.addService(Context.NOTIFICATION_SERVICE, notification);
ServiceManager.addService(mount, new MountService(context));
ServiceManager.addService(DeviceStorageMonitorService.SERVICE, new DeviceStorageMonitorService(context));
ServiceManager.addService(Context.LOCATION_SERVICE, new LocationManagerService(context));
ServiceManager.addService( Context.SEARCH_SERVICE, new SearchManagerService(context) );
if (INCLUDE_DEMO) { Log.i(TAG, Installing demo data...); (new DemoThread(context)).start(); } Intent intent = new Intent().setComponent(new ComponentName( com.google.android.server.checkin, com.google.android.server.checkin.CheckinService));
ServiceManager.addService(checkin, new FallbackCheckinService(context));
wallpaper = new WallpaperManagerService(context); ServiceManager.addService(Context.WALLPAPER_SERVICE, wallpaper);
ServiceManager.addService(Context.AUDIO_SERVICE, new AudioService(context));
headset = new HeadsetObserver(context);
dock = new DockObserver(context, power);
ServiceManager.addService(Context.BACKUP_SERVICE, new BackupManagerService(context));
ServiceManager.addService(Context.APPWIDGET_SERVICE, appWidget);
package 掃描部分,整個流程為所示:
最終的zip檔案(apk)讀取是在下面這兩個函數:
/* * Open the specified file read-only. We memory-map the entire thing and * close the file before returning. */ status_t ZipFileRO::open(const char* zipFileName) { int fd = -1; off_t length; assert(mFileMap == NULL);
LOGD(opening zip '%s' , zipFileName); /* * Open and map the specified file. */ fd = ::open(zipFileName, O_RDONLY); if (fd < 0) { LOGW(Unable to open zip '%s': %s , zipFileName, strerror(errno)); return NAME_NOT_FOUND; }
length = lseek(fd, 0, SEEK_END); if (length < 0) { close(fd); return UNKNOWN_ERROR; }
mFileMap = new FileMap();
if (mFileMap == NULL) { close(fd); return NO_MEMORY; } if (!mFileMap->create(zipFileName, fd, 0, length, true)) { LOGW(Unable to map '%s': %s , zipFileName, strerror(errno)); close(fd); return UNKNOWN_ERROR; }
mFd = fd; /* * Got it mapped, verify it and create data structures for fast access. */ if (!parseZipArchive()) { mFileMap->release(); mFileMap = NULL; return UNKNOWN_ERROR; }
LOGD(done opening zip ); return OK; } /* * Parse the Zip archive, verifying its contents and initializing internal * data structures. */ bool ZipFileRO::parseZipArchive(void) {
#define CHECK_OFFSET(_off) { if ((unsigned int) (_off) >= maxOffset) { LOGE(ERROR: bad offset %u (max %d): %s , (unsigned int) (_off), maxOffset, #_off); goto bail; } } const unsigned char* basePtr = (const unsigned char*)mFileMap->getDataPtr(); const unsigned char* ptr; size_t length = mFileMap->getDataLength(); bool result = false; unsigned int i, numEntries, cdOffset; unsigned int val;
/* * The first 4 bytes of the file will either be the local header * signature for the first file (kLFHSignature) or, if the archive doesn't * have any files in it, the end-of-central-directory signature * (kEOCDSignature). */ val = get4LE(basePtr); if (val == kEOCDSignature) { LOGI(Found Zip archive, but it looks empty ); goto bail; } else if (val != kLFHSignature) { LOGV(Not a Zip archive (found 0x%08x) , val); goto bail; } /* * Find the EOCD. We'll find it immediately unless they have a file * comment. */ ptr = basePtr + length - kEOCDLen; while (ptr >= basePtr) { if (*ptr == (kEOCDSignature & 0xff) && get4LE(ptr) == kEOCDSignature) break; ptr--; } if (ptr < basePtr) { LOGI(Could not find end-of-central-directory in Zip ); goto bail; } /* * There are two interesting items in the EOCD block: the number of * entries in the file, and the file offset of the start of the * central directory. * * (There's actually a count of the #of entries in this file, and for * all files which comprise a spanned archive, but for our purposes * we're only interested in the current file. Besides, we expect the * two to be equivalent for our stuff.) */ numEntries = get2LE(ptr + kEOCDNumEntries); cdOffset = get4LE(ptr + kEOCDFileOffset); /* valid offsets are [0,EOCD] */ unsigned int maxOffset; maxOffset = (ptr - basePtr) +1; LOGV(+++ numEntries=%d cdOffset=%d , numEntries, cdOffset); if (numEntries == 0 || cdOffset >= length) { LOGW(Invalid entries=%d offset=%d (len=%zd) , numEntries, cdOffset, length); goto bail; } /* * Create hash table. We have a minimum 75% load factor, possibly as * low as 50% after we round off to a power of 2. */ mNumEntries = numEntries; mHashTableSize = roundUpPower2(1 + ((numEntries * 4) / 3)); mHashTable = (HashEntry*) calloc(1, sizeof(HashEntry) * mHashTableSize); /* * Walk through the central directory, adding entries to the hash * table. */ ptr = basePtr + cdOffset; for (i = 0; i < numEntries; i++) { unsigned int fileNameLen, extraLen, commentLen, localHdrOffset; const unsigned char* localHdr; unsigned int hash; if (get4LE(ptr) != kCDESignature) { LOGW(Missed a central dir sig (at %d) , i); goto bail; } if (ptr + kCDELen > basePtr + length) { LOGW(Ran off the end (at %d) , i); goto bail; } localHdrOffset = get4LE(ptr + kCDELocalOffset); CHECK_OFFSET(localHdrOffset); fileNameLen = get2LE(ptr + kCDENameLen); extraLen = get2LE(ptr + kCDEExtraLen); commentLen = get2LE(ptr + kCDECommentLen); //LOGV(+++ %d: localHdr=%d fnl=%d el=%d cl=%d , // i, localHdrOffset, fileNameLen, extraLen, commentLen); //LOGV( '%.*s' , fileNameLen, ptr + kCDELen); /* add the CDE filename to the hash table */ hash = computeHash((const char*)ptr + kCDELen, fileNameLen); addToHash((const char*)ptr + kCDELen, fileNameLen, hash);
// localHdr = basePtr + localHdrOffset; // if (get4LE(localHdr) != kLFHSignature) { // LOGW(Bad offset to local header: %d (at %d) , // localHdrOffset, i); // goto bail; // } ptr += kCDELen + fileNameLen + extraLen + commentLen; CHECK_OFFSET(ptr - basePtr); } result = true; bail: return result; #undef CHECK_OFFSET } 紅色部分是修改後的代碼,大家可以對比一下。(未完。。。)
|