美文网首页
Android-Python混合开发 2 (.py交叉编译成.s

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

作者: 神气小风 | 来源:发表于2020-12-18 01:05 被阅读0次

    上一篇文章介绍了,如何通过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文件了

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

    相关文章

      网友评论

          本文标题:Android-Python混合开发 2 (.py交叉编译成.s

          本文链接:https://www.haomeiwen.com/subject/lulzgktx.html