自定义原生Android代码时, 遇到的错误

文档链接->https://capacitorjs.com/docs/android/custom-code

文档中出问题的步骤如图:

所报错如图:

方便搜索错误而写(仅用于搜索, 请在阅读时忽略)
Method does not override method from its superclass

Cannot resolve symbol ‘Bundle’

‘onCreate(android.os.Bundle)’ in ‘com.getcapacitor.BridgeActivity’ cannot be applied to ‘(Bundle)’

Method ‘onCreate(Bundle)’ is never used

由报错图可知, 此错误是由于官方文档的不严谨造成的, 解决方式, 如图:

ok, 我是在quasar框架下使用的, 后续在utils中创建了官方提示的绑定代码, 然后在项目中(如vue单文件组件)做了引入调用, 成功执行。

在quasar中使用的时候, 注意capacitor的node_modules的相对路径, import { registerPlugin } from '../../src-capacitor/node_modules/@capacitor/core'

通过自定义原生代码, 来调用二进制文件

我的需求是, 在安卓或ios应用中, 执行使用go语言代码交叉编译至arm64移动平台的二进制文件。

因此我需要通过自定义原生代码来实现对二进制文件的执行调用, 然后映射到前端的typeScript中, 在前端的runtime中做符合其生命周期的调用。

我决定从安卓开始适配, 然后ios, 不过经过昨天一天的验证, 发现这件事情太折腾, 且一定程度上对于上架商店具有风险性(谷歌或苹果官方貌似并不太喜欢应用程序调用进程以外的二进制程序, 特别是ios的严苛程序, 甚至可能根本行不通), 因此即使全部跑通, 也能预测到后续的折腾程度并不会太轻, 违背了低成本独立开发的初衷, 因此对于非团队来说, 本次是一个失败的尝试(但是, 这件事情是确实可行的(但需要用户对手机开头一些权限(如开发者模式下的无线调试权限)))。

如果你也像我一样, 没接触过Android开发, 但又想快速调试Android代码, 可以移步查看我本次折腾中遇到的一些坑

本次折腾中, 遇到的一些坑

首先, 介绍下本次折腾的结果

  • 成功将go语言交叉编译的二进制文件, 通过安卓调试桥(adb)在安卓中执行(其中起了一个gin服务, 并且使用了gorm和gen, 数据库用的sqlite, sqlite的驱动用的go驱动——<因为用c_go的驱动需要在linux上执行交叉编译, win下的gcc的posix版本并支持完整线程模型和Linux系统调用(Cygwin这种只是c/c++初学者的套壳游戏, 并不能用作真正的交叉编译)>)
  • 明确了安卓调试桥(adb)的作用, 并不是像Linux的bash那样在自身进行操作的, 而是必须通过外部使用adb工具来对所需操作设备进行链接(usb或网络), 然后才能进一步执行命令。(不过可以通过dadb的方式来自己链接自己从而实现对自身进行操作——<虽然不需要root权限, 但却还是需要用户开启自身手机开发者选项下的远程调试功能>)

    也因此, 我称之为一次失败的尝试。

  • 明确了无法通过 java中的 Runtime.getRuntime().exec() 方法, 来调用二进制文件的运行(第安卓版本可以, 目前失败是因为权限问题, 也就是技术上可行, 但实际上是不受android高版本非root系统的权限允许的)。具体这篇文章介绍的比较详细Runtime.exec()执行shell、cmd命令常见陷阱与完善不过这篇文章并不是针对android平台的, 也因此遗漏了安卓中的权限问题。

下面, 介绍下身为小白, 需要了解的一些安卓开发的基本常识:

  • 目录窗格的左上方, 可以选择目录模式, 正常模式下为Android, 但是此模式下会隐藏很多文件, 因此我们在必要的时候需要使用Project模式。如下图:

  • 程序运行后, 我们需要进入安卓系统内的调试输出窗口(里面有我们代码中的log信息以及一些print之类的打印信息, 以及报错信息), 可通过如图所示进入:

  • 可在android studio内的终端, 或任意终端中, 使用adb命令调试开发虚拟机, 我们可以用adb devices查看链接设备, adb shell进行链接设备,此后, 就可以使用如nohup之类的命令对二进制文件进行后台启动等之类的类linux的bash命令了。 具体常用的自行搜索adb命令即可。 (需要注意的是, 对于开发虚拟机, 我们进入后可直接通过su命令以获得root权限, 从而查看一切目录——<后面介绍libs和jniLibs时会用到>)

  • 明确了安卓中, 使用动态库时的二进制目标路径规则, 我们需要按照不同cpu架构来编译二进制文件, 还需要放入对应的正确名称的文件夹, android中把此类文件夹称为ABI, 目前常见的 ABI 有如下几种(其中最常用的四个已被我标明):

    • armeabi:第 5 代 ARM v5TE,使用软件浮点运算,兼容所有 ARM 设备,通用性强,速度慢(只支持 armeabi)。
    • armeabi-v7a(相当与go交叉编译时的arm):第 7 代 ARM v7,使用硬件浮点运算,具有高级扩展功能(支持 armeabi 和 armeabi-v7a)。
    • arm64-v8a(相当与go交叉编译时的arm64):第 8 代,64 位,包含 AArch32、AArch64 两个执行状态对应 32-bit 和 64-bit(支持 armeabi-v7a、armeabi 和 arm64-v8a)。
    • x86(相当与go交叉编译时的386):Intel 32 位,一般用于平板(支持 x86 和 armeabi 但性能有所损耗)。
    • x86_64(相当与go交叉编译时的amd64):Intel 64 位,一般用于平板(支持 x86 和 x86_64)。
    • mips:32 位,支持 RISC。
    • mips64:64 位,支持 RISC。

      参考链接:『Android Studio』如何导入 SO 库

  • 在android studio内的view窗口, 查看虚拟机内的文件系统, 打开方式如下图:

  • 明确了安卓项目中, libs目录(与src目录以及build.gradle文件同级别)下, 对应ABI下的.so文件会到达的android系统的位置(一般为data/data/包名下/lib路径)。

    可通过终端adb 或 view窗口查看等方式确认路径是否正确。

    此方式需要修改build.gradle文件内容, 需要在其中添加如下内容:

    1
    2
    3
    4
    5
    6
    7
    8
    android {
    // ...
    sourceSets {
    main {
    jniLibs.srcDirs = ['libs']
    }
    }
    }

    作用是将libs目录指定为jniLibs文件夹。不过新版本的的安卓内, 还可以主动为so库创建专门的jniLibs目录, 路径为/src/main/jniLibs(与java目录同级别)。

  • 明确了安卓项目中, jniLibs目录(与java目录同级)下, 对应的ABI下的.so文件会到达的android系统的位置(一般为data/app/...路径下——<路径中...表示从此处开始的目录名, 是被混淆过的目录名, 每个目录名都对应一个ABI的名称>)

    我们可通过代码获取app所在系统中, 实际的符合其cpu架构的.so路径(也因此, 我们的jniLibs目录中, 需要尽可能的补全同一个二进制文件, 所有ABI下对应的交叉编译版本)

    1
    2
    3
    ApplicationInfo ai = getApplicationInfo();
    String libPath = ai.nativeLibraryDir;
    // libPath即为对应的路径字符串, 后续使用时可组合对应的二进制文件名称

    需要注意的是,如果你的 minimum SDK 版本在 23 以上,你需要在 AndroidManifest.xml 的 中定义:

    1
    android:extractNativeLibs="true"

    android:extractNativeLibs 的作用是告诉系统在安装时是否提取 so 文件到 “/data/app/” 中的程序目录下面,在 API 23 之后 android:extractNativeLibs 的默认值为 false,所有的 so 将从 apk 中加载,以节省应用安装后的空间,但是由于我们加入的并不是真正的 so 文件,调用采用绝对路径来执行可执行文件,所以必须将 android:extractNativeLibs 设置为 true,以保证它能被提取到 apk 之外,以便我们正常调用。

  • 明确了capacitor所生成的android目录, 是与android studio中的项目为同步以一个项目。 (若是删除的话, 也会重新自动生成一个, 不过若是你所删除掉的目录中, 有你自定义的文件或修改的话, 则无法恢复, 因此若你对原生代码有编写需求, 最好不要忽略android目录中的特定文件, 需要把它们也给纳入你的版本管理中。)

    明确了此项后, 我决定尝试下GoBind的方式, 以在移动端的js中来间接调用go函数, 将交叉编译的二进制文件, 改为编译成真正的sdk。具体方法如此链接https://github.com/golang/go/wiki/Mobile#sdk-applications-and-generating-bindings

