Android與Python愛之初體驗

hxors 8年前發布 | 44K 次閱讀 Python Android開發 移動開發

前言

看到這個標題,大家可能會認為就是Android運行python腳本,或者用python寫app,這些用QPython和P4A就可以實現了。我在想既然C可以調用Python,那么Android能不能通過JNI去調用C里的方法,C再去調用Python方法,實現Android與Python交互呢?用最近很熱的一個概念來說JNI就是個殼。(本文假設大家有JNI開發基礎)

想法

由于需求很明確了,所以整體流程大概就是這樣。

交互流程

為什么要用python

首先看下我們為什么要在Android里需要使用Python,我認為主要有一下幾個優點

  1. 代碼簡潔,這個真的是極度簡潔的語言,比如我們想要print一個hello world,Java要這樣做
    public class Hello {
     public static void main(String[] args) {
         System.out.println("Hello world");
     }
    }
    而Python只需要一句話就可以print出來
    print ("hello world")
  2. 上手快,按網友所說,只需要讀完Python API就可以成為大神,實際體驗確實如此,十分好上手,如果現在讓我推薦一個沒有學過編程的人學習一款腳本語言,我會推薦他學一下python。
  3. 前期開發效率高,正如前兩個優點所說,代碼簡潔、上手快而且由于屬于超高級語言,很多東西都封裝好了,決定了他前期開發效率很高。
  4. 可移植性強,由于是解釋性語言,只需要有解釋器,他可以運行在任何平臺。
  5. 拓展性強,C/JAVA都有接口可以調用到Python,Python也可以調用到C,對Python進項拓展。
  6. 豐富的庫,由于超高級語言,封裝了很多方法,而且好多大牛對其開發了庫。

當然還有幾個缺點必須要強調一下。

  1. 強制縮進,代碼簡潔是把雙刃劍,由于縮進所以簡潔,而又由于縮進導致無法自動格式化代碼,而且代碼塊的分割都是靠縮進,這時可能會造成混亂。
  2. 運行速度相對較慢,當然這個對相對C這種接近底層的語言來說的,Python在運行時先解析,再運行,而且由于高層語言相比底層語言都會慢那么一點。
  3. 版本兼容性較差,這個體現最明顯的就是Python3和Python2,Python3不向下兼容

Python C

Python C是C語言調用Python的一組API,通過它我們可以調用到Python方法。

Python C開發步驟

  1. 引入頭文件Python.h;
  2. 初始化python(Py_Initialize();)
  3. 引入模塊(pModule = PyImport_Import("pythoncode");)
  4. 獲取模塊中的函數(PyObject_GetAttrString(pModule, "hello");
  5. 調用獲取的函數(PyEval_CallObject(pFunction, NULL);
  6. 釋放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中 。

但是在開發過程中遇到了以下幾個問題:

  1. 頭文件找不到(Python.h)
  2. 沒有移動平臺的python.so
  3. 兼容性
  4. 找不到.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

 

 本文由用戶 hxors 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!