再来看看支付宝,可以发现支付宝中提供了很多第三方app的入口,而点击这些入口跳转的也都是native页面。应用市场上的支付宝app一共只有二三十MB,而如果这些app都集成到支付宝中,那支付宝的size就不是二三十MB了,那就是二三十GB了!!
本篇blog的主题是介绍Android插件化技术,并且会提供一个仿支付宝插件化技术的demo,告诉你支付宝是如何把一个第三方app作为插件集成到自己的app里的。
###二. 插件化好处
- 宿主和插件分开编译
编译时只需要编译宿主app,插件app是在编译好后下发到宿主app里的。 - 并发开发
宿主app什么时候发布版本跟插件app什么时候开发完没有关系,宿主app只要开发完并且为插件app提供一个入口就可以了。 - 动态更新插件
插件app在开发完后下发到宿主app里,点击相应的入口就可以跳转到最新版的插件app了。 - 按需下载模块
- 解决方法数或变量数爆棚(65536)
###三. 随便一个app都能集成到支付宝吗?
答案是:不能!
我们来思考,支付宝要跳转到一个插件的Activity,而插件是没有被安装的,它没有上下文,也就没有生命周期,那么插件Activity的生命周期就要由宿主app来控制。为此,我们需要建立一套标准。
###四. 插件化程序结构
我们先来看下插件化程序结构,了解下其大致框架,对程序有宏观感受。下面我们直接开始撸代码。
###五. 动态加载apk
#####1. 插件app的activity没有在宿主app中注册,该怎么办?
插桩,一个空的Activity,专门用来加载插件app中的activity,这个Activity叫ProxyActivity,后面我会具体去讲这个空Activity该如何实现。我们只需要在宿主app里注册这个Activity就可以了。
#####2. 加载插件app中的Activity
实际场景中插件apk肯定是由服务端下发后,保存到SD卡的某个文件夹下。这里将编译好的插件apk放到手机外置SD卡的根目录中,我们来演示宿主app如何去加载插件app中的Activity。
###六. 资源加载
接下来我们来看下FluginManager的loadPath方法如何实现。如果要实现这个功能,首先想到的肯定是用反射。
可是你别忘记了,插件app根本就没有安装,这里是无法找到这个Class的。我们需要DexClassLoader来完成Activity类的加载。
PluginManager的getDexClassLoader的实现如下:
讲完了如何加载Activity,我们来讲下如何加载Activity中用到的资源文件。我们在日常开发中需要资源文件时,我们是通过getResources()来获取。例如加载一个图片:
getResources().getDrawable()
可现在我们需要获取的是另外一个app的资源,所以这里就需要自己实现一个getResources()方法。
PluginManager的getResources方法实现如下:
至此,一个插件app的activity加载功能就实现完成了,下面我们来看如何跳转。
###七. 跳转到插件app中的Activity
由于我们需要读取SD卡中的插件apk,这里别忘记加上SD卡的读写权限
这就可以实现跳转了。下面我们来看下效果
奇怪,为什么我们跳转到插件app的activity是空白的?我们来看下插件app的activity应该长什么样子。
当然这里我只在插件app的主activity里放了一张图片,并没有写复杂的布局。可是为什么我们跳过来的是空白页呢?我们再看下ProxyActivity的代码:
package com.ctrip.pluginapplication
import android.content.res.Resources
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
/**
* 壳!专门用来加载插件Activity
* @author Zhenhua on 2018/3/3.
* @email zhshan@ctrip.com ^.^
*/
class ProxyActivity : AppCompatActivity() {
/**
* 要跳转的activity的name
*/
private var className = ""
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
/**
* step1:得到插件app的activity的className
*/
className = intent.getStringExtra("className")
/**
* step2:通过反射拿到class,
* 但不能用以下方式,因为插件app没有被安装!
*/
// classLoader.loadClass(className)
// Class.forName(className)
}
override fun getClassLoader(): ClassLoader {
//不用系统的ClassLoader,用dexClassLoader加载
return PluginManager.getInstance().getDexClassLoader() as? ClassLoader
?: super.getClassLoader()
}
override fun getResources(): Resources {
//不用系统的resources,自己实现一个resources
return PluginManager.getInstance().getResources() ?: super.getResources()
}
}
我们发现,我们这里还没有在ProxyActivity里写逻辑啊,我们只是得到了插件app的主activity的name,这时activity还没有生命周期。?我们接着来实现。我们需要让ProxyActivity控制插件app的activity的生命周期,所以我们需要得到插件app的activity的实例,然后去控制其生命周期:
package com.ctrip.pluginapplication
import android.content.res.Resources
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import com.ctrip.standard.AppInterface
/**
* 壳!专门用来加载插件Activity
* @author Zhenhua on 2018/3/3.
* @email zhshan@ctrip.com ^.^
*/
class ProxyActivity : AppCompatActivity() {
/**
* 要跳转的activity的name
*/
private var className = ""
private var appInterface: AppInterface? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
/**
* step1:得到插件app的activity的className
*/
className = intent.getStringExtra("className")
/**
* step2:通过反射拿到class,
* 但不能用以下方式
* classLoader.loadClass(className)
* Class.forName(className)
* 因为插件app没有被安装!
* 这里我们调用我们重写过多classLoader
*/
var activityClass = classLoader.loadClass(className)
var constructor = activityClass.getConstructor()
var instance = constructor.newInstance()
appInterface = instance as?AppInterface
appInterface?.attach(this)
var bundle = Bundle()
appInterface?.onCreate(bundle)
}
override fun onStart() {
super.onStart()
appInterface?.onStart()
}
override fun onResume() {
super.onResume()
appInterface?.onResume()
}
override fun onDestroy() {
super.onDestroy()
appInterface?.onDestroy()
}
override fun getClassLoader(): ClassLoader {
//不用系统的ClassLoader,用dexClassLoader加载
return PluginManager.getInstance().getDexClassLoader() as? ClassLoader
?: super.getClassLoader()
}
override fun getResources(): Resources {
//不用系统的resources,自己实现一个resources
return PluginManager.getInstance().getResources() ?: super.getResources()
}
}
这时我们就可以成功跳转了。ok,插件化实现完成。我们来看下效果。
这里**附上demo(点击下载)**,如有任何疑问可留言提问,博主每天都会查看。
~~~华丽丽的分割线:在插件app中实现更多功能
之前我们的插件app的activity其实就只是加载了一个imageView,我们现在来实现这样一个功能:“点击ImageView,弹出一个toast”。
代码如下:
我们来看下效果。。
然而,点击竟然crash了。我们来贴下错误日志:
原来我们在插件Activity中不能用自己的上下文,我们应该用that!!
代码已经更新到github上,欢迎下载体验。