0%

Kotlin/Native 用KMM写Flutter插件

一、用KMM写Flutter插件

Google官方有一个写Flutter例子How to write a Flutter plugin,这里把Google plugin_codelab 例子改成用KMM写Flutter插件。

二、如何运行

Android: run shared/plugin_codelab/example/android

iOS:

1、build shared.framework

1
2
use ./gradlew releaseIOSFramework
or use new version Android Studio sync

2、run shared/plugin_codelab/example/ios

Tips: before run,shared/build/cocoapods/framework/shared.framework should be generated. The shared.h header file shared/build/cocoapods/framework/shared.framework/Headers/shared.h is generated.

三、设计思路

Android/iOS插件PluginCodelabPlugin只需要实现KMM Module的接口,不写任何逻辑,把逻辑通过接口放在KMM Module中。

1、定义接口中间层用于转发数据

如参考Flutter插件的MethodCall、MethodChannel,定义CommonMethodCall数据类、CommonMethodChannel.Result接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
data class CommonMethodCall(
val method: String,
val arguments: Any?,
)

class CommonMethodChannel {
interface Result {
fun success(result: Any?)

fun error(errorCode: String?, errorMessage: String?, errorDetails: Any?)

fun notImplemented()
}
}

2、在KMM中的commonMain实现CommonCodelabPlugin插件的公共逻辑

CommonCodelabPlugin需要初始化并启动synth?.start(),处理getPlatformVersion、onKeyDown、onKeyUp逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class CommonCodelabPlugin {

private val synth = Synth()

init {
synth?.start()
}

fun onMethodCall(call: CommonMethodCall, result: CommonMethodChannel.Result) {
when (call.method) {
"getPlatformVersion" -> {
result.success(Platform().platform)
}
"onKeyDown" -> {
try {
val arguments = call.arguments as List<*>
val numKeysDown = synth?.keyDown((arguments[0] as Int))
result.success(numKeysDown)
} catch (ex: Exception) {
result.error("1", ex.message, ex.cause)
}
}
"onKeyUp" -> {
try {
val arguments = call.arguments as List<*>
val numKeysDown = synth?.keyUp((arguments[0] as Int))
result.success(numKeysDown)
} catch (ex: Exception) {
result.error("1", ex.message, ex.cause)
}
}
else -> {
result.notImplemented()
}
}
}
}

还有包括插件名称也属于公共逻辑

1
2
// 插件Channel名称
const val PLUGIN_CODE_LAB_CHANNEL = "plugin_codelab"

3、实现平台差异特性

这里只列出expect接口,具体实现平台差异特性类请查看源码

1
2
3
4
5
6
7
8
9
10
11
expect class Synth() {
fun start()

fun keyDown(key: Int): Int

fun keyUp(key: Int): Int
}

expect class Platform() {
val platform: String
}

4、Android Flutter实现插件KMM接口

Android Flutter实现插件KMM接口,注意这里只实现接口用于中转Flutter与Android/iOS 数据,不能有任何业务逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class PluginCodelabPlugin : FlutterPlugin, MethodCallHandler {
private var channel: MethodChannel? = null
private var commonCodelabPlugin: CommonCodelabPlugin? = null

override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
setup(this, flutterPluginBinding.binaryMessenger)
}

override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
commonCodelabPlugin?.onMethodCall(
call = CommonMethodCall(call.method, call.arguments),
result = object : CommonMethodChannel.Result {
override fun success(successResult: Any?) {
result.success(successResult)
}

override fun error(errorCode: String?, errorMessage: String?, errorDetails: Any?) {
result.error(errorCode, errorMessage, errorDetails)
}

override fun notImplemented() {
result.notImplemented()
}
})
}

override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel?.setMethodCallHandler(null)
}

companion object {
private fun setup(plugin: PluginCodelabPlugin, binaryMessenger: BinaryMessenger) {
plugin.channel = MethodChannel(binaryMessenger, PLUGIN_CODE_LAB_CHANNEL)
plugin.channel?.setMethodCallHandler(plugin)
plugin.commonCodelabPlugin = CommonCodelabPlugin()
}
}
}

5、iOS Flutter实现插件KMM接口

Android Flutter实现插件KMM接口,注意这里只实现接口用于中转Flutter与Android/iOS 数据,不能有任何业务逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#import "PluginCodelabPlugin.h"

@implementation PluginCodelabPlugin{
int _numKeysDown;
FlutterResult _flutterResult;
SharedCommonCodelabPlugin* _codelabPlugin;
}

- (instancetype)init {
self = [super init];
if (self) {
// create music
_codelabPlugin = [[SharedCommonCodelabPlugin alloc] init];
}
return self;
}

- (void)dealloc {
// destroy music
}

+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
FlutterMethodChannel* channel = [FlutterMethodChannel
methodChannelWithName: SharedPluginCodeLabKt.PLUGIN_CODE_LAB_CHANNEL
binaryMessenger:[registrar messenger]];
PluginCodelabPlugin* instance = [[PluginCodelabPlugin alloc] init];
[registrar addMethodCallDelegate:instance channel:channel];
}

- (void)handleMethodCall:(FlutterMethodCall *)call
result:(FlutterResult)result {
SharedCommonMethodCall *methodCall = [[SharedCommonMethodCall alloc] initWithMethod:call.method arguments:call.arguments];
_flutterResult = result;
[_codelabPlugin onMethodCallCall:methodCall result:self ];
}

- (void)errorErrorCode:(NSString * _Nullable)errorCode errorMessage:(NSString * _Nullable)errorMessage errorDetails:(id _Nullable)errorDetails {
NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain code:errorCode.intValue userInfo:@{@"errorMessage":errorMessage, @"errorDetails":errorDetails}];
if (_flutterResult) {
_flutterResult(error);
}
}

- (void)notImplemented {
if (_flutterResult) {
_flutterResult(FlutterMethodNotImplemented);
}
}

- (void)successResult:(id _Nullable)result {
if (_flutterResult) {
_flutterResult(result);
}
}

@end

到这里,已经完成了使用KMM开发一个Flutter插件。使用KMM开发插件的好处是公共逻辑都使用kotlin写,一般公共逻辑比较简单适合使用kotlin写,便于维护。而且,实现了KMM写插件,Flutter写UI。

四、参考链接

Github项目地址:kmm-flutter-plugin

Kotlin/Native KMM项目架构

一、什么是KMM?

Kotlin Multiplatform Mobile ( KMM ) 是一个 SDK,旨在简化跨平台移动应用程序的创建。在 KMM 的帮助下,您可以在 iOS 和 Android 应用程序之间共享通用代码,并仅在必要时编写特定于平台的代码。

KMM用纯Kotlin编写一次代码,即可在iOS和Android上运行,开发应用的公共业务逻辑只需要编写一次。KMM减少了为不同平台编写和维护相同代码所花费的时间。在Jenkins上一次构建可以产出aar、framework、klib,Android依赖aar,iOS依赖framework,性能与原生一致。当然可以使用KMM依赖klib开发Android、iOS应用。

二、KMM项目架构

项目架构主要分为原生系统层、Android/iOS业务SDK层、KMM SDK层、KMM业务逻辑SDK层、iOS sdkframework层、Android/iOS App层。

原生系统层:这里提下原生系统层的目的是,有些平台特性需要分开实现,比如读取文件、打印日志、摄像头等。

Android/iOS业务SDK层:主要是包括一些现有的Android/iOS SDK,需要直接依赖现有SDK来开发KMM时,在commonMain expect声明接口,在androidMain、iosMain actual分别依赖现有SDK实现。这样就可以使用已有的SDK,后续也可以保持接口不变,直接使用KMM实现SDK,如alog、PlatformMMKV。

KMM SDK层:如alog、PlatformMMKV写成一个SDK可以供其他KMM模块(business)使用。

KMM业务逻辑SDK层:具体业务的逻辑模块,比如登录逻辑、获取首页列表逻辑、查看首页列表数据详情等。

iOS sdkframework层:Kotlin/Native构建一个framework时,产物是二进制,也包含了Kotlin/Native的基础库、Runtime,会使包大小增加1M+左右,而且多个Kotlin/Native构建的framework不会共享基础库导致每一个framework都会增加1M+,为了避免包过大,统一构建一个framework。

App层:Android的依赖无变化,依赖aar或者jar;iOS依赖sdkframework,这样iOS包大小只增加1M+。当然如果依赖了一些库如ktor网络库,包也会变大,避免这个问题也可以不用依赖ktor,直接依赖现有的网络库来实现一个KMM SDK。

三、使用expect/actual编写平台特定的代码

以打印日志为例,打造一个alog日志SDK

在commonMain定义IALog接口,声明fun v函数,其他函数忽略。并定义expect ALogImpl类来实现平台特性打印日志

1
2
3
4
5
6
interface IALog {
fun v(tag: String, message: String)
...
}

expect class ALogImpl(): IALog

在androidMain实现ALogImpl

1
2
3
4
5
6
7
import android.util.Log
actual class ALogImpl actual constructor() : IALog {
override fun v(tag: String, message: String) {
Log.v(tag, message)
}
...
}

在iosMain实现ALogImpl

1
2
3
4
5
6
7
import platform.Foundation.NSLog
internal actual class ALogImpl actual constructor(): IALog {
override fun v(tag: String, message: String) {
NSLog("[$tag] $message")
}
...
}

到此,我们已经使用KMM实现了一个alog日志SDK。

四、依赖现有的Android/iOS SDK开发KMM SDK

