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

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

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

点开getModule方法,如图:


Android-Python混合开发 2 (.py交叉编译成.so)_第1张图片
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 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-Python混合开发 2 (.py交叉编译成.so)_第2张图片
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文件了

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

你可能感兴趣的:(Android-Python混合开发 2 (.py交叉编译成.so))