在树莓派3上运行Android6

最近需要在 android 上进行测试,无奈在电脑上使用模拟器运行速度太慢了。
正好我还有几个闲置的树莓派3设备,于是就尝试在树莓派上运行android看看。

刻录系统到SD卡

1.先下载 android6 的树莓派镜像: https://pan.baidu.com/s/1YHrmjN3be7UaLAdBJr-YhQ

2.下载完成后解压,然后开始执行写入操作: sudo dd if=andrpi3-20160626.img of=/dev/disk3 bs=4096000

这里我用的读卡器,被识别为 disk3。文件比较大,请耐心等待。我写入差不多花了一个小时的时间。

运行系统

将SD卡插回到树莓派中,并接上鼠标、键盘、显示器,然后开机。
这里稍微注意一下,树莓派3需要 5V/2A 的电源才能工作。
刚开始我试了一下开不了机,结果才发现是电源电压不足。

运行效果如图:

连接上网络之后使用 adb 进行操作: adb connect 192.168.0.101
这里我的树莓派的ip为 192.168.0.101

在storybook中使用antd

最近一个 react 的项目有用到了 antd 这个 ui 库。这里作个笔记记录一下如何在 storybook 中显示 antd 的组件。

项目是使用 create-react-app 创建的,项目目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
├── .storybook/
├── README.md
├── antd-theme.js
├── node_modules
├── package.json
├── public
└── src
├── App.js
├── components
│   ├── button.js
│   └── button.stories.js
├── index.js
└── registerServiceWorker.js

这里创建了一个 button 组件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// src/components/button.js
import React, { Component, Fragment } from 'react';
import { Button } from 'antd';

class ButtonGhost extends Component {
render() {
return (
<Fragment>
<h3 className="ex-title">Ghost Button</h3>
<div style={{ background: 'rgb(47, 45, 165)', padding: '26px 16px 16px' }}>
<Button type="primary">Primary</Button>
<Button className="ml20" ghost>Default</Button>
<Button className="ml20" type="dashed" ghost>Dashed</Button>
<Button className="ml20" type="danger" ghost>danger</Button>
</div>
</Fragment>
);
}
}

export default ButtonGhost;

对应的 storybook 案例如下:

1
2
3
4
5
6
7
// src/components/button.stories.js
import React from 'react';
import { storiesOf } from '@storybook/react';
import Button from './button';

storiesOf('General', module)
.add('Button', () => <Button />)

然后 storybook 配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
// .storybook/config.js
import React from 'react'
import { configure } from '@storybook/react'
import { ThemeProvider } from 'styled-components'

const req = require.context('../src/components', true, /\.stories\.js$/)

function loadStories() {
req.keys().forEach((filename) => req(filename))
}

configure(loadStories, module)

然后运行 storybook : start-storybook -p 6006 -c .storybook,效果如下:

这是由于 antd 的 css 没有加载,因此所有按钮的样式都没有。
参考 https://ant.design/docs/react/use-with-create-react-app-cn 的说明,修改 babel 和 webpack 的配置。
参考 https://storybook.js.org/configurations/custom-webpack-config 的说明,修改 storybook 的 webpack 配置。

创建文件 .storybook/.babelrc

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
{
"presets": [
[
"env",
{
"modules": false,
"targets": {
"browsers": [
">1%",
"last 4 versions",
"Firefox ESR",
"not ie < 11"
]
}
}
],
"react",
"stage-3"
],
"plugins": [
[
"import",
{
"libraryName": "antd",
"style": true
}
]
]
}

创建文件 .storybook/webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
module.exports = {
module: {
rules: [
{
test: /\.less$/,
use: [{
loader: "style-loader"
}, {
loader: "css-loader"
}, {
loader: "less-loader",
options: {
//modifyVars: antdTheme, // 如果要自定义主题样式
javascriptEnabled: true
}
}]
}
]
}
};

然后在 .storybook/config.js 文件添加 import 'antd/dist/antd.less';
最后的效果如下:

使用python编写游戏修改器

最近比较怀旧,在玩一个比较老的PC游戏。由于游戏难度太高了,于是就打算自己写一个修改器。
通过查阅资料,在 Windows 下的修改器主要需要用到四个函数:OpenProcess, CloseHandle, WriteProcessMemory, ReadProcessMemory。

