android系統(tǒng)6.0之后忠荞,對mac地址的獲取添加了權(quán)限委煤,按照普通的方法獲取如下:
WifiManager wifiMan = (WifiManager)context.getSystemService(Context.WIFI_SERVICE) ;
WifiInfo wifiInf = wifiMan.getConnectionInfo();
return wifiInf.getMacAddress();
但是這種情況碧绞,獲取到的mac地址都是:02:00:00:00:00:00讥邻,得知通過WifiInfo.getMacAddress()獲取的MAC地址是一個“假”的固定值,官方其實(shí)是有說明的: “保護(hù)用戶隱私數(shù)據(jù)” 系宜。
查詢資料找到了相應(yīng)的解決方法:
使用Java獲取設(shè)備網(wǎng)絡(luò)設(shè)備信息的API——NetworkInterface.getNetworkInterfaces() ——仍然可以間接地獲取到MAC地址鲫惶。
// 有興趣的朋友可以看下NetworkInterface在Android FrameWork中怎么實(shí)現(xiàn)的
public static String macAddress() throws SocketException {
String address = null;
// 把當(dāng)前機(jī)器上的訪問網(wǎng)絡(luò)接口的存入 Enumeration集合中
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
while (interfaces.hasMoreElements()) {
NetworkInterface netWork = interfaces.nextElement();
// 如果存在硬件地址并可以使用給定的當(dāng)前權(quán)限訪問,則返回該硬件地址(通常是 MAC)欢策。
byte[] by = netWork.getHardwareAddress();
if (by == null || by.length == 0) {
continue;
}
StringBuilder builder = new StringBuilder();
for (byte b : by) {
builder.append(String.format("%02X:", b));
}
if (builder.length() > 0) {
builder.deleteCharAt(builder.length() - 1);
}
String mac = builder.toString();
Log.d("mac", "interfaceName="+netWork.getName()+", mac="+mac);
// 從路由器上在線設(shè)備的MAC地址列表踩寇,可以印證設(shè)備Wifi的 name 是 wlan0
if (netWork.getName().equals("wlan0")) {
Log.d("mac", " interfaceName ="+netWork.getName()+", mac="+mac);
address = mac;
}
}
return address;
}
運(yùn)行結(jié)果:
05-09 18:39:31.528: D/mac(5132): interfaceName=wlan0, mac=8C:BE:BE:BD:EC:0E
05-09 18:39:31.528: D/mac(5132): interfaceName=wlan0, mac=8C:BE:BE:BD:EC:0E
05-09 18:39:31.528: D/mac(5132): interfaceName=p2p0, mac=8E:BE:BE:BD:EC:0E
05-09 18:39:31.538: D/mac(5132): interfaceName=dummy0, mac=42:35:AD:88:CE:D2
05-09 18:39:31.538: D/mac(5132): interfaceName=usbnet0, mac=52:D9:15:FA:64:1A
05-09 18:39:31.538: D/mac(5132): interfaceName=rmnet0, mac=C6:76:E0:64:56:AC
注意:在使用上述代碼時俺孙,記得添加以下權(quán)限:
<uses-permission android:name="android.permission.INTERNET" />
小心求證
既然NetworkInterface可以正常獲取掷贾,那得好好看看它在 Android framework 中的實(shí)現(xiàn)源碼:
public byte[] getHardwareAddress() throws SocketException {
try {
// Parse colon-separated bytes with a trailing newline: "aa:bb:cc:dd:ee:ff\n".
String s = IoUtils.readFileAsString("/sys/class/net/" + name + "/address");
byte[] result = new byte[s.length()/3];
for (int i = 0; i < result.length; ++i) {
result[i] = (byte) Integer.parseInt(s.substring(3*i, 3*i + 2), 16);
}
// We only want to return non-zero hardware addresses.
for (int i = 0; i < result.length; ++i) {
if (result[i] != 0) {
return result;
}
}
return null;
} catch (Exception ex) {
throw rethrowAsSocketException(ex);
}
}
原來MAC地址是直接從"/sys/class/net/" + name + "/address"文件中讀取的想帅!
這個name是什么呢?
繼續(xù)翻源碼:
private static final File SYS_CLASS_NET = new File("/sys/class/net");
...
public static Enumeration<NetworkInterface> getNetworkInterfaces() throws SocketException {
return Collections.enumeration(getNetworkInterfacesList());
}
private static List<NetworkInterface> getNetworkInterfacesList() throws SocketException {
String[] interfaceNames = SYS_CLASS_NET.list();
NetworkInterface[] interfaces = new NetworkInterface[interfaceNames.length];
String[] ifInet6Lines = readIfInet6Lines();
for (int i = 0; i < interfaceNames.length; ++i) {
interfaces[i] = NetworkInterface.getByNameInternal(interfaceNames[i], ifInet6Lines);
...
}
List<NetworkInterface> result = new ArrayList<NetworkInterface>();
for (int counter = 0; counter < interfaces.length; counter++) {
...
result.add(interfaces[counter]);
}
return result;
}
可以看出/sys/class/net目錄下的一個文件夾即對應(yīng)一個NetworkInterface的name旨剥。
user@android:/$ ls /sys/class/net/
dummy0
lo
p2p0
rev_rmnet0
rev_rmnet1
rev_rmnet2
rev_rmnet3
rmnet0
rmnet1
rmnet2
rmnet3
rmnet_smux0
sit0
wlan0
從路由器上在線設(shè)備的MAC地址列表轨帜,可以印證我這臺設(shè)備Wifi的name是wlan0
那么讀取文件/sys/class/net/wlan0/address就輕松得到了這臺設(shè)備的MAC地址
user@android:/$ cat /sys/class/net/wlan0/address
ac:22:0b:3e:d4
不出所料蚌父!
進(jìn)而毛萌,問題又變成如何獲取設(shè)備的Wifi的interface name朝聋?
探尋 Wifi interface name
回到開頭冀痕,我們是通過context.getSystemService(Context.WIFI_SERVICE) 獲取的WifiManager。
而WifiManager肯定是與遠(yuǎn)程系統(tǒng)服務(wù)的IBinder在交互言蛇,而系統(tǒng)服務(wù)都是在SystemServer.run()中被啟動的。
在SystemServer.java中搜索關(guān)鍵字”WIFI_SERVICE”吨拗,很容易便找到mSystemServiceManager.startService(WIFI_SERVICE_CLASS);
順藤摸瓜劝篷,又找到系統(tǒng)服務(wù)實(shí)現(xiàn)類com.android.server.wifi.WifiService娇妓,WifiService中的邏輯很簡單,構(gòu)造真正的實(shí)現(xiàn)類com.android.server.wifi.WifiServiceImpl對象并注冊到系統(tǒng)服務(wù)中:
WifiService.java
public final class WifiService extends SystemService {
private static final String TAG = "WifiService";
final WifiServiceImpl mImpl;
public WifiService(Context context) {
super(context);
mImpl = new WifiServiceImpl(context);
}
@Override
public void onStart() {
Log.i(TAG, "Registering " + Context.WIFI_SERVICE);
publishBinderService(Context.WIFI_SERVICE, mImpl);
}
@Override
public void onBootPhase(int phase) {
if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY) {
mImpl.checkAndStartWifi();
}
}
}
打開WifiServiceImpl.java,從構(gòu)造方法處志群,一眼就看到了關(guān)鍵代碼:mInterfaceName = SystemProperties.get("wifi.interface", "wlan0");
如此這般終于找到定義設(shè)備的Wifi的interface name的地方:SystemProperties
通過adb可以很容易得到這個屬性值:adb shell getprop wifi.interface
那么在我們應(yīng)用里可以通過Java的反射獲取SystemProperties锌云,進(jìn)而調(diào)用靜態(tài)方法get即可拿到Wifi的interface name。
結(jié)論
縱然Google掩耳盜鈴似的把API返回值篡改了子漩,但終究是沒有改底層的實(shí)現(xiàn)石洗,通過閱讀源碼的方式還是很容易找到解決辦法的讲衫。
通過反射SystemProperties獲取屬性wifi.interface的值
讀取/sys/class/net/+interfaceName+/address文件的第一行即是Wifi MAC地址
請注意:
個別設(shè)備可能會拒絕你訪問/sys目錄
/sys/class/net/+interfaceName+/address文件不一定存在,請?jiān)谧x取之前確認(rèn)設(shè)備的Wifi已經(jīng)打開或已連接
未免廠商實(shí)現(xiàn)不一的風(fēng)險招驴,請先嘗試用NetworkInterface.getByName(interfaceName)來間接獲取Wifi MAC地址