1. 程式人生 > >Android提高啟動速度的實現方法

Android提高啟動速度的實現方法

原文地址:http://www.eoeandroid.com/thread-29953-1-1.html

Android重量級開發之--提高android啟動速度研究

     大家都知道啟動速度慢是智慧作業系統的一個通病,Android也不例外,啟動速度大概在1分鐘左右,雖然日本有一個叫quick boot的一秒啟動android的產品,但是畢竟是旁門左道。所以從常規來提高android的啟動速度成了大家研究的重點,也是難點。下面將初步研究的一下經驗跟大家分享一下。

首先看一下android系統的啟動流程:

bootloader 
          載入程式

kernel 

         核心

init 
          init
初始化(這個大家都比較熟悉了,不要多說)

    • loads several daemons and services, including zygote
    • see /init.rc and init.<platform>.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

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;//這個代表了裝載時間小於1250us1.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<String> EXCLUDED_CLASSES
            = new HashSet<String>(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<String> SERVICES = new HashSet<String>(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'\n", zipFileName);

    /*
     * Open and map the specified file.
     */

    fd = ::open(zipFileName, O_RDONLY);


    if (fd < 0) {
        LOGW("Unable to open zip '%s': %s\n", 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\n", 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\n");

    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\n",                     \
                (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\n");
        goto bail;
    } else if (val != kLFHSignature) {
        LOGV("Not a Zip archive (found 0x%08x)\n", 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\n");
        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\n", numEntries, cdOffset);
    if (numEntries == 0 || cdOffset >= length) {
        LOGW("Invalid entries=%d offset=%d (len=%zd)\n",
            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)\n", i);
            goto bail;
        }
        if (ptr + kCDELen > basePtr + length) {
            LOGW("Ran off the end (at %d)\n", 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\n",
        //    i, localHdrOffset, fileNameLen, extraLen, commentLen);
        //LOGV(" '%.*s'\n", 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)\n",
             //   localHdrOffset, i);
          //  goto bail;
     //   }

        ptr += kCDELen + fileNameLen + extraLen + commentLen;
        CHECK_OFFSET(ptr - basePtr);
    }

    result = true;

bail:
    return result;
#undef CHECK_OFFSET
}


     紅色部分是修改後的程式碼,大家可以對比一下。(未完。。。)



再傳兩個patch