这几个都是C++的函数,在Python中可以通过ctypes来直接调用。
然后接下来介绍一下基本的操作流程。

1.通过 任务管理器 或者其他方式得到需要修改的游戏进程。然后通过 OpenProcess 注入该进程。

1
2
3
4
5
6
7
8
9
PROCESS_QUERY_INFORMATION = 0x0400
PROCESS_VM_OPERATION = 0x0008
PROCESS_VM_READ = 0x0010
PROCESS_VM_WRITE = 0x0020

hProcess = ctypes.windll.kernel32.OpenProcess(
PROCESS_QUERY_INFORMATION|PROCESS_VM_READ|PROCESS_VM_OPERATION|PROCESS_VM_WRITE,
False, pid
)

2.然后可以通过 ReadProcessMemory 来扫描游戏的内存找到需要修改的数值项的内存地址。

1
2
3
4
5
6
7
8
9
buf = ctypes.c_int32()
nread = ctypes.c_size_t()
ret = ctypes.windll.kernel32.ReadProcessMemory(
hProcess,
base_addr,
ctypes.byref(buf),
ctypes.sizeof(buf),
ctypes.byref(nread)
)

这里是读取 base_addr 地址之后的4个字节的内容。可以通过循环来遍历游戏的内存,找到需要修改的地址。
当然,为了方便也可以直接使用 Cheat Engine 之类的软件来查找,然后把找到的内存地址记录下来即可。

3.得到需要修改的内存地址之后,就可以 WriteProcessMemory 来修改该地址保存的值。

1
2
3
4
5
6
7
8
9
buf = ctypes.c_int32(value)
nread = ctypes.c_size_t()
ret = ctypes.windll.kernel32.WriteProcessMemory(
hProcess,
base_addr,
ctypes.byref(buf),
ctypes.sizeof(buf),
ctypes.byref(nwrite)
)

这里是往 base_addr 这个地址写入值为 value 的4字节内容。

4.最后如果不再需要修改了的话,就通过 CloseHandle 关闭该注入操作。

1
ctypes.windll.kernel32.CloseHandle(hProcess)

