精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

MultiDex工作原理分析和優(yōu)化方案

移動(dòng)開(kāi)發(fā) Android
動(dòng)態(tài)加載技術(shù)(插件化)系列已經(jīng)坑了有一段時(shí)間了,不過(guò)UP主我并沒(méi)有放棄治療哈,相信在不就的未來(lái)就可以看到“系統(tǒng)Api Hook模式”和插件化框架Frontia的更新了。今天要講的是動(dòng)態(tài)加載技術(shù)的親戚 —— MultiDex。他們的核心原理之一都是dex文件的加載。

動(dòng)態(tài)加載技術(shù)(插件化)系列已經(jīng)坑了有一段時(shí)間了,不過(guò)UP主我并沒(méi)有放棄治療哈,相信在不就的未來(lái)就可以看到“系統(tǒng)Api Hook模式”和插件化框架Frontia的更新了。今天要講的是動(dòng)態(tài)加載技術(shù)的親戚 —— MultiDex。他們的核心原理之一都是dex文件的加載。

MultiDex是Google為了解決“65535方法數(shù)超標(biāo)”以及“INSTALL_FAILED_DEXOPT”問(wèn)題而開(kāi)發(fā)的一個(gè)Support庫(kù),具體如何使用MultiDex現(xiàn)在市面已經(jīng)有一大堆教程(可以參考給 App 啟用 MultiDex 功能),這里不再贅述。這篇日志主要是配合源碼分析MultiDex的工作原理,以及提供一些MultiDex優(yōu)化的方案。

Dex的工作機(jī)制

等等,這個(gè)章節(jié)講的不是MultiDex嗎,怎么變成Dex了?沒(méi)錯(cuò)哈,沒(méi)有Dex,哪來(lái)的MultiDex。在Android中,對(duì)Dex文件操作對(duì)應(yīng)的類(lèi)叫做DexFile。在CLASSLOADER 的工作機(jī)制中,我們說(shuō)到:

對(duì)于 Java 程序來(lái)說(shuō),編寫(xiě)程序就是編寫(xiě)類(lèi),運(yùn)行程序也就是運(yùn)行類(lèi)(編譯得到的class文件),其中起到關(guān)鍵作用的就是類(lèi)加載器 ClassLoader。

Android程序的每一個(gè)Class都是由ClassLoader#loadClass方法加載進(jìn)內(nèi)存的,更準(zhǔn)確來(lái)說(shuō),一個(gè)ClassLoader實(shí)例會(huì)有一個(gè)或者多個(gè)DexFile實(shí)例,調(diào)用了ClassLoader#loadClass之后,ClassLoader會(huì)通過(guò)類(lèi)名,在自己的DexFile數(shù)組里面查找有沒(méi)有那個(gè)DexFile對(duì)象里面存在這個(gè)類(lèi),如果都沒(méi)有就拋ClassNotFound異常。ClassLoader通過(guò)調(diào)用DexFile的一個(gè)叫defineClass的Native方法去加載指定的類(lèi),這點(diǎn)與JVM略有不同,后者是直接調(diào)用ClassLoader#defineCLass方法,反正最后實(shí)際加載類(lèi)的方法都叫defineClass就沒(méi)錯(cuò)了🌝。

創(chuàng)建DexFile對(duì)象

首先來(lái)看看造DexFile對(duì)象的構(gòu)方法。

  1. public final class DexFile { 
  2.     private int mCookie; 
  3.     private final String mFileName; 
  4.     ... 
  5.  
  6.     public DexFile(File file) throws IOException { 
  7.         this(file.getPath()); 
  8.     } 
  9.     
  10.     public DexFile(String fileName) throws IOException { 
  11.         mCookie = openDexFile(fileName, null, 0); 
  12.         mFileName = fileName; 
  13.         guard.open("close"); 
  14.     } 
  15.     
  16.     private DexFile(String sourceName, String outputName, int flags) throws IOException { 
  17.         mCookie = openDexFile(sourceName, outputName, flags); 
  18.         mFileName = sourceName; 
  19.         guard.open("close"); 
  20.     } 
  21.  
  22.     static public DexFile loadDex(String sourcePathName, String outputPathName, 
  23.         int flags) throws IOException { 
  24.         return new DexFile(sourcePathName, outputPathName, flags); 
  25.     } 
  26.    
  27.     public Class loadClass(String name, ClassLoader loader) { 
  28.         String slashName = name.replace('.''/'); 
  29.         return loadClassBinaryName(slashName, loader); 
  30.     } 
  31.    
  32.     public Class loadClassBinaryName(String name, ClassLoader loader) { 
  33.         return defineClass(name, loader, mCookie); 
  34.     } 
  35.     private native static Class defineClass(String name, ClassLoader loader, int cookie); 
  36.  
  37.     native private static int openDexFile(String sourceName, String outputName, 
  38.         int flags) throws IOException; 
  39.  
  40.     native private static int openDexFile(byte[] fileContents) 
  41.     ... 
  42.  

通過(guò)以前分析過(guò)的源碼,我們知道ClassLoader主要是通過(guò)DexFile.loadDex這個(gè)靜態(tài)方法來(lái)創(chuàng)建它需要的DexFile實(shí)例的,這里創(chuàng)建DexFile的時(shí)候,保存了Dex文件的文件路徑mFileName,同時(shí)調(diào)用了openDexFile的Native方法打開(kāi)Dex文件并返回了一個(gè)mCookie的整型變量(我不知道這個(gè)干啥用的,我猜它是一個(gè)C++用的資源句柄,用于Native層訪問(wèn)具體的Dex文件)。在Native層的openDexFile方法里,主要做了檢查當(dāng)前創(chuàng)建來(lái)的Dex文件是否是有效的Dex文件,還是是一個(gè)帶有Dex文件的壓縮包,還是一個(gè)無(wú)效的Dex文件。

加載Dex文件里的類(lèi)

加載類(lèi)的時(shí)候,ClassLoader又是通過(guò)DexFile#loadClass這個(gè)方法來(lái)完成的,這個(gè)方法里調(diào)用了defineClass這個(gè)Native方法,看來(lái)DexFile才是加載Class的具體API,加載Dex文件和加載具體Class都是通過(guò)Native方法完成,ClassLoader有點(diǎn)名不副實(shí)啊。

MultiDex的工作機(jī)制

當(dāng)一個(gè)Dex文件太肥的時(shí)候(方法數(shù)目太多、文件太大),在打包Apk文件的時(shí)候就會(huì)出問(wèn)題,就算打包的時(shí)候不出問(wèn)題,在Android 5.0以下設(shè)備上安裝或運(yùn)行Apk也會(huì)出問(wèn)題(具體原因可以參考給 App 啟用 MultiDex 功能)。既然一個(gè)Dex文件不行的話,那就把這個(gè)碩大的Dex文件拆分成若干個(gè)小的Dex文件,剛好一個(gè)ClassLoader可以有多個(gè)DexFile,這就是MultiDex的基本設(shè)計(jì)思路。

工作流程

MultiDex的工作流程具體分為兩個(gè)部分,一個(gè)部分是打包構(gòu)建Apk的時(shí)候,將Dex文件拆分成若干個(gè)小的Dex文件,這個(gè)Android Studio已經(jīng)幫我們做了(設(shè)置 “multiDexEnabled true”),另一部分就是在啟動(dòng)Apk的時(shí)候,同時(shí)加載多個(gè)Dex文件(具體是加載Dex文件優(yōu)化后的Odex文件,不過(guò)文件名還是.dex),這一部分工作從Android 5.0開(kāi)始系統(tǒng)已經(jīng)幫我們做了,但是在Android 5.0以前還是需要通過(guò)MultiDex Support庫(kù)來(lái)支持(MultiDex.install(Context))。

所以我們需要關(guān)心的是第二部分,這個(gè)過(guò)程的簡(jiǎn)單示意流程圖如下。 

 

 

(圖中紅色部分為耗時(shí)比較大的地方)

源碼分析

現(xiàn)在官方已經(jīng)部署的MultiDex Support版本是com.android.support:multidex:1.0.1,但是現(xiàn)在倉(cāng)庫(kù)的master分支已經(jīng)有了許多新的提交(其中最明顯的區(qū)別是加入了FileLock來(lái)控制多進(jìn)程同步問(wèn)題),所以這里分析的源碼都是最新的master分支上的。

MultiDex Support的入口是MultiDex.install(Context),先從這里入手吧。(這次我把具體的分析都寫(xiě)在代碼的注釋了,這樣看是不是更簡(jiǎn)潔明了些?)

  1. public static void install(Context context) { 
  2.         Log.i(TAG, "install"); 
  3.          
  4.         // 1. 判讀是否需要執(zhí)行MultiDex。 
  5.         if (IS_VM_MULTIDEX_CAPABLE) { 
  6.             Log.i(TAG, "VM has multidex support, MultiDex support library is disabled."); 
  7.             return
  8.         } 
  9.         if (Build.VERSION.SDK_INT < MIN_SDK_VERSION) { 
  10.             throw new RuntimeException("Multi dex installation failed. SDK " + Build.VERSION.SDK_INT 
  11.                     + " is unsupported. Min SDK version is " + MIN_SDK_VERSION + "."); 
  12.         } 
  13.         try { 
  14.             ApplicationInfo applicationInfo = getApplicationInfo(context); 
  15.             if (applicationInfo == null) { 
  16.                 // Looks like running on a test Context, so just return without patching. 
  17.                 return
  18.             } 
  19.              
  20.             // 2. 如果這個(gè)方法已經(jīng)調(diào)用過(guò)一次,就不能再調(diào)用了。 
  21.             synchronized (installedApk) { 
  22.                 String apkPath = applicationInfo.sourceDir; 
  23.                 if (installedApk.contains(apkPath)) { 
  24.                     return
  25.                 } 
  26.                 installedApk.add(apkPath); 
  27.                  
  28.                 // 3. 如果當(dāng)前Android版本已經(jīng)自身支持了MultiDex,依然可以執(zhí)行MultiDex操作, 
  29.                 // 但是會(huì)有警告。 
  30.                 if (Build.VERSION.SDK_INT > MAX_SUPPORTED_SDK_VERSION) { 
  31.                     Log.w(TAG, "MultiDex is not guaranteed to work in SDK version " 
  32.                             + Build.VERSION.SDK_INT + ": SDK version higher than " 
  33.                             + MAX_SUPPORTED_SDK_VERSION + " should be backed by " 
  34.                             + "runtime with built-in multidex capabilty but it's not the " 
  35.                             + "case here: java.vm.version=\"" 
  36.                             + System.getProperty("java.vm.version") + "\""); 
  37.                 } 
  38.          
  39.                 // 4. 獲取當(dāng)前的ClassLoader實(shí)例,后面要做的工作,就是把其他dex文件加載后, 
  40.                 // 把其DexFile對(duì)象添加到這個(gè)ClassLoader實(shí)例里就完事了。 
  41.                 ClassLoader loader; 
  42.                 try { 
  43.                     loader = context.getClassLoader(); 
  44.                 } catch (RuntimeException e) { 
  45.                     Log.w(TAG, "Failure while trying to obtain Context class loader. " + 
  46.                             "Must be running in test mode. Skip patching.", e); 
  47.                     return
  48.                 } 
  49.                 if (loader == null) { 
  50.                     Log.e(TAG, 
  51.                             "Context class loader is null. Must be running in test mode. " 
  52.                             + "Skip patching."); 
  53.                     return
  54.                 } 
  55.                 try { 
  56.                   // 5. 清除舊的dex文件,注意這里不是清除上次加載的dex文件緩存。 
  57.                   // 獲取dex緩存目錄是,會(huì)優(yōu)先獲取/data/data/<package>/code-cache作為緩存目錄。 
  58.                   // 如果獲取失敗,則使用/data/data/<package>/files/code-cache目錄。 
  59.                   // 這里清除的是第二個(gè)目錄。 
  60.                   clearOldDexDir(context); 
  61.                 } catch (Throwable t) { 
  62.                   Log.w(TAG, "Something went wrong when trying to clear old MultiDex extraction, " 
  63.                       + "continuing without cleaning.", t); 
  64.                 } 
  65.                  
  66.                 // 6. 獲取緩存目錄(/data/data/<package>/code-cache)。 
  67.                 File dexDir = getDexDir(context, applicationInfo); 
  68.                  
  69.                 // 7. 加載緩存文件(如果有)。 
  70.                 List<File> files = MultiDexExtractor.load(context, applicationInfo, dexDir, false); 
  71.                  
  72.                 // 8. 檢查緩存的dex是否安全 
  73.                 if (checkValidZipFiles(files)) { 
  74.                     // 9. 安裝緩存的dex 
  75.                     installSecondaryDexes(loader, dexDir, files); 
  76.                 } else { 
  77.                     // 9. 從apk壓縮包里面提取dex文件 
  78.                     Log.w(TAG, "Files were not valid zip files.  Forcing a reload."); 
  79.                     files = MultiDexExtractor.load(context, applicationInfo, dexDir, true); 
  80.                     if (checkValidZipFiles(files)) { 
  81.                         // 10. 安裝提取的dex 
  82.                         installSecondaryDexes(loader, dexDir, files); 
  83.                     } else { 
  84.                         throw new RuntimeException("Zip files were not valid."); 
  85.                     } 
  86.                 } 
  87.             } 
  88.         } catch (Exception e) { 
  89.             Log.e(TAG, "Multidex installation failure", e); 
  90.             throw new RuntimeException("Multi dex installation failed (" + e.getMessage() + ")."); 
  91.         } 
  92.         Log.i(TAG, "install done"); 
  93.     }  

具體代碼的分析已經(jīng)在上面代碼的注釋里給出了,從這里我們也可以看出,整個(gè)MultiDex.install(Context)的過(guò)程中,關(guān)鍵的步驟就是MultiDexExtractor#load方法和MultiDex#installSecondaryDexes方法。

(這部分是題外話)其中有個(gè)MultiDex#clearOldDexDir(Context)方法,這個(gè)方法的作用是刪除/data/data/<package>/files/code-cache,一開(kāi)始我以為這個(gè)方法是刪除上一次執(zhí)行MultiDex后的緩存文件,不過(guò)這明顯不對(duì),不可能每次MultiDex都重新解壓dex文件一邊,這樣每次啟動(dòng)會(huì)很耗時(shí),只有第一次冷啟動(dòng)的時(shí)候才需要解壓dex文件。后來(lái)我又想是不是以前舊版的MultiDex曾經(jīng)把緩存文件放在這個(gè)目錄里,現(xiàn)在新版本只是清除以前舊版的遺留文件?但是我找遍了整個(gè)MultiDex Repo的提交也沒(méi)有見(jiàn)過(guò)類(lèi)似的舊版本代碼。后面我仔細(xì)看MultiDex#getDexDir這個(gè)方法才發(fā)現(xiàn),原來(lái)MultiDex在獲取dex緩存目錄是,會(huì)優(yōu)先獲取/data/data/<package>/code-cache作為緩存目錄,如果獲取失敗,則使用/data/data/<package>/files/code-cache目錄,而后者的緩存文件會(huì)在每次App重新啟動(dòng)的時(shí)候被清除。感覺(jué)MultiDex獲取緩存目錄的邏輯不是很?chē)?yán)謹(jǐn),而獲取緩存目錄失敗也是MultiDex工作工程中少數(shù)有重試機(jī)制的地方,看來(lái)MultiDex真的是一個(gè)臨時(shí)的兼容方案,Google也許并不打算認(rèn)真處理這些歷史的黑鍋。

接下來(lái)再看看MultiDexExtractor#load這個(gè)方法。 

  1. static List<File> load(Context context, ApplicationInfo applicationInfo, File dexDir, 
  2.             boolean forceReload) throws IOException { 
  3.         Log.i(TAG, "MultiDexExtractor.load(" + applicationInfo.sourceDir + ", " + forceReload + ")"); 
  4.         final File sourceApk = new File(applicationInfo.sourceDir); 
  5.          
  6.         // 1. 獲取當(dāng)前Apk文件的crc值。 
  7.         long currentCrc = getZipCrc(sourceApk); 
  8.         // Validity check and extraction must be done only while the lock file has been taken. 
  9.         File lockFile = new File(dexDir, LOCK_FILENAME); 
  10.         RandomAccessFile lockRaf = new RandomAccessFile(lockFile, "rw"); 
  11.         FileChannel lockChannel = null
  12.         FileLock cacheLock = null
  13.         List<File> files; 
  14.         IOException releaseLockException = null
  15.         try { 
  16.             lockChannel = lockRaf.getChannel(); 
  17.             Log.i(TAG, "Blocking on lock " + lockFile.getPath()); 
  18.              
  19.             // 2. 加上文件鎖,防止多進(jìn)程沖突。 
  20.             cacheLock = lockChannel.lock(); 
  21.             Log.i(TAG, lockFile.getPath() + " locked"); 
  22.              
  23.             // 3. 先判斷是否強(qiáng)制重新解壓,這里第一次會(huì)優(yōu)先使用已解壓過(guò)的dex文件,如果加載失敗就強(qiáng)制重新解壓。 
  24.             // 此外,通過(guò)crc和文件修改時(shí)間,判斷如果Apk文件已經(jīng)被修改(覆蓋安裝),就會(huì)跳過(guò)緩存重新解壓dex文件。 
  25.             if (!forceReload && !isModified(context, sourceApk, currentCrc)) { 
  26.                 try { 
  27.                  
  28.                     // 4. 加載緩存的dex文件 
  29.                     files = loadExistingExtractions(context, sourceApk, dexDir); 
  30.                 } catch (IOException ioe) { 
  31.                     Log.w(TAG, "Failed to reload existing extracted secondary dex files," 
  32.                             + " falling back to fresh extraction", ioe); 
  33.                      
  34.                     // 5. 加載失敗的話重新解壓,并保存解壓出來(lái)的dex文件的信息。 
  35.                     files = performExtractions(sourceApk, dexDir); 
  36.                     putStoredApkInfo(context, 
  37.                             getTimeStamp(sourceApk), currentCrc, files.size() + 1); 
  38.                 } 
  39.             } else { 
  40.                 // 4. 重新解壓,并保存解壓出來(lái)的dex文件的信息。 
  41.                 Log.i(TAG, "Detected that extraction must be performed."); 
  42.                 files = performExtractions(sourceApk, dexDir); 
  43.                 putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1); 
  44.             } 
  45.         } finally { 
  46.             if (cacheLock != null) { 
  47.                 try { 
  48.                     cacheLock.release(); 
  49.                 } catch (IOException e) { 
  50.                     Log.e(TAG, "Failed to release lock on " + lockFile.getPath()); 
  51.                     // Exception while releasing the lock is bad, we want to report it, but not at 
  52.                     // the price of overriding any already pending exception. 
  53.                     releaseLockException = e; 
  54.                 } 
  55.             } 
  56.             if (lockChannel != null) { 
  57.                 closeQuietly(lockChannel); 
  58.             } 
  59.             closeQuietly(lockRaf); 
  60.         } 
  61.         if (releaseLockException != null) { 
  62.             throw releaseLockException; 
  63.         } 
  64.         Log.i(TAG, "load found " + files.size() + " secondary dex files"); 
  65.         return files; 
  66.     }  

