Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android動態加載學習總結(一):類加載器

Android動態加載學習總結(一):類加載器

編輯:關於Android編程

前言:動態加載在應用開發中有著很重要的地位,當我們項目越來越大,我們可以通過插件化來減少應用的內存,然後動態加載那些插件。還有一個方面,如果我們的應用頻繁的更新,頻繁的發布新版本,肯定會造成用戶體驗下降,那麼我們可以用動態加載技術在不發布新版本的情況下更新一些模塊。

那麼既然要用動態加載,就肯定涉及到類加載器,我們先看一下Java中的類加載器。

一、Java中類加載器

在這裡,我們不去說類加載的具體過程,只是總結一下Java中類加載器與雙親委派模型。

1.類加載器與類本身確定類的唯一性

對於一個類,這個類本身和加載它的類加載器共同確定其在虛擬機中的唯一性。
我們使用兩個類加載器進行加載同一個類,那麼這兩個類是不相等的,那麼虛擬機中會存在兩個同名的類。

2.Java三種預定義類型類加載器

啟動類加載器(Bootstrap ClassLoader,也稱為引導類加載器)

該類加載器負責將存放在\lib目錄中,或者被-Xbootclasspath參數所指定的路徑中的,並且是虛擬機識別的(這點很重要)類加載到虛擬機中。

加載虛擬機識別的這一點與雙親委派模型配合很重要。在下面介紹,先留意這一點。

對於啟動類加載器還有一點需要注意,也是和雙親委派模型有關,如果我們自定義一個類加載器,想把一個加載請求委派給啟動類加載器,只需要使用null替代即可(可以看下面的loadClass()方法中的代碼實現)。

開發者不可以直接使用該加載器。

擴展類加載器(Extension ClassLoader)

該加載器由sun.misc.Launcher$ExtClassLoader實現,負責加載\lib\ext目錄中的,或者被java.ext.dirs系統變量所指定的路程中的所有類庫。

開發者可以直接使用該加載器。

應用程序類加載器(Application ClassLoader,也稱為系統類加載器)

該類加載器由sun.misc.Launcher$AppClassLoader實現。由於這個類加載器是ClassLoader中的getSystemClassLoader()方法的返回值,所以也稱為系統類加載器。該加載器負責加載用戶類路徑(ClassPath)上所指定的類庫。

開發者可以直接使用這個類加載器。如果應用程序中沒有自定義過自己的類加載器,一般情況下該加載器是程序中的默認的類加載器。

3.雙親委派模型

這裡寫圖片描述

上圖顯示了類加載器之間的層次關系,被稱為類加載器的雙親委派模型。

除了頂層的啟動類加載器外,其余的類加載器都應當有自己的父類加載器。

那麼根據上圖,如果一個類加載器收到了類加載的請求,它首先不會自己嘗試加載這個類,而是把請求委派給父類加載器去加載,每一個層次的類加載器都是這樣,那麼所有的請求其實最後都是會到啟動類加載器中,如果父類加載器反饋無法加載,子加載器才會嘗試自己加載。

實現雙親委派模型的代碼在loadClass()方法中:

protected Class loadClass(String className, boolean resolve) throws ClassNotFoundException {  
//檢查請求加載的類是否已經加載過了
Class c = findLoadedClass(className);  
    //如果沒有,進行以下步驟
    if (clazz == null) {  
        try {  
        //嘗試使用父類加載器加載,父類加載器不為空,則使用父類加載器嘗試加載
            if(parent != null){
            c = parent.loadClass(className, false);
            }
            //如果父類加載器為null,則使用啟動類加載器作為父加載器,關於這一點,參考上面啟動類加載器的介紹
            else{
            c = findBootstrapClassOrNull(name);  
            }
        } catch (ClassNotFoundException e) {  
            // 如果父類加載器拋出異常,說明父類加載器無法完成加載請求
        }  
        if (c == null) {  
        //父類無法完成加載時調用本身的findClass方法來進行類加載
            c = findClass(className);  
        }  
    }  
    if(resolve){
    resolveClass(c);
    }
    //如果加載過了,就直接返回已經加載的類
    return clazz;  
} 

4.雙親委派模型的優點

大家都知道Object類是個基礎類,如果我們自己寫了一個Object類,那麼如果沒有雙親委派模型的話,再加上我們並沒有用啟動類加載器去加載我們寫的這個Object類的話,系統中會存在兩個Object類(參考上述的類在虛擬機中的唯一性)。那麼有了雙親委派模型,我們寫了一個Object類,會先去檢查它是否加載了(肯定已經加載了),那麼我們寫的這個就不會被重復加載,也就保證了基礎類的唯一性,防止混亂破壞。就算沒有檢查,根據上面關於啟動類加載器的介紹,必須是虛擬機識別的,Object存放在rt.jar中,我們寫的不會被識別。