参考链接:

打包构建所踩的坑

我以往的经验除了服务端开发, 也就用qt做过以下桌面应用, 对于移动端的打包还是没有经验的, 也因此折腾了一下午。

安卓端

第一阶段

按照quasar官网下载android studio, 配置环境变量。 此处不过多赘述。

需要注意的是, 对于下图中所述的第三条命令, 最好通过手动的方式在ui窗口设置, 否则会覆盖掉你系统中用户变量下原有的path的已配置变量。

开发命令正常, 成功通过android studio启动了我的套壳app, 并且可通过浏览器的远程调试功能edge://inspect/#devices进行调试。

此处参考链接https://capacitorjs.com/docs/vscode/debugging#use-chrome-inspect

开发体验的话, 仅web端是可以热更新的。 但若涉及到原生代码的更改, 则需要在Android stdio重新启动项目, 以及重新链接浏览器的调试功能。

第二阶段

对于java的环境上我踩了一个坑, 就是说, dev时可以正常使用, 但是到了build时, 就出问题了

一开始的build问题是 JAVA_HOME 找不到。 我以为是没有装java环境的原因, 还因此注册了oracle帐号下载了java8的开发环境, 然后安装了开发环境, 并配置了其环境变量。

然后悲剧了, 直接报错了, 下面是大致的报错内容:

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
FAILURE: Build failed with an exception.

* What went wrong:
A problem occurred configuring root project 'android'.
> Could not resolve all files for configuration ':classpath'.
> Could not resolve com.android.tools.build:gradle:8.0.0.
Required by:
project :
> No matching variant of com.android.tools.build:gradle:8.0.0 was found. The consumer was configured to find a library for use during runtime, compatible with Java 8, packaged as a jar, and its dependencies declared externally, as well as attribute 'org.gradle.plugin.api-version' with value '8.0.2' but:
- Variant 'apiElements' capability com.android.tools.build:gradle:8.0.0 declares a library, packaged as a jar, and its dependencies declared externally:
- Incompatible because this component declares a component for use during compile-time, compatible with Java 11 and the consumer needed a component for use during
runtime, compatible with Java 8
- Other compatible attribute:
- Doesn't say anything about org.gradle.plugin.api-version (required '8.0.2')
- Variant 'javadocElements' capability com.android.tools.build:gradle:8.0.0 declares a component for use during runtime, and its dependencies declared externally:
- Incompatible because this component declares documentation and the consumer needed a library
- Other compatible attributes:
- Doesn't say anything about its target Java version (required compatibility with Java 8)
- Doesn't say anything about its elements (required them packaged as a jar)
- Doesn't say anything about org.gradle.plugin.api-version (required '8.0.2')
- Variant 'runtimeElements' capability com.android.tools.build:gradle:8.0.0 declares a library for use during runtime, packaged as a jar, and its dependencies declared externally:
- Incompatible because this component declares a component, compatible with Java 11 and the consumer needed a component, compatible with Java 8
- Other compatible attribute:
- Doesn't say anything about org.gradle.plugin.api-version (required '8.0.2')
- Variant 'sourcesElements' capability com.android.tools.build:gradle:8.0.0 declares a component for use during runtime, and its dependencies declared externally:
- Incompatible because this component declares documentation and the consumer needed a library
- Other compatible attributes:
- Doesn't say anything about its target Java version (required compatibility with Java 8)
- Doesn't say anything about its elements (required them packaged as a jar)
- Doesn't say anything about org.gradle.plugin.api-version (required '8.0.2')

* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.

* Get more help at https://help.gradle.org

BUILD FAILED in 1s

App • ⚠️ Command "./gradlew.bat" failed with exit code: 1

App • ⚠️ Gradle build failed!
App • ⚠️ As an alternative, you can use the "--ide" param and build from the IDE.

