Android動態載入jar、apk的實現
前段時間到阿里巴巴參加支付寶技術分享沙龍,看到支付寶在Android使用外掛化的技術,挺好奇的。正好這幾天看到了農民伯伯的相關文章,因此簡單整理了下,有什麼錯誤希望大神指正。
核心類
1.1 DexClassLoader類
可以載入jar/apk/dex,可以從SD卡中載入未安裝的apk。
1.2 PathClassLoader類
只能載入已經安裝到Android系統中的apk檔案。
一、正文
1.1 動態載入jar
類似於eclipse的外掛化實現, 首先定義好介面, 使用者實現介面功能後即可通過動態載入的方式載入jar檔案, 以實現具體功能。注意
首先我們定義如下介面 :
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.