Android與Python混合編程

前言

早在2017年的時候,出於業餘興趣,我就開始研究關於Python移植到Android上的實現方案,我一直但願能實現Android與Python的混合編程,併爲此寫了一系列博客,我但願藉助JNI技術,實現Java與Python的交互。或許是出於上班忙,時間少,精力有限,人的惰性等等緣由,一直沒有實現一套框架,下降Android與Python混編的難度,作到儘量封裝C語言代碼,讓使用者無需掌握NDK開發,C語言編程等。原理是早已走通了,剩下的就是苦力活,寫C代碼,寫JNI代碼,對接口一一封裝。html

如今終於不用遺憾了,由於已經有人作了我一直想作的事,並且是以我想要的思路。我一直關注着Android與Python混合編程的信息,當我看到Chaquopy框架時,真的難掩的開心,比我本身實現的還要開心!前端

若是有人想探尋Android與Python的混編的原理與實現,那我以前寫的博客還能派上一點用場java

Android 平臺的Python——基礎篇(一)

Android 平臺的Python——基礎篇(一)node

Android 平臺的Python——JNI方案(二)

Android 平臺的Python——JNI方案(二)python

Android 平臺的Python——CLE方案實現(三)

Android 平臺的Python——CLE方案實現(三)android

Android 平臺的Python——第三方庫移植

Android 平臺的Python——第三方庫移植git

Android 平臺的Python——編譯Python解釋器

Android 平臺的Python——編譯Python解釋器程序員

Chaquopy是什麼?

簡單的直觀的解釋,它是在Android Studio中基於Gradle的構建系統實現的一個插件。它能夠幫助咱們用最簡便的方式實現Android技術與Python混合編程。甚至對於Python的忠實擁躉來講,能夠徹底使用Python語言開發一個apk,基本不用寫Java代碼。github

實際上Chaquopy並不只僅是一個插件那麼簡單,它是一套框架。gradle插件這部分只是用來打包apk的而已web

基礎用法-快速入門

首先使用Android studio建立一個hello工程,快速編寫代碼感覺一下

請先確保你當前電腦上的Python環境可用,Chaquopy是根據當前電腦上的Python版原本選擇集成對應的版本解釋器到apk中的。如你的電腦上有多個Python版本,可經過配置明確指定對應的版本

defaultConfig {
    python {
        buildPython "C:/Python36/python.exe"
    }
}
複製代碼

配置依賴

工程根目錄下的 build.gradle

buildscript {
    repositories {
        google()
        jcenter()
        // 設置倉庫
        maven { url "https://chaquo.com/maven" }
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.3.1'
        // 導入Chaquopy框架的包
        classpath "com.chaquo.python:gradle:6.3.0"
    }
}
複製代碼

app模塊下的 build.gradle

apply plugin: 'com.android.application'
// 應用插件
apply plugin: 'com.chaquo.python'

android {
    compileSdkVersion 28
    defaultConfig {
        applicationId "org.hello"
        minSdkVersion 16
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"

        // 指定abi,如需在模擬器調試,增長"x86",不然指定"armeabi-v7a"便可
        ndk {
            abiFilters "armeabi-v7a", "x86"
        }
    }
    buildTypes {}
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
}
複製代碼

配置完成後,同步一下gradle,網絡情況不良可能會失敗,多同步幾回,親測無需代理,同步成功後,所需的依賴就準備好了

編寫代碼

同步成功後,在工程中的main目錄下會生成python文件夾,如未生成,手動生成一個便可,該目錄即用來存放咱們本身編寫的python代碼

Python代碼

python文件夾中建立hello.py

from java import jclass

def greet(name):
    print("--- hello,%s ---" % name)

def add(a,b):
    return a + b

def sub(count,a=0,b=0,c=0):
    return count - a - b -c

def get_list(a,b,c,d):
    return [a,b,c,d]

def print_list(data):
    print(type(data))
    # 遍歷Java的ArrayList對象
    for i in range(data.size()):
        print(data.get(i))

