上一篇文章介绍了,如何通过Chaquopy实现java和python互相调用。那么java是怎么调用python的呢?
我们找到这行代码:
PyObject pyObject_person = py.getModule("py_util").callAttr("getPerson", 20);
点开getModule方法,如图:
发现是个native方法,Android中jni会用到native方法,我们知道python就是由c编写的,那么Chaquopy的底层逻辑是否是通过jni实现的呢?就是先由java通过jni调用c, 然后再由c调用python?其实已经有开源项目给我们答案了
pyBridge
GitHub地址
我在运行pyBridge项目时,开始报错,说是没有libpybridge.so. 等我编译出来libpybridge.so后运行,不报错,却卡在了pybridge.c文件中的
// 代码卡在了此处
PyImport_AppendInittab("androidlog", PyInit_androidlog);
因为不懂c,实在越不过去,就更换了下思路,自己通过CrystaX_NDK,使用python3.5的版本,仿照pyBridge项目从0开始撸代码
1)新建一个Androidstudio项目
在app下的build.gradle中,添加:
android {
compileSdkVersion 26
defaultConfig {
applicationId "com.sqxf.pynative"
minSdkVersion 21
targetSdkVersion 29
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
// 添加
ndk{
abiFilters "armeabi-v7a"
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
//添加
sourceSets.main {
jni.srcDirs = []
jniLibs.srcDir 'src/main/libs'
}
// 添加
dataBinding {
enabled = true
}
}
然后编写activity_main.xml,放一个按钮,用来调用python
2)新建java类 AssetExtractor,用来把assets包资源拷贝到手机中
public class AssetExtractor {
private final static String LOGTAG = "AssetExtractor";
private Context mContext;
private AssetManager mAssetManager;
public AssetExtractor(Context context) {
mContext = context;
mAssetManager = context.getAssets();
}
/**
* Sets a version for the extracted assets version.
*
* @param version: int
*/
public void setAssetsVersion(int version) {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mContext);
SharedPreferences.Editor editor = preferences.edit();
editor.putInt("assetsVersion", version);
editor.apply();
}
/**
* Returns the version for the extracted assets.
*
* @return int
*/
public int getAssetsVersion() {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mContext);
return preferences.getInt("assetsVersion", 0);
}
/**
* Returns a list of assets in the APK.
*
* @param path: the path in the assets folder.
* @return the list of assets.
*/
public List listAssets(String path) {
List assets = new ArrayList<>();
try {
String assetList[] = mAssetManager.list(path);
if (assetList.length > 0) {
for (String asset : assetList) {
List subAssets = listAssets(path + '/' + asset);
assets.addAll(subAssets);
}
} else {
assets.add(path);
}
} catch (IOException e) {
e.printStackTrace();
}
return assets;
}
/**
* Returns the path to the assets data dir on the device.
*
* @return String with the data dir path.
*/
public String getAssetsDataDir() {
String appDataDir = mContext.getApplicationInfo().dataDir;
return appDataDir + "/assets/";
}
/**
* Copies an asset from the APK to the device.
*
* @param src: the source path in the APK.
* @param dst: the destination path in the device.
*/
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();
}
}
/**
* Copies the assets from the APK to the device.
*
* @param path: the source path
*/
public void copyAssets(String path) {
for (String asset : listAssets(path)) {
copyAssetFile(asset, getAssetsDataDir() + asset);
}
}
/**
* Recursively deletes the contents of a folder.
*
* @param file: the File object.
*/
private void recursiveDelete(File file) {
if (file.isDirectory()) {
for (File f : file.listFiles())
recursiveDelete(f);
}
Log.i(LOGTAG, "Removing " + file.getAbsolutePath());
file.delete();
}
/**
* Removes recursively the assets from the device.
*
* @param path: the path to the assets folder
*/
public void removeAssets(String path) {
File file = new File(getAssetsDataDir() + path);
recursiveDelete(file);
}
/**
* Returns if the path exists in the device assets.
*
* @param path: the path to the assets folder
* @return Boolean
*/
public Boolean existsAssets(String path) {
File file = new File(getAssetsDataDir() + path);
return file.exists();
}
}
再创建个java类,PyBridge.java,用来调用native方法
public class PyBridge {
/**
* Initializes the Python interpreter.
*
* @param datapath the location of the extracted python files
* @return error code
*/
public static native int start(String datapath);
/**
* Stops the Python interpreter.
*
* @return error code
*/
public static native int stop();
/**
* Sends a string payload to the Python interpreter.
*
* @param payload the payload string
* @return a string with the result
*/
public static native String call(String payload);
/**
* Sends a JSON payload to the Python interpreter.
*
* @param payload JSON payload
* @return JSON response
*/
public static JSONObject call(JSONObject payload) {
String result = call(payload.toString());
try {
return new JSONObject(result);
} catch (JSONException e) {
e.printStackTrace();
return null;
}
}
// Load library
static {
System.loadLibrary("pybridge");
// System.loadLibrary("my_math");
}
}
然后完成MainActivity中的按钮点击事件callPython方法:
private void callPython() {
AssetExtractor assetExtractor = new AssetExtractor(this);
assetExtractor.removeAssets("python");
assetExtractor.copyAssets("python");
String pythonPath = assetExtractor.getAssetsDataDir() + "python";
Log.e("path", "path == " + pythonPath);
// Start the Python interpreter
PyBridge.start(pythonPath);
// Call a Python function
try {
JSONObject json = new JSONObject();
json.put("function", "greet");
json.put("name", "Python 3.5");
JSONObject result = PyBridge.call(json);
String answer = result.getString("result");
binding.textview.setText(answer);
} catch (JSONException e) {
e.printStackTrace();
}
// Stop the interpreter
PyBridge.stop();
}
创建assets/python/bootstrap.py文件
bootstrap.py:
"""
This file is executed when the Python interpreter is started.
Use this file to configure all your necessary python code.
"""
import json
def router(args):
"""
Defines the router function that routes by function name.
:param args: JSON arguments
:return: JSON response
"""
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,
})
def hello(ars):
a = 10
print ("11111111111111111",a)
return a
def greet(args):
"""Simple function that greets someone."""
return 'Hello哈哈 %s' % args['name']
def add(args):
"""Simple function to add two numbers."""
return args['a'] + args['b']
def mul(args):
"""Simple function to multiply two numbers."""
return args['a'] * args['b']
routes = {
'greet': greet,
'add': add,
'mul': mul,
}
在CrystaX_NDK\crystax-ndk-10.3.2\sources\python\3.5\libs\armeabi-v7a路径下,找到stdlib.zip,拷贝到assets/python下
再创建jni文件夹,里面分别创建Android.mk, Application.mk, pybridge.c文件
pygridge.c:
/**
This file defines the JNI implementation of the PyBridge class.
It implements the native methods of the class and makes sure that
all the prints and errors from the Python interpreter is redirected
to the Android log. This is specially useful as it allows us to
debug the Python code running on the Android device using logcat.
*/
#include
#include
#include
#define LOG(x) __android_log_write(ANDROID_LOG_INFO, "pybridge", (x))
/* --------------- */
/* Android log */
/* --------------- */
static PyObject *androidlog(PyObject *self, PyObject *args)
{
char *str;
if (!PyArg_ParseTuple(args, "s", &str))
return NULL;
LOG(str);
Py_RETURN_NONE;
}
static PyMethodDef AndroidlogMethods[] = {
{"log", androidlog, METH_VARARGS, "Logs to Android stdout"},
{NULL, NULL, 0, NULL}
};
static struct PyModuleDef AndroidlogModule = {
PyModuleDef_HEAD_INIT,
"androidlog", /* m_name */
"Log for Android", /* m_doc */
-1, /* m_size */
AndroidlogMethods /* m_methods */
};
PyMODINIT_FUNC PyInit_androidlog(void)
{
return PyModule_Create(&AndroidlogModule);
}
void setAndroidLog()
{
// Inject bootstrap code to redirect python stdin/stdout
// to the androidlog module
PyRun_SimpleString(
"import sys\n" \
"import androidlog\n" \
"class LogFile(object):\n" \
" def __init__(self):\n" \
" self.buffer = ''\n" \
" def write(self, s):\n" \
" s = self.buffer + s\n" \
" lines = s.split(\"\\n\")\n" \
" for l in lines[:-1]:\n" \
" androidlog.log(l)\n" \
" self.buffer = lines[-1]\n" \
" def flush(self):\n" \
" return\n" \
"sys.stdout = sys.stderr = LogFile()\n"
);
}
/* ------------------ */
/* Native methods */
/* ------------------ */
/**
This function configures the location of the standard library,
initializes the interpreter and sets up the python log redirect.
It runs a file called bootstrap.py before returning, so make sure
that you configure all your python code on that file.
Note: the function must receives a string with the location of the
python files extracted from the assets folder.
// 这里记得改方法名,不然会找不到native方法
*/
JNIEXPORT jint JNICALL Java_com_sqxf_pynative_pybridge_PyBridge_start
(JNIEnv *env, jclass jc, jstring path)
{
LOG("Initializing the Python interpreter");
// Get the location of the python files
const char *pypath = (*env)->GetStringUTFChars(env, path, NULL);
// Build paths for the Python interpreter
char paths[512];
snprintf(paths, sizeof(paths), "%s:%s/stdlib.zip", pypath, pypath);
// Set Python paths
wchar_t *wchar_paths = Py_DecodeLocale(paths, NULL);
Py_SetPath(wchar_paths);
// Initialize Python interpreter and logging
PyImport_AppendInittab("androidlog", PyInit_androidlog);
Py_Initialize();
setAndroidLog();
// Bootstrap
PyRun_SimpleString("import bootstrap");
// Cleanup
(*env)->ReleaseStringUTFChars(env, path, pypath);
PyMem_RawFree(wchar_paths);
return 0;
}
JNIEXPORT Java_com_sqxf_pynative_pybridge_PyBridge_stop
(JNIEnv *env, jclass jc)
{
LOG("Finalizing the Python interpreter");
Py_Finalize();
return 0;
}
/**
This function is responsible for receiving a payload string
and sending it to the router function defined in the bootstrap.py
file.
*/
JNIEXPORT jstring JNICALL Java_com_sqxf_pynative_pybridge_PyBridge_call
(JNIEnv *env, jclass jc, jstring payload)
{
LOG("Call into Python interpreter");
char *hellos="aaaaaaaaaaaa";
// Get the payload string
jboolean iscopy;
const char *payload_utf = (*env)->GetStringUTFChars(env, payload, &iscopy);
// Import module
PyObject* myModuleString = PyUnicode_FromString((char*)"bootstrap");
PyObject* myModule = PyImport_Import(myModuleString);
PyObject* myhelloFunction = PyObject_GetAttrString(myModule, (char*)"hello");
PyObject* helloargs = PyTuple_Pack(1, PyUnicode_FromString(hellos));
PyObject_CallObject(myhelloFunction, helloargs);
// Get reference to the router function
PyObject* myFunction = PyObject_GetAttrString(myModule, (char*)"router");
PyObject* args = PyTuple_Pack(1, PyUnicode_FromString(payload_utf));
// Call function and get the resulting string
PyObject* myResult = PyObject_CallObject(myFunction, args);
char *myResultChar = PyUnicode_AsUTF8(myResult);
// Store the result on a java.lang.String object
jstring result = (*env)->NewStringUTF(env, myResultChar);
// Cleanup
(*env)->ReleaseStringUTFChars(env, payload, payload_utf);
Py_DECREF(myModuleString);
Py_DECREF(myModule);
Py_DECREF(myFunction);
Py_DECREF(args);
Py_DECREF(myResult);
return result;
}
这里着重说一下,native方法名命名规则,为: Java+下划线+完整包名+下划线+类名+下划线+方法名, 其中包名的点用下划线代替,具体看代码
最后项目结构是这个样子:
接下来,我们需要手动编写Android.mk,和Application.mk文件,为交叉编译做准备
Android.mk:
LOCAL_PATH := $(call my-dir)
CRYSTAX_PATH := E:\android\CrystaX_NDK\crystax-ndk-10.3.2
# Build libpybridge.so
include $(CLEAR_VARS)
LOCAL_MODULE := pybridge
LOCAL_SRC_FILES := pybridge.c
LOCAL_LDLIBS := -llog
LOCAL_SHARED_LIBRARIES := python3.5m
include $(BUILD_SHARED_LIBRARY)
# Include libpython3.5m.so
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/
include $(PREBUILT_SHARED_LIBRARY)
Application.mk:
APP_PLATFORM := android-21
APP_ABI := armeabi-v7a
3)开始交叉编译,这里使用CrystaX_NDK进行编译,打开cmd,cd到项目中的jni目录下,执行命令: crystax-ndk路径/ndk-build
成功后,这样显示:
此时,项目中生成了lib包和obj包,如图:
此时,大功告成,运行后,点击button,开始调用native代码,最后,在textivew中显示的就是bootstrap.py文件返回的数据
4) 如何把bootstrap.py文件交叉编译成.so文件?
通过刚才的编译,我们已经把pybridge.c文件成功编译成了.so文件了,所以如果能把bootstrap.py先转成.c文件,然后用Android.mk文件就能够再编译出.so文件了。这里通过Cython把.py转换成.c文件
一 单独创建一个文件夹,把bootstrap.py文件拷贝进去,再创建setup.py文件,编写代码:
from distutils.core import setup
from Cython.Build import cythonize
# 填写你要转换的.py文件
setup(ext_modules = cythonize(["bootstrap.py"]))
cmd到目录中,执行命令:
python setup.py build_ext
完成后,你会发现文件夹中出现了bootstrap.c文件,把bootstrap.c文件拷贝到jni目录中,同时,更改Android.mk文件,添加把bootstrap.c编译的代码:
include $(CLEAR_VARS)
LOCAL_MODULE := bootstrap
LOCAL_SRC_FILES := bootstrap.c
LOCAL_LDLIBS := -llog
LOCAL_SHARED_LIBRARIES := python3.5m
include $(BUILD_SHARED_LIBRARY)
最后,再次使用CrystaX_NDK进行编译,完成后,发现,在libs文件夹中,生成了libbootstrap.so文件,如图: