Android與Python愛之初體驗
前言
看到這個標題,大家可能會認為就是Android運行python腳本,或者用python寫app,這些用QPython和P4A就可以實現了。我在想既然C可以調用Python,那么Android能不能通過JNI去調用C里的方法,C再去調用Python方法,實現Android與Python交互呢?用最近很熱的一個概念來說JNI就是個殼。(本文假設大家有JNI開發基礎)
想法
由于需求很明確了,所以整體流程大概就是這樣。
交互流程
為什么要用python
首先看下我們為什么要在Android里需要使用Python,我認為主要有一下幾個優點
- 代碼簡潔,這個真的是極度簡潔的語言,比如我們想要print一個hello world,Java要這樣做
public class Hello { public static void main(String[] args) { System.out.println("Hello world"); } }
而Python只需要一句話就可以print出來print ("hello world")
- 上手快,按網友所說,只需要讀完Python API就可以成為大神,實際體驗確實如此,十分好上手,如果現在讓我推薦一個沒有學過編程的人學習一款腳本語言,我會推薦他學一下python。
- 前期開發效率高,正如前兩個優點所說,代碼簡潔、上手快而且由于屬于超高級語言,很多東西都封裝好了,決定了他前期開發效率很高。
- 可移植性強,由于是解釋性語言,只需要有解釋器,他可以運行在任何平臺。
- 拓展性強,C/JAVA都有接口可以調用到Python,Python也可以調用到C,對Python進項拓展。
- 豐富的庫,由于超高級語言,封裝了很多方法,而且好多大牛對其開發了庫。
當然還有幾個缺點必須要強調一下。
- 強制縮進,代碼簡潔是把雙刃劍,由于縮進所以簡潔,而又由于縮進導致無法自動格式化代碼,而且代碼塊的分割都是靠縮進,這時可能會造成混亂。
- 運行速度相對較慢,當然這個對相對C這種接近底層的語言來說的,Python在運行時先解析,再運行,而且由于高層語言相比底層語言都會慢那么一點。
- 版本兼容性較差,這個體現最明顯的就是Python3和Python2,Python3不向下兼容
Python C
Python C是C語言調用Python的一組API,通過它我們可以調用到Python方法。
Python C開發步驟
- 引入頭文件Python.h;
- 初始化python(Py_Initialize();)
- 引入模塊(pModule = PyImport_Import("pythoncode");)
- 獲取模塊中的函數(PyObject_GetAttrString(pModule, "hello");
) - 調用獲取的函數(PyEval_CallObject(pFunction, NULL);
) - 釋放python(Py_Finalize();)
對應的代碼如下:
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; }</pre>
當然,直接運行這段代碼會報錯,因為Python.h找不到還有相應的lib找不到,這里強烈建議使用mac或者Linux開發!!!填坑效率會比Windows高好多。具體怎么樣處理這里先不說,如果實在需要,留言給我,我會另開一篇博文,畢竟這里是講Android調用python的,而這個是在桌面環境下C調用Python的,而且百度也很多。
JNI Python C
當我成功使用C語言調用Python之后,我著手在JNI開發里調用Python,Python文件放在assets中 。
但是在開發過程中遇到了以下幾個問題:
- 頭文件找不到(Python.h)
- 沒有移動平臺的python.so
- 兼容性
- 找不到.py文件
接下來一個一個填坑。
頭文件找不到(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)這段代碼其實也把下一個問題解決了。
另外我們剛項目開始的時候可能為了開發方便,會在gradle中配置JNI資源文件夾路徑,可是這導致了run project的時候AS也會對其中的C文件進行語法檢查,這樣由于沒有外部頭文件依賴,編譯不會通過,所以我們需要在gradle中把JNI資源文件夾刪了,用 [] 代替
sourceSets.main { jni.srcDirs = [] jniLibs.srcDir 'src/main/libs' }當我們編譯成功SO庫之后,C文件在運行中并不會被調用,而是調用編譯為.so的文件中的方法。
沒有移動平臺的python.so
想要運行Python必須要有解釋器,Android本身沒有帶,所以我們需要在程序中內嵌一個解釋器,可是苦于找不到合適的so庫,曾把P4A的python編譯了一次,可是版本兼容性差,可用性不高。直到找到了Crystax NDK,它在10.3之后已經開始支持python for Android了,而且這個NDK資源包還填了幾乎所有Android調用python的坑,包括第一個找不到頭文件的問題,兼容的問題。在MK文件中,我們還需要加一段代碼,編譯crystax so庫。
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個常見平臺需要適配,其余的都沒問題,只有X86和X86_64的有問題,推測crystax NDK Windows還沒完善,因為mac下是可以直接編譯的,所以有關編譯的東西最好用Linux和Mac,Windows下我刪了一個頭文件,就可以運行了,沒有發現異常。具體哪個我忘了,不過運行時報錯哪個就去相應的文件里把頭文件依賴刪了就行,就一個。
然后生成7個平臺的so庫只需要在Application.mk中添加以下代碼即可(APP_PLATFORM看個人調節):
APP_PLATFORM := android-19 APP_ABI := armeabi-v7a armeabi mips mips64 arm64-v8a x86 x86_64找不到.py文件
不知道什么原因,assets文件夾里的py文件獲取不到,似乎是不能識別asset路徑?求大神告知。解決方法就是把assets文件夾里的文件復制到設備的data文件夾里,再進行初始化。
//遍歷 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; }
//復制 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/"; } //調用復制代碼 public void copyAssets(String path) { for (String asset : listAssets(path)) { copyAssetFile(asset, getAssetsDataDir() + asset); } }</pre>
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 ");//這個為了引入庫,若不需要引入可以不用 return 0; } //釋放 JNIEXPORT jint JNICALL Java_com_jcmels_liba_pybridge_PyBridge_stop (JNIEnv env, jclass jc) { Py_Finalize(); return 0; } //調用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"); PyObject_CallObject(myFunction, NULL); }</pre>Python方面就是個簡單的hello函數,返回“hello”字符串。
優化
當我把上述問題一一解決之后,終于見到之前寫的python代碼里返回的hello語句了。可由此也出現了一個問題,當我調用Python方法的時候,必須先引入模塊,再引入方法,而且當我們需要添加Python方法的時候,我們還要去寫重復的調用方法,只是換個方法名,而且需要再次編譯各平臺so庫,我就想有沒有一種方法可以只修改Python方法和java調用方法,而不去動C方法呢。
修改后的流程圖如下:
優化后流程
Python端增加一個路由方法,再寫一個函數字典,把所有方法都加到字典里,C里調用的就是這個路由方法,java端調用的時候傳入json里面包含了所需python方法,當json傳入python中路由方法之后,自動匹配到相應的方法,每次添加新的方法只需要在python中添加字典已經方法,java調用時傳入新的方法即可。
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函數字典:
routes = { 'hello': hello, 'add': add, 'mul': mul, }JNI C調用python方法:
JNIEXPORT jstring JNICALL Java_com_jventura_pybridge_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調用:
json.put("function", "hello"); PyBridge.call(json);后記
到此,Android call Python就基本完成了,調用第三方庫的話只需要把ctype文件(Crystax文件夾中的sources\python\3.5\libs\對應平臺\modules_ctypes.so)放到assets文件夾中就可以通過 cdll.LoadLibrary 來調用第三方庫了。
來自:http://www.jianshu.com/p/aba8a1ae783e