以上都是针对 Windows 系统的,对于 Linux 系统的话 可以通过 ptrace (http://man7.org/linux/man-pages/man2/ptrace.2.html) 操作实现。由于我没有 Linux 的游戏就没有研究了。

上面修改器的完整源代码,如有需要可通过以下链接获取:
https://github.com/wusuopu/cheat_engine_caesar3

使用pytest测试flask应用

python 本身就有 unittest 单元测试框架,但是觉得它并不是很好用,我更倾向于使用 pytest 。

下面通过一个例子来介绍如何使用 pytest 对 flask 应用进行单元测试。

首先新建一个 flask 应用,并针对根路径创建一条路由。代码如下:

1
2
3
4
5
6
# server.py
app = flask.Flask(__name__)

@app.route('/')
def home():
return 'ok'

然后针对首页编写单元测试,代码如下:

1
2
3
4
5
6
# tests/test_app.py

def test_home_page(client):
rv = client.get('/')
assert rv.status_code == 200
assert rv.data == b'ok'

然后执行命令运行该测试用例: pytest -s tests/test_app.py

在 pytest 中编写测试用例就只需要新建一个以 test_ 开头的函数即可。

以上是针对flask路由作的最基本测试。接下来编写一个新的路由,该页面只有用户登录之后才能访问。代码如下:

1
2
3
4
5
6
# server.py
@app.route('/member')
@flask_security.decorators.login_required
def member():
user = flask_security.core.current_user
return str(user.id)

要对该路由进行测试,则需要先创建一个用户。

1
2
3
4
5
6
7
8
9
# tests/test_app.py
def setup_module(module):
App.testing = True
fixture.setup()


def teardown_module(module):
"""
"""

上面的 setup_moduleteardown_module 函数分别是在所有的测试用例执行之前与执行之后执行。在这里我们通过 setup_module 在执行测试之前先创建一个用户。然后再创建一个 pytest 的 fixture:
1
2
3
4
5
6
7
8
# tests/conftest.py

@pytest.fixture
def auth_client(client):
with client.session_transaction() as sess:
sess['user_id'] = str(fixture.users[0].id)

yield client

这里创建了一个 auth_client fixture,之后所有以 auth_client 发起的请求都是登录状态的。

最后再针对 /member 路由编写两个测试用例,分别是未登录状态与登录状态下的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def test_member_page_without_login(client):
"""
没有登录则跳转到登录页面
"""
rv = client.get('/member')
assert rv.headers['Location'] == 'http://localhost/login?next=%2Fmember'
assert rv.status_code == 302


def test_member_page_with_login(auth_client):
"""
已经登录则返回当前用户id
"""
rv = auth_client.get('/member')
assert rv.status_code == 200
assert rv.data.decode('utf8') == str(fixture.users[0].id)

以上就是一个简单的 flask 应用了。但是有时一个稍微复杂一点的应用会用到一些第三方的api。这时针对这种情况编写测试用例时就需要用到 mock 功能了。再编写一个新的路由页面:

1
2
3
4
5
6
7
8
# server.py

@app.route('/movies')
def movies():
data = utils.fetch_movies()
if not data:
return '', 500
return flask.jsonify(data)

1
2
3
4
5
6
7
8
9
# utils.py

def fetch_movies():
try:
url = 'http://api.douban.com/v2/movie/top250?start=0&count=1'
res = requests.get(url, timeout=5)
return res.json()
except Exception as e:
return {}

请求该路由会返回豆瓣top250的电影信息。然后再编写两个测试用例分别模拟api调用成功与失败的情况。

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
# tests/test_app.py
def test_movies_api(client):
"""
调用豆瓣api成功的情况
"""
fetch_movies_patch = mock.patch('utils.fetch_movies')

func = fetch_movies_patch.start()
func.return_value = {'start': 0, 'count': 0, 'subjects': []}

rv = client.get('/movies')
assert rv.status_code == 200
assert func.called

fetch_movies_patch.stop()


def test_movies_api_with_error(client):
"""
调用豆瓣api出错的情况
"""
fetch_movies_patch = mock.patch('utils.fetch_movies')

func = fetch_movies_patch.start()
func.return_value = None

rv = client.get('/movies')
assert rv.status_code == 500
assert func.called

fetch_movies_patch.stop()

这里使用 python 的 mock 模块来模拟让某个函数返回固定的结果。

完整的代码请访问: https://github.com/wusuopu/flask-test-example

HAProxy+Nginx+gunicorn获取真实ip

之前在部署在 nginx + uwsgi 应用时都是通过如下方法来获取真实的客户端ip的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
upstream app_server {
server unix:///tmp/gunicorn.sock fail_timeout=0;
}

server {
listen 80;
server_name localhost;
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;

proxy_pass http://app_server;
}
}

这样在 uwsgi 的应用程序中只需要读取 http headers 中的 X-Forwarded-For 字段即可。

但是最近由于运维架构的是采用 haproxy + nginx + uwsgi 是形式,导致了在 uwsgi 应用程序中获取到的 ip 都是 haproxy 的。
为了要获取到真实的ip地址,需要由 haproxy 将 ip 传给 nginx,再由 nginx 传给 uwsgi。
在网上搜索了半天 haproxy 的相关配置,感觉太复杂了。因此还是决定从 nginx 入手。

经过实验将 nginx 的配置改为如下即可:

1
2
3
proxy_set_header X-Forwarded-For $http_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;

在Docker中运行X11程序

如果是Linux系统的话,相对比较方便。先构建一个带gui各应的docker image,然后将
本机的X11 sock挂载到container内,
docker run -ti --rm -e DISPLAY=$DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix chrome

如果是mac OS系统的话,相对麻烦一些。

1.安装所需的软件:

1
2
brew install socat
brew cask install xquartz

2.依次运行刚刚安装的两个程序:

1
2
socat TCP-LISTEN:6000,reuseaddr,fork UNIX-CLIENT:\"$DISPLAY\"
open -a XQuartz

3.设置X11, XQuartz -> Preference -> Security -> Allow connections from network clients

4.docker run --rm -e DISPLAY=ifconfig | grep “inet\ “ | tail -1 | cut -d “ “ -f 2:0 chrome