那麼從另一個方面,基礎類Object怎麼保證的在任何環境下都是同一個類(即加載器在任何情況下都是同一個),這就是雙親委派模型的作用了,每次這個加載請求都會委派給處於最頂端的啟動類加載器進行加載,虛擬機識別rt.jar,那麼就保證了每次都是由啟動類加載器加載的Object,那麼根據第1條的唯一性,這樣做就保證了Object的唯一性。

5.自定義類加載器符合雙親委派模型

我們根據上面的介紹知道,雙親委派模型的邏輯都在loadClass()方法中,那麼我們為了不破壞雙親委派模型,自定義類加載器時不去重寫loadClass()方法,而是重寫findClass()方法,將自己的類加載邏輯寫到findClass()方法中,在loadClass()方法中,最後父類加載器無法加載的時候,調用的就是findClass()方法。這樣我們就保證了我們自定義的類加載器是符合雙親委派模型的。如果重寫loadClass()方法,會出現一系列錯誤,比如基礎類加載不上等。

findClass()方法是在JDK1.2以後引入的。

5.defineClass()方法

//定義類型,一般在findClass方法中讀取到對應字節碼後調用,final,不可繼承,一般不用重寫,直接調用。在findClass()方法中調用。

protected final Class defineClass(String name, byte[] b, int off, int len) throws ClassFormatError
{ … } 

介紹這個是為了注明下面的Android類加載與Java中類加載的區別。

二、Android類加載器

前面介紹的是Java中的類加載器,我們知道android中的虛擬機是Dalvik,它不是標准的Java虛擬機,所以在類加載機制上,和Java中的類加載器有一些區別。

我們在java標准的虛擬機中,如果自定義類加載器,會繼承ClassLoader,並重寫findClass()方法,在內部調用defineClass()去從一個二進制流中加載Class。那麼在Android中,這個defineClass()方法去調用VMClassLoader的defineClass本地靜態方法,而這個方法內部除了拋出了異常“UnsupportedOperationException”還有一些屬性值,其他什麼都沒做。在博客開頭寫的鏈接中有源碼,這就不貼了。

那麼在Dalvik虛擬中,動態加載類就需要另外由ClassLoader派生出的兩個類:DexClassLoader和PathClassLoader。

這兩個類重載了ClassLoader的findClass()方法,並沒有重寫loadClass()方法,所以這兩個類加載器符合雙親委派模型。

在介紹這兩個類加載器區別之前,說明兩點
(1)Dalvik虛擬機識別的是dex文件,而不是class文件,因此,我們加載的是dex文件,或者包含dex文件的apk文件或jar文件。
(2)DexFile類。上述兩個類加載器都是通過DexFile這個類去加載類。

區別:
PathClassLoader不能主動從zip包中釋放出dex,所以只支持直接操作dex格式文件,或者已經安裝的apk(已經安裝的apk在cache中存在緩存的dex文件)。而DexClassLoader可以支持.apk、.jar和.dex文件,並且會在指定的outpath路徑釋放出dex文件。

下面會有加載的demo。

加載好類以後,我們可以通過反射來使用加載好的類,但過多的使用反射會有一定的性能開銷,代碼復雜凌亂。我們還有一種方式,即接口。我們可以將一些方法提取出來作為一個接口,將待加載的類實現這個接口,在寫待加載的類時,注意要有一個參數為空的構造函數,我們在主代碼中就能將類對象強制轉為接口對象,直接調用成員方法。

三、Demo

這個Demo是Android中的動態加載機制裡的,為了展示,簡略了一下,並且由於android系統的變更,其中有一些小坑,我們需要改一下代碼。

1.接口與待加載類的實現與處理

這裡寫圖片描述

接口代碼
public interface IDynamic {
    void init(Activity var1);
    //彈出消息
    void showSomething();

    void destory();
}
待加載類
//導入接口
import com.example.interfaces.IDynamic;

import android.app.Activity;
import android.widget.Toast;
//實現上述的接口
public class Dynamic implements IDynamic{  

    private Activity mActivity;

    @Override
    public void init(Activity activity) {
        mActivity = activity;
    }
//復寫接口的showSomething()方法,內部代碼只是彈出一個消息
    @Override
    public void showSomething() {
        Toast.makeText(mActivity, "something is here!", Toast.LENGTH_SHORT).show();
    }



    @Override
    public void destory() {
    }

}  

接口的處理

將接口導出一個jar包(由於AS導包得修改Gradle,嫌麻煩加上電腦上有Eclipse for Android ,我用它打的包):

這裡寫圖片描述

然後將接口的jar包放入AS工程裡的libs目錄下。

待加載類的處理

將待加載類按上述方式打包,將打好的jar包復制到SDK的build-tools目錄下,打開命令行,進入build-tools目錄,輸入:
dx –dex –output dynamic1.apk dynamic.jar
注意:上面dex和output前是兩個‘-’ ,csdn顯示的是一個,只不過長度長一點,坑了我一會。
如下圖:
這裡寫圖片描述

經過上述步驟,build-tools目錄下就會生成一個名為dynamic1.apk的文件,那麼上述步驟究竟做了什麼?

