我是python小白一枚,對kivy開發手機app產生了興趣,並沒感覺到kivy寫代碼有多難,折騰打包成手機apk倒是花了好長時間,走過了大大小小的坑,這里把經驗記錄下來,供大家參考。
kivy打包有幾種方法,可以自己配置環境,通過python for android(p4a),或者buildozer打包,也可以使用別人配置好環境的虛擬機打包。配置環境坑實在太多了,建議直接研究使用虛擬機打包,省時省力,把更多精力用在代碼上吧。
我打包用的虛擬機是某大佬做的,網址是:https://github.com/nkiiiiid/kivy-apk
虛擬機解壓安裝后占將近50GB空間,如果電腦硬盤不夠用,可以下載到移動硬盤上,安裝在移動硬盤上。
具體的使用方法,上面的網頁有詳細說明。
手機軟件運行日志查看方法,大佬們用的都是mumu模擬器,操作太復雜了,小白的我實在不愛花那么多時間研究了,就用了另一個簡單點的笨方法,在ubuntu系統上安裝好adb環境后,按以下步驟操作就能查看日志了:
1. 手機打開開發者模式
2. 開發者模式里面打開USB調試
3. 電腦下載adb工具
4. 手機連接電腦
5. 電腦adb devices看到有設備
6. 電腦adb shell logcat -s python
7. 手機打開APP
再說說打包的配置文件修改吧,buildozer.spec是打包失敗的禍根,很多手機上失退,打包失敗都可能是配置不對造成的。遇到失敗或閃退,首先查看手機運行日志,其次認真在buildozer.spec上找原因。下面附上一個我打包用過的buildozer.spec供參考,不同的應用程序在打包時都要相應地修改配置文件上的參數
[app]
# (str) Title of your application
title = JEA
# (str) Package name
package.name = JEA
# (str) Package domain (needed for android/ios packaging)
package.domain = org.kivydev
# (str) Source code where the main.py live
source.dir = .
# (list) Source files to include (let empty to include all the files)
source.include_exts = py,png,jpg,kv,atlas,ttf
# (list) List of inclusions using pattern matching
#source.include_patterns = assets/*,images/*.png
# (list) Source files to exclude (let empty to not exclude anything)
#source.exclude_exts = spec
# (list) List of directory to exclude (let empty to not exclude anything)
#source.exclude_dirs = tests, bin
# (list) List of exclusions using pattern matching
#source.exclude_patterns = license,images/*/*.jpg
# (str) Application versioning (method 1)
version = 1.8
# (str) Application versioning (method 2)
# version.regex = __version__ = ['"](.*)['"]
# version.filename = %(source.dir)s/main.py
# (list) Application requirements
# comma separated e.g. requirements = sqlite3,kivy
requirements = python3,kivy,requests,beautifulsoup4
# (str) Custom source folders for requirements
# Sets custom source for any requirements with recipes
# requirements.source.kivy = ../../kivy
# (list) Garden requirements
#garden_requirements =
# (str) Presplash of the application
presplash.filename = %(source.dir)s/data/presplash.png
# (str) Icon of the application
icon.filename = %(source.dir)s/data/icon.png
# (str) Supported orientation (one of landscape, sensorLandscape, portrait or all)
orientation = all
# (list) List of service to declare
#services = NAME:ENTRYPOINT_TO_PY,NAME2:ENTRYPOINT2_TO_PY
#
# OSX Specific
#
#
author = 漏 Copyright Guoming Liu
# change the major version of python used by the app
osx.python_version = 3
# Kivy version to use
osx.kivy_version = 1.9.1
#
# Android specific
#
# (bool) Indicate if the application should be fullscreen or not
fullscreen = 0
# (string) Presplash background color (for new android toolchain)
# Supported formats are: #RRGGBB #AARRGGBB or one of the following names:
# red, blue, green, black, white, gray, cyan, magenta, yellow, lightgray,
# darkgray, grey, lightgrey, darkgrey, aqua, fuchsia, lime, maroon, navy,
# olive, purple, silver, teal.
#android.presplash_color = #FFFFFF
# (list) Permissions
#android.permissions = INTERNET
android.permissions = INTERNET,ACCESS_WIFI_STATE,ACCESS_NETWORK_STATE,CHANGE_NETWORK_STATE,CHANGE_WIFI_STATE,WRITE_EXTERNAL_STORAGE,BIND_INPUT_METHOD
# (int) Target Android API, should be as high as possible.
android.api = 27
# (int) Minimum API your APK will support.
android.minapi = 21
# (int) Android SDK version to use
#android.sdk = 27
# (str) Android NDK version to use
android.ndk = 19c
# (int) Android NDK API to use. This is the minimum API your app will support, it should usually match android.minapi.
android.ndk_api = 21
# (bool) Use --private data storage (True) or --dir public storage (False)
#android.private_storage = True
# (str) Android NDK directory (if empty, it will be automatically downloaded.)
android.ndk_path = /home/kivydev/andr/android-ndk-r19c
# (str) Android SDK directory (if empty, it will be automatically downloaded.)
android.sdk_path = /home/kivydev/andr/android-sdk-linux
# (str) ANT directory (if empty, it will be automatically downloaded.)
android.ant_path = /home/kivydev/andr/apache-ant-1.9.4
# (bool) If True, then skip trying to update the Android sdk
# This can be useful to avoid excess Internet downloads or save time
# when an update is due and you just want to test/build your package
# android.skip_update = False
# (bool) If True, then automatically accept SDK license
# agreements. This is intended for automation only. If set to False,
# the default, you will be shown the license when first running
# buildozer.
# android.accept_sdk_license = False
# (str) Android entry point, default is ok for Kivy-based app
#android.entrypoint = org.renpy.android.PythonActivity
# (str) Android app theme, default is ok for Kivy-based app
# android.apptheme = "@android:style/Theme.NoTitleBar"
# (list) Pattern to whitelist for the whole project
#android.whitelist =
# (str) Path to a custom whitelist file
#android.whitelist_src =
# (str) Path to a custom blacklist file
#android.blacklist_src =
# (list) List of Java .jar files to add to the libs so that pyjnius can access
# their classes. Don't add jars that you do not need, since extra jars can slow
# down the build process. Allows wildcards matching, for example:
# OUYA-ODK/libs/*.jar
#android.add_jars = foo.jar,bar.jar,path/to/more/*.jar
# (list) List of Java files to add to the android project (can be java or a
# directory containing the files)
#android.add_src =
# (list) Android AAR archives to add (currently works only with sdl2_gradle
# bootstrap)
#android.add_aars =
# (list) Gradle dependencies to add (currently works only with sdl2_gradle
# bootstrap)
#android.gradle_dependencies =
# (list) add java compile options
# this can for example be necessary when importing certain java libraries using the 'android.gradle_dependencies' option
# see https://developer.android.com/studio/write/java8-support for further information
# android.add_compile_options = "sourceCompatibility = 1.8", "targetCompatibility = 1.8"
# (list) Gradle repositories to add {can be necessary for some android.gradle_dependencies}
# please enclose in double quotes
# e.g. android.gradle_repositories = "maven { url 'https://kotlin.bintray.com/ktor' }"
#android.add_gradle_repositories =
# (list) packaging options to add
# see https://google.github.io/android-gradle-dsl/current/com.android.build.gradle.internal.dsl.PackagingOptions.html
# can be necessary to solve conflicts in gradle_dependencies
# please enclose in double quotes
# e.g. android.add_packaging_options = "exclude 'META-INF/common.kotlin_module'", "exclude 'META-INF/*.kotlin_module'"
#android.add_gradle_repositories =
# (list) Java classes to add as activities to the manifest.
#android.add_activites = com.example.ExampleActivity
# (str) OUYA Console category. Should be one of GAME or APP
# If you leave this blank, OUYA support will not be enabled
#android.ouya.category = GAME
# (str) Filename of OUYA Console icon. It must be a 732x412 png image.
#android.ouya.icon.filename = %(source.dir)s/data/ouya_icon.png
# (str) XML file to include as an intent filters in <activity> tag
#android.manifest.intent_filters =
# (str) launchMode to set for the main activity
#android.manifest.launch_mode = standard
# (list) Android additional libraries to copy into libs/armeabi
#android.add_libs_armeabi = libs/android/*.so
#android.add_libs_armeabi_v7a = libs/android-v7/*.so
#android.add_libs_arm64_v8a = libs/android-v8/*.so
#android.add_libs_x86 = libs/android-x86/*.so
#android.add_libs_mips = libs/android-mips/*.so
# (bool) Indicate whether the screen should stay on
# Don't forget to add the WAKE_LOCK permission if you set this to True
#android.wakelock = False
# (list) Android application meta-data to set (key=value format)
#android.meta_data =
# (list) Android library project to add (will be added in the
# project.properties automatically.)
#android.library_references =
# (list) Android shared libraries which will be added to AndroidManifest.xml using <uses-library> tag
#android.uses_library =
# (str) Android logcat filters to use
#android.logcat_filters = *:S python:D
# (bool) Copy library instead of making a libpymodules.so
#android.copy_libs = 1
# (str) The Android arch to build for, choices: armeabi-v7a, arm64-v8a, x86, x86_64
android.arch = armeabi-v7a
#
# Python for android (p4a) specific
#
# (str) python-for-android fork to use, defaults to upstream (kivy)
#p4a.fork = kivy
# (str) python-for-android branch to use, defaults to master
#p4a.branch = master
# (str) python-for-android git clone directory (if empty, it will be automatically cloned from github)
#p4a.source_dir =
# (str) The directory in which python-for-android should look for your own build recipes (if any)
#p4a.local_recipes =
# (str) Filename to the hook for p4a
#p4a.hook =
# (str) Bootstrap to use for android builds
# p4a.bootstrap = sdl2
# (int) port number to specify an explicit --port= p4a argument (eg for bootstrap flask)
#p4a.port =
#
# iOS specific
#
# (str) Path to a custom kivy-ios folder
#ios.kivy_ios_dir = ../kivy-ios
# Alternately, specify the URL and branch of a git checkout:
ios.kivy_ios_url = https://github.com/kivy/kivy-ios
ios.kivy_ios_branch = master
# Another platform dependency: ios-deploy
# Uncomment to use a custom checkout
#ios.ios_deploy_dir = ../ios_deploy
# Or specify URL and branch
ios.ios_deploy_url = https://github.com/phonegap/ios-deploy
ios.ios_deploy_branch = 1.7.0
# (str) Name of the certificate to use for signing the debug version
# Get a list of available identities: buildozer ios list_identities
#ios.codesign.debug = "iPhone Developer: <lastname> <firstname> (<hexstring>)"
# (str) Name of the certificate to use for signing the release version
#ios.codesign.release = %(ios.codesign.debug)s
[buildozer]
# (int) Log level (0 = error only, 1 = info, 2 = debug (with command output))
log_level = 2
# (int) Display warning if buildozer is run as root (0 = False, 1 = True)
warn_on_root = 1
# (str) Path to build artifact storage, absolute or relative to spec file
build_dir = /home/kivydev/test/.buildozer
# (str) Path to build output (i.e. .apk, .ipa) storage
# bin_dir = ./bin
# -----------------------------------------------------------------------------
# List as sections
#
# You can define all the "list" as [section:key].
# Each line will be considered as a option to the list.
# Let's take [app] / source.exclude_patterns.
# Instead of doing:
#
#[app]
#source.exclude_patterns = license,data/audio/*.wav,data/images/original/*
#
# This can be translated into:
#
#[app:source.exclude_patterns]
#license
#data/audio/*.wav
#data/images/original/*
#
# -----------------------------------------------------------------------------
# Profiles
#
# You can extend section / key with a profile
# For example, you want to deploy a demo version of your application without
# HD content. You could first change the title to add "(demo)" in the name
# and extend the excluded directories to remove the HD content.
#
#[app@demo]
#title = My Application (demo)
#
#[app:source.exclude_patterns@demo]
#images/hd/*
#
# Then, invoke the command line with the "demo" profile:
#
#buildozer --profile demo android debug
特別要強調一下,requirements這個配置參數中一定要把你打包要含的文件擴展名全寫進去,否則會出錯。permissions參數非常重要,如果使用網絡,使用存儲空間或者用到其它權限,一定要在這里聲明,否則肯定閃退,關於權限,網上有其它很詳細的中文貼子,大家可以參考。
我在應用程序中使用了bs4庫,但打包時在requirements中寫上bs4閃退了,后來聽大佬建議改成全稱beautifulsoup4,就成功了。反正是,各種坑,大家過了入門這個階段,就一片光明了。
下面我把我調試成功,能在手機上正確運行的一個應用發在這里,供大家參考,程序中涉及到ScrollView,中文字體使用,ScreenManager等多個技術點,雖然是小應用,但可以參考。
# main.py
from kivy.app import App from kivy.uix.boxlayout import BoxLayout from kivy.core.text import LabelBase from kivy.uix.label import Label from kivy.lang import Builder from kivy.uix.screenmanager import ScreenManager, Screen from kivy.uix.scrollview import ScrollView from kivy.uix.button import Button import re import requests from bs4 import BeautifulSoup LabelBase.register(name='droid',fn_regular='droid.ttf') class SecondWindow(Screen): def ShowEarthquake(self): eq = self.ids.eq_list url = 'http://news.ceic.ac.cn/index.html' #headers={"Connection":"close",'User-Agent': "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36"} resp = requests.get(url) resp.encoding = resp.apparent_encoding content = resp.text soup = BeautifulSoup(content, 'html.parser') rows = soup.find_all('tr') catalog = [] for row in rows: cell = [i.text for i in row.find_all('td')] if len(cell) != 0: mag = cell[0] eq_time = cell[1] latitude = cell[2] longitude = cell[3] depth = cell[4] location = cell[5] string = f"{eq_time},{location}(緯度:{latitude},經度:{longitude})發生{mag}級地震,震源深度{depth}公里" catalog.append(string) temp = '據中國地震台網測定:' for line in catalog: temp = temp + '\n' + line eq.text = temp class WindowManager(ScreenManager): pass # We must inherited from BoxLayout,Screen class in MainWindow,otherwise the screen in app will go wrong class MainWindow(BoxLayout, Screen): def validate_user(self): dic = {'張三': ['12345678900', '000001'], '李四': ['18888888888', '12345678']} user = self.ids.name_field # get the name from id in kv fil small = self.ids.small big = self.ids.big if user.text in dic: short_number = dic[user.text][0] long_number = dic[user.text][1] small.text = f"[color=#0000FF]大號:{short_number} [/color]" big.text = f"[color=#0000FF]小號:{long_number} [/color]" else: small.text = '' big.text = '' small.text = "[color=#FF0000]對不起,查無此人 [/color]" class PhoneApp(App): def build(self): return kv kv = Builder.load_file("phone.kv") if __name__ == "__main__": sa = PhoneApp() sa.run()
#phone.kv
WindowManager: MainWindow: SecondWindow: #<FlatButton@ButtonBehavior+Label> # font_size: 30 <MainWindow>: id: main_win name: "First" orientation: "vertical" space_x: self.size[0]/6 #space_x defines the width of space to 1/3 full screen_width canvas.before: Color: rgba: (0,0,0,1) #white Color Rectangle: size: self.size pos: self.pos BoxLayout: size_hint_y: 0.1 canvas.before: Color: rgba: (.06,.45,.45,1) #white Color Rectangle: size: self.size pos: self.pos Label: font_name: 'droid' text: "吉林省地震局電話查詢" font_size: self.height / 3 size_hint_x: 1 BoxLayout: size_hint_y: 0.8 orientation: "vertical" BoxLayout: size_hint_y: 0.5 orientation: "vertical" padding: main_win.space_x, 5 spacing: 10 canvas.before: Color: rgba: (1, 222/255, 173/255, 1) #white Color Rectangle: size: self.size pos: self.pos BoxLayout: size_hint_y: 0.3 TextInput: id: name_field size_hint_x: 0.7 font_size: self.height * 3 / 5 padding_y: [self.height / 2.0 - (self.line_height / 2.0) * len(self._lines), 0] font_name: 'droid' hint_text: "姓名" multiline: False focus: True on_text_validate: root.validate_user() Button: size_hint_x: 0.3 font_size: self.height * 2 /5 background_color: (32/255,178/255,170/255,1) font_name: 'droid' text: '查詢' on_release: root.validate_user() Label: size_hint_y: 0.3 id: small font_name: 'droid' font_size: self.height / 2 pos: 10, 10 text: '' markup: True Label: size_hint_y: 0.3 id: big font_name: 'droid' font_size: self.height / 2 pos: 10, 10 text: '' markup: True BoxLayout: size_hint_y: 0.5 canvas.before: Color: rgba: (1, 222/255, 173/255, 1) #white Color Rectangle: size: self.size pos: self.pos BoxLayout: Label: size_hint_x: 0.2 text: '' Button: size_hint_x: 0.6 size_hint_y: 0.4 pos_hint: {'center_x': .5, 'center_y': .5} background_color: (32/255,178/255,170/255,1) font_size: self.height * 2 / 5 font_name: 'droid' text: '最新地震' on_release: app.root.current = "Second" Label: size_hint_x: 0.2 text: '' BoxLayout: size_hint_y: 0.1 canvas.before: Color: rgba: (.06,.45,.45,1) #white Color Rectangle: size: self.size pos: self.pos Label: font_name: 'droid' text: "軟件研發:長白山火山監測站 研發日期:2020-07-03" font_size: self.height / 4 bold: True size_hint_x: 1 <SecondWindow>: name: "Second" BoxLayout: orientation: "vertical" Button: size_hint_y: 0.1 font_name: 'droid' font_size: self.height * 3 / 5 text: "震情信息查詢" on_release: root.ShowEarthquake() ScrollView: size_hint_y: 0.8 id: scrlv #size_hint: (1, 0.5) do_scroll_x: False do_scroll_y: True TextInput: id: eq_list font_name: 'droid' size_hint_x: 1.0 font_size: 70 size_hint: 1, None #text_size: self.width,None height: max( (len(self._lines)+1) * self.line_height, scrlv.height) focus: True markup: True Button: size_hint_y: 0.1 font_name: 'droid' font_size: self.height * 3 /5 text: "返回首頁" on_release: app.root.current = "First"
最后還要分享一個經驗,上面的手機應用程序打包安裝到手機上后,可能會遇到無法在人名輸入框中使用輸入法的問題,可以這樣子解決:
SDL2.dll
,修改SDL_windowskeyboard.c代碼,定位到IME_Init 函數
videodata->ime_uiless = UILess_SetupSinks(videodata);
語句,注釋掉,重新編譯生成dll並替換。

