Capacitor原理浅究

Capacitor原理浅究

1. 插件

1.1 What Web Can Do?

在开发插件之前,官方推荐打开 https://whatwebcando.today/ ,以查看你的需求是否需要做成插件的形式,比如我们是使用Webview打开H5页面,那么就使用Webview打开这个地址,看目前Web API都可以做哪些工作,不能做哪些工作。

如果已经有 Web API 可以实现我们的需求,那么我们就不需要做无谓的工作。即便此WebAPI仅支持Android使用或者仅支持iOS使用,我们也可以省去一端的开发工作。

1.2 插件开发

参考 https://capacitorjs.com/docs/plugins/tutorial/introduction

使用 npx @capacitor/create-plugin 命令创建插件,输入插件的以下信息:

npm包名、文件夹名、package id、类名、库url、作者、license、插件描述信息。

或者直接以参数形式放在命令后面,官方也给了示例:

npx @capacitor/create-plugin \
  --name @capacitor-community/screen-orientation \
  --package-id io.ionic.plugins.screenorientation \
  --class-name ScreenOrientation \
  --repo "https://ionic.io" \
  --license "MIT" \
  --description "Work with the screen orientation in a common way for iOS, Android, and web"

下面是生成的插件文件夹内容:

├─.git
├─android(Android相关文件)
│ ├─.gradle(gradle依赖文件夹)
│ ├─gradle/wrapper
│ ├─src
│ │ ├─androidTest/java/com/getcapacitor/android
│ │ ├─main
│ │ │ ├─java/com/fawkes/plugins/screenorientation(Android插件代码)
│ │ │ ├─res(资源文件夹)
│ │ │ └─AndroidManifest.xml
│ │ └─test/java/com/getcapacitor
│ ├─.gitignore
│ ├─build.gradle
│ ├─gradle.properties
│ ├─gradlew
│ ├─gradlew.bat
│ ├─progurad-rules.pro
│ └─settings.gradle
├─ios(iOS相关文件)
│ ├─Plugin(iOS插件代码)
│ │ ├─Info.plist
│ │ ├─…
│ ├─Plugin.xcodeproj
│ ├─Plugin.xcworkspace
│ ├─PluginTests
│ └─Podfile
├─src
│ ├─definations.ts(插件API声明)
│ ├─index.ts(注册插件并export)
│ └─web.ts(网页端API实现)
├─.eslintignore
├─.gitignore
├─.prettierignore
├─CapacitorCommunityScreenOrientation.podspec(用于iOS发布到Cocoapod)
├─CONTRIBUTING.md
├─package.json
├─README.md
├─rollup.config.js
└─tsconfig.json

其实和Cordova的文件夹也是有相似之处的,比如android文件夹类似Cordova插件的src/android文件夹,src文件夹对应Cordova插件的www文件夹,只是Capacitor插件使用ts。

开发插件时,以Android为例,可以新建一个Capacitor工程,在app模块中开发相应代码,开发完成后再把文件拷贝到插件对应文件夹,注意修改Java文件中的包名。

完成后,使用npm publish上传到npm仓库,这一点也是和Cordova相同的。

1.3 插件使用

安装插件需要 npm i [插件npm包名] 然后 npx cap sync 即可。

H5中调用插件,以Vue为例,只需要import后调用相关方法即可。

代码如下,引入camera插件并使用:

import { Camera, CameraResultType } from '@capacitor/camera';

const takePicture = async () => {
  const image = await Camera.getPhoto({
    quality: 90,
    allowEditing: true,
    resultType: CameraResultType.Uri
  });

  // image.webPath will contain a path that can be set as an image src.
  // You can access the original file using image.path, which can be
  // passed to the Filesystem API to read the raw data of the image,
  // if desired (or pass resultType: CameraResultType.Base64 to getPhoto)
  var imageUrl = image.webPath;

  console.log('image', image)

  // Can be set to the src of an image now
  
};

1.4 插件代码

1.4.1 Android

在 Capacitor Android 平台,插件是以 Module 的形式引入项目,其实和 Uniapp 类似,相比于Cordova直接把插件代码放到app Modules下,组件化的形式让代码更加简洁,可维护性更高。

插件类需要继承 com.getcapacitor.Plugin 类,并添加 com.getcapacitor.annotation.CapacitorPlugin 注解,CapacitorPlugin 注解中有三个属性,name、requestCodes、permissions。

插件对js暴露的方法需要加上 @PluginMethod 注解,而方法的参数统一为 PluginCall
我们可以对比下Cordova,Cordova的插件方法都是通过一个入口: execute(String action, JSONArray args, CallbackContext callbackContext) 调用,通过 action 判断调用哪个具体的方法,args 是js传递过来的参数,callbackContext 则是回调类,再看Capacitor则是用@PluginMethod 注解代替了 action 参数,PluginCall 则包含了 args 和 callbackContext,相对来说更加简约。

除了 @Capacitor@PluginMethod 之外,代码中还用到了 @PermissionCallback@ActivityCallback注解,分别用来接收 权限申请 和 Activity结果返回 的回调,使用时直接获取权限或打开Activity时将方法名作为参数传递即可。

下面是官方 Camera 插件的插件类部分代码:

@SuppressLint("InlinedApi")
@CapacitorPlugin(
    name = "Camera",
    permissions = {
        @Permission(strings = { Manifest.permission.CAMERA }, alias = CameraPlugin.CAMERA),
        // SDK VERSIONS 32 AND BELOW
        @Permission(
            strings = { Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE },
            alias = CameraPlugin.PHOTOS
        ),
        /*
        SDK VERSIONS 33 AND ABOVE
        This alias is a placeholder and the PHOTOS alias will be updated to use these permissions
        so that the end user does not need to explicitly use separate aliases depending
        on the SDK version.
         */
        @Permission(strings = { Manifest.permission.READ_MEDIA_IMAGES }, alias = CameraPlugin.MEDIA)
    }
)
public class CameraPlugin extends Plugin {

    // Permission alias constants
    static final String CAMERA = "camera";
    static final String PHOTOS = "photos";
    static final String MEDIA = "media";

    // Message constants
    ...

    @PluginMethod
    public void getPhoto(PluginCall call) {
        ...
    }


    @PluginMethod
    public void pickLimitedLibraryPhotos(PluginCall call) {
        call.unimplemented("not supported on android");
    }

    private boolean checkCameraPermissions(PluginCall call) {
       ...
    }

    private boolean checkPhotosPermissions(PluginCall call) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
            if (getPermissionState(PHOTOS) != PermissionState.GRANTED) {
                requestPermissionForAlias(PHOTOS, call, "cameraPermissionsCallback");
                return false;
            }
        } else if (getPermissionState(MEDIA) != PermissionState.GRANTED) {
            requestPermissionForAlias(MEDIA, call, "cameraPermissionsCallback");
            return false;
        }

        return true;
    }

    /**
     * Completes the plugin call after a camera permission request
     *
     * @see #getPhoto(PluginCall)
     * @param call the plugin call
     */
    @PermissionCallback
    private void cameraPermissionsCallback(PluginCall call) {
        if (call.getMethodName().equals("pickImages")) {
            openPhotos(call, true, true);
        } else {
            if (settings.getSource() == CameraSource.CAMERA && getPermissionState(CAMERA) != PermissionState.GRANTED) {
                Logger.debug(getLogTag(), "User denied camera permission: " + getPermissionState(CAMERA).toString());
                call.reject(PERMISSION_DENIED_ERROR_CAMERA);
                return;
            } else if (settings.getSource() == CameraSource.PHOTOS) {
                PermissionState permissionState = (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU)
                    ? getPermissionState(PHOTOS)
                    : getPermissionState(MEDIA);
                if (permissionState != PermissionState.GRANTED) {
                    Logger.debug(getLogTag(), "User denied photos permission: " + permissionState.toString());
                    call.reject(PERMISSION_DENIED_ERROR_PHOTOS);
                    return;
                }
            }
            doShow(call);
        }
    }

    @Override
    protected void requestPermissionForAliases(@NonNull String[] aliases, @NonNull PluginCall call, @NonNull String callbackName) {
        // If the SDK version is 33 or higher, use the MEDIA alias permissions instead.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            for (int i = 0; i < aliases.length; i++) {
                if (aliases[i].equals(PHOTOS)) {
                    aliases[i] = MEDIA;
                }
            }
        }
        super.requestPermissionForAliases(aliases, call, callbackName);
    }

    @ActivityCallback
    public void processPickedImage(PluginCall call, ActivityResult result) {
        ...
    }

    /**
     * After processing the image, return the final result back to the caller.
     * @param call
     * @param bitmap
     * @param u
     */
    @SuppressWarnings("deprecation")
    private void returnResult(PluginCall call, Bitmap bitmap, Uri u) {
        ...
        call.reject(UNABLE_TO_PROCESS_IMAGE);
        ...
        call.resolve(data);
        ...
    }

   
    @Override
    @PluginMethod
    public void requestPermissions(PluginCall call) {
        // If the camera permission is defined in the manifest, then we have to prompt the user
        // or else we will get a security exception when trying to present the camera. If, however,
        // it is not defined in the manifest then we don't need to prompt and it will just work.
        if (isPermissionDeclared(CAMERA)) {
            // just request normally
            super.requestPermissions(call);
        } else {
            // the manifest does not define camera permissions, so we need to decide what to do
            // first, extract the permissions being requested
            JSArray providedPerms = call.getArray("permissions");
            List<String> permsList = null;
            if (providedPerms != null) {
                try {
                    permsList = providedPerms.toList();
                } catch (JSONException e) {}
            }

            if (permsList != null && permsList.size() == 1 && permsList.contains(CAMERA)) {
                // the only thing being asked for was the camera so we can just return the current state
                checkPermissions(call);
            } else {
                // we need to ask about photos so request storage permissions
                requestPermissionForAlias(PHOTOS, call, "checkPermissions");
            }
        }
    }

    @Override
    public Map<String, PermissionState> getPermissionStates() {
        Map<String, PermissionState> permissionStates = super.getPermissionStates();

        // If Camera is not in the manifest and therefore not required, say the permission is granted
        if (!isPermissionDeclared(CAMERA)) {
            permissionStates.put(CAMERA, PermissionState.GRANTED);
        }

        // If the SDK version is 33 or higher, update the PHOTOS state to match the MEDIA state.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && permissionStates.containsKey(MEDIA)) {
            permissionStates.put(PHOTOS, permissionStates.get(MEDIA));
        }

        return permissionStates;
    }

    @Override
    protected Bundle saveInstanceState() {
        Bundle bundle = super.saveInstanceState();
        if (bundle != null) {
            bundle.putString("cameraImageFileSavePath", imageFileSavePath);
        }
        return bundle;
    }

    @Override
    protected void restoreState(Bundle state) {
        String storedImageFileSavePath = state.getString("cameraImageFileSavePath");
        if (storedImageFileSavePath != null) {
            imageFileSavePath = storedImageFileSavePath;
        }
    }
}
1.4.2 iOS

在 Capacitor iOS 平台,插件以 Pod 库的形式引入。
下面是 Podfile 文件的内容,可以看到已经默认添加了 StatusBar 等插件,其中 camera 插件是使用 npm i @capacitor/camera && npx cap sync 的方式手动添加的。

require_relative '../../node_modules/@capacitor/ios/scripts/pods_helpers'

platform :ios, '13.0'
use_frameworks!

# workaround to avoid Xcode caching of Pods that requires
# Product -> Clean Build Folder after new Cordova plugins installed
# Requires CocoaPods 1.6 or newer
install! 'cocoapods', :disable_input_output_paths => true

def capacitor_pods
  pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
  pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
  pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
  pod 'CapacitorCamera', :path => '../../node_modules/@capacitor/camera'
  pod 'CapacitorHaptics', :path => '../../node_modules/@capacitor/haptics'
  pod 'CapacitorKeyboard', :path => '../../node_modules/@capacitor/keyboard'
  pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar'
end

target 'App' do
  capacitor_pods
  # Add your Pods here
end

post_install do |installer|
  assertDeploymentTarget(installer)
end

还是以 camera 插件为例,看下插件类的代码,可以看到名为 CameraPlugin 的文件有三个,CapacitorPlugin.h、CapacitorPlugin.m、 CapacitorPlugin.swift。
Capacitor.h 定义了两个变量,插件的版本号:

#import <UIKit/UIKit.h>

//! Project version number for Plugin.
FOUNDATION_EXPORT double PluginVersionNumber;

//! Project version string for Plugin.
FOUNDATION_EXPORT const unsigned char PluginVersionString[];

// In this header, you should import all the public headers of your framework using statements like #import <Plugin/PublicHeader.h>

CapacitorPlugin.m 中使用Capacitor的宏定义 CAP_PLUGINCAP_PLUGIN_METHOD 注册插件和插件方法,而其中的 CAPCameraPlugin 和 CAPPluginReturnPromise 之类则是在 Capacitor.swift 中定义的。

#import <Foundation/Foundation.h>
#import <Capacitor/Capacitor.h>

CAP_PLUGIN(CAPCameraPlugin, "Camera",
  CAP_PLUGIN_METHOD(getPhoto, CAPPluginReturnPromise);
  CAP_PLUGIN_METHOD(pickImages, CAPPluginReturnPromise);
  CAP_PLUGIN_METHOD(checkPermissions, CAPPluginReturnPromise);
  CAP_PLUGIN_METHOD(requestPermissions, CAPPluginReturnPromise);
  CAP_PLUGIN_METHOD(pickLimitedLibraryPhotos, CAPPluginReturnPromise);
  CAP_PLUGIN_METHOD(getLimitedLibraryPhotos, CAPPluginReturnPromise);
)

Capacitor.swift 中是具体的代码实现,继承了CAPPLugin,可以看到代码中有一些 @objc 修饰符,@objc 修饰的类名和方法名可以被 Objective-C 调用,对应 Capacitor.m 中的 CAPCameraPlugin 和 CAPPluginReturnPromise 等。

import Foundation
import Capacitor
import Photos
import PhotosUI

@objc(CAPCameraPlugin)
public class CameraPlugin: CAPPlugin {
    private var call: CAPPluginCall?
    private var settings = CameraSettings()
    private let defaultSource = CameraSource.prompt
    private let defaultDirection = CameraDirection.rear
    private var multiple = false

    private var imageCounter = 0

    @objc override public func checkPermissions(_ call: CAPPluginCall) {
        var result: [String: Any] = [:]
        ... 
        call.resolve(result)
    }

    @objc override public func requestPermissions(_ call: CAPPluginCall) {
       ... 
    }

    @objc func pickLimitedLibraryPhotos(_ call: CAPPluginCall) {
        ... 
        call.unavailable("Not available on iOS 13")
        ... 
    }

    @objc func getLimitedLibraryPhotos(_ call: CAPPluginCall) {
        ... 
    }

    @objc func getPhoto(_ call: CAPPluginCall) {
        ...  
    }

    @objc func pickImages(_ call: CAPPluginCall) {
        ... 
    }

// public delegate methods
extension CameraPlugin: UIImagePickerControllerDelegate, UINavigationControllerDelegate, UIPopoverPresentationControllerDelegate {
    public func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
        picker.dismiss(animated: true)
        self.call?.reject("User cancelled photos app")
    }

    public func popoverPresentationControllerDidDismissPopover(_ popoverPresentationController: UIPopoverPresentationController) {
        self.call?.reject("User cancelled photos app")
    }

    public func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
        self.call?.reject("User cancelled photos app")
    }

    public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
        ...
    }
}

@available(iOS 14, *)
extension CameraPlugin: PHPickerViewControllerDelegate {
    public func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
        ...
    }
}

private extension CameraPlugin {
    func returnImage(_ processedImage: ProcessedImage, isSaved: Bool) {
        ...    
    }
}

1.5 Cordova插件兼容

Capacitor是如何实现兼容Cordova插件的?

Cordova插件安装和Capacitor插件安装一样,npm install xxx && npx cap sync 即可,使用则和使用Cordova时一致,直接调用即可。

Capacitor工程 H5目录下,除了 Capacitor 的 capacitor.config.jsoncapacitor.plugins.json (此文件仅Android)文件,还是有Cordova相关的 cordova.jscordova_plugins.js 以及 plugins 文件夹。

其中 cordova.js 是经过 Capacitor 改造的,主要是修改了 js 与 native 相互调用的那块代码,改造成Capacitor 的调用形式。

而插件的原生代码,
Android 平台下,是放在 Capacitor 预置的一个名为 capacitor-cordova-android-plugins 的 Module 下,在此 Module 下的 build.gradle 文件中可以看到,引入了 implementation "org.apache.cordova:framework:$cordovaAndroidVersion" 依赖,保证插件中引入Cordova的类也不会报错。
iOS平台中,是放在 Pods 下 Capacitor 预置的一个名为 CapacitorPluginsTarget 下。

在调用插件时,插件js中会调用 cordova.js 中的 cordova/exec,经过Capacitor改造后,
Android平台,最终会调用 cordova.js 中的 window.androidBridge.postMessage(JSON.stringify(command)); ,而window.androidBridge 是在 capacitor-android 模块下的 MessageHandler.java 文件中添加到 Webview 中的,postMessage 的实现也在此文件中。
iOS 平台,最终会调用 cordova.js 中的 window.webkit.messageHandlers.bridge.postMessage(command);,而其中的 bridge 是在 WebviewDelegationHandler.swift 文件中定义的。

2. bridge

研究过Capacitor是如何兼容Cordova插件之后,我们再看看Capacitor自己的插件的调用原理是什么样的。

www下capacitor 相关的只有两个文件,capacitor.config.jsoncapacitor.plugins.json(此文件仅Android有)。

其中 capacitor.config.json 是一些配置信息,比如app id、应用名称、H5资源包在哪个文件夹等,示例:

{
   "appId": "io.ionic.starter",
   "appName": "TestAAA",
   "webDir": "dist",
   "server": {
      "androidScheme": "https"
   }
}

capacitor.plugins.json 则是插件列表,包含插件npm包名和插件类路径,示例:

[
   {
      "pkg": "@capacitor/app",
      "classpath": "com.capacitorjs.plugins.app.AppPlugin"
   },
   {
      "pkg": "@capacitor/camera",
      "classpath": "com.capacitorjs.plugins.camera.CameraPlugin"
   },
   {
      "pkg": "@capacitor/haptics",
      "classpath": "com.capacitorjs.plugins.haptics.HapticsPlugin"
   },
   {
      "pkg": "@capacitor/keyboard",
      "classpath": "com.capacitorjs.plugins.keyboard.KeyboardPlugin"
   },
   {
      "pkg": "@capacitor/status-bar",
      "classpath": "com.capacitorjs.plugins.statusbar.StatusBarPlugin"
   }
]

2.1 Android

Android 平台,js与native交互的代码放在 capacitor-android 模块中(真实路径为 打包工程根目录/node_moduls/@capacitor/android/capacitor 文件夹下)。
我们可以从 app 模块的 MainActivity 开始,一步步探索,这里只聊一下其中的核心功能点。

2.1.1 BridgeActivity

与Cordova相似,MainActivity 在Cordova中继承了 CordovaActivity,而Capacitor中是继承的 BridgeActivity。
下面是部分代码,可以看到 BridgeActivity 的主要工作就是注册插件。

public class BridgeActivity extends AppCompatActivity {

    protected Bridge bridge;
    protected boolean keepRunning = true;  // config.xml中的设置,默认值为true
    protected CapConfig config;  // Capacitor配置,有默认配置,也可以从文件读取
    protected List<Class<? extends Plugin>> initialPlugins = new ArrayList<>();
    protected final Bridge.Builder bridgeBuilder = new Bridge.Builder(this);

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        // 获取 assets下内容
        PluginManager loader = new PluginManager(getAssets());

        try {
            // 获取 Capacitor 插件
            // 读取 capacitor.plugins.json,获取插件的 classpath,添加到插件列表
            bridgeBuilder.addPlugins(loader.loadPluginClasses());
        } catch (PluginLoadException ex) {
            Logger.error("Error loading plugins.", ex);
        }

        this.load();
    }

    protected void load() {
        Logger.debug("Starting BridgeActivity");
        bridge = bridgeBuilder.addPlugins(initialPlugins).setConfig(config).create();

        this.keepRunning = bridge.shouldKeepRunning();
        this.onNewIntent(getIntent());
    }

    // 手动注册插件
    public void registerPlugin(Class<? extends Plugin> plugin) {
        bridgeBuilder.addPlugin(plugin);
    }
    // 手动批量注册插件
    public void registerPlugins(List<Class<? extends Plugin>> plugins) {
        bridgeBuilder.addPlugins(plugins);
    }
}
2.1.2 Bridge

跟着进入 Bridge.java,这里官方为此类写的注释:

The Bridge class is the main engine of Capacitor. It manages loading and communicating with all Plugins, proxying Native events to Plugins, executing Plugin methods, communicating with the WebView, and a whole lot more.
Bridge类是Capacitor的主要引擎。它负责管理所有插件的加载和通信,将本机事件代理到插件、执行插件方法、与 WebView 通信等等。
Generally, you’ll not use Bridge directly, instead, extend from BridgeActivity to get a WebView instance and proxy native events automatically.
通常,开发时不会直接使用 Bridge,而是扩展 BridgeActivity 以自动获取 WebView 实例并代理本机事件。

Bridge.java代码有一千多行,这里只放一些核心代码:

Builder
Bridge类中有一个 Builder 静态内部类,从2.1 BridgeActivity 中就可以看到,Bridge对象是通过 new Bridge.Builder(this) 初始化了一个 Builder 类,设置一些参数后,使用 create() 构建出来的。

在Builder中,除了 参数、两个构造器 和 一些 set / add 方法,就是 create() 方法。

