Android-Python混合开发 2 (.py交叉编译成.so)

上一篇文章介绍了,如何通过Chaquopy实现java和python互相调用。那么java是怎么调用python的呢?
我们找到这行代码:

PyObject pyObject_person =  py.getModule("py_util").callAttr("getPerson", 20);

点开getModule方法,如图:


111.png

发现是个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<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;
    }

    /**
     * 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 <Python.h>
#include <jni.h>
#include <android/log.h>

#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+下划线+完整包名+下划线+类名+下划线+方法名, 其中包名的点用下划线代替,具体看代码

最后项目结构是这个样子:


222.png

接下来,我们需要手动编写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

成功后,这样显示:


222.png

此时,项目中生成了lib包和obj包,如图:


333.png

此时,大功告成,运行后,点击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文件,如图:


444.png
最后,到了激动人心的时刻,把libbootstrap.so拷贝到assets/python里面。并且重命名为:bootstrap.so 同时,删除掉bootstrap.py这个原始文件, 再次运行项目,发现,成功了

到此,已经成功的实现Android调用python,并且能够把.py文件交叉编译成.so文件了

如果觉得写的不错,帮助到了您,请您公众号搜索: 神气小风 并添加关注

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,377评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,390评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,967评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,344评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,441评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,492评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,497评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,274评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,732评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,008评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,184评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,837评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,520评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,156评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,407评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,056评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,074评论 2 352

推荐阅读更多精彩内容