根据报错内容, 判断出我安卓打包所用的java版本, 貌似和系统版本不一致。 哈哈, 突然想到了dev环境时正常使用的Android Studio, 既然dev环境能用, 也就是说我的电脑上已经有Android Studio所自动安卓的java环境了, 而一开始命令行找不到是因为ide中的gradle设置并为在环境变量中体现, 而quasar的build依赖的直接从终端构建apk包的命令, 需要的是在环境变量中的gradle 此处的参考链接https://capacitorjs.com/docs/basics/workflow#compiling-your-native-binary

因此, 我只需要打开android studio, 查看 gradle 版本就好了(果然, quasar给默认配置的是17), 与我自行配置的java环境不一致, 这就是我第二次遇到的报错的原因了

图中ui面板的打开方式为, 在Android Studio中点击 “File” -> “Settings” -> “Build, Execution, Deployment” -> “Build Tools” -> "Gradle"

基于上述原因, 我删除了安卓java8是配置的JAVA_HOME的环境变量的value, 并将value重新配置为与Android Studio IDE 中所示的, 由IDE自动下载适配的版本。(即将图中所显示gradle路径配给了JAVA_HOME环境变量的value)

整体而言,仅下载android studio即可, 无需自行下载java开发环境, quasar文档给的完全够用, 只不过要想在终端执行构建而不是在Android studio ide中构建的话, 需要在环境变量中额外添加JAVA_HOME, 其value为Android Studio中gradle相同的路径。

第三阶段

经过第二阶段, 我的build命令总算是执行成功了, 也如愿得到了apk包。 但是, 当我在模拟器以及真机中执行安装时, 竟然安装失败了。

然后, 我反复检查前二个阶段不可能再出错后, 我把原因归结于现有安卓系统的安全限制——即不允许未签名的应用进行安装。

那么, 带着自己的假设, 我继续安照quasar文档, 准备为我的apk包执行签名。

为此, 我们需要签名所用的密钥, 当然此密钥是可以通过工具生成的, 而quasar推荐的工具是 keytool

此工具是C:\Program Files\Android\Android Studio\jbr\bin目录下的一个二进制可执行程序, 因此我们可以将此路径配置到Path环境变量的value中, 以全局使用此工具。

  • keytool -genkey -v -keystore my-release-key.keystore -alias alias_name -keyalg RSA -keysize 2048 -validity 20000 执行此命令, 以生成名字为my-release-key.keystore的密钥文件。(我是新建了一个名为key的目录, 然后cd进入此目录执行的此命令。 执行后你会看到新生成的密钥文件)

  • 需要注意的是, 每次使用此命令新生成一个密钥时, 都需要为此密钥设置一个密码(此密码与密钥绑定, 主要用于保护密钥不被盗用, 而与密钥签名无关), 还需要填写一些详细信息, 如姓名、单位等 (可以略过, 不过还是建议填一下的), 以防密钥丢失被人直接利用。 因此, 你要记住你对此密钥设置的密码, 因为当你使用此密钥时, 你需要输入你设置的密码后才可正常使用。

  • 最后, 还需要注意的是, 请妥善报错你刚刚生成的密钥文件, 因为如果你想在谷歌应用商店对你的应用不断维护更新, 那么你的签名一定要保持一致。否则, 只能通过app下架的方式, 重新使用新的密钥文件签名上传了(那么对于你的用户来说, 签名断档会造成应用无法更新——即使包名相同也无法覆盖更新)。

此时, 我们已经有一个密钥了, 那么我们后续就可以使用签名工具, 对我们的apk文件进行签名了, 这里quasar推荐的签名工具为apksigner

此工具是 Android SDK 的一部分,用于根据所提供密钥文件对apk进行签名。你可以在, 此路径下找到”C:\Users\YourUsername\AppData\Local\Android\Sdk\build-tools\YourVersion\“

至于 YourVersion, 你可以选择一个较早的版本以适配更多机型(前提是你的应用未用到高版本sdk的api)

  • 使用命令apksigner sign --ks my-release-key.keystore --ks-key-alias alias_name <path-to-unsigned-apk-file>即可完成签名。

    使用时, 注意’密钥路径’以及’apk文件路径’的正确性

不过, 在签名之前, quasar建议我们通过zipalign工具对我们的apk文件进行性能优化。

