Android切换用户后无法获取MAC地址的解决方法

 更新时间:2026年04月27日 09:21:12   作者:峥嵘life  
本文记录了Android多用户场景下子用户系统应用无法获取MAC地址的问题及其解决方案,问题在于bionic层ifaddrs.cpp中对UID判断的逻辑错误,文中详细分析了问题原因及各种解决方案的优缺点,需要的朋友可以参考下

一、前言

最近在做 Android 多用户功能适配时,发现一个问题:

在主用户下可以正常获取有线网(Ethernet)的 MAC 地址,但切换到子用户后,通过 `NetworkInterface.getHardwareAddress()` 获取到的 MAC 地址为 null。
获取mac地址,需要系统应用或者系统权限应用;普通应用是获取不到的;
目前问题是系统权限应用,在子用户下也是无法获取到有线网节点eth0的mac地址。

这个问题影响了子用户下的网络信息展示、设备标识、某些应用激活等功能。

经过分析,问题出在 bionic 库的 ifaddrs.cpp 中,对多用户场景的 UID 判断代码。

本文记录完整的分析过程和解决方案。

Android13 之后好像就有这个问题,本文的代码具体代码展示是Android16的。

二、问题现象

在 Android 设备上创建子用户(userId=10)后,子用户中的系统应用调用以下代码获取 MAC 地址:

NetworkInterface ni = NetworkInterface.getByName("eth0");
byte[] mac = ni.getHardwareAddress();  // 子用户下返回 null
场景结果
主用户(userId=0)系统应用✅ 正常返回 MAC 地址
主用户(userId=0)普通应用❌ 返回 null(正常,安全限制)
子用户(userId=10)系统应用❌ 返回 null(异常,本文要解决的问题
子用户(userId=10)普通应用❌ 返回 null(正常,安全限制)

三、原因分析

1、NetworkInterface.getHardwareAddress() 调用链路

从应用层到内核层的完整调用链路如下:

应用层: NetworkInterface.getHardwareAddress()
  ↓
Java 层: libcore/ojluni/src/main/java/java/net/NetworkInterface.java
  ↓  返回 ni.hardwareAddr 字段
JNI 层: libcore/luni/src/main/native/libcore_io_Linux.cpp
  ↓  调用 getifaddrs()
Bionic 层: bionic/libc/bionic/ifaddrs.cpp  ← 【问题所在】
  ↓  通过 netlink 发送 RTM_GETLINK 请求
内核层: netlink socket → 返回 AF_PACKET 类型地址(包含 MAC)

getHardwareAddress() 的 Java 层代码本身没有做 UID 权限判断,它只是返回 hardwareAddr 字段:

// libcore/ojluni/src/main/java/java/net/NetworkInterface.java
public byte[] getHardwareAddress() throws SocketException {
    NetworkInterface ni = getByName(name);
    if (ni == null) {
        throw new SocketException("NetworkInterface doesn't exist anymore");
    }
    if (ni.hardwareAddr == null && !"lo".equals(name)
            && !Compatibility.isChangeEnabled(RETURN_NULL_HARDWARE_ADDRESS)) {
        return DEFAULT_MAC_ADDRESS.clone(); // 02:00:00:00:00:00
    }
    return ni.hardwareAddr;  // 关键:这个值来自 native 层
}

hardwareAddr 的值是在 native 层通过 getifaddrs() 获取并填充的。如果 getifaddrs() 没有返回 AF_PACKET 类型的地址信息,hardwareAddr 就是 null。

2、getifaddrs() 中的 UID 判断逻辑

问题的根源在 bionic/libc/bionic/ifaddrs.cpp 中的 getifaddrs() 函数:

// bionic/libc/bionic/ifaddrs.cpp
int getifaddrs(ifaddrs** out) {
  *out = nullptr;
  NetlinkConnection nc;
  // 关键判断:只有 uid < 10000 的进程才发送 RTM_GETLINK 请求
  bool getlink_success = false;
  if (getuid() < FIRST_APPLICATION_UID) {  // FIRST_APPLICATION_UID = 10000
    getlink_success =
        nc.SendRequest(RTM_GETLINK) && nc.ReadResponses(__getifaddrs_callback, out);
  }
  bool getaddr_success =
      nc.SendRequest(RTM_GETADDR) && nc.ReadResponses(__getifaddrs_callback, out);
  // ...
}

这里的逻辑是:

  • RTM_GETLINK:获取网络接口的链路层信息(包含 MAC 地址),只对 uid < 10000 的进程发送
  • RTM_GETADDR:获取网络接口的 IP 地址信息,所有进程都可以发送

注释也说明了原因:SELinux policy only allows RTM_GETLINK messages to be sent by system apps

3、Android 多用户 UID 计算规则

Android 多用户下,UID 的计算公式为:

uid = userId * 100000 + appId

其中:

  • userId:用户 ID,主用户为 0,子用户从 10 开始
  • appId:应用 ID,系统进程 < 10000,普通应用 ≥ 10000
  • 100000:用户偏移量(AID_USER_OFFSET

各场景下的 UID 值:

场景userIdappIdgetuid() 返回值
主用户 system_server010001000
主用户 shell020002000
主用户普通应用01006810068
子用户 system1010001001000
子用户 shell1020001002000
子用户普通应用10100681010068

4、问题根因定位

原代码的判断条件:

if (getuid() < FIRST_APPLICATION_UID)  // 即 getuid() < 10000
  • 主用户 system(uid=1000):1000 < 10000 ✅ → 发送 RTM_GETLINK → 获取到 MAC
  • 子用户 system(uid=1001000):1001000 < 10000 ❌ → 不发送 RTM_GETLINK → MAC 为 null

问题根因:ifaddrs.cpp 使用完整的 uid 做判断,没有考虑多用户场景。子用户的系统进程 uid 远大于 10000,被错误地当作普通应用处理,导致 RTM_GETLINK 请求不会发送,MAC 地址无法获取。

四、解决方案

1、修改NetworkInterface.getHardwareAddress()的返回信息 ?

这个是肯定不行的。

因为NetworkInterface 的代码位置在 libcore/ojluni/src/main/java/java/net/NetworkInterface.java。

这个是Java的类包,无法导入Android的类,获取不到Android的信息。

并且这个类库不是随系统编译的,试过代码中加入Java打印,编译验证是没有的显示的,

估计要用特殊指令单独编译这块代码,才会更新系统相关依赖包。

2、修改 bionic 层 ifaddrs.cpp

文件路径:bionic/libc/bionic/ifaddrs.cpp

getuid() 的判断改为提取 appId 后再比较:

修改前:

int getifaddrs(ifaddrs** out) {
  *out = nullptr;
  NetlinkConnection nc;
  bool getlink_success = false;
  if (getuid() < FIRST_APPLICATION_UID) {
    getlink_success =
        nc.SendRequest(RTM_GETLINK) && nc.ReadResponses(__getifaddrs_callback, out);
  }
  // ...
}

修改后:

int getifaddrs(ifaddrs** out) {
  *out = nullptr;
  NetlinkConnection nc;

  // 修改:使用 appId 判断,支持多用户场景
  // Android 多用户下 uid = userId * 100000 + appId
  // 子用户的系统应用 appId 仍然 < FIRST_APPLICATION_UID
  bool getlink_success = false;
  uid_t appId = getuid() % 100000;
  if (appId < FIRST_APPLICATION_UID) {
    getlink_success =
        nc.SendRequest(RTM_GETLINK) && nc.ReadResponses(__getifaddrs_callback, out);
  }
  // ...
}

核心改动就一行:把 getuid() 换成 getuid() % 100000

100000 是 Android 中用户偏移量(AID_USER_OFFSET)的固定值,从未改变过。通过取模运算提取出 appId,就能正确识别所有用户下的系统进程。

修改前后效果对比

场景uid原逻辑 getuid() < 10000修改后 getuid() % 100000 < 10000
主用户 system1000✅ 发送 RTM_GETLINK✅ 发送
主用户普通应用10068❌ 不发送❌ 不发送
子用户 system1001000不发送(问题)✅ 发送(appId=1000)
子用户普通应用1010068❌ 不发送❌ 不发送(appId=10068)

修改后,子用户的系统应用可以正常获取 MAC 地址。

普通应用可以吗?测试了一下还是不行!

就算强制进入获取 getlink_success 的逻辑,普通应用还是会返回0;

估计还要适配系统其他权限问题,比较麻烦所以这个解决方案对普通应用不行。

并且这种修改对 EDLA 认证可能会有影响,不建议使用。

3、应用层替代方案

如果不方便修改 bionic 层,也可以在应用层(系统应用)通过读取 sysfs 文件来获取 MAC 地址:

/**
 * 通过 sysfs 获取有线网 MAC 地址
 * 不依赖 NetworkInterface.getHardwareAddress(),不受 bionic 层 UID 限制
 */
public static String getEthernetMac() {
    try {
        return new String(Files.readAllBytes(
            Paths.get("/sys/class/net/eth0/address"))).trim();
    } catch (IOException e) {
        Log.e(TAG, "Failed to read MAC address from sysfs", e);
        return null;
    }
}

但这种方式需要 SELinux 策略允许应用读取 sysfs_net

# device/<vendor>/<device>/sepolicy/private/your_app.te
allow your_app_domain sysfs_net:file { read open getattr };
allow your_app_domain sysfs_net:dir { search };

UserDebug版本确实可以通过cat sys/class/net/eth0/address 获取有线网mac地址

但是配置策略比较麻烦,有需要的可以自行测试。

wifi的mac地址同理:sys/class/net/wlan0/address

这个方案也是只能适配系统应用,并且要适配权限,比较麻烦,不建议修改。

4、主线程的服务/应用获取并记录mac地址

可以在主用户进程中获取 MAC 地址写入个系统属性,子用户直接读属性:

String mac= getMacFromNetworkInterface();//主用户可以拿到
SystemProperties.set("persist.debug.eth.mac",mac);
// 子用户应用中,非系统应用需要反射获取
String mac=SystemProperties.get("persist.debug.etho.mac","");

可以在系统wifi服务或者自定义的系统应用服务启动时获取mac,切换用户过程,这些服务是一直在的。

这方案修改代码最少,又不影响系统其他功能。

如果不行用prop属性,是否可以用Settings属性?

一般的Settings.System、Secure都时候会重置的,Global属性不会重置,这个获取和设置更加简单一点。

//系统服务
Settings.Global.putString(getContentResolver(), "mac_address", macStringXXX);
//普通应用,直接调用,不用反射
String deviceMac = Settings.Global.getString(getContentResolver(),"mac_address");

这个是目前最简单的实现方式,Settings.Global 保存和获取mac地址信息;

普通应用是没有Settings设置权限的,只有读取权限。

5、让普通用户也可以设置和获取Settings.Global 属性

这个需要修改framework的代码,也是不太建议的。

系统修改下面两个地方其中一个:

DefaultPermissionGrantPolicy.java → 给指定包名自动授权
	grantPermissionsToPackage
PermissionManagerService.java → 全局放行权限(所有应用都能用)
	private boolean checkPermission(String perm, int pid, int uid, boolean debug)

普通应用定义权限:

android.permission.WRITE_SECURE_SETTINGS
android.permission.WRITE_GLOBAL_SETTINGS

之前普通应用就可以通过Settings.Global.putString 和 Settings.Global.getString 设置获取属性;

但是这个是破坏Android安全性的,EDLA认证是会有报错的。

五、其他

1、小结

Android 切换用户后无法获取 MAC 地址的根本原因是 bionic/libc/bionic/ifaddrs.cpp 中的 getifaddrs() 函数使用完整的 uid 做权限判断,没有考虑多用户场景。

子用户的系统进程 uid(如 1001000)远大于 FIRST_APPLICATION_UID(10000),被错误地当作普通应用,导致 RTM_GETLINK 请求不会发送,NetworkInterface.getHardwareAddress() 返回 null。

解决方案是将 getuid() 改为 getuid() % 100000,提取 appId 后再做判断,这样所有用户下的系统进程都能正确获取 MAC 地址,同时不影响普通应用的安全限制。

修改方式和修改涉及的文件:

  • bionic 层bionic/libc/bionic/ifaddrs.cpp(核心修改,改一行代码)
  • SELinux 策略:如有需要,确保子用户的系统应用有 netlink_route_socket 权限
  • 应用层替代:可通过读取 /sys/class/net/eth0/address 绕过,需配置 SELinux 策略
  • 系统服务:系统服务启动时获取mac地址保存到prop属性或者Settings.Global ,子用户可以读取。
  • 目前验证测试,通过系统服务设置Settings.Global 的mac属性,普通用户获取 Settings.Global 是最简单的。

2、获取ip和mac地址几种方式

1、ifconfig

2、获取wifi 的ip
可以通过 WifiManager 获取当前连接的wifi信息,获取到ip地址;

3、获取有线网、wifi的ip、mac
通过获取 ConnectivityManager获取连接的网络 Network-->LinkProperties获取到ip地址。

4、获取有线网、wifi、热点、p2p的ip、mac
通过获取所有节点信息:NetworkInterface.getNetworkInterfaces() 获取对应的ip地址和MAC地址。

3、普通应用反射获取prop的封装方法

封装类和方法,可以直接使用:

import android.text.TextUtils;
import android.util.Log;
import java.lang.reflect.Method;
public class SystemPropertiesUtil {
    private static final String TAG = "SystemPropertiesUtil";
    private static Method sGetMethod;
    private static Method sSetMethod;
    static {
        try {
            Class<?> clazz = Class.forName("android.os.SystemProperties");
            sGetMethod = clazz.getMethod("get", String.class, String.class);
            sSetMethod = clazz.getMethod("set", String.class, String.class);
        } catch (Exception e) {
            Log.e(TAG, "Failed to init SystemProperties methods", e);
        }
    }
    /**
     * 获取系统属性值
     *
     * @param key 属性名,如 "ro.build.display.id"
     * @param defaultValue 默认值,属性不存在或无权限时返回
     * @return 属性值
     */
    public static String get(String key, String defaultValue) {
        try {
            if (sGetMethod != null) {
                String value = (String) sGetMethod.invoke(null, key, defaultValue);
                return value;
            }
        } catch (Exception e) {
            Log.e(TAG, "Failed to get property: " + key, e);
        }
        return defaultValue;
    }
    /**
     * 获取系统属性值,默认返回空串
     */
    public static String get(String key) {
        return get(key, "");
    }
    /**
     * 获取 boolean 类型属性
     */
    public static boolean getBoolean(String key, boolean defaultValue) {
        String value = get(key, "");
        if (TextUtils.isEmpty(value)) return defaultValue;
        return "true".equalsIgnoreCase(value) || "1".equals(value);
    }
    /**
     * 获取 int 类型属性
     */
    public static int getInt(String key, int defaultValue) {
        String value = get(key, "");
        try {
            return TextUtils.isEmpty(value) ? defaultValue : Integer.parseInt(value);
        } catch (NumberFormatException e) {
            return defaultValue;
        }
    }
    /**
     * 设置系统属性(普通应用通常没有权限,仅系统应用可用)
     */
    public static void set(String key, String value) {
        try {
            if (sSetMethod != null) {
                sSetMethod.invoke(null, key, value);
            }
        } catch (Exception e) {
            Log.e(TAG, "Failed to set property: " + key, e);
        }
    }
}

如果系统应用通过prop设置mac地址属性,普通用户就用上面这个封装方法获取prop的属性。

以上就是Android切换用户后无法获取MAC地址的解决方法的详细内容,更多关于Android无法获取MAC地址的资料请关注脚本之家其它相关文章!

相关文章

  • android中RecyclerView悬浮吸顶效果

    android中RecyclerView悬浮吸顶效果

    本篇文章主要介绍了android中RecyclerView悬浮吸顶效果,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-06-06
  • Android开发之模仿微信打开网页的进度条效果(高仿)

    Android开发之模仿微信打开网页的进度条效果(高仿)

    这篇文章主要介绍了Android开发之模仿微信打开网页的进度条效果(高仿)的相关资料,非常不错,具有参考借鉴价值,需要的朋友可以参考下
    2016-07-07
  • android仿微信表情雨下落效果的实现方法

    android仿微信表情雨下落效果的实现方法

    这篇文章主要给大家介绍了关于android仿微信表情雨下落效果的实现方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2018-09-09
  • OpenGL Shader实例分析(7)雪花飘落效果

    OpenGL Shader实例分析(7)雪花飘落效果

    这篇文章主要为大家详细介绍了OpenGL Shader实例分析第7篇,实现雪花飘落效果,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2019-02-02
  • Android Choreographer源码详细分析

    Android Choreographer源码详细分析

    Choreographer的作用主要是配合Vsync,给上层App的渲染提供一个稳定的Message处理的时机,也就是Vsync到来的时候,系统通过对Vsync信号周期的调整,来控制每一帧绘制操作的时机
    2022-08-08
  • Android项目中实体类entity的作用详解

    Android项目中实体类entity的作用详解

    这篇文章主要介绍了Android项目中实体类entity的作用详解,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-04-04
  • Android实现通话最小化悬浮框效果

    Android实现通话最小化悬浮框效果

    本片内容给大家介绍了Android音视频通话过程中最小化成悬浮框的实现的方法以及代码写法。
    2017-11-11
  • recycleview实现拼多多首页水平滑动效果

    recycleview实现拼多多首页水平滑动效果

    这篇文章主要为大家详细介绍了recycleview实现拼多多首页水平滑动效,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-05-05
  • Android EditText实现输入表情

    Android EditText实现输入表情

    editText是TextView的子类,TextView能用的工具EditText都能用,接下来通过实例代码给大家分享Android EditText实现输入表情功能,感兴趣的朋友一起看看吧
    2017-08-08
  • Android实现裁剪照片功能

    Android实现裁剪照片功能

    这篇文章主要为大家详细介绍了Android实现裁剪照片功能,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-03-03

最新评论