最近公司启动了隐私合规检测,报告告知我们在用户未同意隐私协议前,我们APP提前获取了WIIF/SSID/Mac地址等信息,不合规需要处理 (就是在同意前以及拒绝后, 不进行某些SDK的初始化…)
APP启动时,在用户授权同意隐私政策前,APP及SDK不可以提前收集和使用IMEI、OAID、IMSI、MAC、应用列表等信息
为了处理上面问题,或者说, 为了自己能够检测改版后的APP 是否符合要求, 我们就需要自己进行对APP进行检测和验证, 确保修改后达到效果
(第三方检测机构出的报告, 我们也要能自己给自己出 ╭(╯^╰)╮)
一通了解折腾以后, 我遇到了这么些框架/工具/demo, 这里逐个介绍一下
Xposed -> Dexposed -> Epic -> VirtualXposed ->HookLoginDemo
Xposed框架不用多说 rovo90 - Xposed , Xpose中文站 但是直接使用这个,需要走root,搞机等操作,较为麻烦
Dexposed 是阿里开源的 alibaba/dexposed,能在非root情况下掌控自己进程空间内的任意Java方法调用, 主要的就是用框架进行hook, 来抓取系统函数调用, 追踪三方SDK的启动情况/函数调用等
Epic - github 是weishu大神基于ART重新实现的一套 Dexposed( ART取代Dalvik成为Android的运行时,Dexposed无法支持更高版本的安卓系统) 这边一篇大神的Epic介绍
Epic可以在你自己的android工程中添加依赖,
dependencies {
compile 'com.github.tiann:epic:0.11.2'
}
然后可以通过
class ThreadMethodHook extends XC_MethodHook{
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
Thread t = (Thread) param.thisObject;
Log.i(TAG, "thread:" + t + ", started..");
}
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
Thread t = (Thread) param.thisObject;
Log.i(TAG, "thread:" + t + ", exit..");
}
}
DexposedBridge.hookAllConstructors(Thread.class, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
Thread thread = (Thread) param.thisObject;
Class<?> clazz = thread.getClass();
if (clazz != Thread.class) {
Log.d(TAG, "found class extend Thread:" + clazz);
DexposedBridge.findAndHookMethod(clazz, "run", new ThreadMethodHook());
}
Log.d(TAG, "Thread: " + thread.getName() + " class:" + thread.getClass() + " is created.");
}
});
DexposedBridge.findAndHookMethod(Thread.class, "run", new ThreadMethodHook());
提供的函数进行 hook操作, 但是只支持 inhook,就是你自己有android工程, 自己在项目里面添加epic依赖后, 自己写代码自己抓日志,
其实, 如果仅仅是为了验证用户隐私确认前合规的改版后APP
. Epic其实足够使用了. 借助Epic,我们可以通过移除SDK来比对监听偷跑的WIFI/MAC地址等行为是否还存在
贴一下我项目里面的一些调用
在MainActivity中, oncreat() 中调用就可以
implementation 'com.github.tiann:epic:0.11.2'
private String TAG ="ddddd";
private void startHook() {
XC_MethodHook hook = new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
Object hookObj = param.thisObject;
String clsName = "unknownClass";
if (hookObj != null) {
clsName = hookObj.getClass().getName();
}
String mdName = "unknownMethod";
if (param.method != null) {
mdName = param.method.getName();
}
Log.d(TAG, "beforeHookedMethod: " + clsName + "-" + mdName);
}
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
Object hookObj = param.thisObject;
String clsName = "unknownClass";
if (hookObj != null) {
clsName = hookObj.getClass().getName();
}
String mdName = "unknownMethod";
if (param.method != null) {
mdName = param.method.getName();
}
Log.d(TAG, "afterHookedMethod: " + clsName + "-" + mdName);
}
};
try {
// hook系统方法
// hook系统方法
// DexposedBridge.hookAllMethods(Thread.class, "run", hook);
// hook构造方法
// DexposedBridge.hookAllConstructors(Thread.class, hook);
DexposedBridge.hookAllMethods(BluetoothLeScanner.class, "startScan", hook);
// deviceID
DexposedBridge.hookAllMethods(TelephonyManager.class,"getDeviceId", hook);
// wifi
DexposedBridge.hookAllMethods(WifiManager.class,"", hook);
// wifi - 构造hook
DexposedBridge.hookAllConstructors(WifiManager.class, hook);
// MAC
DexposedBridge.hookAllMethods(WifiManager.class,"getMacAddress", hook);
} catch (Throwable t) {
t.printStackTrace();
}
}
踩坑1: Epic依赖的
minSdkVersion
是21
, 记得把项目的minSdkVersion改为一样否则跑不了程序
但是,检测机构是如何检测我们的APP?
猜测也是借助于 VirtualXposed 等工具,自行编写hook插件 , 来扫描 提交审核的 APP
VirtualXposed - 官方介绍及使用视频 是一个apk应用程序, 目的就是
过简单的 APP 使用 Xposed,无需 root、解锁引导加载程序或刷写系统映像。
具体使用开官网/或者视频,
VirtualXposed 是基于VirtualApp 和 epic 在非ROOT环境下运行Xposed模块的实现(支持5.0~10.0)。
我最后使用的是 0.20.3 版本, 其他版本都没成功, 要么唤起不了自己的APP, 要么安装不了 Xposed install 失败
这是目前我测试确认可以使用的环境:
设备: 小米 Mix2s
Androdi版本:10
VirtualXposed版本: 0.20.3
踩坑1: VirtualXposed 仅支持到 Android 5~10 , 在2022的今天, 也好在我的Mix2s还能使用, 一开始我用公司测试机 都是Android11, 不支持…
踩坑2: 确定自己项目的APP 是 32位 还是 64位 , 32位的仅仅能使用 0.18版本
HookLoginDemo 是一个插件(也是APP) , 基于这个插件 , 可以让我们方便的对我们的APP进行hook, 通过查看日志来方便的检测
在用户同意隐私前
我们的APP或第三方SDK, 有没有偷跑一些系统API, (就像检测机构对我们APP做的检测一样
)
当然, 也可以在
HookLoginDemo
中添加跟多的检测 比如SSID / WIFI列表. 只要稍微修改代码编译成apk 安装到手机, 接着就是 VirtualXposed 的时间了
贴一下 HookLoginDemo 中
HookLogin
代码的修改 (有错误的代码… 后面再完善)
package com.example.hooklogin;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Context;
import android.location.LocationManager;
import android.util.Log;
import android.widget.AutoCompleteTextView;
import android.widget.EditText;
import android.widget.Toast;
import java.lang.reflect.Field;
import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage;
import static de.robv.android.xposed.XposedHelpers.findField;
public class HookLogin implements IXposedHookLoadPackage {
private static final String TAG = "HookLogin";
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) {
if (lpparam == null) {
return;
}
Log.e(TAG, "Load app packageName:" + lpparam.packageName);
/*判断hook的包名*/
// if (!MyApplication.pageName.equals(lpparam.packageName) && !"com.enhance.kaomanfen.yasilisteningapp".equals(lpparam.packageName)
// && !"com.tal.tiku".equals(lpparam.packageName)) {
// return;
// }
// //固定格式
// XposedHelpers.findAndHookMethod(
// "com.", // 需要hook的方法所在类的完整类名
// lpparam.classLoader, // 类加载器,固定这么写就行了
// "attachBaseContext", // 需要hook的方法名
// Context.class,
// new XC_MethodHook() {
// @Override
// protected void beforeHookedMethod(MethodHookParam param) {
// XposedBridge.log("调用getDeviceId()获取了imei");
// }
//
// @Override
// protected void afterHookedMethod(MethodHookParam param) throws Throwable {
// XposedBridge.log(getMethodStack());
// super.afterHookedMethod(param);
// }
// }
// );
//固定格式
XposedHelpers.findAndHookMethod(
android.telephony.TelephonyManager.class.getName(), // 需要hook的方法所在类的完整类名
lpparam.classLoader, // 类加载器,固定这么写就行了
"getDeviceId", // 需要hook的方法名
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
XposedBridge.log("调用getDeviceId()获取了imei");
}
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
XposedBridge.log(getMethodStack());
super.afterHookedMethod(param);
}
}
);
XposedHelpers.findAndHookMethod(
android.telephony.TelephonyManager.class.getName(),
lpparam.classLoader,
"getDeviceId",
int.class,
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
XposedBridge.log("调用getDeviceId(int)获取了imei");
}
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
XposedBridge.log(getMethodStack());
super.afterHookedMethod(param);
}
}
);
// XposedHelpers.findAndHookMethod(
// "com.android.internal.telephony.PhoneSubInfo",
// lpparam.classLoader,
// "getDeviceId",
// new XC_MethodHook() {
// @Override
// protected void beforeHookedMethod(MethodHookParam param) {
// XposedBridge.log("调用PhoneSubInfo的getDeviceId()获取了imei");
// }
// }
// );
XposedHelpers.findAndHookMethod(
android.telephony.TelephonyManager.class.getName(),
lpparam.classLoader,
"getSubscriberId",
int.class,
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
XposedBridge.log("调用getSubscriberId获取了imsi");
}
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
XposedBridge.log(getMethodStack());
super.afterHookedMethod(param);
}
}
);
XposedHelpers.findAndHookMethod(
android.net.wifi.WifiInfo.class.getName(),
lpparam.classLoader,
"getMacAddress",
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
XposedBridge.log("调用getMacAddress()获取了mac地址");
}
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
XposedBridge.log(getMethodStack());
super.afterHookedMethod(param);
}
}
);
// wifiName
XposedHelpers.findAndHookMethod(
android.net.wifi.WifiInfo.class.getName(),
lpparam.classLoader,
"getWifiName",
new XC_MethodHook() {
protected void beforeHookedMethod(MethodHookParam param) {
XposedBridge.log("调用getWifiName()获取了wifi地址");
}
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
XposedBridge.log(getMethodStack());
super.afterHookedMethod(param);
}
}
);
//SSID
XposedHelpers.findAndHookMethod(
android.net.wifi.WifiInfo.class.getName(),
lpparam.classLoader,
"getSSID",
new XC_MethodHook() {
protected void beforeHookedMethod(MethodHookParam param) {
XposedBridge.log("调用getSSID()获取了wifi地址");
}
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
XposedBridge.log(getMethodStack());
super.afterHookedMethod(param);
}
}
);
//BSSID
XposedHelpers.findAndHookMethod(
android.net.wifi.WifiInfo.class.getName(),
lpparam.classLoader,
"getBSSID",
new XC_MethodHook() {
protected void beforeHookedMethod(MethodHookParam param) {
XposedBridge.log("调用getBSSID()获取了wifi地址");
}
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
XposedBridge.log(getMethodStack());
super.afterHookedMethod(param);
}
}
);
XposedHelpers.findAndHookMethod(
java.net.NetworkInterface.class.getName(),
lpparam.classLoader,
"getHardwareAddress",
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
XposedBridge.log("调用getHardwareAddress()获取了mac地址");
}
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
XposedBridge.log(getMethodStack());
super.afterHookedMethod(param);
}
}
);
XposedHelpers.findAndHookMethod(
android.provider.Settings.Secure.class.getName(),
lpparam.classLoader,
"getString",
ContentResolver.class,
String.class,
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
XposedBridge.log("调用Settings.Secure.getstring获取了" + param.args[1]);
}
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
XposedBridge.log(getMethodStack());
super.afterHookedMethod(param);
}
}
);
XposedHelpers.findAndHookMethod(
LocationManager.class.getName(),
lpparam.classLoader,
"getLastKnownLocation",
String.class,
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
XposedBridge.log("调用getLastKnownLocation获取了GPS地址");
}
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
XposedBridge.log(getMethodStack());
super.afterHookedMethod(param);
}
}
);
}
private String getMethodStack() {
StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
StringBuilder stringBuilder = new StringBuilder();
for (StackTraceElement temp : stackTraceElements) {
stringBuilder.append(temp.toString() + "\n");
}
return stringBuilder.toString();
// try
// {
// throw new NullPointerException();
// }
// catch (Exception ex)
// {
// return Log.getStackTraceString(ex);
// }
}
}
后续自己研究一下 , 计划写一个插件, 方便以后使用,
太极 是VirtualXposed的兄弟版本, 具体怎么用还没尝试, 后续把使用计划回来补全
相关资料参考
如何在android原生中加载RN页面
RN打包生产jsbundle文件
RN加载Bundle的方式
简要介绍下项目当前情况, 纯RN项目一枚
在尽量不改动原版功能的基础上,做好隐私协议的前置(在各种RN组件/三方组件加载前),不同意就多次确认后,退出APP, 基本上就是这种思路
两个
处理方案1.方案一:
RN工程中, 仍然使用一个bundle资源,
通过配置index.js / idnea.js 两个入口
在原生APP启动的时候(这里用安卓举例),
用户拒绝隐私->退出app
用户同意隐私-> 动态修改 getJSMainModuleName()
入口->重刷新MainActivity生命周期
@Override
protected String getJSMainModuleName() {
// 这里通过本地缓存数据指定加载不同入口文件
return "indea";
return "indeX";
}
在MaincActivity.java中添加重刷代码
public void destoryAndRecreate() {
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
runOnUiThread(new Runnable() {
@Override
public void run() {
recreate();
}
});
}
}, 1000);
}
2.方案二 :
方案1的变种 ,区别是, 在Assets中放入一个极简的bundle包, 这意味着,我们打包的apk,将携带两个bundle
区别就在于:
方案一
加载的是一个包括indea.js
/index.js
文件的, 名为index.android.bundle
的budle
方案二
分别加载的是 一个仅包含indea.js文件的名为indea.android.bundle
的bundle和 一个仅包含index.js文件的名为index.android.bundle
的bundle
同样的通过如下逻辑
用户拒绝隐私->退出app
用户同意隐私-> ->动态加载getBundleAssetName()
两个bundle->动态修改 getJSMainModuleName()
入口->重刷新MainActivity生命周期
重写的 getBundleAssetName()
代码举例
@Nullable
@org.jetbrains.annotations.Nullable
@Override
protected String getBundleAssetName() {
// return "------";
// 这里通过本地缓存数据指定加载不同bundle资源
return "indea.android.bundle";
return "index.android.bundle";
}
1.双bundle,代码隔离/逻辑隔离
首先加载极简隐私bundle, 无任何三方SDK引用/无RN相关特殊组件影响,仅RN最基础UI组件逻辑,
如果是方案一, 混在一起不讨本人喜欢
2.方案一已经实现了, 方案二没实现过, 一定要搞! 不会就要搞!
依旧拿Android举例
Android端的原生加载RN源码,网上挺多, 这里记录一下个人的理解,方便后期自己查阅和修改
这里稍作记录
1.MainApplication中
public class MainApplication extends Application implements ReactApplication {
2.ReactApplication中发现
public interface ReactApplication {
ReactNativeHost getReactNativeHost();
}
3.ReactNativeHost中发现
String jsBundleFile = this.getJSBundleFile();
if (jsBundleFile != null) {
builder.setJSBundleFile(jsBundleFile);
} else {
builder.setBundleAssetName((String)Assertions.assertNotNull(this.getBundleAssetName()));
}
ReactInstanceManager reactInstanceManager = builder.build();
ReactMarker.logMarker(ReactMarkerConstants.BUILD_REACT_INSTANCE_MANAGER_END);
return reactInstanceManager;
源码基本写的较为明白, 优先从 this.getJSBundleFile()
加载bundleFile,
但是仔细看
@Nullable
protected String getJSBundleFile() {
return null;
}
getJSBundleFile()
默认是返回null的
这里也插一个小实践 ,如果是直接
react-native run-android
运行RN命令, RN会默认打包出debug模式的apk文件,帮我们install到手机/模拟器上去注意: 在打包apk之前,
react-native run-android
先是会打包出bundle文件:index.android.bundle
,
这里打包出的index.android.bundle
是作为Assets资源在后面打包apk的过程中,存入了Assets资源目录
并不会帮我们存放到./android/app/src/main/assets/
文件路径下,
所以在./android/app/src/main/assets/
中找不到index.android.bundle
, 因为已经被打包到apk包的asset资源目录了
这个debug的apk, 就是自带了index.android.bundle的一个apk包也因此无论是直接
react-native run-android
还是gradlew直接打包apk, 我们如果不通过打包bundle的特殊命令,是无法直接获取bundle资源的, 想要打bundle包,得自己单独打包
继续说回getJSBundleFile()
源码这里很明确,RN就是这样设计的, jsBundleFile
默认是null,
if (jsBundleFile != null) {
builder.setJSBundleFile(jsBundleFile);
} else {
builder.setBundleAssetName((String)Assertions.assertNotNull(this.getBundleAssetName()));
}
1.优先从
getJSBundleFile()加载资源
2.如果没有,就从apk打包的Asset资源目录里面加载bundle(即 index.android.bundle这个默认的bundle)
这里就很明确了, 如果不做任何处理, 一定走入else逻辑分支, 从 getBundleAssetName()
中获取BundleAssetName
, 继续看
@Nullable
protected String getBundleAssetName() {
return "index.android.bundle";
}
getBundleAssetName()
还真的就是默认返回一个 "index.android.bundle"
资源路径程序会在Assets中加载该名称的bundle
结合上面提过的小实践, 也印证了直接 react-native run-android
, RN会默认打包出 index.android.bundle
存在Assets资源目录里面
我们再看看运行日志
这里要注意一点:
1.如果getBundleAssetName()
在开发/调试模式, 并不会真的走asset资源下的bundle, 而是会走 pakcager server, 就是从我们的终端服务器中获取, 毕竟是为了要热刷新
2.如果是在打包出apk以后,getBundleAssetName()
会去asset资源中加载真正的index.android.bundle
, 当然如果加载不到, 表现就是启动后闪退啦
那么, 如果:
1.手动打包一个名为:
indea.android.bundle
的bundle作为极简bundle, 存放于项目工程./android/app/scr/main/assets/
中
(亦或者./android/app/scr/main/assets/bundleA/
, 自己添加一个bundleA的文件目录也是可以的)
- 重写 getJSMainModuleName(), 修改主入口为 :
indea
@Override
protected String getJSMainModuleName() {
return "indea";
}
3.重写 getBundleAssetName() ,指定加载 asset资源下的
indea.android.bundle
@Nullable
@org.jetbrains.annotations.Nullable
@Override
protected String getBundleAssetName() {
return "indea.android.bundle"; //
or
// 如果是放在 :asset/bundleA/indea.android.bundle 这里, 那么返回也要这样写
return "bundleA/indea.android.bundle";
}
这三步处理好, 这不就是
方案二
么
1.getJSMainModuleName()
的用法用于在RN工程中, 寻找MainModule的文件入口,
讲白了就是在Android工程中,安卓原生启动后,会从这个接口指定RN层代码的入口
举个例子
RN端 我有一个index.js
入口 代码如下
import {AppRegistry} from 'react-native';
import App from './App';
import {name as appName} from './app.json';
AppRegistry.registerComponent(appName, () => App);
我还有个indea.js
入口 代码如下
import {AppRegistry} from 'react-native';
import {name as appName} from './app.json';
import TestApp from "./test/TestApp";
AppRegistry.registerComponent(appName, () => TestApp);
在Android端
如果不重写 ReactNativeHost
下的 getJSMainModuleName()
我们看到源码里面是这样的
protected String getJSMainModuleName() {
return "index.android";
}
安卓默认会去找一个叫做 index.android
的MainModule主入口js文件,
这里 可以重写 getJSMainModuleName()
改为寻找 我们上面两个例子的入口文件名
例子1
@Override
protected String getJSMainModuleName() {
return "index";
}
例子2
@Override
protected String getJSMainModuleName() {
return "indea";
}
分别运行安卓, 进入的效果也是分别进入了 App.js
和 TestApp.js
getJSMainModuleName()
就是一个用来指定 RN工程中, 主入口js文件的函数, 是getJSMainModuleName
选择了RN的主入口js文件, 当然如果不匹配就报错了
2.getBundleAssetName()
的用法记得只要把打包的bundle文件 放在
./android/app/scr/main/assets/
文件夹下, 在打包apk的过程中会存放在apk包内,通过getBundleAssetName()
加载使用,
(当然,采用方案二
, 我们会有俩bundle ,indea.android.bundle
和默认的index.android.bundle
,apk包也会变大咯, 2022年了,大公司的apk,当然是越大越好啦 :XD )
记录下indea.android.bundle
打包/使用
1.bundle包-直接打包到./android/app/src/maic/assets/
react-native bundle --entry-file indea.js --bundle-output ./android/app/src/main/assets/indea.android.bundle --platform android --assets-dest ./android/app/src/main/assets/ --dev false --verbose
MainApplication
中代码 @Override
protected String getJSMainModuleName() {
return "indea";
}
@Nullable
@org.jetbrains.annotations.Nullable
@Override
protected String getBundleAssetName() {
return "indea.android.bundle"; // 取android工程下
}
1.bundle包-直接打包到./android/app/src/maic/assets/bundleA
react-native bundle --entry-file indea.js --bundle-output ./android/app/src/main/assets/bundleA/indea.android.bundle --platform android --assets-dest ./android/app/src/main/assets/bundleA/ --dev false --verbose
2.修改 MainApplication中代码
@Override
protected String getJSMainModuleName() {
return "indea";
}
@Nullable
@org.jetbrains.annotations.Nullable
@Override
protected String getBundleAssetName() {
// 如果是放在 :asset/bundleA/indea.android.bundle 这里, 那么返回也要这样写
return "bundleA/indea.android.bundle";
}
indea.android.bundle
bundle资源打包在工程文件中
加载 - 开发模式
if(self.privateAgree_flag){
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
}else{
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"indea" fallbackResource:nil];
}
加载 - 打包模式
if(self.privateAgree_flag){
return [[NSBundle mainBundle] URLForResource:@"./bundle/index.ios" withExtension:@"jsbundle"];
}else{
return [[NSBundle mainBundle] URLForResource:@"./bundleA/indea.ios" withExtension:@"jsbundle"];
}
iOS端比较简单, 通过自定义一个全局标识
privateAgree_flag
来确定APP启动时加载不同的bundle,
在开发模式下, iOS 通过jsBundleURLForBundleRoot
指定rn的入口
在发布模式下,iOS通过URLForResource
加载不同的jsbundle包
react-native 调用Settings.Secure.getstring获取了android_id / app上架违规获取android_id被拒
又在腾讯应用宝的审核中, 发现过渡搜集问题…
目前已经做过一次申请,缓存读取的方案, 但是依然检测多次触发获取,
现在打算尝试上面链接的方案…
是否有效后续补上…
上面说的改为内联方式 ---- 目前确定无效
解决方案:
通过下载堆栈调用发现,四次获取android_Id都是调用了
getUniqueId()
查看文档发现. 是一个Promise, 推测是否异步问题导致, 故而改为 同步调用获取
目前该项已经通过扫描