public Bridge create() {
            // Cordova 初始化,获取 config.xml 中的 配置信息 以及 Cordova插件列表
            ConfigXmlParser parser = new ConfigXmlParser();
            parser.parse(activity.getApplicationContext());
            CordovaPreferences preferences = parser.getPreferences();
            preferences.setPreferencesBundle(activity.getIntent().getExtras());
            List<PluginEntry> pluginEntries = parser.getPluginEntries();

            // MockCordovaInterfaceImpl 继承自CordovaInterfaceImpl
            MockCordovaInterfaceImpl cordovaInterface = new MockCordovaInterfaceImpl(activity);
            if (instanceState != null) {
                cordovaInterface.restoreInstanceState(instanceState);
            }

            WebView webView = this.fragment != null ? fragment.getView().findViewById(R.id.webview) : activity.findViewById(R.id.webview);
            MockCordovaWebViewImpl mockWebView = new MockCordovaWebViewImpl(activity.getApplicationContext());
            mockWebView.init(cordovaInterface, pluginEntries, preferences, webView);
            PluginManager pluginManager = mockWebView.getPluginManager();
            cordovaInterface.onCordovaInit(pluginManager);

            // Bridge initialization
            Bridge bridge = new Bridge(activity, serverPath, fragment, webView, plugins,
                pluginInstances, cordovaInterface, pluginManager, preferences, config
            );

            if (webView instanceof CapacitorWebView) {
                CapacitorWebView capacitorWebView = (CapacitorWebView) webView;
                capacitorWebView.setBridge(bridge);
            }

            bridge.setCordovaWebView(mockWebView);
            bridge.setWebViewListeners(webViewListeners);
            bridge.setRouteProcessor(routeProcessor);

            if (instanceState != null) {
                bridge.restoreInstanceState(instanceState);
            }

            return bridge;
        }

构造器
Bridge 有两个构造器,不过其中一个已经废弃。
下面的代码包含 构造器 和 构造器中用到的部分参数。

// Our MessageHandler for sending and receiving data to the WebView
private final MessageHandler msgHandler;
// The ThreadHandler for executing plugin calls
private final HandlerThread handlerThread = new HandlerThread("CapacitorPlugins");
// Our Handler for posting plugin calls. Created from the ThreadHandler
private Handler taskHandler = null;
// A map of Plugin Id's to PluginHandle's
private Map<String, PluginHandle> plugins = new HashMap<>();

private Bridge(
    AppCompatActivity context, ServerPath serverPath,
    Fragment fragment, WebView webView,
    List<Class<? extends Plugin>> initialPlugins,
    List<Plugin> pluginInstances,
    MockCordovaInterfaceImpl cordovaInterface,
    PluginManager pluginManager, CordovaPreferences preferences,
    CapConfig config
) {
    this.app = new App();
    
    ... // 构造器中参数赋值  this.xxx = xxx,这里就省略掉了

    // Start our plugin execution threads and handlers
    handlerThread.start();
    taskHandler = new Handler(handlerThread.getLooper());

    // Capacitor配置
    this.config = config != null ? config : CapConfig.loadDefault(getActivity());
    // 获取 config中的loggingEnabled 字段,是否打印日志,默认true
    Logger.init(this.config);

    // Initialize web view and message handler for it
    // initWebView() 是初始化了webview的一些配置,如是否启用JS,是否启用Database、UseAgent、内容url等
    this.initWebView();
    // 与Cordova的AllowList类似,从config中获取配置,放行哪些地址的请求
    this.setAllowedOriginRules();
    this.msgHandler = new MessageHandler(this, webView, pluginManager);

    // Grab any intent info that our app was launched with
    Intent intent = context.getIntent();
    this.intentUri = intent.getData();
    // Register our core plugins
    // 注册插件,包括 核心插件、initialPlugins、pluginInstances,对每个插件调用 registerPlugin(pluginClass) 方法
    this.registerAllPlugins();
    
    // 最终是调用 webView.loadUrl(appUrl) 打开 Webview
    this.loadWebView();
}

/**
 * Register a plugin class
 * @param pluginClass a class inheriting from Plugin
 */
public void registerPlugin(Class<? extends Plugin> pluginClass) {
    String pluginId = pluginId(pluginClass);
    if (pluginId == null) return;

    try {
        this.plugins.put(pluginId, new PluginHandle(this, pluginClass));
    } catch (InvalidPluginException ex) {
        logInvalidPluginException(pluginClass);
    } catch (PluginLoadException ex) {
        logPluginLoadException(pluginClass, ex);
    }
}

MessageHandler
从构造器方法中,可以发现,插件的使用应该主要是在 MessageHandler 类中。

可以看到 js 调用 Navtive 还是使用的 @JavascriptInterface 注解,再查看 Capacitor工程 node_modules 下 @capacitor/android/capacitor/src/main/assets/native-bridge.js,可以看到最终插件的调用都是通过 win.androidBridge.postMessage(JSON.stringify(data));win.webkit.messageHandlers.bridge.postMessage(data); 实现的。

而 Native 调用 js 则跟Cordova不太一样,添加了一个新的方式,使用 JavaScriptReplyProxy 实现。

/**
 * MessageHandler handles messages from the WebView, dispatching them
 * to plugins.
 */
public class MessageHandler {

    private Bridge bridge;
    private WebView webView;
    private PluginManager cordovaPluginManager;
    private JavaScriptReplyProxy javaScriptReplyProxy;