对于已经启动了的container,可以在container内执行命令 export DISPLAY=<ip>:0 来设置 DISPLAY 从而使用本机的 X11 服务。

倘若当前你的 mac OS 没有连接网络,那么可能就没有ip地址供container内访问。
此时也许可以执行命令: sudo ifconfig lo0 alias 10.200.10.1/24 来手动设置一个ip。
然后在container内再设置 export DISPLAY=10.200.10.1:0

由octpress迁移到了hexo

这两天将博客的系统由原来的ruby octpress 迁移到了 nodejs hexo。

决定要进行迁移主要是两个原因:
1.使用 octpress 来生成页面感觉越来越慢了;
2.octpress 的页面样式表不知怎么的突然就坏掉了,整个页面显示都不正常了。这个是最主要的原因。

迁移的工作还算是比较顺利,只是体验了一下 hexo 感觉 bug 也不少。只得自己写些 patch,然后将就着用吧。

通过串口连接raspberry pi

最近在玩树莓派,有时没有网络,也没有显示器,此时如果想要连接树莓派执行一些操作的话会很麻烦。
因为之前玩过 ARM 的开发板编程,因此想能不能通过串口登录到 pi 呢。于是网上查了一下,还真的可以哦。
以下就作为备忘笔记记录一下操作过程。

1.首先需要一根 USB 转串口的线,如果没有的话可以去某宝上买一根吧,反正也不贵。我选的是 PL2303。
再根据系统以及芯片的不同而下载安装不同的驱动程序。 对于 mac OS 用户执行命令: ls /dev/ | grep tty.usb
如果驱动都安装正确的话应该是会有输出结果的。

2.然后在 pi 的系统上启用 serial。我安装的是 debian 系统,执行命令: sudo raspi-config
选择 advanced options -> serial 进行启用 serial。

3.串口连接

对于 Raspberry Pi3 的 GPIO 引脚如下:
pi3_gpio

串口线与 pi 的连接方式为: GND -> GND, RXD -> TXD, TXD -> RXD, 如图:
pi3-board

最后在电脑上使用串口连接软件进行连接,对应的串口设置为 115200 8N1,如图:
raspberry-pi-serial

参考资料: http://elinux.org/RPi_Serial_Connection

使用CodePush对ReactNative进行热更新

CodePush是微软提供的可用于对 Cordova 和 ReactNative 进行代码热更新的库。
在其官方的文档中已经写得很详细了,按照其说明来配置即可。我这里只是对在使用过程中遇到的一些坑作为总结。

创建应用

首先注册一个账号并创建一个 CodePush 的应用:

1
2
3
npm install -g code-push-cli
code-push register
code-push app add <appName>

安装配置CodePush

按照说明 https://github.com/Microsoft/react-native-code-push#getting-started,使用 rnpm 进行安装即可:

1
2
npm install --save react-native-code-push
rnpm link react-native-code-push

安装完成之后还需要再进行一些配置,对于 iOS 需要将 AppDelegate.m 文件中的 jsCodeLocation 修改为: jsCodeLocation = [CodePush bundleURL];
同时再在 Info.plist 文件中添加一项 CodePushDeploymentKey,其值为 CodePush 应用的 Deployment Key。

对于 android 需要在 MainActivity 类的 getPackages 方法中设置 Deployment Key。同时根据 ReactNative 的版本不同而使用不同的方法来设置 getJSBundleFile
参考: https://github.com/Microsoft/react-native-code-push#android-setup

程序更新

在安装、配置完成之后,即可以使用CodePush进行程序的更新操作了。
根据官方的说明只需要调用 CodePush.sync() 即可完成自动更新操作。
我针对自己的情况再进行封装了一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function syncCodePush() {
NetInfo.fetch().done(
(reach) => {
// 检查网络环境
if (_.includes(['wifi', 'WIFI', 'VPN'], reach)) {
CodePush.sync().done(
() => {
// 检查更新成功
},
(err) => {
// 更新失败
}
);
}
}
);
}

以上函数是保证只有在wifi的网络环境下才进行更新操作,同时由于 CodePush.sync() 返回的是一个 Promise 对象,
在这里我就遇到了由于网络异常而下载出错,从而导致 app 崩溃。因此需要处理 reject 的情况。

有时在程序更新之后的首次运行时可能会需要作一些迁移的操作,这里可以使用 getUpdateMetadata 来检查程序是不是首次运行。

1
2
3
4
5
6
7
Codepush.getUpdateMetadata().then(
(update) => {
if (update && update.isFirstRun) {
// 首次运行执行一些操作
}
}
).done( callback );

发布更新

在 app 发布安装包发布出去之后,已经有用户下载安装了。此时如果再有 js 代码更新或者图片文件的改动的话,可以使用 CodePush 进行发布。
进入 ReactNative 的项目根目录,使用 code-push 命令进行发布更新。例如:

1
code-push release-react DemoApp ios -m -d Staging --des "更新描述" -t "~2.0.0"

以上命令是发布一个紧急更新到 Staging ,只有 ios appp 的版本为 2.0~3.0 的才会下载该更新包。
由于是紧急更新,app在下载安装完成之后会自动重启应用该更新包。否则的话就需要用户下次手动启动app时该更新包才会生效。

在 CodePush 中针对 ios 和 android 可以共用一个应用,只是我个人感觉这样在管理 deployment history 时不太方便。
因此我通常会创建两个应用,例如: DemoApp-iOS、DemoApp-Android 这样的。

需要注意的是,由于 CodePush 的 server 是在国外,因此下载的速度会比较慢。

最后我自己使用 Electron + Vue.js 开发了一个 CodePush 的简易管理工具,https://github.com/wusuopu/code-push-gui
可以对 CodePush 的 app 跟 deployment 进行简单的管理。

ReactNative获取设备屏幕尺寸

在做移动开发过程中,有时我们需要适配不同尺寸大小的屏幕。这里我们就需要到获取设备屏幕的大小。
由于我们是使用的 ReactNative 来开发手机 app,这里就介绍一下在 ReactNative 中如何获取到设备屏幕的分辨率的。
也算是对之前踩坑的总结吧。

在此之前需要先了解 ReactNative 中的尺寸计算单位,它并不是使用的px。http://facebook.github.io/react-native/releases/next/docs/pixelratio.html

使用 Dimensions 模块

在 ReactNative 中有一个 Dimensions 模块,通过它可以获取当前设备的屏幕分辨率。
参考: http://facebook.github.io/react-native/releases/next/docs/dimensions.html

1
var {height, width} = Dimensions.get('window');

刚开始时我也是使用这种方法来得到整个屏幕的分辨率的,感觉轻松搞定。然而这里面却有一个坑。

首先来看看 ios 和 android 中的界面结构:

ios-screen-struct
android-screen-struct

如图所示,屏幕的宽度计算比较简单,就是从左边到右边的距离即可。
然后就是屏幕的高度了,这里其实我们是需要获取到可用区域的高度。
如图所示,对于 ios 系统来说可用区域高度就是整个屏幕的高度减去 Status Bar 的高度;
对于 android 系统来说就是屏幕的高度减去 Status Bar 和 Soft Menu Bar 的高度。

获取 iOS 设备的屏幕分辨率

正如上面所说的,在 ios 下的计算方法为:

1
2
var WIDTH = Dimensions.get('window').width;
var HEIGHT = Dimensions.get('window').height - STATUS_BAR_HEIGHT;

在 ios 系统状态栏高度(STATUS_BAR_HEIGHT)通常为 20。
不过如果你设置了隐藏状态栏的话,那么 STATUS_BAR_HEIGHT 则为0。

以上是手机竖屏的情况,在横屏状态下则交换两个值:

1
2
var LANDSCAPE_WIDTH = HEIGHT + STATUS_BAR_HEIGHT;
var LANDSCAPE_HEIGHT = WIDTH - STATUS_BAR_HEIGHT;

到了这里虽然麻烦了一点,但是总体来说也还好。问题都解决了。
感觉生活是如此美好啊,然而这个世界上却还有一个系统名为 android。
它有着数不清种类屏幕大小的设备,然后瞬间感觉整个人都不好了。

获取 Android 设备的屏幕分辨率

如果按照 ios 下的方法来做,获取到 WIDTH 是没有问题,
但是 HEIGHT 的话还需要减去 Status Bar 和 Soft Menu Bar 的高度。
因此我们还需要获取到状态栏的高度(STATUS_BAR_HEIGHT)和虚拟按钮的高度(SOFT_MENU_BAR_HEIGHT)。

