1. 程式人生 > >Android外掛化原理和實踐 (二) 之 載入外掛中的類程式碼

Android外掛化原理和實踐 (二) 之 載入外掛中的類程式碼

我們在上一篇文章《Android外掛化原理和實踐 (一)之 外掛化簡介和基本原理簡述》中介紹了外掛化一些基本知識和歷史,最後還列出了三個根本問題。接下來我們打算圍繞著這三個根本問題展開對外掛化的學習。首先本章將介紹第一個根本問題:宿主和外掛中如何相互呼叫程式碼。要實現它們相互呼叫,就得要宿主先將外掛載入起來。Android中要想從載入外部外掛就在於ClassLoader。

1 初識PathClassLoader和DexClassLoader

ClassLoader下有子類BaseDexClassLoader,BaseDexClassLoader又有兩個重要的子類:PathClassLoader和DexClassLoader。先來看看PathClassLoader和DexClassLoader的原始碼:

PathClassLoader.java

    /**
     * Provides a simple {@link ClassLoader} implementation that operates on a list
     * of files and directories in the local file system, but does not attempt to
     * load classes from the network. Android uses this class for its system class
     * loader and for its application class loader(s).
     */
    public class PathClassLoader extends BaseDexClassLoader {
        /**
         * Creates a {@code PathClassLoader} that operates on a given list of files
         * and directories. This method is equivalent to calling
         * {@link #PathClassLoader(String, String, ClassLoader)} with a
         * {@code null} value for the second argument (see description there).
         *
         * @param dexPath the list of jar/apk files containing classes and
         * resources, delimited by {@code File.pathSeparator}, which
         * defaults to {@code ":"} on Android
         * @param parent the parent class loader
         */
        public PathClassLoader(String dexPath, ClassLoader parent) {
            super(dexPath, null, null, parent);
        }

        /**
         * Creates a {@code PathClassLoader} that operates on two given
         * lists of files and directories. The entries of the first list
         * should be one of the following:
         *
         * <ul>
         * <li>JAR/ZIP/APK files, possibly containing a "classes.dex" file as
         * well as arbitrary resources.
         * <li>Raw ".dex" files (not inside a zip file).
         * </ul>
         *
         * The entries of the second list should be directories containing
         * native library files.
         *
         * @param dexPath the list of jar/apk files containing classes and
         * resources, delimited by {@code File.pathSeparator}, which
         * defaults to {@code ":"} on Android
         * @param libraryPath the list of directories containing native
         * libraries, delimited by {@code File.pathSeparator}; may be
         * {@code null}
         * @param parent the parent class loader
         */
        public PathClassLoader(String dexPath, String libraryPath,
                ClassLoader parent) {
            super(dexPath, null, libraryPath, parent);
        }
    }

DexClassLoader.java

    /**
     * A class loader that loads classes from {@code .jar} and {@code .apk} files
     * containing a {@code classes.dex} entry. This can be used to execute code not
     * installed as part of an application.
     *
     * <p>This class loader requires an application-private, writable directory to
     * cache optimized classes. Use {@code Context.getDir(String, int)} to create
     * such a directory: <pre>   {@code
     *   File dexOutputDir = context.getDir("dex", 0);
     * }</pre>
     *
     * <p><strong>Do not cache optimized classes on external storage.</strong>
     * External storage does not provide access controls necessary to protect your
     * application from code injection attacks.
     */
    public class DexClassLoader extends BaseDexClassLoader {
        /**
         * Creates a {@code DexClassLoader} that finds interpreted and native
         * code.  Interpreted classes are found in a set of DEX files contained
         * in Jar or APK files.
         *
         * <p>The path lists are separated using the character specified by the
         * {@code path.separator} system property, which defaults to {@code :}.
         *
         * @param dexPath the list of jar/apk files containing classes and
         *     resources, delimited by {@code File.pathSeparator}, which
         *     defaults to {@code ":"} on Android
         * @param optimizedDirectory directory where optimized dex files
         *     should be written; must not be {@code null}
         * @param libraryPath the list of directories containing native
         *     libraries, delimited by {@code File.pathSeparator}; may be
         *     {@code null}
         * @param parent the parent class loader
         */
        public DexClassLoader(String dexPath, String optimizedDirectory,
                String libraryPath, ClassLoader parent) {
            super(dexPath, new File(optimizedDirectory), libraryPath, parent);
        }
    }

PathClassLoader和DexClassLoader這兩個類很簡單,區別僅在於建構函式的第2個引數optimizedDirectory,其中PathClassLoader把這個引數設定為null。我們來翻譯一下PathClassLoader的頭部註釋,大概意思是:PathClassLoader用於載入本地檔案系統上的檔案和目錄,它不能從網路上載入,Android使用這個類來載入系統類及應用程式。再來翻譯一下DexClassLoader的頭部註釋,大概意思是:DexClassLoader用於載入來自外部jar、apk包含的dex目錄。