我們知道DexClassLoader和PathClassLoader這兩個類加載器加載的並不是class文件,而是將class文件優化後的dex文件,上述步驟便是將jar包解壓,將其中的class文件優化成dex文件,然後壓縮為apk文件。

目標類的實現與處理

import android.app.Activity;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
import com.example.interfaces.IDynamic;
import java.util.List;
import dalvik.system.PathClassLoader;

/**
 * Created by Administrator on 2016/7/27.
 */
public class MainActivity extends Activity {
    private IDynamic lib;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main_layout);
        //彈出事件的按鈕
        Button showSomething_btn = (Button) findViewById(R.id.show_something);

        /**使用PathClassLoader方法加載類*/
        //創建一個意圖,用來找到指定的apk:這裡的"com.dynamic.impl是指定apk中在AndroidMainfest.xml文件中定義的
        Intent intent = new Intent("com.example.impl", null);
        //獲得包管理器
        PackageManager pm = getPackageManager();
        List resolveinfoes =  pm.queryIntentActivities(intent, 0);
        //獲得指定的activity的信息
        ActivityInfo actInfo = resolveinfoes.get(0).activityInfo;
        //這個我修改了參考資料中博客的代碼,按照他的代碼,我沒有運行成功
        final String apkPath = "/data/app/com.example.administrator.dynamicloading-1/dynamic1.apk";

        //native代碼的目錄
        String libPath = actInfo.applicationInfo.nativeLibraryDir;
        //創建類加載器,把dex加載到虛擬機中
        //第一個參數:是指定apk安裝的路徑,這個路徑要注意只能是通過actInfo.applicationInfo.sourceDir來獲取
        //第二個參數:是C/C++依賴的本地庫文件目錄,可以為null
        //第三個參數:是上一級的類加載器
        PathClassLoader pcl = new PathClassLoader(apkPath,libPath,this.getClassLoader());
        //加載類
        try {
//注意,加上你的Dynamic所在的包名
            Class libProviderClazz = pcl.loadClass("com.example.impl.Dynamic");
            lib = (IDynamic)libProviderClazz.newInstance();
            if(lib != null){
                lib.init(MainActivity.this);
            }
        } catch (Exception exception) {
            exception.printStackTrace();
        }
        //彈出事件
        showSomething_btn.setOnClickListener(new View.OnClickListener() {
            public void onClick(View view) {
                if(lib != null){
                    lib.showSomething();
                }else{
                    Toast.makeText(getApplicationContext(), "類加載失敗", Toast.LENGTH_SHORT).show();
                }
            }
        });
    }
}
AndroidManifest的處理


    
        
            
                
//注意,和目標類中相對應               
 
                
            
        
    

將dynamic1.apk放入相應目錄

好了,處理的差不多了,該處理我們在待加載類處理那一步中生成的dynamic1.apk了。因為用DexClassLoader按照Android中的動態加載機制中的方法我並沒有成功,因為博主是將jar包放進SD卡中,現在好像並不支持這樣做,所以這次我打算用PathClassLoader,看了上面介紹的小伙伴已經知道,PathClassLoader只能識別dex文件和已經安裝好的apk文件(安裝好的會有相應的緩存dex文件),那麼我們接下來就要想辦法把咱們生成的apk文件放到一個目錄下,放在哪個目錄下呢?

一開始我也不清楚,不過我將下面這個屬性用Toast顯示了一下,便得到了目錄
String apkPath = actInfo.applicationInfo.sourceDir;

要放的目錄是:
/data/app/com.example.administrator.dynamicloadi
ng-1

這裡需要說明一點,如果你用的是真機的話,可能需要root權限才能進去這些目錄,我用的模擬器,不需要權限。

還有一個坑,這個項目需要先運行一下,app目錄下才會生成com.example.administrator.dynamicloading-1這個文件(當然你生成的不一定是這個,取決於的你的包名)

注意:下面的一些命令都有一個前提,需要開啟模擬器。或者連著真機。

項目運行完後,你可以使用以下命令看看有沒有相應包名的文件:
這裡寫圖片描述

adb shell進入模擬器,然後cd /data/app進入app目錄,然後ls命令查看一下該目錄中都有哪些文件。
我要進入的目錄如下(因為上面那個屬性Toast顯示的是它):

這裡寫圖片描述

好了,那麼用命令行將apk文件放入這個目錄,先使用exit命令退出模擬器。

接下來,用命令將dynamic1.apk放入這個目錄
命令如下:
adb push apk所在目錄 要放入的目錄

例如我的apk在剛才的build-tools目錄下,要放進/data/app/com.example.administrator.dynamicloadi
ng-1,所以命令如下圖:

這裡寫圖片描述

如果想查看有沒有復制成功,可以按照上述的方法檢查一下,adb shell ,然後進入這個目錄,ls一下。

好了,運行項目,看看結果:
這裡寫圖片描述

成功加載了。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved