Reduce the volume of the installation package, and deliver modular upgrades through the network to reduce silent network traffic upgrades, upgrade without perception to solve the problem that the number of models in earlier versions exceeds the threshold, leading to uninstallation. Code decoupling statusThe plug-in framework technology in Android has been discussed and implemented many times. Plug-ins are usually packaged in the form of apk or dex.
Dex plug-ins often provide some functional interfaces. This method is similar to the jar form in java, but because the Dalvik VM of Android cannot directly and dynamically load the Byte Code of Java, therefore, we need to provide Dalvik Byte Code, while dex is a jar in the form of Dalvik Byte Code.
Plug-ins in apk form provide more functions than dex form. For example, you can package resources into apk, or implement system components such as Activity or Service in the plug-in.
This article mainly discusses the plug-in framework in the apk form. There are two ways to install and not install the apk form.
The installation of apk is relatively simple. The main principle is to share the plug-in apk with the main program with a UserId.createPackageContext
Construct the context of the plug-in to access the resources in the plug-in apk through context. Many app theme frameworks are implemented by installing the plug-in apk, such as the Go topic. The disadvantage of this method is that you need to install it manually, and the experience is not very good.
The method of not installing apk solves the disadvantages of manual installation, but the implementation is complicated, mainly throughDexClassloader
To ensure the flexibility of the plug-in framework, these system components are not well declared in the main program in advance, the real difficulty in implementing the plug-in framework lies in this.
DexClassloaderHere is a description of the java class loader in the second edition of "deep understanding of java Virtual Machine: JVM advanced features and Best Practices:
The virtual machine design team put the "get and describe the binary byte stream of this class through the full qualified name of a class" action in the class loading stage outside the Java Virtual Machine for implementation, so that the application can decide how to obtain the required classes. The Code module that implements this action is called the class loader ".
The implementation of the Android Virtual Machine refers to the java JVM. Therefore, the class Loader concept is also used for loading classes in Android, but compared with the class file loaded by the JVM loader, the Dalvik Virtual Machine of Android loads the Dex format.PathClassloader
AndDexclassloader
.
PathClassloader
Read by default/data/dalvik-cache
The cached dex file. If an uninstalled apk is usedPathClassloader
In/data/dalvik-cache
The corresponding dex cannot be found in the directory, so it will throwClassNotFoundException
.
DexClassloader
You can load dex and apk files in any path. by specifying the path generated by odex, you can load uninstalled apk files. The following code showsDexClassloader
Usage:
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
Solved the class loading problem. If the plug-in apk only contains some simple API calls, the above Code can meet the requirements, however, the plug-in framework discussed here also needs to address resource access and Android system component calls.
Call of system components in the plug-inAndroid Framework containsActivity
,Service
,Content Provider
AndBroadcastReceiver
The following describes how to start the Activity in the plug-in the main program. The call method for the other three components is similar.
We all know that the Activity needs to be declared in AndroidManifest. xml. When apk is installedPackageManagerService
The AndroidManifest. xml file in the apk will be parsed. At this time, the activities contained in the program will be determined.ActivityNotFound
Exception: I believe most Android Developers have encountered this exception.
Starting the Activity in the plug-in will inevitably face how AndroidManifest in the main program. this Activity is declared in xml. However, to ensure the flexibility of the plug-in framework, we cannot predict which activities are included in the plug-in, so we cannot declare them in advance.
To solve the above problem, we will introduce a Proxy-based solution. The general principle is to declare someProxyActivity
To start the Activity in the plug-in.ProxyActivity
,ProxyActivity
All system callbacks in will call the corresponding implementation in the plug-in Activity. The final effect is that the started Activity is actually a declared Activity in the main program, however, the code is executed in the plug-in Activity. This solves the problem that the plug-in Activity cannot be started without being declared. In the upper layer, the Activity in the plug-in is started. The following describes the entire process.
PluginSDKAll plug-ins and main programs must rely on PluginSDK for development. The activities in all plug-ins inherit fromBasePluginActivity
,BasePluginActivity
Inherited fromActivity
And implementsIPluginActivity
Interface.
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
AndPluginProxyActivity
At the core of the entire plug-in framework, the following code is analyzed:
First, let's take a look.PluginProxyActivity#onResume
:
@Overrideprotected void onResume() { super.onResume(); if(mPluginActivity != null){ mPluginActivity.IOnResume(); }}
VariablemPluginActivity
IsIPluginActivity
Because the plug-in Activity implementsIPluginActivity
Interface, so you can guessmPluginActivity.IOnResume()
The final execution of the plug-in ActivityonResume
In the code, we will confirm this speculation.
BasePluginActivity
ImplementedIPluginActivity
Interfaces, how are these interfaces implemented? Check the Code:
@Overridepublic void IOnCreate(Bundle savedInstanceState) { onCreate(savedInstanceState);}@Overridepublic void IOnResume() { onResume();}@Overridepublic void IOnStart() { onStart();}@Overridepublic void IOnPause() { onPause();}...
The interface implementation is very simple, but the callback function corresponding to the interface is called. Where will the callback function be adjusted? As mentioned above, all plug-in activities will inherit fromBasePluginActivity
That is to say, the callback function will be eventually adjusted to the corresponding callback in the plug-in Activity, suchIOnResume
The execution is in the plug-in Activity.onResume
Code, which also proves the previous speculation.
Some of the above code snippets reveal the core logic of the plug-in framework. Other code is more about implementing this logic service, and the source code of the entire project will be provided later, you can analyze and understand it on your own.
Obtain resources in the plug-inOne way to load Resources in the plug-in apk is to add the path of the plug-in apk to the path of the main program resource search. The following code shows this method:
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;}
To allow the plug-in Activity to use our custom Context when accessing resources, we needBasePluginActivity
In initialization:
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
By reloadinggetAssets
To implement the Context that contains the apk search path of the plug-in:
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;}@Overridepublic Resources getResources() { return mResources;}@Overridepublic AssetManager getAssets() { return mAsset;}...
SummaryThis article describes a Proxy-based plug-in framework. All the code is in Github, and the code only extracts the core part of the entire framework. If it needs to be improved in the production environment, for exampleContent Provider
AndBroadcastReceiver
The Proxy class of the component is not implemented, and the Proxy implementation of the Activity is incomplete, including many callbacks that are not processed. At the same time, I cannot guarantee that this framework does not have any fatal defects. This article aims to summarize and learn. We welcome you to discuss it together.