本章将讲述Java与Native之间如何实现相互调用。我将围绕围绕如下三点来讲解。

#mermaid-svg-qeVnGlVrLWrB5ryX .label{font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family);fill:#333;color:#333}#mermaid-svg-qeVnGlVrLWrB5ryX .label text{fill:#333}#mermaid-svg-qeVnGlVrLWrB5ryX .node rect,#mermaid-svg-qeVnGlVrLWrB5ryX .node circle,#mermaid-svg-qeVnGlVrLWrB5ryX .node ellipse,#mermaid-svg-qeVnGlVrLWrB5ryX .node polygon,#mermaid-svg-qeVnGlVrLWrB5ryX .node path{fill:#ECECFF;stroke:#9370db;stroke-width:1px}#mermaid-svg-qeVnGlVrLWrB5ryX .node .label{text-align:center;fill:#333}#mermaid-svg-qeVnGlVrLWrB5ryX .node.clickable{cursor:pointer}#mermaid-svg-qeVnGlVrLWrB5ryX .arrowheadPath{fill:#333}#mermaid-svg-qeVnGlVrLWrB5ryX .edgePath .path{stroke:#333;stroke-width:1.5px}#mermaid-svg-qeVnGlVrLWrB5ryX .flowchart-link{stroke:#333;fill:none}#mermaid-svg-qeVnGlVrLWrB5ryX .edgeLabel{background-color:#e8e8e8;text-align:center}#mermaid-svg-qeVnGlVrLWrB5ryX .edgeLabel rect{opacity:0.9}#mermaid-svg-qeVnGlVrLWrB5ryX .edgeLabel span{color:#333}#mermaid-svg-qeVnGlVrLWrB5ryX .cluster rect{fill:#ffffde;stroke:#aa3;stroke-width:1px}#mermaid-svg-qeVnGlVrLWrB5ryX .cluster text{fill:#333}#mermaid-svg-qeVnGlVrLWrB5ryX div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family);font-size:12px;background:#ffffde;border:1px solid #aa3;border-radius:2px;pointer-events:none;z-index:100}#mermaid-svg-qeVnGlVrLWrB5ryX .actor{stroke:#ccf;fill:#ECECFF}#mermaid-svg-qeVnGlVrLWrB5ryX text.actor>tspan{fill:#000;stroke:none}#mermaid-svg-qeVnGlVrLWrB5ryX .actor-line{stroke:grey}#mermaid-svg-qeVnGlVrLWrB5ryX .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333}#mermaid-svg-qeVnGlVrLWrB5ryX .messageLine1{stroke-width:1.5;stroke-dasharray:2, 2;stroke:#333}#mermaid-svg-qeVnGlVrLWrB5ryX #arrowhead path{fill:#333;stroke:#333}#mermaid-svg-qeVnGlVrLWrB5ryX .sequenceNumber{fill:#fff}#mermaid-svg-qeVnGlVrLWrB5ryX #sequencenumber{fill:#333}#mermaid-svg-qeVnGlVrLWrB5ryX #crosshead path{fill:#333;stroke:#333}#mermaid-svg-qeVnGlVrLWrB5ryX .messageText{fill:#333;stroke:#333}#mermaid-svg-qeVnGlVrLWrB5ryX .labelBox{stroke:#ccf;fill:#ECECFF}#mermaid-svg-qeVnGlVrLWrB5ryX .labelText,#mermaid-svg-qeVnGlVrLWrB5ryX .labelText>tspan{fill:#000;stroke:none}#mermaid-svg-qeVnGlVrLWrB5ryX .loopText,#mermaid-svg-qeVnGlVrLWrB5ryX .loopText>tspan{fill:#000;stroke:none}#mermaid-svg-qeVnGlVrLWrB5ryX .loopLine{stroke-width:2px;stroke-dasharray:2, 2;stroke:#ccf;fill:#ccf}#mermaid-svg-qeVnGlVrLWrB5ryX .note{stroke:#aa3;fill:#fff5ad}#mermaid-svg-qeVnGlVrLWrB5ryX .noteText,#mermaid-svg-qeVnGlVrLWrB5ryX .noteText>tspan{fill:#000;stroke:none}#mermaid-svg-qeVnGlVrLWrB5ryX .activation0{fill:#f4f4f4;stroke:#666}#mermaid-svg-qeVnGlVrLWrB5ryX .activation1{fill:#f4f4f4;stroke:#666}#mermaid-svg-qeVnGlVrLWrB5ryX .activation2{fill:#f4f4f4;stroke:#666}#mermaid-svg-qeVnGlVrLWrB5ryX .mermaid-main-font{font-family:"trebuchet ms", verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-qeVnGlVrLWrB5ryX .section{stroke:none;opacity:0.2}#mermaid-svg-qeVnGlVrLWrB5ryX .section0{fill:rgba(102,102,255,0.49)}#mermaid-svg-qeVnGlVrLWrB5ryX .section2{fill:#fff400}#mermaid-svg-qeVnGlVrLWrB5ryX .section1,#mermaid-svg-qeVnGlVrLWrB5ryX .section3{fill:#fff;opacity:0.2}#mermaid-svg-qeVnGlVrLWrB5ryX .sectionTitle0{fill:#333}#mermaid-svg-qeVnGlVrLWrB5ryX .sectionTitle1{fill:#333}#mermaid-svg-qeVnGlVrLWrB5ryX .sectionTitle2{fill:#333}#mermaid-svg-qeVnGlVrLWrB5ryX .sectionTitle3{fill:#333}#mermaid-svg-qeVnGlVrLWrB5ryX .sectionTitle{text-anchor:start;font-size:11px;text-height:14px;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-qeVnGlVrLWrB5ryX .grid .tick{stroke:#d3d3d3;opacity:0.8;shape-rendering:crispEdges}#mermaid-svg-qeVnGlVrLWrB5ryX .grid .tick text{font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-qeVnGlVrLWrB5ryX .grid path{stroke-width:0}#mermaid-svg-qeVnGlVrLWrB5ryX .today{fill:none;stroke:red;stroke-width:2px}#mermaid-svg-qeVnGlVrLWrB5ryX .task{stroke-width:2}#mermaid-svg-qeVnGlVrLWrB5ryX .taskText{text-anchor:middle;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-qeVnGlVrLWrB5ryX .taskText:not([font-size]){font-size:11px}#mermaid-svg-qeVnGlVrLWrB5ryX .taskTextOutsideRight{fill:#000;text-anchor:start;font-size:11px;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-qeVnGlVrLWrB5ryX .taskTextOutsideLeft{fill:#000;text-anchor:end;font-size:11px}#mermaid-svg-qeVnGlVrLWrB5ryX .task.clickable{cursor:pointer}#mermaid-svg-qeVnGlVrLWrB5ryX .taskText.clickable{cursor:pointer;fill:#003163 !important;font-weight:bold}#mermaid-svg-qeVnGlVrLWrB5ryX .taskTextOutsideLeft.clickable{cursor:pointer;fill:#003163 !important;font-weight:bold}#mermaid-svg-qeVnGlVrLWrB5ryX .taskTextOutsideRight.clickable{cursor:pointer;fill:#003163 !important;font-weight:bold}#mermaid-svg-qeVnGlVrLWrB5ryX .taskText0,#mermaid-svg-qeVnGlVrLWrB5ryX .taskText1,#mermaid-svg-qeVnGlVrLWrB5ryX .taskText2,#mermaid-svg-qeVnGlVrLWrB5ryX .taskText3{fill:#fff}#mermaid-svg-qeVnGlVrLWrB5ryX .task0,#mermaid-svg-qeVnGlVrLWrB5ryX .task1,#mermaid-svg-qeVnGlVrLWrB5ryX .task2,#mermaid-svg-qeVnGlVrLWrB5ryX .task3{fill:#8a90dd;stroke:#534fbc}#mermaid-svg-qeVnGlVrLWrB5ryX .taskTextOutside0,#mermaid-svg-qeVnGlVrLWrB5ryX .taskTextOutside2{fill:#000}#mermaid-svg-qeVnGlVrLWrB5ryX .taskTextOutside1,#mermaid-svg-qeVnGlVrLWrB5ryX .taskTextOutside3{fill:#000}#mermaid-svg-qeVnGlVrLWrB5ryX .active0,#mermaid-svg-qeVnGlVrLWrB5ryX .active1,#mermaid-svg-qeVnGlVrLWrB5ryX .active2,#mermaid-svg-qeVnGlVrLWrB5ryX .active3{fill:#bfc7ff;stroke:#534fbc}#mermaid-svg-qeVnGlVrLWrB5ryX .activeText0,#mermaid-svg-qeVnGlVrLWrB5ryX .activeText1,#mermaid-svg-qeVnGlVrLWrB5ryX .activeText2,#mermaid-svg-qeVnGlVrLWrB5ryX .activeText3{fill:#000 !important}#mermaid-svg-qeVnGlVrLWrB5ryX .done0,#mermaid-svg-qeVnGlVrLWrB5ryX .done1,#mermaid-svg-qeVnGlVrLWrB5ryX .done2,#mermaid-svg-qeVnGlVrLWrB5ryX .done3{stroke:grey;fill:#d3d3d3;stroke-width:2}#mermaid-svg-qeVnGlVrLWrB5ryX .doneText0,#mermaid-svg-qeVnGlVrLWrB5ryX .doneText1,#mermaid-svg-qeVnGlVrLWrB5ryX .doneText2,#mermaid-svg-qeVnGlVrLWrB5ryX .doneText3{fill:#000 !important}#mermaid-svg-qeVnGlVrLWrB5ryX .crit0,#mermaid-svg-qeVnGlVrLWrB5ryX .crit1,#mermaid-svg-qeVnGlVrLWrB5ryX .crit2,#mermaid-svg-qeVnGlVrLWrB5ryX .crit3{stroke:#f88;fill:red;stroke-width:2}#mermaid-svg-qeVnGlVrLWrB5ryX .activeCrit0,#mermaid-svg-qeVnGlVrLWrB5ryX .activeCrit1,#mermaid-svg-qeVnGlVrLWrB5ryX .activeCrit2,#mermaid-svg-qeVnGlVrLWrB5ryX .activeCrit3{stroke:#f88;fill:#bfc7ff;stroke-width:2}#mermaid-svg-qeVnGlVrLWrB5ryX .doneCrit0,#mermaid-svg-qeVnGlVrLWrB5ryX .doneCrit1,#mermaid-svg-qeVnGlVrLWrB5ryX .doneCrit2,#mermaid-svg-qeVnGlVrLWrB5ryX .doneCrit3{stroke:#f88;fill:#d3d3d3;stroke-width:2;cursor:pointer;shape-rendering:crispEdges}#mermaid-svg-qeVnGlVrLWrB5ryX .milestone{transform:rotate(45deg) scale(0.8, 0.8)}#mermaid-svg-qeVnGlVrLWrB5ryX .milestoneText{font-style:italic}#mermaid-svg-qeVnGlVrLWrB5ryX .doneCritText0,#mermaid-svg-qeVnGlVrLWrB5ryX .doneCritText1,#mermaid-svg-qeVnGlVrLWrB5ryX .doneCritText2,#mermaid-svg-qeVnGlVrLWrB5ryX .doneCritText3{fill:#000 !important}#mermaid-svg-qeVnGlVrLWrB5ryX .activeCritText0,#mermaid-svg-qeVnGlVrLWrB5ryX .activeCritText1,#mermaid-svg-qeVnGlVrLWrB5ryX .activeCritText2,#mermaid-svg-qeVnGlVrLWrB5ryX .activeCritText3{fill:#000 !important}#mermaid-svg-qeVnGlVrLWrB5ryX .titleText{text-anchor:middle;font-size:18px;fill:#000;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-qeVnGlVrLWrB5ryX g.classGroup text{fill:#9370db;stroke:none;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family);font-size:10px}#mermaid-svg-qeVnGlVrLWrB5ryX g.classGroup text .title{font-weight:bolder}#mermaid-svg-qeVnGlVrLWrB5ryX g.clickable{cursor:pointer}#mermaid-svg-qeVnGlVrLWrB5ryX g.classGroup rect{fill:#ECECFF;stroke:#9370db}#mermaid-svg-qeVnGlVrLWrB5ryX g.classGroup line{stroke:#9370db;stroke-width:1}#mermaid-svg-qeVnGlVrLWrB5ryX .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5}#mermaid-svg-qeVnGlVrLWrB5ryX .classLabel .label{fill:#9370db;font-size:10px}#mermaid-svg-qeVnGlVrLWrB5ryX .relation{stroke:#9370db;stroke-width:1;fill:none}#mermaid-svg-qeVnGlVrLWrB5ryX .dashed-line{stroke-dasharray:3}#mermaid-svg-qeVnGlVrLWrB5ryX #compositionStart{fill:#9370db;stroke:#9370db;stroke-width:1}#mermaid-svg-qeVnGlVrLWrB5ryX #compositionEnd{fill:#9370db;stroke:#9370db;stroke-width:1}#mermaid-svg-qeVnGlVrLWrB5ryX #aggregationStart{fill:#ECECFF;stroke:#9370db;stroke-width:1}#mermaid-svg-qeVnGlVrLWrB5ryX #aggregationEnd{fill:#ECECFF;stroke:#9370db;stroke-width:1}#mermaid-svg-qeVnGlVrLWrB5ryX #dependencyStart{fill:#9370db;stroke:#9370db;stroke-width:1}#mermaid-svg-qeVnGlVrLWrB5ryX #dependencyEnd{fill:#9370db;stroke:#9370db;stroke-width:1}#mermaid-svg-qeVnGlVrLWrB5ryX #extensionStart{fill:#9370db;stroke:#9370db;stroke-width:1}#mermaid-svg-qeVnGlVrLWrB5ryX #extensionEnd{fill:#9370db;stroke:#9370db;stroke-width:1}#mermaid-svg-qeVnGlVrLWrB5ryX .commit-id,#mermaid-svg-qeVnGlVrLWrB5ryX .commit-msg,#mermaid-svg-qeVnGlVrLWrB5ryX .branch-label{fill:lightgrey;color:lightgrey;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-qeVnGlVrLWrB5ryX .pieTitleText{text-anchor:middle;font-size:25px;fill:#000;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-qeVnGlVrLWrB5ryX .slice{font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-qeVnGlVrLWrB5ryX g.stateGroup text{fill:#9370db;stroke:none;font-size:10px;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-qeVnGlVrLWrB5ryX g.stateGroup text{fill:#9370db;fill:#333;stroke:none;font-size:10px}#mermaid-svg-qeVnGlVrLWrB5ryX g.statediagram-cluster .cluster-label text{fill:#333}#mermaid-svg-qeVnGlVrLWrB5ryX g.stateGroup .state-title{font-weight:bolder;fill:#000}#mermaid-svg-qeVnGlVrLWrB5ryX g.stateGroup rect{fill:#ECECFF;stroke:#9370db}#mermaid-svg-qeVnGlVrLWrB5ryX g.stateGroup line{stroke:#9370db;stroke-width:1}#mermaid-svg-qeVnGlVrLWrB5ryX .transition{stroke:#9370db;stroke-width:1;fill:none}#mermaid-svg-qeVnGlVrLWrB5ryX .stateGroup .composit{fill:white;border-bottom:1px}#mermaid-svg-qeVnGlVrLWrB5ryX .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px}#mermaid-svg-qeVnGlVrLWrB5ryX .state-note{stroke:#aa3;fill:#fff5ad}#mermaid-svg-qeVnGlVrLWrB5ryX .state-note text{fill:black;stroke:none;font-size:10px}#mermaid-svg-qeVnGlVrLWrB5ryX .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.7}#mermaid-svg-qeVnGlVrLWrB5ryX .edgeLabel text{fill:#333}#mermaid-svg-qeVnGlVrLWrB5ryX .stateLabel text{fill:#000;font-size:10px;font-weight:bold;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-qeVnGlVrLWrB5ryX .node circle.state-start{fill:black;stroke:black}#mermaid-svg-qeVnGlVrLWrB5ryX .node circle.state-end{fill:black;stroke:white;stroke-width:1.5}#mermaid-svg-qeVnGlVrLWrB5ryX #statediagram-barbEnd{fill:#9370db}#mermaid-svg-qeVnGlVrLWrB5ryX .statediagram-cluster rect{fill:#ECECFF;stroke:#9370db;stroke-width:1px}#mermaid-svg-qeVnGlVrLWrB5ryX .statediagram-cluster rect.outer{rx:5px;ry:5px}#mermaid-svg-qeVnGlVrLWrB5ryX .statediagram-state .divider{stroke:#9370db}#mermaid-svg-qeVnGlVrLWrB5ryX .statediagram-state .title-state{rx:5px;ry:5px}#mermaid-svg-qeVnGlVrLWrB5ryX .statediagram-cluster.statediagram-cluster .inner{fill:white}#mermaid-svg-qeVnGlVrLWrB5ryX .statediagram-cluster.statediagram-cluster-alt .inner{fill:#e0e0e0}#mermaid-svg-qeVnGlVrLWrB5ryX .statediagram-cluster .inner{rx:0;ry:0}#mermaid-svg-qeVnGlVrLWrB5ryX .statediagram-state rect.basic{rx:5px;ry:5px}#mermaid-svg-qeVnGlVrLWrB5ryX .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#efefef}#mermaid-svg-qeVnGlVrLWrB5ryX .note-edge{stroke-dasharray:5}#mermaid-svg-qeVnGlVrLWrB5ryX .statediagram-note rect{fill:#fff5ad;stroke:#aa3;stroke-width:1px;rx:0;ry:0}:root{--mermaid-font-family: '"trebuchet ms", verdana, arial';--mermaid-font-family: "Comic Sans MS", "Comic Sans", cursive}#mermaid-svg-qeVnGlVrLWrB5ryX .error-icon{fill:#522}#mermaid-svg-qeVnGlVrLWrB5ryX .error-text{fill:#522;stroke:#522}#mermaid-svg-qeVnGlVrLWrB5ryX .edge-thickness-normal{stroke-width:2px}#mermaid-svg-qeVnGlVrLWrB5ryX .edge-thickness-thick{stroke-width:3.5px}#mermaid-svg-qeVnGlVrLWrB5ryX .edge-pattern-solid{stroke-dasharray:0}#mermaid-svg-qeVnGlVrLWrB5ryX .edge-pattern-dashed{stroke-dasharray:3}#mermaid-svg-qeVnGlVrLWrB5ryX .edge-pattern-dotted{stroke-dasharray:2}#mermaid-svg-qeVnGlVrLWrB5ryX .marker{fill:#333}#mermaid-svg-qeVnGlVrLWrB5ryX .marker.cross{stroke:#333}:root { --mermaid-font-family: "trebuchet ms", verdana, arial;}#mermaid-svg-qeVnGlVrLWrB5ryX {color: rgba(0, 0, 0, 0.75);font: ;}