这里我们使用react-native-extra-dimensions-android这个库。

1
2
3
4
5
6
const ExtraDimensions = require('react-native-extra-dimensions-android');

const STATUS_BAR_HEIGHT = ExtraDimensions.get('STATUS_BAR_HEIGHT');
const SOFT_MENU_BAR_HEIGHT = ExtraDimensions.get('SOFT_MENU_BAR_HEIGHT');
const WIDTH = ExtraDimensions.get('REAL_WINDOW_WIDTH');
const HEIGHT = ExtraDimensions.get('REAL_WINDOW_HEIGHT') - STATUS_BAR_HEIGHT - SOFT_MENU_BAR_HEIGHT;

以上是设备竖屏的结果,在横屏下你以为是不是只需要交换两个值就搞定了呢。

1
2
var LANDSCAPE_WIDTH = HEIGHT + STATUS_BAR_HEIGHT;
var LANDSCAPE_HEIGHT = WIDTH - STATUS_BAR_HEIGHT;

Naive!!如果这么轻松就搞定了的话,android就不叫做android了。

先来看看下面两张图片吧,分别是一个android平板设备在横屏和竖屏状态下的截图:

android-pad-landscape
android-pad-portrait

你把你手中的 android 手机分别进入横屏和竖屏状态下,再对照上面两张图片你会发现什么。
没错的,在平板设备上屏幕旋转之后 Soft Menu Bar 也跟着旋转了,
而在手机设备上 Soft Menu Bar 是始终固定在手机底部的。

尼玛,太坑爹了。看到这里瞬间呕血三升,要适配手机跟平板实现是太麻烦了。
以下是我的解决办法,先检查当前设备是否为平板,然后再分别处理。
至于平板的判断方法就自己想办法了,我的方法也不一定准。

1
2
3
4
5
6
7
if (isPad) {
LANDSCAPE_WIDTH = HEIGHT + STATUS_BAR_HEIGHT + SOFT_MENU_BAR_HEIGHT;
LANDSCAPE_HEIGHT = WIDTH - STATUS_BAR_HEIGHT - SOFT_MENU_BAR_HEIGHT;
} else {
LANDSCAPE_WIDTH = HEIGHT + STATUS_BAR_HEIGHT;
LANDSCAPE_HEIGHT = WIDTH - STATUS_BAR_HEIGHT;
}

以上是在 android 下遇到的第一个大坑。

到了这里我以为一切都该结束了吧,然而没想到还有一种手机叫做魅族。如下图:

Mezu-smart-bar

在屏幕右下角其实是有一个按钮的,然而左图所示的,该按钮被魅族手机的 Smart Bar 遮住了。
进入系统设置将 Smart Bar 隐藏后效果如右图所示。

看到这里又吐了两口老血,此为第二个大坑。

1
2
3
4
5
6
7
8
9
10
11
12
13
const SMART_BAR_HEIGHT = ExtraDimensions.get('SMART_BAR_HEIGHT');

if (SMART_BAR_HEIGHT) {
HEIGHT -= SMART_BAR_HEIGHT;
}

....
// 在上面 LANDSCAPE 的计算结果上再作如下处理

if (SMART_BAR_HEIGHT) {
LANDSCAPE_WIDTH += SMART_BAR_HEIGHT;
LANDSCAPE_HEIGHT -= SMART_BAR_HEIGHT;
}

在魅族的手机上计算屏幕高度时还需要再减去 Smart Bar 的高度,
同时还需要注意的是, Smart Bar 跟平板上的Soft Menu Bar 一样会随着屏幕旋转而转动的。

最后还有一点需要注意的是,在安装 react-native-extra-dimensions-android 库时不能直接使用 npm install --save react-native-extra-dimensions-android 进行安装,
而是需要直接通过 git 仓库来安装: npm install --save git+https://github.com/jaysoo/react-native-extra-dimensions-android.git

因为在 npm 上 react-native-extra-dimensions-android 的最新版为 0.17.0,
SMART_BAR_HEIGHT 的功能是在此之后才添加进来的。

在库的版本上面又被坑了一下。