zipalign 是 Android SDK 的一部分,用于优化 APK 文件以提高应用性能。同样, 可以在此路径下找到”C:\Users\YourUsername\AppData\Local\Android\Sdk\build-tools\YourVersion\“

  • 使用命令zipalign -v 4 <path-to-same-apk-file> HelloWorld.apk即可完成优化

    使用时, 注意原apk路径在前面, 新生成的路径在后面, 如: zipalign -v 4 原apk包带包名的路径 优化后新生成的apk包带包名的路径, 新生成的包的名称可以与原包名不一致。

    也可通过-f直接覆盖原包文件生成(不常用, 因为我不推荐用哈哈)

  • 粘一段gpt4对此命令的解释:

    zipalign 的基本命令格式如下:

    1
    zipalign [-f] [-v] <alignment> infile.apk outfile.apk

    在这个命令中¹³:

    • -f:覆盖现有的 outfile.apk
    • -v:详细输出。
    • <alignment>:对齐的字节数,例如 ‘4’ 提供 32 位对齐。
    • infile.apk:源文件。
    • outfile.apk:输出文件。

    例如,如果你想要对齐 myApp.apk 并将结果保存为 myAppAligned.apk,你可以使用以下命令:

    1
    zipalign -v 4 myApp.apk myAppAligned.apk

到此为止, 签名成功, 得到了签名后的最新apk文件。

此时再次对最新此apk文件进行安装, 发现已经可以安装成功并正常运行了。

关于版本号统一管理的同步问题

对于安卓的版本号, 我们需要在src-capacitor/android/app/build.gradle中, 手动更改其中的versionCodeversionName两个字段。

tips: versionCode 必须是数字, 而且必须是整数类型<我们一般每次给它+1就好了>。 versionName 可以是任意字符串, 这也是用户查看app详细信息时可以直接看到的字符串内容(一般将其与package.json中的version同步更改)。

对于自动同步package.json 与 原生配置文件版本号的功能, 不应由quasar的配置文件来处理, 其问题应属于capacitor的范畴, 以下是相关问题及对应解决办法:
[https://github.com/ionic-team/capacitor/issues/3943]

推荐使用问题中所提到的 这个脚本 https://github.com/arzyu/capacitor-sync-version?tab=readme-ov-file

因其简单粗暴且刚好可以满足我们的需求。需要注意的是:

  • 其依赖的capacitor hooks, 是仅在build时才会触发的。(也就是说, 平时的dev模式下, 不会触发(除非我们手动执行<手动执行时没必要执行capacitor,直接执行我们自己的对应脚本即可>)
  • 其安装位置, 以及钩子的配置位置, 是在src-capacitor下的这个package.json。

以上虽然可能已经失效了, 但是我们可以自己做相关脚本, 然后通过自己的make工作流中实现同样的目的。(复用其在build.gradle文件中的相关代码即可)

不过,如果能在quasar工作流中实现, 那将是更好的。

尝试通过GoBind的方式, 用go给移动端写sdk

由于本小节, 涉及go语言的篇幅较多, 不太适合完全放在此处, 请移步此链接查看通过gomobile为移动端<android、ios>编写sdk

对于从原生代码调用二进制文件的方式, 优势不止一点

sdk的方式 调用二进制文件的方式
可以在原生语言内, 通过线程来执行其它语言的sdk, 就和调用自身语言是一样的。 二进制程序只能作为独立的进程被执行, 是多进程的调用, 资源消耗更大, 且在移动平台的严苛权限要求下, 此方式是不合规的

最终, 要想实现与原有二进制文件相同的作用, 只需要将整个程序入口作为函数来打包sdk就好了, 这里由于我的二进制是一个服务, 因此我采取新线程的方式执行它:

  • android中:

    1
    2
    3
    4
    5
    6
    7
    Thread thread = new Thread(new Runnable() {
    @Override
    public void run() {
    // 在这里调用我们打包的sdk入口函数
    }
    });
    thread.start();

    那么到此为止, 对于capacitor混开android逻辑以及级别配置方面的东西如签名等, 已经是全部跑通了, 接下来就剩发布应用商店了。

    这里粘一下目前位置, 准备的业务逻辑的位置图: