1. 程式人生 > >【Android高階】DexClassloader和PathClassloader動態載入外掛的實現

【Android高階】DexClassloader和PathClassloader動態載入外掛的實現

(一)DexClassloader

一、基本概念:


         在Android中可以跟java一樣實現動態載入jar,但是Android使用德海Dalvik VM,不能直接載入java打包jar的byte code,需要通過dx工具來優化Dalvik byte code。
         Android在API中給出可動態載入的有:DexClassLoader 和 PathClassLoader。
         DexClassLoader:可載入jar、apk和dex,可以從SD卡中載入(本文使用這種方式)
         PathClassLoader:只能載入已經安裝搭配Android系統中的apk檔案

二、實施
        編寫介面:Dynamic

package com.smilegames.dynamic.interfaces;

public interface Dynamic {
	public String helloWorld();
	
	public String smileGames();
	
	public String fyt();
}
        編寫實現類:DynamicTest
package com.smilegames.dynamic.impl;

import com.smilegames.dynamic.interfaces.Dynamic;

public class DynamicTest implements Dynamic {

	@Override
	public String helloWorld() {
		return "Hello Word!";
	}

	@Override
	public String smileGames() {
		return "Smile Games";
	}

	@Override
	public String fyt() {
		return "fengyoutian";
	}

}
三、打包並編譯成dex
       將介面打包成jar:dynamic.jar(只打包這Dynamic.java這一個介面
       將實現類打包成jar:dynamic_test.jar(只打包DynamicTest.java這一個實現類
       將打包後的實現類(dynamic_test.jar)編譯成dex:dynamic_impl.jar
              1、將dynamic_test.jar拷貝到SDK安裝目錄android-sdk-windows\platform-tools下(ps:如果platform-tools沒有dx.bat,可拷貝到build-tools目錄下有dx.bat的子目錄

              2、執行以下命令:
dx --dex --output=dynamic_impl.jar dynamic_test.jar
             3、將dynamic.jar引入測試例項
             4、將dynamic_impl.jar放到模擬器或真機的sdcard

四、修改onCreate例子
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

//        或許activity按鈕
        helloWorld = (Button) findViewById(R.id.helloWorld);
        smileGames = (Button) findViewById(R.id.smileGames);
        fyt = (Button) findViewById(R.id.fyt);

        /*使用DexCkassLoader方式載入類*/
        // dex壓縮檔案的路徑(可以是apk,jar,zip格式)
        String dexPath = Environment.getExternalStorageDirectory().toString() + File.separator + "dynamic_impl.jar";

        // dex解壓釋放後的目錄
        String dexOutputDirs = Environment.getExternalStorageDirectory().toString();

        // 定義DexClassLoader
        // 第一個引數:是dex壓縮檔案的路徑
        // 第二個引數:是dex解壓縮後存放的目錄
        // 第三個引數:是C/C++依賴的本地庫檔案目錄,可以為null
        // 第四個引數:是上一級的類載入器
        DexClassLoader dexClassLoader = new DexClassLoader(dexPath,dexOutputDirs,null,getClassLoader());

        Class libProvierClazz = null;
        // 使用DexClassLoader載入類
        try {
            libProvierClazz = dexClassLoader.loadClass("com.smilegames.dynamic.impl.DynamicTest");
            // 建立dynamic例項
            dynamic = (Dynamic) libProvierClazz.newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }

        helloWorld.setOnClickListener(new HelloWorldOnClickListener());
        smileGames.setOnClickListener(new SmileGamesOnClickListener());
        fyt.setOnClickListener(new FytOnClickListener());
    }

   private final class HelloWorldOnClickListener implements View.OnClickListener {
        @Override
        public void onClick(View v) {
            if (null != dynamic) {
                Toast.makeText(getApplicationContext(), dynamic.helloWorld(), 1500).show();
            } else {
                Toast.makeText(getApplicationContext(), "類載入失敗", 1500).show();
            }
        }
    }

    private final class SmileGamesOnClickListener implements View.OnClickListener {
        @Override
        public void onClick(View v) {
            if (null != dynamic) {
                Toast.makeText(getApplicationContext(), dynamic.smileGames(), 1500).show();
            } else {
                Toast.makeText(getApplicationContext(), "類載入失敗", 1500).show();
            }
        }
    }
       1、執行這段程式碼時4.0.1以上版本會報:java.lang.IllegalArgumentException: optimizedDirectory not readable/writable: /storage/sdcard0
      可以通過這個授權解決:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> 
      2、授權之後又會報:java.lang.IllegalArgumentException: Optimized data directory /storage/sdcard0 is not owned by the current user. Shared storage cannot protect your application from code injection attacks.
      這個問題的原因是:在4.1系統由於This class loader requires an application-private, writable directory to cache optimized classes為了防止一下問題:
External storage does not provide access controls necessary to protect your application from code injection attacks.
所以加了一個判斷Libcore.os.getuid() != Libcore.os.stat(parent).st_uid判斷兩個程式是不是同一個uid
 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
 }
 }
 
mCookie = openDexFile(sourceName, outputName, flags);
 mFileName = sourceName;
 guard.open("close");
 //System.out.println("DEX FILE cookie is " + mCookie);

 }
        解決方法是:指定dexoutputpath為APP自己的快取目錄
File dexOutputDir = context.getDir("dex", 0);
DexClassLoader dexClassLoader = new DexClassLoader(dexPath,dexOutputDir.getAbsolutePath(),null,getClassLoader());
ps:第四步的問題最終是2導致的,所以只需用2的解決方案即可,不需要1的授權。

 執行結果自行嘗試。。。

(二)PathClassloader


	public void dexLoad() {
		/** 使用DexClassLoader方式載入類 */
		// dex壓縮檔案的路徑(可以是apk,jar,zip格式)
		String dexPath = Environment.getExternalStorageDirectory().toString()
				+ File.separator + "dynamic_temp.jar";
		// dex解壓釋放後的目錄
		// String dexOutputDir = getApplicationInfo().dataDir;
		// String dexOutputDirs = Environment.getExternalStorageDirectory()
		// .toString();
		// 定義DexClassLoader
		// 第一個引數:是dex壓縮檔案的路徑
		// 第二個引數:是dex解壓縮後存放的目錄
		// 第三個引數:是C/C++依賴的本地庫檔案目錄,可以為null
		// 第四個引數:是上一級的類載入器
		File dexOutputDir = getApplicationContext().getDir("dex", 0);
		DexClassLoader cl = new DexClassLoader(dexPath,
				dexOutputDir.getAbsolutePath(), null, getClassLoader());

		Log.i("tag", dexOutputDir.getAbsolutePath());

		Class libProviderClazz;
		try {
			libProviderClazz = cl.loadClass("com.dynamic.impl.Dynamic");
			// dynamic = (IDynamic) libProviderClazz.newInstance();
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

	}

	// MyInterface dynamic;
	Class dynamic;

	public void pathLoad() {
		/** 使用PathClassLoader方法載入類 */
		// 建立一個意圖,用來找到指定的apk:這裡的"com.dynamic.impl是指定apk中在AndroidMainfest.xml檔案中定義的<action name="com.dynamic.impl"/>
		Intent intent = new Intent(
				"com.example.androidendyeartest.MainActivity", null);
		// 獲得包管理器
		PackageManager pm = getPackageManager();
		List<ResolveInfo> resolveinfoes = pm.queryIntentActivities(intent, 0);
		// 獲得指定的activity的資訊
		ActivityInfo actInfo = resolveinfoes.get(0).activityInfo;
		// 獲得apk的目錄或者jar的目錄
		String apkPath = actInfo.applicationInfo.sourceDir;
		// native程式碼的目錄
		String libPath = actInfo.applicationInfo.nativeLibraryDir;
		// 建立類載入器,把dex載入到虛擬機器中
		// 第一個引數:是指定apk安裝的路徑,這個路徑要注意只能是通過actInfo.applicationInfo.sourceDir來獲取
		// 第二個引數:是C/C++依賴的本地庫檔案目錄,可以為null
		// 第三個引數:是上一級的類載入器
		PathClassLoader pcl = new PathClassLoader(apkPath, libPath,
				this.getClassLoader());
		// 載入類
		try {
			dynamic = pcl.loadClass("com.test.bean.MyBean");
	

		} catch (Exception exception) {
			exception.printStackTrace();
		}
	}
應用:
	@OnClick(R.id.b1)
	public void t1(View v) {
		if (dynamic != null) {
			// dynamic.show1(getApplicationContext());

			Method method;
			try {

				method = dynamic.getMethod("show1",Context.class);
				//Context context=getApplicationContext();
				method.invoke(dynamic.newInstance(), getApplicationContext());
			} catch (Exception e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}

		} else {
			Toast.makeText(getApplicationContext(), "類載入失敗", 1500).show();
		}
	}