一,项目前提:

我们目前是想实现一个人脸识别考勤的项目,而厂商给我们所提供的是c++封装好的jdk 。为了方便跟我们的Java平台对接,因此需要一些手段将C++项目融入到我们的Java 平台当中。我们最终选用JNA来对c++ sdk 来进行封装。项目使用jdk(1.8.0_201)、 idea(2018.3.3)、jna版本(3.4.0)

ps:小编在jna版本上曾经踩过坑,发现使用的jna(4.2.2), 运行代码始终不成功,类继承 Structure 类的时候 。重写

getFieldOrder() 方法写法也很繁琐, 后来发现很多人都用的是3.4.0版本的JNA 。并且这个版本的jna可以不用非要去重写getFieldOrder()这个方法。后面我们会提到为什么可以不用重写这个方法了。

二,项目方案选择:

JNI:是Java Native Interface的缩写,它提供了若干的API实现了Java和其他语言的通信(主要是C&C++)。从Java1.1开始,JNI标准成为java平台的一部分,它允许Java代码和其他语言写的代码进行交互。

1,JNI

1,jni的简单应用

2,jni在java中的应用(jdk1.1 之后 才加入了JNI的概念:在 IO 、net 、lang包下面偏多)。下面我们去JDK中IO中去看下这些本地方法吧。

    /*** Reads a subarray as a sequence of bytes.* @param b the data to be written* @param off the start offset in the data* @param len the number of bytes that are written* @exception IOException If an I/O error has occurred.*/private native int readBytes(byte b[], int off, int len) throws IOException;

这个方法是出现在 FileInputStream 这个类中。

    /*** Reads up to <code>b.length</code> bytes of data from this input* stream into an array of bytes. This method blocks until some input* is available.** @param      b   the buffer into which the data is read.* @return     the total number of bytes read into the buffer, or*             <code>-1</code> if there is no more data because the end of*             the file has been reached.* @exception  IOException  if an I/O error occurs.*/public int read(byte b[]) throws IOException {return readBytes(b, 0, b.length);}/*** Reads up to <code>len</code> bytes of data from this input stream* into an array of bytes. If <code>len</code> is not zero, the method* blocks until some input is available; otherwise, no* bytes are read and <code>0</code> is returned.** @param      b     the buffer into which the data is read.* @param      off   the start offset in the destination array <code>b</code>* @param      len   the maximum number of bytes read.* @return     the total number of bytes read into the buffer, or*             <code>-1</code> if there is no more data because the end of*             the file has been reached.* @exception  NullPointerException If <code>b</code> is <code>null</code>.* @exception  IndexOutOfBoundsException If <code>off</code> is negative,* <code>len</code> is negative, or <code>len</code> is greater than* <code>b.length - off</code>* @exception  IOException  if an I/O error occurs.*/public int read(byte b[], int off, int len) throws IOException {return readBytes(b, off, len);}

然后这个 readBytes(b, off, len) 方法就会调用上面的这个本地Native 方法。至于这些方法的作用,自己看一看应该也能看得懂,但至于Native内部怎么实现,也不必太过追究。

2,JNA的使用

1,JNA相比JNI有什么优势呢?

下面来看一看JNA的维基百科解释

PS:关键点在于:JNA的设计旨在以最少的努力以自然的方式提供原生访问。它能支持macOS,Microsoft Windows,FreeBSD / OpenBSD,Solaris,Linux,AIX,Windows Mobile和Android 这么多的平台。so, 我们选型JNA(3.4.0)


三、JNA爬坑历路

1,结构体和我们的Java类的数据类型的转换问题。

这里给老司机们推荐几篇文章,https://blog.csdn.net/ctwy291314/article/details/82895604 , https://blog.csdn.net/ctwy291314/article/details/84626829 等等文章。这里其实有一些我没那样用,但是我的程序运行没有问题的。 例如 c中 int 对应 java 中的 int 。还有附上JNA在GitHub(全球最大的同性交友网站)上的源码地址:https://github.com/java-native-access/jna 。

这里是在网上找的对数据类型转换的一张图表。我在使用过程中大体没有出现什么问题。

下面是我在程序中用到的一些数据类型转换的图表,还有就是我的demo。

( 查询人脸识别人员组信息,pstInParam与pstOutParam内存由用户申请释放)