    public MessageHandler(Bridge bridge, WebView webView, PluginManager cordovaPluginManager) {
        this.bridge = bridge;
        this.webView = webView;
        this.cordovaPluginManager = cordovaPluginManager;

        if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER) && !bridge.getConfig().isUsingLegacyBridge()) {
            WebViewCompat.WebMessageListener capListener = (view, message, sourceOrigin, isMainFrame, replyProxy) -> {
                if (isMainFrame) {
                    postMessage(message.getData());
                    javaScriptReplyProxy = replyProxy;
                } else {
                    Logger.warn("Plugin execution is allowed in Main Frame only");
                }
            };
            try {
                WebViewCompat.addWebMessageListener(webView, "androidBridge", bridge.getAllowedOriginRules(), capListener);
            } catch (Exception ex) {
                webView.addJavascriptInterface(this, "androidBridge");
            }
        } else {
            webView.addJavascriptInterface(this, "androidBridge");
        }
    }

    // 通用的 js调用 Native 的方法,使用 @JavascriptInterface 注解
    /**
     * The main message handler that will be called from JavaScript
     * to send a message to the native bridge.
     * @param jsonStr
     */
    @JavascriptInterface
    @SuppressWarnings("unused")
    public void postMessage(String jsonStr) {
        try {
            JSObject postData = new JSObject(jsonStr);

            String type = postData.getString("type");

            boolean typeIsNotNull = type != null;
            boolean isCordovaPlugin = typeIsNotNull && type.equals("cordova");
            boolean isJavaScriptError = typeIsNotNull && type.equals("js.error");

            String callbackId = postData.getString("callbackId");

            if (isCordovaPlugin) {
                String service = postData.getString("service");
                String action = postData.getString("action");
                String actionArgs = postData.getString("actionArgs");

                Logger.verbose(
                    Logger.tags("Plugin"),
                    "To native (Cordova plugin): callbackId: " +
                    callbackId +
                    ", service: " +
                    service +
                    ", action: " +
                    action +
                    ", actionArgs: " +
                    actionArgs
                );

                this.callCordovaPluginMethod(callbackId, service, action, actionArgs);
            } else if (isJavaScriptError) {
                Logger.error("JavaScript Error: " + jsonStr);
            } else {
                String pluginId = postData.getString("pluginId");
                String methodName = postData.getString("methodName");
                JSObject methodData = postData.getJSObject("options", new JSObject());

                Logger.verbose(
                    Logger.tags("Plugin"),
                    "To native (Capacitor plugin): callbackId: " + callbackId + ", pluginId: " + pluginId + ", methodName: " + methodName
                );

                this.callPluginMethod(callbackId, pluginId, methodName, methodData);
            }
        } catch (Exception ex) {
            Logger.error("Post message error:", ex);
        }
    }

    // 根据配置和Webview是否支持, 选择使用 JavaScriptReplyProxy 或者 evaluateJavascript 的方式给 js 发消息
    public void sendResponseMessage(PluginCall call, PluginResult successResult, PluginResult errorResult) {
        try {
            PluginResult data = new PluginResult();
            data.put("save", call.isKeptAlive());
            data.put("callbackId", call.getCallbackId());
            data.put("pluginId", call.getPluginId());
            data.put("methodName", call.getMethodName());

            boolean pluginResultInError = errorResult != null;
            if (pluginResultInError) {
                data.put("success", false);
                data.put("error", errorResult);
                Logger.debug("Sending plugin error: " + data.toString());
            } else {
                data.put("success", true);
                if (successResult != null) {
                    data.put("data", successResult);
                }
            }

            boolean isValidCallbackId = !call.getCallbackId().equals(PluginCall.CALLBACK_ID_DANGLING);
            if (isValidCallbackId) {
                if (bridge.getConfig().isUsingLegacyBridge()) {
                    legacySendResponseMessage(data);
                } else if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER) && javaScriptReplyProxy != null) {
                    javaScriptReplyProxy.postMessage(data.toString());
                } else {
                    legacySendResponseMessage(data);
                }
            } else {
                bridge.getApp().fireRestoredResult(data);
            }
        } catch (Exception ex) {
            Logger.error("sendResponseMessage: error: " + ex);
        }
        if (!call.isKeptAlive()) {
            call.release(bridge);
        }
    }

    // 使用 evaluateJavascript 的方式给 js 发消息
    private void legacySendResponseMessage(PluginResult data) {
        final String runScript = "window.Capacitor.fromNative(" + data.toString() + ")";
        final WebView webView = this.webView;
        webView.post(() -> webView.evaluateJavascript(runScript, null));
    }

    // 调用 Capacitor 插件
    private void callPluginMethod(String callbackId, String pluginId, String methodName, JSObject methodData) {
        PluginCall call = new PluginCall(this, pluginId, callbackId, methodName, methodData);
        bridge.callPluginMethod(pluginId, methodName, call);
    }
    // 调用 Cordova 插件
    private void callCordovaPluginMethod(String callbackId, String service, String action, String actionArgs) {
        bridge.execute(
            () -> {
                cordovaPluginManager.exec(service, action, callbackId, actionArgs);
            }
        );
    }
}

2.2 iOS

iOS 平台下,桥接代码在 Pods -> Development Pods -> Capacitor 下(真实路径为 打包工程根目录/node_moduls/@capacitor/ios/Capacitor/Capacitor 文件夹下)。

2.2.1 CAPBridgeViewController

我们也是从入口文件开始看,Capacitor创建的打包工程入口文件使用 Main.storyboard 实现,可以看到里面加载的是 CAPBridgeViewController
loadView() 中,可以看到,我们上面提到的 WebViewDelegationHandler 在这里实例化并作为参数传递到 prepareWebView()CapacitorBridge 中。
CapacitorBridge 稍后再谈,在 prepareWebview() 中把 WKWebview 的 uiDelegatenavigationDelegate 设置为了 WebViewDelegationHandler,这部分也是WKWebview的通用操作。

override public final func loadView() {
        // load the configuration and set the logging flag
        let configDescriptor = instanceDescriptor()
        let configuration = InstanceConfiguration(with: configDescriptor, isDebug: CapacitorBridge.isDevEnvironment)
        CAPLog.enableLogging = configuration.loggingEnabled

        // get the web view
        let assetHandler = WebViewAssetHandler(router: router())
        assetHandler.setAssetPath(configuration.appLocation.path)
        assetHandler.setServerUrl(configuration.serverURL)
        
        // 之前我们也提到了,在 WebViewDelegationHandler 中定义了 js 与 Native 的交互
        // 这个 delegationHandler 也作为参数传递到了 WKWebview 和 CapacitorBridge 中
        let delegationHandler = WebViewDelegationHandler()
        
        // 对webview做一些初始化配置
        prepareWebView(with: configuration, assetHandler: assetHandler, delegationHandler: delegationHandler)
        view = webView
        // create the bridge
        capacitorBridge = CapacitorBridge(with: configuration,
                                          delegate: self,
                                          cordovaConfiguration: configDescriptor.cordovaConfiguration,
                                          assetHandler: assetHandler,
                                          delegationHandler: delegationHandler)
        capacitorDidLoad()
    }
    
    
extension CAPBridgeViewController {
    private func prepareWebView(with configuration: InstanceConfiguration, assetHandler: WebViewAssetHandler, delegationHandler: WebViewDelegationHandler) {
        ...
        // set our delegates
        aWebView.uiDelegate = delegationHandler
        aWebView.navigationDelegate = delegationHandler
    }
}
2.2.2 WebViewDelegationHandler

WebViewDelegationHandler 继承了多个代理类,主要有 WKNavigationDelegate, WKUIDelegate, WKScriptMessageHandler
WKNavigationDelegate 用于页面加载时的一些生命周期方法;
WKUIDelegate 用于复写js原生的 alert、confirm、prompt 等方法;
WKScriptMessageHandler 则用于接收 window.webkit.messageHandlers.*.postMessage 的消息。
可以看到,在 userContentController didReceive 方法中,根据消息的内容,通过 CapacitorBridge 分别调用Capacitor 插件和 Cordova 插件。
部分代码如下:

import Foundation
import WebKit

// adopting a public protocol in an internal class is by design
// swiftlint:disable lower_acl_than_parent
@objc(CAPWebViewDelegationHandler)
internal class WebViewDelegationHandler: NSObject, WKNavigationDelegate, WKUIDelegate, WKScriptMessageHandler {
    weak var bridge: CapacitorBridge?
    fileprivate(set) var contentController = WKUserContentController()
    enum WebViewLoadingState {
        case unloaded
        case initialLoad(isOpaque: Bool)
        case subsequentLoad
    }
    fileprivate(set) var webViewLoadingState = WebViewLoadingState.unloaded

    private let handlerName = "bridge"

    init(bridge: CapacitorBridge? = nil) {
        super.init()
        self.bridge = bridge
        // 这里 add 后,就可以在js中使用 window.webkit.messageHandlers.bridge.postMessage
        contentController.add(self, name: handlerName)
    }

    func cleanUp() {
        contentController.removeScriptMessageHandler(forName: handlerName)
    }

    // MARK: - WKNavigationDelegate
    // 判断是否放行请求
    public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
        ...
    }

    // MARK: - WKScriptMessageHandler

    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        guard let bridge = bridge else {
            return
        }

        let body = message.body
        if let dict = body as? [String: Any] {
            let type = dict["type"] as? String ?? ""

            if type == "js.error" {
                if let error = dict["error"] as? [String: Any] {
                    logJSError(error)
                }
            } else if type == "message" {
                let pluginId = dict["pluginId"] as? String ?? ""
                let method = dict["methodName"] as? String ?? ""
                let callbackId = dict["callbackId"] as? String ?? ""

                let options = dict["options"] as? [String: Any] ?? [:]

                if pluginId != "Console" {
                    CAPLog.print("⚡️  To Native -> ", pluginId, method, callbackId)
                }

                bridge.handleJSCall(call: JSCall(options: options, pluginId: pluginId, method: method, callbackId: callbackId))
            } else if type == "cordova" {
                let pluginId = dict["service"] as? String ?? ""
                let method = dict["action"] as? String ?? ""
                let callbackId = dict["callbackId"] as? String ?? ""

                let args = dict["actionArgs"] as? Array ?? []
                let options = ["options": args]

                CAPLog.print("To Native Cordova -> ", pluginId, method, callbackId, options)

                bridge.handleCordovaJSCall(call: JSCall(options: options, pluginId: pluginId, method: method, callbackId: callbackId))
            }
        }
    }

    // MARK: - WKUIDelegate
    // 使用 window.alert() 需实现此方法
    public func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) {
        ...
    }
    // 使用 window.confirm() 需实现此方法
    public func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (Bool) -> Void) {
        ...
    }
    // 使用 window.prompt() 需实现此方法
    public func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt prompt: String, defaultText: String?, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (String?) -> Void) {
        ...
    }

    public func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
        if let url = navigationAction.request.url {
            UIApplication.shared.open(url, options: [:], completionHandler: nil)
        }
        return nil
    }
}
2.2.3 CapacitorBridge

init
首先看下初始化方法,主要是一些变量的初始化,以及插件等到初始化。
exportCoreJS:将 window.Capacitor、window.WEBVIEW_SERVER_URL、native-bridge.js 注入到 js;
registerPlugins:将Capacitor插件变量和插件方法注入到 js;
setupCordovaCompatibility:将 cordova.js、cordova_plugins.js、插件js,注入到js

