NFC开发系列专栏 - 第三篇:无界面NFC后台服务方案
一、问题背景
在企业级NFC应用开发中,我们经常面临这样的需求:实现无界面的NFC后台服务,让NFC功能在后台默默工作,不打扰用户正常操作。
理想情况是让平台同事通过NfcDispatcher发送广播携带Tag对象,我们的服务接收广播后直接处理NFC数据。然而,实际开发中会遇到诸多技术挑战。
核心问题
- 用户体验:频繁弹出Activity会打断用户操作
- 技术限制:Tag对象在IPC过程中存在句柄失效问题
- 系统约束:Android系统对NFC权限和安全机制的严格限制
二、方案探索与技术分析
方案一:直接传递Tag对象(不可行)
实现思路
让平台同事通过广播直接传递Tag对象给NFC服务。
java
// 平台方发送广播
Intent intent = new Intent("com.nfc.TAG_DISCOVERED");
intent.putExtra("tag", tag); // 直接传递Tag对象
intent.putExtra("uid", tag.getId());
context.sendBroadcast(intent);
// NFC服务接收广播
BroadcastReceiver nfcReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Tag tag = intent.getParcelableExtra("tag");
if (tag != null) {
processTag(tag);
}
}
};
private void processTag(Tag tag) {
MifareClassic mfc = MifareClassic.get(tag);
try {
mfc.connect(); // 这里会抛出IOException
// ... 数据读取操作
} catch (IOException e) {
Log.e("NFC", "连接失败: " + e.getMessage());
}
}
问题分析
java
// 异常堆栈示例
java.io.IOException: transceive failed
at android.nfc.tech.MifareClassic.connect(MifareClassic.java:123)
at com.example.NfcService.processTag(NfcService.java:45)
技术原因分析:
- 句柄失效:Tag对象包含底层硬件连接句柄,在跨进程传递后句柄失效
- 安全机制:Android系统禁止跨进程直接传递NFC连接对象
- 状态丢失:Tag对象在序列化/反序列化过程中丢失硬件连接状态
结论:此方案完全不可行,必须在接收方重新建立NFC连接。
方案二:平台解析数据(不推荐)
实现思路
让平台同事直接解析NFC数据,将解析结果通过广播传递。
java
// 平台方解析并发送结果
public class PlatformNfcProcessor {
public void processAndBroadcast(Tag tag) {
try {
// 平台解析NFC数据
String account = parseAccount(tag);
String password = parsePassword(tag);
// 发送解析结果
Intent intent = new Intent("com.nfc.DATA_PARSED");
intent.putExtra("account", account);
intent.putExtra("password", password);
intent.putExtra("timestamp", System.currentTimeMillis());
context.sendBroadcast(intent);
} catch (Exception e) {
Log.e("Platform", "NFC解析失败", e);
}
}
}
// NFC服务接收解析结果
BroadcastReceiver dataReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String account = intent.getStringExtra("account");
String password = intent.getStringExtra("password");
// 直接使用解析结果
saveCredentials(account, password);
}
};
优缺点分析
优点:
- ✅ 完全避免了Tag对象传递问题
- ✅ 数据传递稳定可靠
- ✅ 不需要处理NFC连接异常
缺点:
- ❌ 增加平台开发复杂度:平台需要理解复杂的NFC协议
- ❌ 协议耦合严重:协议变更时需要同步更新平台代码
- ❌ 违反单一职责:平台承担了本应属于业务方的解析工作
- ❌ 维护成本高:多套代码需要同步维护
- ❌ 安全风险:敏感数据在多个系统间传递
结论:此方案技术上可行,但架构设计不合理,不推荐使用。
方案三:传递原始块数据(中等)
实现思路
让平台传递原始的块数据,而不是Tag对象或解析结果。
java
// 平台方读取原始数据
public class PlatformRawDataReader {
public void readAndBroadcastRawData(Tag tag) {
try {
MifareClassic mfc = MifareClassic.get(tag);
mfc.connect();
// 读取关键扇区的原始数据
List<byte[]> rawDataList = new ArrayList<>();
// 读取扇区1(协议标识和数据长度)
rawDataList.add(readSector(mfc, 1));
// 读取扇区5(数据内容)
rawDataList.add(readSector(mfc, 5));
// 发送原始数据
Intent intent = new Intent("com.nfc.RAW_DATA");
intent.putExtra("rawData", rawDataList);
intent.putExtra("cardType", "MIFARE_CLASSIC");
intent.putExtra("uid", tag.getId());
context.sendBroadcast(intent);
mfc.close();
} catch (Exception e) {
Log.e("Platform", "读取原始数据失败", e);
}
}
private byte[] readSector(MifareClassic mfc, int sector) throws IOException {
boolean auth = mfc.authenticateSectorWithKeyA(sector, getDefaultKey());
if (!auth) {
throw new IOException("扇区认证失败: " + sector);
}
byte[] sectorData = new byte[48]; // 3个数据块 * 16字节
int firstBlock = mfc.sectorToBlock(sector);
for (int i = 0; i < 3; i++) { // 跳过控制块
byte[] blockData = mfc.readBlock(firstBlock + i);
System.arraycopy(blockData, 0, sectorData, i * 16, 16);
}
return sectorData;
}
}
// NFC服务处理原始数据
public class NfcRawDataProcessor {
public void processRawData(Intent intent) {
String cardType = intent.getStringExtra("cardType");
ArrayList<byte[]> rawData = (ArrayList<byte[]>) intent.getSerializableExtra("rawData");
byte[] uid = intent.getByteArrayExtra("uid");
try {
// 使用业务层解析原始数据
String result = NfcBusinessManager.parseRawData(rawData, cardType);
// 保存解析结果
saveToSystem(result);
} catch (Exception e) {
Log.e("NFC", "原始数据解析失败", e);
}
}
}
优缺点分析
优点:
- ✅ 避免了Tag对象句柄失效问题
- ✅ 协议解析逻辑仍在业务方控制范围内
- ✅ 平台只负责硬件操作,职责清晰
- ✅ 数据传输稳定可靠
缺点:
- ❌ 平台需要实现块数据读取:增加了平台的工作量
- ❌ 密钥管理复杂:平台需要知道认证密钥
- ❌ 错误处理复杂:需要处理各种读取异常
- ❌ 协议变更仍需协调:需要修改平台读取逻辑
结论:此方案技术上可行,但仍有较高的协调成本。
方案四:1px透明Activity + Toast提示(推荐)
设计理念
创建1px完全透明的Activity,配合全局Toast工具类进行状态提示,避免打断用户当前操作。
核心实现
1. 透明Activity样式定义
xml
<!-- res/values/styles.xml -->
<style name="PixelTransparentTheme" parent="Theme.Translucent.NoTitleBar">
<item name="android:windowIsFloating">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:backgroundDimEnabled">false</item>
<item name="android:windowContentOverlay">@null</item>
</style>
2. 1px透明Activity实现
java
/**
* 1px透明NFC处理Activity
* 设计原则:
* 1. 完全透明,不干扰用户操作
* 2. 使用Toast进行状态反馈
* 3. 处理完成后立即销毁
*/
public class TransparentNfcActivity extends Activity {
private static final String TAG = "TransparentNfcActivity";
private static final int AUTO_CLOSE_DELAY = 500; // 500ms后自动关闭
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(TAG, "透明Activity创建");
// 设置1px透明窗口
setupPixelWindow();
// 无需设置布局内容,保持空白
// 立即处理NFC Intent
handleNfcIntent(getIntent());
// 延时自动关闭
new Handler(Looper.getMainLooper()).postDelayed(this::finish, AUTO_CLOSE_DELAY);
}
/**
* 设置1px透明窗口
*/
private void setupPixelWindow() {
Window window = getWindow();
WindowManager.LayoutParams params = window.getAttributes();
// 设置窗口大小为1x1像素
params.width = 1;
params.height = 1;
// 设置窗口位置在屏幕角落
params.gravity = Gravity.TOP | Gravity.START;
params.x = 0;
params.y = 0;
// 添加窗口标志:不可触摸、不获取焦点
params.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE |
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL |
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS;
window.setAttributes(params);
Log.d(TAG, "1px透明窗口已设置");
}
/**
* 处理NFC Intent
*/
private void handleNfcIntent(Intent intent) {
if (intent == null) {
Log.w(TAG, "Intent为空");
ToastUtil.showError("Intent无效");
return;
}
String action = intent.getAction();
Log.d(TAG, "收到NFC Intent: " + action);
if (isNfcIntent(action)) {
Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
if (tag != null) {
Log.d(TAG, "检测到NFC标签,UID: " + NfcUtils.bytesToHex(tag.getId()));
// 显示处理中提示
ToastUtil.showInfo("正在读取NFC...");
// 异步处理NFC标签
processNfcTagAsync(tag);
} else {
Log.w(TAG, "Tag对象为空");
ToastUtil.showError("标签检测失败");
}
} else {
Log.w(TAG, "非NFC Intent: " + action);
ToastUtil.showError("Intent类型不匹配");
}
}
private boolean isNfcIntent(String action) {
return action != null && (
NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action) ||
NfcAdapter.ACTION_TECH_DISCOVERED.equals(action) ||
NfcAdapter.ACTION_TAG_DISCOVERED.equals(action)
);
}
/**
* 异步处理NFC标签
*/
private void processNfcTagAsync(Tag tag) {
new Thread(() -> {
try {
String result = NfcBusinessManager.getInstance(getApplicationContext())
.processNfcTagSync(tag);
Log.d(TAG, "NFC处理成功: " + result);
ToastUtil.showSuccess(result);
} catch (Exception e) {
Log.e(TAG, "NFC处理失败", e);
ToastUtil.showError("读取失败: " + e.getMessage());
}
}).start();
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
Log.d(TAG, "收到新的NFC Intent");
handleNfcIntent(intent);
}
@Override
public void onBackPressed() {
// 禁用返回键,直接关闭
finish();
}
@Override
protected void onDestroy() {
super.onDestroy();
Log.d(TAG, "透明Activity销毁");
}
}
3. 全局Toast工具类(带防抖)
java
/**
* 全局Toast工具类
* 功能:
* 1. 防抖处理:2秒内只能弹出一次
* 2. 重复内容简化:连续弹出时,以最后一个为准
* 3. 自动队列管理
*/
public class ToastUtil {
private static final String TAG = "ToastUtil";
private static final long DEBOUNCE_INTERVAL = 2000; // 防抖间隔2秒
private static Toast sCurrentToast;
private static long sLastShowTime = 0;
private static String sLastMessage = "";
private static final Handler sHandler = new Handler(Looper.getMainLooper());
/**
* 显示成功提示
*/
public static void showSuccess(String message) {
showToast(message, Toast.LENGTH_SHORT);
}
/**
* 显示错误提示
*/
public static void showError(String message) {
showToast(message, Toast.LENGTH_SHORT);
}
/**
* 显示普通信息
*/
public static void showInfo(String message) {
showToast(message, Toast.LENGTH_SHORT);
}
/**
* 核心Toast显示方法(带防抖)
*/
private static void showToast(String message, int duration) {
if (message == null || message.isEmpty()) {
return;
}
long currentTime = System.currentTimeMillis();
// 防抖逻辑:2秒内的重复调用
if (currentTime - sLastShowTime < DEBOUNCE_INTERVAL) {
// 如果消息内容相同,忽略本次调用
if (message.equals(sLastMessage)) {
Log.d(TAG, "防抖:忽略重复消息 - " + message);
return;
}
// 如果消息内容不同,取消当前Toast,显示新Toast
Log.d(TAG, "防抖:更新消息 - " + message);
cancelCurrentToast();
}
// 更新状态
sLastShowTime = currentTime;
sLastMessage = message;
// 在主线程中显示Toast
sHandler.post(() -> {
// 取消旧Toast
cancelCurrentToast();
// 创建并显示新Toast
sCurrentToast = Toast.makeText(
ContextHolder.getApplicationContext(),
message,
duration
);
sCurrentToast.show();
Log.d(TAG, "Toast显示: " + message);
});
}
/**
* 取消当前Toast
*/
private static void cancelCurrentToast() {
if (sCurrentToast != null) {
sCurrentToast.cancel();
sCurrentToast = null;
}
}
/**
* 重置防抖状态(测试用)
*/
public static void reset() {
sLastShowTime = 0;
sLastMessage = "";
cancelCurrentToast();
}
}
4. Application Context持有者
java
/**
* 全局Context持有者
*/
public class ContextHolder {
private static Context sApplicationContext;
public static void init(Context context) {
sApplicationContext = context.getApplicationContext();
}
public static Context getApplicationContext() {
if (sApplicationContext == null) {
throw new IllegalStateException("ContextHolder未初始化,请在Application中调用init()");
}
return sApplicationContext;
}
}
// 在Application中初始化
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
ContextHolder.init(this);
}
}
5. AndroidManifest.xml配置
xml
<activity
android:name=".TransparentNfcActivity"
android:exported="true"
android:theme="@style/PixelTransparentTheme"
android:launchMode="singleTask"
android:excludeFromRecents="true"
android:noHistory="true"
android:finishOnTaskLaunch="true"
android:clearTaskOnLaunch="true"
android:configChanges="orientation|screenSize|keyboardHidden">
<!-- NFC Intent过滤器 -->
<intent-filter>
<action android:name="android.nfc.action.TECH_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="android.nfc.action.TAG_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<meta-data
android:name="android.nfc.action.TECH_DISCOVERED"
android:resource="@xml/nfc_tech_filter" />
</activity>
优缺点分析
优点:
- ✅ 完全避免IPC问题:Activity直接接收NFC Intent
- ✅ 不打断用户操作:1px透明窗口,用户无感知
- ✅ 状态反馈友好:Toast提示轻量级,不遮挡界面
- ✅ 防抖机制完善:2秒内重复调用自动过滤
- ✅ 实现简单:标准Android开发方式
- ✅ 资源占用低:无复杂UI,快速创建销毁
- ✅ 无需修改平台代码:标准NFC处理流程
缺点:
- ❌ Toast可能被覆盖:系统Toast可能被其他应用Toast覆盖
- ❌ 状态反馈有限:无法展示复杂的处理进度
- ❌ 仍有Activity创建开销:虽然很小,但仍存在
结论:此方案技术可靠、用户体验优秀,适合大多数无界面NFC处理场景。
三、方案对比总结
综合对比表
| 方案 | 技术可行性 | 开发复杂度 | 用户体验 | 平台协调成本 | 推荐指数 |
|---|---|---|---|---|---|
| 直接传递Tag对象 | ❌ 不可行 | 低 | 差(功能异常) | 无 | ⭐ |
| 平台解析数据 | ✅ 可行 | 高 | 优秀 | 高 | ⭐⭐ |
| 传递原始数据 | ✅ 可行 | 中 | 优秀 | 中 | ⭐⭐⭐ |
| 1px透明Activity + Toast | ✅ 可行 | 低 | 优秀 | 无 | ⭐⭐⭐⭐⭐ |
选择建议
强烈推荐:1px透明Activity + Toast方案
选择理由:
- 技术成熟稳定:基于标准Android NFC开发,无技术风险
- 用户体验最佳:1px透明设计完全不影响用户操作
- 开发成本最低:无需协调平台,独立开发
- 维护成本低:所有逻辑在业务方,便于维护
- 防抖机制完善:避免频繁Toast打扰用户
- 资源占用最小:无复杂UI,极低内存占用
实施建议:
- 窗口设置:1x1像素,完全透明,位于屏幕角落
- Toast时机:仅在必要时提示(开始读取、成功、失败)
- 防抖策略:2秒内相同消息只显示一次
- 自动关闭:Activity在500ms后自动销毁
- 异步处理:NFC处理放在后台线程,避免阻塞
使用场景:
- ✅ 企业级NFC考勤系统
- ✅ NFC门禁卡识别
- ✅ NFC支付快速验证
- ✅ 所有需要"无感知"的NFC场景
通过1px透明Activity + Toast方案,我们成功解决了无界面NFC后台服务的技术挑战,在保证功能完整性的同时,提供了极致的用户体验。这个方案已经在实际项目中得到验证,具有很强的实用性和推广价值。