Android与Python爱之初体验

作者: Jcme丶Ls | 来源:发表于2016-10-27 17:40 被阅读11120次
    • 独家授权码个蛋转载 *

    前言

    看到这个标题,大家可能会认为就是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")
    
    1. 上手快,按网友所说,只需要读完Python API就可以成为大神,实际体验确实如此,十分好上手,如果现在让我推荐一个没有学过编程的人学习一款脚本语言,我会推荐他学一下python。
    2. 前期开发效率高,正如前两个优点所说,代码简洁、上手快而且由于属于超高级语言,很多东西都封装好了,决定了他前期开发效率很高。
    3. 可移植性强,由于是解释性语言,只需要有解释器,他可以运行在任何平台。
    4. 拓展性强,C/JAVA都有接口可以调用到Python,Python也可以调用到C,对Python进项拓展。
    5. 丰富的库,由于超高级语言,封装了很多方法,而且好多大牛对其开发了库。

    当然还有几个缺点必须要强调一下。

    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;
    }
    

    当然,直接运行这段代码会报错,因为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);
            }
        }
    

    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");�
    jstring result = PyObject_CallObject(myFunction, NULL);�
    return result;
    }
    

    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_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调用:

    json.put("function", "hello");
    PyBridge.call(json);
    

    后记

    到此,Android call Python就基本完成了,调用第三方库的话只需要把ctype文件(Crystax文件夹中的sources\python\3.5\libs\对应平台\modules_ctypes.so)放到assets文件夹中就可以通过cdll.LoadLibrary来调用第三方库了。
    在此感谢joaoventura大神的指导!

    相关文章

      网友评论

      • sIN_110803:作者,你好,我也打算使用在android 中调用 python的方法;现在是直接调用JNI的,感觉比较好移植,能不能来一份demo看看 ,或者简单介绍一下该怎么集成的步骤呢。 万分感谢!
        sinduanan@163.com
        sIN_110803:@Jcme丶Ls :smile: 我刚接触到这方面的需求android里需要调python的算法,我很模糊, 看你写的这篇文章,就是类似于Java 调 C c再调python, 想知道具体怎么调用,,,
        Jcme丶Ls:@sIN_110803 你好,JNI现在能调用Python了?最近没有关注这块,我看下先
      • 6b8ec1e9b038:您好,可以把这个Android Demo给我发一份吗?学习学习。735049220@qq.com
      • 王小样_:你好,请问学习Python,推荐什么书?或者网站
        屈子腾:runoob.com/python.
      • 024a6bba9544: 你好,可以把这个Android Demo给我发一下吗?15801682338@126.com
        024a6bba9544:@Jcme丶Ls 好的,没事
        Jcme丶Ls:@魂魄 非常抱歉,demo现在暂时给不了,出了点问题,最近也有点忙,零碎时间我再看看吧
      • bluceoy::+1: 你好,可以把这个Android Demo给我发一下吗?bluceoy@126.com
      • 何日君在来:能把项目工程分享学习一下吗?谢谢
        bruce_ying@163.com
      • 9fb17ef77a3e:你好,能加一下6365151的qq号么,我想了解一下libpython的编译问题,cython已准备好了,so文件也生成了,但是不知道怎么出出arm的libpython.so
        Jcme丶Ls:MK文件里面写了的
      • 14905dedf6cf:可以把这个Android Demo给我发一下吗:pray:
        547683692@163.com
        14905dedf6cf: @丿请叫在下丶托儿索 没有啊!
        丿请叫在下丶托儿索:1114065696@qq.com 求发
        Jcme丶Ls:@600_11bb 下周整理给你吧,最近有点忙
      • 莫等闲_9ad8:你好 加下qq522748394, 我也需要Android 调Python的算法,没做过 ,请教下
      • 4dc265f33861:你好,请教你一个问题,最近在搞android调用python方法,看了你的 Android与Python爱之初体验,想用你的方法去调用,
        但是发现一开始就出错了,我的Android.mk文件如下:
        LOCAL_PATH := $(call my-dir)

        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)

        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)

        include $(CLEAR_VARS)
        LOCAL_MODULE := pb
        LOCAL_SRC_FILES := pb.c
        LOCAL_LDLIBS := -llog
        LOCAL_SHARED_LIBRARIES := python3.5m
        APP_STL := gnustl_static
        include $(BUILD_SHARED_LIBRARY)
        然后make moudle app,之后报错了:
        E:\Soft_Program_Files\crystax-ndk-10.3.2\build\core\prebuilt-library.mk
        Error:(45) *** Android NDK: Aborting . Stop.
        Error:Execution failed for task ':app:ndkBuild'.
        > Process 'command 'E:\Soft_Program_Files\crystax-ndk-10.3.2/ndk-build.cmd'' finished with non-zero exit value 2

        就是请教下你,这个错是怎么回事?我是win10上使用crystax-ndk来编译的。
        Jcme丶Ls: @雨欣_b9c5 整个crystax ndk文件夹下的所有文件、文件夹放到NDK的文件夹中
        4dc265f33861:@Jcme丶Ls 谢谢回复,AS的ndk路径已经改成E:\Soft_Program_Files\crystax-ndk-10.3.2,用其中的ndk-build.cmd执行的命令,你的意思是把crystax.so放到原来的ndk文件夹中,然后用原来的ndk编译?
        Jcme丶Ls: @雨欣_b9c5 crystax是需要放在原本的ndk文件夹中的,看路径你好像没放
      • funpig:请教一个问题,Python怎么调用Android里面.so库的方法?
        例如Android App有一个库libTest.so,里面有个方法test,使用下面的代码
        lib = ctypes.CDLL( './libTest.so' ) //总是在这句就报错了!!
        token = lib. test('python call libTest.so')
        print token
        funpig:是这样的,这个libTest.so是我从其他apk里面拿到的,我想在python环境下使用它。Crystax是android环境下使用python吧?我对Crystax不熟悉,说的不对请见谅
        Jcme丶Ls:@funpig Crystax文件夹中的sources\python\3.5\libs\对应平台\modules_ctypes.so放到assets文件夹中了吗?我通过cdll.LoadLibrary是可以调用so库的。
        funpig:lib = ctypes.CDLL( './libTest.so' ) //总是在这句就报错了!! ./libTest.so: unknown file type, first eight bytes: 0x7F 0x45 0x4C 0x46 0x01 0x01 0x01 0x00
      • CodingShell:想一下和直接用kotlin写比起来有什么优势?
        Jcme丶Ls:@CodingShell kotlin我还没用过,可以考虑下一个POC就用它 :grin:

      本文标题:Android与Python爱之初体验

      本文链接:https://www.haomeiwen.com/subject/xwcsuttx.html