init(with configuration: InstanceConfiguration, delegate bridgeDelegate: CAPBridgeDelegate, cordovaConfiguration: CDVConfigParser, assetHandler: WebViewAssetHandler, delegationHandler: WebViewDelegationHandler, autoRegisterPlugins: Bool = true) {
        self.bridgeDelegate = bridgeDelegate
        self.webViewAssetHandler = assetHandler
        self.webViewDelegationHandler = delegationHandler
        self.config = configuration
        self.cordovaParser = cordovaConfiguration
        self.notificationRouter = NotificationRouter()
        self.notificationRouter.handleApplicationNotifications = configuration.handleApplicationNotifications
        self.autoRegisterPlugins = autoRegisterPlugins
        super.init()

        self.webViewDelegationHandler.bridge = self
        // 将 window.Capacitor、window.WEBVIEW_SERVER_URL、native-bridge.js 注入到 js
        exportCoreJS(localUrl: configuration.localURL.absoluteString)
        // Capacitor插件注入到js
        registerPlugins()
        // 将 cordova.js、cordova_plugins.js、插件js,注入到js
        setupCordovaCompatibility()
        observers.append(NotificationCenter.default.addObserver(forName: type(of: self).tmpVCAppeared.name, object: .none, queue: .none) { [weak self] _ in
            self?.tmpWindow = nil
        })

        self.setupWebDebugging(configuration: configuration)
    }

handleJSCall
此方法用来调用Capacitor 插件的接口。
通过 pluginId 定位到指定插件 plugin,通过方法名获取 Selector,使用plugin.perform(selector, with: pluginCall) 执行插件的方法。

/**
     Handle a call from JavaScript. First, find the corresponding plugin, construct a selector,
     and perform that selector on the plugin instance.

     Quiet the length warning because we don't want to refactor the function at this time.
     */
    // swiftlint:disable:next function_body_length
    func handleJSCall(call: JSCall) {
        let load = {
            NSClassFromString(call.pluginId)
                .flatMap { $0 as? CAPPlugin.Type }
                .flatMap(self.loadPlugin(type:))
        }

        guard let plugin = plugins[call.pluginId] ?? load() else {
            CAPLog.print("⚡️  Error loading plugin \(call.pluginId) for call. Check that the pluginId is correct")
            return
        }

        let selector: Selector
        if call.method == "addListener" || call.method == "removeListener" {
            selector = NSSelectorFromString(call.method + ":")
        } else {
            guard let method = plugin.getMethod(named: call.method) else {
                CAPLog.print("⚡️  Error calling method \(call.method) on plugin \(call.pluginId): No method found.")
                CAPLog.print("⚡️  Ensure plugin method exists and uses @objc in its declaration, and has been defined")
                return
            }

            selector = method.selector
        }

        // 确认插件可调用此selector
        if !plugin.responds(to: selector) {
            // we don't want to break up string literals
            // swiftlint:disable line_length
            CAPLog.print("⚡️  Error: Plugin \(plugin.getId()) does not respond to method call \"\(call.method)\" using selector \"\(selector)\".")
            CAPLog.print("⚡️  Ensure plugin method exists, uses @objc in its declaration, and arguments match selector without callbacks in CAP_PLUGIN_METHOD.")
            CAPLog.print("⚡️  Learn more: \(docLink(DocLinks.CAPPluginMethodSelector.rawValue))")
            // swiftlint:enable line_length
            return
        }

        // Create a plugin call object and handle the success/error callbacks
        dispatchQueue.async { [weak self] in
            // let startTime = CFAbsoluteTimeGetCurrent()

            let pluginCall = CAPPluginCall(callbackId: call.callbackId,
                                           options: JSTypes.coerceDictionaryToJSObject(call.options,
                                                                                       formattingDatesAsStrings: plugin.shouldStringifyDatesInCalls) ?? [:],
                                           success: {(result: CAPPluginCallResult?, pluginCall: CAPPluginCall?) -> Void in
                                            if let result = result {
                                                self?.toJs(result: JSResult(call: call, callResult: result), save: pluginCall?.keepAlive ?? false)
                                            } else {
                                                self?.toJs(result: JSResult(call: call, result: .dictionary([:])), save: pluginCall?.keepAlive ?? false)
                                            }
                                           }, error: {(error: CAPPluginCallError?) -> Void in
                                            if let error = error {
                                                self?.toJsError(error: JSResultError(call: call, callError: error))
                                            } else {
                                                self?.toJsError(error: JSResultError(call: call,
                                                                                     errorMessage: "",
                                                                                     errorDescription: "",
                                                                                     errorCode: nil,
                                                                                     result: .dictionary([:])))
                                            }
                                           })

            if let pluginCall = pluginCall {
                plugin.perform(selector, with: pluginCall)
                if pluginCall.keepAlive {
                    self?.saveCall(pluginCall)
                }
            }

            // let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime
            // CAPLog.print("Native call took", timeElapsed)
        }
    }

handleCordovaJSCall
此方法用来调用Cordova 插件的接口。由于Capacitor和Cordova的插件参数和回调还是有区别的,所以也是分成两个方法,不过原理大致一致。
通过 CordovaPluginManager 和 插件id 获取插件变量,通过方法名获取 selector,最终也是通过 plugin.perform(selector, with: pluginCall) 执行插件中定义的方法。

func handleCordovaJSCall(call: JSCall) {
        // Create a selector to send to the plugin

        if let plugin = self.cordovaPluginManager?.getCommandInstance(call.pluginId.lowercased()) {
            let selector = NSSelectorFromString("\(call.method):")
            if !plugin.responds(to: selector) {
                CAPLog.print("Error: Plugin \(plugin.className ?? "") does not respond to method call \(selector).")
                CAPLog.print("Ensure plugin method exists and uses @objc in its declaration")
                return
            }

            let arguments: [Any] = call.options["options"] as? [Any] ?? []
            let pluginCall = CDVInvokedUrlCommand(arguments: arguments,
                                                  callbackId: call.callbackId,
                                                  className: plugin.className,
                                                  methodName: call.method)
            plugin.perform(selector, with: pluginCall)

        } else {
            CAPLog.print("Error: Cordova Plugin mapping not found")
            return
        }
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值