- 獨(dú)家授權(quán)碼個(gè)蛋轉(zhuǎn)載 *
前言
看到這個(gè)標(biāo)題,大家可能會(huì)認(rèn)為就是Android運(yùn)行python腳本,或者用python寫app,這些用QPython和P4A就可以實(shí)現(xiàn)了。我在想既然C可以調(diào)用Python,那么Android能不能通過(guò)JNI去調(diào)用C里的方法,C再去調(diào)用Python方法,實(shí)現(xiàn)Android與Python交互呢?用最近很熱的一個(gè)概念來(lái)說(shuō)JNI就是個(gè)殼。(本文假設(shè)大家有JNI開發(fā)基礎(chǔ))
想法
由于需求很明確了,所以整體流程大概就是這樣。

為什么要用python
首先看下我們?yōu)槭裁匆贏ndroid里需要使用Python,我認(rèn)為主要有一下幾個(gè)優(yōu)點(diǎn)
- 代碼簡(jiǎn)潔,這個(gè)真的是極度簡(jiǎn)潔的語(yǔ)言,比如我們想要print一個(gè)hello world,Java要這樣做
public class Hello {
public static void main(String[] args) {
System.out.println("Hello world");
}
}
而Python只需要一句話就可以print出來(lái)
print ("hello world")
- 上手快,按網(wǎng)友所說(shuō),只需要讀完P(guān)ython API就可以成為大神,實(shí)際體驗(yàn)確實(shí)如此,十分好上手,如果現(xiàn)在讓我推薦一個(gè)沒(méi)有學(xué)過(guò)編程的人學(xué)習(xí)一款腳本語(yǔ)言,我會(huì)推薦他學(xué)一下python。
- 前期開發(fā)效率高,正如前兩個(gè)優(yōu)點(diǎn)所說(shuō),代碼簡(jiǎn)潔、上手快而且由于屬于超高級(jí)語(yǔ)言,很多東西都封裝好了,決定了他前期開發(fā)效率很高。
- 可移植性強(qiáng),由于是解釋性語(yǔ)言,只需要有解釋器,他可以運(yùn)行在任何平臺(tái)。
- 拓展性強(qiáng),C/JAVA都有接口可以調(diào)用到Python,Python也可以調(diào)用到C,對(duì)Python進(jìn)項(xiàng)拓展。
- 豐富的庫(kù),由于超高級(jí)語(yǔ)言,封裝了很多方法,而且好多大牛對(duì)其開發(fā)了庫(kù)。
當(dāng)然還有幾個(gè)缺點(diǎn)必須要強(qiáng)調(diào)一下。
- 強(qiáng)制縮進(jìn),代碼簡(jiǎn)潔是把雙刃劍,由于縮進(jìn)所以簡(jiǎn)潔,而又由于縮進(jìn)導(dǎo)致無(wú)法自動(dòng)格式化代碼,而且代碼塊的分割都是靠縮進(jìn),這時(shí)可能會(huì)造成混亂。
- 運(yùn)行速度相對(duì)較慢,當(dāng)然這個(gè)對(duì)相對(duì)C這種接近底層的語(yǔ)言來(lái)說(shuō)的,Python在運(yùn)行時(shí)先解析,再運(yùn)行,而且由于高層語(yǔ)言相比底層語(yǔ)言都會(huì)慢那么一點(diǎn)。
- 版本兼容性較差,這個(gè)體現(xiàn)最明顯的就是Python3和Python2,Python3不向下兼容
Python C
Python C是C語(yǔ)言調(diào)用Python的一組API,通過(guò)它我們可以調(diào)用到Python方法。
Python C開發(fā)步驟
- 引入頭文件Python.h;
- 初始化python(Py_Initialize();)
- 引入模塊(pModule = PyImport_Import("pythoncode");)
- 獲取模塊中的函數(shù)(PyObject_GetAttrString(pModule, "hello");
) - 調(diào)用獲取的函數(shù)(PyEval_CallObject(pFunction, NULL);
) - 釋放python(Py_Finalize();)
對(duì)應(yīng)的代碼如下:
#include <stdio.h>
#include "Python.h"
int main()
{
Py_Initialize();
PyObject *pModule;
PyObject *pFunction;
pModule = PyImport_Import("pythoncode");
pFunction = PyObject_GetAttrString(pModule, "hello");
PyEval_CallObject(pFunction, NULL);
Py_Finalize();
return 0;
}
當(dāng)然,直接運(yùn)行這段代碼會(huì)報(bào)錯(cuò),因?yàn)镻ython.h找不到還有相應(yīng)的lib找不到,這里強(qiáng)烈建議使用mac或者Linux開發(fā)?。?!填坑效率會(huì)比Windows高好多。具體怎么樣處理這里先不說(shuō),如果實(shí)在需要,留言給我,我會(huì)另開一篇博文,畢竟這里是講Android調(diào)用python的,而這個(gè)是在桌面環(huán)境下C調(diào)用Python的,而且百度也很多。
JNI Python C
當(dāng)我成功使用C語(yǔ)言調(diào)用Python之后,我著手在JNI開發(fā)里調(diào)用Python,Python文件放在assets中 。
但是在開發(fā)過(guò)程中遇到了以下幾個(gè)問(wèn)題:
- 頭文件找不到(Python.h)
- 沒(méi)有移動(dòng)平臺(tái)的python.so
- 兼容性
- 找不到.py文件
接下來(lái)一個(gè)一個(gè)填坑。
頭文件找不到(Python.h)
在MK文件中添加引用,
include $(CLEAR_VARS)
LOCAL_MODULE := pybridge
LOCAL_SRC_FILES := pybridge.c
LOCAL_LDLIBS := -llog
LOCAL_SHARED_LIBRARIES := python3.5m
APP_STL := gnustl_static
include $(BUILD_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := python3.5m
LOCAL_SRC_FILES := $(CRYSTAX_PATH)/sources/python/3.5/libs/$(TARGET_ARCH_ABI)/libpython3.5m.so
LOCAL_EXPORT_CFLAGS := -I $(CRYSTAX_PATH)/sources/python/3.5/include/python/
APP_STL := gnustl_static
include $(PREBUILT_SHARED_LIBRARY)
這段代碼其實(shí)也把下一個(gè)問(wèn)題解決了。
另外我們剛項(xiàng)目開始的時(shí)候可能為了開發(fā)方便,會(huì)在gradle中配置JNI資源文件夾路徑,可是這導(dǎo)致了run project的時(shí)候AS也會(huì)對(duì)其中的C文件進(jìn)行語(yǔ)法檢查,這樣由于沒(méi)有外部頭文件依賴,編譯不會(huì)通過(guò),所以我們需要在gradle中把JNI資源文件夾刪了,用[]代替
sourceSets.main {
jni.srcDirs = []
jniLibs.srcDir 'src/main/libs'
}
當(dāng)我們編譯成功SO庫(kù)之后,C文件在運(yùn)行中并不會(huì)被調(diào)用,而是調(diào)用編譯為.so的文件中的方法。
沒(méi)有移動(dòng)平臺(tái)的python.so
想要運(yùn)行Python必須要有解釋器,Android本身沒(méi)有帶,所以我們需要在程序中內(nèi)嵌一個(gè)解釋器,可是苦于找不到合適的so庫(kù),曾把P4A的python編譯了一次,可是版本兼容性差,可用性不高。直到找到了Crystax NDK,它在10.3之后已經(jīng)開始支持python for Android了,而且這個(gè)NDK資源包還填了幾乎所有Android調(diào)用python的坑,包括第一個(gè)找不到頭文件的問(wèn)題,兼容的問(wèn)題。在MK文件中,我們還需要加一段代碼,編譯crystax so庫(kù)。
include $(CLEAR_VARS)
LOCAL_MODULE := crystax
LOCAL_SRC_FILES := $(CRYSTAX_PATH)/sources/crystax/libs/$(TARGET_ARCH_ABI)/libcrystax.so
LOCAL_EXPORT_CFLAGS := -I $(CRYSTAX_PATH)/sources/crystax/include/crystax/
APP_STL := gnustl_static
include $(PREBUILT_SHARED_LIBRARY)
兼容性
Android目前有7個(gè)常見平臺(tái)需要適配,其余的都沒(méi)問(wèn)題,只有X86和X86_64的有問(wèn)題,推測(cè)crystax NDK Windows還沒(méi)完善,因?yàn)閙ac下是可以直接編譯的,所以有關(guān)編譯的東西最好用Linux和Mac,Windows下我刪了一個(gè)頭文件,就可以運(yùn)行了,沒(méi)有發(fā)現(xiàn)異常。具體哪個(gè)我忘了,不過(guò)運(yùn)行時(shí)報(bào)錯(cuò)哪個(gè)就去相應(yīng)的文件里把頭文件依賴刪了就行,就一個(gè)。
然后生成7個(gè)平臺(tái)的so庫(kù)只需要在Application.mk中添加以下代碼即可(APP_PLATFORM看個(gè)人調(diào)節(jié)):
APP_PLATFORM := android-19
APP_ABI := armeabi-v7a armeabi mips mips64 arm64-v8a x86 x86_64
找不到.py文件
不知道什么原因,assets文件夾里的py文件獲取不到,似乎是不能識(shí)別asset路徑?求大神告知。解決方法就是把a(bǔ)ssets文件夾里的文件復(fù)制到設(shè)備的data文件夾里,再進(jìn)行初始化。
//遍歷
public List<String> listAssets(String path) {
List<String> assets = new ArrayList<>();
try {
String assetList[] = mAssetManager.list(path);
if (assetList.length > 0) {
for (String asset : assetList) {
List<String> subAssets = listAssets(path + '/' + asset);
assets.addAll(subAssets);
}
} else {
assets.add(path);
}
} catch (IOException e) {
e.printStackTrace();
}
return assets;
}
//復(fù)制
private void copyAssetFile(String src, String dst) {
File file = new File(dst);
Log.i(LOGTAG, String.format("Copying %s -> %s", src, dst));
try {
File dir = file.getParentFile();
if (!dir.exists()) {
dir.mkdirs();
}
InputStream in = mAssetManager.open(src);
OutputStream out = new FileOutputStream(file);
byte[] buffer = new byte[1024];
int read = in.read(buffer);
while (read != -1) {
out.write(buffer, 0, read);
read = in.read(buffer);
}
out.close();
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
//獲取asset目錄
public String getAssetsDataDir() {
String appDataDir = mContext.getApplicationInfo().dataDir;
return appDataDir + "/assets/";
}
//調(diào)用復(fù)制代碼
public void copyAssets(String path) {
for (String asset : listAssets(path)) {
copyAssetFile(asset, getAssetsDataDir() + asset);
}
}
JNI C代碼:
//初始化
JNIEXPORT jint JNICALL Java_com_jcmels_liba_pybridge_PyBridge_start
(JNIEnv *env, jclass jc, jstring path)
{
const char *pypath = (*env)->GetStringUTFChars(env, path, NULL);
char paths[512];
snprintf(paths, sizeof(paths), "%s:%s/stdlib.zip", pypath, pypath);
wchar_t *wchar_paths = Py_DecodeLocale(paths, NULL);
Py_SetPath(wchar_paths);
Py_Initialize();
PyRun_SimpleString("import helloPy");
PyRun_SimpleString("from ctypes import *");//這個(gè)為了引入庫(kù),若不需要引入可以不用
return 0;
}
//釋放
JNIEXPORT jint JNICALL Java_com_jcmels_liba_pybridge_PyBridge_stop
(JNIEnv *env, jclass jc)
{
Py_Finalize();
return 0;
}
//調(diào)用
JNIEXPORT jstring JNICALL Java_com_jcmels_liba_pysayhello_PyBridge_call?
(JNIEnv *env, jclass jc)?{
PyObject* myModuleString = PyUnicode_FromString((char*)"helloPy");?
PyObject* myModule = PyImport_Import(myModuleString);?
PyObject* myFunction = PyObject_GetAttrString(myModule, (char*)"hello");?
jstring result = PyObject_CallObject(myFunction, NULL);?
return result;
}
Python方面就是個(gè)簡(jiǎn)單的hello函數(shù),返回“hello”字符串。
優(yōu)化
當(dāng)我把上述問(wèn)題一一解決之后,終于見到之前寫的python代碼里返回的hello語(yǔ)句了。可由此也出現(xiàn)了一個(gè)問(wèn)題,當(dāng)我調(diào)用Python方法的時(shí)候,必須先引入模塊,再引入方法,而且當(dāng)我們需要添加Python方法的時(shí)候,我們還要去寫重復(fù)的調(diào)用方法,只是換個(gè)方法名,而且需要再次編譯各平臺(tái)so庫(kù),我就想有沒(méi)有一種方法可以只修改Python方法和java調(diào)用方法,而不去動(dòng)C方法呢。
修改后的流程圖如下:

Python端增加一個(gè)路由方法,再寫一個(gè)函數(shù)字典,把所有方法都加到字典里,C里調(diào)用的就是這個(gè)路由方法,java端調(diào)用的時(shí)候傳入json里面包含了所需python方法,當(dāng)json傳入python中路由方法之后,自動(dòng)匹配到相應(yīng)的方法,每次添加新的方法只需要在python中添加字典已經(jīng)方法,java調(diào)用時(shí)傳入新的方法即可。
Python路由方法:
def router(args):
values = json.loads(args)
try:
function = routes[values.get('function')]
status = 'ok'
res = function(values)
except KeyError:
status = 'fail'
res = None
return json.dumps({
'status': status,
'result': res,
})
Python函數(shù)字典:
routes = {
'hello': hello,
'add': add,
'mul': mul,
}
JNI C調(diào)用python方法:
JNIEXPORT jstring JNICALL Java_com_jcmels_liba_pysayhello_PyBridge_call
(JNIEnv *env, jclass jc, jstring payload)
{
jboolean iscopy;
const char *payload_utf = (*env)->GetStringUTFChars(env, payload, &iscopy);
PyObject* myModuleString = PyUnicode_FromString((char*)"helloPy");
PyObject* myModule = PyImport_Import(myModuleString);
PyObject* myFunction = PyObject_GetAttrString(myModule, (char*)"router");
PyObject* args = PyTuple_Pack(1, PyUnicode_FromString(payload_utf));
PyObject* myResult = PyObject_CallObject(myFunction, args);
char *myResultChar = PyUnicode_AsUTF8(myResult);
char *res = malloc(sizeof(char) * strlen(myResultChar) + 1);
strcpy(res, myResultChar);
jstring result = (*env)->NewStringUTF(env, res);
return result;
}
java調(diào)用:
json.put("function", "hello");
PyBridge.call(json);
后記
到此,Android call Python就基本完成了,調(diào)用第三方庫(kù)的話只需要把ctype文件(Crystax文件夾中的sources\python\3.5\libs\對(duì)應(yīng)平臺(tái)\modules_ctypes.so)放到assets文件夾中就可以通過(guò)cdll.LoadLibrary來(lái)調(diào)用第三方庫(kù)了。
在此感謝joaoventura大神的指導(dǎo)!