# python調用Java類 
def get_java_bean():
    JavaBean = jclass("org.hello.JavaBean")
    jb = JavaBean("python")
    jb.setData("json")
    jb.setData("xml")
    jb.setData("xhtml")
    return jb
複製代碼

Java代碼

MainActivity.java

package org.hello;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import com.chaquo.python.Kwarg;
import com.chaquo.python.PyObject;
import com.chaquo.python.android.AndroidPlatform;
import com.chaquo.python.Python;
import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity {
    static final String TAG = "PythonOnAndroid";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initPython();
        callPythonCode();
    }
    // 初始化Python環境
    void initPython(){
        if (! Python.isStarted()) {
            Python.start(new AndroidPlatform(this));
        }
    }
    // 調用python代碼
    void callPythonCode(){
        Python py = Python.getInstance();
        // 調用hello.py模塊中的greet函數,並傳一個參數
        // 等價用法:py.getModule("hello").get("greet").call("Android");
        py.getModule("hello").callAttr("greet", "Android");
        
        // 調用python內建函數help(),輸出了幫助信息
        py.getBuiltins().get("help").call();

        PyObject obj1 = py.getModule("hello").callAttr("add", 2,3);
        // 將Python返回值換爲Java中的Integer類型
        Integer sum = obj1.toJava(Integer.class);
        Log.d(TAG,"add = "+sum.toString());
        
        // 調用python函數,命名式傳參,等同 sub(10,b=1,c=3)
        PyObject obj2 = py.getModule("hello").callAttr("sub", 10,new Kwarg("b", 1), new Kwarg("c", 3));
        Integer result = obj2.toJava(Integer.class);
        Log.d(TAG,"sub = "+result.toString());
        
        // 調用Python函數,將返回的Python中的list轉爲Java的list
        PyObject obj3 = py.getModule("hello").callAttr("get_list", 10,"xx",5.6,'c');
        List<PyObject> pyList = obj3.asList();
        Log.d(TAG,"get_list = "+pyList.toString());
        
        // 將Java的ArrayList對象傳入Python中使用
        List<PyObject> params = new ArrayList<PyObject>();
        params.add(PyObject.fromJava("alex"));
        params.add(PyObject.fromJava("bruce"));
        py.getModule("hello").callAttr("print_list", params);
  
        // Python中調用Java類
        PyObject obj4 = py.getModule("hello").callAttr("get_java_bean");
        JavaBean data = obj4.toJava(JavaBean.class);
        data.print();
    }
}

複製代碼

準備一個類,讓Python返調Java類

package org.hello;

import android.util.Log;
import java.util.ArrayList;
import java.util.List;

public class JavaBean {
    private String name;
    private List<String> data;

    public JavaBean(String n){
        this.name = n;
        data = new ArrayList<String>();
    }

    public void setData(String el){
        this.data.add(el);
    }

    public void print(){
        for (String it: data) {
            Log.d("Java Bean - "+this.name,it);
        }
    }
}
複製代碼

小結:

  1. Python沒有方法重載,一般一個函數會聲明不少參數,注意使用Kwarg類進行命名式傳參
  2. 注意對象轉換,PyObject類是橋樑,fromJava函數將一個Java對象轉換爲相應的Python對象,toJava函數正好相反,將Python中的對象轉換成Java中的對象
  3. 以上未演示map用法,實際上與List相似,對應Python中的字典對象,PyObject提供了asMap方法

進階用法

生成靜態代理

咱們可使用Python類來擴展Java,實質上就是編寫Python類後,使用工具自動生成對應的Java類

在gradle中進行配置python模塊

defaultConfig {
        python {
            staticProxy "test_class"
        }
}
複製代碼

在Python目錄中建立test_class.py

from android.os import Bundle
from android.support.v7.app import AppCompatActivity
from com.chaquo.python.hello import R
from java import jvoid, Override, static_proxy,jint,method


class MainActivityEx(static_proxy(AppCompatActivity)):

 @Override(jvoid, [Bundle])
    def onCreate(self, state):
        AppCompatActivity.onCreate(self, state)
        self.setContentView(R.layout.activity_main)

    

	''' 要想Java類生成對應方法,必須使用該裝飾器,指定返回值和參數類型 '''
 @method(jint, [jint])
    def func(self,num):
        return 1 + num
複製代碼

Make工程以後會生成對應的Java代碼。注意,生成的代碼並不在src下,在方法中引用一下MainActivityEx,並自動導包後,可點進去查看生成的源碼

// Generated at 2019-08-31T12:29:18Z with the command line:
// --path D:\workspace\flutter_space\flutter_web\hello\app\build\generated\python\sources\debug;D:\workspace\flutter_space\flutter_web\hello\app\build\generated\python\requirements\debug/common --java D:\workspace\flutter_space\flutter_web\hello\app\build\generated\python\proxies\debug test_class

package test_class;

import com.chaquo.python.*;
import java.lang.reflect.*;
import static com.chaquo.python.PyObject._chaquopyCall;

@SuppressWarnings("deprecation")
public class MainActivityEx extends android.support.v7.app.AppCompatActivity implements StaticProxy {
    static {
        Python.getInstance().getModule("test_class").get("MainActivityEx");
    }
    
    public MainActivityEx() {
        PyObject result;
        result = _chaquopyCall(this, "__init__");
        if (result != null) result.toJava(void.class);
    }
    
    @Override public void onCreate(android.os.Bundle arg0) {
        PyObject result;
        result = _chaquopyCall(this, "onCreate", arg0);
        if (result != null) result.toJava(void.class);
    }
    
    public int func(int arg0) {
        PyObject result;
        result = _chaquopyCall(this, "func", arg0);
        return result.toJava(int.class);
    }

    // 省略......
}

複製代碼

注意,要使用靜態代理生成器,Python中的類必須使用static_proxy方法進行包裝,如需生成方法,還須要使用相關的Python裝飾器,詳細用法見Static proxy文檔

靜態代理可同時配置多個

defaultConfig {
        python {
           staticProxy(
           "chaquopy.test.static_proxy.basic",
            "chaquopy.test.static_proxy.header",
           "chaquopy.test.static_proxy.method"
           )
        }
}
複製代碼

第三方庫引入

Chaquopy支持90%的純Python源碼的第三方庫,如BeautifulSoup等,固然,Python不少知名庫都是C/C++語言寫的,使用Python包裝一層而已,例如numpypillowscikit-learn等等,像這樣的二進制包,Chaquopy框架也支持一部分,這就至關可貴了,實際上,Python移植到安卓平臺,最難搞的就是第三方庫的移植。想查看Chaquopy支持哪些包含二進制包的Python庫,請點擊Chaquopy pypi

增長gradle配置

defaultConfig {
    python {
        // ......
        pip {
            install "Beautifulsoup4"
            install "requests"
            install "numpy"
        }
    }
}
複製代碼

hello.py中增長代碼

from bs4 import BeautifulSoup
import requests
import numpy as np

# ...省略...

# 爬取網頁並解析
def get_http():
    r = requests.get("https://www.baidu.com/")
    r.encoding ='utf-8'
    bsObj = BeautifulSoup(r.text,"html.parser")
    for node in bsObj.findAll("a"):
        print("---**--- ", node.text)
        
# 使用numpy
def print_numpy():
    y = np.zeros((5,), dtype = np.int)
    print(y)
複製代碼

MainActivity.java增長調用代碼

void callPythonCode(){
        // ......省略
        py.getModule("hello").callAttr("get_http");
        py.getModule("hello").callAttr("print_numpy");
    }
複製代碼

使用了網絡,還需增長網絡權限

<uses-permission android:name="android.permission.INTERNET"/>
複製代碼

徹底使用Python開發

前面說過了,Chaquopy框架能夠徹底使用Python語言編寫apk,而且開發者還提供了一個 模板工程

