探索Android开源框架 - 10. 插件化原理
2021-12-27 本文已影响0人
今阳说
什么是插件化
- 插件化技术最初源于免安装运行apk的想法,这个免安装的apk可以理解为插件
- 将app中一些不常用的功能模块做成插件,一方面减小了安装包的大小,另一方面可以实现app功能的动态扩展;
- 插件框架有两个作用:一是“自解耦”,二是“免安装”
- 自解耦指的是一个应用原本由一份代码编译而成,希望改成将其中的一些功能单独编译,像插件一样动态插在主应用上。这样一来可是使主应用体积变小,下载安装更方便。二来可以是比较独立的功能可以单独开发调试,甚至单独更新版本。
- 免安装指的一个应用原本需要安装过程才能启动运行,希望改为无需安装即可从一个已经安装运行的App中启动起来。这一需求的主要目的是提高流量复用的能力。
插件化发展史
- AndroidDynamicLoader:给予 Fragment 实现了插件化框架,可以动态加载插件中的 Fragment 实现页面的切换;
- dynamic-load-apk(任玉刚):最早使用ProxyActivity这种静态代理技术,由ProxyActivity去控制插件中PluginActivity的生命周期(缺点:插件中的activity必须继承PluginActivity,开发时要小心处理context);
- DroidPlugin:通过Hook系统服务的方式启动插件中的Activity,使得开发插件的过程和开发普通的app没有什么区别(缺点:由于hook过多系统服务,异常复杂且不够稳定)
- 之后各个框架在实现原理上都是趋近于选择尽量少的hook,并通过在manifest中预埋一些组件实现对四大组件的插件化,又做了不同程度的扩展
- 携程 DynamicApk
- VirtualApp:能够完全模拟app的运行环境,能够实现app的免安装运行和双开技术
- Small: 一个跨平台插件化框架
- 360 RePlugin
- 滴滴 VirtualApk
- 阿里 Atlas:一个结合组件化和热修复技术的一个app基础框架,号称是一个容器化框架
- 腾讯 Shadow:一个完全无Hack,甚至零反射实现的Android插件框架,插件的代码完全是一个正常可安装的App代码,无需引用任何Shadow的库
插件化原理
类加载 ClassLoader
java 中的 ClassLoader:
- BootstrapClassLoader:负责加载 JVM 运行时的核心类,比如 JAVA_HOME/lib/rt.jar 等
- ExtensionClassLoader:负责加载 JVM 的扩展类,比如 JAVA_HOME/lib/ext 下面的 jar 包
- AppClassLoader:负责加载 classpath 里的 jar 包和目录
android 中的 ClassLoader:
- PathClassLoader 和 DexClassLoader 都能加载外部的 dex/apk,只不过区别是 DexClassLoader 可以指定 optimizedDirectory,也就是 dex2oat 的产物 .odex 存放的位置,而 PathClassLoader 只能使用系统默认位置。
- 但是这个 optimizedDirectory 在 Android 8.0 以后也被舍弃了,只能使用系统默认的位置了,也就是说,在 8.0 上,PathClassLoader 和 DexClassLoader 其实已经没有什么区别了。
双亲委派机制:
- 每一个 ClassLoader 中都有一个 parent 对象,代表的是父类加载器,在加载一个类的时候,会先使用父类加载器去加载,如果在父类加载器中没有找到,自己再进行加载,如果 parent 为空,那么就用系统类加载器来加载。通过这样的机制可以保证系统类都是由系统类加载器加载的。
如何加载插件中的类
- 通过给插件apk生成相应的DexClassLoader便可以访问其中的类,这里又有两种实现方案单DexClassLoader与多DexClassLoader
单DexClassLoader
- 插件的DexClassLoader中的pathList合并到主工程的DexClassLoader中
- 优点是不同的插件以及主工程间可以直接互相调用类和方法,并且可以将不同插件的公共模块抽出来放在一个common插件中直接供其他插件使用(Small采用此方案)
- 缺点:若两个不同的插件工程引用了一个库的不同版本,则程序可能会出错,所以要通过一些规范去避免该情况发生。
- 实现代码如下
binding.btnSingleDexClassLoader.setOnClickListener {
loadDex(this, listOf(plugin001Path,plugin002Path))
val clazzApp = Class.forName("com.jinyang.plugindemo.TestApp")
val methodApp = clazzApp.getMethod("test")
methodApp.invoke(clazzApp.newInstance())
val clazzPlugin001 = Class.forName("com.jinyang.plugin001.TestPlugin001")
val methodPlugin001 = clazzPlugin001.getMethod("test")
methodPlugin001.invoke(clazzPlugin001.newInstance())
val clazzPlugin002 = Class.forName("com.jinyang.plugin002.TestPlugin002")
val methodPlugin002 = clazzPlugin002.getMethod("test")
methodPlugin002.invoke(clazzPlugin002.newInstance())
}
fun loadDex(context: Context, pluginPaths: List<String>) {
try {
// 获取 pathList
val systemClassLoader = Class.forName("dalvik.system.BaseDexClassLoader")
val pathListField = systemClassLoader.getDeclaredField("pathList")
pathListField.isAccessible = true
// 获取 dexElements
val dexPathListClass = Class.forName("dalvik.system.DexPathList")
val dexElementsField = dexPathListClass.getDeclaredField("dexElements")
dexElementsField.isAccessible = true
// 获取宿主的Elements
val hostClassLoader = context.classLoader
val hostPathList = pathListField.get(hostClassLoader)
val hostElements = dexElementsField.get(hostPathList) as kotlin.Array<*>
var newElements: kotlin.Array<*> = hostElements
// 遍历获取插件的Elements
for (path in pluginPaths) {
val pluginClassLoader = PathClassLoader(path, context.classLoader)
val pluginPathList = pathListField.get(pluginClassLoader)
val pluginElements = dexElementsField.get(pluginPathList) as kotlin.Array<*>
// 创建数组
val temp = Array.newInstance(
pluginElements.javaClass.componentType!!,
newElements.size + pluginElements.size
) as kotlin.Array<*>
// 给新数组赋值,先用宿主的,再用插件的
System.arraycopy(newElements, 0, temp, 0, newElements.size)
System.arraycopy(
pluginElements,
0,
temp,
newElements.size,
pluginElements.size
)
// 合并
dexElementsField.set(hostPathList, temp)
newElements = temp
}
} catch (e: Exception) {
e.printStackTrace()
}
}
多DexClassLoader
- 为每个插件都生成一个DexClassLoader,当加载该插件中的类时需要通过对应DexClassLoader加载
- 这种方案的优点是不同插件的类是隔离的,当不同插件引用了同一个类库的不同版本时,不会出问题(RePlugin采用此方案)
- 代码如下
val nativeLibDir = File(filesDir, "pluginlib").absolutePath
val dexOutPath = File(filesDir, "dexout").absolutePath
val plugin001Path = File(filesDir.absolutePath, "plugin001.apk").absolutePath
val pluginClassLoader = DexClassLoader(plugin001Path, dexOutPath, nativeLibDir, this::class.java.classLoader)
val plugin002Path: String = File(filesDir.absolutePath, "plugin002.apk").absolutePath
val pluginClassLoader2 = DexClassLoader(plugin002Path, dexOutPath, nativeLibDir, this::class.java.classLoader)
资源加载
- Android系统通过Resource对象加载资源,因此,只要将插件apk的路径加入到AssetManager中,便能够实现对插件资源的访问
资源路径的处理
1. 合并式:
- addAssetPath时加入所有插件和主工程的路径;
- 优点:插件和宿主能直接相互访问资源
- 缺点:会引入资源冲突(由于主工程和各个插件都是独立编译的,生成的资源id会存在相同的情况)
- 实现代码如下,其中我将插件的包名与宿主设置成相同的,具体原因见下面shadow部分的说明:
binding.btnPrintResources.setOnClickListener {
val plugin001Path = File(filesDir.absolutePath, "plugin001.apk").absolutePath
val plugin002Path: String = File(filesDir.absolutePath, "plugin002.apk").absolutePath
val mResources = loadResources(this,resources.assets, listOf(pluginPath, pluginPath2))
val strAppId = mResources?.getIdentifier("str_app", "string", "com.jinyang.plugindemo")
log("str_app:"+ strAppId?.let { it1 -> mResources.getString(it1) })
val strPlugin001Id = mResources?.getIdentifier("str_plugin001", "string", "com.jinyang.plugindemo")
log("str_plugin001:"+ strPlugin001Id?.let { it1 -> mResources.getString(it1) })
val strPlugin002Id = mResources?.getIdentifier("str_plugin002", "string", "com.jinyang.plugindemo")
log("str_plugin002:"+ strPlugin002Id?.let { it1 -> mResources.getString(it1) })
}
fun loadResources(context: Context,assetManager:AssetManager, pluginPaths: List<String>): Resources? {
try {
val addAssetPathMethod = assetManager::class.java.getDeclaredMethod("addAssetPath", String::class.java)
addAssetPathMethod.isAccessible = true
for (path in pluginPaths) {
addAssetPathMethod.invoke(assetManager, path)
}
return Resources(
assetManager,
context.resources.displayMetrics,
context.resources.configuration
)
} catch (e: Exception) {
e.printStackTrace()
}
return null
}
2. 独立式:
- 各个插件只添加自己apk路径
- 优点:资源隔离,不存在资源冲突
- 缺点:资源共享比较麻烦(如果想要实现资源的共享,必须拿到对应的Resource对象)
- 实现方式如下,在各个插件的baseActivity中重写getResources,getAssets方法
open class PluginBaseActivity : Activity() {
private var pluginClassLoader: ClassLoader? = null
private var pluginPath: String?=null
private var pluginAssetManager: AssetManager? = null
private var pluginResources: Resources? = null
private var pluginTheme: Resources.Theme? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val nativeLibDir = File(filesDir, "pluginlib").absolutePath
val dexOutPath = File(filesDir, "dexout").absolutePath
pluginPath = File(filesDir.absolutePath, "plugin002.apk").absolutePath
pluginClassLoader = DexClassLoader(pluginPath, dexOutPath, nativeLibDir, this::class.java.classLoader)
handleResources()
}
override fun getResources(): Resources? {
return pluginResources ?: super.getResources()
}
override fun getAssets(): AssetManager {
return pluginAssetManager ?: super.getAssets()
}
override fun getClassLoader(): ClassLoader {
return pluginClassLoader ?: super.getClassLoader()
}
private fun handleResources() {
try {
pluginAssetManager = AssetManager::class.java.newInstance()
val addAssetPathMethod = pluginAssetManager?.javaClass?.getMethod("addAssetPath", String::class.java)
addAssetPathMethod?.invoke(pluginAssetManager, pluginPath)
} catch (e: Exception) {
}
pluginResources = Resources(pluginAssetManager, super.getResources().displayMetrics, super.getResources().configuration)
pluginTheme = pluginResources?.newTheme()
pluginTheme?.setTo(super.getTheme())
}
}
解决资源id冲突问题的方法:
- 修改aapt源码,定制aapt工具,编译期间修改PP段;(DynamicAPK使用此方案),原理参考:Android中如何修改编译的资源ID值
- 修改aapt的产物resources.arsc文件,即,编译后期重新整理插件Apk的资源,编排ID;(VirtualApk使用此方案),原理参考:插件化-解决插件资源ID与宿主资源ID冲突的问题
- 通过配置aaptOptions,build.gradle中的android节点加入如下代码,不过此方法只有在compileSdkVersion为28及以上才生效
android {
aaptOptions {
additionalParameters "--package-id", "0x66","--allow-reserved-package-id"
}
...
}
Context的处理
- 通常我们通过Context对象访问资源,光创建出Resource对象还不够,因此还需要一些额外的工作
// 获取自己创建的resources
val resources = LoadUtils.getResources(application)
// 创建自己的Context
mContext = ContextThemeWrapper(baseContext, 0)
// 把自己的Context中的resources替换为我们自己的
val clazz = mContext::class.java
val mResourcesField = clazz.getDeclaredField("mResources")
mResourcesField.isAccessible = true
mResourcesField.set(mContext, resources)
- 也可以参考VirtualAPK中的实现:com.didi.virtualapk.internal.ResourcesManager
public static void hookResources(Context base, Resources resources) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return;
}
try {
// 替换主工程context中LoadedApk的mResource对象
Reflector reflector = Reflector.with(base);
reflector.field("mResources").set(resources);
Object loadedApk = reflector.field("mPackageInfo").get();
Reflector.with(loadedApk).field("mResources").set(resources);
// 将新的Resource添加到主工程ActivityThread的mResourceManager中,并且根据Android版本做了不同处理
Object activityThread = ActivityThread.currentActivityThread();
Object resManager;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
resManager = android.app.ResourcesManager.getInstance();
} else {
resManager = Reflector.with(activityThread).field("mResourcesManager").get();
}
Map<Object, WeakReference<Resources>> map = Reflector.with(resManager).field("mActiveResources").get();
Object key = map.keySet().iterator().next();
map.put(key, new WeakReference<>(resources));
} catch (Exception e) {
Log.w(TAG, e);
}
}
加载四大组件
- 四大组件的插件化是插件化技术的核心
加载 插件Activity
插件Activity的两个问题
- 在宿主的Manifest没有注册:插件是动态加载的,所以插件的四大组件不可能注册到宿主的 Manifest 文件中,启动一个没有在 Manifest 中注册的 Activity会报错ActivityNotFoundException
- 生命周期无法被调用:一个 Activity 主要的工作,都是在其生命周期方法中调用的
解决方法
- 手动去调用插件 Activity 的生命周期;
- 欺骗系统,让系统以为 Activity 是注册在 Manifest 中的
- 调用插件 Activity 的生命周期主要有三种实现方式,反射、接口和Hook实现, 其中反射和接口 实现相对简单,不需要对系统内部实现做过多了解;而且比较稳定,不用适配各种厂商ROM及不同Android版本的API,但是通过反射效率太低,通过接口需要实现的方法数量很多;
1. 反射实现 调用 插件Activity 的生命周期:
- 创建一个反射生命周期的工具类ReflectActivityLifeCircle,其中通过class.getMethod来反射调用Activity的各个声明周期方法,代码如下:
class ReflectActivityLifeCircle(activity: String?, activityClassLoader: ClassLoader?) {
private var clazz: Class<Activity>? = activityClassLoader?.loadClass(activity) as Class<Activity>?
private var activity: Activity? = clazz?.newInstance()
private fun getMethod(methodName: String, vararg params: Class<*>): Method? {
return clazz?.getMethod(methodName, *params)
}
fun attach(proxyActivity: Activity?) {
getMethod("attach", Activity::class.java)?.invoke(activity, proxyActivity) }
fun onCreate(savedInstanceState: Bundle?) {
getMethod("onCreate", Bundle::class.java)?.invoke(activity, savedInstanceState)
}
fun onStart() {
getMethod("onStart")?.invoke(activity)
}
fun onResume() {
getMethod("onResume")?.invoke(activity)
}
fun onPause() {
getMethod("onPause")?.invoke(activity)
}
fun onStop() {
getMethod("onStop")?.invoke(activity)
}
fun onDestroy() {
getMethod("onDestroy")?.invoke(activity)
}
}
- 在宿主中创建一个代理Activity,其生命周期直接调用ReflectActivityLifeCircle的方法,通过反射调用插件Activity的生命周期,代码如下
class ProxyReflectActivity : ProxyBaseActivity() {
private var reflectActivityLifeCircle: ReflectActivityLifeCircle? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val nativeLibDir = File(filesDir, "pluginlib").absolutePath
val dexOutPath = File(filesDir, "dexout").absolutePath
val pluginPath = intent.getStringExtra("pluginPath")
val pluginActivityName = intent.getStringExtra("activityName")
val pluginClassLoader = DexClassLoader(pluginPath, dexOutPath, nativeLibDir, this::class.java.classLoader)
reflectActivityLifeCircle= ReflectActivityLifeCircle(pluginActivityName,pluginClassLoader)
reflectActivityLifeCircle?.attach(this)
reflectActivityLifeCircle?.onCreate(savedInstanceState)
}
override fun onStart() {
super.onStart()
reflectActivityLifeCircle?.onStart()
}
override fun onResume() {
super.onResume()
reflectActivityLifeCircle?.onResume()
}
override fun onPause() {
super.onPause()
reflectActivityLifeCircle?.onPause()
}
override fun onStop() {
super.onStop()
reflectActivityLifeCircle?.onStop()
}
override fun onDestroy() {
super.onDestroy()
reflectActivityLifeCircle?.onDestroy()
}
companion object{
fun startPluginActivity(context: Context, pluginPath: String, activityName: String) {
val intent = Intent(context, ProxyReflectActivity::class.java)
intent.putExtra("pluginPath", pluginPath)
intent.putExtra("activityName", activityName)
context.startActivity(intent)
}
}
}
- 反射实现会对性能有所影响,主流的插件化框架没有采用此方式
2. 通过接口实现 调用 插件Activity 的生命周期
- 定义一个接口,注意宿主和插件中用的接口全路径应相同
interface IPluginActivity {
fun attach(proxyActivity: Activity)
fun onCreate(savedInstanceState: Bundle?)
fun onStart()
fun onResume()
fun onPause()
fun onStop()
fun onDestroy()
}
- 在插件的baseActivity中实现该接口
open class BasePluginActivity : Activity(), IPluginActivity {
var proxyActivity: Activity? = null
override fun attach(proxyActivity: Activity) {
this.proxyActivity = proxyActivity
}
override fun onCreate(savedInstanceState: Bundle?) {
if (proxyActivity == null) {
super.onCreate(savedInstanceState)
}
}
override fun setContentView(layoutResID: Int) {
log("proxyActivity=$proxyActivity,layoutResID=$layoutResID")
proxyActivity?.let {
it.setContentView(layoutResID)
} ?: run {
super.setContentView(layoutResID)
}
}
override fun setContentView(view: View?) {
proxyActivity?.let {
it.setContentView(view)
} ?: run {
super.setContentView(view)
}
}
override fun onStart() {
if (proxyActivity == null) {
super.onStart()
}
}
override fun onResume() {
if (proxyActivity == null) {
super.onResume()
}
}
override fun onPause() {
if (proxyActivity == null) {
super.onPause()
}
}
override fun onStop() {
if (proxyActivity == null) {
super.onStop()
}
}
override fun onDestroy() {
if (proxyActivity == null) {
super.onDestroy()
}
}
override fun getResources(): Resources? {
if (proxyActivity == null) {
return super.getResources()
}
return proxyActivity?.resources
}
override fun getTheme(): Resources.Theme? {
if (proxyActivity == null) {
return super.getTheme()
}
return proxyActivity?.theme
}
override fun getLayoutInflater(): LayoutInflater {
if (proxyActivity == null) {
return super.getLayoutInflater()
}
return proxyActivity?.layoutInflater!!
}
}
- 在宿主中创建一个代理Activity,通过插件的classLoader及插件ActivityName获取插件Activity实例,并强转为IPluginActivity类型,并在宿主Activity的生命周期中调用IPluginActivity对应方法
class ProxyInterfaceActivity : Activity() {
private var activity: IPluginActivity?=null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val nativeLibDir = File(filesDir, "pluginlib").absolutePath
val dexOutPath = File(filesDir, "dexout").absolutePath
val pluginPath = intent.getStringExtra("pluginPath")
val pluginActivityName = intent.getStringExtra("activityName")
val pluginClassLoader = DexClassLoader(pluginPath, dexOutPath, nativeLibDir, this::class.java.classLoader)
//通过插件的classLoader及插件ActivityName获取插件Activity实例,并强转为IPluginActivity类型
activity=pluginClassLoader?.loadClass(pluginActivityName)?.newInstance() as IPluginActivity
//在宿主Activity的生命周期中调用IPluginActivity对应生命周期方法
activity?.attach(this)
activity?.onCreate(savedInstanceState)
}
override fun onStart() {
super.onStart()
activity?.onStart()
}
override fun onResume() {
super.onResume()
activity?.onResume()
}
override fun onPause() {
super.onPause()
activity?.onPause()
}
override fun onStop() {
super.onStop()
activity?.onStop()
}
override fun onDestroy() {
super.onDestroy()
activity?.onDestroy()
}
companion object {
fun startPluginActivity(context: Context, pluginPath: String, activityName: String) {
val intent = Intent(context, ProxyInterfaceActivity::class.java)
intent.putExtra("pluginPath", pluginPath)
intent.putExtra("activityName", activityName)
context.startActivity(intent)
}
}
}
3. Hook实现
- 主要有两种解决方案 ,一种是通过Hook IActivityManager来实现,一种是Hook Instrumentation实现
Activity的启动过程
- 分为两种,一种是根Activity的启动过程,一种是普通Activity的启动过程
- 根Activity的启动过程: 首先Launcher进程向AMS请求创建根Activity,AMS会判断根Activity所需的应用程序进程是否存在并启动,如果不存在就会请求Zygote进程创建应用程序进程。应用程序进程启动后,AMS会请求应用程序进程创建并启动根Activity。
- 普通Activity的启动过程: 在应用程序进程中的Activity向AMS请求创建普通Activity,AMS会对这个Activty的生命周期管和栈进行管理,校验Activity等等。如果Activity满足AMS的校验,AMS就会请求应用程序进程中的ActivityThread去创建并启动普通Activity。
Hook IActivityManager方案实现
- AMS是在SystemServer进程中,无法直接进行修改,只能在应用程序进程中做文章。可以采用预先占坑的方式来解决没有在AndroidManifest.xml中显示声明的问题,具体做法就是使用一个在AndroidManifest.xml中注册的Activity来进行占坑,用来通过AMS的校验,然后用插件Activity替换占坑的Activity
- 在宿主中创建一个占坑Activity
class StubHookActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
}
<activity
android:name=".hook.StubHookActivity"
android:exported="false" />
- 使用占坑Activity通过AMS验证
// Android 8.0与7.0的AMS家族有一些差别,主要是Android 8.0去掉了AMS的代理ActivityManagerProxy,代替它的是IActivityManager,直接采用AIDL来进行进程间通信。
// Android7.0的Activity的启动会调用ActivityManagerNative的getDefault方法, Android8.0的Activity的启动会调用ActivityManager的getService方法,两者都返回了IActivityManager类型的对象。
public static void hookAMS() {
try {
Object singleTon = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
//android 29或以上版本的API
@SuppressLint("PrivateApi")
Class<?> activityManagerClass = Class.forName("android.app.ActivityTaskManager");
Field iActivityManagerSingletonField = activityManagerClass.getDeclaredField("IActivityTaskManagerSingleton");
iActivityManagerSingletonField.setAccessible(true);
singleTon = iActivityManagerSingletonField.get(null);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
//android 26或以上版本的API是一样的
Class<?> activityManagerClass = Class.forName("android.app.ActivityManager");
Field iActivityManagerSingletonField = activityManagerClass.getDeclaredField("IActivityManagerSingleton");
iActivityManagerSingletonField.setAccessible(true);
singleTon = iActivityManagerSingletonField.get(null);
} else {
//android 26或以下版本的API是一个系列
Class<?> activityManagerClass = Class.forName("android.app.ActivityManagerNative");
Field iActivityManagerSingletonField = activityManagerClass.getDeclaredField("gDefault");
iActivityManagerSingletonField.setAccessible(true);
singleTon = iActivityManagerSingletonField.get(null);
}
Class<?> singleTonClass = Class.forName("android.util.Singleton");
Field mInstanceField = singleTonClass.getDeclaredField("mInstance");
mInstanceField.setAccessible(true);
// 获取到IActivityManagerSingleton的对象
final Object iActivityManager = mInstanceField.get(singleTon);
Class<?> iActivityManagerClass;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
iActivityManagerClass = Class.forName("android.app.IActivityTaskManager");
} else {
iActivityManagerClass = Class.forName("android.app.IActivityManager");
}
Object newInstance = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
new Class[]{iActivityManagerClass},
(o, method, args) -> {
if ("startActivity".equals(method.getName())) {
// 拦截startActivity方法,接着获取参数args中第一个Intent对象
// 它是原本要启动插件Plugin002Activity的Intent
Intent intent = null;
int index = 0;
for (int i = 0; i < args.length; i++)
if (args[i] instanceof Intent) {
index = i;
break;
}
intent = (Intent) args[index];
//新建一个subIntent用来启动的StubActivity
Intent subIntent = new Intent();
String packageName = "com.jinyang.plugindemo";
subIntent.setClassName(packageName, packageName + ".hook.StubHookActivity");
//将这个Plugin002Activity的Intent保存到subIntent中,便于以后还原Plugin002Activity
subIntent.putExtra(HookHelper.TARGET_INTENT, intent);
//用subIntent赋值给参数args,这样启动的目标就变为了StubActivity,用来通过AMS的校验。
args[index] = subIntent;
}
return method.invoke(iActivityManager, args);
});
mInstanceField.set(singleTon, newInstance);
} catch (Exception e) {
e.printStackTrace();
}
}
- 试着跳转一下会发现并没有跳转到Plugin002Activity,而是跳到了StubHookActivity,因为被我们的hookAMS拦截了
binding.btnHookIActivityManager.setOnClickListener {
hookAMS()//可以放到Application中对全局生效
val intent = Intent()
val packageName = "com.jinyang.plugindemo"
val activityName = "com.jinyang.plugin002.Plugin002Activity"
intent.component = ComponentName(packageName, activityName)
startActivity(intent)
}
- 还原插件Activity:ActivityThread类中有一个静态变量sCurrentActivityThread,用于表示当前的ActivityThread对象,通过替换其mH:Handler, 重写handleMessage并拦截对应的msg,将启动StubHookActivity的Intent替换回启动Plugin002Activity
public static void hookHandler() {
try {
// 获取ActivityThread实例
final Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Field activityThreadField = activityThreadClass.getDeclaredField("sCurrentActivityThread");
activityThreadField.setAccessible(true);
final Object activityThread = activityThreadField.get(null);
// 获取Handler实例
Field mHField = activityThreadClass.getDeclaredField("mH");
mHField.setAccessible(true);
Object mH = mHField.get(activityThread);
Class<?> handlerClass = Class.forName("android.os.Handler");
Field mCallbackField = handlerClass.getDeclaredField("mCallback");
mCallbackField.setAccessible(true);
mCallbackField.set(mH, (Handler.Callback) msg -> {
switch (msg.what) {
case 100: // API 28 以前直接接收
try {
// 获取ActivityClientRecord中的intent对象
Field intentField = msg.obj.getClass().getDeclaredField("intent");
intentField.setAccessible(true);
Intent proxyIntent = (Intent) intentField.get(msg.obj);
// 拿到插件的Intent
Intent intent = proxyIntent.getParcelableExtra(TARGET_INTENT);
// 替换回来
proxyIntent.setComponent(intent.getComponent());
} catch (Exception e) {
e.printStackTrace();
}
break;
case 159: // API 28 以后加入了 lifecycle, 这里msg发生了变化
try {
Field mActivityCallbacksField = msg.obj.getClass().getDeclaredField("mActivityCallbacks");
mActivityCallbacksField.setAccessible(true);
List<Object> mActivityCallbacks = (List<Object>) mActivityCallbacksField.get(msg.obj);
for (int i = 0; i < mActivityCallbacks.size(); i++) {
Class<?> itemClass = mActivityCallbacks.get(i).getClass();
Log.d("LJY_LOG","itemClass:"+itemClass);
if (itemClass.getName().equals("android.app.servertransaction.LaunchActivityItem")) {
Field intentField = itemClass.getDeclaredField("mIntent");
intentField.setAccessible(true);
Intent proxyIntent = (Intent) intentField.get(mActivityCallbacks.get(i));
Intent intent = proxyIntent.getParcelableExtra(TARGET_INTENT);
proxyIntent.setComponent(intent.getComponent());
break;
}
}
} catch (Exception e) {
Log.d("LJY_LOG", "e = " + e.getMessage());
}
break;
default:
break;
}
return false;// 这里必须返回false
});
} catch (Exception e) {
e.printStackTrace();
}
}
- 再次试着调用一下跳转
binding.btnHookIActivityManager.setOnClickListener {
hookAMS()
hookHandler()
val intent = Intent()
val packageName = "com.jinyang.plugindemo"
val activityName = "com.jinyang.plugin002.Plugin002Activity"
intent.component = ComponentName(packageName, activityName)
startActivity(intent)
}
- 会发现报错了,找不到类,我们还需要把插件类加载到classLoader,还记得前面类加载中的单DexClassLoader时我们的loadDex方法么
binding.btnHookIActivityManager.setOnClickListener {
loadDex(this, listOf(plugin001Path,plugin002Path))
hookAMS()
hookHandler()
val intent = Intent()
val packageName = "com.jinyang.plugindemo"
val activityName = "com.jinyang.plugin002.Plugin002Activity"
intent.component = ComponentName(packageName, activityName)
startActivity(intent)
}
- 会发现又报错了Resources$NotFoundException,这里还需要在插件的baseActivity中替换一下Resources, 可以参考我们上面将资源替换时的loadResources方法
class Plugin002Activity : PluginBaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_plugin002)
}
}
open class PluginBaseActivity2 : Activity() {
private var mResources: Resources? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val plugin002Path: String = File(filesDir.absolutePath, "plugin002.apk").absolutePath
mResources = loadResources(this, AssetManager::class.java.newInstance(), listOf( plugin002Path))
}
override fun getResources(): Resources? {
return mResources ?: super.getResources()
}
override fun getAssets(): AssetManager {
return mResources?.assets ?: super.getAssets()
}
}
- 再试着跳转一下就会成功了
Hook Instrumentation方案实现
- startActivityForResult方法中调用了Instrumentation的execStartActivity方法来激活Activity的生命周期;
- ActivityThread启动Activity的过程中会调用ActivityThread的performLaunchActivity方法,其中调用了mInstrumentation的newActivity方法,其内部会用类加载器来创建Activity的实例;
- 那么我们就可以用自定义的Instrumentation来替换掉mInstrumentation,在Instrumentation的execStartActivity方法中用宿主的占坑Activity来通过AMS的验证,在Instrumentation的newActivity方法中还原为插件Activity;
- Hook Instrumentation实现要比Hook IActivityManager实现要简单一些,实现步骤如下
- 自定义Instrumentation
class InstrumentationProxy(
var realContext: Context,
var base: Instrumentation,
var context: ContextWrapper
) : Instrumentation() {
private val KEY_COMPONENT = "commontec_component"
companion object {
/**
* hook 系统,替换 Instrumentation 为我们自己的 InstrumentationProxy
*/
fun inject(activity: Activity, context: ContextWrapper) {
// Reflect 是从 VirtualApp 里拷贝的反射工具类,使用很流畅~
val reflect = Reflect.on(activity)
val activityThread = reflect.get<Any>("mMainThread")
val base = Reflect.on(activityThread).get<Instrumentation>("mInstrumentation")
val mInstrumentation = InstrumentationProxy(activity, base, context)
Reflect.on(activityThread).set("mInstrumentation", mInstrumentation)
Reflect.on(activity).set("mInstrumentation", mInstrumentation)
}
}
/**
* newActivity 是创建 Activity 实例,这里要返回真正需要运行的插件 Activity,
* 这样后面系统就会基于这个 Activity 实例来进行对应的生命周期的调用。
*/
override fun newActivity(cl: ClassLoader, className: String, intent: Intent): Activity? {
val componentName = intent.getParcelableExtra<ComponentName>(KEY_COMPONENT)
var clazz = context.classLoader.loadClass(componentName?.className)
intent.component = componentName
return clazz.newInstance() as Activity?
}
/**
* hook 系统的资源处理方式: 生成 Resources 以后,直接反射替换掉 Activity 中的 mResource 变量即可
*/
private fun injectActivity(activity: Activity?) {
val intent = activity?.intent
val base = activity?.baseContext
try {
//反射替换 mResources 资源
Reflect.on(base).set("mResources", context.resources)
Reflect.on(activity).set("mResources", context.resources)
Reflect.on(activity).set("mBase", context)
Reflect.on(activity).set("mApplication", context.applicationContext)
// for native activity
val componentName: ComponentName? =
intent!!.getParcelableExtra<ComponentName>(KEY_COMPONENT)
val wrapperIntent = Intent(intent)
wrapperIntent.setClassName(componentName?.packageName!!, componentName.className)
activity.intent = wrapperIntent
} catch (e: Exception) {
}
}
override fun callActivityOnCreate(activity: Activity?, icicle: Bundle?) {
injectActivity(activity)
super.callActivityOnCreate(activity, icicle)
}
override fun callActivityOnCreate(
activity: Activity?,
icicle: Bundle?,
persistentState: PersistableBundle?
) {
injectActivity(activity)
super.callActivityOnCreate(activity, icicle, persistentState)
}
/**
* 替换 intent 中的类名为占位 Activity 的类名,这样系统在 Manifest 中查找的时候就可以找到 Activity
*/
private fun injectIntent(intent: Intent?) {
var component: ComponentName? = null
var oldComponent = intent?.component
if (component == null || component.packageName == realContext.packageName) {
component = ComponentName(
"com.jinyang.plugindemo",
"com.jinyang.plugindemo.hook.StubHookActivity"
)
intent?.component = component
intent?.putExtra(KEY_COMPONENT, oldComponent)
}
}
/**
* execStartActivity 是在启动 Activity 的时候必经的一个过程,这时还没有到达 AMS,
* 所以,在这里把 Activity 替换成宿主中已经注册的 StubActivity,
* 这样 AMS 在检测 Activity 的时候就认为已经注册过了
*/
fun execStartActivity(
who: Context,
contextThread: IBinder,
token: IBinder,
target: Activity,
intent: Intent,
requestCode: Int
): Instrumentation.ActivityResult? {
log("exec...")
injectIntent(intent)
return Reflect.on(base)
.call("execStartActivity", who, contextThread, token, target, intent, requestCode).get()
}
fun execStartActivity(
who: Context?,
contextThread: IBinder?,
token: IBinder?,
target: Activity?,
intent: Intent,
requestCode: Int,
options: Bundle?
): Instrumentation.ActivityResult? {
log("exec...")
injectIntent(intent)
return Reflect.on(base)
.call(
"execStartActivity",
who,
contextThread,
token,
target,
intent,
requestCode,
options ?: Bundle()
)
.get()
}
fun execStartActivity(
who: Context,
contextThread: IBinder,
token: IBinder,
target: Fragment,
intent: Intent,
requestCode: Int,
options: Bundle?
): Instrumentation.ActivityResult? {
log("exec...")
injectIntent(intent)
return Reflect.on(base)
.call(
"execStartActivity",
who,
contextThread,
token,
target,
intent,
requestCode,
options ?: Bundle()
)
.get()
}
fun execStartActivity(
who: Context,
contextThread: IBinder,
token: IBinder,
target: String,
intent: Intent,
requestCode: Int,
options: Bundle?
): Instrumentation.ActivityResult? {
log("exec...")
injectIntent(intent)
return Reflect.on(base)
.call(
"execStartActivity",
who,
contextThread,
token,
target,
intent,
requestCode,
options ?: Bundle()
)
.get()
}
}
- 调用InstrumentationProxy.inject,hook系统,替换 Instrumentation 为我们自己的 AppInstrumentation,再进行跳转
binding.btnHookInstrumentation.setOnClickListener {
InstrumentationProxy.inject(
this,
loadContext(this, resources.assets, listOf(plugin001Path, plugin002Path))
)
val intent = Intent()
intent.setClass(this, pluginClassLoader.loadClass(activityName))
startActivity(intent)
}
fun loadContext(baseContext: Context,
assetManager: AssetManager,
pluginPaths: List<String>): ContextWrapper {
loadDex(baseContext, pluginPaths)
val resources = loadResources(baseContext,assetManager, pluginPaths)
// 创建自己的Context
val mContext = ContextThemeWrapper(baseContext, 0)
// 把自己的Context中的resources替换为我们自己的
val clazz = mContext::class.java
val mResourcesField = clazz.getDeclaredField("mResources")
mResourcesField.isAccessible = true
mResourcesField.set(mContext, resources)
return mContext
}
加载 插件Service
- 插件中创建一个service
class PluginService : Service() {
override fun onCreate() {
log("plugin onCreate")
super.onCreate()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
log("plugin onStartCommand")
return super.onStartCommand(intent, flags, startId)
}
override fun onDestroy() {
log("plugin onDestroy")
super.onDestroy()
}
override fun onBind(intent: Intent?): IBinder? {
log("plugin onBind")
return null
}
override fun onUnbind(intent: Intent?): Boolean {
log("plugin onUnbind")
return super.onUnbind(intent)
}
}
- 在宿主 app 里添加一个占位 Service,然后在对应的生命周期里调用插件 Service 的生命周期方法
<service android:name=".service.StubService" />
class StubService : Service() {
var serviceName: String? = null
var pluginService: Service? = null
companion object {
var pluginClassLoader: ClassLoader? = null
fun startService(context: Context, classLoader: ClassLoader, serviceName: String) {
log("StubService.startService")
pluginClassLoader = classLoader
val intent = Intent(context, StubService::class.java)
intent.putExtra("serviceName", serviceName)
context.startService(intent)
}
fun stopService(context: Context, classLoader: ClassLoader, serviceName: String) {
log("StubService.stopService")
pluginClassLoader = classLoader
val intent = Intent(context, StubService::class.java)
intent.putExtra("serviceName", serviceName)
context.stopService(intent)
}
}
override fun onCreate() {
super.onCreate()
log("StubService.onCreate")
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
log("StubService.onStartCommand")
val res = super.onStartCommand(intent, flags, startId)
serviceName = intent?.getStringExtra("serviceName")
if (pluginService == null) {
pluginService = pluginClassLoader?.loadClass(serviceName)?.newInstance() as Service
pluginService?.onCreate()
}
return pluginService?.onStartCommand(intent, flags, startId) ?: res
}
override fun onDestroy() {
super.onDestroy()
log("StubService.onDestroy")
if (pluginService!=null) {
pluginService?.onDestroy()
pluginService = null
}
}
override fun onBind(intent: Intent?): IBinder? {
log("StubService.onBind")
return pluginService?.onBind(intent)
}
override fun onUnbind(intent: Intent?): Boolean {
log("StubService.onUnbind")
return pluginService?.onUnbind(intent) ?: super.onUnbind(intent)
}
}
- 通过占坑service启动和结束插件service
val plugin001Path = File(filesDir.absolutePath, "plugin001.apk").absolutePath
val nativeLibDir = File(filesDir, "pluginlib").absolutePath
val dexOutPath = File(filesDir, "dexout").absolutePath
val pluginClassLoader =
DexClassLoader(plugin001Path, dexOutPath, nativeLibDir, this::class.java.classLoader)
val serviceName = "com.jinyang.plugin001.PluginService"
binding.btnStartService.setOnClickListener {
StubService.startService(this, pluginClassLoader, serviceName)
}
binding.btnStopService.setOnClickListener {
StubService.stopService(this, pluginClassLoader, serviceName)
}
加载 插件BroadcastReceiver
- 动态广播:通过 ClassLoader 加载插件 apk 中的广播类然后直接注册
- 创建一个注册插件广播的工具类
class BroadcastUtils {
companion object {
private val broadcastMap = HashMap<String, BroadcastReceiver>()
fun registerBroadcastReceiver(context: Context, classLoader: ClassLoader, action: String, broadcastName: String) {
log("BroadcastUtils.registerBroadcastReceiver")
val receiver = classLoader.loadClass(broadcastName).newInstance() as BroadcastReceiver
val intentFilter = IntentFilter(action)
context.registerReceiver(receiver, intentFilter)
broadcastMap[action] = receiver
}
fun unregisterBroadcastReceiver(context: Context, action: String) {
log("BroadcastUtils.unregisterBroadcastReceiver")
val receiver = broadcastMap.remove(action)
if (receiver!=null) {
context.unregisterReceiver(receiver)
}
}
}
}
- 注册并使用插件中的广播
val testAction = "com.ljy.action.testBroadcastReceiver"
val broadcastName = "com.jinyang.plugin001.PluginBroadcastReceiver"
binding.btnRegisterBroadcastReceiver.setOnClickListener {
BroadcastUtils.registerBroadcastReceiver(
this,
pluginClassLoader,
testAction,
broadcastName
)
}
binding.btnSendBroadcast.setOnClickListener {
sendBroadcast(Intent(testAction))
}
binding.btnUnregisterBroadcastReceiver.setOnClickListener {
BroadcastUtils.unregisterBroadcastReceiver(this, testAction)
}
- 静态广播:VirtualApk处理的方式是将插件apk的AndroidManifest.xml中静态注册的Receiver通过动态registerReceiver注册到宿主Context中, 实现代码如下
/**
* 将插件apk的AndroidManifest.xml中静态注册的Receiver通过动态registerReceiver注册到宿主Context中
*/
fun parserPluginStaticBroadcast(context: Context, pluginPath: String?) {
try {
//实例化 PackageParser对象
val mPackageParserClass = Class.forName("android.content.pm.PackageParser")
val mPackageParser = mPackageParserClass.newInstance()
// 1.执行此方法 public Package parsePackage(File packageFile, int flags),就是为了,拿到Package
val mPackageParserMethod = mPackageParserClass.getMethod(
"parsePackage",
File::class.java,
Int::class.javaPrimitiveType
)
val mPackage = mPackageParserMethod.invoke(
mPackageParser,
File(pluginPath),
PackageManager.GET_ACTIVITIES
)
//获取mPackage中的ArrayList<Activity> receivers属性
val receiversField = mPackage.javaClass.getDeclaredField("receivers")
val receivers = receiversField[mPackage]
val arrayList = receivers as ArrayList<*>
//此Activity不是组件的Activity,是PackageParser里面的内部类
for (mActivity in arrayList) { // mActivity --> <receiver android:name=".StaticReceiver">
//通过反射拿到intents ArrayList<II> intents; 一个<receiver>标签可以对应多个Intent-Filter
val mComponentClass =
Class.forName("android.content.pm.PackageParser\$Component")
val intentsField = mComponentClass.getDeclaredField("intents")
val intents: ArrayList<IntentFilter> = intentsField[mActivity] as ArrayList<*>
//上面是拿到了IntentFilter,下面就是获取组件的名字 activityInfo.name
/**
* 执行此方法,就能拿到 ActivityInfo
* public static final ActivityInfo generateActivityInfo(Activity a, int flags,
* PackageUserState state, int userId)
*/
val mPackageUserState = Class.forName("android.content.pm.PackageUserState")
val mUserHandle = Class.forName("android.os.UserHandle")
val userId = mUserHandle.getMethod("getCallingUserId").invoke(null) as Int
val generateActivityInfoMethod = mPackageParserClass.getDeclaredMethod(
"generateActivityInfo",
mActivity.javaClass,
Int::class.javaPrimitiveType,
mPackageUserState,
Int::class.javaPrimitiveType
)
generateActivityInfoMethod.isAccessible = true
val mActivityInfo = generateActivityInfoMethod.invoke(
null,
mActivity,
0,
mPackageUserState.newInstance(),
userId
) as ActivityInfo
val receiverClassName = mActivityInfo.name
Log.e("LJY_LOG", "receiverClassName : $receiverClassName")
val mStaticReceiverClass = context.classLoader.loadClass(receiverClassName)
val broadcastReceiver = mStaticReceiverClass.newInstance() as BroadcastReceiver
for (intentFilter in intents) {
Log.e("LJY_LOG", "intentFilter mActions size " + intentFilter.countActions())
context.registerReceiver(broadcastReceiver, intentFilter)
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
加载 插件ContentProvider
- 这里面需要处理的就是,如何转发对应的 Uri 到正确的插件 Provider 中呢,解决方案是在 Uri 中定义不同的插件路径,比如 plugin1 的 Uri 对应就是 content://com.zy.stubprovider/plugin1 ,plugin2 对应的 uri 就是 content://com.zy.stubprovider/plugin2 ,然后在 StubContentProvider 中根据对应的 plugin 分发不同的插件 Provider。
- 宿主中创建一个占坑的ContentProvider,并在其中通过插件classLoader加载插件ContentProvider
<provider
android:name=".contentprovider.StubContentProvider"
android:authorities="com.ljy.StubContentProvider" />
class StubContentProvider : ContentProvider() {
private var pluginProvider: ContentProvider? = null
private var uriMatcher: UriMatcher? = UriMatcher(UriMatcher.NO_MATCH)
override fun insert(uri: Uri, values: ContentValues?): Uri? {
log("StubContentProvider.insert")
return loadPluginProvider()?.insert(uri, values)
}
override fun query(uri: Uri, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, sortOrder: String?): Cursor? {
log("StubContentProvider.query: uri=$uri")
if (isPlugin1(uri)) {
return loadPluginProvider()?.query(uri, projection, selection, selectionArgs, sortOrder)
}
return null
}
override fun onCreate(): Boolean {
log("StubContentProvider.onCreate")
uriMatcher?.addURI("com.ljy.StubContentProvider", "plugin001", 1)
uriMatcher?.addURI("com.ljy.StubContentProvider", "plugin002", 2)
return true
}
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?): Int {
log("StubContentProvider.update")
return loadPluginProvider()?.update(uri, values, selection, selectionArgs) ?: 0
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
log("StubContentProvider.delete")
return loadPluginProvider()?.delete(uri, selection, selectionArgs) ?: 0
}
override fun getType(uri: Uri): String {
log("StubContentProvider.getType")
return loadPluginProvider()?.getType(uri) ?: ""
}
private fun loadPluginProvider(): ContentProvider? {
if (pluginProvider == null) {
pluginProvider = classLoader?.loadClass("com.jinyang.plugin001.PluginContentProvider")?.newInstance() as ContentProvider?
}
return pluginProvider
}
private fun isPlugin1(uri: Uri?): Boolean {
log("StubContentProvider.isPlugin1:${uriMatcher?.match(uri)}")
if (uriMatcher?.match(uri) == 1) {
return true
}
return false
}
}
- 使用
binding.btnQueryContentProvider1.setOnClickListener {
val uri = Uri.parse("content://com.ljy.StubContentProvider/plugin001")
val cursor = contentResolver.query(uri, null, null, null, null)
cursor?.moveToFirst()
val res = cursor?.getString(0)
log("provider query res: $res")
cursor?.close()
}
binding.btnQueryContentProvider2.setOnClickListener {
val uri = Uri.parse("content://com.ljy.StubContentProvider/plugin002")
val cursor = contentResolver.query(uri, null, null, null, null)
cursor?.moveToFirst()
val res = cursor?.getString(0)
log("provider query res: $res")
cursor?.close()
}
- 到此插件化的原理基本讲完了,下面我们来看看Shadow吧,毕竟本系列文章是探索Android开源框架
Shadow
Shadow原理:
- 通过运用AOP思想,利用字节码编辑工具,在编译期把插件中的所有Activity的父类都改成一个普通类,然后让壳子持有这个普通类型的父类去转调它就不用Hack任何系统实现了。(任何软件工程遇到的问题都可以通过增加一个中间层来解决)
Shadow为什么要求插件和宿主包名一致
- ApplicationId一般是在build.gradle中设置的,在编译时这个字符串会被记录在2个位置。第1是记录在应用的AndroidManifest.xml中,第2是记录在应用的resources.arsc文件中;
- 记录在AndroidManifest.xml中的包名主要用来构造应用的Context对象,系统也会通过context获取包名来识别context来自于哪个安装的应用
- Shadow设计时避免使用私有API,通过一层中间件将插件代码变成宿主代码的一部分,既然是一部分,ApplicationId怎么会不一样呢?所以要求插件和宿主的ApplicationId保持一致,就永远不会将插件代码没有安装这件事暴露给系统;
- 记录在resources.arsc的ApplicationId,Resources对象上有一些API是接收包名作为参数的,如果这个包名在独立安装和插件环境下动态获取的不一样,那么有可能造成这些API失效,找不到想要的资源。
Shadow使用
- 使用上主要参考官方的sample,路径为Shadow-master\projects\sample\maven,其中有三个project,host-project为宿主项目,manager-project为插件管理项目,plugin-project为插件项目;