android 通过代理activity的方式实现插件化
2017-07-18 16:18
543 查看
前言:
一直以来就对插件化这技术推崇已久,在去年也写过两篇关于插件化基础的文章: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下载
相关文章推荐
- android 通过代理activity的方式实现插件化
- android传递数据方式4--通过Intent实现Activity之间的数据传递
- Android中通过view方式获取当前Activity的屏幕截图实现方法
- Android通过共享用户ID来实现多Activity进程共享
- android 分享功能实现 即通过其他activity分享
- Android通过共享用户ID实现多个Activity进程共享(SharedUserID)
- android 分享功能实现 即通过其他activity分享
- [转]Android通过共享用户ID来实现多个Activity进程共享
- Android通过共享用户ID来实现多Activity进程共享
- Android通过类对象的方式实现JSON数据的解析
- struts2中通过Action以InputStream的下载文件以及在iOS以及Android端接收的实现方式
- 用代理方式实现android系统抓包
- Android通过共享用户ID来实现多Activity进程共享
- Android通过共享用户ID来实现多Activity进程共享
- Android通过共享用户ID来实现多Activity进程共享
- struts2中通过Action以InputStream的下载文件以及在iOS以及Android端接收的实现方式
- Android通过共享用户ID来实现多Activity进程共享
- Android通过共享用户ID来实现多个Activity进程共享
- android:cha1.2 通过Intent实现Activity之间的通信
- Android通过共享用户ID来实现多个Activity进程共享