android 通过代理activity的方式实现插件化
来源:互联网 发布:怎样退货给淘宝卖家 编辑:程序博客网 时间:2024/06/06 13:58
前言:
一直以来就对插件化这技术推崇已久,在去年也写过两篇关于插件化基础的文章:
Java中的ClassLoader 动态加载机制
Android中的动态加载
都是关于classLoader如何加载外部apk中的代码,在"android中的动态加载"这篇博客末尾,提了下如何打开插件的activity,所以这篇文章就是谈如何通过代理Activity实现打开插件中的activity的。由于插件化的技术现在相对而言已经很成熟了,很多公司都已经运用在项目中,网上也有许多关于插件化的介绍,只是看到网上很多资料都写的不是很完整(可能我技术层面还没到达),只说了一些片面的代码或者是一些基础原理,并没有真正意义上的demo实现。原理是这么回事,但是真正到自己实现起来就发现知道原理并不能立马做好项目。网上也有许多demo是错误的,对初学者学习插件化造成了很大的困扰,在参考了许多资料之后,才决定写一篇关于如何实现插件化的文章。(这里对于宿主如何访问插件的资源、dexclassloader等就不再叙述了,这些基础知识网上有很多资料都有介绍)
参考资料:https://github.com/singwhatiwanna/dynamic-load-apk
实现代理activity的两种方式:(如何管理插件activity的生命周期)
1.通过反射方式实现代理activity。
2.通过接口方式实现代理activity
接下来就会分别使用以上两种方式实现插件化,先来看下运行结果。
了解过apk的安装都知道或者说了解PackageManagerService的都应该清楚,(如果不知道的可以看下我这篇博客:android apk安装过程源码解析)在安装过程中apk会被解析,然后apk中的信息比如四大组件都会被封装到一个package对象中,这样我们启动一个activity就只要从这个package对象中获取这个activity的信息就可以启动了。但是要启动一个插件中的activity(这个apk在外部,并没有被注册到我们的package中),这样的方式就行不通了。所以就需要在宿主activity中创建一个代理activity来管理插件activity的生命周期,而我们插件中的activity就相当于一个普通的类了。
先看下插件项目的目录结构:
可以看到我们的插件项目仅仅2个Activity以及一个接口,(这个接口是用来)其实从运行结果也能看到,插件就一个需要显示的activity,并且内容也仅仅是一个textview。接下来就来看下我们插件中的3个类的内容。
PluginInterface:
声明activity的生命周期方法,通过接口的方式来管理activity的生命周期,避免反射减少性能消耗。
package com.example.lujianxin.proxy;import android.app.Activity;import android.os.Bundle;/** * Created by lujianxin on 2017/7/14. */public interface PluginInterface { public void onStart(); public void onResume(); public void onPause(); public void onStop(); public void onDestroy(); public void onCreate(Bundle savedInstanceState); public void setProxy(Activity proxyActivity);}
这里仅仅只为demo服务,所以没有写另外的方法了,如果有需要可以根据自己的项目来添加.
BaseActivity:
package com.example.lujianxin.proxy;import android.app.Activity;import android.os.Bundle;import android.support.annotation.LayoutRes;import android.view.View;import android.widget.TextView;import android.widget.Toast;/** * Created by lujianxin on 2017/7/13. */public class BaseActivity extends Activity implements PluginInterface { public Activity mProxyActivity; @Override public void onCreate(Bundle savedInstanceState) { } @Override public void setContentView(@LayoutRes int layoutResID) { if(mProxyActivity != null && mProxyActivity instanceof Activity){ mProxyActivity.setContentView(layoutResID); TextView tv = (TextView) mProxyActivity.findViewById(R.id.proxy_tv); tv.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Toast.makeText(mProxyActivity, "this is proxy activity!", Toast.LENGTH_LONG).show(); } }); } } @Override public void onStart() { } @Override public void onResume() { } @Override public void onPause() { } @Override public void onStop() { } @Override public void onDestroy() { } @Override public void setProxy(Activity proxyActivity) { mProxyActivity = proxyActivity; }}
这个类也简单,接受一个activity代理对象,通过这个代理对象来管理插件中的activity生命周期。然后重写setcontentview()方法,让这个代理对象来执行这个setcontentview()方法。并且实现接口为"接口方式管理activity生命周期"提供服务的。
MainActivity:
package com.example.lujianxin.proxy;import android.os.Bundle;import android.util.Log;public class MainActivity extends BaseActivity implements PluginInterface { private static final String TAG = "MainActivity"; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } @Override public void onStart() { Log.d(TAG, "plugin-onStart"); } @Override public void onResume() { Log.d(TAG, "plugin-onResume"); } @Override public void onPause() { Log.d(TAG, "plugin-onPause"); } @Override public void onStop() { Log.d(TAG, "plugin-onStop"); } @Override public void onDestroy() { Log.d(TAG, "plugin-onDestroy"); }}
MainActivity仅仅只是在调用oncreate的时候执行父类的setcontentview()方法。本来在这之前我是没准备写BaseActivity的,所以这里MainActivity也实现了接口,这里可以去掉。
可以看到,插件还是很简单的,只是我们在应用的时候,应该把插件中的activity看成是一个类,而不是android中的activity。
宿主目录结构:
从上面可以看到,宿主有4个类加一个接口:
1.MainActivity就是我们结果展示的两个button页面
2.BaseActivity中处理插件资源的访问
3.PlugProxyActivity中通过接口的方式管理插件activity的生命周期。
4.ProxyActivity中通过反射的方式管理插件activity的生命周期。
5.PluginInterface和我们插件的一样,这样包名也需要一样。
在具体分析宿主之前,要说下的是我这里没有把插件放到服务器上,直接通过复制到/data/data/宿主包名/cache下。
可以通过以上命令来把我们的apk放入到该目录下,这里测试的话用模拟器就行了,如果是真机的话,就需要root了。
先来看下BaseActivity:
package com.example.lujianxin.myapplication;import android.app.Activity;import android.content.pm.ActivityInfo;import android.content.pm.PackageInfo;import android.content.pm.PackageManager;import android.content.res.AssetManager;import android.content.res.Resources;import android.os.Build;import android.os.Bundle;import android.util.Log;import java.lang.reflect.Method;/** * Created by lujianxin on 2017/7/13. */public class BaseActivity extends Activity { protected AssetManager mAssetManager; protected Resources mResources; protected Resources.Theme mTheme; private ActivityInfo mActivityInfo; private PackageInfo packageInfo; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } //访问插件中的资源 protected void loadResources(String dexPath, Activity mProxyActivity) { initializeActivityInfo(dexPath); try { AssetManager assetManager = AssetManager.class.newInstance(); Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class); addAssetPath.invoke(assetManager, dexPath); mAssetManager = assetManager; } catch (Exception e) { Log.i("inject", "loadResource error:"+Log.getStackTraceString(e)); e.printStackTrace(); } Resources superRes = super.getResources(); superRes.getDisplayMetrics(); superRes.getConfiguration(); if (mActivityInfo.theme > 0) { mProxyActivity.setTheme(mActivityInfo.theme); } Resources.Theme superTheme = mProxyActivity.getTheme(); mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(),superRes.getConfiguration()); mTheme = mResources.newTheme(); mTheme.setTo(superTheme); try { mTheme.applyStyle(mActivityInfo.theme, true); } catch (Exception e) { e.printStackTrace(); } } @Override public AssetManager getAssets() { return mAssetManager == null ? super.getAssets() : mAssetManager; } @Override public Resources getResources() { return mResources == null ? super.getResources() : mResources; } private void initializeActivityInfo(String dexPath) { packageInfo = getApplicationContext().getPackageManager().getPackageArchiveInfo(dexPath, PackageManager.GET_ACTIVITIES | PackageManager.GET_SERVICES); if ((packageInfo.activities != null) && (packageInfo.activities.length > 0)) {// if (mClass == null) {// mClass = packageInfo.activities[0].name;// } //Finals 修复主题BUG int defaultTheme = packageInfo.applicationInfo.theme; for (ActivityInfo a : packageInfo.activities) {// if (a.name.equals(mClass)) { mActivityInfo = a; // Finals ADD 修复主题没有配置的时候插件异常 if (mActivityInfo.theme == 0) { if (defaultTheme != 0) { mActivityInfo.theme = defaultTheme; } else { if (Build.VERSION.SDK_INT >= 14) { mActivityInfo.theme = android.R.style.Theme_DeviceDefault; } else { mActivityInfo.theme = android.R.style.Theme; } }// } } } } } @Override public Resources.Theme getTheme() { return mTheme == null ? super.getTheme() : mTheme; }}
这里重点要关注的是Theme,与网上的很多资料不同,这里是直接拿DL框架中的代码过来的,如果用网上的资料来进行资源处理,会造成bug。
MainActivity:
package com.example.lujianxin.myapplication;import android.app.Activity;import android.content.Intent;import android.os.Environment;import android.os.Bundle;import android.view.View;import android.widget.Button;import android.widget.Toast;public class MainActivity extends Activity { private static final String TAG = "MainActivity"; private Button interfaceBtn, reflexBtn; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Toast.makeText(this, Environment.getExternalStorageDirectory().getAbsolutePath(), Toast.LENGTH_LONG).show(); interfaceBtn = (Button) findViewById(R.id.interface_btn); reflexBtn = (Button) findViewById(R.id.reflex_btn); reflexBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { //通过反射方式处理 Intent intent = new Intent(MainActivity.this, ProxyActivity.class); startActivity(intent); } }); interfaceBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { //通过接口的方式处理 Intent intent = new Intent(MainActivity.this, PlugProxyActivity.class); startActivity(intent); } }); }}
这里就起一个跳转的作用。
PlugProxyActivity:
package com.example.lujianxin.myapplication;import android.os.Bundle;import android.util.Log;import com.example.lujianxin.proxy.PluginInterface;import java.io.File;import java.lang.reflect.Constructor;import dalvik.system.DexClassLoader;/** * Created by lujianxin on 2017/7/14. */public class PlugProxyActivity extends BaseActivity { private static final String TAG = "ProxyActivity"; private static final String dexPath = "/data/data/com.example.lujianxin.myapplication/cache/proxy1.apk"; private static final String activityName = "com.example.lujianxin.proxy.MainActivity"; // dex解压之后存放的路径,如果是一个固定的路径运行程序的时候会报错:optimizedDirectory not readable/writable private File dexOutputDir; private Class<?> clazz; private PluginInterface mPluginInterface; @Override protected void onCreate(Bundle savedInstanceState) { loadResources(dexPath, this); super.onCreate(savedInstanceState); try { DexClassLoader loader = initClassLoader(); //动态加载插件Activity clazz = loader.loadClass(activityName); Constructor<?> localConstructor = clazz.getConstructor(new Class[] {}); //拿到我们的activity 强转成它实现的接口PluginInterface mPluginInterface = (PluginInterface) localConstructor.newInstance(new Object[] {}); mPluginInterface.setProxy(this); Bundle bundle = new Bundle(); mPluginInterface.onCreate(bundle); } catch (Exception e) { Log.d(TAG, e.toString()); } } private DexClassLoader initClassLoader(){ dexOutputDir = getApplicationContext().getDir("dex", 0); String filesDir = this.getCacheDir().getAbsolutePath(); String libPath = filesDir + File.separator +"proxy1.apk"; Log.i(TAG, "file-exist-PlugProxyActivity:"+new File(libPath).exists()); DexClassLoader loader = new DexClassLoader(libPath, dexOutputDir.getAbsolutePath(), null, getClass().getClassLoader()); return loader; } @Override protected void onDestroy() { mPluginInterface.onDestroy(); super.onDestroy(); } @Override protected void onPause() { mPluginInterface.onPause(); super.onPause(); } @Override protected void onResume() { mPluginInterface.onResume(); super.onResume(); } @Override protected void onStart() { mPluginInterface.onStart(); super.onStart(); } @Override protected void onStop() { mPluginInterface.onStop(); super.onStop(); }}
这里的重点就是,插件与宿主都存在一个包名相同的接口,同时插件activity要实现这个接口,然后我们通过classloader加载到插件中的activity的时候,就可以强壮成这个接口,然后通过这个接口来管理插件中的activity的生命周期。
ProxyActivity:
package com.example.lujianxin.myapplication;import android.app.Activity;import android.os.Bundle;import android.util.Log;import java.io.File;import java.lang.reflect.Constructor;import java.lang.reflect.Method;import dalvik.system.DexClassLoader;/** * Created by lujianxin on 2017/7/12. */public class ProxyActivity extends BaseActivity { private static final String TAG = "ProxyActivity"; private static final String dexPath = "/data/data/com.example.lujianxin.myapplication/cache/proxy1.apk"; private static final String activityName = "com.example.lujianxin.proxy.MainActivity"; // dex解压之后存放的路径,如果是一个固定的路径运行程序的时候会报错:optimizedDirectory not readable/writable private File dexOutputDir; private Class<?> clazz; private Object pluginActivity; @Override protected void onCreate(Bundle savedInstanceState) { loadResources(dexPath, this); super.onCreate(savedInstanceState); try { DexClassLoader loader = initClassLoader(); //获取插件activity实例 clazz = loader.loadClass(activityName); Constructor<?> localConstructor = clazz.getConstructor(new Class[] {}); pluginActivity = localConstructor.newInstance(new Object[] {}); //将代理对象设置给插件Activity Method setProxy = clazz.getMethod("setProxy",new Class[] { Activity.class }); setProxy.setAccessible(true); setProxy.invoke(pluginActivity, new Object[] { this }); //调用它的onCreate方法 Method onCreate = clazz.getDeclaredMethod("onCreate", new Class[] { Bundle.class }); onCreate.setAccessible(true); onCreate.invoke(pluginActivity, new Object[] { new Bundle() }); } catch (Exception e) { Log.i(TAG, e.toString()); } } private DexClassLoader initClassLoader(){ dexOutputDir = getApplicationContext().getDir("dex", 0); String filesDir = this.getCacheDir().getAbsolutePath(); String libPath = filesDir + File.separator +"proxy1.apk"; Log.i(TAG, "file-exist-proxyActivity:"+new File(libPath).exists()); DexClassLoader loader = new DexClassLoader(libPath, dexOutputDir.getAbsolutePath(), null, getClass().getClassLoader()); return loader; } @Override protected void onDestroy() { super.onDestroy(); ; try { Method method = clazz.getDeclaredMethod("onDestroy"); method.setAccessible(true); method.invoke(pluginActivity, new Object[]{}); } catch(Exception e){ Log.d(TAG, e.toString()); } } @Override protected void onPause() { super.onPause(); try { Method method = clazz.getDeclaredMethod("onPause"); method.setAccessible(true); method.invoke(pluginActivity, new Object[]{}); } catch(Exception e){ Log.d(TAG, e.toString()); } } @Override protected void onResume() { super.onResume(); try { Method method = clazz.getDeclaredMethod("onResume"); method.setAccessible(true); method.invoke(pluginActivity, new Object[]{}); } catch(Exception e){ Log.d(TAG, e.toString()); } } @Override protected void onStart() { super.onStart(); try { Method method = clazz.getDeclaredMethod("onStart"); method.setAccessible(true); method.invoke(pluginActivity, new Object[]{}); } catch(Exception e){ Log.d(TAG, e.toString()); } } @Override protected void onStop() { super.onStop(); try { Method method = clazz.getDeclaredMethod("onStop"); method.setAccessible(true); method.invoke(pluginActivity, new Object[]{}); } catch(Exception e){ Log.d(TAG, e.toString()); } }}
这里的实现就简单明了了,通过反射机制来调起插件activity的生命周期方法。
以上就是整个demo的实现,末尾会添加demo的链接。通过代理activity实现插件化,我们应该把插件中的所有类都看成是没有android特有的生命周期的类,运行的还是我们自己宿主中的activity,只是我们宿主activity的生命周期中运行的内容就是插件中的内容。前段时间看到有篇博客说的蛮不错的,这种方式就相当于牵线木偶,而我们的插件就相当于这个木偶,我们加载插件就相当于操作这个木偶;这种方式并不需要过多的涉及底层,另外还有一种Hook的方式实现-360DroidPlugin,想了解的可以去看看,对于framework层的了解有很大帮助。
插件化demo下载
阅读全文
0 0
- android 通过代理activity的方式实现插件化
- android 通过代理activity的方式实现插件化
- 【Android】Android插件开发 —— 打开插件的Activity(代理方式)
- Android插件化系列第(五)篇---Activity的插件化方案(代理模式)
- android传递数据方式4--通过Intent实现Activity之间的数据传递
- Android插件开发 —— 通过预注册方式打开activity(记录我踩过的坑)
- android程序共享activity实现插件化
- 通过代理上网的方式
- 通过代理Activity模式,以移花接木的方式,加载sd卡目录下的apk界面
- 通过代理Activity模式,以移花接木的方式,加载sd卡目录下的apk界面
- 【Android】Android插件开发 —— 打开插件的Activity(预注册方式)
- android如何通过TextView实现不同的Activity的切换
- android:cha1.2 通过Intent实现Activity之间的通信
- Android通过Activity栈方式对Activity管理
- Android通过Activity栈方式对Activity管理
- Android通过Activity栈方式对Activity管理
- Android 便捷的方式实现Activity变暗的效果
- 由浅入深分析mybatis通过动态代理实现拦截器(插件)的原理
- java.lang.NoClassDefFoundError: com/mchange/v2/ser/Indirector
- GSON
- MobX 入门教程
- 指针实现 strcpy()字符串拷贝
- Batch Normalization导读
- android 通过代理activity的方式实现插件化
- Lombok
- Terminator安装与设置
- 【Java学习之路】Java编程之判断素数
- c++父类与子类的转换(QT环境下)
- Android的按键页面跳转设计
- Google Analytics
- jsp与Servlet
- Python3 简介及使用