Python打包安卓APK实战:python-for-android构建与交叉编译指南
简介一套能将 Python 程序打包成独立 Android APK 的完整工程源码适合移动开发者以及想把 Kivy 等 Python 项目迁移到安卓平台的工程师。它基于 python-for-android 工具链支持多种引导程序既能编译 Python 解释器与依赖库也能产出可自定义应用名、图标和代码的独立安卓工程。资源包共 588 个文件大小仅 1.87MB文件类型以 265 个 py 源码、94 个 patch 补丁、29 个 java 接口为主配合 xml 配置、rst 文档和 mk 构建脚本基本覆盖打包流程的关键环节。目前已有 756 人学习适合希望避开 Android 原生开发、快速把 Python 应用上架应用商店的开发者。借助这套工程能够理解 python-for-android 两阶段运作机制直接拿到可安装的 APK 项目骨架并灵活拓展组件与后端支持。1. 为什么要把 Python 打包成安卓 APK做过移动端工具的人都会撞到同一个问题Python 写后台逻辑很顺手但到安卓上就没了原生解释器。以前只能在服务器上跑脚本或者用 Termux 这种模拟环境凑合想分发给别人安装几乎不可能。python-for-android 的价值在于它把 CPython 解释器、依赖库和你的 .py 文件一起编进一个标准 Android 工程里最终产出一个能上架、能安装、能卸载的独立 APK不需要用户在手机上装任何 Python 运行时。这个流程最早是给 Kivy 图形框架设计的现在也支持 SDL2、pygame 等其他引导程序。你拿到手的源码包里有 make.bat、gradlew.bat、biglink、glob.c、start.c、_android_sound_jni.c、_android_jni.c、pyjniusjni.c、_android_billing_jni.c 这些组成构建链的底层文件说明它不是个黑盒安装器而是一套可以按需裁剪的编译流水线。适合谁用写过 Python 工具、想把它变成安卓 App 的开发者以及想理解 Android Native 层和 Python 之间 JNI 桥接原理的人。2. python-for-android 的构建链从解释器编译到发行版2.1 两个核心阶段发行版构建与 APK 生成python-for-android 把工作拆成两个阶段。第一个阶段是构建一个“发行版”把 CPython 源码交叉编译成 Android 能跑的 .so再把你指定的 Python 依赖比如 requests、numpy用同样的交叉编译环境编进去最后连同启动引导代码一起组装成一个完整的 Android Gradle 工程。这个发行版是独立的可以反复使用——同一个发行版可以生成不同包名、图标、版本号的 APK只要你的 Python 代码变了不需要重新编译解释器。第二阶段是对外暴露的简单接口。你只要告诉它“我的代码在哪、要装哪些 pip 包、用什么启动模式”它就会基于已有的发行版生成 APK。源码包里的gradlew.bat就是 Gradle 构建入口而start.c是 Python 解释器在 Android 上的 C 启动入口里面处理了sys.path的初始化、环境变量和入口脚本的调用。// start.c 中的关键逻辑简化示意 int main(int argc, char **argv) { // 1. 设置 HOME 路径到应用私有目录 setenv(HOME, get_app_root(), 1); // 2. 设置 PYTHONHOME 指向 lib/python3.x setenv(PYTHONHOME, get_python_home(), 1); // 3. 调用 CPython 的 Py_Main执行 main.py return Py_Main(argc, argv); }这里需要知道Android 上没有/usr/bin/python所有环境变量、动态库路径都必须由宿主 App 提供。start.c先通过setenv把 Python 的查找路径指到 APK 解压后的私有目录再调用Py_Main。如果不理解这层你改了入口脚本名或在子目录里放模块却报ModuleNotFoundError就不知道去哪查。2.2 引导模式选型SDL2 与 Kivy 的区别引导程序定义了 APK 启动时如何初始化显示和事件循环。源码包里出现的_android_sound_jni.c、_android_billing_jni.c暗示它会涉及声音和计费接口这属于 SDL2 引导程序之外的独立 JNI 模块。常见选择有两个sdl2适合 Kivy、Pygame 或纯后台服务。SDL2 负责窗口和输入Kivy 在它上层画 UI。webview如果你用 HTML/JS 做 UI只把 Python 当后端逻辑可以选这个模式体积更小。选型不是拍脑袋。上架应用市场时包体积直接影响审核体验SDl2 引导会增加约 20MB 的 .so而纯 Python 服务模式可以去掉所有图形依赖。我一般先把功能跑通再回头精简buildozer.spec里的requirements把用不到的大包删掉。2.3 构建命令与发行版缓存最常见的构建工具是 buildozer它封装了 python-for-android 的复杂参数。先看一个标准的buildozer.spec关键片段[app] title My Python App package.name myapp package.domain com.example source.dir . requirements python3,kivy,requests orientation portrait fullscreen 0 [buildozer] log_level 2 warn_on_root 1参数说明requirements必填。第一个必须是python3后面是你需要的 pip 包名。python-for-android 只支持编译有预编译配方或能交叉编译的包纯 Python 包通常直接拷进去。source.dir你的 Python 项目目录最终会作为assets打进 APK。package.name和package.domain共同组成应用的唯一 ID上架后不可更改。执行构建命令# 首次构建会自动下载 NDK、SDK 和 python-for-android buildozer android debug构建耗时取决于机器和依赖数量首次一般在 10-30 分钟。发行版会缓存在~/.local/share/python-for-android下第二次构建只重新打包 APK速度会快很多。如果源码包里有现成的make.bat说明项目也支持 Windows 下的批处理构建——在 Windows 上你需要预先装好 Android NDK 并设置ANDROID_NDK_HOME环境变量然后直接运行make.bat即可走同一套流程。3. 交叉编译环境NDK、编译器和 JNI 桥接层3.1 NDK 版本与架构参数Python 解释器不是写一遍就能在所有安卓设备上跑它需要针对不同 CPU 架构分别编译出 .so 文件。现代 Android 设备主要使用arm64-v8a老一点的还有armeabi-v7a模拟器可能用x86_64。python-for-android 默认打所有架构的包但实际分发时建议只保留arm64-v8aAPK 体积能小三分之一。错误做法是随便拿一个 NDK 就开始编。python-for-android 对 NDK 版本有严格校验版本过新会触发clang: error: unknown argument版本太旧又编不出 Python 3.11 需要的特性。建议用 r25b 或 r21e这两个是社区验证过与各个配方兼容性最好的版本。配置方式写在环境变量里export ANDROIDSDK$HOME/Android/Sdk export ANDROIDNDK$HOME/Android/android-ndk-r25b export ANDROIDAPI30 export ANDROIDNDKVER25b变量含义ANDROIDAPI目标 API 级别。30 对应 Android 11一般不要低于 24否则 Android 7 以下设备的兼容性会有问题。ANDROIDNDKVERNDK 版本标识python-for-android 会根据它在内部路径中查找编译器。Windows 下运行时make.bat会读取这些环境变量并调用gradlew.bat编译最终的 APK。注意 Windows 上路径不能带空格NDK 放到纯英文目录下是第一步。3.2 JNI 层的职责与源码文件映射Python 代码不能直接调用 Android 系统 API中间需要 JNI 做桥接。源码包里的_android_jni.c和pyjniusjni.c就是干这个的。前者封装了 Android 的Context、Activity生命周期接口后者是 pyjnius 库的本地实现让 Python 里可以这样写from jnius import autoclass Intent autoclass(android.content.Intent) context autoclass(org.kivy.android.PythonActivity).mActivitypyjniusjni.c里做了几件事加载 JavaVM、注册 Native 方法、把 Python 的字符串参数转成 jstring。常见坑是多个 .so 之间通过全局变量共享 JavaVM 指针如果加载顺序乱了会出现JNI_GET_CREATED_JAVAVMS找不到的崩溃。解决办法是在Application.attachBaseContext里先加载主 JNI 库再加载其他库顺序不能反。构建时这些 C 文件会通过 Android 的 CMake 或 NDK 工具链编成 libpython 和 libpyjnius。下面的命令演示手动编译单个 JNI 文件方便调试$ANDROIDNDK/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi21-clang \ -shared -fPIC -o libmyjni.so _android_jni.c \ -I$ANDROIDNDK/sysroot/usr/include -L$ANDROIDNDK/sysroot/usr/lib/arm-linux-androideabi这里-I指定系统头文件-L链接库路径。手动编译通常只用于排查语法或链接错误日常构建不需要你直接执行这些命令了解它有助于解决undefined reference to JNI_OnLoad这类问题。3.3 链接器 biglink 的作用Android 的 .so 加载器有个限制传统符号重定位在 API 23 以下会有性能问题python-for-android 在构建多模块 Python 扩展时会用biglink工具把所有.o文件合并成一个大的共享库避免动态加载过多小 .so。源码里的biglink就是这样一个脚本它把几十个编译单元链接进单一libpython或libmain。这带来的影响是如果你往项目里塞了带 C 扩展的第三方包比如pydensecrf它可能没有现成配方编译时链接器报符号缺失。此时要么改用官方配方要么看biglink生成的 map 文件里缺哪个符号再去源码里找对应 C 文件补进start.c的依赖列表。4. 实战打包Python 项目变成独立 APK4.1 项目目录结构一个能成功打包的 Python 项目目录结构必须有规矩myapp/ ├── main.py # 入口必须要存在 ├── mylib/ # 业务逻辑包 │ └── __init__.py ├── assets/ # 静态资源 ├── requirements.txt # 非必须buildozer 会按 spec 安装 └── buildozer.spec # 构建配置源码包里没有main.py那就要在source.dir指定的目录中自己创建。python-for-android 会去source.dir下找main.py或你在 spec 里指定的entrypoint作为sys.argv[0]。如果入口写的是if __name__ __main__在 Android 上也能正常执行因为start.c最终调用的方式等同于命令行执行。一个最容易犯的错把main.py放在子目录里然后在 spec 里写source.dir myapp但入口模块里用了相对路径读取同目录的文件。打包后当前工作目录不是你想象的项目根目录而是 APK 的解压目录。稳妥做法是用以下方式取路径import os import sys def app_root(): # Android 私有存储路径不是 os.getcwd() if hasattr(sys, _MEIPASS): return sys._MEIPASS return os.path.dirname(os.path.abspath(__file__))可以看到打包环境下__file__比相对路径可靠得多。如果你用res或assets目录记得在 spec 里加source.include_exts py,png,jpg,kv,ttf显式声明打包进去的文件后缀不然资源文件会被漏掉。4.2 用 buildozer 编译出第一个 APK在干净的 Ubuntu 20.04或 WSL2上依次执行pip install --user buildozer cython buildozer initbuildozer init会生成默认的 buildozer.spec。然后手动编辑 spec按你的项目情况改title、package.name、requirements。注意requirements里除了python3至少要有kivy或sdl2除非你只做后台服务且选了service引导模式。构建命令buildozer -v android debug-v是详细模式能看到每一步在跑什么。成功后会生成bin/myapp-0.1-arm64-v8a-debug.apk。直接安装adb install bin/myapp-0.1-arm64-v8a-debug.apk安装后立刻打开看有没有闪退。adb logcat是排错的第一工具adb logcat -s python:V ActivityManager:E错误类型日志关键词常见原因找不到模块ImportError: No module named xrequirements 没写或纯 Python 包未被打包JNI 崩溃JNI DETECTED ERROR IN APPLICATIONJNI 函数签名不匹配版本过旧This app isnt compatible with your deviceminSdkVersion 设置太高符号缺失cannot locate symbol PyInt_AsLongPython 2/3 不匹配如果在构建阶段报python-for-android: error: invalid bootstrap多半是 spec 里bootstrap kivy但 requirements 没写kivy。一个原则bootstrap 名称必须在 requirements 中有对应库。4.3 定制包名、图标与应用市场上传buildozer.spec里有一组[app]参数直接对应 APK 元信息spec 参数示例值说明package.namemyappAndroid 应用名称标识package.domaincom.example反向域名最终包名是 domain nametitle我的 Python 应用桌面显示名icon.filename%SOURCE_DIR%/icon.png必须 PNG建议 512x512presplash.filenameloading.png启动第一帧画面实测对冷启动体验影响很大orientationportrait锁定竖屏不写可能被平板拉伸注意package.domain在首次构建后改动会导致 APK 被视为不同应用旧版本无法覆盖安装。上传到应用市场前需要用android release构建签名版buildozer android release这一步会提示你输入 keystore 路径和密码。没有 keystore 的话先创建keytool -genkey -v -keystore myapp.keystore -alias myapp -keyalg RSA -keysize 2048 -validity 10000然后把 keystore 路径写进buildozer.spec的p4a.local_sdk或[app]的sign.keystore字段。这里的核心点是debug APK 和 release APK 的签名不同直接改名上架会被系统拒绝必须走 release 构建流程。5. 进阶技巧用私有配方处理 C 扩展与体积瘦身当 requirements 里某个包没有官方配方时你会看到No such recipe的报错。这个包如果纯粹是 Python 代码可以直接把它放进source.dir不用放进 requirements但它会被当成你的代码而非依赖包后续 pip 升级时不会自动同步。如果包本身带 C 扩展正确的做法是写一个私有配方在项目根目录创建/recipes/mypackage/__init__.py里面定义get_recipe类指定编译步骤。一个更实用的体积瘦身技巧是去掉多余的架构。默认会打所有 ABI 的包四核变大到 80MB 以上。编辑buildozer.specandroid.archs arm64-v8a这行一旦设置后续构建只生成一个 .so 集合。如果你的 App 只在中国大陆应用市场分发arm64-v8a 覆盖率已超过 95%不需要为几台老设备保留armeabi-v7a。瘦身后再检查解压后的 assets 目录unzip -l bin/*.apk | grep -E \.so|\.py | sort -k1 -n | tail -20观察最大的 .so 是不是libpython3.11.so。如果是说明你把 numpy、pandas 这类大库编进了包里而业务可能只是用到了其中 10% 的功能。此时换成numpy的纯 Python 替代方案或改用内置math、statistics包体积会显著下降。另一个容易被忽略的问题是glob.c这个文件——它用于支持 Python 的glob模块在 Android 上的行为但 Android 的文件系统权限模型和 Linux 不同glob.glob(/sdcard/*.txt)在 targetSdkVersion 30 以上会返回空列表因为需要申请READ_EXTERNAL_STORAGE权限。在 spec 里加android.permissions READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE同时代码里用ActivityCompat.requestPermissions动态申请。这个坑我见过不止一次代码在 PC 上正常查文件打包到安卓上就查不到核心不是 Python 代码问题而是权限模型变了。验证 APK 内文件布局是否正确的最后一步aapt dump badging bin/myapp-0.1-arm64-v8a-release.apk | grep package输出包含package: namecom.example.myapp versionCode1确认和你在市场中填写的包名一致。如果版本号不对去buildozer.spec改version.code和version.release这两个字段不在 profile 里很容易漏掉。完成这一步APK 就具备上架的条件了。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →