摘要

网上关于Android NFC 操作的文章没有一篇合适自己,没有从根本解决问题,故整理一翻,理清思路。NFC是Near Field Communication缩写,即近距离无线通讯技术。可以在移动设备、消费类电子产品、PC 和智能控件工具间进行近距离无线通信。比如读取图书标签、公交卡等。

NFC Tag解析顺序(可以不在Activity配置)

nfc类型有三种NDEF,TECH,TAG,优先级依次降低,其对应的Intent过滤规则分别是:

NDEF:

<intent-filter><action android:name = "android.nfc.action.NDEF_DISCOVERED" /><data android:mimeType = "text/plain" />
</intent-filter>

TECH :

 <intent-filter>             <action android:name="android.nfc.action.TECH_DISCOVERED" /></intent-filter> <meta-data android:name="android.nfc.action.TECH_DISCOVERED"android:resource="@xml/filter_nfc" />

TAG:

<intent-filter><action android:name="android.nfc.action.TAG_DISCOVERED"/><category android:name="android.intent.category.DEFAULT"/>
</intent-filter>

假如有三个Activity分别设置上述三种类型,NFC感应时会依次寻找对应的Activity。

补充说明:Intent Filter就是 用来注册 Activity 、 Service 和 Broadcast Receiver 具有能在某种数据上执行一个动作的能力。
使用 Intent Filter ,应用程序组件告诉 Android ,它们能为其它程序的组件的动作请求提供服务,包括同一个程序的组
件、本地的或第三方的应用程序。

支持的Tag分类

Android为使用的android.nfc.tech包情况提供了通用的支持,android.nfc.tech包如下表中所描述的 。您可以使用的getTechList()方法来确定标签的技术支持,并建立 与之一由android.nfc.tech类相应TagTechnology对象。

Class Description 对应识别的卡种
TagTechnology 所有标签技术类必须实现的接口。
NfcA 提供NFC-A(ISO 14443-3A)的性能和I / O操作的访问。 m1卡
NfcB 提供NFC-B (ISO 14443-3B)的性能和I / O操作的访问。
NfcF 提供 NFC-F (JIS 6319-4)的性能和I / O操作的访问。
NfcV 提供 NFC-V (ISO 15693)的性能和I / O操作的访问。 15693卡
IsoDep 提供 ISO-DEP (ISO 14443-4)的性能和I / O操作的访问。 CPU卡
Ndef 提供NFC标签已被格式化为NDEF的数据和操作的访问。
NdefFormatable 提供可能被格式化为NDEF的 formattable的标签。
MifareClassic 如果此Android设备支持MIFARE,提供访问的MIFARE Classic性能和I / O操作。 m1卡
MifareUltralight 如果此Android设备支持MIFARE,提供访问的MIFARE 超轻性能和I / O操作。

14443协议和15693协议的区别: 14443是近场耦合,15693是远copy场耦合,14443具有加密功能,15693具有穿透性好,抗干扰性高! 14443按加密的方式分为14443a/b,14443a一般百多用于私用,例如:会员消费卡这类的;14443b因加密特性较好,用于公用多,例如:身份证。 15693常用于高频读距要求知高的场合,例如:货物身份识别、图书标签等

下面讲下不同卡种的概念,捋清思路

 名称 类别 描述 应用
M1卡 非接触式IC卡 有密码,可读可写,价格贵,感应距离短,芯片种类有S50,S70(芯片,是指菲利浦下属子公司恩智浦出品的芯片缩写) 一卡通,公交等系统
CPU卡 接触式也有非接触式 相当于微型计算机,具有用户空间大、读取速度快、支持一卡多用等特点 可适用于金融、保险、交警、政府行业等多个领域
15693卡

非接触IC卡

ISO15693是针对射频识别应用的一个国际标准,该标准定义了工作在13.56Mhz下智能标签和读写器的空气接口及数据通信规范,符合此标准的标签最远识读距离达到2米。 开放式门禁、开放式会议签到、贵重物品管理、数字化景区门票管理、数字化图书馆图书管理、医药管理、资产管理、产品防伪、物流及供应链等多种领域进行大量应用
M1-UltraLight卡 非接触式IC卡 无密码,又称为MF0,从UltraLight(超轻的)这个名字就可以看出来,它是一个低成本、小容量的卡片。低成本,是指它是目前市场中价格最低的遵守ISO14443A协议的芯片之一;小容量,是指其存储容量只有512bit(Mifare S50有8192bit)。,量分成16个块,每块包含4个字节。Mifare UltraLight的读写操作和 Mifare S50是完全兼容的。 门禁、考勤、会议签到、身份识别等

NFC使用步骤

1.权限

  <uses-permission android:name="android.permission.NFC" /><uses-featureandroid:name="android.hardware.nfc"android:required="true" />

2.在manifest节点下为Activity添加Intent Filter(可不配置)

3.Activity
过程:初始化   ->   设置系统调度   ->   系统调用onNewIntent(Intent intent) -> Tag读写流程 -> 最后取消系统调度

每次检测到Tag时,Activity的生命周期回调onPause()->onNewIntent()-> onResume()

Tag读写流程:

  • 建立连接
  • 读写
  • 关闭连接

1.初始化

        mNfcAdapter = NfcAdapter.getDefaultAdapter(this);pi = PendingIntent.getActivity(this, 0, new Intent(this, getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0);

2.设置系统调度

 @Overrideprotected void onResume() {super.onResume();mNfcAdapter.enableForegroundDispatch(this, pi, null, null); //启动}

3.系统调用onNewIntent(Intent intent)

   @Overrideprotected void onNewIntent(Intent intent) {super.onNewIntent(intent);handleNfcIntent(intent);}/*** 处理nfc标签** @param intent*/private void handleNfcIntent(Intent intent) {String action = intent.getAction();//根据不同的Action进行获取不同的标签   if (NfcAdapter.ACTION_TAG_DISCOVERED.equals(action)) {//默认Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);processTag(tag);}}

此过程能拿到卡片的uid,方法调用:

 Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);//卡片uidString uid=bytes2HexStr(tag.getId());public static String bytes2HexStr(byte[] src) {StringBuilder builder = new StringBuilder();if (src == null || src.length <= 0) {return "";}char[] buffer = new char[2];for (int i = 0; i < src.length; i++) {buffer[0] = Character.forDigit((src[i] >>> 4) & 0x0F, 16);buffer[1] = Character.forDigit(src[i] & 0x0F, 16);builder.append(buffer);}return builder.toString().toUpperCase();}

 卡片uid分析:

UID: 全球唯一标识符,每张卡都不一样,8个字节,读写器可以用UID号来操作指定的卡。

以ISO15693卡片为例,假如uid的16进制数据()为:

cabce04b000104e0(逆序分析UID[7]..UID[0]

cabce04b00 01 04 e0

e0:UID[7]为E0是固定的

04:UID[6]为卡制造商编码(如NXP公司为04,TI公司为07,上海贝岭为23)

01:UID[5]为产品类别代码,比如ICODE SL2 ICS20是01H,Tag-it HF-I Plus Chip为80H,Tag-it HF-I Plus Inlay为00H。

cabce04b00:UID[4]..UID[0]才是真正卡号,是制造商内部分配的号码。

 打印被读取卡片支持的Tag列表:

  for (String tech : tag.getTechList()) {LogUtils.i("------------" + tech);}

4.Tag读写流程

 protected void processTag( Tag tag) {IsoDep isodep= IsoDep.get(tag);if (isodep!= null){isodep.connect(); // 建立连接byte[] data = new byte[20];byte[] response = isodep.transceive(data); // 传送消息isoDep.close(); // 关闭连接}}
  • 根据实际要读取的卡片支持的Tag,取出对应的class实例,然后进行连接读写操作,如下常见Tag实例。

    1. m1卡: MifareClassic mfc = MifareClassic.get(tag);

    2. iso15693卡:NfcV tech = NfcV.get(tag);

  • 发送过去的字节数据,根据实际协议文档进行组装;
  • 返回出来的字节数据,根据实际协议文档进行解析

5.取消系统调度

 @Overrideprotected void onPause() {if (mNfcAdapter != null)mNfcAdapter.disableForegroundDispatch(this); // 取消调度}

Tag读写工具类

iso15693卡:

package com.haiheng.device.devicedriver.nfc;import android.nfc.tech.NfcV;import com.haiheng.core.util.ByteUtils;import java.io.IOException;/*** NfcV(ISO 15693)读写操作*   用法*  NfcV mNfcV = NfcV.get(tag);*  mNfcV.connect();* <p>*  NfcVUtils mNfcVutil = new NfcVUtils(mNfcV);*  取得UID*  mNfcVutil.getUID();*  读取block在1位置的内容*  mNfcVutil.readOneBlock(1);*  从位置7开始读2个block的内容*  mNfcVutil.readBlocks(7, 2);*  取得block的个数*  mNfcVutil.getBlockNumber();*  取得1个block的长度*  mNfcVutil.getOneBlockSize();*  往位置1的block写内容*  mNfcVutil.writeBlock(1, new byte[]{0, 0, 0, 0})** @author Kelly* @version 1.0.0* @filename NfcVUtils.java* @time 2018/10/30 10:29* @copyright(C) 2018 song*/
public class NfcVUtils {private NfcV mNfcV;/*** UID数组行式*/private byte[] ID;private String UID;private String DSFID;private String AFI;/*** block的个数*/private int blockNumber;/*** 一个block长度*/private int oneBlockSize;/*** 信息*/private byte[] infoRmation;/***  * 初始化*  * @param mNfcV NfcV对象*  * @throws IOException*  */public NfcVUtils(NfcV mNfcV) throws IOException {this.mNfcV = mNfcV;ID = this.mNfcV.getTag().getId();byte[] uid = new byte[ID.length];int j = 0;for (int i = ID.length - 1; i >= 0; i--) {uid[j] = ID[i];j++;}this.UID = ByteUtils.byteArrToHexString(uid);getInfoRmation();}public String getUID() {return UID;}/***  * 取得标签信息 *  */private byte[] getInfoRmation() throws IOException {byte[] cmd = new byte[10];cmd[0] = (byte) 0x22; // flagcmd[1] = (byte) 0x2B; // commandSystem.arraycopy(ID, 0, cmd, 2, ID.length); // UIDinfoRmation = mNfcV.transceive(cmd);blockNumber = infoRmation[12];oneBlockSize = infoRmation[13];AFI = ByteUtils.byteArrToHexString(new byte[]{infoRmation[11]});DSFID = ByteUtils.byteArrToHexString(new byte[]{infoRmation[10]});return infoRmation;}public String getDSFID() {return DSFID;}public String getAFI() {return AFI;}public int getBlockNumber() {return blockNumber + 1;}public int getOneBlockSize() {return oneBlockSize + 1;}/***  * 读取一个位置在position的block*  * @param position 要读取的block位置*  * @return 返回内容字符串*  * @throws IOException*  */public String readOneBlock(int position) throws IOException {byte cmd[] = new byte[11];cmd[0] = (byte) 0x22;cmd[1] = (byte) 0x20;System.arraycopy(ID, 0, cmd, 2, ID.length); // UIDcmd[10] = (byte) position;byte res[] = mNfcV.transceive(cmd);if (res[0] == 0x00) {byte block[] = new byte[res.length - 1];System.arraycopy(res, 1, block, 0, res.length - 1);return ByteUtils.byteArrToHexString(block);}return null;}/***  * 读取从begin开始end个block*  * begin + count 不能超过blockNumber*  * @param begin block开始位置*  * @param count 读取block数量*  * @return 返回内容字符串*  * @throws IOException*  */public String readBlocks(int begin, int count) throws IOException {if ((begin + count) > blockNumber) {count = blockNumber - begin;}StringBuffer data = new StringBuffer();for (int i = begin; i < count + begin; i++) {data.append(readOneBlock(i));}return data.toString();}/***  * 将数据写入到block,*  * @param position 要写内容的block位置*  * @param data 要写的内容,必须长度为blockOneSize*  * @return false为写入失败,true为写入成功*  * @throws IOException *  */public boolean writeBlock(int position, byte[] data) throws IOException {byte cmd[] = new byte[15];cmd[0] = (byte) 0x22;cmd[1] = (byte) 0x21;System.arraycopy(ID, 0, cmd, 2, ID.length); // UID//blockcmd[10] = (byte) position;//valueSystem.arraycopy(data, 0, cmd, 11, data.length);byte[] rsp = mNfcV.transceive(cmd);if (rsp[0] == 0x00)return true;return false;}
}

m1卡:

package com.haiheng.hand.device.nfc;import android.nfc.Tag;
import android.nfc.tech.MifareClassic;import com.haiheng.core.util.ByteUtils;
import com.haiheng.core.util.log.LogWriter;import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;/*** m1卡一般16个扇区64块,第一个扇区等闲不能操作。每个扇区4块,每块16字节。* 扇区从0数起,第2扇区第一块索引就是8,每个扇区前3块存数,据最后一块一般存密码。*** @author Kelly* @version 1.0.0* @filename M1CardUtils.java* @time 2019/3/15 14:02* @copyright(C) 2019 song*/
public class M1CardUtils {/*** 读指定扇区的指定块** @param mifareClassic* @param sectorIndex 扇区索引 0~15(16个扇区)* @param blockIndex    块索引 0~3* @param keyA  密钥,可为空,没有使用默认的密钥* @return*/public static byte[] readBlock(MifareClassic mifareClassic, int sectorIndex, int blockIndex,byte[] keyA) throws IOException {try {String metaInfo = "";mifareClassic.connect();int type = mifareClassic.getType();//获取TAG的类型int sectorCount = mifareClassic.getSectorCount();//获取TAG中包含的扇区数String typeS = getMifareClassicType(type);metaInfo += "卡片类型:" + typeS + "\n共" + sectorCount + "个扇区\n共" + mifareClassic.getBlockCount() + "个块\n存储空间: " + mifareClassic.getSize() + "B\n";LogWriter.i(metaInfo);byte[] data = null;String hexString = null;if (m1AuthByKey(mifareClassic, sectorIndex,keyA) || m1Auth(mifareClassic, sectorIndex)){int bCount;int bIndex;bCount = mifareClassic.getBlockCountInSector(sectorIndex);//获得当前扇区的所包含块的数量;bIndex = mifareClassic.sectorToBlock(sectorIndex);//当前扇区的第1块的块号for (int i = 0; i < bCount; i++) {data = mifareClassic.readBlock(bIndex);hexString = ByteUtils.byteArrToHexString(data);LogWriter.w(sectorIndex + "扇区" + bIndex + "块:" + hexString);if (blockIndex == i) {break;}bIndex++;}}else {LogWriter.w("密码校验失败,扇区:" + sectorIndex);}return data;} catch (Exception e) {throw new IOException(e);} finally {try {if (mifareClassic != null) {mifareClassic.close();}} catch (IOException e) {throw new IOException(e);}}}/*** 读指定扇区的所有块** @param mifareClassic* @param sectorIndex 扇区索引 0~15(16个扇区)* @param keyA  密钥,可为空,没有使用默认的密钥* @return*/public static List<byte[]> readBlock(MifareClassic mifareClassic, int sectorIndex, byte[] keyA) throws IOException {List<byte[]> dataList = new ArrayList<byte[]>();try {String metaInfo = "";mifareClassic.connect();int type = mifareClassic.getType();//获取TAG的类型int sectorCount = mifareClassic.getSectorCount();//获取TAG中包含的扇区数String typeS = getMifareClassicType(type);metaInfo += "卡片类型:" + typeS + "\n共" + sectorCount + "个扇区\n共" + mifareClassic.getBlockCount() + "个块\n存储空间: " + mifareClassic.getSize() + "B\n";LogWriter.i(metaInfo);if (m1AuthByKey(mifareClassic, sectorIndex,keyA) || m1Auth(mifareClassic, sectorIndex)){int bCount;int bIndex;bCount = mifareClassic.getBlockCountInSector(sectorIndex);//获得当前扇区的所包含块的数量;bIndex = mifareClassic.sectorToBlock(sectorIndex);//当前扇区的第1块的块号for (int i = 0; i < bCount; i++) {byte[] data = mifareClassic.readBlock(bIndex);String hexString = ByteUtils.byteArrToHexString(data);LogWriter.w(sectorIndex + "扇区" + bIndex + "块:" + hexString);dataList.add(data);bIndex++;}}else {LogWriter.w("密码校验失败,扇区:" + sectorIndex);}return dataList;} catch (Exception e) {throw new IOException(e);} finally {try {if (mifareClassic != null) {mifareClassic.close();}} catch (IOException e) {throw new IOException(e);}}}/*** 读所有扇区的所有块** @param mifareClassic* @param keyA  密钥,可为空,没有使用默认的密钥* @return*/public static Map<Integer,List<byte[]>> readBlock(MifareClassic mifareClassic,byte[] keyA) throws IOException {try {Map<Integer,List<byte[]>> result = new HashMap<Integer,List<byte[]>>();String metaInfo = "";mifareClassic.connect();int type = mifareClassic.getType();//获取TAG的类型int sectorCount = mifareClassic.getSectorCount();//获取TAG中包含的扇区数,一般m1卡扇区数为16个String typeS = getMifareClassicType(type);metaInfo += "卡片类型:" + typeS + "\n共" + sectorCount + "个扇区\n共" + mifareClassic.getBlockCount() + "个块\n存储空间: " + mifareClassic.getSize() + "B\n";LogWriter.i(metaInfo);for (int j = 0; j < sectorCount; j++) {if (m1AuthByKey(mifareClassic, j,keyA) || m1Auth(mifareClassic, j)){List<byte[]> dataList = new ArrayList<byte[]>();int bCount;int bIndex;bCount = mifareClassic.getBlockCountInSector(j);//获得当前扇区的所包含块的数量;bIndex = mifareClassic.sectorToBlock(j);//当前扇区的第1块的块号for (int i = 0; i < bCount; i++) {byte[] data = mifareClassic.readBlock(bIndex);String hexString = ByteUtils.byteArrToHexString(data);LogWriter.w(j + "扇区" + bIndex + "块:" + hexString);dataList.add(data);bIndex++;}result.put(j,dataList);}else {LogWriter.w("密码校验失败,扇区:" + j);}}return result;} catch (Exception e) {throw new IOException(e);} finally {try {if (mifareClassic != null) {mifareClassic.close();}} catch (IOException e) {throw new IOException(e);}}}/*** 获取m1卡类型* @param type* @return*/private static String getMifareClassicType(int type) {String str = null;switch (type) {case MifareClassic.TYPE_CLASSIC:str = "TYPE_CLASSIC";break;case MifareClassic.TYPE_PLUS:str = "TYPE_PLUS";break;case MifareClassic.TYPE_PRO:str = "TYPE_PRO";break;case MifareClassic.TYPE_UNKNOWN:str = "TYPE_UNKNOWN";break;}return str;}/*** 往指定扇区的指定块写数据* @param tag* @param sectorIndex 扇区索引 0~15(16个扇区)* @param blockIndex 块索引 0~63* @param blockByte 写入数据必须是16字节* @return* @throws IOException*/public static boolean writeBlock(Tag tag, int sectorIndex,int blockIndex,byte[] blockByte) throws IOException {MifareClassic mifareClassic = MifareClassic.get(tag);try {mifareClassic.connect();if (m1Auth(mifareClassic, sectorIndex)) {mifareClassic.writeBlock(blockIndex, blockByte);} else {return false;}} catch (IOException e) {throw new IOException(e);} finally {try {mifareClassic.close();} catch (IOException e) {throw new IOException(e);}}return true;}/*** 使用默认密码校验** @param mifareClassic* @param position* @return* @throws IOException*/public static boolean m1Auth(MifareClassic mifareClassic, int position) throws IOException {if (mifareClassic.authenticateSectorWithKeyA(position, MifareClassic.KEY_DEFAULT)) {return true;} else if (mifareClassic.authenticateSectorWithKeyB(position, MifareClassic.KEY_DEFAULT)) {return true;}return false;}/*** 使用指定密码校验** @param mifareClassic* @param position* @param keyA* @return* @throws IOException*/public static boolean m1AuthByKey(MifareClassic mifareClassic, int position,byte[] keyA) throws IOException {if (keyA != null && keyA.length == 6){if(mifareClassic.authenticateSectorWithKeyA(position, keyA)) {return true;}}return false;}
}

特别说明:关于Tag读写只是进行简单工具类封装,没有进行面向组件封装。

配置示例源码:https://github.com/kellysong/android-blog-demo/tree/master/nfc-demo

Android NFC使用详解相关推荐

  1. Android NFC开发详解 总结和NFC读卡实例解析

    文章目录 前言 一.什么是NFC? 二.基础知识 1.什么是NDEF? 2.NFC技术的操作模式 3.标签的技术类型 4.实现方式的分类 5.流程 三.获取标签内容 1.检查环境 2.获取NFC标签 ...

  2. 《Android游戏开发详解》——第1章,第1.6节函数(在Java中称为“方法”更好)...

    本节书摘来自异步社区<Android游戏开发详解>一书中的第1章,第1.6节函数(在Java中称为"方法"更好),作者 [美]Jonathan S. Harbour,更 ...

  3. JMessage Android 端开发详解

    JMessage Android 端开发详解 目前越来越多的应用会需要集成即时通讯功能,这里就为大家详细讲一下如何通过集成 JMessage 来为你的 App 增加即时通讯功能. 首先,一个最基础的 ...

  4. 《Java和Android开发实战详解》——2.5节良好的Java程序代码编写风格

    本节书摘来自异步社区<Java和Android开发实战详解>一书中的第2章,第2.5节良好的Java程序代码编写风格,作者 陈会安,更多章节内容可以访问云栖社区"异步社区&quo ...

  5. Android事件流程详解

    Android事件流程详解 网络上有不少博客讲述了android的事件分发机制和处理流程机制,但是看过千遍,总还是觉得有些迷迷糊糊,因此特地抽出一天事件来亲测下,向像我一样的广大入门程序员详细讲述an ...

  6. Android Studio 插件开发详解二:工具类

    转载请标明出处:http://blog.csdn.net/zhaoyanjun6/article/details/78112856 本文出自[赵彦军的博客] 在插件开发过程中,我们按照开发一个正式的项 ...

  7. 《Android游戏开发详解》一2.16 区分类和对象

    本节书摘来异步社区<Android游戏开发详解>一书中的第2章,第2.16节,作者: [美]Jonathan S. Harbour 译者: 李强 责编: 陈冀康,更多章节内容可以访问云栖社 ...

  8. Android Framework系统服务详解

    Android Framework系统服务详解 操作环境 系统:Linux (Ubuntu 12.04) 平台:高通 Android版本:5.1 PS: 符号...为省略N条代码 一.大致原理分析 A ...

  9. android屏幕适配详解

    android屏幕适配详解 官方地址:http://developer.android.com/guide/practices/screens_support.html 一.关于布局适配建议 1.不要 ...

最新文章

  1. 【剑指offer-Java版】17合并两个排序链表
  2. 史上最大“云办公”实验开始,你参加了吗?
  3. ResNeXt——与 ResNet 相比,相同的参数个数,结果更好:一个 101 层的 ResNeXt 网络,和 200 层的 ResNet 准确度差不多,但是计算量只有后者的一半...
  4. ListView执行notifyDatasetChanged无数据显示,getView未执行
  5. 通过一个例子介绍 IDA pro 的简单使用
  6. 【笔记】JAVA 中国象棋游戏 部分源码
  7. 02 button的练习
  8. openresty的安装和使用
  9. 模拟get和post请求(支持自定义header和测试CDN节点)
  10. struts2中action手动获取參数
  11. pve 虚拟环境 vi/vim不能右键粘贴设置方法
  12. SPSS 逐步回归【SPSS 028期】
  13. Intellij IDEA企业版破解
  14. MSSQL父子关系表的SQL查询(SQL Query for Parent Child Relationship)
  15. 类的封装性、继承性和多态性设计
  16. [高精度]高精度的封装
  17. python批量自动化工作
  18. 模拟【The Biggest Water Problem】+模拟【明明的随机数】
  19. mybatis 实体嵌套查询
  20. 2023年首更,警惕6本SCISSCI期刊被剔除

热门文章

  1. 最新2023基于微信小程序的学生公寓生活管理系统+后台管理系统(SSM+mysql)-JAVA.VUE(毕业设计+论文+开题报告+运行)
  2. Linux硬件信息查看命令
  3. 如何给你的社群定位?
  4. C++ Visual Studio 2017 Error: Cannot open include file: .h: No such file or directory
  5. 单机网页游戏的如何修改服务器数据库,三国志单机页游网页版一键端+教程及修改工具...
  6. 网络营销推广是什么_什么样的网站适合做SEO?
  7. 桌面录屏录音技术,录屏软件原理
  8. 史上最简单的无缝衔接轮播图
  9. 「IT人」职业生涯指南——附神级跳槽攻略图!
  10. BZOJ 4480: [Jsoi2013]快乐的jyy(回文自动机)