alog的实现过于简单,使用了android.util.Log、platform.Foundation.NSLog。如果使用现有的Android/iOS SDK,如何实现呢?比如Android使用mars-xlog、iOS使用CocoaLumberjack

Android的实现没什么变化,依赖mars-xlog即可

1
2
3
4
5
6
7
8
9
implementation("com.tencent.mars:mars-xlog:1.2.6")

import com.tencent.mars.xlog.Log
actual class ALogImpl actual constructor() : IALog {
override fun v(tag: String, message: String) {
Log.v(tag, message)
}
...
}

在ios实现依赖CocoaLumberjack,需要用到native.cocoapods插件

1
2
3
4
5
6
7
8
9
10
11
plugins {
kotlin("multiplatform")
kotlin("native.cocoapods")
id("com.android.library")
}

cocoapods {
...
frameworkName = "alog"
pod("CocoaLumberjack")
}

通过cinterop一些gradle Task会自动生成头文件给iosMain使用,比如生成alog-cinterop-CocoaLumberjack.klib包含1_CocoaLumberjack.knm。

1
2
3
4
5
6
7
8
9
10
11
12
import cocoapods.CocoaLumberjack.*
internal actual class ALogImpl actual constructor(): IALog {
private val dLog = DDLog
override fun v(tag: String, message: String) {
dLog.log(asynchronousLog, toMessage(tag, "[$tag] $message", DDLogLevelVerbose, DDLogFlagVerbose))
}

private fun toMessage(tag: String, message: String, level: DDLogLevel, flag: DDLogFlag): DDLogMessage {
return DDLogMessage(message, level, flag, 0, "", null, 0, tag, 0, null)
}
...
}

为了方便Android/iOS App使用,添加一个ALog.kt类

1
2
3
4
5
6
7
8
9
/**
* Android App使用 ALog.i(tag, message)
*/
val ALog: IALog by lazy { ALogImpl() }

/**
* iOS App使用ALogKt.i(tag, message)
*/
fun d(tag: String, message: String) = ALog.d(tag, message)

到此,alog就完成了依赖现有的Android/iOS SDK(mars-xlog、CocoaLumberjack)开发alog KMM SDK。

五、声明Android/iOS公共接口以及独有接口

用expect修饰commonMain中声明公共的接口

1
2
3
4
expect interface IALog {
fun v(tag: String, message: String)
...
}

在iosMain中用actual修饰来实现真正的接口

1
2
3
4
actual interface IALog {
actual fun v(tag: String, message: String)
...
}

在androidMain中用actual修饰来实现真正的接口,带actual修饰的方法为Android/iOS公共方法,不带actual修饰的方法为Android独有(Android有这个接口iOS没有这个接口)

1
2
3
4
5
6
actual interface IALog {
actual fun v(tag: String, message: String)
...

fun v(tag: String, format: String, vararg args: Any?)
}

这样Android就可以使用fun v(tag: String, format: String, vararg args: Any?)函数,而iOS没有这个函数。好处是通常一些SDK在commonMain中会定义一套公共接口,有时候Android或iOS有一些独有接口,就可以用这种方式声明。同理data class也是可以这样使用。

六、为iOS统一构建成一个framework

为了避免Kotlin/Native构建framework时包过大,统一构建一个framework,下面把包名称为sdkframework。这里提一下几个值得注意的问题。有2种方式构建:1、本地构建,写一个sdkframework项目依赖其他模块的klib包,来构建sdkframework。2、构建系统上构建依赖其他模块的klib包构建,业务直接pod sdkframework即可。第1种方案比较灵活,版本号可以写脚本控制,但是要求开发人员使用的电脑都要配置KMM开发环境。第2种方案业务接入更加简单,跟iOS原生开发的SDK一样,无需KMM环境,主要问题是各个业务依赖klib的版本不一致,导致构建sdkframework多个版本,这时需要用不同分支构建不同业务的sdkframework,版本号加后缀来区别 1.0.0-love、1.0.0-like。

6.1 sdkframework模块的iosMain需要有一个kotlin文件

如果iosMain没有kotlin文件,将无法生成 iOS framework,为其添加一个文件即可,如SDKTest.kt

1
2
3
4
5
6
// 加个类,避免Framework没生成
class SDKTest {
fun test() {

}
}

6.2 生成头文件sdkframework.h时,把注释也带上

生成头文件sdkframework.h时,如果需要把注释也带上,那需要在gradle中添加Task

1
2
3
targets.withType<org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget> {
compilations.get("main").kotlinOptions.freeCompilerArgs += "-Xexport-kdoc"
}

6.3 依赖的模块需要使用export来导出到sdkframework.h头文件中

sdkframework依赖了utils、alog、PlatformMMKV、business,需要添加export,把这几个模块的类和方法导出到sdkframework.h头文件中,这样iosApp才可以使用这几个模块的类和方法。

1
2
3
4
5
6
7
8
9
10
11
12
val iosX64 = iosX64()
val iosArm64 = iosArm64()
targets {
configure(listOf(iosX64, iosArm64)) {
binaries.withType(org.jetbrains.kotlin.gradle.plugin.mpp.Framework::class.java) {
export(project(":utils"))
export(project(":alog"))
export(project(":PlatformMMKV"))
export(project(":business"))
}
}
}

6.4 sdkframework本地依赖的模块使用了pod,sdkframework也要pod,以klib依赖可避免该问题

sdkframework依赖utils、alog、PlatformMMKV、business模块源码构建framework时,模块使用了pod的,那sdkframework也要pod。如PlatformMMKV pod(“MMKV”, “1.2.8”),那sdkframework也要pod(“MMKV”, “1.2.8”)。那如何避免这个问题,可以先把utils、alog、PlatformMMKV、business模块在构建系统上构建成klib,sdkframework依赖各个模块的klib即可。

6.5 use_frameworks! 和 use_modular_headers!

上面说到的第1点本地构建,在iosApp本地依赖构建sdkframework时,要将依赖项正确导入 Kotlin/Native 模块,Podfile必须包含use_modular_headers! 或 use_frameworks! 指令,查看文档链接。当然,如果是第2点构建系统上构建则不需要使用这2个指令。

七、参考链接:

1、源码地址:https://github.com/libill/kmmApp

2、kmm-getting-started

3、Multiplatform programming

4、KMM 求生日记二:Kotlin/Native 被踩中的坑

什么是插件化?

插件化包括宿主和插件2部分。把需要实现的模块或功能独立的提取出来,比如相册,每个模块相当于一个独立的apk,这个apk就是一个插件。有一个加载相册插件apk的App叫做宿主,宿主是一个普通的App,但包含了加载插件apk的功能,使得插件apk正常运行。插件化可以减少宿主App的规模,可以把插件apk放到服务器上,当需要使用到相应的功能时再去加载相应的插件apk。宿主和插件都有各自的包名和版本号,包名可以区别宿主和各个插件,版本号可用于宿主和插件的升级。

简单介绍下VirtualAPK

VirtualAPK对插件没有额外的约束,原生的apk即可作为插件。插件工程编译生成apk后,即可通过宿主App加载,每个插件apk被加载后,都会在宿主中创建一个单独的LoadedPlugin对象。如下图所示,通过这些LoadedPlugin对象,VirtualAPK就可以管理插件并赋予插件新的意义,使其可以像手机中安装过的App一样运行。

一、从代码入口分析

代码分析基于0.9.8版本

compile 'com.didi.virtualapk:core:0.9.8'

VirtualApk初始化插件引擎需要在Application的attachBaseContext进行

public class VAApplication extends Application {

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        PluginManager.getInstance(base).init();
    }

}

getInstalce方法用单例设计模式创建PluginManager对象,使用了synchronized关键字同步锁

public static PluginManager getInstance(Context base) {
    if (sInstance == null) {
        synchronized (PluginManager.class) {
            if (sInstance == null) {
                sInstance = createInstance(base);
            }
        }
    }

    return sInstance;
}

创建PluginManager对象后,接着会调用hookCurrentProcess方法

protected void hookCurrentProcess() {
    hookInstrumentationAndHandler();
    hookSystemServices();
    hookDataBindingUtil();
}

到这里可以知道,hook了Instrumentation、Handler、SystemServices、DataBindingUtil。下面逐个分析下这四个hook流程,这里分析流程的目的是要了解hook是怎么回事,hook了是要干嘛呢。

二、hookInstrumentationAndHandler

先看看hookInstrumentationAndHandler的代码

protected void hookInstrumentationAndHandler() {
    try {
        ActivityThread activityThread = ActivityThread.currentActivityThread();
        Instrumentation baseInstrumentation = activityThread.getInstrumentation();

        final VAInstrumentation instrumentation = createInstrumentation(baseInstrumentation);
        
        Reflector.with(activityThread).field("mInstrumentation").set(instrumentation);
        Handler mainHandler = Reflector.with(activityThread).method("getHandler").call();
        Reflector.with(mainHandler).field("mCallback").set(instrumentation);
        this.mInstrumentation = instrumentation;
        Log.d(TAG, "hookInstrumentationAndHandler succeed : " + mInstrumentation);
    } catch (Exception e) {
        Log.w(TAG, e);
    }
}

第一行ActivityThread activityThread = ActivityThread.currentActivityThread();这里会不会让你想起什么来,ActivityThread是一个@hide类,为什么可以直接使用@hide类呢?可以按点击去试试看,会跳到AndroidStub模块下的ActivityThread。AndroidStub定义了许多路径一样的类但是里面都是实现抛出RuntimeException.为了尽量避免使用反射浪费性能,使用了AndroidStub模块来欺骗编译器。欺骗编译器需要查看Android framework层源码,定义和原码中一摸一样的方法,实现抛出RuntimeException。CoreLibrary使用provided依赖AndroidStub,provided依赖是不打包依赖包,而是运行时提供,所以成功欺骗了编辑器,用来提高了性能。

public final class ActivityThread {

    public static ActivityThread currentActivityThread() {
        throw new RuntimeException("Stub!");
    }
    ...
}

CoreLibrary使用provided依赖AndroidStub

final String projectAndroidStub = ':AndroidStub'
dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    provided project(projectAndroidStub)
}

现在回到hook instrumentation上,使用了Reflector反射器直接把framework层的ActivityThread类下mInstrumentation变量变成了VAInstrumentation,使得VAInstrumentation起到了代理作用。

final VAInstrumentation instrumentation = createInstrumentation(baseInstrumentation);
Reflector.with(activityThread).field("mInstrumentation").set(instrumentation);

代理的目的就是先让VAInstrumentation处理自己的逻辑,处理完后再给framework层的Instrumentation处理,以实现达到欺骗系统的作用,校验的是宿主占坑Activity,启动插件中的Activity。

public class VAInstrumentation extends Instrumentation implements Handler.Callback {
...
protected Instrumentation mBase;

@Override
public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, String target, Intent intent, int requestCode, Bundle options) {
    injectIntent(intent);
    return mBase.execStartActivity(who, contextThread, token, target, intent, requestCode, options);
}

protected void injectIntent(Intent intent) {
    mPluginManager.getComponentsHandler().transformIntentToExplicitAsNeeded(intent);
    // null component is an implicitly intent
    if (intent.getComponent() != null) {
        Log.i(TAG, String.format("execStartActivity[%s : %s]", intent.getComponent().getPackageName(), intent.getComponent().getClassName()));
        // resolve intent with Stub Activity if needed
        this.mPluginManager.getComponentsHandler().markIntentIfNeeded(intent);
    }
}
...
}

现在来看看hook Handler,

Handler mainHandler = Reflector.with(activityThread).method("getHandler").call();
Reflector.with(mainHandler).field("mCallback").set(instrumentation);

看看ActivityThread源码下getHandler()是什么?原来是H类。

public final class ActivityThread extends ClientTransactionHandler {
    ...
    final H mH = new H();
    final Handler getHandler() {
        return mH;
    }
    ...
}

那为什么可以直接把H类的mCallback直接替换成功VAInstrumentation实现的Handler.Callback不会引起其他问题,导致无法执行H类的handleMessage呢?看看Handler源码就知道了。new H()的时候mCallback为null,使用代理VAInstrumentation后mCallback.handleMessage(msg)会一直返回false,会继续执行handleMessage方法。起到了代理H类的效果,先执行VAInstrumentation的handleMessage,再执行H类的handleMessage。

public class Handler {
    ...
    public Handler() {
        this(null, false);
    }

    public Handler(Callback callback, boolean async) {
        ...
        mCallback = callback;
        mAsynchronous = async;
    }

    public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }
    ...
}

三、hookSystemServices

hook SystemServices的时候,先从ActivityManager.class或ActivityManagerNative.class中反射获取Singleton对象,再使用ActivityManagerProxy动态代理动态代理获取一个 IActivityManager.

protected void hookSystemServices() {
    try {
        Singleton<IActivityManager> defaultSingleton;

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            defaultSingleton = Reflector.on(ActivityManager.class).field("IActivityManagerSingleton").get();
        } else {
            defaultSingleton = Reflector.on(ActivityManagerNative.class).field("gDefault").get();
        }
        IActivityManager origin = defaultSingleton.get();
        IActivityManager activityManagerProxy = (IActivityManager) Proxy.newProxyInstance(mContext.getClassLoader(), new Class[] { IActivityManager.class },
            createActivityManagerProxy(origin));

        // Hook IActivityManager from ActivityManagerNative
        Reflector.with(defaultSingleton).field("mInstance").set(activityManagerProxy);

        if (defaultSingleton.get() == activityManagerProxy) {
            this.mActivityManager = activityManagerProxy;
            Log.d(TAG, "hookSystemServices succeed : " + mActivityManager);
        }
    } catch (Exception e) {
        Log.w(TAG, e);
    }
}

ActivityManager.class下的IActivityManagerSingleton

public class ActivityManager {
    ...
    private static final Singleton<IActivityManager> IActivityManagerSingleton =
            new Singleton<IActivityManager>() {
                @Override
                protected IActivityManager create() {
                    final IBinder b = ServiceManager.getService(Context.ACTIVITY_SERVICE);
                    final IActivityManager am = IActivityManager.Stub.asInterface(b);
                    return am;
                }
            };
    }
    ...
}

Reflector.with(defaultSingleton).field(“mInstance”).set(activityManagerProxy);这里的mInstance其实是Singleton类的mInstance变量。

public abstract class Singleton<T> {
    private T mInstance;

    protected abstract T create();

    public final T get() {
        synchronized (this) {
            if (mInstance == null) {
                mInstance = create();
            }
            return mInstance;
        }
    }
}

三、hook DataBindingUtil

分析完上面2个原理,到这里应该比较清晰hook是什么回事了,就是在执行framework层代码之前,先执行Proxy代理的代码,来实现一些意想不到的效果。hook DataBindingUtil可以自己看看代码分析下。

protected void hookDataBindingUtil() {
    Reflector.QuietReflector reflector = Reflector.QuietReflector.on("android.databinding.DataBindingUtil").field("sMapper");
    Object old = reflector.get();
    if (old != null) {
        try {
            Callback callback = Reflector.on("android.databinding.DataBinderMapperProxy").constructor().newInstance();
            reflector.set(callback);
            addCallback(callback);
            Log.d(TAG, "hookDataBindingUtil succeed : " + callback);
        } catch (Reflector.ReflectedException e) {
            Log.w(TAG, e);
        }
    }
}

四、支持插件中的Activity

这里需要了解Activity的启动流程,如果你还没有了解可以点击这里。前面我们了解了hook Instrumentation,看看VAInstrumentation到底做了什么。启动Activity时会执行execStartActivity,而在执行execStartActivity之前做了injectIntent,是为了绕过系统校验是否在宿主的AndroidManifest.xml中注册过插件中的Activity。也就是要达到插件中的Activity不用在宿主中注册就可以启动。

@Override
public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, String target, Intent intent, int requestCode, Bundle options) {
    injectIntent(intent);
    return mBase.execStartActivity(who, contextThread, token, target, intent, requestCode, options);
}

protected void injectIntent(Intent intent) {
    mPluginManager.getComponentsHandler().transformIntentToExplicitAsNeeded(intent);
    // null component is an implicitly intent
    if (intent.getComponent() != null) {
        Log.i(TAG, String.format("execStartActivity[%s : %s]", intent.getComponent().getPackageName(), intent.getComponent().getClassName()));
        // resolve intent with Stub Activity if needed
        this.mPluginManager.getComponentsHandler().markIntentIfNeeded(intent);
    }
}

transformIntentToExplicitAsNeeded作用是把隐式启动的Activity转化为显式启动。下图可以知道把Intent { act=com.didi.virtualapk.plugin.BookManagerActivity }转化为显式Intent { act=com.didi.virtualapk.plugin.BookManagerActivity cmp=com.didi.virtualapk.demo/.aidl.BookManagerActivity },ComponentName由null变成包含BookManagerActivity数据的ComponentName。

markIntentIfNeeded其实就是记录了下插件的信息包括isPlugin、插件package、要启动的插件Activity类,记录的目的是绕过系统校验后,再把这些信息取出来,启动真正要启动的插件Activity。

public void markIntentIfNeeded(Intent intent) {
    if (intent.getComponent() == null) {
        return;
    }

    String targetPackageName = intent.getComponent().getPackageName();
    String targetClassName = intent.getComponent().getClassName();
    // search map and return specific launchmode stub activity
    if (!targetPackageName.equals(mContext.getPackageName()) && mPluginManager.getLoadedPlugin(targetPackageName) != null) {
        intent.putExtra(Constants.KEY_IS_PLUGIN, true);
        intent.putExtra(Constants.KEY_TARGET_PACKAGE, targetPackageName);
        intent.putExtra(Constants.KEY_TARGET_ACTIVITY, targetClassName);
        dispatchStubActivity(intent);
    }
}

这里要看看dispatchStubActivity把targetActivity替换成stubActivity的过程。

图中可以看到stubActivity是com.didi.virtualapk.core.A$1,这是CoreLibrary/src/main/AndroidManifest.xml下提前注册占坑Activity。包含了四种启动模式,不同的启动模式取不同的占坑Activity,达到支持插件Activity的四种启动模式。

<!-- Stub Activities -->
<activity android:exported="false" android:name=".A$1" android:launchMode="standard"/>
<activity android:exported="false" android:name=".A$2" android:launchMode="standard"
    android:theme="@android:style/Theme.Translucent" />

<!-- Stub Activities -->
<activity android:exported="false" android:name=".B$1" android:launchMode="singleTop"/>
<activity android:exported="false" android:name=".B$2" android:launchMode="singleTop"/>
<activity android:exported="false" android:name=".B$3" android:launchMode="singleTop"/>
<activity android:exported="false" android:name=".B$4" android:launchMode="singleTop"/>
<activity android:exported="false" android:name=".B$5" android:launchMode="singleTop"/>
<activity android:exported="false" android:name=".B$6" android:launchMode="singleTop"/>
<activity android:exported="false" android:name=".B$7" android:launchMode="singleTop"/>
<activity android:exported="false" android:name=".B$8" android:launchMode="singleTop"/>

