早在2017年的時候,出於業餘興趣,我就開始研究關於Python移植到Android上的實現方案,我一直但願能實現Android與Python的混合編程,併爲此寫了一系列博客,我但願藉助JNI技術,實現Java與Python的交互。或許是出於上班忙,時間少,精力有限,人的惰性等等緣由,一直沒有實現一套框架,下降Android與Python混編的難度,作到儘量封裝C語言代碼,讓使用者無需掌握NDK開發,C語言編程等。原理是早已走通了,剩下的就是苦力活,寫C代碼,寫JNI代碼,對接口一一封裝。html
如今終於不用遺憾了,由於已經有人作了我一直想作的事,並且是以我想要的思路。我一直關注着Android與Python混合編程的信息,當我看到Chaquopy框架時,真的難掩的開心,比我本身實現的還要開心!前端
若是有人想探尋Android與Python的混編的原理與實現,那我以前寫的博客還能派上一點用場java
Android 平臺的Python——JNI方案(二)python
Android 平臺的Python——CLE方案實現(三)android
Android 平臺的Python——編譯Python解釋器程序員
簡單的直觀的解釋,它是在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
文件夾中建立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
複製代碼
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);
}
}
}
複製代碼
小結:
Kwarg
類進行命名式傳參PyObject
類是橋樑,fromJava
函數將一個Java對象轉換爲相應的Python對象,toJava
函數正好相反,將Python中的對象轉換成Java中的對象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包裝一層而已,例如numpy
、pillow
、scikit-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"/>
複製代碼
前面說過了,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