// 查询人脸识别人员组信息,pstInParam与pstOutParam内存由用户申请释放
CLIENT_NET_API BOOL CALL_METHOD CLIENT_FindGroupInfo(LLONG lLoginID, const NET_IN_FIND_GROUP_INFO* pstInParam, NET_OUT_FIND_GROUP_INFO *pstOutParam, int nWaitTime = 1000);

PS:这里的LLONG是我们自己自定义的一个类型。目的是考虑兼容32bit的操作系统 和 64bit的操作系统。

下面是我对LLONG这种数据类型的封装,代码如下:

    public static class LLong extends IntegerType {private static final long serialVersionUID = 1L;/** Size of a native long, in bytes. */public static int size;static {size = Native.LONG_SIZE;if (Utils.getOsPrefix().toLowerCase().equals("linux-amd64")|| Utils.getOsPrefix().toLowerCase().equals("win32-amd64")) {size = 8;} else if (Utils.getOsPrefix().toLowerCase().equals("linux-i386")|| Utils.getOsPrefix().toLowerCase().equals("win32-x86")) {size = 4;}}
// 获取操作平台信息public static String getOsPrefix() {String arch = System.getProperty("os.arch").toLowerCase();final String name = System.getProperty("os.name");String osPrefix;switch(Platform.getOSType()) {case Platform.WINDOWS: {if ("i386".equals(arch))arch = "x86";osPrefix = "win32-" + arch;}break;case Platform.LINUX: {if ("x86".equals(arch)) {arch = "i386";}else if ("x86_64".equals(arch)) {arch = "amd64";}osPrefix = "linux-" + arch;}                break;default: {osPrefix = name.toLowerCase();if ("x86".equals(arch)) {arch = "i386";}if ("x86_64".equals(arch)) {arch = "amd64";}int space = osPrefix.indexOf(" ");if (space != -1) {osPrefix = osPrefix.substring(0, space);}osPrefix += "-" + arch;}break;}return osPrefix;}
public static String getOsName() {String osName = "";String osPrefix = getOsPrefix();if(osPrefix.toLowerCase().startsWith("win32-x86")||osPrefix.toLowerCase().startsWith("win32-amd64") ) {osName = "win";} else if(osPrefix.toLowerCase().startsWith("linux-i386")|| osPrefix.toLowerCase().startsWith("linux-amd64")) {osName = "linux";}return osName;}

上面写的一些Util来的这一些方法,就是对操作系统long类型的判断,32位系统的 long 类型占4字节 , 64位系统的long类型占 8字节。

下面我们来看这个接口的C++中的封装。

void CDispatchGroupDlg::RefreshDispatchInfo(const char *szGroupId, BOOL bRefreshShow)
{int i = 0;BOOL bRet = FALSE;NET_IN_FIND_GROUP_INFO stuInParam = {sizeof(stuInParam)};NET_OUT_FIND_GROUP_INFO stuOutParam = {sizeof(stuOutParam)};stuOutParam.nMaxGroupNum = 50;NET_FACERECONGNITION_GROUP_INFO *pGroupInfo = NULL;stuOutParam.pGroupInfos = new NET_FACERECONGNITION_GROUP_INFO[stuOutParam.nMaxGroupNum];if (NULL == stuOutParam.pGroupInfos){MessageBox(ConvertString("Memory error"), "");bRet = FALSE;goto e_clear;}memset(stuOutParam.pGroupInfos, 0, sizeof(NET_FACERECONGNITION_GROUP_INFO)*stuOutParam.nMaxGroupNum);for (i = 0; i < stuOutParam.nMaxGroupNum; i++){pGroupInfo = stuOutParam.pGroupInfos + i;pGroupInfo->dwSize = sizeof(*pGroupInfo);}bRet = CLIENT_FindGroupInfo(m_lLoginID, &stuInParam, &stuOutParam, DEFAULT_WAIT_TIME);if (!bRet){MessageBox(ConvertString("Failed to find group infos!"), "");bRet = FALSE;goto e_clear;}for (i = 0; i < stuOutParam.nRetGroupNum; i++){pGroupInfo = stuOutParam.pGroupInfos + i;if (0 == strcmp(szGroupId, pGroupInfo->szGroupId)){memcpy(&m_stuDispatchGroupInfo, pGroupInfo, sizeof(m_stuDispatchGroupInfo));}}if (bRefreshShow){CleanDispatchList();m_DispatchInfoList.ResetContent();for (i = 0; i < m_stuDispatchGroupInfo.nRetChnCount; i++){CString str;str.Format("%4d                   %4d                %s", m_stuDispatchGroupInfo.nChannel[i]+1, m_stuDispatchGroupInfo.nSimilarity[i], "已布控");m_DispatchInfoList.AddString(str);NET_DISPATCH_INFO *pstDispatchInfo = new NET_DISPATCH_INFO;if (pstDispatchInfo){memset(pstDispatchInfo, 0, sizeof(*pstDispatchInfo));//pstDispatchInfo->nIndex = i;pstDispatchInfo->nChannel = m_stuDispatchGroupInfo.nChannel[i];pstDispatchInfo->nSimilarity = m_stuDispatchGroupInfo.nSimilarity[i];pstDispatchInfo->bDispatch = TRUE;m_lstDispatchChannelInfo.push_back(pstDispatchInfo);}}}

首先 NET_IN_FIND_GROUP_INFO stuInParam = {sizeof(stuInParam)}; sizeof() 库函数在c++中返回结构体对象返回的字节数。

// CLIENT_FindGroupInfo接口输入参数
typedef struct tagNET_IN_FIND_GROUP_INFO
{DWORD               dwSize;char                szGroupId[ST_COMMON_STRING_64];// 人员组ID,唯一标识一组人员,为空表示查询全部人员组信息
}NET_IN_FIND_GROUP_INFO;// CLIENT_FindGroupInfo接口输出参数
typedef struct tagNET_OUT_FIND_GROUP_INFO
{DWORD               dwSize;NET_FACERECONGNITION_GROUP_INFO *pGroupInfos;      // 人员组信息,由用户申请空间,大小为sizeof(NET_FACERECONGNITION_GROUP_INFO)*nMaxGroupNumint                 nMaxGroupNum;                  // 当前申请的数组大小int                 nRetGroupNum;                  // 设备返回的人员组个数
}NET_OUT_FIND_GROUP_INFO;

下面先来看看这两个结构体:

本例中用到的数据类型转换(跟上面不一样的数据类型)
demo java c++
  int DWORD
  short

WORD

  LLONG LONG
public class NET_IN_FIND_GROUP_INFO extends Structure implements Common{public int dwSize;public byte[] szGroupId = new byte[NET_COMMON_STRING_64];//人员组ID,唯一标识一组人员,为空表示查询全部人员组信息public NET_IN_FIND_GROUP_INFO(){this.dwSize = this.size();}@Overrideprotected List getFieldOrder() {return Arrays.asList(new String[] { "dwSize", "szGroupId" ,});}
}
public class NET_FACERECONGNITION_GROUP_INFO extends Structure implements Common {public int             dwSize;public int           emFaceDBType;                                     // 人员组类型,详见EM_FACE_DB_TYPE, 取值为EM_FACE_DB_TYPE中的值public byte[]        szGroupId = new byte[NET_COMMON_STRING_64];          // 人员组ID,唯一标识一组人员(不可修改,添加操作时无效)public byte[]      szGroupName = new byte[NET_COMMON_STRING_128];   // 人员组名称public byte[]         szGroupRemarks = new byte[NET_COMMON_STRING_256]; // 人员组备注信息public int             nGroupSize;                                       // 当前组内人员数public int          nRetSimilarityCount;                              // 实际返回的库相似度阈值个数public int[]      nSimilarity = new int[MAX_SIMILARITY_COUNT];     // 库相似度阈值,人脸比对高于阈值认为匹配成功public int         nRetChnCount;                                     // 实际返回的通道号个数public int[]         nChannel = new int[NET_MAX_CAMERA_CHANNEL_NUM];   // 当前组绑定到的视频通道号列表public int[]        nFeatureState = new int[MAX_FEATURESTATE_NUM];   // 人脸组建模状态信息:// [0]-准备建模的人员数量,不保证一定建模成功// [1]-建模失败的人员数量,图片不符合算法要求,需要更换图片// [2]-已建模成功人员数量,数据可用于算法进行人脸识别// [3]-曾经建模成功,但因算法升级变得不可用的数量,重新建模就可用public NET_FACERECONGNITION_GROUP_INFO(){this.dwSize = this.size();}@Overrideprotected List getFieldOrder() {return Arrays.asList(new String[] { "dwSize", "emFaceDBType","szGroupId","szGroupName", "szGroupRemarks" ,"nGroupSize", "nRetSimilarityCount","nSimilarity","nRetChnCount", "nChannel" ,"nFeatureState",});}}

我的Common 接口 :都是放一些常量的接口,读者可不必在意。

this.dwSize = this.size(); 通过撸Structure 这个类的源码才知道 这个类里面的Size,所以我们用构造方法来初始化结构体的dwSize;

下面我们来看一下Structure这个类的calculateSize() 方法 的具体的实现方法,有兴趣的可以去撸一撸这个地方的实现原理。这里解决了 sizeof()  这个c++ 中的类库函数了。

int calculateSize(boolean force, boolean avoidFFIType) {boolean needsInit = true;Structure.LayoutInfo info;synchronized(layoutInfo) {info = (Structure.LayoutInfo)layoutInfo.get(this.getClass());}if (info == null || this.alignType != info.alignType || this.typeMapper != info.typeMapper || !this.fieldOrderMatch(info.fieldOrder)) {info = this.deriveLayout(force, avoidFFIType);needsInit = false;}if (info != null) {this.structAlignment = info.alignment;this.structFields = info.fields;info.alignType = this.alignType;info.typeMapper = this.typeMapper;info.fieldOrder = this.fieldOrder;if (!info.variable) {synchronized(layoutInfo) {layoutInfo.put(this.getClass(), info);}}if (needsInit) {this.initializeFields();}return info.size;} else {return -1;}}

PS:这个地方遇到的坑:1,刚开始我么还没有收到厂家的关于C++ 的代码,刚开始都不知道怎么入参合适,都不知道dwsize这个字段是个啥意思。后来他们给了厂家的c++的代码,于是马上装了一个vs 2015的社区版,在大佬的帮助下才艰难的把这个C++ 的程序跑起来。(因为我也是一直做的Java开发,对C++也不是很熟悉,全靠大学的那一点儿基础,还好大学学的C,C++ 还勉强算学的可以的类型,不然人都要炸掉。)

在调用这个接口的时候遇到了以下的问题:

1,NET_IN_FIND_GROUP_INFO stuIn = new NET_IN_FIND_GROUP_INFO();

stuIn = 1000;的这个时候完全不知道这里该怎么赋值, 但是和厂家那边沟通他们那边感觉也不是开发人员,只知道这里是一定要赋值的,然后我就在这里赋值1000。 发现调用这个CLIENT_FindGroupInfo()接口的时候接口始终返回false;

后来给 NET_OUT_FIND_GROUP_INFO stuOut = new NET_OUT_FIND_GROUP_INFO();

后来给 stuOut.dwSize = 1000; 发现调用这个CLIENT_FindGroupInfo()接口的时候接口始终返回true 了;

解决方案: 后来发现 Structure 这个类中有一个size 参数,就是对应的是dwSize 这个参数的值:所以在构造函数中就将这个数值赋值进去。

2,我先把这个java部分代码贴出来:

public NET_FACERECONGNITION_GROUP_INFO[] findGroupInfo(LLong loginHandle , String groupId) {NET_FACERECONGNITION_GROUP_INFO[] groupInfoRet = null;/** 入参*/NET_IN_FIND_GROUP_INFO stuIn = new NET_IN_FIND_GROUP_INFO();System.arraycopy(groupId.getBytes(), 0, stuIn.szGroupId, 0, groupId.getBytes().length);/** 出参*/int max = 20;NET_FACERECONGNITION_GROUP_INFO[] groupInfo  = new NET_FACERECONGNITION_GROUP_INFO[max];for(int i = 0; i < max; i++) {groupInfo[i] = new NET_FACERECONGNITION_GROUP_INFO();}NET_OUT_FIND_GROUP_INFO stuOut = new NET_OUT_FIND_GROUP_INFO();stuOut.pGroupInfos = new Memory(groupInfo[0].size() * groupInfo.length);     // Pointer初始化stuOut.pGroupInfos.clear(groupInfo[0].size() * groupInfo.length);stuOut.nMaxGroupNum = groupInfo.length;ToolKits.SetStructArrToPointerData(groupInfo, stuOut.pGroupInfos);  // 将数组内存拷贝给Pointerif(Clibrary.INSTANCE.CLIENT_FindGroupInfo(loginHandle , stuIn, stuOut, 4000)) {// 将Pointer的值输出到 数组 NET_FACERECONGNITION_GROUP_INFOToolKits.GetPointerDataToStructArr(stuOut.pGroupInfos, groupInfo);if(stuOut.nRetGroupNum > 0) {// 根据设备返回的,将有效的人脸库信息返回groupInfoRet = new NET_FACERECONGNITION_GROUP_INFO[stuOut.nRetGroupNum];for(int i = 0; i < stuOut.nRetGroupNum; i++) {groupInfoRet[i] = groupInfo[i];}}} else {log.info("查询人员信息失败");return null;}return groupInfoRet;}

如下是ToolKits 这个类的部分代码:

/*** 将结构体数组拷贝到内存** @param pNativeData* @param pJavaStuArr*/public static void SetStructArrToPointerData(Structure[] pJavaStuArr, Pointer pNativeData) {long offset = 0;for (int i = 0; i < pJavaStuArr.length; ++i) {SetStructDataToPointer(pJavaStuArr[i], pNativeData, offset);offset += pJavaStuArr[i].size();}}public static void SetStructDataToPointer(Structure pJavaStu, Pointer pNativeData, long OffsetOfpNativeData) {pJavaStu.write();Pointer pJavaMem = pJavaStu.getPointer();pNativeData.write(OffsetOfpNativeData, pJavaMem.getByteArray(0, pJavaStu.size()), 0, pJavaStu.size());}

头文件的说明:

NET_FACERECONGNITION_GROUP_INFO *pGroupInfos;      // 人员组信息,由用户申请空间,大小为sizeof(NET_FACERECONGNITION_GROUP_INFO)*nMaxGroupNum。

C++中的代码:

memset(stuOutParam.pGroupInfos, 0, sizeof(NET_FACERECONGNITION_GROUP_INFO)*stuOutParam.nMaxGroupNum);

Java中的代码:

        stuOut.pGroupInfos = new Memory(groupInfo[0].size() * groupInfo.length);     // Pointer初始化stuOut.pGroupInfos.clear(groupInfo[0].size() * groupInfo.length);stuOut.nMaxGroupNum = groupInfo.length;ToolKits.SetStructArrToPointerData(groupInfo, stuOut.pGroupInfos);  // 将数组内存拷贝给Pointer

对于这种在  NET_OUT_FIND_GROUP_INFO 类中 有一个属性是 pGroupInfos 是 Pointer 类型的。

public Pointer pGroupInfos; // 人员组信息,由用户申请空间, 指向 NET_FACERECONGNITION_GROUP_INFO 的指针

说明:这个指针空间需要自己去开辟,指针指向NET_FACERECONGNITION_GROUP_INFO这个结构体。

1,首先用 Memory 开辟内存空间,然后调用java.sun.jna.Memory 类中的 clear()方法,目的是锁定内存,

本质是调用了jni里面的setMemory 方法。

2,结构体和Pointer之间的关系:

比如上面,我们会出现很多将Pointer 指向 结构体,

public static void SetStructDataToPointer(Structure pJavaStu, Pointer pNativeData, long OffsetOfpNativeData) {pJavaStu.write();Pointer pJavaMem = pJavaStu.getPointer();pNativeData.write(OffsetOfpNativeData, pJavaMem.getByteArray(0, pJavaStu.size()),     0, pJavaStu.size());}

pJavaStu.getPointer();便可以得到一个Pointer类型了

pJavaStu.write();  Writes the fields of the struct to native memory

pJavaStu.read(); Reads the fields of the struct from native memory

https://java-native-access.github.io/jna/4.2.0/com/sun/jna/Structure.html 上面截图的来源;

2,数据类型转换问题:

下面是我自己整理的 java 和 C++ 平台数据类型转换的一个表格

数据类型转换
Java C++ 额外可以
LLONG(自己定义)64bit 就是 LONG 类型 LLONG  
int DWORD  
int BOOL  
LONG LDWORD  
Pointer void*  
short WORD  
String / Pointer char*  
Structure struct*/struct  
String const char * chDVRIP  
IntByReference
int * error  
Pointer stuUIDS Structure *stuUIDs  
Pointer pBuffer char *  pBuffer  

四、总结

至此我在JNA这一块儿爬坑目前看似是告一段落,终于把接入商汤封装的SDK摄像头进行了封装。但是对 Structure 这个类的很多属性和 方法 仍然还不知道 是怎么设计和 运行的。比如 ALIGN_DEFAULT 、 ALIGN_NONE 、 ALIGN_GNUC 、ALIGN_MSVC 这四个 属性分别代表什么意思呢?欢迎大家在博客下多多讨论交流这方面的经验。

Jna调用C++使用心得分享相关推荐

  1. 腾讯广告算法大赛 | 复赛第一周周冠军心得分享

    腾讯广告算法大赛 | 复赛第一周周冠军心得分享 腾讯广告算法大赛复赛第一周周冠军揭晓, 熟悉的队伍,熟悉的配方! 没错,依然是你们熟悉的葛文强团队! 今天,他们将对FFM方法进行详细介绍. 小板凳儿排 ...

  2. 【12月原创】RT-thread - 柿饼UI学习心得分享

    柿饼UI学习心得分享(2) 概述 介绍: Persimmon 是一套运行在RT-Thread嵌入式实时操作系统上的图形用户组件界面,用于提供图形界面的用户交互. 它采用C++语言编写,基于C语言实现的 ...

  3. ZooKeeper源码阅读心得分享+源码基本结构+源码环境搭建

    首发CSDN:徐同学呀,原创不易,转载请注明源链接.我是徐同学,用心输出高质量文章,希望对你有所帮助. 一.心得分享 如何阅读ZooKeeper源码?从哪里开始阅读?最近把ZooKeeper源码看了个 ...

  4. python 爬虫抓取心得分享

    /** author: insun title:python 爬虫抓取心得分享 blog:http://yxmhero1989.blog.163.com/blog/static/11215795620 ...

  5. Atitit.java jna  调用c  c++ dll的原理与实践  总结  v2  q27

    Atitit.java jna  调用c  c++ dll的原理与实践  总结  v2  q27 1. Jna简单介绍1 2. Jna范例halo owrld1 3. Jna概念2 3.1. (1)需 ...

  6. html从入门到精通前锋,街篮新手攻略 从入门到精通的心得分享二

    街篮毕竟是一款竞技手游,上期介绍了街篮的一些玩法和基本技巧,本期就不再提介绍而是针对实战,以下就是将街篮的实战技巧分享给大家,希望对大家了解街篮有所帮助. (本文为超好玩原创攻略,转载请注明出处) 推 ...

  7. linux jna调用so动态库

    文中提到:为什么命名为libtest.so而不是test.so呢?因为jna在找so文件的时候,要匹配前缀为lib的so文件 http://zhenaihua0213.blog.163.com/blo ...

  8. 转:SAP 零售业POS心得分享

    转:SAP 零售业POS心得分享 转:SAP 零售业POS心得分享 最近看了一些SAP进行中的零售业项目,觉得有些心得,希望透过本篇文章让大家多了解SAP跟零售业POS连接的做法,能够更顺利地完成项目 ...

  9. Qt for Android 调用android原生接口分享图片或文字

    在用Qt开发android应用的时候,有一个需求是通过调用android原生接口去实现图片分享功能,原理很简单,首先在java文件中用android接口封装一个分享功能的方法,然后在C++中调用QAn ...

最新文章

  1. 关于版本控制工具GitHub安装报错
  2. Kali Linux重设root密码
  3. 国家航天局:中国空间站预计到2022年前后建成
  4. 仅需2张图,AI便可生成完整运动过程
  5. approxPolyDP函数
  6. Android的Menu状态动态设置方法onPrepareOptionsMenu(Menu menu) (转载)
  7. 通过实战跑分来展示HBase2.x的写入性能
  8. 机器学习中的逻辑回归
  9. 知识库 IIS6.0中Response 对象 错误 ASP 0251 : 80004005
  10. 《JavaScript》高级程序设计---第3章
  11. css背景图background - 多背景定义
  12. ubuntu 上的python不能解析jpeg,png?
  13. c# 读取写入excel单元格(包括对excel的一些基本操作)
  14. C#入门篇5-3:流程控制语句 for
  15. Android 动画分类
  16. EMUELEC游戏添加删除工具
  17. Python教程传送门,手把手带你学会Python!
  18. Android RxJava应用:优雅实现网络请求嵌套回调
  19. java前台界面设计_前端程序员要懂的 UI 设计知识
  20. 学习Java可以做些什么?

热门文章

  1. 【网站搭建】cloudflare实现显示url转发(301永久转发)
  2. iOS开发UI高级—26Quartz2D使用(信纸条纹)
  3. 常考的Ajax面试题
  4. VMARE 12 安装黑苹果 OS X 10.11
  5. Flutter 自定义水印拍照相机
  6. 图像处理工具如何正确的颜色和对比度
  7. 模板配置dateformat
  8. 年龄计算机在线计算适合你的对象,年龄计算器恋爱对象APP
  9. 75. python高级------jsion
  10. android 设备管理器 设置,设置android应用为设备管理器