本篇文章基 android-11.0.0_r17
编写
我们在做App开发的时候,通常会有获取唯一标示的需求,在这里Android提供了ANDROID_ID
的方式来满足大家需求
我们先看一下如何获取ANDROID_ID
,然后逐渐深入系统,去查看ANDROID_ID
的生成规则,帮助大家更好的理解和选择ANDROID_ID
String androidId = Settings.Secure.getString(getContentResolver(), Settings.Secure.ANDROID_ID);
我可以可能是通过Settings
下的Secure
内部类下的getString方法来获取,并且传入了一个 ContentResolver 对象以及系统提供的 Settings.Secure.ANDROID_ID
常量值,看到这里大家不妨猜一下,我想这里肯定是通过IPC
的方式访问Settings应用,然后根据自身的应用信息来去生成一个ID,我们继续往下
public static String getString(ContentResolver resolver, String name) {
return getStringForUser(resolver, name, resolver.getUserId());
}
public static String getStringForUser(ContentResolver resolver, String name,
int userHandle) {
if (MOVED_TO_GLOBAL.contains(name)) {
Log.w(TAG, "Setting " + name + " has moved from android.provider.Settings.Secure"
+ " to android.provider.Settings.Global.");
return Global.getStringForUser(resolver, name, userHandle);
}
if (MOVED_TO_LOCK_SETTINGS.contains(name)) {
synchronized (Secure.class) {
if (sLockSettings == null) {
sLockSettings = ILockSettings.Stub.asInterface(
(IBinder) ServiceManager.getService("lock_settings"));
sIsSystemProcess = Process.myUid() == Process.SYSTEM_UID;
}
}
if (sLockSettings != null && !sIsSystemProcess) {
// No context; use the ActivityThread‘s context as an approximation for
// determining the target API level.
Application application = ActivityThread.currentApplication();
boolean isPreMnc = application != null
&& application.getApplicationInfo() != null
&& application.getApplicationInfo().targetSdkVersion
<= VERSION_CODES.LOLLIPOP_MR1;
if (isPreMnc) {
try {
return sLockSettings.getString(name, "0", userHandle);
} catch (RemoteException re) {
// Fall through
}
} else {
throw new SecurityException("Settings.Secure." + name
+ " is deprecated and no longer accessible."
+ " See API documentation for potential replacements.");
}
}
}
return sNameValueCache.getStringForUser(resolver, name, userHandle);
}
首先判断两个List里是否存在name,这里如果name=ANDROID_ID的话,两个if都不会命中,会直接从最后行来获取,这里可能是缓存,我们先进去看看具体逻辑
@UnsupportedAppUsage
public String getStringForUser(ContentResolver cr, String name, final int userHandle) {
//首先判断当前应用是不是属于当前登录用户
final boolean isSelf = (userHandle == UserHandle.myUserId());
int currentGeneration = -1;
if (isSelf) {
synchronized (NameValueCache.this) {
if (mGenerationTracker != null) {
if (mGenerationTracker.isGenerationChanged()) {
if (DEBUG) {
Log.i(TAG, "Generation changed for type:"
+ mUri.getPath() + " in package:"
+ cr.getPackageName() +" and user:" + userHandle);
}
mValues.clear();
} else if (mValues.containsKey(name)) {
return mValues.get(name);
}
if (mGenerationTracker != null) {
currentGeneration = mGenerationTracker.getCurrentGeneration();
}
}
}
} else {
if (LOCAL_LOGV) Log.v(TAG, "get setting for user " + userHandle
+ " by user " + UserHandle.myUserId() + " so skipping cache");
}
IContentProvider cp = mProviderHolder.getProvider(cr);
// Try the fast path first, not using query(). If this
// fails (alternate Settings provider that doesn‘t support
// this interface?) then we fall back to the query/table
// interface.
if (mCallGetCommand != null) {
try {
Bundle args = null;
if (!isSelf) {
args = new Bundle();
args.putInt(CALL_METHOD_USER_KEY, userHandle);
}
boolean needsGenerationTracker = false;
synchronized (NameValueCache.this) {
if (isSelf && mGenerationTracker == null) {
needsGenerationTracker = true;
if (args == null) {
args = new Bundle();
}
args.putString(CALL_METHOD_TRACK_GENERATION_KEY, null);
if (DEBUG) {
Log.i(TAG, "Requested generation tracker for type: "+ mUri.getPath()
+ " in package:" + cr.getPackageName() +" and user:"
+ userHandle);
}
}
}
Bundle b;
// If we‘re in system server and in a binder transaction we need to clear the
// calling uid. This works around code in system server that did not call
// clearCallingIdentity, previously this wasn‘t needed because reading settings
// did not do permission checking but thats no longer the case.
// Long term this should be removed and callers should properly call
// clearCallingIdentity or use a ContentResolver from the caller as needed.
if (Settings.isInSystemServer() && Binder.getCallingUid() != Process.myUid()) {
final long token = Binder.clearCallingIdentity();
try {
b = cp.call(cr.getPackageName(), cr.getAttributionTag(),
mProviderHolder.mUri.getAuthority(), mCallGetCommand, name,
args);
} finally {
Binder.restoreCallingIdentity(token);
}
} else {
b = cp.call(cr.getPackageName(), cr.getAttributionTag(),
mProviderHolder.mUri.getAuthority(), mCallGetCommand, name, args);
}
if (b != null) {
String value = b.getString(Settings.NameValueTable.VALUE);
// Don‘t update our cache for reads of other users‘ data
if (isSelf) {
synchronized (NameValueCache.this) {
if (needsGenerationTracker) {
MemoryIntArray array = b.getParcelable(
CALL_METHOD_TRACK_GENERATION_KEY);
final int index = b.getInt(
CALL_METHOD_GENERATION_INDEX_KEY, -1);
if (array != null && index >= 0) {
final int generation = b.getInt(
CALL_METHOD_GENERATION_KEY, 0);
if (DEBUG) {
Log.i(TAG, "Received generation tracker for type:"
+ mUri.getPath() + " in package:"
+ cr.getPackageName() + " and user:"
+ userHandle + " with index:" + index);
}
if (mGenerationTracker != null) {
mGenerationTracker.destroy();
}
mGenerationTracker = new GenerationTracker(array, index,
generation, () -> {
synchronized (NameValueCache.this) {
Log.e(TAG, "Error accessing generation"
+ " tracker - removing");
if (mGenerationTracker != null) {
GenerationTracker generationTracker =
mGenerationTracker;
mGenerationTracker = null;
generationTracker.destroy();
mValues.clear();
}
}
});
currentGeneration = generation;
}
}
if (mGenerationTracker != null && currentGeneration ==
mGenerationTracker.getCurrentGeneration()) {
mValues.put(name, value);
}
}
} else {
if (LOCAL_LOGV) Log.i(TAG, "call-query of user " + userHandle
+ " by " + UserHandle.myUserId()
+ " so not updating cache");
}
return value;
}
// If the response Bundle is null, we fall through
// to the query interface below.
} catch (RemoteException e) {
// Not supported by the remote side? Fall through
// to query().
}
}
Cursor c = null;
try {
Bundle queryArgs = ContentResolver.createSqlQueryBundle(
NAME_EQ_PLACEHOLDER, new String[]{name}, null);
// Same workaround as above.
if (Settings.isInSystemServer() && Binder.getCallingUid() != Process.myUid()) {
final long token = Binder.clearCallingIdentity();
try {
c = cp.query(cr.getPackageName(), cr.getAttributionTag(), mUri,
SELECT_VALUE_PROJECTION, queryArgs, null);
} finally {
Binder.restoreCallingIdentity(token);
}
} else {
c = cp.query(cr.getPackageName(), cr.getAttributionTag(), mUri,
SELECT_VALUE_PROJECTION, queryArgs, null);
}
if (c == null) {
Log.w(TAG, "Can‘t get key " + name + " from " + mUri);
return null;
}
String value = c.moveToNext() ? c.getString(0) : null;
synchronized (NameValueCache.this) {
if (mGenerationTracker != null
&& currentGeneration == mGenerationTracker.getCurrentGeneration()) {
mValues.put(name, value);
}
}
if (LOCAL_LOGV) {
Log.v(TAG, "cache miss [" + mUri.getLastPathSegment() + "]: " +
name + " = " + (value == null ? "(null)" : value));
}
return value;
} catch (RemoteException e) {
Log.w(TAG, "Can‘t get key " + name + " from " + mUri, e);
return null; // Return null, but don‘t cache it.
} finally {
if (c != null) c.close();
}
}
别看这个方法这么多代码,但是其实真正核心的逻辑只有两行,得到SettingsProvider的ContentProvider
然后在调用call方法来查询数据,返回一个Bundle数据,我们的AndroidID就在这个Bundle中,我们先去看这个Provider的实现
IContentProvider cp = mProviderHolder.getProvider(cr);
b = cp.call(cr.getPackageName(), cr.getAttributionTag(),
mProviderHolder.mUri.getAuthority(), mCallGetCommand, name, args);
我们来到了SettingsProvider这个项目下,找到对应的Provider,代码如下
@Override
public Bundle call(String method, String name, Bundle args) {
final int requestingUserId = getRequestingUserId(args);
switch (method) {
case Settings.CALL_METHOD_GET_CONFIG: {
Setting setting = getConfigSetting(name);
return packageValueForCallResult(setting, isTrackingGeneration(args));
}
case Settings.CALL_METHOD_GET_GLOBAL: {
Setting setting = getGlobalSetting(name);
return packageValueForCallResult(setting, isTrackingGeneration(args));
}
case Settings.CALL_METHOD_GET_SECURE: {
Setting setting = getSecureSetting(name, requestingUserId,
/*enableOverride=*/ true);
return packageValueForCallResult(setting, isTrackingGeneration(args));
}
case Settings.CALL_METHOD_GET_SYSTEM: {
Setting setting = getSystemSetting(name, requestingUserId);
return packageValueForCallResult(setting, isTrackingGeneration(args));
}
case Settings.CALL_METHOD_PUT_CONFIG: {
String value = getSettingValue(args);
final boolean makeDefault = getSettingMakeDefault(args);
insertConfigSetting(name, value, makeDefault);
break;
}
case Settings.CALL_METHOD_PUT_GLOBAL: {
String value = getSettingValue(args);
String tag = getSettingTag(args);
final boolean makeDefault = getSettingMakeDefault(args);
final boolean overrideableByRestore = getSettingOverrideableByRestore(args);
insertGlobalSetting(name, value, tag, makeDefault, requestingUserId, false,
overrideableByRestore);
break;
}
case Settings.CALL_METHOD_PUT_SECURE: {
String value = getSettingValue(args);
String tag = getSettingTag(args);
final boolean makeDefault = getSettingMakeDefault(args);
final boolean overrideableByRestore = getSettingOverrideableByRestore(args);
insertSecureSetting(name, value, tag, makeDefault, requestingUserId, false,
overrideableByRestore);
break;
}
case Settings.CALL_METHOD_PUT_SYSTEM: {
String value = getSettingValue(args);
boolean overrideableByRestore = getSettingOverrideableByRestore(args);
insertSystemSetting(name, value, requestingUserId, overrideableByRestore);
break;
}
case Settings.CALL_METHOD_SET_ALL_CONFIG: {
String prefix = getSettingPrefix(args);
Map<String, String> flags = getSettingFlags(args);
Bundle result = new Bundle();
result.putBoolean(Settings.KEY_CONFIG_SET_RETURN,
setAllConfigSettings(prefix, flags));
return result;
}
case Settings.CALL_METHOD_RESET_CONFIG: {
final int mode = getResetModeEnforcingPermission(args);
String prefix = getSettingPrefix(args);
resetConfigSetting(mode, prefix);
break;
}
case Settings.CALL_METHOD_RESET_GLOBAL: {
final int mode = getResetModeEnforcingPermission(args);
String tag = getSettingTag(args);
resetGlobalSetting(requestingUserId, mode, tag);
break;
}
case Settings.CALL_METHOD_RESET_SECURE: {
final int mode = getResetModeEnforcingPermission(args);
String tag = getSettingTag(args);
resetSecureSetting(requestingUserId, mode, tag);
break;
}
case Settings.CALL_METHOD_DELETE_CONFIG: {
int rows = deleteConfigSetting(name) ? 1 : 0;
Bundle result = new Bundle();
result.putInt(RESULT_ROWS_DELETED, rows);
return result;
}
case Settings.CALL_METHOD_DELETE_GLOBAL: {
int rows = deleteGlobalSetting(name, requestingUserId, false) ? 1 : 0;
Bundle result = new Bundle();
result.putInt(RESULT_ROWS_DELETED, rows);
return result;
}
case Settings.CALL_METHOD_DELETE_SECURE: {
int rows = deleteSecureSetting(name, requestingUserId, false) ? 1 : 0;
Bundle result = new Bundle();
result.putInt(RESULT_ROWS_DELETED, rows);
return result;
}
case Settings.CALL_METHOD_DELETE_SYSTEM: {
int rows = deleteSystemSetting(name, requestingUserId) ? 1 : 0;
Bundle result = new Bundle();
result.putInt(RESULT_ROWS_DELETED, rows);
return result;
}
case Settings.CALL_METHOD_LIST_CONFIG: {
String prefix = getSettingPrefix(args);
Bundle result = packageValuesForCallResult(getAllConfigFlags(prefix),
isTrackingGeneration(args));
reportDeviceConfigAccess(prefix);
return result;
}
case Settings.CALL_METHOD_REGISTER_MONITOR_CALLBACK_CONFIG: {
RemoteCallback callback = args.getParcelable(
Settings.CALL_METHOD_MONITOR_CALLBACK_KEY);
setMonitorCallback(callback);
break;
}
case Settings.CALL_METHOD_LIST_GLOBAL: {
Bundle result = new Bundle();
result.putStringArrayList(RESULT_SETTINGS_LIST,
buildSettingsList(getAllGlobalSettings(null)));
return result;
}
case Settings.CALL_METHOD_LIST_SECURE: {
Bundle result = new Bundle();
result.putStringArrayList(RESULT_SETTINGS_LIST,
buildSettingsList(getAllSecureSettings(requestingUserId, null)));
return result;
}
case Settings.CALL_METHOD_LIST_SYSTEM: {
Bundle result = new Bundle();
result.putStringArrayList(RESULT_SETTINGS_LIST,
buildSettingsList(getAllSystemSettings(requestingUserId, null)));
return result;
}
default: {
Slog.w(LOG_TAG, "call() with invalid method: " + method);
} break;
}
return null;
}
这里呢,我们只关心 CALL_METHOD_GET_SECURE
,但是为了代码的完整性,我保留了所有代码。在上面的逻辑上可以看到是先是通过getSecureSetting
方法获取了Setting
对象,然后调用packageValueForCallResult
方法传入Setting对象,我们先看getSecureSetting
的方法实现
private Setting getSecureSetting(String name, int requestingUserId, boolean enableOverride) {
if (DEBUG) {
Slog.v(LOG_TAG, "getSecureSetting(" + name + ", " + requestingUserId + ")");
}
// Resolve the userId on whose behalf the call is made.
final int callingUserId = resolveCallingUserIdEnforcingPermissionsLocked(requestingUserId);
// Ensure the caller can access the setting.
enforceSettingReadable(name, SETTINGS_TYPE_SECURE, UserHandle.getCallingUserId());
// Determine the owning user as some profile settings are cloned from the parent.
final int owningUserId = resolveOwningUserIdForSecureSettingLocked(callingUserId, name);
if (!isSecureSettingAccessible(name, callingUserId, owningUserId)) {
// This caller is not permitted to access this setting. Pretend the setting doesn‘t
// exist.
SettingsState settings = mSettingsRegistry.getSettingsLocked(SETTINGS_TYPE_SECURE,
owningUserId);
return settings != null ? settings.getNullSetting() : null;
}
// As of Android O, the SSAID is read from an app-specific entry in table
// SETTINGS_FILE_SSAID, unless accessed by a system process.
if (isNewSsaidSetting(name)) {
PackageInfo callingPkg = getCallingPackageInfo(owningUserId);
synchronized (mLock) {
return getSsaidSettingLocked(callingPkg, owningUserId);
}
}
if (enableOverride) {
if (Secure.LOCATION_MODE.equals(name)) {
final Setting overridden = getLocationModeSetting(owningUserId);
if (overridden != null) {
return overridden;
}
}
}
// Not the SSAID; do a straight lookup
synchronized (mLock) {
return mSettingsRegistry.getSettingLocked(SETTINGS_TYPE_SECURE,
owningUserId, name);
}
}
private PackageInfo getCallingPackageInfo(int userId) {
try {
return mPackageManager.getPackageInfo(getCallingPackage(),
PackageManager.GET_SIGNATURES, userId);
} catch (RemoteException e) {
throw new IllegalStateException("Package " + getCallingPackage() + " doesn‘t exist");
}
}
我们的查询Setting的这个操作,会通过 isNewSsaidSetting(name)
这个判断来处理,然后他调用getCallingPackageInfo
来查询应用签名信息,最后把存有应用签名信息的PackageInfo
传入getSsaidSettingLocked
方法中。我们继续查看
private Setting getSsaidSettingLocked(PackageInfo callingPkg, int owningUserId) {
// Get uid of caller (key) used to store ssaid value
// 这个name是uid,是应用ID
String name = Integer.toString(
UserHandle.getUid(owningUserId, UserHandle.getAppId(Binder.getCallingUid())));
if (DEBUG) {
Slog.v(LOG_TAG, "getSsaidSettingLocked(" + name + "," + owningUserId + ")");
}
// Retrieve the ssaid from the table if present.
// 先通过mSettings缓存来获取对应的Setting对象,到这里如果Settings不是空的,其实就已经查出来ssaid了
final Setting ssaid = mSettingsRegistry.getSettingLocked(SETTINGS_TYPE_SSAID, owningUserId,name);
// If the app is an Instant App use its stored SSAID instead of our own.
// 获取免安装应用的ssaid,咱们正常安装的apk都是返回空值,具体操作在PackageManagerService下的getInstantAppAndroidId方法,感兴趣的同学可以去自己看一下,这里不延伸了
final String instantSsaid;
final long token = Binder.clearCallingIdentity();
try {
instantSsaid = mPackageManager.getInstantAppAndroidId(callingPkg.packageName,
owningUserId);
} catch (RemoteException e) {
Slog.e(LOG_TAG, "Failed to get Instant App Android ID", e);
return null;
} finally {
Binder.restoreCallingIdentity(token);
}
final SettingsState ssaidSettings = mSettingsRegistry.getSettingsLocked(
SETTINGS_TYPE_SSAID, owningUserId);
if (instantSsaid != null) {
// Use the stored value if it is still valid.
if (ssaid != null && instantSsaid.equals(ssaid.getValue())) {
return mascaradeSsaidSetting(ssaidSettings, ssaid);
}
// The value has changed, update the stored value.
final boolean success = ssaidSettings.insertSettingLocked(name, instantSsaid, null,
true, callingPkg.packageName);
if (!success) {
throw new IllegalStateException("Failed to update instant app android id");
}
Setting setting = mSettingsRegistry.getSettingLocked(SETTINGS_TYPE_SSAID,
owningUserId, name);
return mascaradeSsaidSetting(ssaidSettings, setting);
}
// Lazy initialize ssaid if not yet present in ssaid table.
// 这里会做一个检测,当我们的App首次安装的时候,缓存的ssaid列表里是没有Setting对象的
if (ssaid == null || ssaid.isNull() || ssaid.getValue() == null) {
Setting setting = mSettingsRegistry.generateSsaidLocked(callingPkg, owningUserId);
return mascaradeSsaidSetting(ssaidSettings, setting);
}
return mascaradeSsaidSetting(ssaidSettings, ssaid);
}
private Setting mascaradeSsaidSetting(SettingsState settingsState, Setting ssaidSetting) {
// SSAID settings are located in a dedicated table for internal bookkeeping
// but for the world they reside in the secure table, so adjust the key here.
// We have a special name when looking it up but want the world to see it as
// "android_id".
if (ssaidSetting != null) {
return settingsState.new Setting(ssaidSetting) {
@Override
public int getKey() {
final int userId = getUserIdFromKey(super.getKey());
return makeKey(SETTINGS_TYPE_SECURE, userId);
}
@Override
public String getName() {
return Settings.Secure.ANDROID_ID;
}
};
}
return null;
}
private Bundle packageValueForCallResult(Setting setting, boolean trackingGeneration) {
if (!trackingGeneration) {
if (setting == null || setting.isNull()) {
return NULL_SETTING_BUNDLE;
}
return Bundle.forPair(Settings.NameValueTable.VALUE, setting.getValue());
}
Bundle result = new Bundle();
result.putString(Settings.NameValueTable.VALUE,
!setting.isNull() ? setting.getValue() : null);
mSettingsRegistry.mGenerationRegistry.addGenerationData(result, setting.getKey());
return result;
}
到这里其实就已经知道整个查询过程了。我在梳理一下整个查询过程。
1.通过Settings.Secure.getString来发起查询请求
2.Settings内部会通过SettingsProvider的方式去查询,此时已经通过IPC的方式查询了,处理流已经到了SettingsProvider进程内
3.SettingsProvider会通过uid进行查询对应的Setting也就是ssaid对象,然后在最后会判断是否为空,因为我们应用首次安装的时候,这个ssaid是空值,系统会生成一遍ssaid,然后在返回封装有ssaid的Setting对象
4.最后调用packageValueForCallResult
方法,把Setting对象转化为Bundle对象并用于进程传递
接下来看一下生成过程 generateSsaidLocked
public Setting generateSsaidLocked(PackageInfo callingPkg, int userId) {
// Read the user‘s key from the ssaid table.
// 获取当前用户维度的setting对象
Setting userKeySetting = getSettingLocked(SETTINGS_TYPE_SSAID, userId, SSAID_USER_KEY);
if (userKeySetting == null || userKeySetting.isNull()
|| userKeySetting.getValue() == null) {
// Lazy initialize and store the user key.
generateUserKeyLocked(userId);
userKeySetting = getSettingLocked(SETTINGS_TYPE_SSAID, userId, SSAID_USER_KEY);
if (userKeySetting == null || userKeySetting.isNull()
|| userKeySetting.getValue() == null) {
throw new IllegalStateException("User key not accessible");
}
}
final String userKey = userKeySetting.getValue();
if (userKey == null || userKey.length() % 2 != 0) {
throw new IllegalStateException("User key invalid");
}
// Convert the user‘s key back to a byte array.
final byte[] keyBytes = HexEncoding.decode(userKey);
// Validate that the key is of expected length.
// Keys are currently 32 bytes, but were once 16 bytes during Android O development.
if (keyBytes.length != 16 && keyBytes.length != 32) {
throw new IllegalStateException("User key invalid");
}
//用 userKey 初始化 HmacSHA256 算法
final Mac m;
try {
m = Mac.getInstance("HmacSHA256");
m.init(new SecretKeySpec(keyBytes, m.getAlgorithm()));
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("HmacSHA256 is not available", e);
} catch (InvalidKeyException e) {
throw new IllegalStateException("Key is corrupted", e);
}
// Mac each of the developer signatures.
// 通过 callingPkg 对象获取调用应用的签名信息,然后进行 HmacSHA256 加密
for (int i = 0; i < callingPkg.signatures.length; i++) {
byte[] sig = callingPkg.signatures[i].toByteArray();
m.update(getLengthPrefix(sig), 0, 4);
m.update(sig);
}
// Convert result to a string for storage in settings table. Only want first 64 bits.
// 截取前16位字符作为当前应用的AndroidId
final String ssaid = HexEncoding.encodeToString(m.doFinal(), false /* upperCase */)
.substring(0, 16);
// Save the ssaid in the ssaid table.
final String uid = Integer.toString(callingPkg.applicationInfo.uid);
final SettingsState ssaidSettings = getSettingsLocked(SETTINGS_TYPE_SSAID, userId);
//插入到缓存中
final boolean success = ssaidSettings.insertSettingLocked(uid, ssaid, null, true,
callingPkg.packageName);
if (!success) {
throw new IllegalStateException("Ssaid settings not accessible");
}
return getSettingLocked(SETTINGS_TYPE_SSAID, userId, uid);
}
其中生成过程也很清楚了,是基于当前用户的随机串(会存储到本地文件,只有初次生成是随机串),加上当前应用的签名信息,采用SHA-256的加密方式得到的加密串截取前16位生成出来,所以只要应用签名不会变,手机登录的用户不会变的情况下,ANDROID_ID就不会变
有些同学可能还不太明白userKey是什么东西。我简单描述下
userKey
是在设备的系统首次启动的时候,根据当前的userId生成的一个随机串,然后存储到 /data/system/users/0/settings_global.xml
目录下,这个只有在恢复出厂设置、变更用户、刷机的时候才会发生变化
所以当下8.0以上如果想要区分设备ID,SSAID是当下最好的选择,毕竟Google大法好
探索 Android的SSAID(Android ID) 的奥秘