整個工程的main目錄下只有一個Python目錄,沒有java目錄,這實際上就是咱們以前說的靜態代理,並非沒有Java代碼,只是根據Python代碼自動生成對應的Java代碼

from android.os import Bundle
from android.support.v7.app import AppCompatActivity
from com.chaquo.python.hello import R
from java import jvoid, Override, static_proxy


class MainActivity(static_proxy(AppCompatActivity)):

 @Override(jvoid, [Bundle])
    def onCreate(self, state):
        AppCompatActivity.onCreate(self, state)
        self.setContentView(R.layout.activity_main)
複製代碼

原理解析

Chaquopy框架並未開源,所以只能經過反編譯apk來探究其實現原理

查看AndroidPlatform.class源碼,有以下方法

private void loadNativeLibs() {
    System.loadLibrary("crystax");
    System.loadLibrary("crypto_chaquopy");
    System.loadLibrary("ssl_chaquopy");
    System.loadLibrary("sqlite3");
    System.loadLibrary("python" + Common.PYTHON_SUFFIX);
    System.loadLibrary("chaquopy_java");
  }
複製代碼

當我看到crystax.so的加載代碼時,馬上明白了其實現原理,它使用的是crystax版本的ndk工具鏈,繼續查看反編譯的資源結構驗證猜測

在這裏插入圖片描述
由其資源結構,基本可知其實現方案,幾乎與我以前研究並寫的一些博客吻合,該框架的實現方式,基本與個人想法不謀而合,也是我推崇的實現方案。

簡單說就是以android的JNI技術爲橋樑,JNI技術解決了Java與C/C++混合編程的問題,而Python官方解釋器則是純C語言實現的,名爲CPython解釋器,在Android上,Python解釋器就是一個so動態庫。JNI接口使得C語言能反射Java的類與方法,而Python運行在C語言之上,那麼Python也就具有了調用Java的能力。整個過程就是Java調用C語言代碼,C再調用CPython解釋器從而執行Python代碼;Python調用CPython解釋器,CPython調用C語言代碼,C語言代碼再反射Java代碼,完成一次反調。這之間,粘合Java與CPython解釋器的一段C語言代碼,也就是Chaquopy框架乾的事,不出所料它應該就是libchaquopy_java.so

在這裏插入圖片描述

還有一點值得說說,看過Python解釋器源碼的應該知道,PyObject是CPyhton解釋器中一切對象的超類,固然,在C語言中它是一個結構體,CPython 提供的C語言API,基本上也就是將C語言結構體轉換爲PyObject實現與Python代碼的交互,Python調用C也同樣,而Chaquopy框架在處理Java與Python交互時,很巧妙的使用Java實現一個PyObject類,個人理解,它實際上就是將CPython解釋器中的PyObject映射到了一個Java類,經過操做這個類實現交互,頗有一點前端裏所謂虛擬DOM的意思。

更多深刻的具體的細節,請直接查看上面給出的我以前寫的博客。

文檔

這篇文章僅做爲一篇開胃菜,更多詳細的具體的用法,仍是須要查看Chaquopy的文檔的,查看文檔也是程序員的基本素養了

若是想學習調用Python解釋器,這裏還有編譯好的各個平臺版本的Python解釋器

缺陷

多線程 Chaquopy是線程安全的。可是,由於它基於CPython(Python參考實現),因此它受到CPython的全局解釋器鎖(GIL)的限制。這意味着儘管Python代碼能夠在任意數量的線程上運行,但在任何給定時刻只會執行其中一個線程。

內存管理 若是Python對象引用直接或間接引用原始Python對象的Java對象,則能夠建立跨語言引用循環。任何一種語言的垃圾收集器都沒法檢測到這樣的循環。避免內存泄漏。要麼在循環中的某處使用弱引用,要麼在再也不須要時手動中斷循環。

歡迎關注個人公衆號:編程之路從0到1

編程之路從0到1
相關文章
相關標籤/搜索