Android与Python混合编程系列-详细版

2 篇文章 1 订阅

目录

1. 前言

Android 平台的Python——基础篇(一)

Android 平台的Python——JNI方案(二)

Chaquopy是什么?

2. 基础用法-快速入门

2.1 配置依赖

2.2 编写代码

2.2.1 Python代码

2.2.2 Java代码

2.3 小结

3 进阶用法

3.1 生成静态代理

3.2 第三方库引入

4. 完全使用Python开发

4.1 原理解析

5. 文档

6. 缺陷


1. 前言

早在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方案(二)

Chaquopy是什么?

简单的直观的解释,它是在Android Studio中基于Gradle的构建系统实现的一个插件。它可以帮助我们用最简便的方式实现Android技术与Python混合编程。甚至对于Python的忠实拥趸来说,可以完全使用Python语言开发一个apk,基本不用写Java代码。

实际上Chaquopy并不仅仅是一个插件那么简单,它是一套框架。gradle插件这部分只是用来打包apk的而已。

2. 基础用法-快速入门

首先使用IDEA创建一个android3工程,创建一个Empty Activity。

请先确保你当前电脑上的Python环境可用,Chaquopy是根据当前电脑上的Python版本来选择集成对应的版本解释器到apk中的。

如你的电脑上有多个Python版本,可通过配置明确指定对应的版本。(这一步在后面有进一步的说明)

defaultConfig {
    python {
        buildPython "D:/Installed/Anaconda3/envs/python36/python.exe"
    }
}

2.1 配置依赖

(1) 工程根目录下的 build.gradle

buildscript {
    repositories {
        google()
        jcenter()
        // 设置仓库
        maven { url "https://chaquo.com/maven" }
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.6.0-rc01'
        // 导入Chaquopy框架的包                
        classpath "com.chaquo.python:gradle:7.0.2"
    }
}

说明:如果chaqupy 7.0.2与Android Gradle插件版本3.6.0不兼容,则会出现如下问题

未能应用插件[id'com.chaquo.python']没有此类属性:dslScope用于类:com.android.build.gradle.internal.api文件.DefaultAndroidSourceDirectorySet。

请参考文章:未能在渐变生成时应用插件[id'com.chaquo.python']

(2) app模块下的 build.gradle

apply plugin: 'com.android.application'
// 应用插件
apply plugin: 'com.chaquo.python'

android {
    compileSdkVersion 30
    buildToolsVersion "30.0.3"

    defaultConfig {
        applicationId "com.example.android3"
        minSdkVersion 28
        targetSdkVersion 30
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

        // 指定abi,如需在模拟器调试,增加"x86",否则指定"armeabi-v7a"即可
        ndk {
            abiFilters "armeabi-v7a", "x86"
        }

        python {
            // 指定python路径
            buildPython "D:/Installed/Anaconda3/envs/python36/python.exe"
            // 安装python库
//            pip {
//                install "numpy"
//            }
        }
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 
                      'proguard-rules.pro'
        }

    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    implementation 'androidx.appcompat:appcompat:1.0.2'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}

配置完成后,同步一下gradle,网络状况不良可能会失败,多同步几次,亲测无需代理,同步成功后,所需的依赖就准备好了。

2.2 编写代码

同步成功后,在工程中的main目录下会生成python文件夹,如未生成,手动生成一个Python Package即可,该目录即用来存放我们自己编写的python代码

2.2.1 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("com.example.android3.JavaBean")
    jb = JavaBean("python")
    jb.setData("json")
    jb.setData("xml")
    jb.setData("xhtml")
    return jb

2.2.2 Java代码

MainActivity.java

package com.example.android3;

import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import com.chaquo.python.PyObject;

import com.chaquo.python.Kwarg;
import android.util.Log;
import com.chaquo.python.Python;
import com.chaquo.python.android.AndroidPlatform;
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 com.example.android3;

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);
        }
    }
}

项目结构如下图所示:

2.3 小结

(1) Python没有方法重载,通常一个函数会声明很多参数,注意使用Kwarg类进行命名式传参

(2) 注意对象转换,PyObject类是桥梁,fromJava函数将一个Java对象转换为相应的Python对象,toJava函数正好相反,将Python中的对象转换成Java中的对象

(3) 以上未演示map用法,实际上与List类似,对应Python中的字典对象,PyObject提供了asMap方法

3 进阶用法

3.1 生成静态代理

我们可以使用Python类来扩展Java,实质上就是编写Python类后,使用工具自动生成对应的Java类

在gradle中进行配置python模块

defaultConfig {
        python {
            buildPython "D:/Installed/Anaconda3/envs/python36/python.exe"
            // Java代码生成的包名
            staticProxy "test_class"
        }
}

在Python目录中创建test_class.py

from android.os import Bundle
from androidx.appcompat.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/Build工程之后会生成对应的Java代码。注意,生成的代码并不在src下,在'\android4\app\build\generated\python\proxies\debug\test_class'目录下,该目录不可以在IDE中进行编辑 。在方法中引用一下MainActivityEx,并自动导包后,可点进去查看生成的源码

// Generated at 2020-12-28T09:49:14Z with the command line:
// --path F:\Workspace\Java_ws\IdeaProjects\android4\app\build\generated\python\sources\debug;F:\Workspace\Java_ws\IdeaProjects\android4\app\build\pip\debug/common --java F:\Workspace\Java_ws\IdeaProjects\android4\app\build\generated\python\proxies\debug 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 androidx.appcompat.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);
    }
    
    public MainActivityEx(PyCtorMarker pcm) {}
    private PyObject _chaquopyDict;
    public PyObject _chaquopyGetDict() { return _chaquopyDict; }
    public void _chaquopySetDict(PyObject dict) { _chaquopyDict = dict; }
}

注意,要使用静态代理生成器,Python中的类必须使用static_proxy方法进行包装,如需生成方法,还需要使用相关的Python装饰器,详细用法见Static proxy文档

静态代理可同时配置多个

defaultConfig {
        python {
           staticProxy(
               "chaquopy.test.static_proxy.basic",
               "chaquopy.test.static_proxy.header",
               "chaquopy.test.static_proxy.method"
           )
        }
}

3.2 第三方库引入

Chaquopy支持90%的纯Python源码的第三方库,如BeautifulSoup等,当然,Python很多知名库都是C/C++语言写的,使用Python包装一层而已,例如numpypillowscikit-learn等等,像这样的二进制包,Chaquopy框架也支持一部分,这就相当难得了,实际上,Python移植到安卓平台,最难搞的就是第三方库的移植。想查看Chaquopy支持哪些包含二进制包的Python库,请点击Chaquopy pypi

(1) 增加app gradle配置

defaultConfig {
    python {
        // ......
        pip {
            install "Beautifulsoup4"
            install "requests"
            install "numpy"
        }
    }
}

(2) 在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)

(3) 在MainActivity.java的类函数callPythonCode()中增加如下两行代码

void callPythonCode(){
        // ......省略
        py.getModule("hello").callAttr("get_http");
        py.getModule("hello").callAttr("print_numpy");
    }

使用了网络,还需增加网络权限。在'app/src/main/AndroidManifest.xml'中的</manifest>上面添加

<uses-permission android:name="android.permission.INTERNET"/>

然后,编译和运行程序。在控制台输出如下

                                                                                  

4. 完全使用Python开发

前面说过,Chaquopy框架可以完全使用Python语言编写apk,并且开发者还提供了一个 模板工程

整个工程的main目录下只有一个Python目录,没有java目录,这实际上就是我们之前说的静态代理,并不是没有Java代码,只是根据Python代码自动生成对应的Java代码

from android.os import Bundle
from androidx.appcompat.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)

4.1 原理解析

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的意思。

更多深入的具体细节,请直接查看上面给出的博客。

5. 文档

这篇文章仅作为一篇开胃菜,更多详细的具体的用法,还是需要查看Chaquopy的文档,查看文档也是程序员的基本素养了

如果想学习调用Python解释器,这里还有编译好的各个平台版本的Python解释器 - android上的python解释器

6. 缺陷

多线程: Chaquopy是线程安全的。但是,因为它基于CPython(Python参考实现),所以它受到CPython的全局解释器锁(GIL)的限制。这意味着尽管Python代码可以在任意数量的线程上运行,但在任何给定时刻只会执行其中一个线程。
内存管理: 如果Python对象直接引用或间接引用原始Python对象的Java对象,则可以创建跨语言引用循环。任何一种语言的垃圾收集器都无法检测到这样的循环。避免内存泄漏。要么在循环中的某处使用弱引用,要么在不再需要时手动中断循环。

 

  • 6
    点赞
  • 46
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
引用\[1\]:在搭建Matlab与HackRF通信的环境时,需要进行混合编程。首先需要安装Matlab2011和VS2013,并将Matlab中的mexw64当作一个函数调用。\[1\]引用\[2\]:在Android Studio中,需要在app模块下的build.gradle文件中添加com.chaquo.python插件,并配置相关参数,如指定abi和依赖项。\[2\]引用\[3\]:在配置完成后,还需要完善HackRF的操作函数,将libhackrf植入到Matlab中。\[3\] 所以,要实现Android Studio与Matlab的混合编程,你需要按照以下步骤进行操作: 1. 安装Matlab2011和VS2013,并确保它们能够正常运行。 2. 在Android Studio的build.gradle文件中,添加com.chaquo.python插件,并配置相关参数,如指定abi和依赖项。 3. 在Matlab中,将mexw64当作一个函数调用,以便与HackRF进行通信。 4. 完善HackRF的操作函数,并将libhackrf植入到Matlab中。 通过以上步骤,你就可以实现Android Studio与Matlab的混合编程,以便进行理论调试和通信实验。希望对你有帮助! #### 引用[.reference_title] - *1* *3* [VS与Matlab混合编译 - mexw64](https://blog.csdn.net/ns708865818/article/details/50740744)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down1,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [android xml java混合编程_AndroidPython混合编程](https://blog.csdn.net/weixin_39728544/article/details/109920529)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值