1. 程式人生 > >Android動態載入jar、apk的實現

Android動態載入jar、apk的實現

        前段時間到阿里巴巴參加支付寶技術分享沙龍,看到支付寶在Android使用外掛化的技術,挺好奇的。正好這幾天看到了農民伯伯的相關文章,因此簡單整理了下,有什麼錯誤希望大神指正。

       核心類

      1.1      DexClassLoader類
   可以載入jar/apk/dex,可以從SD卡中載入未安裝的apk。
   1.2      PathClassLoader類  
   只能載入已經安裝到Android系統中的apk檔案。

    一、正文

       1.1 動態載入jar

   類似於eclipse的外掛化實現, 首先定義好介面, 使用者實現介面功能後即可通過動態載入的方式載入jar檔案, 以實現具體功能。注意

, 這裡的jar包需要經過android dx工具的處理 , 否則不能使用。

首先我們定義如下介面 :

package com.example.interf;  
/** 
 * @Title: ILoader.java 
 * @Package com.example.loadjardemo 
 * @Description:  通用介面, 需要使用者實現
 * @version V1.0 
 */
public interface ILoader {
	 public String sayHi();
}
  
使用者需實現,該介面, 並且將工程匯出為jar包的形式。

示例如下 :

public class JarLoader implements ILoader {

	public JarLoader() {
	}

	@Override
	public String sayHi() {
		return "I am jar loader.";
	}

}

最後, 實現功能的程式碼打包成jar包 :

首先選中工程, 右鍵後選擇“匯出”, 然後選擇“java”-----“jar檔案”, 然後將你的具體功能實現類匯出為jar,檔名為loader.jar,如下圖所示 : 


將打包好的jar拷貝到SDK安裝目錄android-sdk-windows\platform-tools下,DOS進入這個目錄,執行如下命令:

dx --dex --output=loader_dex.jar loader.jar

然後將loader_dex.jar放到android手機中, 這裡我們放到SD卡根目錄下。

動態載入程式碼 : 
	/**
	 * 
	 * @Title: loadJar 
	 * @Description: 專案工程中必須定義介面, 而被引入的第三方jar包實現這些介面,然後進行動態載入 。
	 *          相當於第三方按照介面協議來開發, 使得第三方應用可以以外掛的形式動態載入到應用平臺中。
	 * @return void    
	 * @throws
	 */
	private void loadJar(){
        final File optimizedDexOutputPath = new File(Environment.getExternalStorageDirectory().toString()
                + File.separator + "loader_dex.jar");
            
            BaseDexClassLoader cl = new BaseDexClassLoader(Environment.getExternalStorageDirectory().toString(),
            		optimizedDexOutputPath, optimizedDexOutputPath.getAbsolutePath(), getClassLoader());
            Class libProviderClazz = null;
            
            try {
                // 載入JarLoader類, 並且通過反射構建JarLoader物件, 然後呼叫sayHi方法
                libProviderClazz = cl.loadClass("com.example.interf.JarLoader");
                ILoader loader = (ILoader)libProviderClazz.newInstance();
                Toast.makeText(MainActivity.this, loader.sayHi() , Toast.LENGTH_SHORT).show();
            } catch (Exception exception) {
                // Handle exception gracefully here.
                exception.printStackTrace();
            }
	}

效果如下圖所示 : 
請等待...

1.2 載入未安裝的apk

     首先新建一個Android專案, 定義如下介面 : 

public interface ISayHello {
	public String sayHello()  ;
}
    

定義一個Activity實現該介面, 如下:

package com.example.loaduninstallapkdemo;

import android.app.Activity;
import android.os.Bundle;
import android.view.Menu;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Toast;

/**
 * 
 * @ClassName: UninstallApkActivity 
 * @Description: 這是被動態載入的Activity類
 *
 */
public class UninstallApkActivity extends Activity implements ISayHello{

	private View mShowView = null ;
	
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		
		mShowView = findViewById(R.id.show) ;
		mShowView.setOnClickListener(new OnClickListener() {
			
			@Override
			public void onClick(View v) {
				Toast.makeText(UninstallApkActivity.this, "這是已安裝的apk被動態載入了", Toast.LENGTH_SHORT).show();
			}
		}) ;
	}

	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		// Inflate the menu; this adds items to the action bar if it is present.
		getMenuInflater().inflate(R.menu.activity_main, menu);
		return true;
	}

	@Override
	public String sayHello(){
		return "Hello, this apk is not installed";
	}
}
然後將該編譯生apk, 並且將該apk拷貝到SD卡根目錄下。

動態載入未安裝的apk

	/**
	 * 
	 * @Title: loadUninstallApk 
	 * @Description: 動態載入未安裝的apk
	 * @return void    
	 * @throws
	 */
	private void loadUninstallApk(){
        String path = Environment.getExternalStorageDirectory() + File.separator;
        String filename = "LoadUninstallApkDemo.apk";

        // 4.1以後不能夠將optimizedDirectory設定到sd卡目錄, 否則丟擲異常.
        File optimizedDirectoryFile = getDir("dex", 0) ;
        DexClassLoader classLoader = new DexClassLoader(path + filename, optimizedDirectoryFile.getAbsolutePath(),
                											null, getClassLoader());

        try {
        	// 通過反射機制呼叫, 包名為com.example.loaduninstallapkdemo, 類名為UninstallApkActivity
            Class mLoadClass = classLoader.loadClass("com.example.loadunstallapkdemo.UninstallApkActivity");
            Constructor constructor = mLoadClass.getConstructor(new Class[] {});
            Object testActivity = constructor.newInstance(new Object[] {});
            
            // 獲取sayHello方法
            Method helloMethod = mLoadClass.getMethod("sayHello", null);
            helloMethod.setAccessible(true);
            Object content = helloMethod.invoke(testActivity, null);
            Toast.makeText(MainActivity.this, content.toString(), Toast.LENGTH_LONG).show();
            
        } catch (Exception e) {
            e.printStackTrace();
        }
	}
DexClassLoader 注意點 :

A class loader that loads classes from .jar and .apk files containing a classes.dex entry. This can be used to execute code not installed as part of an application.

This class loader requires an application-private, writable directory to cache optimized classes. Use Context.getDir(String, int) to create such a directory:

File dexOutputDir = context.getDir("dex",0);

Do not cache optimized classes on external storage. External storage does not provide access controls necessary to protect your application from code injection attacks.


效果如圖 : 


1.3 載入已安裝的apk

將1.2中的apk安裝到手機中,我的例子中,該apk的包名為“com.example.loaduninstallapkdemo”,Activity名為"UninstallApkActivity". 載入程式碼如下 :

        /**
	 * 
	 * @Title: loadInstalledApk 
	 * @Description: 動態載入已安裝的apk    
	 * @return void    
	 * @throws
	 */
	private void loadInstalledApk() {
		try {
			String pkgName = "com.example.loaduninstallapkdemo";
			Context context = createPackageContext(pkgName,
			        Context.CONTEXT_IGNORE_SECURITY | Context.CONTEXT_INCLUDE_CODE) ;
			
			// 獲取動態載入得到的資源
			Resources resources = context.getResources() ;
			// 過去該apk中的字串資源"tips", 並且toast出來,apk換膚的實現就是這種原理
			String toast = resources.getString(resources.getIdentifier("tips", "string", pkgName) ) ;
			Toast.makeText(MainActivity.this, toast, Toast.LENGTH_SHORT).show() ;
			
			Class cls = context.getClassLoader().loadClass(pkgName + ".UninstallApkActivity") ;
			// 跳轉到該Activity
			startActivity(new Intent(context, cls)) ;
		} catch (NameNotFoundException e) {
			e.printStackTrace();
		}catch (ClassNotFoundException e) {
			Log.d("", e.toString()) ;
		}
	}</span>


效果如圖:

訊息被Toast出來, 並且跳轉到了目標Activity.