因为需要将各业务线通过划分jsbundle的形式进行分离,以达到
参考了携程以及各种网络版本的做法,大致总结为三种
综上所述,js端的bundle拆分用第三种方案最优
因为Metro官方文档过于简陋,实在看不懂,所以借鉴了一些使用Metro的项目
比如(感谢开原作者的贡献):https://github.com/smallnew/react-native-multibundler
这个项目较为完整,简要配置下就可以直接使用,所以js端拆包主要参考自这个项目,通过配置Metro的createModuleIdFactory,processModuleFilter回调,我们可以很容易的自定义生成moduleId,以及筛选基础包内容,来达到基础业务包分离的目的,因为实际上拆分jsbundle主要工作也就在于moduleId分配以及打包filter配置,我们可以观察下打包后的js代码结构
通过react-native bundle --platform android --dev false --entry-file index.common.js --bundle-output ./CodePush/common.android.bundle.js --assets-dest ./CodePush --config common.bundle.js --minify false
指令打出基础包(minify设为false便于查看打包后源码)
1 | function (global) { |
这里主要看__r
,__d
两个变量,赋值了两个方法metroRequire
,define
,具体逻辑也很简单,define
相当于在表中注册,require
相当于在表中查找,js代码中的import
,export
编译后就就转换成了__d
与__r
,再观察一下原生Metro代码的node_modules/metro/src/lib/createModuleIdFactory.js
文件,代码为:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16function createModuleIdFactory() {
const fileToIdMap = new Map();
let nextId = 0;
return path => {
let id = fileToIdMap.get(path);
if (typeof id !== "number") {
id = nextId++;
fileToIdMap.set(path, id);
}
return id;
};
}
module.exports = createModuleIdFactory;
逻辑比较简单,如果查到map里没有记录这个模块则id自增,然后将该模块记录到map中,所以从这里可以看出,官方代码生成moduleId的规则就是自增,所以这里要替换成我们自己的配置逻辑,我们要做拆包就需要保证这个id不能重复,但是这个id只是在打包时生成,如果我们单独打业务包,基础包,这个id连续性就会丢失,所以对于id的处理,我们还是可以参考上述开源项目,每个包有十万位间隔空间的划分,基础包从0开始自增,业务A从1000000开始自增,又或者通过每个模块自己的路径或者uuid等去分配,来避免碰撞,但是字符串会增大包的体积,这里不推荐这种做法。所以总结起来js端拆包还是比较容易的,这里就不再赘述
用过CodePush的同学都能感受到它强大的功能以及稳定的表现,更新,回滚,强更,环境管控,版本管控等等功能,越用越香,但是它不支持拆包更新,如果自己重新实现一套功能类似的代价较大,所以我尝试通过改造来让它支持多包独立更新,来满足我们拆包的业务需求,改造原则:
通过阅读源码,我们可以发现,只要隔离了包下载的路径以及每个包自己的状态信息文件,然后对多包并发更新时,做一些同步处理,就可以做到多包独立更新
改造后的包存放路径如上图所示
app.json文件存放包的信息,由检测更新的接口返回以及本地逻辑写入的一些信息,比如hash值,下载url,更新包的版本号,bundle的相对路径(本地代码写入)等等
codepush.json会记录当前包的hash值以及上一个包的hash值,用于回滚,所以正常来讲一个包会有两个版本,上一版本用于备份回滚,回滚成功后会删除掉当前版本,具体逻辑可以自行阅读了解,所以我这里总结一下改动
主要改动为增加pathPrefix和bundleFileName两个传参,用于分离bundle下载的路径
增加了bundleFileName和pathPrefix参数的方法有
只增加了pathPrefix参数的方法有
因为官方代码只对单个包状态做管理,所以这里我们要改为支持对多个包状态做管理
因为拆包后,对包的加载是增量的,所以我们在初始化业务场景A的ReactRootView时,增量加载业务A的jsbundle,其他业务场景同理,获取业务A jsbundle路径需要借助改造后的CodePush方法,通过传入bundleFileName,pathPrefix
官方代码为加载完bundle即重新创建整个RN环境,拆包后此种方法不可取,如果业务包更新完后,重新加载业务包然后再重建RN环境,会导致基础包代码丢失而报错,所以增加一个只加载jsbundle,不重建RN环境的方法,在更新业务包的时候使用
比如官方更新代码为:
CodePushNativeModule#loadBundle方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41private void loadBundle(String pathPrefix, String bundleFileName) {
try {
// #1) Get the ReactInstanceManager instance, which is what includes the
// logic to reload the current React context.
final ReactInstanceManager instanceManager = resolveInstanceManager();
if (instanceManager == null) {
return;
}
String latestJSBundleFile = mCodePush.getJSBundleFileInternal(bundleFileName, pathPrefix);
// #2) Update the locally stored JS bundle file path
setJSBundle(instanceManager, latestJSBundleFile);
// #3) Get the context creation method and fire it on the UI thread (which RN enforces)
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
try {
// We don't need to resetReactRootViews anymore
// due the issue https://github.com/facebook/react-native/issues/14533
// has been fixed in RN 0.46.0
//resetReactRootViews(instanceManager);
instanceManager.recreateReactContextInBackground();
mCodePush.initializeUpdateAfterRestart(pathPrefix);
} catch (Exception e) {
// The recreation method threw an unknown exception
// so just simply fallback to restarting the Activity (if it exists)
loadBundleLegacy();
}
}
});
} catch (Exception e) {
// Our reflection logic failed somewhere
// so fall back to restarting the Activity (if it exists)
CodePushUtils.log("Failed to load the bundle, falling back to restarting the Activity (if it exists). " + e.getMessage());
loadBundleLegacy();
}
}
改造为业务包增量加载,基础包才重建ReactContext1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30if ("CommonBundle".equals(pathPrefix)) {
new Handler(Looper.getMainLooper()).post(new Runnable() {
public void run() {
try {
// We don't need to resetReactRootViews anymore
// due the issue https://github.com/facebook/react-native/issues/14533
// has been fixed in RN 0.46.0
//resetReactRootViews(instanceManager);
instanceManager.recreateReactContextInBackground();
mCodePush.initializeUpdateAfterRestart(pathPrefix);
} catch (Exception e) {
// The recreation method threw an unknown exception
// so just simply fallback to restarting the Activity (if it exists)
loadBundleLegacy();
}
}
});
} else {
JSBundleLoader latestJSBundleLoader;
if (latestJSBundleFile.toLowerCase().startsWith("assets://")) {
latestJSBundleLoader = JSBundleLoader.createAssetLoader(getReactApplicationContext(), latestJSBundleFile, false);
} else {
latestJSBundleLoader = JSBundleLoader.createFileLoader(latestJSBundleFile);
}
CatalystInstance catalystInstance = resolveInstanceManager().getCurrentReactContext().getCatalystInstance();
latestJSBundleLoader.loadScript(catalystInstance);
mCodePush.initializeUpdateAfterRestart(pathPrefix);
}
启动业务ReactRootView时增量加载jsbundle的逻辑同上
CodePush#sync代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41const sync = (() => {
let syncInProgress = false;
const setSyncCompleted = () => { syncInProgress = false; };
return (options = {}, syncStatusChangeCallback, downloadProgressCallback, handleBinaryVersionMismatchCallback) => {
let syncStatusCallbackWithTryCatch, downloadProgressCallbackWithTryCatch;
if (typeof syncStatusChangeCallback === "function") {
syncStatusCallbackWithTryCatch = (...args) => {
try {
syncStatusChangeCallback(...args);
} catch (error) {
log(`An error has occurred : ${error.stack}`);
}
}
}
if (typeof downloadProgressCallback === "function") {
downloadProgressCallbackWithTryCatch = (...args) => {
try {
downloadProgressCallback(...args);
} catch (error) {
log(`An error has occurred: ${error.stack}`);
}
}
}
if (syncInProgress) {
typeof syncStatusCallbackWithTryCatch === "function"
? syncStatusCallbackWithTryCatch(CodePush.SyncStatus.SYNC_IN_PROGRESS)
: log("Sync already in progress.");
return Promise.resolve(CodePush.SyncStatus.SYNC_IN_PROGRESS);
}
syncInProgress = true;
const syncPromise = syncInternal(options, syncStatusCallbackWithTryCatch, downloadProgressCallbackWithTryCatch, handleBinaryVersionMismatchCallback);
syncPromise
.then(setSyncCompleted)
.catch(setSyncCompleted);
return syncPromise;
};
})();
改造后1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60const sync = (() => {
let syncInProgress = false;
//增加一个管理并发任务的队列
let syncQueue = [];
const setSyncCompleted = () => {
syncInProgress = false;
回调完成后执行队列里的任务
if (syncQueue.length > 0) {
log(`Execute queue task, current queue: ${syncQueue.length}`);
let task = syncQueue.shift(1);
sync(task.options, task.syncStatusChangeCallback, task.downloadProgressCallback, task.handleBinaryVersionMismatchCallback)
}
};
return (options = {}, syncStatusChangeCallback, downloadProgressCallback, handleBinaryVersionMismatchCallback) => {
let syncStatusCallbackWithTryCatch, downloadProgressCallbackWithTryCatch;
if (typeof syncStatusChangeCallback === "function") {
syncStatusCallbackWithTryCatch = (...args) => {
try {
syncStatusChangeCallback(...args);
} catch (error) {
log(`An error has occurred : ${error.stack}`);
}
}
}
if (typeof downloadProgressCallback === "function") {
downloadProgressCallbackWithTryCatch = (...args) => {
try {
downloadProgressCallback(...args);
} catch (error) {
log(`An error has occurred: ${error.stack}`);
}
}
}
if (syncInProgress) {
typeof syncStatusCallbackWithTryCatch === "function"
? syncStatusCallbackWithTryCatch(CodePush.SyncStatus.SYNC_IN_PROGRESS)
: log("Sync already in progress.");
//检测到并发任务,放入队列排队
syncQueue.push({
options,
syncStatusChangeCallback,
downloadProgressCallback,
handleBinaryVersionMismatchCallback
});
log(`Enqueue task, current queue: ${syncQueue.length}`);
return Promise.resolve(CodePush.SyncStatus.SYNC_IN_PROGRESS);
}
syncInProgress = true;
const syncPromise = syncInternal(options, syncStatusCallbackWithTryCatch, downloadProgressCallbackWithTryCatch, handleBinaryVersionMismatchCallback);
syncPromise
.then(setSyncCompleted)
.catch(setSyncCompleted);
return syncPromise;
};
})();
该方案主流程已经ok,多包并发更新,单包独立更新基本没有问题,现在还在边界场景以及压力测试当中,待方案健壮后再上源码做详细分析
该方案同样满足自建server的需求,关于自建server可以参考:https://github.com/lisong/code-push-server
再次感谢开源作者的贡献
]]>本项目基于Wechaty,这是一个封装了微信基本事件的开源库,比如onLogin(登陆事件)
,onMessage(消息事件)
,onLogout(登出事件)
等等,详细使用可参考该项目文档,
有了这些事件,我们可以基于Node.js很轻松的开发一些常用功能。
本项目Node版本需要 >= 10,可以自行安装nvm进行版本切换。
本项目工程代码wechat-boy
首先新建工程,创建一个package.json,引入要用的库1
2
3
4
5
6
7
8
9
10
11
12"devDependencies": {
"babel-cli": "^6.26.0",
"babel-preset-es2015": "^6.24.1", // es6转es5,不想写es6可以不用这两个
"node-schedule": "^1.3.2", // 定时任务
"qrcode-terminal": "^0.12.0", // 打印登陆二维码
"rimraf": "^2.6.3", // 终端指令工具
"wechaty": "^0.22.6", // Wechaty基础库
"moment": "2.24.0" // 日期处理
},
"dependencies": {
"axios": "^0.18.0" // 网络请求
}
然后执行npm install 或者 yarn,然后可以在index.js里简单写一个demo,1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16import QrTerm from 'qrcode-terminal';
Wechaty.instance({ name: 'wechat-boy' })
.on('scan', qrcode => {
console.log(`onScan: ${qrcode}`);
QrTerm.generate(qrcode);
})
.on('login', user => {
console.log(`onLogin: ${user.name()}`);
})
.on('message', msg => {
console.log(`from ${msg.from().name()} message: ${msg.text()}`)
})
.on('logout', usr => {
console.log(`user ${user.name()} logout`)
})
然后执行npm start,第一次会下载一些依赖,而且依赖很大,要等一段时间,如果实在下载不下来可能要全局FQ了,
可参考我另一篇使用proxifier全局代理
启动成功后会打印出登陆二维码
扫码登陆,就可以侦听消息事件了
上一步里的回调侦听事件测试ok了后,我们就可以做点实际功能了,实现自动聊天功能只需在message
回调里操作就行。
编辑message回调方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53on('message', async (msg) => {
if (msg.self()) return;
const room = msg.room();
const content = msg.text();
const contact = msg.from();
let reply;
if (room) {
// 代表群消息
} else {
// 个人消息
reply = await Service.reply(content);
console.log(`tuling reply: ${reply}`)
await contact.say(reply)
}
})
// Service.reply
static async reply(content) {
let response;
try {
const data = await this.get('http://www.tuling123.com/openapi/api', {
params: {
// key需要去http://www.tuling123.com申请
key: TULING_API_KEY,
info: content,
}
});
if (data.code === 100000) {
response = data.text;
} else {
throw new Error(TULING_ERROR_MESSAGE);
}
} catch (e) {
response = e.message;
}
return response;
}
// axios get
static async get(url, params) {
let response;
try {
response = await axios.get(url, params);
console.log('------------success-----------');
console.log(`${response.status}\n${response.statusText}\n${JSON.stringify(response.data, null, 2)}\n`)
} catch (e) {
console.log('------------error-------------');
console.error(e);
throw e
}
return response.data;
}
加入这些逻辑,此时你的账号已经可以自动聊骚聊天啦
每日自动播报功能就要用到schedule任务了,这里我们使用node-schedule库,在login的回调中执行定时功能1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58import Schedule from 'node-schedule'
const boy = Wechaty.instance({ name: 'wechat-boy' });
boy
.on('login', (usr) => {
// 设置定时任务, 每天凌晨8点触发,
// 每分钟的第30秒: '30 * * * * *'
// 每小时的1分30秒 :'30 1 * * * *'
// 每天的1点1分30秒 :'30 1 1 * * *'
// 每月的1日1点1分30秒 :'30 1 1 1 * *'
// 每周1的1点1分30秒 :'30 1 1 * * 1'
// 详情见node_schedule文档
Schedule.scheduleJob('0 0 8 * * *', async () => {
// 寻找备注名称为${alias}的联系人
const contact = await boy.Contact.find({ alias: `${alias}` })
await contact.say(await Service.getNews());
await contact.say(await Service.getWeather());
})
})
// Service.getNews
static async getNews() {
let msg;
let response;
try {
response = await this.get('http://v.juhe.cn/toutiao/index', {
params: {
// 需要去https://www.juhe.cn/申请
key: NEWS_KEY,
},
});
msg = Util.handleNewsData(response);
} catch (e) {
console.error(e);
msg = '获取新闻失败';
}
return msg;
}
// Service.getWeather
static async getWeather() {
let msg;
let response;
try {
response = await this.get(TIANQI_URL, {
params: {
cityname: TIANQI_CITY,
// 需要去https://www.juhe.cn/申请
key: TIANQI_KEY
},
});
msg = Util.handleWeatherData(response);
} catch (e) {
console.error(e);
msg = '获取天气失败';
}
return msg;
}
一个简单的,拥有每日播报,自动聊天的微信机器人就实现了。
完整代码wechat-boy
]]>市面上实现这种方案最早的应用应该是”黑阈”,我们在使用的时候需要开启调试模式,然后通过adb或者注入器注入主服务,才可以使用后台管制以及其他高级权限的功能。所以本方案也是基于这种注入服务的方式,来实现各种需要高级权限的功能
这种方案的关键点是这个拥有shell级权限的服务,Android提供了app_process指令供我们启动一个进程,我们可以通过该指令起一个Java服务,如果是通过shell执行的,该服务会从/system/bin/sh
fork出来,并且拥有shell级权限
这里我写了一个service.dex服务来测试一下,并通过shell启动它1
2
3
4
5// 先将service.dex push至Android设备
adb push service.dex /data/local/tmp/
// 然后通过app_process启动,并指定一个名词
adb shell nohup app_process -Djava.class.path=/data/local/tmp/server.dex /system/bin --nice-name=club.syachiku.hackrootservice shellService.Main
然后再看看该服务的信息1
2
3
4
5// 列出所有正在运行的服务
adb shell ps
// 找到服务名为club.syachiku.hackrootservice的服务
shell 24154 1 777484 26960 ffffffff b6e7284c S club.syachiku.hackrootservice
可以看到该服务pid为24154,ppid为1,也说明该服务是从/system/bin/sh
fork出来的1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41// 查看该服务具体信息
adb shell cat 24154/status
Name:main
State:S (sleeping)
Tgid:24154
Pid:24154
PPid:1
TracerPid:0
Uid:2000200020002000
Gid:2000200020002000
FDSize:32
Groups:1004 1007 1011 1015 1028 3001 3002 3003 3006
VmPeak: 777484 kB
VmSize: 777484 kB
VmLck: 0 kB
VmPin: 0 kB
VmHWM: 26960 kB
VmRSS: 26960 kB
VmData: 11680 kB
VmStk: 8192 kB
VmExe: 12 kB
VmLib: 52812 kB
VmPTE: 134 kB
VmSwap: 0 kB
Threads:13
SigQ:0/6947
SigPnd:0000000000000000
ShdPnd:0000000000000000
SigBlk:0000000000001204
SigIgn:0000000000000001
SigCgt:00000002000094f8
CapInh:0000000000000000
CapPrm:0000000000000000
CapEff:0000000000000000
CapBnd:00000000000000c0
Seccomp:0
Cpus_allowed:f
Cpus_allowed_list:0-3
voluntary_ctxt_switches:18
nonvoluntary_ctxt_switches:76
可以看到Uid,Gid为2000,就是shell的Uid
分析了app_process的可行性,我们可以给出一个方案,通过app_process启动一个socket服务,然后让我们的App与该服务通信,来代理App做一些见不得人需要shell级权限的事情,比如静默卸载,安装,全局广播等等
这里我们新建一个名为hack-root的工程
然后在代码目录下新建一个shellService包,新建一个Main入口类,我们先输出一些测试代码,来测试是否执行成功
1 | public class Main { |
然后使用app_process指令执行该服务
1 | adb shell app_process -Djava.class.path=/sdcard/classes.dex /system/bin shellService.Main |
如果控制台输出Abort
应该是一些基本的路径问题,稍作仔细检查一下,成功执行后会看到我们的打印的日志
运行测试没问题了就开写socket服务吧1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28public class Main {
public static void main(String[] args) {
// 利用looper让线程循环
Looper.prepareMainLooper();
System.out.println("*****************hack server starting****************");
// 开一个子线程启动服务
new Thread(new Runnable() {
public void run() {
new SocketService(new SocketService.SocketListener() {
public String onMessage(String msg) {
// 接收客户端传过来的消息
return resolveMsg(msg);
}
});
}
}).start();
Looper.loop();
}
private static String resolveMsg(String msg) {
// 执行客户端传过来的消息并返回执行结果
ShellUtil.ExecResult execResult =
ShellUtil.execute("pm uninstall " + msg);
return execResult.getMessage();
}
}
SocketServer1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62public class SocketService {
private final int PORT = 10500;
private SocketListener listener;
public SocketService(SocketListener listener) {
this.listener = listener;
try {
// 利用ServerSocket类启动服务,然后指定一个端口
ServerSocket serverSocket = new ServerSocket(PORT);
System.out.println("server running " + PORT + " port");
ArrayBlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(10);
// 新建一个线程池用来并发处理客户端的消息
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5,
10,
5000,
TimeUnit.MILLISECONDS,
queue
);
while (true) {
Socket socket = serverSocket.accept();
// 接收到新消息
executor.execute(new processMsg(socket));
}
} catch (Exception e) {
System.out.println("SocketServer create Exception:" + e);
}
}
class processMsg implements Runnable {
Socket socket;
public processMsg(Socket s) {
socket = s;
}
public void run() {
try {
// 通过流读取内容
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String line = bufferedReader.readLine();
System.out.println("server receive: " + line);
PrintWriter printWriter = new PrintWriter(socket.getOutputStream());
String repeat = listener.onMessage(line);
System.out.println("server send: " + repeat);
// 服务端返回给客户端的消息
printWriter.print(repeat);
printWriter.flush();
printWriter.close();
bufferedReader.close();
socket.close();
} catch (IOException e) {
System.out.println("socket connection error:" + e.toString());
}
}
}
public interface SocketListener{
// 通话消息回调
String onMessage(String text);
}
}
ShellUtil
1 | public class ShellUtil { |
一个简易的socket服务就搭建好了,可以用来接收客户端传过来的指令并且执行然后返回结果
首先编写一个socketClient
1 | public class SocketClient { |
然后UI组件相关的事件,我们暂时只实现一个静默卸载App的功能1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46public class MainActivity extends AppCompatActivity {
private TextView textView;
private ScrollView scrollView;
private EditText uninsTxtInput;
private Button btnUnins;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btnUnins = findViewById(R.id.btn_uninstall);
uninsTxtInput = findViewById(R.id.pkg_input);
textView = findViewById(R.id.tv_output);
scrollView = findViewById(R.id.text_container);
btnUnins.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
sendMessage(uninsTxtInput.getText().toString());
}
});
}
private void sendMessage(String msg) {
new SocketClient(msg, new SocketClient.SocketListener() {
public void onMessage(String msg) {
showOnTextView(msg);
}
});
}
private void showOnTextView(final String msg) {
runOnUiThread(new Runnable() {
public void run() {
String baseText = textView.getText().toString();
if (baseText != null) {
textView.setText(baseText + "\n" + msg);
} else {
textView.setText(msg);
}
scrollView.smoothScrollTo(0, scrollView.getHeight());
}
});
}
}
布局代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<EditText
android:id="@+id/pkg_input"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:hint="input package name"
app:layout_constraintEnd_toStartOf="@+id/btn_uninstall"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/btn_uninstall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginTop="8dp"
android:text="uninstall"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ScrollView
android:id="@+id/text_container"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
android:padding="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/pkg_input">
<TextView
android:id="@+id/tv_output"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</ScrollView>
</android.support.constraint.ConstraintLayout>
代码相关的工作基本完工,一个简单的,实现了静默卸载Demo就完成了
/data/local/tmp/
执行服务
前台执行:
1 | // 拔掉数据线会终止服务 |
后台执行:
1 | // 会一直运行除非手动kill pid或者重启设备 |
安装apk,输入要卸载的包名,点击UNINSTALL进行静默卸载
https://github.com/zjkhiyori/hack-root 欢迎fork || star
感谢下列开源作者
]]>已经2018年了,广大人民群众基本都用上es6了,所以这里也配一下es6的环境
新建工程,创建一个package.json1
2
3
4
5
6{
"name": "projectName",
"version": "1.0.0",
"devDependencies": {},
"dependencies": {}
}
node版本建议不要太低,低版本对es6的兼容性不好,我这里使用的是8.9.0
然后安装babel1
2
3
4yarn add babel-cli
// 老版本babel-preset-2015已经废弃
yarn add babel-preset-env
然后再写几句es6测试一下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// 新建一个诗人类
class Poet {
poem(...poemText) {
console.log(poemText);
}
}
epxort default Poet;
//新建一个Test.js类
import Poet from './Poet';
let poet = new Poet();
poet.poem('苟', '岂');
// 然后用babel node指令跑一下Test.js
babel-node Test.js
// [ '苟', '岂' ]
// 成功念出我想念的诗
es6环境就ok了
我这里用的是一个随便找的库excel-export,这个库的issue已经有人在喊”This project is DEAD!!!”
无视他继续安装1
yarn add excel-
使用方法我这里也只是简单用一下基本功能,高级用法还是直接看该库的文档
1 | const conf = {}; |
Demo的代码地址ExcelGenerator
]]>最近又有一台服务器被GFW给干掉了,又得掏腰包买服务器跟搭建环境了,这次记录一下方便以后翻出来看吧
项目地址: shadowsocks(可以看到打开是个空项目,分支切换到master就可以看到原始项目了,据说是作者被请喝茶才出此下策?这些人这么好忽悠的吗?)
因为本人对科学上网有一定需求,公司邮箱也用Gmail,手机也用google全家桶,所以一般都会配备至少两台服务器,如果跟我一样有需求的,建议也至少配备两台
关于服务器购买就不多说了,我用的是这个Aplpharacks,一台VPS 512m或者256m内存的完全够用,我有一台早期买的128m的,大部分时间速度比其他几台要快,可能是哥伦比亚机房的原因?不过现在已经没得卖了,现在主要卖的都是洛杉矶机房,稳定性一般,网络好峰值可达4m/s,网络差的时候基本瘫痪
搭建过程很简单,首先下载基础包跟python包管理器
我一般都是用Ubuntu,这里以Ubuntu为例1
2
3
4
5
6
7
8
9apt-get install python-pip
// 如果有 E: Unable to locate package python-pip 错误
// 执行一下 sudo apt-get update 再install
// 安装git
sudo apt-get install git
// 安装ss基础包
pip install git+https://github.com/shadowsocks/shadowsocks.git@master
安装好了键入1
ssserver -h
有相应指令帮助提示就表面基础包已经ok
然后就是编辑配置信息1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28// 新建一个json配置文件
touch ~/shadowsocks.json
// 然后编辑
vim ~/shadowsocks.json
{
// 你的服务器ip
"server": "*.*.*.*",
// 配置多个端口及密码,
"port_password": {
"8080": "hello",
"8081": "hello",
"8082": "hello",
"8083": "hello",
"8084": "hello",
"8085": "hello",
"443": "hello"
},
// 单个端口配置
"server_port":8388,
"password": "hello",
"local_address": "127.0.0.1",
"local_port": 1080,
"timeout": 500,
"method": "rc4-md5"
}
这里强烈建议多开几个端口备用,因为使用中发现GFW会对端口进行干扰,如果只配置了一个端口,被干扰后整台服务器无法使用,所以多开几个端口备用,一个端口失效了换其他端口试试
弄完配置文件就可以部署服务了
执行指令1
2
3
4前台启动方式
ssserver -c ~/shadowsocks.json
后台启动方式
ssserver -c ~/shadowsocks.json -d start
执行完后可以看看端口使用情况1
netstat -tunpl
可以看到刚才配置的端口使用情况,能看到刚才配置的端口就表明部署ok了,剩下就是客户端的事情了
客户端建议下载ShadowsocksX-NG
客户端配置好应该就能上网了,注意加密方式与服务端保持一致
因为chrome支持http转socks5协议,而终端不支持,所以终端无法走socks5协议,此时我们就需要一个全局代理,我用的是Proxifier,价格40刀,软件卖的比较贵,有条件的同学还请支持正版,网上也有很多破解资源
打开代理配置,增加一条配置
这里填入你shadowsocksX-NG的本地监听地址以及端口
然后打开rules,default设置为下图
然后回Proxifier首页将应用设置为gobal(默认为NONE)
此时connection栏目就能监控到你的网络请求,控制台也能科学上网了
然后我就可以登录我可怜的,才用了几个月的,被封禁的服务器了TAT。。。
]]>首先算法理解了主要思想,那么代码实现也是信手拈来
首先我们用lodash创建一个随机数组1
2_.shuffle([1,2,3,4,5,6,7,8,9,10])
// [2, 3, 4, 1, 7, 5, 8, 10, 9, 6 ]
现在将它进行排序
首先选取基准数,我们就直接取最后一位作为基准数,左起设一个index值记录替换次数,每替换一次index自增1
然后从左至右开始遍历1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32[2, 3, 4, 1, 7, 5, 8, 10, 9, 6 ]
// 6 > 2替换下标为0,0的两个数,index自增1,
[2, 3, 4, 1, 7, 5, 8, 10, 9, 6 ]
// 6 > 3替换下标为1,1的两个数,index自增1
[2, 3, 4, 1, 7, 5, 8, 10, 9, 6 ]
// 6 > 4替换下标为2,2的两个数,index自增1
[2, 3, 4, 1, 7, 5, 8, 10, 9, 6 ]
// 6 > 1替换下标为3,3的两个数,index自增1
[2, 3, 4, 1, 7, 5, 8, 10, 9, 6 ]
// 6 < 7不替换,index为4不变
[2, 3, 4, 1, 7, 5, 8, 10, 9, 6 ]
// 6 > 5替换下标为4, 5的两个数,index自增1
[2, 3, 4, 1, 5, 7, 8, 10, 9, 6 ]
// 6 < 8不替换,index为5不变
[2, 3, 4, 1, 5, 7, 8, 10, 9, 6 ]
// 6 < 10不替换,index为5不变
[2, 3, 4, 1, 5, 7, 8, 10, 9, 6 ]
// 6 < 9不替换,index为5不变
[2, 3, 4, 1, 5, 6, 8, 10, 9, 7 ]
// 遍历结束替换pivot与index下标的值,这样就分割出了两个数组
[2, 3, 4, 1, 5]
[8, 10, 9, 7]
// 然后再分别将这两个数组进行重复上述操作
有了上述思路就可以开始撸代码了1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32// 定义交换逻辑
function swap(arr, i, j) {
const temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
// 定义分割逻辑
function partition(arr, left, right) {
const pivot = arr[right];
let index = left;
for (let i = left; i < right; i++) {
if (pivot > arr[i]) {
swap(arr, i, index);
index++;
}
}
swap(arr, right, index);
return index;
}
function sort(arr, left, right) {
if (left > right) return;
const index = partition(arr, left, right);
sort(arr, left, index - 1);
sort(arr, index + 1, right);
}
function quick_sort(arr) {
sort(arr, 0, arr.length - 1);
return arr
}
代码可能较其他方法冗余,个人认为这是思路最清晰的写法
然后加一些测试代码1
2
3
4
5
6
7
8
9
10
11
12const _ = require('lodash');
const arr = _.shuffle([1,2,3,4,5,6,7,8,9,10]);
console.log(arr);
console.log(quick_sort(arr));
// [ 8, 3, 9, 4, 7, 1, 6, 2, 5, 10 ]
// [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]
// [ 2, 8, 4, 3, 9, 5, 1, 10, 6, 7 ]
// [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]
// [ 10, 7, 8, 4, 6, 1, 2, 9, 3, 5 ]
// [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]
测试ok
]]>Android提取apk有两种途径,一种是通过adb1
2
3
4
5
6
7
8
9
10// 列出所有安装包
adb shell pm list package
// 找到你需要提取的包名,然后获取路径
adb shell pm path packageName
// 输出路径后拷贝或者pull到你的电脑
adb pull apkPath ~/Download
// 某些设备可能需要root权限才能访问这个path路径
另一种方法就是通过编写App进行提取/备份apk
这种方法主要通过packageManager
来获取系统的所有应用信息packageInfo
,里面包含了应用路径,包名,应用名等信息,然后根据路径进行拷贝以及备份
个人通过闲暇时间写的一个apk提取工具,使用React Native构建,数据框架使用Mobx,欢迎下载体验
]]>最近使用0.54.0版本开发有个调试的bug非常恶心,debug js remotely总是抛1
2
3
4DeltaPatcher.js:58 Uncaught (in promise) Error: DeltaPatcher should receive a fresh Delta when being initialized
at DeltaPatcher.applyDelta (DeltaPatcher.js:58)
at deltaUrlToBlobUrl (deltaUrlToBlobUrl.js:34)
at <anonymous>
想再次debug就得杀掉进程重新打开,官方解释在0.55版本会修复此问题,看了下pr改动都是js代码,随即更新版本修复此问题。若想以后碰到类似框架性的问题,想要自己能有排错纠错能力,还是老老实实啃源码吧
首先看看Reload,先从Activity
下手,初始demo里MainActivity
继承了ReactActivity
,RN工程的初始化,加载jsbundle的触发都在这个ReactActivity
中,然后具体业务逻辑又交给了它的代理类ReactActivityDelegate
,里面做了初始化RN框架逻辑,框架初始化的流程先不管,主要看看reload流程
1 | public boolean onKeyUp(int keyCode, KeyEvent event) { |
ReactActivity
中侦听了物理按键,在keyCode为82即menu按键的时候,获取了RN主要的管理类ReactInstanceManager
,然后调起了调试框DevOptionsDialog
,具体业务逻辑在DevSupportManagerImpl
这个类中,还可以看到有另外一个doubleTapR操作可以直接进行reload jsbundle,继续跟到DevSupportManagerImpl
中,这里定义了调试dialog,跟到R.string.catalyst_reloadjs
这个事件,触发了handleReloadJS
,reload的流程入口就在这个方法中
1 |
|
可以看到这个方法主要是在取bundleURL,还区分了debug js remotely模式,可以看到这里的mJSAppBundleName
是在构造函里数获取的,然后构造函数用IDE的函数跳转功能并不能找到在哪里构造的,仔细观察DevSupportManagerImpl
的接口DevSupportManager
,可以看到在DevSupportManagerFactory
这个工厂类中有使用,这里是用的反射进行构造的1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47public static DevSupportManager create(
Context applicationContext,
ReactInstanceManagerDevHelper reactInstanceManagerHelper,
// 这个是mJSAppBundleName
String packagerPathForJSBundleName,
boolean enableOnCreate,
RedBoxHandler redBoxHandler,
DevBundleDownloadListener devBundleDownloadListener,
int minNumShakes) {
if (!enableOnCreate) {
return new DisabledDevSupportManager();
}
try {
// ProGuard is surprisingly smart in this case and will keep a class if it detects a call to
// Class.forName() with a static string. So instead we generate a quasi-dynamic string to
// confuse it.
String className =
new StringBuilder(DEVSUPPORT_IMPL_PACKAGE)
.append(".")
.append(DEVSUPPORT_IMPL_CLASS)
.toString();
Class<?> devSupportManagerClass =
Class.forName(className);
Constructor constructor =
devSupportManagerClass.getConstructor(
Context.class,
ReactInstanceManagerDevHelper.class,
String.class,
boolean.class,
RedBoxHandler.class,
DevBundleDownloadListener.class,
int.class);
return (DevSupportManager) constructor.newInstance(
applicationContext,
reactInstanceManagerHelper,
packagerPathForJSBundleName,
true,
redBoxHandler,
devBundleDownloadListener,
minNumShakes);
} catch (Exception e) {
throw new RuntimeException(
"Requested enabled DevSupportManager, but DevSupportManagerImpl class was not found" +
" or could not be created",
e);
}
}
跟到最后可以发现是在ReactNativeHost
这个抽象类的getJSMainModuleName()
方法拿到的,这个方法可以给用户重写进行自定义,再回到handleReloadJS
方法,拼接出来的bundleURL长这样http://localhost:8081/index.delta?platform=android&dev=true&minify=false
,host就是我们本地Nodejs启动的服务器地址1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71public void reloadJSFromServer(final String bundleURL) {
ReactMarker.logMarker(ReactMarkerConstants.DOWNLOAD_START);
mDevLoadingViewController.showForUrl(bundleURL);
mDevLoadingViewVisible = true;
final BundleDownloader.BundleInfo bundleInfo = new BundleDownloader.BundleInfo();
// 触发下载任务
mDevServerHelper.downloadBundleFromURL(
// 侦听下载
new DevBundleDownloadListener() {
public void onSuccess() {
mDevLoadingViewController.hide();
mDevLoadingViewVisible = false;
synchronized (DevSupportManagerImpl.this) {
mBundleStatus.isLastDownloadSucess = true;
mBundleStatus.updateTimestamp = System.currentTimeMillis();
}
if (mBundleDownloadListener != null) {
mBundleDownloadListener.onSuccess();
}
UiThreadUtil.runOnUiThread(
new Runnable() {
public void run() {
ReactMarker.logMarker(ReactMarkerConstants.DOWNLOAD_END, bundleInfo.toJSONString());
mReactInstanceManagerHelper.onJSBundleLoadedFromServer();
}
});
}
public void onProgress(@Nullable final String status, @Nullable final Integer done, @Nullable final Integer total) {
mDevLoadingViewController.updateProgress(status, done, total);
if (mBundleDownloadListener != null) {
mBundleDownloadListener.onProgress(status, done, total);
}
}
public void onFailure(final Exception cause) {
mDevLoadingViewController.hide();
mDevLoadingViewVisible = false;
synchronized (DevSupportManagerImpl.this) {
mBundleStatus.isLastDownloadSucess = false;
}
if (mBundleDownloadListener != null) {
mBundleDownloadListener.onFailure(cause);
}
FLog.e(ReactConstants.TAG, "Unable to download JS bundle", cause);
UiThreadUtil.runOnUiThread(
new Runnable() {
public void run() {
if (cause instanceof DebugServerException) {
DebugServerException debugServerException = (DebugServerException) cause;
showNewJavaError(debugServerException.getMessage(), cause);
} else {
showNewJavaError(
mApplicationContext.getString(R.string.catalyst_jsload_error),
cause);
}
}
});
}
},
mJSBundleTempFile,
bundleURL,
bundleInfo);
}
这个方法触发了下载任务和下载成功后续的操作,跟进mDevServerHelper.downloadBundleFromUR()
方法,走到BundleDownloader
类的downloadBundleFromURL
方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70public void downloadBundleFromURL(
final DevBundleDownloadListener callback,
final File outputFile,
final String bundleURL,
final @Nullable BundleInfo bundleInfo) {
// 实例化okhttp请求
final Request request =
new Request.Builder()
.url(mBundleDeltaClient.toDeltaUrl(bundleURL))
// FIXME: there is a bug that makes MultipartStreamReader to never find the end of the
// multipart message. This temporarily disables the multipart mode to work around it,
// but
// it means there is no progress bar displayed in the React Native overlay anymore.
// .addHeader("Accept", "multipart/mixed")
.build();
mDownloadBundleFromURLCall = Assertions.assertNotNull(mClient.newCall(request));
mDownloadBundleFromURLCall.enqueue(
new Callback() {
@Override
public void onFailure(Call call, IOException e) {
// ignore callback if call was cancelled
if (mDownloadBundleFromURLCall == null || mDownloadBundleFromURLCall.isCanceled()) {
mDownloadBundleFromURLCall = null;
return;
}
mDownloadBundleFromURLCall = null;
callback.onFailure(
DebugServerException.makeGeneric(
"Could not connect to development server.",
"URL: " + call.request().url().toString(),
e));
}
@Override
public void onResponse(Call call, final Response response) throws IOException {
// ignore callback if call was cancelled
if (mDownloadBundleFromURLCall == null || mDownloadBundleFromURLCall.isCanceled()) {
mDownloadBundleFromURLCall = null;
return;
}
mDownloadBundleFromURLCall = null;
final String url = response.request().url().toString();
// Make sure the result is a multipart response and parse the boundary.
String contentType = response.header("content-type");
Pattern regex = Pattern.compile("multipart/mixed;.*boundary=\"([^\"]+)\"");
Matcher match = regex.matcher(contentType);
try (Response r = response) {
if (match.find()) {
processMultipartResponse(
url, r, match.group(1), outputFile, bundleInfo, callback);
} else {
// In case the server doesn't support multipart/mixed responses, fallback to normal
// download.
processBundleResult(
url,
r.code(),
r.headers(),
Okio.buffer(r.body().source()),
outputFile,
bundleInfo,
callback);
}
}
}
});
}
先看看这个方法的形参
/data/data/com.socketclientrn/files/ReactNativeDevBundle.js
再看函数具体逻辑,内部使用了okhttp进行下载,下载成功后,onResponse
回调中对返回数据进行了缓存。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51private void processBundleResult(
String url,
int statusCode,
Headers headers,
BufferedSource body,
File outputFile,
BundleInfo bundleInfo,
DevBundleDownloadListener callback)
throws IOException {
// Check for server errors. the server error has the expected form, fail with more .
200) { (statusCode !=
String bodyString = body.readUtf8();
DebugServerException debugServerException = DebugServerException.parse(bodyString);
(debugServerException != null) {
callback.onFailure(debugServerException);
} {
StringBuilder sb = new StringBuilder();
sb.append("The development server returned response error code: ").append(statusCode).append("\n\n")
.append("URL: ").append(url).append("\n\n")
.append("Body:\n")
.append(bodyString);
callback.onFailure(new DebugServerException(sb.toString()));
}
return;
}
bundleInfo != null) { (
populateBundleInfo(url, headers, bundleInfo);
}
File tmpFile = new File(outputFile.getPath() + ".tmp");
boolean bundleUpdated;
(BundleDeltaClient.isDeltaUrl(url)) {
// bundle URL has the delta extension, we need to use the delta patching logic. the
bundleUpdated = mBundleDeltaClient.storeDeltaInFile(body, tmpFile);
} {
mBundleDeltaClient.reset();
bundleUpdated = storePlainJSInFile(body, tmpFile);
}
bundleUpdated) { (
// bundle from the server, move it to its final destination. we have received a new
(!tmpFile.renameTo(outputFile)) {
throw new IOException("Couldn't rename " + tmpFile + " to " + outputFile);
}
}
callback.onSuccess();
}
内部具体的流操作使用了okio,具体缓存的时候在参数outputFile
后面加了个.tmp
然后进行存储,存储ok后回调DevBundleDownloadListener
。
再回到DevSupportManagerImpl
的reloadJSFromServer
方法,可以在onSuccess
回调中看到判空mBundleDownloadListener
然后调用的逻辑,这个回调是初始化DevSupportManagerImpl
传进来的,调用链跟到最后是在ReactNativeHost
的createReactInstanceManager
方法中构建ReactInstanceManager
时传递的,这个方法开发者是可以重写的,提供给开发者侦听jsbundle下载是否成功与失败
1 | private ReactInstanceManagerDevHelper createDevHelperInterface() { |
跟着调用链,最后走到了createCachedBundleFromNetworkLoader
方法里1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public static JSBundleLoader createCachedBundleFromNetworkLoader(
final String sourceURL,
final String cachedFileLocation) {
return new JSBundleLoader() {
public String loadScript(CatalystInstanceImpl instance) {
try {
instance.loadScriptFromFile(cachedFileLocation, sourceURL, false);
return sourceURL;
} catch (Exception e) {
throw DebugServerException.makeGeneric(e.getMessage(), e);
}
}
};
}
createCachedBundleFromNetworkLoader
构造完JSBundleLoader
后,就开始调用CatalystInstanceImpl
去加载jsbundle了,CatalystInstance
是Java,C,JavaScript三端通信的入口。1
2
3
4
5
6/* package */ void loadScriptFromFile(String fileName, String sourceURL, boolean loadSynchronously) {
mSourceURL = sourceURL;
jniLoadScriptFromFile(fileName, sourceURL, loadSynchronously);
}
private native void jniLoadScriptFromFile(String fileName, String sourceURL, boolean loadSynchronously);
可以看到最终加载jsbundle是在C里面完成的
reload总的流程可以总结为:点击reload -> DevSupportManagerImpl
拼接URL,触发下载 -> BundleDownloader
请求服务器下载jsbundle -> 回调DevSupportManagerImpl
-> 调用CatalystInstanceImpl
通知C加载新的jsbundle
先看看Debug JS Remotely的点击事件,1
2
3
4
5
6
7
8
9options.put(
remoteJsDebugMenuItemTitle,
new DevOptionHandler() {
public void onOptionSelected() {
mDevSettings.setRemoteJSDebugEnabled(!mDevSettings.isRemoteJSDebugEnabled());
handleReloadJS();
}
});
先设置反了一下remote_js_debug
这个key,使用SharedPreference存储,然后就走到handleReloadJS
方法里
1 | if (mDevSettings.isRemoteJSDebugEnabled()) { |
这里区分了debug js remotely模式与普通开发模式,主要看看reloadJSInProxyMode
方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26private void reloadJSInProxyMode() {
// When using js proxy, there is no need to fetch JS bundle as proxy executor will do that
// anyway
mDevServerHelper.launchJSDevtools();
JavaJSExecutor.Factory factory = new JavaJSExecutor.Factory() {
public JavaJSExecutor create() throws Exception {
WebsocketJavaScriptExecutor executor = new WebsocketJavaScriptExecutor();
SimpleSettableFuture<Boolean> future = new SimpleSettableFuture<>();
executor.connect(
mDevServerHelper.getWebsocketProxyURL(),
getExecutorConnectCallback(future));
// TODO(t9349129) Don't use timeout
try {
future.get(90, TimeUnit.SECONDS);
return executor;
} catch (ExecutionException e) {
throw (Exception) e.getCause();
} catch (InterruptedException | TimeoutException e) {
throw new RuntimeException(e);
}
}
};
mReactInstanceManagerHelper.onReloadWithJSDebugger(factory);
}
先调用了launchJSDevtools
方法,里面仅仅做了一个简单的request,URL为http://localhost:8081/launch-js-devtools
,目的应该是打开调试网页,然后实例化了一个实现JavaJSExecutor.Factory
接口的匿名类,create
方法会在调用recreateReactContextInBackground
方法里的子线程中调用,跟进到connectInternal
方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57private void connectInternal(
String webSocketServerUrl,
final JSExecutorConnectCallback callback) {
final JSDebuggerWebSocketClient client = new JSDebuggerWebSocketClient();
final Handler timeoutHandler = new Handler(Looper.getMainLooper());
client.connect(
webSocketServerUrl, new JSDebuggerWebSocketClient.JSDebuggerCallback() {
// It's possible that both callbacks can fire on an error so make sure we only
// dispatch results once to our callback.
private boolean didSendResult = false;
public void onSuccess(@Nullable String response) {
client.prepareJSRuntime(
new JSDebuggerWebSocketClient.JSDebuggerCallback() {
public void onSuccess(@Nullable String response) {
timeoutHandler.removeCallbacksAndMessages(null);
mWebSocketClient = client;
if (!didSendResult) {
callback.onSuccess();
didSendResult = true;
}
}
public void onFailure(Throwable cause) {
timeoutHandler.removeCallbacksAndMessages(null);
if (!didSendResult) {
callback.onFailure(cause);
didSendResult = true;
}
}
});
}
public void onFailure(Throwable cause) {
timeoutHandler.removeCallbacksAndMessages(null);
if (!didSendResult) {
callback.onFailure(cause);
didSendResult = true;
}
}
});
timeoutHandler.postDelayed(
new Runnable() {
public void run() {
client.closeQuietly();
callback.onFailure(
new WebsocketExecutorTimeoutException(
"Timeout while connecting to remote debugger"));
}
},
CONNECT_TIMEOUT_MS);
}
这里使用了websocket与本地服务器进行连接,服务器URL为:ws://localhost:8081/debugger-proxy?role=client
,
继续跟到JSDebuggerWebSocketClient
的connect
方法1
2
3
4
5
6
7
8
9
10
11
12
13
14public void connect(String url, JSDebuggerCallback callback) {
if (mHttpClient != null) {
throw new IllegalStateException("JSDebuggerWebSocketClient is already initialized.");
}
mConnectCallback = callback;
mHttpClient = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.readTimeout(0, TimeUnit.MINUTES) // Disable timeouts for read
.build();
Request request = new Request.Builder().url(url).build();
mHttpClient.newWebSocket(request, this);
}
这里是使用okhttp来和本地服务器进行长连接,建立起连接后可以看到JSDebuggerWebSocketClient
里onMessage
,sendMessage
方法与服务器通信的逻辑。这里我们先回到reloadJSInProxyMode
方法,跟到onReloadWithJSDebugger
方法1
2
3
4
5
6
7
8private void onReloadWithJSDebugger(JavaJSExecutor.Factory jsExecutorFactory) {
Log.d(ReactConstants.TAG, "ReactInstanceManager.onReloadWithJSDebugger()");
recreateReactContextInBackground(
new ProxyJavaScriptExecutor.Factory(jsExecutorFactory),
JSBundleLoader.createRemoteDebuggerBundleLoader(
mDevSupportManager.getJSBundleURLForRemoteDebugging(),
mDevSupportManager.getSourceUrl()));
}
这里逻辑与普通debug模式差不多,都是构造JSBundleLoader
和JavaScriptExecutorFactory
,跟到createRemoteDebuggerBundleLoader
方法中
1 | /** |
可以从注释中看出,此时jsbundle也是从本地服务器下载的
跳出逻辑看看JSBundleLoader,暴露了四个方法
createAssetLoader
从asset目录中创建loadercreateFileLoader
从具体某个文件中创建loadercreateCachedBundleFromNetworkLoader
从URL中加载createRemoteDebuggerBundleLoader
同上
所以加载JSBundle可以归类为以上三种方式
开头的问题是js层面的,好像跟我分析的Java层并没什么卵关系。。但是读一读源码,总归没有坏处
]]>首先说下结果,以供参考
github的issue有FlatList不显示的问题,表现的跟我不太一样,他们解决方案是将removeClippedSubviews={false}
,我尝试了一下不适用我的场景
最终找出罪魁祸首是mobx的observable变量与FlatList的data在release环境下,未关闭RN log日志所导致的冲突
transform-remove-console
插件来关闭的)首先问题如下:
我的RN版本0.54.0
,mobx3.4.1
,mobx-react4.3.5
一个长度为两百多的数组只显示了二十三条,后面全是空白,继续往下划是一个无限空白的list,还伴随着闪屏现象,太可怕,更可怕的是debug包无此问题,release包却有,最怕排查这种问题,耗时耗力
当时第一反应是没做分页一次性加载太多数据导致的,因为没有想到这个接口会有这么多数据,一般也就十几二十条,随即进行了分页处理,然而并没有什么卵用,仍然是二十三条后就显示空白,再往后滑动闪屏,此时也没有其他头绪,这下子只能啃源码来看什么原因了,不过好在FlatList是纯js实现的,不需要再去啃Java代码了。
首先找到FlatList.js文件,看它的render函数
通过配置legacyImplementation
来选择使用MetroListView
或者VirtualizedList
前者是老的ListView,后者就是替代老ListView的新列表组件,官方解释这个变量是用来比较性能的,一般用不着,着重看看VirtualizedList
,view出了问题首先就看看renderItem
方法,下图为VirtualizedList
的renderItem
方法
这里就只是区分了多栏与单栏列表,我的使用场景是单栏列表,这行代码就只是给FlatList使用者回传了一个info参数,再看看info参数具体,找到VirtualizedList
的代码,再找renderItem
这个props在哪里调用的,下图为CellRenderer的render
方法里renderItem
回传参数
可以看到是在CellRenderer
这个组件的render方法里调用的,传入了item,index,separators
,我们要找的就是item,但是item是从props中拿到的,再找找CellRenderer
在哪里使用,可以看到是在_pushCells
方法中使用,_pushCells
方法在VirtualizedList
的render方法中调用,下图为VirtualizedList
的render
方法
cells作为React.cloneElement(element,[props],[...children])
的第三参数,如上图代码,此时基本可以确定问题应该在这个cells
参数上了,再回头看看_pushCells
方法,下图为VirtualizedList
的_pushCells
方法
可以看到item数据是来自props的getItem
方法,这个方法传入了一个data和一个ii下标,顾名思义应该就是在取单个列表的渲染数据,这个data就是FlatList的data,我们的列表数据源,再回到调用方FlatList找到getItem
方法,下图为FlatList
的getItem
方法
这个方法只是对多栏和单栏列表取数据的逻辑做了区分,我们可以试着把取出来的数据打印出来看是否有异常,加好调试代码,再编译一个带log的release包
可以看到第23个都挺正常,到了24个就不正常了,到了28个直接抛出error了,加了调试日志之后还会crash了,所以这个数据源可能有问题,联想到我用的Mobx框架,传给data的是一个Observable Array,而非普通Array,猜测是Observable Array与FlatList在此环境下有冲突,随后将其替换成普通Array,然后打包,测试一切正常
当时得出结论是FlatList和Observable Array搭配使用就会在release环境出问题,但是如果是这种结果,那问题影响面就太大了,然后发现我打的release包为了方便定位bug,将transform-remove-console
这个插件屏蔽了,打开了js日志。随后我又试着关闭日志,FlatList继续使用Observable Array,然后打包,测试一切正常,然后就经过了几番测试,基本确认了问题所在,实在有点玄学,为了定位这一个bug,打了快一天的包。。当然结论不重要,重要的是解决问题过程,以后再遇到这种问题,解决起来应该更加得心应手
闭包在程序界是一个很抽象的概念,以至于被人称为编程界的哈姆雷特,我们先来看看MDN对其的解释
Closures are functions that refer to independent (free) variables (variables that are used locally, but defined in an enclosing scope). In other words, these functions ‘remember’ the environment in which they were created.
- 闭包是一个函数,特指那些可以访问独立变量的函数(这种独立变量在本地使用,但是却定义在一个封闭的作用域),换句话说这类函数能够记忆创建它们时的环境
其实我个人理解更倾向于:
当嵌套函数的引用传递给了嵌套函数作用域之外的变量,或者对象的属性,此时就会形成一个闭包
嗯,解释的很好,但我还是不知道这是个啥
那还是少废话上代码吧。。1
2
3
4
5
6
7
8
9function person() {
var name = 'ergouzi';
console.log(name);
}
person();
console.log(name);
输出:
//ergouzi
//undefined
很普通的一个函数,正常理解函数与变量的思维就是:函数执行,定义变量,函数执行完毕,变量销毁。再来看看另一种写法1
2
3
4
5
6
7
8
9
10
11function person() {
var name = 'ergouzi';
var nameFunc = function() {
console.log(name);
}
return nameFunc;
}
var personFunc = person();
personFunc();
输出:
//ergouzi
可以看到,这里即使person函数执行完毕了,但是里面的name变量却没有被销毁,这里再套用开头解释的概念,应该能理解部分了吧。咱们再来验证一下这种“被记忆的独立变量”的特性1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17function person() {
var name = 'ergouzi';
var funcObj = {
'nameFunc': function () {
console.log(name);
},
'changeFunc': function () {
name = 'goudanzi';
}
}
return funcObj;
}
var funcObj = person();
funcObj.changeFunc();
funcObj.nameFunc();
输出:
//goudanzi
可以看到,我们在该独立变量的作用域外部改变了它的值,所以说明相同环境里创建的闭包函数,引用的独立变量为同一份拷贝,即同一个对象。其实用chrome调试一下就能很清楚的看到闭包函数长啥样,比如我这里的闭包函数它长这样(还长得挺漂亮的)
我们可以看到两个函数“changeFunc”,“nameFunc”,从他们的Scopes里面都能找到Closure并且创建环境都为person,记忆的独立变量都为“name”,
再来看点哦莫西罗伊的1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17for (var i = 0; i < 10; i++) {
setTimeout(function(){
console.log(i)
}, 0);
}
输出:
//10
//10
//10
//10
//10
//10
//10
//10
//10
//10
//简要解释一下输出值,因为setTimeout是异步函数,在i=0第一次循环时只是定义了第一个定时函数而并没有执行它,待到执行第一个定时函数,但此时i的值已经变了
一个普通的for循环,每次循环定义了一个定时器函数,因为没有给定时器函数的句柄传参,它只能拿到i最后的值。我们换一种“闭包”一点的写法1
2
3
4
5
6
7for (var i = 0; i < 10; i++) {
setTimeout(((j) => console.log(j))(i), 0);
}
//或者这样写
for (var i = 0; i < 10; i++) {
(j => setTimeout((j) => console.log(j), 0))(i);
}
这里用到了es6的箭头函数,想详细了解箭头函数请移步箭头函数
这里的代码将每次循环的i值传给了一个闭包函数,此时这个闭包函数记忆了这个i的值,等到执行定时函数时,就可以正常打印出i值。
参考文档https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures
简单来说就是参数消元,比如我有以下代码1
add = (x, y, z) => x + y + z
我们可以将它进行柯里化1
add = x => y => z => x + y + z
柯里化后的函数有个特点就是返回了一个新的函数,我们又可以对这个新的函数进行配置然后得到另一个新的函数,可以类比为函数的”预加载”功能,有了这个功能我们可以对通用代码进行一些封装,比如封装一个简单的请求1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20const requestFunc = headers => method => url => body => fetch(url, {
method,
headers,
body,
});
const request = requestFunc({
'content-Type': 'multipart/form-data',
...params,
});
const postRequest = request('POST');
const getRequest = request('GET');
const loginRequest = postRequest('https://host/package/login');
const getInfoRequest = getRequest('https://host/package/user/info')
// do login
loginRequest({ name: 'Tom', password: '123456' }).then(response => {});
// get info
getInfoRequest().then(response => {})'
当然,还有其他的精彩应用场景,比如使用这种特性来进行逻辑解耦和中间件的配置,Redux的源码也使用了大量柯里化函数,比如applyMiddleWare就是传入一个柯里化函数作为中间件,来获取内部状态信息。有兴趣的可以去细细品味
]]>