<!-- Stub Activities -->
<activity android:exported="false" android:name=".C$1" android:launchMode="singleTask"/>
<activity android:exported="false" android:name=".C$2" android:launchMode="singleTask"/>
<activity android:exported="false" android:name=".C$3" android:launchMode="singleTask"/>
<activity android:exported="false" android:name=".C$4" android:launchMode="singleTask"/>
<activity android:exported="false" android:name=".C$5" android:launchMode="singleTask"/>
<activity android:exported="false" android:name=".C$6" android:launchMode="singleTask"/>
<activity android:exported="false" android:name=".C$7" android:launchMode="singleTask"/>
<activity android:exported="false" android:name=".C$8" android:launchMode="singleTask"/>

<!-- Stub Activities -->
<activity android:exported="false" android:name=".D$1" android:launchMode="singleInstance"/>
<activity android:exported="false" android:name=".D$2" android:launchMode="singleInstance"/>
<activity android:exported="false" android:name=".D$3" android:launchMode="singleInstance"/>
<activity android:exported="false" android:name=".D$4" android:launchMode="singleInstance"/>
<activity android:exported="false" android:name=".D$5" android:launchMode="singleInstance"/>
<activity android:exported="false" android:name=".D$6" android:launchMode="singleInstance"/>
<activity android:exported="false" android:name=".D$7" android:launchMode="singleInstance"/>
<activity android:exported="false" android:name=".D$8" android:launchMode="singleInstance"/>

如果想了解如何取对应设计模式的占坑,可以查看StubActivityInfo类,如取值stubActivity为com.didi.virtualapk.core.A$1

public static final String STUB_ACTIVITY_STANDARD = "%s.A$%d";
stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, usedStandardStubActivity);

injectIntent绕过校验后,会执行newActivity,在classloader加载占坑类com.didi.virtualapk.core.A$1时,由于只是占坑,不存在这个类,会走ClassNotFoundException异常逻辑。

@Override
public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
    try {
        cl.loadClass(className);
        Log.i(TAG, String.format("newActivity[%s]", className));
        
    } catch (ClassNotFoundException e) {
        ComponentName component = PluginUtil.getComponent(intent);
        
        if (component == null) {
            return newActivity(mBase.newActivity(cl, className, intent));
        }

        String targetClassName = component.getClassName();
        Log.i(TAG, String.format("newActivity[%s : %s/%s]", className, component.getPackageName(), targetClassName));

        LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(component);

        if (plugin == null) {
            // Not found then goto stub activity.
            boolean debuggable = false;
            try {
                Context context = this.mPluginManager.getHostContext();
                debuggable = (context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
            } catch (Throwable ex) {
    
            }

            if (debuggable) {
                throw new ActivityNotFoundException("error intent: " + intent.toURI());
            }
            
            Log.i(TAG, "Not found. starting the stub activity: " + StubActivity.class);
            return newActivity(mBase.newActivity(cl, StubActivity.class.getName(), intent));
        }
        
        Activity activity = mBase.newActivity(plugin.getClassLoader(), targetClassName, intent);
        activity.setIntent(intent);

        // for 4.1+
        Reflector.QuietReflector.with(activity).field("mResources").set(plugin.getResources());

        return newActivity(activity);
    }

    return newActivity(mBase.newActivity(cl, className, intent));
}

通过this.mPluginManager.getLoadedPlugin(component)获取已经加载的插件,并重新设置了恢复了要启动的插件Activity。就这样callActivityOnCreate的时候也是调用要启动的插件Activity。

@Override
public void callActivityOnCreate(Activity activity, Bundle icicle) {
    injectActivity(activity);
    mBase.callActivityOnCreate(activity, icicle);
}

五、支持插件中的Service

前面已经了解了hookSystemServices的过程,是使用了动态代理生成代理类。

IActivityManager activityManagerProxy = (IActivityManager) Proxy.newProxyInstance(mContext.getClassLoader(), new Class[] { IActivityManager.class },
    createActivityManagerProxy(origin));

再看看createActivityManagerProxy做了什么

protected ActivityManagerProxy createActivityManagerProxy(IActivityManager origin) throws Exception {
    return new ActivityManagerProxy(this, origin);
}

既然用了动态代理,那就看看ActivityManagerProxy的invoke方法

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    if ("startService".equals(method.getName())) {
        try {
            return startService(proxy, method, args);
        } catch (Throwable e) {
            Log.e(TAG, "Start service error", e);
        }
    } else if ("stopService".equals(method.getName())) {
        try {
            return stopService(proxy, method, args);
        } catch (Throwable e) {
            Log.e(TAG, "Stop Service error", e);
        }
    }
    ...
}

在执行Service生命周期等关键方法时都做了相应的代理处理,看看startService

protected Object startService(Object proxy, Method method, Object[] args) throws Throwable {
    IApplicationThread appThread = (IApplicationThread) args[0];
    Intent target = (Intent) args[1];
    ResolveInfo resolveInfo = this.mPluginManager.resolveService(target, 0);
    if (null == resolveInfo || null == resolveInfo.serviceInfo) {
        // is host service
        return method.invoke(this.mActivityManager, args);
    }

    return startDelegateServiceForTarget(target, resolveInfo.serviceInfo, null, RemoteService.EXTRA_COMMAND_START_SERVICE);
}

resolveService判断是不是宿主工程的Service,宿主Service走原来的逻辑,插件Service就走startDelegateServiceForTarget

public ResolveInfo resolveService(Intent intent, int flags) {
    for (LoadedPlugin plugin : this.mPlugins.values()) {
        ResolveInfo resolveInfo = plugin.resolveService(intent, flags);
        if (null != resolveInfo) {
            return resolveInfo;
        }
    }

    return null;
}

startDelegateServiceForTarget里面执行wrapperTargetIntent,

protected ComponentName startDelegateServiceForTarget(Intent target, ServiceInfo serviceInfo, Bundle extras, int command) {
    Intent wrapperIntent = wrapperTargetIntent(target, serviceInfo, extras, command);
    return mPluginManager.getHostContext().startService(wrapperIntent);
}

关键点local ? LocalService.class : RemoteService.class,明确了需要哪个Service做代理类。

protected Intent wrapperTargetIntent(Intent target, ServiceInfo serviceInfo, Bundle extras, int command) {
    // fill in service with ComponentName
    target.setComponent(new ComponentName(serviceInfo.packageName, serviceInfo.name));
    String pluginLocation = mPluginManager.getLoadedPlugin(target.getComponent()).getLocation();

    // start delegate service to run plugin service inside
    boolean local = PluginUtil.isLocalService(serviceInfo);
    Class<? extends Service> delegate = local ? LocalService.class : RemoteService.class;
    Intent intent = new Intent();
    intent.setClass(mPluginManager.getHostContext(), delegate);
    intent.putExtra(RemoteService.EXTRA_TARGET, target);
    intent.putExtra(RemoteService.EXTRA_COMMAND, command);
    intent.putExtra(RemoteService.EXTRA_PLUGIN_LOCATION, pluginLocation);
    if (extras != null) {
        intent.putExtras(extras);
    }

    return intent;
}

RemoteService继承了LocalService,RemoteService作用是loadPlugin,其他工作交给LocalService。这样做也合理,如果是插件的交给RemoteService来loadPlugin,省下的逻辑都是相同的交给LocalService处理就可以了。

public int onStartCommand(Intent intent, int flags, int startId) {
    if (intent == null) {
        return super.onStartCommand(intent, flags, startId);
    }

    Intent target = intent.getParcelableExtra(EXTRA_TARGET);
    if (target != null) {
        String pluginLocation = intent.getStringExtra(EXTRA_PLUGIN_LOCATION);
        ComponentName component = target.getComponent();
        LoadedPlugin plugin = PluginManager.getInstance(this).getLoadedPlugin(component);
        if (plugin == null && pluginLocation != null) {
            try {
                PluginManager.getInstance(this).loadPlugin(new File(pluginLocation));
            } catch (Exception e) {
                Log.w(TAG, e);
            }
        }
    }

    return super.onStartCommand(intent, flags, startId);
}

LocalService按照service的启动流程,loadClass先加载service,反射调用attach,在调用onCreate方法,rememberService记录attach后的service,再调用service.onStartCommand执行命令。如果是启动了的直接调用onStartCommand。

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    if (null == intent || !intent.hasExtra(EXTRA_TARGET) || !intent.hasExtra(EXTRA_COMMAND)) {
        return START_STICKY;
    }

    Intent target = intent.getParcelableExtra(EXTRA_TARGET);
    int command = intent.getIntExtra(EXTRA_COMMAND, 0);
    if (null == target || command <= 0) {
        return START_STICKY;
    }

    ComponentName component = target.getComponent();
    LoadedPlugin plugin = mPluginManager.getLoadedPlugin(component);
    
    if (plugin == null) {
        Log.w(TAG, "Error target: " + target.toURI());
        return START_STICKY;
    }
    // ClassNotFoundException when unmarshalling in Android 5.1
    target.setExtrasClassLoader(plugin.getClassLoader());
    switch (command) {
        case EXTRA_COMMAND_START_SERVICE: {
            ActivityThread mainThread = ActivityThread.currentActivityThread();
            IApplicationThread appThread = mainThread.getApplicationThread();
            Service service;

            if (this.mPluginManager.getComponentsHandler().isServiceAvailable(component)) {
                service = this.mPluginManager.getComponentsHandler().getService(component);
            } else {
                try {
                    service = (Service) plugin.getClassLoader().loadClass(component.getClassName()).newInstance();

                    Application app = plugin.getApplication();
                    IBinder token = appThread.asBinder();
                    Method attach = service.getClass().getMethod("attach", Context.class, ActivityThread.class, String.class, IBinder.class, Application.class, Object.class);
                    IActivityManager am = mPluginManager.getActivityManager();

                    attach.invoke(service, plugin.getPluginContext(), mainThread, component.getClassName(), token, app, am);
                    service.onCreate();
                    this.mPluginManager.getComponentsHandler().rememberService(component, service);
                } catch (Throwable t) {
                    return START_STICKY;
                }
            }

            service.onStartCommand(target, 0, this.mPluginManager.getComponentsHandler().getServiceCounter(service).getAndIncrement());
            break;
        }
        ...
    }

    return START_STICKY;
}

