Android与Python爱之初体验

hxors 7年前
   <h2><strong>前言</strong></h2>    <p>看到这个标题,大家可能会认为就是Android运行python脚本,或者用python写app,这些用QPython和P4A就可以实现了。我在想既然C可以调用Python,那么Android能不能通过JNI去调用C里的方法,C再去调用Python方法,实现Android与Python交互呢?用最近很热的一个概念来说JNI就是个壳。(本文假设大家有JNI开发基础)</p>    <h2><strong>想法</strong></h2>    <p>由于需求很明确了,所以整体流程大概就是这样。</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/2c2190b4cef560e7fe962133174c0d9f.png"></p>    <p style="text-align: center;">交互流程</p>    <h2><strong>为什么要用python</strong></h2>    <p>首先看下我们为什么要在Android里需要使用Python,我认为主要有一下几个优点</p>    <ol>     <li>代码简洁,这个真的是极度简洁的语言,比如我们想要print一个hello world,Java要这样做 <pre>  public class Hello {   public static void main(String[] args) {       System.out.println("Hello world");   }  }</pre> 而Python只需要一句话就可以print出来 <pre>  print ("hello world")</pre> </li>     <li>上手快,按网友所说,只需要读完Python API就可以成为大神,实际体验确实如此,十分好上手,如果现在让我推荐一个没有学过编程的人学习一款脚本语言,我会推荐他学一下python。</li>     <li>前期开发效率高,正如前两个优点所说,代码简洁、上手快而且由于属于超高级语言,很多东西都封装好了,决定了他前期开发效率很高。</li>     <li>可移植性强,由于是解释性语言,只需要有解释器,他可以运行在任何平台。</li>     <li>拓展性强,C/JAVA都有接口可以调用到Python,Python也可以调用到C,对Python进项拓展。</li>     <li>丰富的库,由于超高级语言,封装了很多方法,而且好多大牛对其开发了库。</li>    </ol>    <p>当然还有几个缺点必须要强调一下。</p>    <ol>     <li>强制缩进,代码简洁是把双刃剑,由于缩进所以简洁,而又由于缩进导致无法自动格式化代码,而且代码块的分割都是靠缩进,这时可能会造成混乱。</li>     <li>运行速度相对较慢,当然这个对相对C这种接近底层的语言来说的,Python在运行时先解析,再运行,而且由于高层语言相比底层语言都会慢那么一点。</li>     <li>版本兼容性较差,这个体现最明显的就是Python3和Python2,Python3不向下兼容</li>    </ol>    <h2><strong>Python C</strong></h2>    <p>Python C是C语言调用Python的一组API,通过它我们可以调用到Python方法。</p>    <h2><strong>Python C开发步骤</strong></h2>    <ol>     <li>引入头文件Python.h;</li>     <li>初始化python(Py_Initialize();)</li>     <li>引入模块(pModule = PyImport_Import("pythoncode");)</li>     <li>获取模块中的函数(PyObject_GetAttrString(pModule, "hello");<br> )</li>     <li>调用获取的函数(PyEval_CallObject(pFunction, NULL);<br> )</li>     <li>释放python(Py_Finalize();)</li>    </ol>    <p>对应的代码如下:</p>    <pre>  #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>    <p>当然,直接运行这段代码会报错,因为Python.h找不到还有相应的lib找不到,这里强烈建议使用mac或者Linux开发!!!填坑效率会比Windows高好多。具体怎么样处理这里先不说,如果实在需要,留言给我,我会另开一篇博文,毕竟这里是讲Android调用python的,而这个是在桌面环境下C调用Python的,而且百度也很多。</p>    <h2><strong>JNI Python C</strong></h2>    <p>当我成功使用C语言调用Python之后,我着手在JNI开发里调用Python,Python文件放在assets中 。</p>    <p>但是在开发过程中遇到了以下几个问题:</p>    <ol>     <li>头文件找不到(Python.h)</li>     <li>没有移动平台的python.so</li>     <li>兼容性</li>     <li>找不到.py文件</li>    </ol>    <p>接下来一个一个填坑。</p>    <h2><strong>头文件找不到(Python.h)</strong></h2>    <p>在MK文件中添加引用,</p>    <pre>  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)</pre>    <p>这段代码其实也把下一个问题解决了。</p>    <p>另外我们刚项目开始的时候可能为了开发方便,会在gradle中配置JNI资源文件夹路径,可是这导致了run project的时候AS也会对其中的C文件进行语法检查,这样由于没有外部头文件依赖,编译不会通过,所以我们需要在gradle中把JNI资源文件夹删了,用 [] 代替</p>    <pre>  sourceSets.main {        jni.srcDirs = []        jniLibs.srcDir 'src/main/libs'  }</pre>    <p>当我们编译成功SO库之后,C文件在运行中并不会被调用,而是调用编译为.so的文件中的方法。</p>    <h2><strong>没有移动平台的python.so</strong></h2>    <p>想要运行Python必须要有解释器,Android本身没有带,所以我们需要在程序中内嵌一个解释器,可是苦于找不到合适的so库,曾把P4A的python编译了一次,可是版本兼容性差,可用性不高。直到找到了Crystax NDK,它在10.3之后已经开始支持python for Android了,而且这个NDK资源包还填了几乎所有Android调用python的坑,包括第一个找不到头文件的问题,兼容的问题。在MK文件中,我们还需要加一段代码,编译crystax so库。</p>    <pre>  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)</pre>    <h2><strong>兼容性</strong></h2>    <p>Android目前有7个常见平台需要适配,其余的都没问题,只有X86和X86_64的有问题,推测crystax NDK Windows还没完善,因为mac下是可以直接编译的,所以有关编译的东西最好用Linux和Mac,Windows下我删了一个头文件,就可以运行了,没有发现异常。具体哪个我忘了,不过运行时报错哪个就去相应的文件里把头文件依赖删了就行,就一个。</p>    <p>然后生成7个平台的so库只需要在Application.mk中添加以下代码即可(APP_PLATFORM看个人调节):</p>    <pre>  APP_PLATFORM := android-19  APP_ABI := armeabi-v7a armeabi mips mips64 arm64-v8a x86 x86_64</pre>    <h2><strong>找不到.py文件</strong></h2>    <p>不知道什么原因,assets文件夹里的py文件获取不到,似乎是不能识别asset路径?求大神告知。解决方法就是把assets文件夹里的文件复制到设备的data文件夹里,再进行初始化。</p>    <pre>  //遍历      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>    <p>JNI C代码:</p>    <pre>  //初始化     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>    <p>Python方面就是个简单的hello函数,返回“hello”字符串。</p>    <h2><strong>优化</strong></h2>    <p>当我把上述问题一一解决之后,终于见到之前写的python代码里返回的hello语句了。可由此也出现了一个问题,当我调用Python方法的时候,必须先引入模块,再引入方法,而且当我们需要添加Python方法的时候,我们还要去写重复的调用方法,只是换个方法名,而且需要再次编译各平台so库,我就想有没有一种方法可以只修改Python方法和java调用方法,而不去动C方法呢。</p>    <p>修改后的流程图如下:</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/8f5905c3bf2ac8a392742cb01b91030d.png"></p>    <p style="text-align: center;">优化后流程</p>    <p>Python端增加一个路由方法,再写一个函数字典,把所有方法都加到字典里,C里调用的就是这个路由方法,java端调用的时候传入json里面包含了所需python方法,当json传入python中路由方法之后,自动匹配到相应的方法,每次添加新的方法只需要在python中添加字典已经方法,java调用时传入新的方法即可。</p>    <p>Python路由方法:</p>    <pre>  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,      })</pre>    <p>Python函数字典:</p>    <pre>  routes = {      'hello': hello,      'add': add,      'mul': mul,  }</pre>    <p>JNI C调用python方法:</p>    <pre>  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;  }</pre>    <p>java调用:</p>    <pre>  json.put("function", "hello");  PyBridge.call(json);</pre>    <h2><strong>后记</strong></h2>    <p>到此,Android call Python就基本完成了,调用第三方库的话只需要把ctype文件(Crystax文件夹中的sources\python\3.5\libs\对应平台\modules_ctypes.so)放到assets文件夹中就可以通过 cdll.LoadLibrary 来调用第三方库了。</p>    <p> </p>    <p> </p>    <p>来自:http://www.jianshu.com/p/aba8a1ae783e</p>    <p> </p>