我們可以利用DexClassLoader來實現動態加載dex文件,而很多資料也只是對於DexClassLoader的使用進行了介紹,沒有深入講解dex的動態加載機制,我們就借助於Android4.4的源碼來探索。先從一個簡單的動態加載dex文件開始 具體實現細節可以參考這篇文章AndroidDex數據動態加載技術
Android4.4的源碼在百度網盤分享: Android 4.4源碼下載
先是我們要封裝到text.jar文件中的很簡單的調用函數,只是簡單的產生Toast:
/* * 對外接口 */ public interface Iinterface { public void call(); public String getData(); }
public class IClass implements Iinterface{ private Context context; public IClass(Context context){ super(); this.context = context; } //@Override public void call() { // TODO Auto-generated method stub Toast.makeText(context, "call method", 0).show(); } //@Override public String getData() { // TODO Auto-generated method stub return "Hello ,I am from IClass"; } }
在MainActivity中只是解壓test.jar文件,然后通過DexClassLoader類來加載dex文件,最后通過反射調用相關方法:
public class FileUtile { //MainActivity "testdex.jar", "testdex.jar" public static void CopyAssertJarToFile(Context context, String filename, String des) { try { //返回 File ,獲取外部存儲目錄即 SDCard //path "/mnt/sdcard/testdex.jar" //File.separator Windows \ linux / File file = new File(Environment.getExternalStorageDirectory().getPath() + File.separator + des); if (file.exists()) { return; } //取得資源文件的輸入流 InputStream inputStream = context.getAssets().open(filename); file.createNewFile(); //創建"/mnt/sdcard/testdex.jar" 文件 FileOutputStream fileOutputStream = new FileOutputStream(file); byte buffer[] = new byte[1024]; int len = 0; while ((len = inputStream.read(buffer)) != 0) { fileOutputStream.write(buffer, 0, len); } inputStream.close(); fileOutputStream.close(); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } } public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); FileUtile.CopyAssertJarToFile(this, "testdex.jar", "testdex.jar"); /*拷貝*/ File file = new File(Environment.getExternalStorageDirectory() .toString() + File.separator + "testdex.jar"); final File optimizedDexOutputPath = getDir("temp", Context.MODE_PRIVATE); /* * Parameters dexPath 需要裝載的APK或者Jar文件的路徑。包含多個路徑用File.pathSeparator間隔開,在Android上默認是 ":" optimizedDirectory 優化后的dex文件存放目錄,不能為null libraryPath 目標類中使用的C/C++庫的列表,每個目錄用File.pathSeparator間隔開; 可以為 null parent 該類裝載器的父裝載器,一般用當前執行類的裝載器 */ DexClassLoader classLoader = new DexClassLoader(file.getAbsolutePath(), optimizedDexOutputPath.getAbsolutePath(), null, getClassLoader()); try { Class<?> iclass = classLoader.loadClass("com.demo.dex.IClass"); Constructor<?> istructor = iclass.getConstructor(Context.class); //利用反射原理去調用 Method method = iclass.getMethod("call", null); String data = (String) method.invoke(istructor.newInstance(this), null); //System.out.println(data); Log.d("CCDebug",data); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
我們從DexClassLoaderl類開始分析:
在\libcore\dalvik\src\main\java\dalvik\system\ DexClassLoader.java文件下
public class DexClassLoader extends BaseDexClassLoader { public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) { super(dexPath, new File(optimizedDirectory), libraryPath, parent); } }
非常簡單的DexClassLoader的構造函數,只是調用了父類BaseDexClassLoader的構造函數,在同一目錄下的BaseDexClassLoader.java的源碼:
public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) { super(parent); this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory); }
同樣的,也是很簡單的調用父類ClassLoader的構造函數,然后生成一個DexPathList對象,在同一目錄下的DexPathList.java文件中:
public DexPathList(ClassLoader definingContext, String dexPath, String libraryPath, File optimizedDirectory) { //省略參數校驗以及異常處理的代碼 this.definingContext = definingContext; …… this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions); …… this.nativeLibraryDirectories = splitLibraryPath(libraryPath); }
我們繼續閱讀DexPathList.java文件中makeDexElements 的關鍵代碼:
private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions) { // …… for (File file : files) { File zip = null; DexFile dex = null; String name = file.getName(); if (name.endsWith(DEX_SUFFIX)) { //.dex文件 // Raw dex file (not inside a zip/jar). try { dex = loadDexFile(file, optimizedDirectory); } catch (IOException ex) { System.logE("Unable to load dex file: " + file, ex); } } else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX) || name.endsWith(ZIP_SUFFIX)) { //.apk .jar .zip文件 zip = file; try { dex = loadDexFile(file, optimizedDirectory); } catch (IOException suppressed) { suppressedExceptions.add(suppressed); } } else if (file.isDirectory()) { // We support directories for looking up resources. // This is only useful for running libcore tests. elements.add(new Element(file, true, null, null)); } else { System.logW("Unknown file type for: " + file); } } //…… return elements.toArray(new Element[elements.size()]); }
DexPathList.java文件中:
private static DexFile loadDexFile(File file, File optimizedDirectory) throws IOException { if (optimizedDirectory == null) { return new DexFile(file); } else { String optimizedPath = optimizedPathFor(file, optimizedDirectory); return DexFile.loadDex(file.getPath(), optimizedPath, 0); } }
//生成odex的目錄 private static String optimizedPathFor(File path, File optimizedDirectory) { String fileName = path.getName(); if (!fileName.endsWith(DEX_SUFFIX)) { int lastDot = fileName.lastIndexOf("."); if (lastDot < 0) { fileName += DEX_SUFFIX; } else { StringBuilder sb = new StringBuilder(lastDot + 4); sb.append(fileName, 0, lastDot); sb.append(DEX_SUFFIX); fileName = sb.toString(); } } File result = new File(optimizedDirectory, fileName); return result.getPath(); }
optimizedPathFor主要是對文件的后綴進行修正,如果沒有后綴名,就在末尾加上.dex,如果文件結尾不是.dex,就將后綴替換為.dex,然后創建我們的.dex文件,然后返回我們創建的.dex文件的路徑,繼續執行DexFile.loadDex() 函數:
static public DexFile loadDex(String sourcePathName, String outputPathName, int flags) throws IOException { return new DexFile(sourcePathName, outputPathName, flags); }
這里直接返回了一個DexFile對象,下面來看看這個類的構造函數:
//sourceName 就是我們要加載的自己的.jar文件路徑 // outputName 在optimizedPathFor() 函數中修正的加載.dex的路徑 private DexFile(String sourceName, String outputName, int flags) throws IOException { if (outputName != null) { try { String parent = new File(outputName).getParent(); /* ??????*/ if (Libcore.os.getuid() != Libcore.os.stat(parent).st_uid) { throw new IllegalArgumentException("Optimized data directory " + parent + " is not owned by the current user. Shared storage cannot protect" + " your application from code injection attacks."); } } catch (ErrnoException ignored) { // assume we'll fail with a more contextual error later } } //我們的重點就是在openDexFile()函數上 mCookie = openDexFile(sourceName, outputName, flags); mFileName = sourceName; guard.open("close"); //System.out.println("DEX FILE cookie is " + mCookie); }
openDexFile函數的返回值是一個整型,保存在mCookie中,文件名保存在mFileName中
private static int openDexFile(String sourceName, String outputName, int flags) throws IOException { return openDexFileNative(new File(sourceName).getCanonicalPath(), (outputName == null) ? null : new File(outputName).getCanonicalPath(), flags); }
在openDexFile()中只是調用了openDexFileNative () 繼續跟入在\ dalvik\v m\nat ive\dalvik _sys tem_DexFile.cpp文件中的openDexFileNative() 函數,接下重點就在這個函數:
static void Dalvik_dalvik_system_DexFile_openDexFileNative(const u4* args, JValue* pResult) { //args[0]: sourceName java層傳入的 //args[1]: outputName StringObject* sourceNameObj = (StringObject*) args[0]; StringObject* outputNameObj = (StringObject*) args[1]; DexOrJar* pDexOrJar = NULL; JarFile* pJarFile; RawDexFile* pRawDexFile; //DexOrJar* JarFile* RawDexFile* 目錄 char* sourceName; char* outputName; //…… sourceName = dvmCreateCstrFromString(sourceNameObj); if (outputNameObj != NULL) outputName = dvmCreateCstrFromString(outputNameObj); else outputName = NULL; /*判斷要加載的dex是否為系統中的dex文件 * gDvm ??? */ if (dvmClassPathContains(gDvm.bootClassPath, sourceName)) { ALOGW("Refusing to reopen boot DEX '%s'", sourceName); dvmThrowIOException( "Re-opening BOOTCLASSPATH DEX files is not allowed"); free(sourceName); free(outputName); RETURN_VOID(); } /* * Try to open it directly as a DEX if the name ends with ".dex". * If that fails (or isn't tried in the first place), try it as a * Zip with a "classes.dex" inside. */ //判斷sourcename擴展名是否是.dex if (hasDexExtension(sourceName) && dvmRawDexFileOpen(sourceName, outputName, &pRawDexFile, false) == 0) { ALOGV("Opening DEX file '%s' (DEX)", sourceName); pDexOrJar = (DexOrJar*) malloc(sizeof(DexOrJar)); pDexOrJar->isDex = true; pDexOrJar->pRawDexFile = pRawDexFile; pDexOrJar->pDexMemory = NULL; //.jar文件 } else if (dvmJarFileOpen(sourceName, outputName, &pJarFile, false) == 0) { ALOGV("Opening DEX file '%s' (Jar)", sourceName); pDexOrJar = (DexOrJar*) malloc(sizeof(DexOrJar)); pDexOrJar->isDex = false; pDexOrJar->pJarFile = pJarFile; pDexOrJar->pDexMemory = NULL; } else { //都不滿足,拋出異常 ALOGV("Unable to open DEX file '%s'", sourceName); dvmThrowIOException("unable to open DEX file"); } if (pDexOrJar != NULL) { pDexOrJar->fileName = sourceName; //把pDexOr這個結構體中的內容加到gDvm中的userDexFile結構的hash表中,便於Dalvik以后的查找 addToDexFileTable(pDexOrJar); } else { free(sourceName); } free(outputName); RETURN_PTR(pDexOrJar); }
接下來再看對.dex文件的處理函數dvmRawDexFileOpen 在dalvik\vm\RawDexFile.cpp文件中:
/* See documentation comment in header. */ int dvmRawDexFileOpen(const char* fileName, const char* odexOutputName, RawDexFile** ppRawDexFile, bool isBootstrap) { DvmDex* pDvmDex = NULL; char* cachedName = NULL; int result = -1; int dexFd = -1; int optFd = -1; u4 modTime = 0; u4 adler32 = 0; size_t fileSize = 0; bool newFile = false; bool locked = false; dexFd = open(fileName, O_RDONLY); //打開dex文件 if (dexFd < 0) goto bail; /* If we fork/exec into dexopt, don't let it inherit the open fd. */ dvmSetCloseOnExec(dexFd);//dexfd不繼承 //校驗dex文件的標志,將第8字節開始的4個字節賦值給adler32。 if (verifyMagicAndGetAdler32(dexFd, &adler32) < 0) { ALOGE("Error with header for %s", fileName); goto bail; } //得到dex文件的大小和修改時間,保存在modTime和filesize中 if (getModTimeAndSize(dexFd, &modTime, &fileSize) < 0) { ALOGE("Error with stat for %s", fileName); goto bail; } //odexOutputName就是odex文件名,如果odexOutputName為空,則自動生成一個。 if (odexOutputName == NULL) { cachedName = dexOptGenerateCacheFileName(fileName, NULL); if (cachedName == NULL) goto bail; } else { cachedName = strdup(odexOutputName); } //主要是驗證緩存文件名的正確性,之后將dexOptHeader結構寫入fd中 optFd = dvmOpenCachedDexFile(fileName, cachedName, modTime, adler32, isBootstrap, &newFile, /*createIfMissing=*/true); locked = true; if (newFile) { u8 startWhen, copyWhen, endWhen; bool result; off_t dexOffset; dexOffset = lseek(optFd, 0, SEEK_CUR); //文件指針的位置 result = (dexOffset > 0); if (result) { startWhen = dvmGetRelativeTimeUsec(); //將dex文件中的內容拷貝到當前odex文件,也就是dexOffset開始 result = copyFileToFile(optFd, dexFd, fileSize) == 0; copyWhen = dvmGetRelativeTimeUsec(); } if (result) { //優化odex文件 result = dvmOptimizeDexFile(optFd, dexOffset, fileSize, fileName, modTime, adler32, isBootstrap); } } /* * Map the cached version. This immediately rewinds the fd, so it * doesn't have to be seeked anywhere in particular. */ //將odex文件映射到內存空間(mmap),並用mprotect將屬性置為只讀屬性,並將映射的dex結構放在pDvmDex數據結構中,具體代碼在下面。 if (dvmDexFileOpenFromFd(optFd, &pDvmDex) != 0) { ALOGI("Unable to map cached %s", fileName); goto bail; } …… }
//Dalvik/vm/RewDexFile.cpp static int verifyMagicAndGetAdler32(int fd, u4 *adler32) { u1 headerStart[12]; ssize_t amt = read(fd, headerStart, sizeof(headerStart)); if (amt < 0) { ALOGE("Unable to read header: %s", strerror(errno)); return -1; } if (amt != sizeof(headerStart)) { ALOGE("Unable to read full header (only got %d bytes)", (int) amt); return -1; } if (!dexHasValidMagic((DexHeader*) (void*) headerStart)) { return -1; } *adler32 = (u4) headerStart[8] | (((u4) headerStart[9]) << 8) | (((u4) headerStart[10]) << 16) | (((u4) headerStart[11]) << 24); return 0; }
//dalvik\vm\DvmDex.cpp /* * Given an open optimized DEX file, map it into read-only shared memory and * parse the contents. * * Returns nonzero on error. */ int dvmDexFileOpenFromFd(int fd, DvmDex** ppDvmDex) { DvmDex* pDvmDex; DexFile* pDexFile; MemMapping memMap; int parseFlags = kDexParseDefault; int result = -1; if (gDvm.verifyDexChecksum) parseFlags |= kDexParseVerifyChecksum; if (lseek(fd, 0, SEEK_SET) < 0) { ALOGE("lseek rewind failed"); goto bail; } //mmap映射fd文件,就是我們之前的odex文件 if (sysMapFileInShmemWritableReadOnly(fd, &memMap) != 0) { ALOGE("Unable to map file"); goto bail; } pDexFile = dexFileParse((u1*)memMap.addr, memMap.length, parseFlags); if (pDexFile == NULL) { ALOGE("DEX parse failed"); sysReleaseShmem(&memMap); goto bail; } pDvmDex = allocateAuxStructures(pDexFile); if (pDvmDex == NULL) { dexFileFree(pDexFile); sysReleaseShmem(&memMap); goto bail; } /* tuck this into the DexFile so it gets released later */ //將映射odex文件的內存拷貝到DvmDex的結構中 sysCopyMap(&pDvmDex->memMap, &memMap); pDvmDex->isMappedReadOnly = true; *ppDvmDex = pDvmDex; result = 0; bail: return result; } /*dalvik\libdex\SysUtil.cpp */ int sysMapFileInShmemWritableReadOnly(int fd, MemMapping* pMap) { off_t start; size_t length; void* memPtr; assert(pMap != NULL); //獲得文件長度和文件開始地址 if (getFileStartAndLength(fd, &start, &length) < 0) return -1; //映射文件 memPtr = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_FILE | MAP_PRIVATE, fd, start); //…… //將保護屬性置為只讀屬性 if (mprotect(memPtr, length, PROT_READ) < 0) { //……. } pMap->baseAddr = pMap->addr = memPtr; pMap->baseLength = pMap->length = length; return 0; //…… }
下面在分析文件后綴不是.dex的情況:
/*如果不是.dex文件*/ int dvmJarFileOpen(const char* fileName, const char* odexOutputName, JarFile** ppJarFile, bool isBootstrap) { ZipArchive archive; DvmDex* pDvmDex = NULL; char* cachedName = NULL; bool archiveOpen = false; bool locked = false; int fd = -1; int result = -1; //打開.jar文件並映射,內存結構放在ZipArchive中,之后將具體分析的代碼 if (dexZipOpenArchive(fileName, &archive) != 0) goto bail; archiveOpen = true; dvmSetCloseOnExec(dexZipGetArchiveFd(&archive)); //不繼承 // openAlternateSuffix函數將fileName的后綴名改為”.odex”,例如 //”Hello.jar”--”Hello.odex”,然后調用open()”打開”Hello.odex文件 //如果成功返回”Hello.odex”的文件描述符 fd = openAlternateSuffix(fileName, "odex", O_RDONLY, &cachedName); if (fd >= 0) { ALOGV("Using alternate file (odex) for %s ...", fileName); //…檢驗optHeader if (!dvmCheckOptHeaderAndDependencies(fd, false, 0, 0, true, true)) { //…… goto tryArchive; } } else { ZipEntry entry; tryArchive: /* * Pre-created .odex absent or stale. Look inside the jar for a * "classes.dex". */ // static const char* kDexInJarName = "classes.dex"; /* 在dexZipFindEntry函數中,對kDexInJarName也就是”class.dex”進行hash運算,找到”class.dex”在archive結構中的表項 */ entry = dexZipFindEntry(&archive, kDexInJarName); if (entry != NULL) { bool newFile = false; //如果odex緩存路徑為空,則自動生成一個路徑 if (odexOutputName == NULL) { cachedName = dexOptGenerateCacheFileName(fileName, kDexInJarName); if (cachedName == NULL) goto bail; } else { cachedName = strdup(odexOutputName); } //創建cachedName對應的文件 (.odex) fd = dvmOpenCachedDexFile(fileName, cachedName, dexGetZipEntryModTime(&archive, entry), dexGetZipEntryCrc32(&archive, entry), //…… locked = true; //…… if (newFile) { //成功創建.odex文件 u8 startWhen, extractWhen, endWhen; bool result; off_t dexOffset; dexOffset = lseek(fd, 0, SEEK_CUR); result = (dexOffset > 0); if (result) { startWhen = dvmGetRelativeTimeUsec(); result = dexZipExtractEntryToFile(&archive, entry, fd) == 0; extractWhen = dvmGetRelativeTimeUsec(); } if (result) { //優化dex文件-.odex result = dvmOptimizeDexFile(fd, dexOffset, dexGetZipEntryUncompLen(&archive, entry), fileName, dexGetZipEntryModTime(&archive, entry), dexGetZipEntryCrc32(&archive, entry), isBootstrap); } //已經得到了.odex文件,下面的流程就和.dex文件一樣了。 //映射.odex文件, if (dvmDexFileOpenFromFd(fd, &pDvmDex) != 0) //………… return result; }
//\dalvik\libdex\SysUtil.cpp int dexZipOpenArchive(const char* fileName, ZipArchive* pArchive) { int fd, err; ……. memset(pArchive, 0, sizeof(ZipArchive)); //打開文件 fd = open(fileName, O_RDONLY | O_BINARY, 0); …… return dexZipPrepArchive(fd, fileName, pArchive); } int dexZipPrepArchive(int fd, const char* debugFileName, ZipArchive* pArchive) { int result = -1; memset(pArchive, 0, sizeof(*pArchive)); pArchive->mFd = fd; //Zip的文件描述符 if (mapCentralDirectory(fd, debugFileName, pArchive) != 0) goto bail; if (parseZipArchive(pArchive) != 0) { goto bail; } /* success */ result = 0; bail: if (result != 0) dexZipCloseArchive(pArchive); //失敗釋放pArchive結構 return result; } static int mapCentralDirectory(int fd, const char* debugFileName, ZipArchive* pArchive) { /* * Get and test file length. */ //檢驗文件長度的有效性 off64_t fileLength = lseek64(fd, 0, SEEK_END); if (fileLength < kEOCDLen) { return -1; } size_t readAmount = kMaxEOCDSearch; if (fileLength < off_t(readAmount)) readAmount = fileLength; u1* scanBuf = (u1*) malloc(readAmount); if (scanBuf == NULL) { return -1; } int result = mapCentralDirectory0(fd, debugFileName, pArchive, fileLength, readAmount, scanBuf); free(scanBuf); return result; } tatic int mapCentralDirectory0(int fd, const char* debugFileName, ZipArchive* pArchive, off64_t fileLength, size_t readAmount, u1* scanBuf) { /* * Make sure this is a Zip archive. */ //校驗文件是否合法的Zip文件 //…… //偏移16的地方 //偏移12 if (sysMapFileSegmentInShmem(fd, centralDirOffset, centralDirSize, &pArchive->mDirectoryMap) != 0) { ALOGW("Zip: cd map failed"); return -1; } pArchive->mNumEntries = numEntries; pArchive->mDirectoryOffset = centralDirOffset; return 0; } int sysMapFileSegmentInShmem(int fd, off_t start, size_t length, MemMapping* pMap) { size_t actualLength; off_t actualStart; int adjust; void* memPtr; assert(pMap != NULL); /* adjust to be page-aligned */ adjust = start % SYSTEM_PAGE_SIZE; actualStart = start - adjust; actualLength = length + adjust; //映射 memPtr = mmap(NULL, actualLength, PROT_READ, MAP_FILE | MAP_SHARED, fd, actualStart); // ……. pMap->baseAddr = memPtr; pMap->baseLength = actualLength; pMap->addr = (char*)memPtr + adjust; pMap->length = length; return 0; }
ZipArchive的結構體如下:
struct ZipArchive { /* open Zip archive */ int mFd; //打開的zip文件 /* mapped central directory area */ off_t mDirectoryOffset; MemMapping mDirectoryMap; //映射內存的結構 /* number of entries in the Zip archive */ int mNumEntries; // int mHashTableSize; //名字hash表的大小 ZipHashEntry* mHashTable; //hash表的表項, }; struct ZipHashEntry { const char* name; unsigned short nameLen; };
我們可以簡要總結下整個的加載流程,首先是對文件名的修正,后綴名置為”.dex”作為輸出文件,然后生個一個DexPathList對象函數直接返回一個DexPathList對象,
在DexPathList的構造函數中調用makeDexElements()函數,在makeDexElement()函數中調用loadDexFile()開始對.dex或者是.jar .zip .apk文件進行處理,
跟入loadDexFile()函數中,會發現里面做的工作很簡單,調用optimizedPathFor()函數對optimizedDiretcory路徑進行修正。
之后才真正通過DexFile.loadDex()開始加載文件中的數據,其中的加載也只是返回一個DexFile對象。
在DexFile類的構造函數中,重點便放在了其調用的openDexFile()函數,在openDexFile()中調用了openDexFileNative()真正進入native層,
在openDexFileNative()的真正實現中,對於后綴名為.dex的文件或者其他文件(.jar .apk .zip)分開進行處理:
.dex文件調用dvmRawDexFileOpen();
其他文件調用dvmJarFileOpen()。
在dvmRawDexFileOpen()函數中,檢驗dex文件的標志,檢驗odex文件的緩存名稱,之后將dex文件拷貝到odex文件中,並對odex進行優化
調用dvmDexFileOpenFromFd()對優化后的odex文件進行映射,通過mprotect置為"只讀"屬性並將映射的內存結構保存在DvmDex*結構中。
dvmJarFileOpen()先對文件進行映射,結構保存在ZipArchive中,然后再嘗試以文件名作為dex文件名來“打開”文件,
如果失敗,則調用dexZipFindEntry在ZipArchive的名稱hash表中找名為"class.dex"的文件,然后創建odex文件,下面就和
dvmRawDexFileOpen()一樣了,就是對dex文件進行優化和映射。
也只是分析了一個大概流程,還有很多有待之后進行深入。而這里對於閱讀Android源碼,有了新的體會,首先是工具上,我之前一直是用Source InSight 但是對於一些函數的實現,找起來卻是不太方便,因為必須要將函數實現的文件導入到工程中,而用VS來閱讀源碼,利用Ctrl+Shift+F的功能,在Android源碼目錄下搜索更為方便,然后可以在Source InSight中進行導入,閱讀。其次不得不說閱讀源碼真的是一個比較痛苦的過程,但真的學習下來,收獲還是很大的。