前言
早在2017年的时候,出于业余兴趣,我就开始研究关于Python移植到Android上的实现方案,我一直希望能实现Android与Python的混合编程,并为此写了一系列博客,我希望借助JNI技术,实现Java与Python的交互。或许是出于上班忙,时间少,精力有限,人的惰性等等原因,一直没有实现一套框架,降低Android与Python混编的难度,做到尽可能封装C语言代码,让使用者无需掌握NDK开发,C语言编程等。原理是早已走通了,剩下的就是苦力活,写C代码,写JNI代码,对接口一一封装。
现在终于不用遗憾了,因为已经有人做了我一直想做的事,而且是以我想要的思路。我一直关注着Android与Python混合编程的信息,当我看到Chaquopy框架时,真的难掩的开心,比我自己实现的还要开心!
如果有人想探寻Android与Python的混编的原理与实现,那我之前的写的博客还能派上一点用场
Android 平台的Python——基础篇(一)
Android 平台的Python——基础篇(一)
Android 平台的Python——JNI方案(二)
Android 平台的Python——JNI方案(二)
Android 平台的Python——CLE方案实现(三)
Android 平台的Python——CLE方案实现(三)
Android 平台的Python——第三方库移植
Android 平台的Python——第三方库移植
Android 平台的Python——编译Python解释器
Android 平台的Python——编译Python解释器
Chaquopy是什么?
简单的直观的解释,它是在Android Studio中基于Gradle的构建系统实现的一个插件。它可以帮助我们用最简便的方式实现Android技术与Python混合编程。甚至对于Python的忠实拥趸来说,可以完全使用Python语言开发一个apk,基本不用写Java代码。
实际上Chaquopy并不仅仅是一个插件那么简单,它是一套框架。gradle插件这部分只是用来打包apk的而已
基础用法-快速入门
首先使用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:workspaceflutter_spaceflutter_webhelloappbuildgeneratedpythonsourcesdebug;D:workspaceflutter_spaceflutter_webhelloappbuildgeneratedpythonrequirementsdebug/common --java D:workspaceflutter_spaceflutter_webhelloappbuildgeneratedpythonproxiesdebug 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"/>
完全使用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调用Java 文档
- Java调用Python文档
- API文档
如果想学习调用Python解释器,这里还有编译好的各个平台版本的Python解释器 - android上的python解释器
缺陷
欢迎关注我的公众号:编程之路从0到1多线程 Chaquopy是线程安全的。但是,因为它基于CPython(Python参考实现),所以它受到CPython的全局解释器锁(GIL)的限制。这意味着尽管Python代码可以在任意数量的线程上运行,但在任何给定时刻只会执行其中一个线程。
内存管理 如果Python对象引用直接或间接引用原始Python对象的Java对象,则可以创建跨语言引用循环。任何一种语言的垃圾收集器都无法检测到这样的循环。避免内存泄漏。要么在循环中的某处使用弱引用,要么在不再需要时手动中断循环。