這個(gè)過(guò)程主要是獲取可以安裝的dex文件列表,可以是上次解壓出來(lái)的緩存文件,也可以是重新從Apk包里面提取出來(lái)的。需要注意的時(shí),如果是重新解壓,這里會(huì)有明顯的耗時(shí),而且解壓出來(lái)的dex文件,會(huì)被壓縮成.zip壓縮包,壓縮的過(guò)程也會(huì)有明顯的耗時(shí)(這里壓縮dex文件可能是問(wèn)了節(jié)省空間)。

如果dex文件是重新解壓出來(lái)的,則會(huì)保存dex文件的信息,包括解壓的apk文件的crc值、修改時(shí)間以及dex文件的數(shù)目,以便下一次啟動(dòng)直接使用已經(jīng)解壓過(guò)的dex緩存文件,而不是每一次都重新解壓。

需要特別提到的是,里面的FileLock是最新的master分支里面新加進(jìn)去的功能,現(xiàn)在最新的1.0.1版本里面是沒(méi)有的。

無(wú)論是通過(guò)使用緩存的dex文件,還是重新從apk中解壓dex文件,獲取dex文件列表后,下一步就是安裝(或者說(shuō)加載)這些dex文件了。最后的工作在MultiDex#installSecondaryDexes這個(gè)方法里面。

  1. private static void installSecondaryDexes(ClassLoader loader, File dexDir, List<File> files) 
  2.             throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, 
  3.             InvocationTargetException, NoSuchMethodException, IOException { 
  4.         if (!files.isEmpty()) { 
  5.             if (Build.VERSION.SDK_INT >= 19) { 
  6.                 V19.install(loader, files, dexDir); 
  7.             } else if (Build.VERSION.SDK_INT >= 14) { 
  8.                 V14.install(loader, files, dexDir); 
  9.             } else { 
  10.                 V4.install(loader, files); 
  11.             } 
  12.         } 
  13.     }  

因?yàn)樵诓煌腟DK版本上,ClassLoader(更準(zhǔn)確來(lái)說(shuō)是DexClassLoader)加載dex文件的方式有所不同,所以這里做了V4/V14/V19的兼容(Magic Code)。

Build.VERSION.SDK_INT < 14 

  1. /** 
  2.      * Installer for platform versions 4 to 13. 
  3.      */ 
  4.     private static final class V4 { 
  5.         private static void install(ClassLoader loader, List<File> additionalClassPathEntries) 
  6.                         throws IllegalArgumentException, IllegalAccessException, 
  7.                         NoSuchFieldException, IOException { 
  8.              
  9.             int extraSize = additionalClassPathEntries.size(); 
  10.             Field pathField = findField(loader, "path"); 
  11.             StringBuilder path = new StringBuilder((String) pathField.get(loader)); 
  12.             String[] extraPaths = new String[extraSize]; 
  13.             File[] extraFiles = new File[extraSize]; 
  14.             ZipFile[] extraZips = new ZipFile[extraSize]; 
  15.             DexFile[] extraDexs = new DexFile[extraSize]; 
  16.             for (ListIterator<File> iterator = additionalClassPathEntries.listIterator(); 
  17.                     iterator.hasNext();) { 
  18.                 File additionalEntry = iterator.next(); 
  19.                 String entryPath = additionalEntry.getAbsolutePath(); 
  20.                 path.append(':').append(entryPath); 
  21.                 int index = iterator.previousIndex(); 
  22.                 extraPaths[index] = entryPath; 
  23.                 extraFiles[index] = additionalEntry; 
  24.                 extraZips[index] = new ZipFile(additionalEntry); 
  25.                 extraDexs[index] = DexFile.loadDex(entryPath, entryPath + ".dex", 0); 
  26.             } 
  27.              
  28.             // 這個(gè)版本是最簡(jiǎn)單的。 
  29.             // 只需要?jiǎng)?chuàng)建DexFile對(duì)象后,使用反射的方法分別擴(kuò)展ClassLoader實(shí)例的以下字段即可。 
  30.             pathField.set(loader, path.toString()); 
  31.             expandFieldArray(loader, "mPaths", extraPaths); 
  32.             expandFieldArray(loader, "mFiles", extraFiles); 
  33.             expandFieldArray(loader, "mZips", extraZips); 
  34.             expandFieldArray(loader, "mDexs", extraDexs); 
  35.         } 
  36.     }  

14 <= Build.VERSION.SDK_INT < 19 

  1. /** 
  2.      * Installer for platform versions 14, 15, 16, 17 and 18. 
  3.      */ 
  4.     private static final class V14 { 
  5.         private static void install(ClassLoader loader, List<File> additionalClassPathEntries, 
  6.                 File optimizedDirectory) 
  7.                         throws IllegalArgumentException, IllegalAccessException, 
  8.                         NoSuchFieldException, InvocationTargetException, NoSuchMethodException { 
  9.              
  10.             // 擴(kuò)展ClassLoader實(shí)例的"pathList"字段。 
  11.             Field pathListField = findField(loader, "pathList"); 
  12.             Object dexPathList = pathListField.get(loader); 
  13.             expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, 
  14.                     new ArrayList<File>(additionalClassPathEntries), optimizedDirectory)); 
  15.         } 
  16.        
  17.         private static Object[] makeDexElements( 
  18.                 Object dexPathList, ArrayList<File> files, File optimizedDirectory) 
  19.                         throws IllegalAccessException, InvocationTargetException, 
  20.                         NoSuchMethodException { 
  21.             Method makeDexElements = 
  22.                     findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class); 
  23.             return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory); 
  24.         } 
  25.     }  

