編輯:關於Android編程
研究插件框架的意義在於以下幾點:
Android中關於插件框架的技術已經有過不少討論和實現,插件通常打包成apk或者dex的形式。
dex形式的插件往往提供了一些功能性的接口,這種方式類似於java中的jar形式,只是由於Android的Dalvik VM無法直接動態加載Java的Byte Code,所以需要我們提供Dalvik Byte Code,而dex就是Dalvik Byte Code形式的jar。
apk形式的插件提供了比dex形式更多的功能,例如可以將資源打包進apk,也可實現插件內的Activity或者Service等系統組件。
本文主要討論apk形式的插件框架,對於apk形式又存在安裝和不安裝兩種方式
安裝apk的方式實現相對簡單,主要原理是通過將插件apk和主程序共享一個UserId,主程序通過createPackageContext
構造插件的context,通過context即可訪問插件apk中的資源,很多app的主題框架就是通過安裝插件apk的形式實現,例如Go主題。這種方式的缺點就是需要用戶手動安裝,體驗並不是很好。
不安裝apk的方式解決了用戶手動安裝的缺點,但實現起來比較復雜,主要通過DexClassloader
的方式實現,同時要解決如何啟動插件中Activity等Android系統組件,為了保證插件框架的靈活性,這些系統組件不太好在主程序中提前聲明,實現插件框架真正的難點在此。
這裡引用《深入理解Java虛擬機:JVM高級特性與最佳實踐》第二版裡對java類加載器的一段描述:
虛擬機設計團隊把類加載階段中的“通過一個類的全限定名來獲取描述此類的二進制字節流”這個動作放到Java虛擬機外部去實現,以便讓應用程序自己決定如何去獲取所需要的類。實現這個動作的代碼模塊稱為“類加載器”。
Android虛擬機的實現參考了java的JVM,因此在Android中加載類也用到了類加載器的概念,只是相對於JVM中加載器加載class文件而言,Android的Dalvik虛擬機加載的是Dex格式,而具體完成Dex加載的主要是PathClassloader
和Dexclassloader
。
PathClassloader
默認會讀取/data/dalvik-cache
中緩存的dex文件,未安裝的apk如果用PathClassloader
來加載,那麼在/data/dalvik-cache
目錄下找不到對應的dex,因此會拋出ClassNotFoundException
。
DexClassloader
可以加載任意路徑下包含dex和apk文件,通過指定odex生成的路徑,可加載未安裝的apk文件。下面一段代碼展示了DexClassloader
的使用方法:
final File optimizedDexOutputPath = context.getDir(odex, Context.MODE_PRIVATE);
try{
DexClassLoader classloader = new DexClassLoader(apkPath,
optimizedDexOutputPath.getAbsolutePath(),
null, context.getClassLoader());
Class clazz = classloader.loadClass(com.plugindemo.test);
Object obj = clazz.newInstance();
Class[] param = new Class[2];
param[0] = Integer.TYPE;
param[1] = Integer.TYPE;
Method method = clazz.getMethod(add, param);
method.invoke(obj, 1, 2);
}catch(InvocationTargetException e){
e.printStackTrace();
}catch(NoSuchMethodException e){
e.printStackTrace();
}catch(IllegalAccessException e){
e.printStackTrace();
}catch(ClassNotFoundException e){
e.printStackTrace();
}catch (InstantiationException e){
e.printStackTrace();
}
DexClassloader
解決了類的加載問題,如果插件apk裡只是一些簡單的API調用,那麼上面的代碼已經能滿足需求,不過這裡討論的插件框架還需要解決資源訪問和Android系統組件的調用。
Android Framework中包含Activity
,Service
,Content Provider
以及BroadcastReceiver
等四大系統組件,這裡主要討論如何在主程序中啟動插件中的Activity,其它3種組件的調用方式類似。
大家都知道Activity需要在AndroidManifest.xml中進行聲明,apk在安裝的時候PackageManagerService
會解析apk中的AndroidManifest.xml文件,這時候就決定了程序包含的哪些Activity,啟動未聲明的Activity會報ActivityNotFound
異常,相信大部分Android開發者曾經都遇到過這個異常。
啟動插件裡的Activity必然會面對如何在主程序中的AndroidManifest.xml中聲明這個Activity,然而為了保證插件框架的靈活性,我們是無法預知插件中有哪些Activity,所以也無法提前聲明。
為了解決上述問題,這裡介紹一種基於Proxy思想的解決方法,大致原理是在主程序的AndroidManifest.xml中聲明一些ProxyActivity
,啟動插件中的Activity會轉為啟動主程序中的一個ProxyActivity
,ProxyActivity
中所有系統回調都會調用插件Activity中對應的實現,最後的效果就是啟動的這個Activity實際上是主程序中已經聲明的一個Activity,但是相關代碼執行的卻是插件Activity中的代碼。這就解決了插件Activity未聲明情況下無法啟動的問題,從上層來看啟動的就是插件中的Activity。下面具體分析整個過程。
所有的插件和主程序需要依賴PluginSDK進行開發,所有插件中的Activity繼承自PluginSDK中的BasePluginActivity
,BasePluginActivity
繼承自Activity
並實現了IPluginActivity
接口。
public interface IPluginActivity {
public void IOnCreate(Bundle savedInstanceState);
public void IOnResume();
public void IOnStart();
public void IOnPause();
public void IOnStop();
public void IOnDestroy();
public void IOnRestart();
public void IInit(String path, Activity context, ClassLoader classLoader, PackageInfo packageInfo);
}
public class BasePluginActivity extends Activity implements IPluginActivity {
...
private ClassLoader mDexClassLoader;
private Activity mActivity;
...
@Override
public void IInit(String path, Activity context, ClassLoader classLoader, PackageInfo packageInfo) {
mIsRunInPlugin = true;
mDexClassLoader = classLoader;
mOutActivity = context;
mApkFilePath = path;
mPackageInfo = packageInfo;
mContext = new PluginContext(context, 0, mApkFilePath, mDexClassLoader);
attachBaseContext(mContext);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
if (mIsRunInPlugin) {
mActivity = mOutActivity;
} else {
super.onCreate(savedInstanceState);
mActivity = this;
}
}
@Override
public void setContentView(int layoutResID) {
if (mIsRunInPlugin) {
mContentView = LayoutInflater.from(mContext).inflate(layoutResID, null);
mActivity.setContentView(mContentView);
} else {
super.setContentView(layoutResID);
}
}
...
@Override
public void IOnCreate(Bundle savedInstanceState) {
onCreate(savedInstanceState);
}
@Override
public void IOnResume() {
onResume();
}
@Override
public void IOnStart() {
onStart();
}
@Override
public void IOnPause() {
onPause();
}
@Override
public void IOnStop() {
onStop();
}
@Override
public void IOnDestroy() {
onDestroy();
}
@Override
public void IOnRestart() {
onRestart();
}
}
public class PluginProxyActivity extends Activity {
IPluginActivity mPluginActivity;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle bundle = getIntent().getExtras();
if(bundle == null){
return;
}
mPluginName = bundle.getString(PluginStatic.PARAM_PLUGIN_NAME);
mLaunchActivity = bundle.getString(PluginStatic.PARAM_LAUNCH_ACTIVITY);
File pluginFile = PluginUtils.getInstallPath(PluginProxyActivity.this, mPluginName);
if(!pluginFile.exists()){
return;
}
mPluginApkFilePath = pluginFile.getAbsolutePath();
try {
initPlugin();
super.onCreate(savedInstanceState);
mPluginActivity.IOnCreate(savedInstanceState);
} catch (Exception e) {
mPluginActivity = null;
e.printStackTrace();
}
}
@Override
protected void onResume() {
super.onResume();
if(mPluginActivity != null){
mPluginActivity.IOnResume();
}
}
@Override
protected void onStart() {
super.onStart();
if(mPluginActivity != null) {
mPluginActivity.IOnStart();
}
}
...
private void initPlugin() throws Exception {
PackageInfo packageInfo;
try {
PackageManager pm = getPackageManager();
packageInfo = pm.getPackageArchiveInfo(mPluginApkFilePath, PackageManager.GET_ACTIVITIES);
} catch (Exception e) {
throw e;
}
if (mLaunchActivity == null || mLaunchActivity.length() == 0) {
mLaunchActivity = packageInfo.activities[0].name;
}
// String optimizedDexOutputPath = getDir(odex, Context.MODE_PRIVATE).getAbsolutePath();
ClassLoader classLoader = PluginStatic.getOrCreateClassLoaderByPath(this, mPluginName, mPluginApkFilePath);
if (mLaunchActivity == null || mLaunchActivity.length() == 0) {
if (packageInfo == null || (packageInfo.activities == null) || (packageInfo.activities.length == 0)) {
throw new ClassNotFoundException(Launch Activity not found);
}
mLaunchActivity = packageInfo.activities[0].name;
}
Class mClassLaunchActivity = (Class) classLoader.loadClass(mLaunchActivity);
getIntent().setExtrasClassLoader(classLoader);
mPluginActivity = (IPluginActivity) mClassLaunchActivity.newInstance();
mPluginActivity.IInit(mPluginApkFilePath, this, classLoader, packageInfo);
}
...
@Override
public void startActivityForResult(Intent intent, int requestCode) {
boolean pluginActivity = intent.getBooleanExtra(PluginStatic.PARAM_IS_IN_PLUGIN, false);
if (pluginActivity) {
String launchActivity = null;
ComponentName componentName = intent.getComponent();
if(null != componentName) {
launchActivity = componentName.getClassName();
}
intent.putExtra(PluginStatic.PARAM_IS_IN_PLUGIN, false);
if (launchActivity != null && launchActivity.length() > 0) {
Intent pluginIntent = new Intent(this, getProxyActivity(launchActivity));
pluginIntent.putExtra(PluginStatic.PARAM_PLUGIN_NAME, mPluginName);
pluginIntent.putExtra(PluginStatic.PARAM_PLUGIN_PATH, mPluginApkFilePath);
pluginIntent.putExtra(PluginStatic.PARAM_LAUNCH_ACTIVITY, launchActivity);
startActivityForResult(pluginIntent, requestCode);
}
} else {
super.startActivityForResult(intent, requestCode);
}
}
}
BasePluginActivity
和PluginProxyActivity
在整個插件框架的核心,下面簡單分析一下代碼:
首先看一下PluginProxyActivity#onResume
:
@Override
protected void onResume() {
super.onResume();
if(mPluginActivity != null){
mPluginActivity.IOnResume();
}
}
變量mPluginActivity
的類型是IPluginActivity
,由於插件Activity實現了IPluginActivity
接口,因此可以猜測mPluginActivity.IOnResume()
最終執行的是插件Activity的onResume
中的代碼,下面我們來證實這種猜測。
BasePluginActivity
實現了IPluginActivity
接口,那麼這些接口具體是怎麼實現的呢?看代碼:
@Override
public void IOnCreate(Bundle savedInstanceState) {
onCreate(savedInstanceState);
}
@Override
public void IOnResume() {
onResume();
}
@Override
public void IOnStart() {
onStart();
}
@Override
public void IOnPause() {
onPause();
}
...
接口實現非常簡單,只是調用了和接口對應的回調函數,那這裡的回調函數最終會調到哪裡呢?前面提到過所有插件Activity都會繼承自BasePluginActivity
,也就是說這裡的回調函數最終會調到插件Activity中對應的回調,比如IOnResume
執行的是插件Activity中的onResume
中的代碼,這也證實了之前的猜測。
上面的一些代碼片段揭示了插件框架的核心邏輯,其它的代碼更多的是為實現這種邏輯服務的,後面會提供整個工程的源碼,大家可自行分析理解。
實現加載插件apk中的資源的一種思路是將插件apk的路徑加入主程序資源查找的路徑中,下面的代碼展示了這種方法:
private AssetManager getSelfAssets(String apkPath) {
AssetManager instance = null;
try {
instance = AssetManager.class.newInstance();
Method addAssetPathMethod = AssetManager.class.getDeclaredMethod(addAssetPath, String.class);
addAssetPathMethod.invoke(instance, apkPath);
} catch (Throwable e) {
e.printStackTrace();
}
return instance;
}
為了讓插件Activity訪問資源時使用我們自定義的Context,我們需要在BasePluginActivity
的初始化中做一些處理:
public void IInit(String path, Activity context, ClassLoader classLoader, PackageInfo packageInfo) {
mIsRunInPlugin = true;
mDexClassLoader = classLoader;
mOutActivity = context;
mApkFilePath = path;
mPackageInfo = packageInfo;
mContext = new PluginContext(context, 0, mApkFilePath, mDexClassLoader);
attachBaseContext(mContext);
}
PluginContext
中通過重載getAssets
來實現包含插件apk查找路徑的Context:
public PluginContext(Context base, int themeres, String apkPath, ClassLoader classLoader) {
super(base, themeres);
mClassLoader = classLoader;
mAsset = getSelfAssets(apkPath);
mResources = getSelfRes(base, mAsset);
mTheme = getSelfTheme(mResources);
mOutContext = base;
}
private AssetManager getSelfAssets(String apkPath) {
AssetManager instance = null;
try {
instance = AssetManager.class.newInstance();
Method addAssetPathMethod = AssetManager.class.getDeclaredMethod(addAssetPath, String.class);
addAssetPathMethod.invoke(instance, apkPath);
} catch (Throwable e) {
e.printStackTrace();
}
return instance;
}
private Resources getSelfRes(Context ctx, AssetManager selfAsset) {
DisplayMetrics metrics = ctx.getResources().getDisplayMetrics();
Configuration con = ctx.getResources().getConfiguration();
return new Resources(selfAsset, metrics, con);
}
private Theme getSelfTheme(Resources selfResources) {
Theme theme = selfResources.newTheme();
mThemeResId = getInnerRIdValue(com.android.internal.R.style.Theme);
theme.applyStyle(mThemeResId, true);
return theme;
}
@Override
public Resources getResources() {
return mResources;
}
@Override
public AssetManager getAssets() {
return mAsset;
}
...
本文描述了一種基於Proxy思想的插件框架,所有的代碼都在Github中,代碼只是抽取了整個框架的核心部分,如果要用在生產環境中還需要完善,比如Content Provider
和BroadcastReceiver
組件的Proxy類未實現,Activity的Proxy實現也是不完整的,包括不少回調都沒有處理。同時我也無法保證這套框架沒有致命缺陷,本文主要是以總結、學習為目的,歡迎大家一起交流。
1.我們之前已經用scrollView實現了輪播圖效果因為需要定時器,所以我們要cd到當前項目根目錄下安裝這個類庫:npm i react-timer-mixin --s
1、直接使用getWindow().getDecorView().getRootView()直接使用getWindow().getDecorView().getRootV
我們都知道取消標題欄有兩種方式,一種是在Java代碼中取消,另一種通過設置styles.xml文件中的Theme即可;如下圖:第一種:第二種:但是運行在Android 5
前段時間,有一位網友發私信給我(@伍歌),問我做過磁場傳感器可以做過指南針嗎?其實我第一節裡面已經說過了,磁場傳感器可以做,只是算法比較麻煩,最簡單的指南針使用方向傳感器
本文介紹本文是翻譯自Google 官方課程 Building Apps