TODO:分析动态代理思想

六、支持插件中的BroadcastReceiver

思路是动态注册广播,将静态注册的广播转变为动态注册,将插件中静态注册的receiver使用mHostContext重新注册一遍。具体代码可以在LoadedPlugin构造方法查看。

public LoadedPlugin(PluginManager pluginManager, Context context, File apk) throws Exception {
    ...

    // Register broadcast receivers dynamically
    Map<ComponentName, ActivityInfo> receivers = new HashMap<ComponentName, ActivityInfo>();
    for (PackageParser.Activity receiver : this.mPackage.receivers) {
        receivers.put(receiver.getComponentName(), receiver.info);

        BroadcastReceiver br = BroadcastReceiver.class.cast(getClassLoader().loadClass(receiver.getComponentName().getClassName()).newInstance());
        for (PackageParser.ActivityIntentInfo aii : receiver.intents) {
            this.mHostContext.registerReceiver(br, aii);
        }
    }
    this.mReceiverInfos = Collections.unmodifiableMap(receivers);
    this.mPackageInfo.receivers = receivers.values().toArray(new ActivityInfo[receivers.size()]);

    // try to invoke plugin's application
    invokeApplication();
}

再看看查询所有的receivers,先对比ComponentName是否相同,component为空时再用intent等去匹配。

public List<ResolveInfo> queryBroadcastReceivers(Intent intent, int flags) {
    ComponentName component = intent.getComponent();
    List<ResolveInfo> resolveInfos = new ArrayList<ResolveInfo>();
    ContentResolver resolver = this.mPluginContext.getContentResolver();

    for (PackageParser.Activity receiver : this.mPackage.receivers) {
        if (receiver.getComponentName().equals(component)) {
            ResolveInfo resolveInfo = new ResolveInfo();
            resolveInfo.activityInfo = receiver.info;
            resolveInfos.add(resolveInfo);
        } else if (component == null) {
            // only match implicit intent
            for (PackageParser.ActivityIntentInfo intentInfo : receiver.intents) {
                if (intentInfo.match(resolver, intent, true, TAG) >= 0) {
                    ResolveInfo resolveInfo = new ResolveInfo();
                    resolveInfo.activityInfo = receiver.info;
                    resolveInfos.add(resolveInfo);
                    break;
                }
            }
        }
    }

    return resolveInfos;
}

七、支持插件中的ContentProvider

先看一下插件化是如何使用ContentProvider的,获取插件LoadedPlugin后,通过PluginContentResolver的wrapperUri转化Uri为后续支持读取的Uri。

// test ContentProvider
Uri bookUri = Uri.parse("content://com.didi.virtualapk.demo.book.provider/book");
LoadedPlugin plugin = PluginManager.getInstance(this).getLoadedPlugin(pkg);
bookUri = PluginContentResolver.wrapperUri(plugin, bookUri);

Cursor bookCursor = getContentResolver().query(bookUri, new String[]{"_id", "name"}, null, null, null);
if (bookCursor != null) {
    while (bookCursor.moveToNext()) {
        int bookId = bookCursor.getInt(0);
        String bookName = bookCursor.getString(1);
        Log.d("ryg", "query book:" + bookId + ", " + bookName);
    }
    bookCursor.close();
}

是用wrapperUri转化下Uri后就可以正常读取ContentProvider的数据了。这里使用到了RemoteContentProvider,这个转化其实就是后面会使用到RemoteContentProvider。

public static Uri wrapperUri(LoadedPlugin loadedPlugin, Uri pluginUri) {
    String pkg = loadedPlugin.getPackageName();
    String pluginUriString = Uri.encode(pluginUri.toString());
    StringBuilder builder = new StringBuilder(RemoteContentProvider.getUri(loadedPlugin.getHostContext()));
    builder.append("/?plugin=" + loadedPlugin.getLocation());
    builder.append("&pkg=" + pkg);
    builder.append("&uri=" + pluginUriString);
    Uri wrapperUri = Uri.parse(builder.toString());
    return wrapperUri;
}

wrapperUri会拼接成

content://com.libill.virtualapk.VirtualAPK.Provider/?plugin=/storage/emulated/0/Test.apk&pkg=com.didi.virtualapk.demo&uri=content%3A%2F%2Fcom.didi.virtualapk.demo.book.provider%2Fbook

我们先看看pluginManager.loadPlugin(apk)时做了一些解释数据,并缓存起来。

// Cache providers
Map<String, ProviderInfo> providers = new HashMap<String, ProviderInfo>();
Map<ComponentName, ProviderInfo> providerInfos = new HashMap<ComponentName, ProviderInfo>();
for (PackageParser.Provider provider : this.mPackage.providers) {
    providers.put(provider.info.authority, provider.info);
    providerInfos.put(provider.getComponentName(), provider.info);
}
this.mProviders = Collections.unmodifiableMap(providers);
this.mProviderInfos = Collections.unmodifiableMap(providerInfos);
this.mPackageInfo.providers = providerInfos.values().toArray(new ProviderInfo[providerInfos.size()]);

当使用getContentResolver的query时,会通过IPC通信,调用acquireProvider获取IContentProvider的binder对象。而PluginContentResolver对acquireProvider等多个方法做了代理。

@Override
protected IContentProvider acquireProvider(Context context, String auth) {
    if (mPluginManager.resolveContentProvider(auth, 0) != null) {
        return mPluginManager.getIContentProvider();
    }
    return super.acquireProvider(context, auth);
}

在getIContentProvider方法,做了hook IContentProvider动作。

public synchronized IContentProvider getIContentProvider() {
    if (mIContentProvider == null) {
        hookIContentProviderAsNeeded();
    }

    return mIContentProvider;
}

使用了反射器对mProviderMap反射操作,获取mProviderMap来能获取到占坑的 Provider。对authority、mProvider做了setAccessible(true),最终IContentProviderProxy.newInstance生成IContentProviderProxy对象。这里还使用了RemoteContentProvider包装Uri。

protected void hookIContentProviderAsNeeded() {
    Uri uri = Uri.parse(RemoteContentProvider.getUri(mContext));
    mContext.getContentResolver().call(uri, "wakeup", null, null);
    try {
        Field authority = null;
        Field provider = null;
        ActivityThread activityThread = ActivityThread.currentActivityThread();
        Map providerMap = Reflector.with(activityThread).field("mProviderMap").get();
        Iterator iter = providerMap.entrySet().iterator();
        while (iter.hasNext()) {
            Map.Entry entry = (Map.Entry) iter.next();
            Object key = entry.getKey();
            Object val = entry.getValue();
            String auth;
            if (key instanceof String) {
                auth = (String) key;
            } else {
                if (authority == null) {
                    authority = key.getClass().getDeclaredField("authority");
                    authority.setAccessible(true);
                }
                auth = (String) authority.get(key);
            }
            if (auth.equals(RemoteContentProvider.getAuthority(mContext))) {
                if (provider == null) {
                    provider = val.getClass().getDeclaredField("mProvider");
                    provider.setAccessible(true);
                }
                IContentProvider rawProvider = (IContentProvider) provider.get(val);
                IContentProvider proxy = IContentProviderProxy.newInstance(mContext, rawProvider);
                mIContentProvider = proxy;
                Log.d(TAG, "hookIContentProvider succeed : " + mIContentProvider);
                break;
            }
        }
    } catch (Exception e) {
        Log.w(TAG, e);
    }
}

IContentProvider使用了动态代理方式

public static IContentProvider newInstance(Context context, IContentProvider iContentProvider) {
    return (IContentProvider) Proxy.newProxyInstance(iContentProvider.getClass().getClassLoader(),
            new Class[] { IContentProvider.class }, new IContentProviderProxy(context, iContentProvider));
}

看看invoke方法,使用wrapperUri后直接调用method.invoke

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    Log.v(TAG, method.toGenericString() + " : " + Arrays.toString(args));
    wrapperUri(method, args);

    try {
        return method.invoke(mBase, args);
    } catch (InvocationTargetException e) {
        throw e.getTargetException();
    }
}

在RemoteContentProvider的getContentProvider可以看到加载和使用插件的ContentProvider。

private ContentProvider getContentProvider(final Uri uri) {
    final PluginManager pluginManager = PluginManager.getInstance(getContext());
    Uri pluginUri = Uri.parse(uri.getQueryParameter(KEY_URI));
    final String auth = pluginUri.getAuthority();
    ContentProvider cachedProvider = sCachedProviders.get(auth);
    if (cachedProvider != null) {
        return cachedProvider;
    }

    synchronized (sCachedProviders) {
        LoadedPlugin plugin = pluginManager.getLoadedPlugin(uri.getQueryParameter(KEY_PKG));
        if (plugin == null) {
            try {
                pluginManager.loadPlugin(new File(uri.getQueryParameter(KEY_PLUGIN)));
            } catch (Exception e) {
                Log.w(TAG, e);
            }
        }

        final ProviderInfo providerInfo = pluginManager.resolveContentProvider(auth, 0);
        if (providerInfo != null) {
            RunUtil.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    try {
                        LoadedPlugin loadedPlugin = pluginManager.getLoadedPlugin(uri.getQueryParameter(KEY_PKG));
                        ContentProvider contentProvider = (ContentProvider) Class.forName(providerInfo.name).newInstance();
                        contentProvider.attachInfo(loadedPlugin.getPluginContext(), providerInfo);
                        sCachedProviders.put(auth, contentProvider);
                    } catch (Exception e) {
                        Log.w(TAG, e);
                    }
                }
            }, true);
            return sCachedProviders.get(auth);
        }
    }

    return null;
}

八、支持插件中的Resources

资源的加载入口时加载插件时生成LoadedPlugin对象,这时开始加载资源。

this.mResources = createResources(context, getPackageName(), apk);

Constants.COMBINE_RESOURCES一直为true,直接进入ResourcesManager.createResources

protected Resources createResources(Context context, String packageName, File apk) throws Exception {
    if (Constants.COMBINE_RESOURCES) {
        return ResourcesManager.createResources(context, packageName, apk);
    } else {
        Resources hostResources = context.getResources();
        AssetManager assetManager = createAssetManager(context, apk);
        return new Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration());
    }
}

createResources加载资源后,用hook Resources一遍

public static synchronized Resources createResources(Context hostContext, String packageName, File apk) throws Exception {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        return createResourcesForN(hostContext, packageName, apk);
    }
    
    Resources resources = ResourcesManager.createResourcesSimple(hostContext, apk.getAbsolutePath());
    ResourcesManager.hookResources(hostContext, resources);
    return resources;
}

createResourcesForN是兼容N

@TargetApi(Build.VERSION_CODES.N)
private static Resources createResourcesForN(Context context, String packageName, File apk) throws Exception {
    long startTime = System.currentTimeMillis();
    String newAssetPath = apk.getAbsolutePath();
    ApplicationInfo info = context.getApplicationInfo();
    String baseResDir = info.publicSourceDir;
    
    info.splitSourceDirs = append(info.splitSourceDirs, newAssetPath);
    LoadedApk loadedApk = Reflector.with(context).field("mPackageInfo").get();

    Reflector rLoadedApk = Reflector.with(loadedApk).field("mSplitResDirs");
    String[] splitResDirs = rLoadedApk.get();
    rLoadedApk.set(append(splitResDirs, newAssetPath));

    final android.app.ResourcesManager resourcesManager = android.app.ResourcesManager.getInstance();
    ArrayMap<ResourcesKey, WeakReference<ResourcesImpl>> originalMap = Reflector.with(resourcesManager).field("mResourceImpls").get();

    synchronized (resourcesManager) {
        HashMap<ResourcesKey, WeakReference<ResourcesImpl>> resolvedMap = new HashMap<>();

        if (Build.VERSION.SDK_INT >= 28
            || (Build.VERSION.SDK_INT == 27 && Build.VERSION.PREVIEW_SDK_INT != 0)) { // P Preview
            ResourcesManagerCompatForP.resolveResourcesImplMap(originalMap, resolvedMap, context, loadedApk);

        } else {
            ResourcesManagerCompatForN.resolveResourcesImplMap(originalMap, resolvedMap, baseResDir, newAssetPath);
        }

        originalMap.clear();
        originalMap.putAll(resolvedMap);
    }

    android.app.ResourcesManager.getInstance().appendLibAssetForMainAssetPath(baseResDir, packageName + ".vastub");

    Resources newResources = context.getResources();

    // lastly, sync all LoadedPlugin to newResources
    for (LoadedPlugin plugin : PluginManager.getInstance(context).getAllLoadedPlugins()) {
        plugin.updateResources(newResources);
    }

    Log.d(TAG, "createResourcesForN cost time: +" + (System.currentTimeMillis() - startTime) + "ms");
    return newResources;
}

按照系统版本、厂家来兼容

private static Resources createResourcesSimple(Context hostContext, String apk) throws Exception {
    Resources hostResources = hostContext.getResources();
    Resources newResources = null;
    AssetManager assetManager;
    Reflector reflector = Reflector.on(AssetManager.class).method("addAssetPath", String.class);
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
        assetManager = AssetManager.class.newInstance();
        reflector.bind(assetManager);
        final int cookie1 = reflector.call(hostContext.getApplicationInfo().sourceDir);;
        if (cookie1 == 0) {
            throw new RuntimeException("createResources failed, can't addAssetPath for " + hostContext.getApplicationInfo().sourceDir);
        }
    } else {
        assetManager = hostResources.getAssets();
        reflector.bind(assetManager);
    }
    final int cookie2 = reflector.call(apk);
    if (cookie2 == 0) {
        throw new RuntimeException("createResources failed, can't addAssetPath for " + apk);
    }
    List<LoadedPlugin> pluginList = PluginManager.getInstance(hostContext).getAllLoadedPlugins();
    for (LoadedPlugin plugin : pluginList) {
        final int cookie3 = reflector.call(plugin.getLocation());
        if (cookie3 == 0) {
            throw new RuntimeException("createResources failed, can't addAssetPath for " + plugin.getLocation());
        }
    }
    if (isMiUi(hostResources)) {
        newResources = MiUiResourcesCompat.createResources(hostResources, assetManager);
    } else if (isVivo(hostResources)) {
        newResources = VivoResourcesCompat.createResources(hostContext, hostResources, assetManager);
    } else if (isNubia(hostResources)) {
        newResources = NubiaResourcesCompat.createResources(hostResources, assetManager);
    } else if (isNotRawResources(hostResources)) {
        newResources = AdaptationResourcesCompat.createResources(hostResources, assetManager);
    } else {
        // is raw android resources
        newResources = new Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration());
    }
    // lastly, sync all LoadedPlugin to newResources
    for (LoadedPlugin plugin : pluginList) {
        plugin.updateResources(newResources);
    }
    
    return newResources;
}

九、参考内容

  1. 聊聊 VirtualAPK 插件化框架的开源
  2. VirtualAPK:滴滴 Android 插件化的实践之路
  3. VirtualApk GitHub Wiki官方入门文档
  4. VirtualAPK 资源加载分析
  5. Android插件化快速入门与实例解析(VirtualApk)
  6. 深度 | 滴滴插件化方案 VirtualApk 源码解析
  7. 插件化探索,滴滴开源框架VirtualAPK的深入分析
  8. 图书推荐《Android插件化开发指南》

什么是事件?事件是用户触摸手机屏幕,引起的一系列TouchEvent,包括ACTION_DOWN、ACTION_MOVE、ACTION_UP、ACTION_CANCEL等,这些action组合后变成点击事件、长按事件等。

在这篇文章中,用打Log测试的方法来了解Android TouchEvent 事件分发,拦截,处理过程。虽然看了一些其他的文章和源码及相关的资料,但是还是觉得需要打下Log和画图来了解一下,不然很容易忘记了事件传递的整个过程。所以写下这篇文章,达到看完这篇文章基本可以了解整个过程,并且可以自己画图画出来给别人看。

先看几个类,主要是画出一个3个ViewGroup叠加的界面,并在事件分发、拦截、处理时打下Log.

GitHub地址:https://github.com/libill/TouchEventDemo

一、通过打log分析事件分发

这里在一个Activity上添加三个ViewGroup来分析,这里值得注意的是Activity、View是没有onInterceptTouchEvent方法的。

一、了解Activity、ViewGroup1、ViewGroup2、ViewGroup3四个类

  1. activity_main.xml

     <?xml version="1.0" encoding="utf-8"?>
         <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     tools:context="com.touchevent.demo.MyActivity">
         <com.touchevent.demo.ViewGroup1
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:background="@color/colorAccent">
         <com.touchevent.demo.ViewGroup2
             android:layout_width="match_parent"
             android:layout_height="match_parent"
             android:layout_margin="50dp"
             android:background="@color/colorPrimary">
             <com.touchevent.demo.ViewGroup3
                 android:layout_width="match_parent"
                 android:layout_height="match_parent"
                 android:layout_margin="50dp"
                 android:background="@color/colorPrimaryDark">
             </com.touchevent.demo.ViewGroup3>
         </com.touchevent.demo.ViewGroup2>
         </com.touchevent.demo.ViewGroup1>
     </android.support.constraint.ConstraintLayout>  
    
  1. 主界面:MainActivity.java
    public class MyActivity extends AppCompatActivity {
        private final static String TAG = MyActivity.class.getName();
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
        }
    
        @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
            Log.i(TAG, "dispatchTouchEvent    action:" + StringUtils.getMotionEventName(ev));
            boolean superReturn = super.dispatchTouchEvent(ev);
            Log.d(TAG, "dispatchTouchEvent    action:" + StringUtils.getMotionEventName(ev) + " " + superReturn);
            return superReturn;
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent ev) {
            Log.i(TAG, "onTouchEvent          action:" + StringUtils.getMotionEventName(ev));
            boolean superReturn = super.onTouchEvent(ev);
            Log.d(TAG, "onTouchEvent          action:" + StringUtils.getMotionEventName(ev) + " " + superReturn);
            return superReturn;
        }
    }
  1. 三个ViewGroup,里面的代码完全一样:ViewGroup1.java,ViewGroup2.java,ViewGroup3.java。由于代码一样所以只贴其中一个类。
    public class ViewGroup1 extends LinearLayout {
        private final static String TAG = ViewGroup1.class.getName();
    
        public ViewGroup1(Context context) {
            super(context);
        }
    
        public ViewGroup1(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
            Log.i(TAG, "dispatchTouchEvent    action:" + StringUtils.getMotionEventName(ev));
            boolean superReturn = super.dispatchTouchEvent(ev);
            Log.d(TAG, "dispatchTouchEvent    action:" + StringUtils.getMotionEventName(ev) + " " + superReturn);
            return superReturn;
        }
    
        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            Log.i(TAG, "onInterceptTouchEvent action:" + StringUtils.getMotionEventName(ev));
            boolean superReturn = super.onInterceptTouchEvent(ev);
            Log.d(TAG, "onInterceptTouchEvent action:" + StringUtils.getMotionEventName(ev) + " " + superReturn);
            return superReturn;
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent ev) {
            Log.i(TAG, "onTouchEvent          action:" + StringUtils.getMotionEventName(ev));
            boolean superReturn = super.onTouchEvent(ev);
            Log.d(TAG, "onTouchEvent          action:" + StringUtils.getMotionEventName(ev) + " " + superReturn);
            return superReturn;
        }
    }

二、不拦截处理任何事件

添加没有拦截处理任何事件的代码,看看事件是怎么传递的,选择Info,查看Log.

从流程图可以看出,事件分发从Activity开始,然后分发到ViewGroup,在这个过程中,只要ViewGroup没有拦截处理,最后还是会回到Activity的onTouchEvent方法。

三、ViewGroup2的dispatchTouchEvent返回true

把ViewGroup2.java的dispatchTouchEvent修改一下,return 返回true使事件不在分发

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
 Log.i(TAG, "dispatchTouchEvent    action:" + StringUtils.getMotionEventName(ev));
 Log.d(TAG, "onInterceptTouchEvent action:" + StringUtils.getMotionEventName(ev) + " " + true);
 return true;
}

此时的Log

从图片可以看出,当ViewGroupon2的dispatchTouchEvent返回true后,事件不会再分发传送到ViewGroup3了,也不会分发到Activity的onTouchEvent了。而是事件到了ViewGroupon2的dispatchTouchEvent后,就停止了。dispatchTouchEvent返回true表示着事件不用再分发下去了。

四、ViewGroup2的onInterceptTouchEvent返回true

把ViewGroup2.java的onInterceptTouchEvent修改一下,return 返回true把事件拦截了

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    Log.i(TAG, "dispatchTouchEvent    action:" + StringUtils.getMotionEventName(ev));
    boolean superReturn = super.dispatchTouchEvent(ev);
    Log.d(TAG, "dispatchTouchEvent    action:" + StringUtils.getMotionEventName(ev) + " " + superReturn);
    return superReturn;
}

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    Log.i(TAG, "onInterceptTouchEvent action:" + StringUtils.getMotionEventName(ev));
    Log.d(TAG, "onInterceptTouchEvent action:" + StringUtils.getMotionEventName(ev) + " " + true);
    return true;
}

此时的Log


可以看出ViewGroup2拦截了事件,就不会继续分发到ViewGroup3;而且ViewGroup3拦截了事件又不处理事件,会把事件传递到Activity的onTouchEvent方法。

五、ViewGroup2的onInterceptTouchEvent、onTouchEvent返回true

把ViewGroup2.java的onTouchEvent修改一下,return 返回true把事件处理了

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    Log.i(TAG, "onInterceptTouchEvent action:" + StringUtils.getMotionEventName(ev));
    Log.d(TAG, "onInterceptTouchEvent action:" + StringUtils.getMotionEventName(ev) + " " + true);
    return true;
}

@Override
public boolean onTouchEvent(MotionEvent ev) {
    Log.i(TAG, "onTouchEvent          action:" + StringUtils.getMotionEventName(ev));
    Log.d(TAG, "onTouchEvent          action:" + StringUtils.getMotionEventName(ev) + " " + true);
    return true;
}


从流程可以总结出,当ViewGroup2的onInterceptTouchEvent、onTouchEvent都返回true时,事件最终会走到ViewGroup2的onTouchEvent方法处理事件,后续的事件都会走到这里来。

上面通过log分析很清楚了,是不是就这样够了?其实还不行,还要从源码的角度去分析下,为什么事件会这样分发。

二、通过源码分析事件分发

一、Activity的dispatchTouchEvent

先看看Activity下的dispatchTouchEvent

public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}

onUserInteraction方法

public void onUserInteraction() {
}

从代码可以了解

  1. 调用Activity的onUserInteraction方法,action为down时会进去onUserInteraction方法,但是这个是空方法不做任何事情,可以忽略。

  2. 调用window的superDispatchTouchEvent方法,返回true时事件分发处理结束,否则会调用Activity的onTouchEvent方法。

  3. 调用Activity的onTouchEvent方法,进入这个条件的方法是window的superDispatchTouchEvent方法返回false。从上面的分析(二、不拦截处理任何事件)可以知道,所有子View的dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent都返回false时会调动Activity的onTouchEvent方法,这个时候也是使window的superDispatchTouchEvent方法返回false成立。

二、window的superDispatchTouchEvent

Activity的getWindow方法

public Window getWindow() {
    return mWindow;
}

mWindow是如何赋值的?
是在Activity的attach方法赋值的,其实mWindow是PhoneWindow。

Activity的attach方法

final void attach(Context context, ActivityThread aThread,
        Instrumentation instr, IBinder token, int ident,
        Application application, Intent intent, ActivityInfo info,
        CharSequence title, Activity parent, String id,
        NonConfigurationInstances lastNonConfigurationInstances,
        Configuration config, String referrer, IVoiceInteractor voiceInteractor,
        Window window, ActivityConfigCallback activityConfigCallback) {
    attachBaseContext(context);

    mFragments.attachHost(null /*parent*/);

    mWindow = new PhoneWindow(this, window, activityConfigCallback);
    mWindow.setWindowControllerCallback(this);
    mWindow.setCallback(this);
    mWindow.setOnWindowDismissedCallback(this);
    mWindow.getLayoutInflater().setPrivateFactory(this);
    ...
}

PhoneWindow的superDispatchTouchEvent方法

private DecorView mDecor;

@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}

DevorView的superDispatchTouchEvent

public boolean superDispatchTouchEvent(MotionEvent event) {
    return super.dispatchTouchEvent(event);
}

而mDecor是一个继承FrameLayout的DecorView,就这样把事件分发到ViewGroup上了。

三、ViewGroup的dispatchTouchEvent

3.1 ViewGroup拦截事件的情况

        // Check for interception.
        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action); // restore action in case it was changed
            } else {
                intercepted = false;
            }
        } else {
            // There are no touch targets and this action is not an initial down
            // so this view group continues to intercept touches.
            intercepted = true;
        }

这里分为2种情况会判断是否需要拦截,也就是当某一条件成立时,会执行onInterceptTouchEvent判断是否需要拦截事件。

  1. 当actionMasked == MotionEvent.ACTION_DOWN时。

  2. 当mFirstTouchTarget != null时。mFirstTouchTarget是成功处理事件的ViewGroup的子View,也就是ViewGroup的子View在以下情况返回true时,这个在log分析流程图轻易得到:

    2.1 dispatchTouchEvent返回true

    2.2 如果子View是ViewGroup时,onInterceptTouchEvent、onTouchEvent返回true

另外还有一种情况是disallowIntercept为true时,intercepted直接赋值false不进行拦截。FLAG_DISALLOW_INTERCEPT是通过requestDisallowInterceptTouchEvent方法来设置的,用于在子View中设置,设置后ViewGroup只能拦截down事件,无法拦截其他move、up、cancel事件。为什么ViewGroup还能拦截down事件呢?因为ViewGroup在down事件时进行了重置,看看以下代码

// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
    // Throw away all previous state when starting a new touch gesture.
    // The framework may have dropped the up or cancel event for the previous gesture
    // due to an app switch, ANR, or some other state change.
    cancelAndClearTouchTargets(ev);
    resetTouchState();
}

private void resetTouchState() {
    clearTouchTargets();
    resetCancelNextUpFlag(this);
    mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    mNestedScrollAxes = SCROLL_AXIS_NONE;
}

通过源码可以了解到,ViewGroup拦截事件后,不再调用onInterceptTouchEvent,而是直接交给mFirstTouchTarget的onTouchEvent处理,如果该onTouchEvent不处理最终会交给Activity的onTouchEvent。

3.2 ViewGroup不拦截事件的情况

ViewGroup不拦截事件时,会遍历子View,使事件分发到子View进行处理。

final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
    final int childIndex = getAndVerifyPreorderedIndex(
            childrenCount, i, customOrder);
    final View child = getAndVerifyPreorderedView(
            preorderedList, children, childIndex);

    // If there is a view that has accessibility focus we want it
    // to get the event first and if not handled we will perform a
    // normal dispatch. We may do a double iteration but this is
    // safer given the timeframe.
    if (childWithAccessibilityFocus != null) {
        if (childWithAccessibilityFocus != child) {
            continue;
        }
        childWithAccessibilityFocus = null;
        i = childrenCount - 1;
    }

    if (!canViewReceivePointerEvents(child)
            || !isTransformedTouchPointInView(x, y, child, null)) {
        ev.setTargetAccessibilityFocus(false);
        continue;
    }

    newTouchTarget = getTouchTarget(child);
    if (newTouchTarget != null) {
        // Child is already receiving touch within its bounds.
        // Give it the new pointer in addition to the ones it is handling.
        newTouchTarget.pointerIdBits |= idBitsToAssign;
        break;
    }

    resetCancelNextUpFlag(child);
    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
        // Child wants to receive touch within its bounds.
        mLastTouchDownTime = ev.getDownTime();
        if (preorderedList != null) {
            // childIndex points into presorted list, find original index
            for (int j = 0; j < childrenCount; j++) {
                if (children[childIndex] == mChildren[j]) {
                    mLastTouchDownIndex = j;
                    break;
                }
            }
        } else {
            mLastTouchDownIndex = childIndex;
        }
        mLastTouchDownX = ev.getX();
        mLastTouchDownY = ev.getY();
        newTouchTarget = addTouchTarget(child, idBitsToAssign);
        alreadyDispatchedToNewTouchTarget = true;
        break;
    }
}
3.2.1 寻找可接收事件的子View

通过canViewReceivePointerEvents判断子View是否能够接收到点击事件。必须符合2种情况,缺一不可:1、点击事件的坐标落在在子View的区域内;2、子View没有正在播放动画。满足条件后,调用dispatchTransformedTouchEvent,其实也是调用子View的dispatchTouchEvent。

private static boolean canViewReceivePointerEvents(@NonNull View child) {
    return (child.mViewFlags & VISIBILITY_MASK) == VISIBLE
            || child.getAnimation() != null;
}

protected boolean isTransformedTouchPointInView(float x, float y, View child,
        PointF outLocalPoint) {
    final float[] point = getTempPoint();
    point[0] = x;
    point[1] = y;
    transformPointToViewLocal(point, child);
    final boolean isInView = child.pointInView(point[0], point[1]);
    if (isInView && outLocalPoint != null) {
        outLocalPoint.set(point[0], point[1]);
    }
    return isInView;
}

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    final boolean handled;
    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        event.setAction(MotionEvent.ACTION_CANCEL);
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }

    ...

    // Perform any necessary transformations and dispatch.
    if (child == null) {
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
        final float offsetX = mScrollX - child.mLeft;
        final float offsetY = mScrollY - child.mTop;
        transformedEvent.offsetLocation(offsetX, offsetY);
        if (! child.hasIdentityMatrix()) {
            transformedEvent.transform(child.getInverseMatrix());
        }

        handled = child.dispatchTouchEvent(transformedEvent);
    }

    // Done.
    transformedEvent.recycle();
    return handled;
}

当dispatchTransformedTouchEvent返回true时,结束for循环遍历,赋值newTouchTarget,相当于发现了可以接收事件的View,不用再继续找了。

newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;

在addTouchTarget方法赋值mFirstTouchTarget。

private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
    final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
    target.next = mFirstTouchTarget;
    mFirstTouchTarget = target;
    return target;
}
3.2.2 ViewGroup自己处理事件

另一种情况是mFirstTouchTarget为空时,ViewGroup自己处理事件,这里注意第三个参数为null,ViewGroup的super.dispatchTouchEvent将调用View的dispatchTouchEvent。

if (mFirstTouchTarget == null) {
    // No touch targets so treat this as an ordinary view.
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
            TouchTarget.ALL_POINTER_IDS);
}

3.3 View处理点击事件的过程

View的dispatchTouchEvent是怎么处理事件的呢?

public boolean dispatchTouchEvent(MotionEvent event) {
    boolean result = false;
    ...
    if (onFilterTouchEventForSecurity(event)) {
        if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
            result = true;
        }
        //noinspection SimplifiableIfStatement
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }

        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }
    ...
    return result;
}
  1. 首先使用onFilterTouchEventForSecurity方法过滤不符合应用安全策略的触摸事件。

     public boolean onFilterTouchEventForSecurity(MotionEvent event) {
         //noinspection RedundantIfStatement
         if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0
                 && (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
             // Window is obscured, drop this touch.
             return false;
         }
         return true;
     }
    
  2. mOnTouchListener != null判断是否设置了OnTouchEvent,设置了就执行mOnTouchListener.onTouch并返回true,不再执行onTouchEvent。这里得出OnTouchEvent的优先级高于OnTouchEvent,便于使用setOnTouchListener设置处理点击事件。

  3. 另一种情况是进入onTouchEvent进行处理。

     public boolean onTouchEvent(MotionEvent event) {
         final float x = event.getX();
         final float y = event.getY();
         final int viewFlags = mViewFlags;
         final int action = event.getAction();
     
         final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                 || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                 || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
     
         if ((viewFlags & ENABLED_MASK) == DISABLED) {
             if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                 setPressed(false);
             }
             mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
             // A disabled view that is clickable still consumes the touch
             // events, it just doesn't respond to them.
             return clickable;
         }
         ...
     }
    

当View不可用时,依然会处理事件,只是看起来不可用。

接着执行mTouchDelegate.onTouchEvent

if (mTouchDelegate != null) {
    if (mTouchDelegate.onTouchEvent(event)) {
        return true;
    }
}

下面看看up事件是怎么处理的

/**
 * <p>Indicates this view can display a tooltip on hover or long press.</p>
 * {@hide}
 */
static final int TOOLTIP = 0x40000000;

if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
    switch (action) {
        case MotionEvent.ACTION_UP:
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
            if ((viewFlags & TOOLTIP) == TOOLTIP) {
                handleTooltipUp();
            }
            if (!clickable) {
                removeTapCallback();
                removeLongPressCallback();
                mInContextButtonPress = false;
                mHasPerformedLongPress = false;
                mIgnoreNextUpEvent = false;
                break;
            }
            boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
            if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                // take focus if we don't have it already and we should in
                // touch mode.
                boolean focusTaken = false;
                if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                    focusTaken = requestFocus();
                }

                if (prepressed) {
                    // The button is being released before we actually
                    // showed it as pressed.  Make it show the pressed
                    // state now (before scheduling the click) to ensure
                    // the user sees it.
                    setPressed(true, x, y);
                }

                if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                    // This is a tap, so remove the longpress check
                    removeLongPressCallback();

                    // Only perform take click actions if we were in the pressed state
                    if (!focusTaken) {
                        // Use a Runnable and post this rather than calling
                        // performClick directly. This lets other visual state
                        // of the view update before click actions start.
                        if (mPerformClick == null) {
                            mPerformClick = new PerformClick();
                        }
                        if (!post(mPerformClick)) {
                            performClickInternal();
                        }
                    }
                }

                if (mUnsetPressedState == null) {
                    mUnsetPressedState = new UnsetPressedState();
                }

                if (prepressed) {
                    postDelayed(mUnsetPressedState,
                            ViewConfiguration.getPressedStateDuration());
                } else if (!post(mUnsetPressedState)) {
                    // If the post failed, unpress right now
                    mUnsetPressedState.run();
                }

                removeTapCallback();
            }
            mIgnoreNextUpEvent = false;
            break;
            ...
    }

    return true;
}

从上面代码可以了解,clickable、TOOLTIP(长按)有一个为true时,就会消耗事件,使onTouchEvent返回true。其中PerformClick内部调用了performClick方法。

public boolean performClick() {
    // We still need to call this method to handle the cases where performClick() was called
    // externally, instead of through performClickInternal()
    notifyAutofillManagerOnClick();

    final boolean result;
    final ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnClickListener != null) {
        playSoundEffect(SoundEffectConstants.CLICK);
        li.mOnClickListener.onClick(this);
        result = true;
    } else {
        result = false;
    }

    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

    notifyEnterOrExitForAutoFillIfNeeded(true);

    return result;
}

如果View设置了OnClickListener,那performClick会调用内部的onClick方法。

public void setOnClickListener(@Nullable OnClickListener l) {
    if (!isClickable()) {
        setClickable(true);
    }
    getListenerInfo().mOnClickListener = l;
}

public void setOnLongClickListener(@Nullable OnLongClickListener l) {
    if (!isLongClickable()) {
        setLongClickable(true);
    }
    getListenerInfo().mOnLongClickListener = l;
}

通过setOnClickListener设置clickable,通过setOnLongClickListener设置LONG_CLICKABLE长按事件。设置后使得onTouchEvent返回true。到这里我们已经分析完成点击事件的分发过程了。

本文参考以下内容:

1、《Android开发艺术探索》

说明:调试Android Framework的Java部分代码,以调试源码android-28为例,需要一个API 28的模拟器配合使用。

一、下载源码

下载源码方式很多,由于调试Framework只需要java代码即可,这里使用Android Studio的SDK Manager下载,以下载android-28为例

下载完后,在sdk/sources下看到android-28源码

二、新建一个项目

包名cn.test.demo,避免com开头是因为源码有com,等下拷贝源码是避免重复

三、拷贝源码到项目的java目录下

可以看到项目目录结构

四、新建一个与源码对应的模拟器

五、启动模拟器,选择debug的进程

六、选择某一个源码类进行调试

如选择ActivityManagerService的子类UiHandler,handleMessage方法后,在模拟器随便点击启动应用,如短信,即可调试

更多请参考极客时间-Android开发高手课