說到載入dex,是不是想起了為了解決65535問題,而使用了multidex來將一個apk檔案內的dex進行拆分。沒錯,其實在使用這個過程中,主的classes.dex是由App使用PathClassLoader進行載入的,而其餘的classes2.dex這些子dex都會以資源的形式在App啟動後使用DexClassLoader進行載入到ClassLoader中的。

接著再來翻譯一下DexClassLoader的建構函式中引數的意思:

dexPath

接收一個jar或apk檔案列表,多個的話,由File.pathSeparator分隔,在Android中預設使用”:”分隔

optimizedDirectory

dex檔案被載入後會被編譯器優化,優化之後的dex存放路徑,不能為空。要注意的是,它需要一個應用私有的可寫的一個路徑,以防止應用被注入攻擊,例子如: File dexOutputDir = context.getDir(“dex”, 0);

libraryPath

包含libraries的目錄列表,同樣用File.pathSeparator分割,可為空,傳入null

parent

父類的class loader

2 載入外掛中的類

好了,我們已經清楚了DexClassLoader是用來載入外部apk的dex,而且也瞭解了它的建構函式的呼叫了,那麼現在就來通過一個Demo來看看它是否真的可以載入外部apk的dex吧。Demo工程目錄如下所示,存在兩個applicaton模組,一個是宿主Host,一個是外掛Plugin。

Plugin外掛中僅有一個TestBean.java類:

package com.zyx.plugin;

public class TestBean {
    private String mName = "子云心";

    public void setName(String name) {
        mName = name;
    }

    public String getName() {
        return mName;
    }
}

Host的MainActivity中編寫所有的邏輯程式碼:

package com.zyx.plugindemo;

import android.app.Activity;
import android.content.Context;
import android.content.res.AssetManager;
import android.os.Bundle;
import android.widget.Toast;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;

import dalvik.system.DexClassLoader;

public class MainActivity extends Activity {
    private final static String sApkName = "Plugin-debug.apk";
    private DexClassLoader mClassLoader;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        simulationDownload(this, sApkName);
        mClassLoader = loadPlugin(this, sApkName);
        onDo();
    }

    /**
     * 載入外掛
     * @param context
     * @param apkName
     * @return
     */
    private DexClassLoader loadPlugin(Context context, String apkName) {
        File extractFile = context.getFileStreamPath(apkName);
        String dexpath = extractFile.getPath();

        File fileRelease = context.getDir("dex", 0);
        String absolutePath = fileRelease.getAbsolutePath();

        DexClassLoader classLoader = new DexClassLoader(dexpath, absolutePath, null, context.getClassLoader());
        return classLoader;
    }

    /**
     * 執行外掛程式碼
     */
    private void onDo() {
        try {
            Class mLoadClassBean = mClassLoader.loadClass("com.zyx.plugin.TestBean");
            Object testBeanObject = mLoadClassBean.newInstance();
            Method getNameMethod = mLoadClassBean.getMethod("getName");
            getNameMethod.setAccessible(true);
            String name = (String) getNameMethod.invoke(testBeanObject);

            Toast.makeText(getApplicationContext(), name, Toast.LENGTH_LONG).show();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 模擬下載,實際上是將Assets中的外掛apk檔案複製到/data/data/files 目錄下
     * @param context
     * @param sourceName
     */
    private void simulationDownload(Context context, String sourceName) {
        AssetManager am = context.getAssets();
        InputStream is = null;
        FileOutputStream fos = null;
        try {
            is = am.open(sourceName);
            File extractFile = context.getFileStreamPath(sourceName);
            fos = new FileOutputStream(extractFile);
            byte[] buffer = new byte[1024];
            int count = 0;
            while ((count = is.read(buffer)) > 0) {
                fos.write(buffer, 0, count);
            }
            fos.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (is != null) {
                    is.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                if (fos != null) {
                    fos.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

上述程式碼中,simulationDownload是一個測試方法程式碼,為了方便開發驗證,我們將Plugin通過編譯後生成的Plugin-debug.apk檔案放在了Host的assets目錄下。然後再通過simulationDownload方法將Plugin-debug.apk檔案轉存到/data/data/files 目錄下來模擬真實開發時的下載邏輯。

我們將重點放回在loadPlugin和onDo兩個方法上,loadPlugin方法中通過獲取Plugin-debug.apk檔案的目錄資訊來構造了一個DexClassLoader物件,然後在onDo方法中,通過這個物件使用了反射來讀到Plugin外掛中的TestBean類。App執行後,會彈出一個Toast,內容就是TestBean中的mName值。

你以為外掛化中載入外掛程式碼就是這麼簡單嗎?你錯了,上面的Demo只能使宿主呼叫外掛中的普通類,而實際上並不能使宿主調起外掛中的四大元件。即使你在宿主中的AndroidMainifest.xml中聲明瞭元件資訊也是不行。可以嘗試在外掛中建立一個TestService服務,並在宿主中的AndroidMainifest.xml中宣告它,然後在宿主中通過startService來啟動服務,最後再次執行App後就會報出以下的異常:

至於為什麼?和解決方法是怎樣?我們留在下一篇文章來介紹。