從API14開(kāi)始,DexClassLoader會(huì)使用一個(gè)DexpDexPathList類(lèi)來(lái)封裝DexFile數(shù)組。 

  1. final class DexPathList { 
  2.     private static final String DEX_SUFFIX = ".dex"
  3.     private static final String JAR_SUFFIX = ".jar"
  4.     private static final String ZIP_SUFFIX = ".zip"
  5.     private static final String APK_SUFFIX = ".apk"
  6.    
  7.     private static Element[] makeDexElements(ArrayList<File> files, 
  8.             File optimizedDirectory) { 
  9.         ArrayList<Element> elements = new ArrayList<Element>(); 
  10.         for (File file : files) { 
  11.             ZipFile zip = null
  12.             DexFile dex = null
  13.             String name = file.getName(); 
  14.             if (name.endsWith(DEX_SUFFIX)) { 
  15.                 // Raw dex file (not inside a zip/jar). 
  16.                 try { 
  17.                     dex = loadDexFile(file, optimizedDirectory); 
  18.                 } catch (IOException ex) { 
  19.                     System.logE("Unable to load dex file: " + file, ex); 
  20.                 } 
  21.             } else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX) 
  22.                     || name.endsWith(ZIP_SUFFIX)) { 
  23.                 try { 
  24.                     zip = new ZipFile(file); 
  25.                 } catch (IOException ex) { 
  26.                     System.logE("Unable to open zip file: " + file, ex); 
  27.                 } 
  28.                 try { 
  29.                     dex = loadDexFile(file, optimizedDirectory); 
  30.                 } catch (IOException ignored) { 
  31.       
  32.                 } 
  33.             } else { 
  34.                 System.logW("Unknown file type for: " + file); 
  35.             } 
  36.             if ((zip != null) || (dex != null)) { 
  37.                 elements.add(new Element(file, zip, dex)); 
  38.             } 
  39.         } 
  40.         return elements.toArray(new Element[elements.size()]); 
  41.     } 
  42.     
  43.     private static DexFile loadDexFile(File file, File optimizedDirectory) 
  44.             throws IOException { 
  45.         if (optimizedDirectory == null) { 
  46.             return new DexFile(file); 
  47.         } else { 
  48.             String optimizedPath = optimizedPathFor(file, optimizedDirectory); 
  49.             return DexFile.loadDex(file.getPath(), optimizedPath, 0); 
  50.         } 
  51.     } 
  52.  

通過(guò)調(diào)用DexPathList#makeDexElements方法,可以加載我們上面解壓得到的dex文件,從代碼也可以看出,DexPathList#makeDexElements其實(shí)也是通過(guò)調(diào)用DexFile#loadDex來(lái)加載dex文件并創(chuàng)建DexFile對(duì)象的。V14中,通過(guò)反射調(diào)用DexPathList#makeDexElements方法加載我們需要的dex文件,在把加載得到的數(shù)組擴(kuò)展到ClassLoader實(shí)例的"pathList"字段,從而完成dex文件的安裝。

從DexPathList的代碼中我們也可以看出,ClassLoader是支持直接加載.dex/.zip/.jar/.apk的dex文件包的(我記得以前在哪篇日志中好像提到過(guò)類(lèi)似的問(wèn)題…)。

19 <= Build.VERSION.SDK_INT 

  1. /** 
  2.     * Installer for platform versions 19. 
  3.     */ 
  4.    private static final class V19 { 
  5.        private static void install(ClassLoader loader, List<File> additionalClassPathEntries, 
  6.                File optimizedDirectory) 
  7.                        throws IllegalArgumentException, IllegalAccessException, 
  8.                        NoSuchFieldException, InvocationTargetException, NoSuchMethodException { 
  9.           
  10.            Field pathListField = findField(loader, "pathList"); 
  11.            Object dexPathList = pathListField.get(loader); 
  12.            ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>(); 
  13.            expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, 
  14.                    new ArrayList<File>(additionalClassPathEntries), optimizedDirectory, 
  15.                    suppressedExceptions)); 
  16.                     
  17.            if (suppressedExceptions.size() > 0) { 
  18.                for (IOException e : suppressedExceptions) { 
  19.                    Log.w(TAG, "Exception in makeDexElement", e); 
  20.                } 
  21.                Field suppressedExceptionsField = 
  22.                        findField(dexPathList, "dexElementsSuppressedExceptions"); 
  23.                IOException[] dexElementsSuppressedExceptions = 
  24.                        (IOException[]) suppressedExceptionsField.get(dexPathList); 
  25.                if (dexElementsSuppressedExceptions == null) { 
  26.                    dexElementsSuppressedExceptions = 
  27.                            suppressedExceptions.toArray( 
  28.                                    new IOException[suppressedExceptions.size()]); 
  29.                } else { 
  30.                    IOException[] combined = 
  31.                            new IOException[suppressedExceptions.size() + 
  32.                                            dexElementsSuppressedExceptions.length]; 
  33.                    suppressedExceptions.toArray(combined); 
  34.                    System.arraycopy(dexElementsSuppressedExceptions, 0, combined, 
  35.                            suppressedExceptions.size(), dexElementsSuppressedExceptions.length); 
  36.                    dexElementsSuppressedExceptions = combined; 
  37.                } 
  38.                suppressedExceptionsField.set(dexPathList, dexElementsSuppressedExceptions); 
  39.            } 
  40.        } 
  41.     
  42.        private static Object[] makeDexElements( 
  43.                Object dexPathList, ArrayList<File> files, File optimizedDirectory, 
  44.                ArrayList<IOException> suppressedExceptions) 
  45.                        throws IllegalAccessException, InvocationTargetException, 
  46.                        NoSuchMethodException { 
  47.            Method makeDexElements = 
  48.                    findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class, 
  49.                            ArrayList.class); 
  50.            return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory, 
  51.                    suppressedExceptions); 
  52.        } 
  53.    }  

V19與V14差別不大,只不過(guò)DexPathList#makeDexElements方法多了一個(gè)ArrayList<IOException>參數(shù),如果在執(zhí)行DexPathList#makeDexElements方法的過(guò)程中出現(xiàn)異常,后面使用反射的方式把這些異常記錄進(jìn)DexPathList的dexElementsSuppressedExceptions字段里面。

無(wú)論是V4/V14還是V19,在創(chuàng)建DexFile對(duì)象的時(shí)候,都需要通過(guò)DexFile的Native方法openDexFile來(lái)打開(kāi)dex文件,其具體細(xì)節(jié)暫不討論(涉及到dex的文件結(jié)構(gòu),很煩,有興趣請(qǐng)閱讀dalvik_system_DexFile.cpp),這個(gè)過(guò)程的主要目的是給當(dāng)前的dex文件做Optimize優(yōu)化處理并生成相同文件名的odex文件,App實(shí)際加載類(lèi)的時(shí)候,都是通過(guò)odex文件進(jìn)行的。因?yàn)槊總€(gè)設(shè)備對(duì)odex格式的要求都不一樣,所以這個(gè)優(yōu)化的操作只能放在安裝Apk的時(shí)候處理,主dex的優(yōu)化我們已經(jīng)在安裝apk的時(shí)候搞定了,其余的dex就是在MultiDex#installSecondaryDexes里面優(yōu)化的,而后者也是MultiDex過(guò)程中,另外一個(gè)耗時(shí)比較多的操作。(在MultiDex中,提取出來(lái)的dex文件被壓縮成.zip文件,又優(yōu)化后的odex文件則被保存為.dex文件。)

到這里,MultiDex的工作流程就結(jié)束了。怎么樣,是不是覺(jué)得和以前談到動(dòng)態(tài)加載技術(shù)(插件化)的時(shí)候說(shuō)的很像?沒(méi)錯(cuò),誰(shuí)叫它們的核心都是dex文件呢。Java老師第一節(jié)課就說(shuō)“類(lèi)就是編程”,搞定類(lèi)你就能搞定整個(gè)世界啊!

優(yōu)化方案

MultiDex有個(gè)比較蛋疼的問(wèn)題,就是會(huì)產(chǎn)生明顯的卡頓現(xiàn)象,通過(guò)上面的分析,我們知道具體的卡頓產(chǎn)生在解壓dex文件以及優(yōu)化dex兩個(gè)步驟。不過(guò)好在,在Application#attachBaseContext(Context)中,UI線程的阻塞是不會(huì)引發(fā)ANR的,只不過(guò)這段長(zhǎng)時(shí)間的卡頓(白屏)還是會(huì)影響用戶(hù)體驗(yàn)。

目前,優(yōu)化方案能想到的有兩種。

PreMultiDex方案

大致思路是,在安裝一個(gè)新的apk的時(shí)候,先在Worker線程里做好MultiDex的解壓和Optimize工作,安裝apk并啟動(dòng)后,直接使用之前Optimize產(chǎn)生的odex文件,這樣就可以避免第一次啟動(dòng)時(shí)候的Optimize工作。 

 

 

 

安裝dex的時(shí)候,核心是創(chuàng)建DexFile對(duì)象并使用其N(xiāo)ative方法對(duì)dex文件進(jìn)行opt處理,同時(shí)生產(chǎn)一個(gè)與dex文件(.zip)同名的已經(jīng)opt過(guò)的dex文件(.dex)。如果安裝dex的時(shí)候,這個(gè)opt過(guò)的dex文件已經(jīng)存在,則跳過(guò)這個(gè)過(guò)程,這會(huì)節(jié)省許多耗時(shí)。所以?xún)?yōu)化的思路就是,下載Apk完成的時(shí)候,預(yù)先解壓dex文件,并預(yù)先觸發(fā)安裝dex文件以生產(chǎn)opt過(guò)的dex文件。這樣覆蓋安裝Apk并啟動(dòng)的時(shí)候,如果MultiDex能命中解壓好的dex和odex文件,則能避開(kāi)耗時(shí)最大的兩個(gè)操作。

不過(guò)這個(gè)方案的缺點(diǎn)也是明顯的,第一次安裝的apk沒(méi)有作用,而且事先需要使用內(nèi)置的apk更新功能把新版本的apk文件下載下來(lái)后,才能做PreMultiDex工作。

異步MultiDex方案

這種方案也是目前比較流行的Dex手動(dòng)分包方案,啟動(dòng)App的時(shí)候,先顯示一個(gè)簡(jiǎn)單的Splash閃屏界面,然后啟動(dòng)Worker線程執(zhí)行MultiDex#install(Context)工作,就可以避免UI線程阻塞。不過(guò)要確保啟動(dòng)以及啟動(dòng)MultiDex#install(Context)所需要的類(lèi)都在主dex里面(手動(dòng)分包),而且需要處理好進(jìn)程同步問(wèn)題。

參考資料:

著作信息:

本文章出自 Kaede 的博客,原創(chuàng)文章若無(wú)特別說(shuō)明,均遵循 CC BY-NC 4.0 知識(shí)共享許可協(xié)議4.0(署名-非商用-相同方式共享),可以隨意摘抄轉(zhuǎn)載,但必須標(biāo)明署名及原地址。

責(zé)任編輯:龐桂玉 來(lái)源: segmentfault
相關(guān)推薦

2010-11-25 10:28:28

MySQL查詢(xún)優(yōu)化器

2024-11-11 14:55:48

2010-09-26 08:50:11

JVM工作原理

2009-07-09 14:01:22

JVM工作原理

2015-07-02 09:56:48

ReactiveCociOS

2023-09-27 12:22:50

Kafka架構(gòu)

2010-09-16 14:42:44

JVM

2011-07-01 11:16:14

Struts

2010-09-08 20:20:39

2010-09-17 15:32:52

JVM工作原理

2025-08-04 02:55:00

AIAgent架構(gòu)

2023-06-08 15:27:17

CAN網(wǎng)絡(luò)

2021-03-18 09:07:20

Nginx原理實(shí)踐

2012-03-01 10:39:46

數(shù)據(jù)中心空調(diào)維護(hù)

2009-11-13 17:20:35

ADO.NET數(shù)據(jù)集工

2023-02-26 01:00:12

索引優(yōu)化慢查詢(xún)

2021-01-24 11:46:26

自動(dòng)化Web 優(yōu)化

2009-10-22 14:26:58

智能布線解決方案

2025-07-31 01:55:00

await性能流式格式

2017-05-17 08:51:39

WebView分析應(yīng)用
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)

日韩成人动漫| 国产麻豆91视频| 午夜a一级毛片亚洲欧洲| 色婷婷国产精品| 视频一区二区三区在线观看| 国产成年妇视频| 久久午夜精品一区二区| 欧美成人网在线| 91视频在线网站| 精品视频在线一区| 色吊一区二区三区| 国产精品久久久久久久久电影网| 男人天堂综合| 国产一区二区导航在线播放| 97色伦亚洲国产| 91香蕉视频在线播放| 妖精视频一区二区三区免费观看| 日韩女优av电影| 国产一区二区在线免费播放| 岛国毛片av在线| 国产精品日韩成人| 蜜桃麻豆91| 蜜臀av在线观看| 国产在线精品一区二区| 国产精品久在线观看| 国产一级理论片| 亚洲成人精品| 中文字幕日韩免费视频| 精品人妻一区二区三区香蕉| 亚洲成av人片在线观看www| 欧美日韩一区小说| 日本成人在线免费视频| 美女搞黄视频在线观看| 亚洲影视在线观看| 免费看黄色a级片| 青青青青在线| 国产精品色婷婷久久58| 奇米影视首页 狠狠色丁香婷婷久久综合 | 亚洲熟妇无码av| 亚洲视频精选| 日韩一二三区不卡| 一级片黄色免费| 欧美美女被草| 欧美日韩美女一区二区| 在线免费观看视频黄| 成人看片网站| 欧美亚日韩国产aⅴ精品中极品| 日本在线观看a| 不卡一二三区| 日本久久一区二区| 手机看片福利日韩| 国产精品99| 欧美日韩国产大片| 三日本三级少妇三级99| **国产精品| 91精品中文字幕一区二区三区 | 国产不卡视频一区二区三区| 91在线播放视频| 亚洲国产剧情在线观看| 成人精品一区二区三区中文字幕| 成人av蜜桃| 人妻与黑人一区二区三区| 97se亚洲国产综合自在线| 精品欧美日韩在线| 国产综合视频一区二区三区免费| 国产欧美日韩麻豆91| 亚欧洲精品在线视频免费观看| av福利精品| 亚洲欧美区自拍先锋| 国产av熟女一区二区三区| ****av在线网毛片| 在线观看一区二区视频| 成年网站免费在线观看| 国产精品三p一区二区| 亚洲美女性视频| 又色又爽的视频| 欧美精品国产一区二区| 91av在线不卡| 一区二区三区免费在线视频| 国产盗摄精品一区二区三区在线| 精品国产乱码久久久久久郑州公司 | 国产av无码专区亚洲av麻豆| 97久久久精品综合88久久| 欧美日韩一区二区视频在线观看| 婷婷五月在线视频| 亚洲国产一区视频| 亚洲国产精品三区| 超碰一区二区三区| 中文字幕精品国产| 国产大片aaa| 日韩av午夜在线观看| 99国产在线| 国产福利在线观看| 亚洲电影一区二区三区| 久久久久国产一区| 巨人精品**| 精品国内自产拍在线观看| 日韩精品成人一区| 国精产品一区一区三区mba视频| 久草热久草热线频97精品| 黄网站在线播放| 色一情一乱一乱一91av| 亚洲成年人av| 97人人精品| 国产成人拍精品视频午夜网站 | 2019中文亚洲字幕| 国产亚洲精品综合一区91| 欧美又粗又大又长| 日本视频在线一区| 九九九九九九精品| 污视频网站在线免费| 欧美色手机在线观看| 97香蕉碰碰人妻国产欧美| 亚洲国产日韩欧美在线| 国产成人精品视频在线观看| 噜噜噜久久,亚洲精品国产品| 国产欧美1区2区3区| www在线观看免费| 毛片在线视频播放| 麻豆一区在线| 丝袜亚洲另类欧美重口| 欧美一级特黄视频| 成人教育av在线| 亚洲av首页在线| 狂野欧美性猛交xxxx| 亚洲精品资源美女情侣酒店| 久久久香蕉视频| 国产呦萝稀缺另类资源| 亚洲精品一区二区三区av| 久久电影tv| 亚洲欧美日韩中文在线| 国产又黄又粗又爽| 99久久综合色| 少妇高潮喷水在线观看| 国偷自产av一区二区三区| 久久99精品久久久久久青青91| 97超碰人人草| 亚洲丝袜另类动漫二区| 国产欧美激情视频| 97精品国产| 国产日韩中文字幕在线| 香蕉视频在线看| 制服视频三区第一页精品| 欧美性生给视频| 精品一区二区免费视频| 天堂av免费看| 五月亚洲婷婷| 欧美黑人xxxⅹ高潮交| 午夜老司机福利| 亚洲午夜在线观看视频在线| 娇妻高潮浓精白浆xxⅹ| 狠狠综合久久| 久久波多野结衣| 欧美亚洲韩国| 中文字幕无线精品亚洲乱码一区 | 国产精品乱人伦| 91n.com在线观看| 91视频综合| 96pao国产成视频永久免费| 四虎亚洲精品| 日韩成人在线免费观看| chinese国产精品| 中文字幕不卡的av| 国产精品嫩草影视| 狠狠色综合网| 日本免费高清不卡| 国产高清日韩| 97香蕉久久超级碰碰高清版| 国产精品久久久久一区二区国产| 欧美裸体一区二区三区| 激情综合五月网| 久久久午夜精品| 国产三级生活片| 黄色av日韩| 日本精品一区二区三区视频 | 久久手机视频| 久久精品黄色| 国内精品400部情侣激情| 欧美套图亚洲一区| 欧美高清性hdvideosex| 精品无码久久久久久久久| 久久久亚洲午夜电影| 不卡的在线视频| 国产亚洲在线| 一本一道久久a久久精品综合| 亚洲不卡在线| 国产精品午夜国产小视频| 欧美午夜大胆人体| 国产一区二区三区视频免费| 亚洲va天堂va欧美ⅴa在线| 色综合天天天天做夜夜夜夜做| 午夜精品福利在线视频| 久久综合色婷婷| 色黄视频免费看| 久久激情中文| 欧美视频在线第一页| 精品国产91乱码一区二区三区四区| 亚洲自拍偷拍网址| 色婷婷综合久久久中字幕精品久久| 欧美精品在线观看| 成人亚洲综合天堂| 亚洲国产欧美一区二区三区久久| 国产精品人人妻人人爽| 色婷婷av一区二区三区之一色屋| 国产一级一片免费播放放a| 亚洲欧洲精品天堂一级| 一区二区三区伦理片| 粉嫩av一区二区三区在线播放| 成人性生生活性生交12| 一本久道综合久久精品| 日韩人妻一区二区三区蜜桃视频| 婷婷精品在线| 国产精品久久波多野结衣| 2019中文亚洲字幕| 国产免费亚洲高清| 外国成人直播| 亲子乱一区二区三区电影| 成年人视频免费在线播放| 精品中文字幕在线观看| 久久久久久久久免费视频| 中文字幕欧美专区| 超碰免费97在线观看| 亚洲欧美日韩国产成人| 午夜视频www| 亚洲国产古装精品网站| 亚洲精品久久久久avwww潮水| 6080日韩午夜伦伦午夜伦| 一本久道久久综合无码中文| 91久久免费观看| 日批视频免费在线观看| 懂色av一区二区三区| 日韩精品手机在线| 午夜精品久久久久| 日韩免费视频网站| 午夜精品成人在线| 久草精品视频在线观看| 亚洲午夜久久久久久久久久久| 国产精品久久久久久久精| 亚洲日本一区二区| 色婷婷在线视频观看| 一区二区三区蜜桃| 久久久一二三区| 午夜久久福利影院| 国产免费av一区二区| 欧美日韩激情小视频| 国产一级免费视频| 91高清在线观看| 在线免费av片| 51精品国自产在线| 亚洲AV无码国产精品午夜字幕| 精品人伦一区二区色婷婷| 二区三区在线视频| 日韩av在线免费观看一区| 青青草免费在线| 中文字幕日韩有码| www免费视频观看在线| 欧美精品18videosex性欧美| 九色porny丨入口在线| 欧美专区日韩视频| 九七电影院97理论片久久tvb| 91久久久久久久一区二区| 午夜免费欧美电影| 欧美理论一区二区| 91麻豆精品国产91久久久平台| 一级黄色免费在线观看| 韩日在线一区| 无码人妻h动漫| 老司机午夜精品| 亚洲欧美激情一区二区三区| av一二三不卡影片| 正在播放国产对白害羞| 亚洲综合免费观看高清完整版| 国产福利拍拍拍| 欧美视频在线一区二区三区 | 亚州精品天堂中文字幕| 青青热久免费精品视频在线18| 91久久精品国产| 久久夜色电影| 亚洲永久激情精品| 亚洲第一毛片| 三上悠亚av一区二区三区| 成人免费精品视频| 亚洲高潮女人毛茸茸| 亚洲制服丝袜av| 69av视频在线观看| 欧美成人性福生活免费看| 久久米奇亚洲| 欧美大片在线看| 色香欲www7777综合网| 97久草视频| 欧美日韩在线网站| 日韩五码在线观看| 国产在线精品免费av| 91中文字幕永久在线| 亚洲精品菠萝久久久久久久| 中文字幕一区二区三区四区欧美| 日韩欧美在线1卡| av中文在线| 97国产精品久久| 亚洲天堂网站| 欧美一区二区综合| 亚洲视频一区| 亚洲免费在线播放视频| 久久久久久久久免费| 久久久久久久久久综合| 欧美日韩国产一二三| 欧美另类自拍| 国产69精品久久久久9| 成人在线分类| 亚洲欧洲一区二区| 丝袜亚洲另类欧美综合| 丰满岳乱妇一区二区| 亚洲欧美日韩中文播放 | 成人性生活av| 精品亚洲欧美日韩| 国产精品黄色| 亚洲一区二区偷拍| 国产精品进线69影院| 国产性生活视频| 日韩精品在线视频| av女在线播放| 国产精品一 二 三| 欧美人成在线| 男生和女生一起差差差视频| 国产精品免费丝袜| 中文字幕日产av| 一区二区三欧美| av一区在线播放| 秋霞久久久久久一区二区| 国产精品久久国产愉拍| 亚洲av无码一区二区三区网址| 亚洲444eee在线观看| 亚洲精品无码久久久| 欧美国产日韩一区二区在线观看 | 亚洲欧美日本精品| xx欧美xxx| 日韩国产欧美精品| 日本在线不卡视频一二三区| 日韩女同一区二区三区| 欧美亚洲综合网| 99免在线观看免费视频高清| 国产精品美女av| 久久视频精品| 午夜免费福利网站| 亚洲国产综合色| 午夜视频福利在线| 青青草成人在线| 欧美日韩中文一区二区| 色片在线免费观看| 亚洲欧洲99久久| 亚洲精品视频91| 欧美亚洲国产日韩2020| 九一成人免费视频| 五月婷婷六月合| 亚洲人成网站在线| 性中国古装videossex| 91av在线播放视频| 欧美色女视频| 人妻换人妻仑乱| 欧美日韩国产中文字幕 | 亚洲成人高清在线| 九色在线播放| 国产日本欧美一区二区三区在线| 91精品99| 免费的av网站| 欧美亚洲国产一区在线观看网站 | 少妇熟女视频一区二区三区 | 久久久久久午夜| 中文有码一区| 中文字幕剧情在线观看| 午夜精品成人在线| 色影院视频在线| 爱情岛论坛亚洲入口| 视频精品一区二区| 欧美成人精品一区二区免费看片| 亚洲精品久久久久| 久久亚洲人体| 国产欧美日韩网站| 国产精品久线在线观看| 欧美一级视频免费| 国产精品一区二区三| 亚洲国产清纯| 夫妇露脸对白88av| 亚洲成人精品久久| 日本欧美一区| 免费高清一区二区三区| 久久精品人人做人人综合 | 999精品嫩草久久久久久99| 国产一级爱c视频| 国产精品久久久久精k8| 天天摸夜夜添狠狠添婷婷| 成人在线播放av| 蜜桃av一区| 精品无码人妻一区二区三区品| 国产一区二区三区中文| 精品久久对白| 992tv人人草| 在线观看国产日韩| 岛国片av在线| 日韩欧美一级在线|