Java和native相互调用
native函数注册
jni的签名signature
Java和native的代码相互调用
静态注册
动态注册
什么是jni签名
如何查看一个类中所以方法的签名
jni如何规范签名信息
如何获取class对象
如何获取属性和方法
如何构造对象
总结

一.native函数注册

当Java代码中执行Native的代码的时候,首先是通过一定的方法来找到这些native方法。这种方式就是native函数的注册,而注册native函数的具体方法不同,会导致系统在运行时采用不同的方式来寻找这些native方法。

native函数注册的注册一共有俩种方式,一种是动态注册,一种是静态注册,下面就依次来介绍一下静态注册和动态注册。

(1)静态注册

什么是静态注册?

先由Java得到本地方法的声明,然后再通过JNI实现该声明方法。

如何实现静态注册?

实现静态注册很简单,只需要根据函数名来遍历Java和JNI函数之间的关联,并且要求JNI层函数的名字必须遵循特定的格式。具体的实现很简单,首先在Java代码中声明native函数,然后通过javac或者javah相关命令来生成native函数的对应得头文件,然后在c/c++文件中引用这些头文件,最后在JNI代码中实现这些函数的具体业务逻辑即可。

好了,下面我们就根据以上原理来实现一个简单的静态注册。下面简单的看一个列子:

1. 在本地Java代码声明native函数

package com.bnd.multimedialearning.jni;public class NDKTools {//声明native函数public static native String getStringFromNDK();static {System.loadLibrary("native-lib");}}

2. 通过javac/javah来生成native函数的对应得头文件
生成头文件的方式很简单,就是利用jdk自带的命令就可以生成,可以通过javah也可以通过javac,先看通过javah命令,再看javac。

  • javah 生成头文件
    首先进入java文件无目录下,我这里包名路径是com.bnd.multimedialearning.jni.NDKTools(请自行换成自己包名)
javah -d ./jni/ -classpath /Users/YOUR_NAME/Library/Android/sdk/platforms/android-21/android.jar:../../build/intermediates/classes/debug/ com.bnd.multimedialearning.jni.NDKTools
  • javac生成头文件
    首先进入java文件无目录下,我这里包名路径是com.bnd.multimedialearning.jni.NDKTools(请自行换成自己包名)
javac -encoding utf8 -h . NDKTools.java

通过这俩个命令我们就可以获得相应的头文件,以及clsss文件,如下所示:
生成的.h文件:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_bnd_multimedialearning_jni_NDKTools */#ifndef _Included_com_bnd_multimedialearning_jni_NDKTools
#define _Included_com_bnd_multimedialearning_jni_NDKTools
#ifdef __cplusplus
extern "C" {
#endif
/** Class:     com_bnd_multimedialearning_jni_NDKTools* Method:    getStringFromNDK* Signature: ()Ljava/lang/String;*/
JNIEXPORT jstring JNICALL Java_com_bnd_multimedialearning_jni_NDKTools_getStringFromNDK(JNIEnv *, jclass);#ifdef __cplusplus
}
#endif
#endif

生成的.class文件:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package com.bnd.multimedialearning.jni;
public class NDKTools {public NDKTools() {}public static native String getStringFromNDK();static {System.loadLibrary("native-lib");}
}

我们重点来看一下.h文件。分析自后你会发现JNI方法名的规范就出来了。

jni的命令规范如下:

JNIEXPORT jstring JNICALL Java_com_bnd_multimedialearning_jni_NDKTools_getStringFromNDK(JNIEnv *, jclass)

返回值 + Java前缀+全路径类名+方法名+参数1JNIEnv+参数2jobject+其他参数

注意事项:

  1. 注意分隔符:
    Java前缀与类名以及类名之间的包名和方法名之间使用"_"进行分割;
  2. 注意静态:
    如果在Java中声明的方法是"静态的",则native方法也是static。否则不是
  3. 如果你的JNI的native方法不是通过静态注册方式来实现的,则不需要符合上面的这些规范,可以格局自己习惯随意命名

3.在JNI代码中实现这些函数
在编写jni代码之前,我们需要将第二步中生成的头文件引入使用,然后在编写jni代码。

//引入头文件
#include "com_bnd_multimedialearning_jni_NDKTools.h"
#include <jni.h>
JNIEXPORT jstring JNICALL
//native具体实现函数
Java_com_bnd_multimedialearning_jni_NDKTools_getStringFromNDK(JNIEnv *env, jclass clazz) {return (*env)->NewStringUTF(env,"Hello from C++,这是老张用传统方式实现jni的调用!");
}

注意:

先根据函数名找到对应的JNI函数。Java层在调用某个函数时,会从对应的JNI中寻找该函数,如果没有就会报错,如果存在就会建立一个关联关系,以后再调用时会直接使用这个函数,这部分的操作由虚拟机完成。

(2)动态注册

既然有了静态注册,为什么又要动态注册了,下面我们就来讲一下如何实现动态注册。

通过上面的介绍,我们知道,静态注册native方法的过程,就是Java层声明的nativ方法和JNI函数一一对应。这种关系就像是一一匹配映射的关系,但是也存在缺点,那就是只能一一对应,而且如果路径稍微错一个地方就会报错。那么有没有更好的方式让Java层的native方法和任意JNI函数连接起来。答案肯定是有的,那就是动态注册。也就是通过RegisterNatives方法把C/C++中的方法自动映射到Java中的native方法,而无需遵循特定的方法命名格式。这样就避免了写错和手动一一对应的问题。

当我们使用System.loadLibarary()方法加载so库的时候,Java虚拟机就会找到这个JNI_OnLoad函数兵调用该函数,这个函数的作用是告诉Dalvik虚拟机此C库使用的是哪一个JNI版本,如果你的库里面没有写明JNI_OnLoad()函数,VM会默认该库使用最老的JNI 1.1版本。由于最新版本的JNI做了很多扩充,也优化了一些内容,如果需要使用JNI新版本的功能,就必须在JNI_OnLoad()函数声明JNI的版本。同时也可以在该函数中做一些初始化的动作,其实这个函数有点类似于Android中的Activity中的onCreate()方法。该函数前面也有三个关键字分别是JNIEXPORTJNICALLjint。其中JNIEXPORTJNICALL是两个宏定义,用于指定该函数时JNI函数。jint是JNI定义的数据类型,因为Java层和C/C++的数据类型或者对象不能直接相互的引用或者使用,JNI层定义了自己的数据类型,用于衔接Java层和JNI层。JNI_OnLoad函数调用案列如下:

JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *vm, void *reserved) {JNIEnv *env;if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {return JNI_FALSE;}jint size = sizeof(getMethods) / sizeof(JNINativeMethod);registerNatives(env, JAVA_CLASS, getMethods, size);LOGD("Methods: %d", size);//指定jni的版本return JNI_VERSION_1_6;
}

该函数会有两个参数,其中*vm为Java虚拟机实例,查看jni.h文件,你会发现JavaVM结构体定义了如下函数。

struct _JavaVM {const struct JNIInvokeInterface* functions;#if defined(__cplusplus)jint DestroyJavaVM(){ return functions->DestroyJavaVM(this); }jint AttachCurrentThread(JNIEnv** p_env, void* thr_args){ return functions->AttachCurrentThread(this, p_env, thr_args); }jint DetachCurrentThread(){ return functions->DetachCurrentThread(this); }jint GetEnv(void** env, jint version){ return functions->GetEnv(this, env, version); }jint AttachCurrentThreadAsDaemon(JNIEnv** p_env, void* thr_args){ return functions->AttachCurrentThreadAsDaemon(this, p_env, thr_args); }
#endif /*__cplusplus*/
};

这些函数的返回值都是jint。

下面,我们举列子说明,如何动态注册。

1. 加载.so库文件

package com.bnd.multimedialearning.jni;public class SampleJni {public static native void printHello(long object);static {System.loadLibrary("SampleJni-lib");}
}
  1. 在jni中实现JNI_OnLoad方法
jint JNI_OnLoad(JavaVM* vm, void* reserved)

本列子具体实现如下:

jint JNI_OnLoad(JavaVM* vm, void* reserved){LOGD("JNI", "enter jni_onload");JNIEnv* env = NULL;jint result = -1;if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {return result;}//获取所有的函数的个数jint size = sizeof(getMethods) / sizeof(JNINativeMethod);LOGD("JNI", "jint size is '%s'\n", size);jniRegisterNativeMethods(env, className, getMethods, size);return JNI_VERSION_1_4;
}
  1. 通过RegisterNatives函数动态的注册native方法
    动态注册是通过RegisterNatives实现的,查看源码会发现如下所示:
  jint RegisterNatives(jclass clazz, const JNINativeMethod* methods,jint nMethods)

下面,我们就剖析一下RegisterNatives这个函数。
参数说明:

名称 说明
clazz 对应得Java类,包括详细的包结构,这里包结构以前用的’.‘现在要换成英文的’/’
methods 所在java类的函数名称
nMethods native函数的个数

在第二步《 在jni中实现JNI_OnLoad方法》中,我们有一个这样方法–jniRegisterNativeMethods,其实这个方法就是我们自定义的注册方法,我们看看jniRegisterNativeMethods方法内部是如何调用RegisterNatives这个函数的。

//注册NativeMethods
static int jniRegisterNativeMethods(JNIEnv* env, const char* className,const JNINativeMethod* gMethods, int numMethods)
{jclass clazz;LOGD("JNI","Registering %s natives\n", className);clazz = (env)->FindClass( className);if (clazz == NULL) {LOGE("JNI","Native registration unable to find class '%s'\n", className);return -1;}int result = 0;if ((env)->RegisterNatives(clazz, getMethods, numMethods) < 0) {LOGE("JNI","RegisterNatives failed for '%s'\n", className);result = -1;}(env)->DeleteLocalRef(clazz);return result;
}

这里你会发现,在第二步《 在jni中实现JNI_OnLoad方法》中,我们通过 jint size = sizeof(getMethods) / sizeof(JNINativeMethod);方法获得函数的个数,而java类是我们全局配置好了的,如下所示,他是包括了完整的包路径的:

static const char *className = "com/bnd/multimedialearning/jni/SampleJni";

在得到className后,我们 通过通用 clazz = (env)->FindClass( className);就能获取clazz 参数,到此,动态注册的三个参数就获取成功了。下面我们就看一下完整的动态注册代码:

#include <jni.h>
#include "Log4Android.h"
#include <stdio.h>
#include <stdlib.h>
#define  LOG_TAG  "NATIVE_LOG"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)using namespace std;#ifdef __cplusplus
extern "C" {#endifstatic const char *className = "com/bnd/multimedialearning/jni/SampleJni";//native对应得函数
static void printHello(JNIEnv *env, jobject, jlong handle) {LOGD("JNI", "native function is print to hello!");
}//获取所有的native函数数组
static JNINativeMethod getMethods[] = {{"printHello", "(J)V", (void*)printHello},
};//注册NativeMethods
static int jniRegisterNativeMethods(JNIEnv* env, const char* className,const JNINativeMethod* gMethods, int numMethods)
{jclass clazz;LOGD("JNI","Registering %s natives\n", className);clazz = (env)->FindClass( className);if (clazz == NULL) {LOGE("JNI","Native registration unable to find class '%s'\n", className);return -1;}int result = 0;if ((env)->RegisterNatives(clazz, getMethods, numMethods) < 0) {LOGE("JNI","RegisterNatives failed for '%s'\n", className);result = -1;}(env)->DeleteLocalRef(clazz);return result;
}//JNI_OnLoad动态注册:动态注册通过RegisterNatives方法把C/C++中的方法映射到Java中的native方法
//注意:
//当我们使用System.loadLibarary()方法加载so库的时候,Java虚拟机就会找到这个JNI_OnLoad函数兵调用该函数,这个函数的作用是告诉Dalvik虚拟机此C库使用的是哪一个JNI版本,如果你的库里面没有写明JNI_OnLoad()函数,VM会默认该库使用最老的JNI 1.1版本。
// 由于最新版本的JNI做了很多扩充,也优化了一些内容,如果需要使用JNI新版本的功能,就必须在JNI_OnLoad()函数声明JNI的版本。同时也可以在该函数中做一些初始化的动作,其实这个函数有点类似于Android中的Activity中的onCreate()方法。该函数前面也有三个关键字分别是JNIEXPORT,JNICALL,jint。
// 其中JNIEXPORT和JNICALL是两个宏定义,用于指定该函数时JNI函数。jint是JNI定义的数据类型,因为Java层和C/C++的数据类型或者对象不能直接相互的引用或者使用,JNI层定义了自己的数据类型,jint就是用于衔接Java层和JNI层。jint JNI_OnLoad(JavaVM* vm, void* reserved){LOGD("JNI", "enter jni_onload");JNIEnv* env = NULL;jint result = -1;if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {return result;}//获取所有的函数的个数jint size = sizeof(getMethods) / sizeof(JNINativeMethod);LOGD("JNI", "jint size is '%s'\n", size);jniRegisterNativeMethods(env, className, getMethods, size);return JNI_VERSION_1_4;
}#ifdef __cplusplus
}
#endif

好了,这样我们就实现了一个动态注册,结合上面的完整代码,我们来做一个简单流程分析。

首先看第一步中JNI_OnLoad函数的实现。JNI_OnLoad主要就是两个代码块,一个是if语句判断,一个是jniRegisterNativeMethods函数的实现。

. if语句分析

    if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {return result;}

这里调用了GetEnv函数是为了获取JNIEnv结构体指针,其实JNIEnv结构体指向了一个函数表,该函数表指向了对应的JNI函数,我们通过这些JNI函数实现JNI编程。

. jniRegisterNativeMethods函数分析
jniRegisterNativeMethods函数是自己定义的函数,内部主要是调用了RegisterNatives实现动态注册。这里面注意一个静态变量getMethods,这个静态变量是一个JNINativeMethod类型的数组。它代表的是一个native方法的数组,如果你在一个Java类中有一个native方法,这里它的size就是1,如果是两个native方法,它的size就是2…,以此类推,我这里定义的getMethods变量的实现如下所示:

//获取所有的native函数数组
static JNINativeMethod getMethods[] = {{"printHello", "(J)V", (void*)printHello},
};

细心点你会发现,他是JNINativeMethod结构体,关于JNINativeMethod 结构体,这里我不在详细介绍,在上一篇《Android JNI(三)——JNI数据结构之JNINativeMethod》里我详细介绍了有关这个结构体相关的知识,不懂的可以前往查看。然后我们接着分析动态注册的实现。

首先通过clazz = (env)->FindClass( className);找到声明native方法的类。然后通过调用RegisterNatives函数将注册函数的Java类,以及注册函数的数组,以及个数注册在一起,这样就实现了绑定。

上面提到JNINativeMethod结构体的时候,我们看到一个参数signature。什么是signaturesignature就是签名,我们下面就来讲一下签名。

二.jni的签名signature

(1)为什么要使用签名?

在上一篇《Android JNI(三)——JNI数据结构之JNINativeMethod》一篇中,我们知道变量signature,用字符串是描述了Java中函数的参数和返回值,这个参数最为复杂。既然这么复杂,为什么又要搞这个签名了,其实这个和Java语法有很大的关系。为了适应和匹配Java语法,所以才搞出了签名这个东西。

大家都知道,Java是支持函数重载的,这就意味着,可以定义相同方法名,不同参数的方法,然后Java根据其不同的参数,找到其对应的实现的方法。这样是很好,所以说JNI肯定要支持的,那JNI要怎么支持这种情况了,如果仅仅是根据函数名,没有办法找到重载的函数的,所以为了解决这个问题,JNI就衍生了一个概念——“签名”,即将参数类型和返回值类型的组合。通过这个组合关系,就实现了签名,就唯一确定了一个方法。如果拥有一个该函数的签名信息和这个函数的函数名,我们就可以顺序的找到对应的Java层中的函数了。

(2) 如何查看类中的方法的签名?

查看类中的方法签名也很简单。可以使用javap命令,如下所示:

javap -s -p Test.class

注意:

这里需要将.java文件转成.class文件,然后通过javap命令方能查看签名。

下面就看一下上面列子中CMakeNDKTools这个类的所有签名,代码如下:

package com.bnd.multimedialearning.jni;public class CMakeNDKTools {public static native String getStringFromNDK();static {System.loadLibrary("CMakeJni-lib");}
}

下面我们执行一下javap -s -p 类名.class命令,查看结果如下:

Compiled from "CMakeNDKTools.java"
public class com.bnd.multimedialearning.jni.CMakeNDKTools {public com.bnd.multimedialearning.jni.CMakeNDKTools();descriptor: ()Vpublic static native java.lang.String getStringFromNDK();descriptor: ()Ljava/lang/String;static {};descriptor: ()V
}

你会看到上面有这些稀奇古怪的东西。比如:()V()Ljava/lang/String;其实这些就是签名以后东西,下面我们就研究下签名的格式。

(3) JNI如何规范函数的签名信息

jni的签名规范或者说签名格式如下:

(参数1类型标示;参数2类型标示;参数3类型标示…)返回值类型标示。

注意:

当参数为引用类型的时候,参数类型的标示的格式为"L+包名",其中包名的.(点)要换成"/",看我上面的例子就差不多,比如String就是Ljava/lang/String;Long就是Ljava/lang/Long;…,其实细心你会发现就是该数据类型或者说该对象所在的完整包结构加类名。只是在包结构前面要加上"L",不要为我为什么要加L,难道不能是其他的,答案当然是可以,只是类型不同,下面就列出各种类型的标示

类型标示 Java类型
Z boolean
B byte
C char
S short
I int
J long
F float
D double

以上就是基本数据类型对应得标示,其实很好记,出了boolean 对应得是Z;J对应这long,其余类型均是基本数据类型首字母大写。

如果返回值是void,对应的签名是V。

重点来说一下数组以及Array这个特殊的类型.如下表所示:

类型标示 Java类型
[签名 数组
[i int[]
[Ljava/lang/Object String[]

三.Java和native代码的相互调用

在上面我们已经知道如何从JNI中调用Java类中的方法,其实在jni.h中已经定义了一系列函数来供我们调用。下面我们就以此举例说明:

(1) 获取Class对象

为了能够在C/C++中调用Java中的类,jni.h的头文件专门定义了jclass类型表示Java中Class类。JNIEnv中有3个函数可以获取jclass。查看jni.h文件你会发现是如下三个函数:

    jclass      (*FindClass)(JNIEnv*, const char*);jclass      GetObjectClass(jobject obj);jclass      GetSuperclass(jclass clazz);

先来看看Findclass:

  • Findclass
    jclass      (*FindClass)(JNIEnv*, const char*);

FindClass是通过类的名称(类的全名,这时候包名不是用’".“点号而是用”/"来区分的)来获取jclass。比如我们获取一个String。如下所示:

jclass jcl_string=env->FindClass("java/lang/String");
  • GetObjectClass
 jclass      GetObjectClass(jobject obj);

通过对象实例来获取jclass,相当于Java中的getClass()函数。

  • GetSuperclass
jclass getSuperClass(jclass obj);

通过jclass可以获取其父类的jclass对象.

(2) 获取属性方法

在Native本地代码中访问Java层的代码,常用的就是获取Java类的属性和方法。为了在C/C++获取Java层的属性和方法,JNI在jni.h头文件中定义了jfieldIDjmethodID这两种类型来分别代表Java端的属性和方法。在访问或者设置Java某个属性的时候,首先就要在本地代码中取得代表该Java类的属性的jfieldID,然后才能在本地代码中进行Java属性的操作,同样,在需要调用Java类的某个方法时,也是需要取得代表该方法的jmethodID才能进行Java方法操作。

常见的调用Java层的方法如下,都是通过JNIEnv来进行操作的,具体的方法实现如下如下所示:

jfieldID GetFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
jmethodID GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
jfieldID GetStaticFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
jmethodID GetStaticMethodID(JNIEnv *env, jclass clazz,const char *name, const char *sig);

注意:

  1. GetFieldID/GetMethodID:获取某个属性/某个方法
  2. GetStaticFieldID/GetStaticMethodID:获取某个静态属性/静态方法

细心一点你会发现,他们都是有4个参数的,而且每个参数都是*JNIEnv *envjclass clazzconst char *nameconst char *sig。关于JNIEnv,前面我们已经讲过了,这里我们就不详细讲解了,JNIEnv代表一个JNI环境接口,jclass上面也说了代表Java层中的"类"name则代表方法名或者属性名。而char *sig代表JNI中的一个特殊字段——签名,这几个字段在上面都详细的介绍过了,这里不在重复介绍。

(3) 如何构造对象

构造一个对象的方法也有多个,大概包括了如下几个:

jobject AllocObject(JNIEnv *env, jclass clazz);jobject NewObject(JNIEnv *env, jclass clazz,jmethodID methodID, ...);jobject NewObjectA(JNIEnv *env, jclass clazz,jmethodID methodID, const jvalue *args);jobject NewObjectV(JNIEnv *env, jclass clazz,jmethodID methodID, va_list args);

这里也不再一一详细介绍这几个函数的参数,以及不同,具体的可以参考我的上一篇博客《Android JNI学习(四)——JNI的常用方法的API》,里面我详细的介绍了开发中各类JNI的api,其中就包括了如上四种,今天这里不用过多语言一一讲解,就讲一下最常用的NewObject系列的使用。

NewObject函数如下:

jobject NewObject(jclass clazz, jmethodID methodID, ...)

我们知道Java类中可能有多个构造函数,当我们要指定调用某个构造函数的时候,会调用下面这个方法:

jmethodID mid = (*env)->GetMethodID(env, cls, "<init>", "()V");
obj = (*env)->NewObject(env, cls, mid);

也就是把指定的构造函数传入进去即可。下面我看看NewObject的二个主要参数。

参数名称 参数说明
clazz 是需要创建的Java对象的Class对象
methodID 传递对应得方法ID,想一想Java对象创建的时候,需要执行什么操作?就是执行构造函数。我们传入对应构造方法即可。

上面的代码也是可以进一步简化一下的,只不过会多了一个参数,如下所示:

jobject NewObjectA(JNIEnv *env, jclass clazz,
jmethodID methodID, jvalue *args);

你会发现。多的一个参数就是jvalue *args,这个参数代表的是对应构造函数的所有参数的,我们可以将传递给构造函数的所有参数放在jvalues类型的数组args中,该数组紧跟着放在methodID参数的后面。NewObject()收到数组中的这些参数后,将把它们传给对应得调用的Java方法。

上面说到,jvalue *args参数是个数组,如果参数不是数组怎么处理,jni.h同样也提供了一个方法,如下:

jobject NewObjectV(JNIEnv *env, jclass clazz,
jmethodID methodID, va_list args);

NewObjectVNewObjectA不同在于,NewObjectV将构造函数的所有参数放到在va_list类型的参数args中,该参数紧跟着放在methodID参数的后面。

四.总结

关于Java与Native之间如何实现相互调用的知识点还是很多的,设计到了动态注册和静态注册,以及jni的签名。如果你是一个新手,我建议你采用静态注册,虽然相对麻烦,但是条理清晰,只有熟悉并掌握了基本的,才能学习更复杂一点的动态注册,因为动态注册设计到了签名相关的知识,还包括了JNINativeMethod数据结构体,以及常用的jni的api,所涉及的知识点还是很多的。

如果您对jni还不是很熟,或者刚刚入门,那么我建议你先看一下我以前几篇博客,在接下来的最后一篇,我将实战讲解jni,做一个总结系统性的归纳。

  1. Android JNI(一)——NDK与JNI基础
  2. Android JNI(二)——实战JNI入门之Hello World
  3. Android JNI(三)——JNI数据结构之JNINativeMethod
  4. Android JNI学习(四)——JNI的常用方法的API

Android JNI学习(五)——Java与Native之间如何实现相互调用相关推荐

  1. Android JNI学习(六)——Java与Native实战演习

    前言: 前几篇我主要介绍了jni先关的基础知识和常用API,相信看过的童靴对JNI已经有了一定的了解,如果不了解也没关系,下面我给出了链接,可以点进去学习.接下来我将实战一个完整案例,案例很简单,就是 ...

  2. android webrtc学习五(webrtc视频数据传递和切换摄像头问题处理)

    android webrtc学习五(webrtc视频数据传递和切换摄像头问题处理) Android webrtc摄像头流程分析 1.打开摄像头 2.获取流数据 摄像头切换 问题场景:在使用华为手机(忘 ...

  3. 为什么Service之间最好不要相互调用?

    技术上来说,可以调用.但是不建议这样使用,除非你这个方法是service公用的工具类.之所以不建议调用,是为了减少耦合性,同一层之间,最好不要耦合.比如maven项目,如果A层Service调用了B层 ...

  4. Android JNI学习(四)——JNI的常用方法的API

    前三篇主要讲解了jni基础相关的理论知识,今天主要讲解一下JNI的常用方法的API,掌握了基本的理论知识和常用的API接下来才能更好的实战. jni的常用API大纲 再看API前,我建议大家主要结合官 ...

  5. jni直接转byte_JNI再探之JNI 数据类型及Java与C++之间互调

    JNI 什么是JNI JNI,全称Java NativeInterface,是一种为Java编写本地方法和JVM嵌入本地应用程序标准的应用程序接口,它允许运行在JVM上的Java代码能够与C/C++实 ...

  6. android jni中的java调c的两种方法

    Andoird 中使用了一种不同传统Java JNI的方式来定义其native的函数. 也就是java虚拟机通过一种机制可以找到对应的C函数  这里就涉及到静态注册和动态注册jni函数的方法 一.这里 ...

  7. Java与.NET 的Web Services相互调用

    一:简介 本文介绍了Java与.NET开发的Web Services相互调用的技术.本文包括两个部分,第一部分介绍了如何用.NET做客户端调用Java写的Web Services,第二部分介绍了如何用 ...

  8. Android JNI 学习(十):String Operations Api Other Apis

    一.String Operations(字符串操作) 1. NewString jstring NewString(JNIEnv *env, const jchar *unicodeChars, js ...

  9. Android JNI 学习(十一):Invocation Api

    1. 简介 Invocation API允许软件提供商在原生程序中内嵌Java虚拟机.因此可以不需要链接任何Java虚拟机代码来提供Java-enabled的应用程序. 以下代码演示如何使用: #in ...

最新文章

  1. net平台c#语言如何实现支付宝payto接口
  2. 云数据中心的网络架构
  3. 高级C语言教程-作用域
  4. CVPR 2019 | 腾讯AI Lab解读六大前沿方向及33篇入选论文
  5. (4.14)向上取整、向下取整、四舍五入取整的实例
  6. 章方:征服耶鲁教授的算法大神程序媛
  7. php post undefined index,PHP 中提示undefined index如何解决(多种方法)
  8. 标准C语言只有,只有Visual C++集成开发环境,可以编译标准C语言程序。
  9. 服务器gsql密码修改,gsql远程登录
  10. 基于麻雀算法优化的核极限学习机(KELM)分类算法 - 附代码
  11. 经典问题之约瑟夫问题_C语言实现
  12. 记录一次腾讯面试经历
  13. 基于asp.net725原创(古代)文学交流网站系统
  14. MySQL中update语句的深入分析
  15. 为什么实体类要实现serializable接口序列化
  16. helm3 使用国内原安装Weave Scope
  17. 中国烟酰胺单核苷酸(NMN)行业研究与投资预测报告(2022版)
  18. uni-app微信小程序结合腾讯地图获取定位导航以及城市选择器
  19. python的scripts里没有pip_python的scripts文件夹无pip等文件解决方法
  20. 深入了解火山PC的编码转换处理

热门文章

  1. Bootloader和App例程,实现M0基于UART的IAP升级功能,升级过程中通信中断,重新上电后Bootloader仍可运行。实现平台:STM32F030R8
  2. 746. Min Cost Climbing Stairs 题解
  3. PHP版本不同可以导入导出吗,请教高人:两个php平台之间的数据导入导出
  4. java em算法_python em算法的实现
  5. 违规停放共享单车 319人被纳入限制骑行“黑名单”
  6. 4999元起!iQOO 9 Pro今日首销:骁龙8旗舰处理器+独立显示芯片Pro
  7. 腾讯“抢”小米黑鲨做元宇宙?
  8. 售价16999元!心系天下三星W22 5G耀世发布
  9. 2021年德国汽车产量预计同比锐减18%
  10. 苏宁易购:2021年度预计商品采购总金额增至不超120亿元