SegmentFault 编程沉思录最新的文章
2019-05-18T23:44:52+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
Android Q 沙箱适配多媒体文件总结
https://segmentfault.com/a/1190000019224425
2019-05-18T23:44:52+08:00
2019-05-18T23:44:52+08:00
DesGemini
https://segmentfault.com/u/desgemini
1
<h2>综述</h2>
<p>所有内容的访问变化见下图:</p>
<p><img src="/img/bVbsPjY?w=1454&h=1348" alt="图片描述" title="图片描述"></p>
<h2>外部媒体文件的扫描,读取和写入</h2>
<p>最容易被踩坑的应该是,对外部媒体文件,照片,视频,图片的读取或写入。</p>
<h3>扫描</h3>
<p>首先是扫描。扫描依然是使用 query MediaStore 的方式。一句话介绍 MediaStore,MediaStore 就是Android系统中的一个多媒体数据库。代码如下图所示,以搜索本地视频为例子:</p>
<pre><code class="java">protected List<VideoInfo> doInBackground(Void... params) {
mContentResolver = context.getContentResolver();
String[] mediaColumns = { MediaStore.Video.Media._ID, MediaStore.Video.Media.DATA,
MediaStore.Video.Media.TITLE, MediaStore.Video.Media.MIME_TYPE,
MediaStore.Video.Media.DISPLAY_NAME, MediaStore.Video.Media.SIZE,
MediaStore.Video.Media.DATE_ADDED, MediaStore.Video.Media.DURATION,
MediaStore.Video.Media.WIDTH, MediaStore.Video.Media.HEIGHT };
Cursor mCursor = mContentResolver.query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, mediaColumns,
null, null, MediaStore.Video.Media.DATE_ADDED);
if (mCursor == null) {
return null;
}
// 注意,DATA 数据在 Android Q 以前代表了文件的路径,但在 Android Q上该路径无法被访问,因此没有意义。
ixData = mCursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATA);
ixMime = mCursor.getColumnIndexOrThrow(MediaStore.Video.Media.MIME_TYPE);
// ID 是在 Android Q 上读取文件的关键字段
ixId = mCursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID);
ixSize = mCursor.getColumnIndexOrThrow(MediaStore.Video.Media.SIZE);
ixTitle = mCursor.getColumnIndexOrThrow(MediaStore.Video.Media.TITLE);
allImages = new ArrayList<VideoInfo>();
mTotalVideoCount = 0;
mCursor.moveToLast();
while (mCursor.moveToPrevious()) {
if (addVideo(mCursor) == 0) {
continue;
} else if (addVideo(mCursor) == 1) {
break;
}
}
mCursor.close();
return allImages;
}</code></pre>
<p>既然 data 不可用,就需要知晓 id 的使用方式,首先是使用 id 拼装出 content uri ,如下所示:</p>
<pre><code class="java">public getRealPath(String id) {
return MediaStore.Video.Media.EXTERNAL_CONTENT_URI.buildUpon().appendPath(String.valueOf(id)).build().toString();
}</code></pre>
<p>Image 同理换成 MediaStore.Images。</p>
<h3>读取和写入</h3>
<p>其次,是读取 content uri。这里需要注意 File file = new File(contentUri); 是无法获取到文件的。file.exist() 为 false。</p>
<p>那么就产生两个问题:1. 如何确定 ContentUri 形式的文件存在 2. 如何读取或写入文件。</p>
<p>首先,对于 Content Uri 的读取,必须借助于 ContentResolver。</p>
<p>其次,对于 1,没有找到 Google 文档中提供比较容易的API,只能采用打开 FileDescriptor 是否成功的形式,代码如下所示:</p>
<pre><code class="java">public boolean isContentUriExists(Context context, Uri uri) {
if (null == context) {
return false;
}
ContentResolver cr = context.getContentResolver();
try {
AssetFileDescriptor afd = cr.openAssetFileDescriptor(uri, "r");
if (null == afd) {
iterator.remove();
} else {
try {
afd.close();
} catch (IOException e) {
}
}
} catch (FileNotFoundException e) {
return false;
}
return true;
}</code></pre>
<p>这种方法最大的问题即是,对应于一个同步 I/O 调用,易造成线程等待。因此,目前对于 MediaStore 中扫描出来的文件可能不存在的情况,没有直接的好方法可以解决过滤。</p>
<p>对于问题 2,如 1 所示,可以借助 Content Uri 从 ContentResolver 里面拿到 AssetFileDescriptor,然后就可以拿到 InputSteam 或 OutputStream,那么接下来的读取和写入就非常自然,如下所示:</p>
<pre><code class="java">public static void copy(File src, ParcelFileDescriptor parcelFileDescriptor) throws IOException {
FileInputStream istream = new FileInputStream(src);
try {
FileOutputStream ostream = new FileOutputStream(parcelFileDescriptor.getFileDescriptor());
try {
IOUtil.copy(istream, ostream);
} finally {
ostream.close();
}
} finally {
istream.close();
}
}
public static void copy(ParcelFileDescriptor parcelFileDescriptor, File dst) throws IOException {
FileInputStream istream = new FileInputStream(parcelFileDescriptor.getFileDescriptor());
try {
FileOutputStream ostream = new FileOutputStream(dst);
try {
IOUtil.copy(istream, ostream);
} finally {
ostream.close();
}
} finally {
istream.close();
}
}
public static void copy(InputStream ist, OutputStream ost) throws IOException {
byte[] buffer = new byte[4096];
int byteCount = 0;
while ((byteCount = ist.read(buffer)) != -1) { // 循环从输入流读取 buffer字节
ost.write(buffer, 0, byteCount); // 将读取的输入流写入到输出流
}
}</code></pre>
<h3>保存媒体文件到公共区域</h3>
<p>这里仅以 Video 示例,Image、Downloads 基本类似:</p>
<pre><code class="java">public static Uri insertVideoIntoMediaStore(Context context, String fileName) {
ContentValues contentValues = new ContentValues();
contentValues.put(MediaStore.Video.Media.DISPLAY_NAME, fileName);
contentValues.put(MediaStore.Video.Media.DATE_TAKEN, System.currentTimeMillis());
contentValues.put(MediaStore.Video.Media.MIME_TYPE, "video/mp4");
Uri uri = context.getContentResolver().insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentValues);
return uri;
}</code></pre>
<p>这里所做的,只是往 MediaStore 里面插入一条新的记录,MediaStore 会返回给我们一个空的 Content Uri,接下来问题就转化为往这个 Content Uri 里面写入,那么应用上一节所述的代码即可实现。</p>
<h3>Video 的 Thumbnail 问题</h3>
<p>在 Android Q 上已经拿不到 Video 的 Thumbnail 路径了,又由于没有暴露 Video 的 Thumbnail 的 id ,导致了 Video 的 Thumbnail 只能使用实时获取 Bitmap 的方法,如下所示:</p>
<pre><code class="java">private Bitmap getThumbnail(ContentResolver cr, long videoId) throws Throwable {
return MediaStore.Video.Thumbnails.getThumbnail(cr, videoId, MediaStore.Video.Thumbnails.MINI_KIND,
null);
}</code></pre>
<p>可以进去看 Android SDK 的实现,其中最关键的部分是:</p>
<pre><code class="java">String column = isVideo ? "video_id=" : "image_id=";
c = cr.query(baseUri, PROJECTION, column + origId, null, null);
if (c != null && c.moveToFirst()) {
bitmap = getMiniThumbFromFile(c, baseUri, cr, options);
if (bitmap != null) {
return bitmap;
}
}</code></pre>
<p>进一步再进去看,可以发现直接就把 Video/Image 文件打开计算 Thumbnail。</p>
<pre><code class="java">private static Bitmap getMiniThumbFromFile(
Cursor c, Uri baseUri, ContentResolver cr, BitmapFactory.Options options) {
Bitmap bitmap = null;
Uri thumbUri = null;
try {
long thumbId = c.getLong(0);
String filePath = c.getString(1);
thumbUri = ContentUris.withAppendedId(baseUri, thumbId);
ParcelFileDescriptor pfdInput = cr.openFileDescriptor(thumbUri, "r");
bitmap = BitmapFactory.decodeFileDescriptor(
pfdInput.getFileDescriptor(), null, options);
pfdInput.close();
} catch (FileNotFoundException ex) {
Log.e(TAG, "couldn't open thumbnail " + thumbUri + "; " + ex);
} catch (IOException ex) {
Log.e(TAG, "couldn't open thumbnail " + thumbUri + "; " + ex);
} catch (OutOfMemoryError ex) {
Log.e(TAG, "failed to allocate memory for thumbnail "
+ thumbUri + "; " + ex);
}
return bitmap;
}</code></pre>
<p>这个 API 毫无疑问设计的非常不合理,没有暴露 Thumbnail 的系统缓存给开发者,造成了每次都要重新I/O 计算的极大耗时。强烈呼吁 Android Q 的正式版能修正这个 API 设计缺陷。</p>
Meta of Meta 求元方法递归
https://segmentfault.com/a/1190000017579011
2019-01-01T16:53:00+08:00
2019-01-01T16:53:00+08:00
DesGemini
https://segmentfault.com/u/desgemini
1
<p>首先,我们解决一个问题,我们有了第一个方法。然后,我们想解决包含这个问题的一类问题,我们总结一个元方法。然后,我们想知道怎么样找到一类问题的方法的方法,这是就是元方法的元方法。或者说,元元方法。这样的一个不断上溯的过程,我称之为求元方法递归。</p>
<p>从互联网业务-编程实际场景举例子,飞猪,大家都知道,阿里的在线旅行电商销售网站。飞猪最开始只是照搬了淘宝的下单组件,这个下单组件针对于一般商品的下单流程,他只能让用户填写收货地址和数量和品类。对于飞猪的签证业务来说,这样的下单组件起不了帮助。因为签证业务需要收集用户的关键护照信息,那么就只能靠客服一对一的聊天和用户确认信息。运营收集到行业商家的痛点,反馈给产品经理,产品经理思考这个需求点的合理性和整体流程,给到技术开发排期上线。好,至此为止,第一层 meta 元方法产生了。因为在这个新版的为签证业务定制的下单组件,用一个方法解决了一类问题的麻烦。</p>
<p>那么我们该如何衡量这个元方法的业务/技术收益?元方法的收益应该是=(单个收益*适用范围-推广成本)。</p>
<p>继续思考,元元方法的产生。我们来讨论第二层是怎么产生的。第二层,如果有很多个类似签证的业务方,比如邮轮,机票,酒店,电话卡,都要定制他们自己的下单组件进来,怎么办?核心思想就是解耦,但是又不仅仅是解耦。比如从解耦层次上,可能是只做数据层解耦,样式定死,支持一部分样式相同的组件的快速部署。又可能是样式、数据同时解耦,还可能是组件级别解耦,业务方根据协议生成组件,交付下单页面渲染布局。</p>
<p>这里可以插一句,内聚/解耦的思想,是业务开发的核心方法论。业务开发面对底层开发在跟你炫耀 OpenGL,xx 算法模型,xx 编译原理的时候,可以先说你说的我也略懂,然后再问一句,那么,你懂xx业务解耦/组件化/动态布局/行业赋能/业务大脑吗?</p>
<p>好,到第二层元元方法就为止了吗?其实不止。必然会有第三层,元元元方法,比如解耦要解到什么地步。假设你确定了这个下单页面可以解耦到组件层级,那明年呢?明年组件层级够用吗?这是时间维度。空间维度呢?换商品详情业务,解耦到什么层级呢?假设我们说商品详情业务可以解耦到数据层级,那么为什么下单可以解耦到组件层级,商品详情业务只能解耦到数据层级?能不能创建出一个推论模式,通过几个条件来判断一个业务究竟能解耦到哪个层级?</p>
<p>前面说的都是业务维度,再从技术一点的维度来说,移动 App 页面开发总共可以归纳为几种形式的解耦,这些解耦各自有几种技术方案来实现。能否把解耦这事情也抽象成一个方案,使得一个业务只需要很低的开发/接入成本就可以切换成解耦型方案?事实上,Weex/Rax/H5也基本等同于是一个直接可插入的组件级别解耦方案了。只是这种大型通用的技术方案,已经不能称之为技术方案了,大家习惯性地把他看成了一个可以吃十几年饭的技术栈。但当我们缩小通用性领域,提高对某一类问题的专业度和适用度的时候,我们必然可以得到一个好的行业解决方案。</p>
<p>第四层...好了,不说了,事实上抽象到第三层已经适用大多数场景了。但这并不意味着,第三层的元元元方法很少,事实上,很多。当我们在讨论 n+1 层的元方法的时候,我们必然是舍弃了 n 层中的一些信息,来获取<br>n1,n2,n3....中的一些公有的共同点。那么,舍弃A、B、C,取 D,和舍弃A、C、D,取 B,他们得到的 n+1层的元方法都是不一样的。所以,元方法无穷无尽,每上一个维度,他的方法空间的大小也会膨胀一个维度。从一个二维的平面顿时变成了一个三维的空间。所以,抽象的世界比真实世界更复杂,只要我们未曾停下对真实世界、传统业务的扩张和改造,抽象的空间永远存在,人的思考永远有价值。</p>
<p>结尾来一个类似自举一般的奇妙总结。这篇本身,也是一个元方法。</p>
Android Weex 自定义 Component 指北
https://segmentfault.com/a/1190000015893922
2018-08-05T21:05:45+08:00
2018-08-05T21:05:45+08:00
DesGemini
https://segmentfault.com/u/desgemini
1
<p>Weex 自定义 Component 开发这块,官方文档和网上示例都较少涉及。工作所需有所研究,总结此文以飨读者。</p>
<h2>基础定义与注册</h2>
<p>如下述代码所示,从 WXComponent 继承出来以后,复写四个构造器方法,就可以完成一个最简单可跑当然也显示不了任何东西的 WXComponet。</p>
<p>需要说明的是 WXComponent 可以指定泛型 T,T extends View,用于指定WXComponent hostView 根布局的类型。这个还是指定的比较好,在某些进阶用法中会需要这个类型。</p>
<pre><code class="java">public class DemoWXComponent extends WXComponent<T> {
public DemoWXComponent(WXSDKInstance instance, WXVContainer parent,
int type,
BasicComponentData basicComponentData) {
super(instance, parent, type, basicComponentData);
}
public DemoWXComponent(WXSDKInstance instance, WXVContainer parent, String instanceId, boolean isLazy,
BasicComponentData basicComponentData) {
super(instance, parent, instanceId, isLazy, basicComponentData);
}
public DemoWXComponent(WXSDKInstance instance, WXVContainer parent, boolean isLazy,
BasicComponentData basicComponentData) {
super(instance, parent, isLazy, basicComponentData);
}
public DemoWXComponent(WXSDKInstance instance, WXVContainer parent,
BasicComponentData basicComponentData) {
super(instance, parent, basicComponentData);
}
}</code></pre>
<p>使用这个 Component 之前,还需要把他注册进 WXSDKEngine。如下所示:<br>一次注册后,在Android程序销毁之前,可以一直使用这个 Component。无需 unregister,WXSDKEngine 也没有提供 unregister 方法,这是显而易见的,因为当前还未产生任何实例。</p>
<pre><code class="java">WXSDKEngine.registerComponent("democomponent", DemoWXComponent.class);</code></pre>
<p><strong>值得一提的是 componet 名称,尽量不要下划线中划线和大写字母,否则可能会踩坑。</strong></p>
<p>好,接下来是 Weex JS 代码怎么调用。无需 import,直接使用,只要 register 方法已经被执行过了,如下所示:</p>
<pre><code class="js"><democomponent /></code></pre>
<p>如果想要设定 props 怎么办,如 <democomponent source=test/>。</p>
<p>那就在 Componet 中增加如下:</p>
<pre><code class="java">@WXComponentProp(name = "source")
public void setSource(String source) {
mSource = source;
// or do some things
}</code></pre>
<p>weex 会根据外部传入的 props,根据注解调用对应 props 的 set 方法。</p>
<h2>生命周期</h2>
<p>显然,看了第一节,只能保证链路上 weex 自定义 Component 能跑起来,没有做其他任何事情。那么,为了能实现我们需要的渲染和其他逻辑,就需要了解 Weex Componet 的生命周期。这里的生命周期,实质就是了解可 Override 的几个 WXComponent 方法,和他们的被调用的时机。这一块官方没有任何文档,全靠去 github 源码中看和试。</p>
<h3>必需 Override</h3>
<h4>initComponentHostView()</h4>
<pre><code class="java">protected T initComponentHostView(@NonNull Context context) {
// for example
mComponentHostView = new FrameLayout(context);
mComponentHostView.setId(R.id.fragment_content);
return mComponentHostView;
}</code></pre>
<p>用于生成根 View 返回给 Weex 来渲染。注意,不要在这里进行任何响应外界设入的 props 的渲染,因为此时极大可能 props 还没有被传入。</p>
<h3>可选 Override</h3>
<h4>bindData()</h4>
<pre><code class="java">@Override
public void bindData(WXComponent component) {
super.bindData(component);
// 这里进行 props 的响应渲染
}</code></pre>
<p>super.bindData() 后即可响应 props 进行渲染,因为此时 props 的set方法都已经被调用过。</p>
<h4>destroy()</h4>
<pre><code class="java">@JSMethod
@Override
public void destroy() {
super.destroy();
// 进行自定义 Component 的必要销毁逻辑
}</code></pre>
<p>如果有额外需要销毁的逻辑,需要写在 destroy 之中。weex 会在退出 WXActivity 或其他等同的时候调用。值得一提的是,我一般加一个 @JSMethod 注解,以提供前端 Weex 开发一个主动销毁的能力,避免需要的时候不能及时推代码生效,而要等到发版。</p>
<h2>暴露方法</h2>
<p>上段其实已经提到,怎样暴露一个 Component 方法给前端调用。如下所示:</p>
<pre><code class="java">@JSMethod
public void getDuration(JSCallback callback) {
if (null != getCurrentShortVideoVh() && null != callback) {
Map<String, Object> map = new HashMap<>(1);
map.put("result", "value");
callback.invoke(map);
}
}</code></pre>
<p>需要注意的是,直接把 void 改成返回值比如 boolean 然后试图 return 是没有用的,weex js 侧收不到。因此,必须要去使用回调来给返回值。如上所示。</p>
<h2>DOM</h2>
<p>Weex 新内核(WeexCore)将 Dom 层和 Layout 引擎下沉到 C++ 层实现,移除 Java 层的 DomObject,提升渲染性能和内核的可通用性。因此,github 最新版不再可以获取到 WXComponent 中的 DomObject。</p>
<h2>Tricks:强转</h2>
<p>如果发现自定义 Component 的逻辑需要用到 Activity,而 WXComponent 只给你提供了 Context 的时候,不要慌,Weex 传入的 Context 其实可以强转 Activity。当然,以防万一,记得用 instance of 保护一下。</p>
<p>同理,如果你想要弹出一个 Fragment,结果发现自己需要一个 FragmentActivity 来getSupportFragmentManager(),不要慌,weex 传入的这个 Activity 也可以强转为<br>FragmentActivity,同样记得加 instance of 保护,否则业务挂了不算我的,因为这毕竟是文档中的未定义行为。</p>
Android Activity间通信的序列化过程中深浅拷贝的讨论
https://segmentfault.com/a/1190000011575464
2017-10-16T16:31:07+08:00
2017-10-16T16:31:07+08:00
DesGemini
https://segmentfault.com/u/desgemini
0
<p>问题的背景是,视频互动业务需要增加弹幕功能,但是播放器的视图是伪横屏的,即,他是一种类似于使用 rotate(90.0)的方式,旋转横屏的,在 Activity 层面上还是一个竖屏的状态。那么弹幕输入的时候的键盘,也是竖屏的。这会带来比较严重的用户体验问题。</p>
<p>由于屏幕旋转状态在 android 下,是一个 Activity 层面上的事情,而且相当的底层,无从 hook,多方调研以后,决定采拉起一个横屏的 Activity 作为键盘输入的专用 Activity。这里的代码很快就可以写好,如下所示。</p>
<pre><code class="java">
/**
* Created by DesGemini on 12/09/2017.
*/
public class DialogActivity extends Activity {
private RelativeLayout mContentView;
private View vSendBtn;
private EditText etDanmakuInput;
private InputMethodManager mInputMethodManager;
public static WeakReference<DanmakuWriteCallback> danmakuWriteCallback = new WeakReference<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mInputMethodManager = (InputMethodManager)this.getSystemService(Context.INPUT_METHOD_SERVICE);
mContentView = (RelativeLayout) getLayoutInflater()
.inflate(R.layout.hiv_danmaku_input_dialog, null);
vSendBtn = mContentView.findViewById(R.id.tv_danmaku_send_btn);
etDanmakuInput = (EditText) mContentView.findViewById(R.id.et_danmaku_input);
vSendBtn.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// 这里需要处理 Activity 间回传逻辑
}
});
setContentView(mContentView);
showSoftKeyboard();
}
private boolean showSoftKeyboard() {
if (this.etDanmakuInput == null) {
return false;
} else {
etDanmakuInput.postDelayed(new Runnable() {
public void run() {
etDanmakuInput.requestFocus();
mInputMethodManager.showSoftInput(etDanmakuInput, 0);
}
}, 100L);
return true;
}
}
@Override
protected void onPause() {
super.onDestroy();
danmakuWriteCallback.getAndSet(null);
}
@Override
public void finish() {
super.finish();
}
}
</code></pre>
<p>DTO 的代码定义如下所示:</p>
<pre><code class="java">public class DanmakuDialogDTO implements Serializable {
public WeakReference<DWDanmakuWriteController.DanmakuWriteCallback> callback;
public Map<String, String> utExtraParams;
}</code></pre>
<p>那么现在问题来了,怎么把这个 Activity 获取到的 String 带回去?</p>
<p>最自然的想法是 onActivityResult,然而,播放器是一个 sdk,写不了 Activity 里的代码,也不可能通知许多业务方一一做改动。</p>
<p>那就只能抛开 android 原生的 Activity 间拉起结束中的通信机制了,思考其他可以通信的方法。很自然地,我们想到了 Callback 。结构如下图。但是 Callback 这样的一个非基本数据类型的对象怎么在 Activity 间传递呢?<br><img src="/img/remote/1460000011575467?w=407&h=650" alt="danmaku_structure" title="danmaku_structure"></p>
<p>尝试通过存入 Intent 的 Extras的方式,然而 putExtra 方法并不能 put 一个 object,只能 put 一个 serializable。那就让这个 DTO(Data Transfer Object)implements serializable 接口吧。没有问题。</p>
<p>然而无法启动 Activity,会有一个 crash 抛出:</p>
<p>java.lang.NullPointerException: Expected to unbox a 'int' primitive type but was returned null</p>
<p>报错堆栈如下:<br>$Proxy1.startActivity(Unknown Source)<br>android.app.Instrumentation.execStartActivity(Instrumentation.java:1520)<br>android.taobao.atlas.runtime.InstrumentationHook$2$1.execStartActivity(InstrumentationHook.java:299)</p>
<p>如果把这个 DTO 的成员变量改为 static 类型,则可以启动 Activity。</p>
<p>背后的原因是因为,在常规的序列化过程中,浅拷贝其实是没什么意义的。浅拷贝意味着复制一个引用的地址,是一个内存地址,但是常规序列化,要么跨进程,要么就是网络传输,序列化为 JSON,在这些常规场景里内存地址没有意义。因此 Java 序列化没有浅拷贝的选项,也往往是针对一个 POJO 或者 Bean 进行序列化,而不会对一个一般的含有很多引用的类进行序列化。</p>
<p>然而 Android 中的 Activity 与 Activity 间的传递对象又有所不同,理论上,都在同一个 Dalvik VM 中运行,相互的类引用都是可以访问到的。但是由于 Android Intent 设计为序列化传递,序列化过程中没有设计浅拷贝的机制,因此就无法浅拷贝地传递引用过去。</p>
<p>那么为什么设为 static 以后就可以传递,不会导致 crash 了呢?是因为静态成员属于类级别的,虽然不能序列化,但是因为我是在同一个机器(而且是同一个进程),我的jvm已经把这个类连带着他的静态变量一起加载进来了,所以获取到的是类层面上的静态变量地址,故,功能正常。</p>
<p>那么就决定是使用<code>public static WeakReference<DWDanmakuWriteController.DanmakuWriteCallback> callback;</code>了。但是事实上遇到了另一个问题:</p>
<p>在第一次 startActivity 的时候,观察到 Android 做了一次 GC,然后该 WeakReference 就被释放了,因此 Callback 的业务功能也不能正常执行。引入 WeakReference,原本是为了避开 static cakllback 导致的可能的内存泄漏,然而在这种主动 GC 的情况下,WeakReference 失效了。如果改用 SoftReference,和强引用并没有什么区别,都不能避免内存的泄漏。</p>
<p>最终,采用 AtomReference 来持有这个 static callback,在 Activity 退出的时机去将 AtomicReference 置空。之所以使用 AtomicReference,是因为考虑到视频 sdk 有并发场景的可能性,避免一边置 null 另一边准备使用的可能。</p>
Node.js + React Native 毕设:农业物联网监测系统的开发手记
https://segmentfault.com/a/1190000007082825
2016-10-06T21:19:10+08:00
2016-10-06T21:19:10+08:00
DesGemini
https://segmentfault.com/u/desgemini
7
<p>毕设大概是大学四年里最坑爹之一的事情了,毕竟一旦选题不好,就很容易浪费一年的时间做一个并没有什么卵用,又不能学到什么东西的鸡肋项目。所幸,鄙人所在的硬件专业,指导老师并不懂软件,他只是想要一个农业物联网的监测系统,能提供给我的就是一个Oracle 11d数据库,带着一个物联网系统运行一年所保存的传感器数据...That's all。然后,因为他不懂软件,所以他显然以结果为导向,只要我交出一个移动客户端和一个服务端,并不会关心我在其中用了多少坑爹的新技术。</p>
<p>那还说什么?上!我以强烈的恶搞精神,决定采用业界最新最坑爹最有可能烂尾的技术,组成一个 Geek 大杂烩,幻想未来那个接手我工作的师兄的一脸懵逼,我露出了邪恶的笑容,一切只为了满足自己的上新欲。</p>
<p>全部代码在 GPL 许可证下开源:</p>
<ul>
<li><p>服务端代码:<a href="https://link.segmentfault.com/?enc=jRGiadMxUGfi2kkNjX1WAw%3D%3D.2%2F6y9NBdoR9MgGfLQhUKvrFrAUxskel8Ta1RcW6eGUiBHcEnepDqp%2BxS7qK7OYko" rel="nofollow">https://github.com/CauT/the-wall</a></p></li>
<li><p>客户端代码:<a href="https://link.segmentfault.com/?enc=J9UaAyG19vkF8%2FmlsiUwmQ%3D%3D.E8TigIjGo4ndZ2c3XXpg%2FOSroXe4%2FoVsfsKUFF0TgDV8QZPI6BK3ZVhRwalN8jbq" rel="nofollow">https://github.com/CauT/Night...</a></p></li>
</ul>
<p>由于数据库是学校实验室所有,所以不能放出数据以供运行,万分抱歉~。理论上应该有一份文档,但事实上太懒,不知道什么时候会填坑~。</p>
<h2>总体架构</h2>
<p>OK,上图说明技术框架。</p>
<p><img src="/img/remote/1460000007083094?w=1062&h=924" alt="总体结构" title="总体结构"><br><br>该物联网监测系统整体上可分为三层:数据库层,服务器层和客户端层。</p>
<h3>数据库和代码层</h3>
<p>数据库层除了原有的Oracle 11d数据库以外,还额外增加了一个Redis数据库。之所以增加第二个数据库,原因为:</p>
<ol>
<li><p>Node.js 的 Oracle 官方依赖 node-oracledb 没有ORM,也就是说,所有的对数据库的操作,都是直接执行SQL语句,简单粗暴,我担心自己孱弱的数据库功底(本行是 Android 开发)会引发锁表问题,所以通过限制只读来避开这个问题。</p></li>
<li><p>由于该系统服务于农业企业的内部管理人员,因此其账号数量和总体数据量必然有限,因此使用 redis 这种内存型数据库,可以不必考虑非关系型数据库在容量占用上的劣势。读取速度反而较传统的 SQL 数据库有一定的优势。</p></li>
<li><p>使用非关系型数据库比关系型数据库好玩多了(雾</p></li>
<li><p>之所以写了右边的Git部分,是因为原本打算利用docker技术搞一个持续集成和部署的程序,实现提交代码=>自动测试=>更新服务器部署更新=>客户端自动更新 这样一整套持续交付的流程,然而最后并没有时间写。</p></li>
</ol>
<h3>服务器层</h3>
<p>服务器层,采用 Node.js 的 Express 框架作为客户端的 API 后台。因为 Node.js 的单线程异步并发结构使之可以轻松实现较高的 QPS,所以非常适合 API 后端这一特点。其框架设计和主要功能如下图所示:</p>
<p><img src="/img/remote/1460000007083095?w=1014&h=730" alt="服务端结构" title="服务端结构"></p>
<p>像网关层:鉴权模块这么装逼的说法,本质也就是<code>app.use(jwt({secret: config.jwt_secret}).unless({path: ['/signin']}));</code>一行而已。因为是直接从毕业论文里拿下来的图,毕业论文都这尿性你们懂的,所以一些故弄玄虚敬请谅解。</p>
<h3>客户端层</h3>
<p>客户端层绝大部分是 React Native 代码,但是监控数据的图表生成这一块功能(如下图),由于 React Native 目前没有开源的成熟实现;试图通过 Native 代码来画图表,需要实现一个 Native 和 React Native 互相嵌套的架构,又面临一些可能的困难;故而最终选择了内嵌一个 html 页面,前端代码采用百度的 Echarts 框架来绘制图表。最终的结构就是大部分 React Native + 少部分 Html5 的客户端结构。</p>
<p>另外就是采用了 Redux 来统一应用的事件分发和 UI 数据管理了。可以说,React Native 若能留名青史,Redux 必定是不可或缺的一大原因。这一点我们后文再述。</p>
<h2>细节详述</h2>
<h3>服务端层</h3>
<p>服务端接口表:</p>
<p><img src="/img/remote/1460000007083096?w=938&h=1398" alt="服务端接口表" title="服务端接口表"></p>
<p>服务端程序的编写过程中,往往涉及到了大量的异步操作,如数据库读取,网络请求,JSON解析等等。而这些异步操作,又往往会因为具体的业务场景的要求,而需要保持一定的执行顺序。此外,还需要保证代码的可读性,显然此时一味嵌套回调函数,只会使我们陷入代码几乎不可读的回调地狱(Callback Hell)中。最后,由于JavaScript单线程的执行环境的特性,我们还需要避免指定不必要的执行顺序,以免降低了程序的运行性能。因此,我在项目中使用Promise模式来处理多异步的逻辑过程。如下代码所示:</p>
<pre><code class="js">function renderGraph(req, res, filtereds) {
var x = [];
var ys = [];
var titles = [];
filtereds[0].forEach(function(row) {
x.push(getLocalTime(row.RECTIME));
});
filtereds.forEach(function(filtered){
if (filtered[0] == undefined)
// even if at least one of multi query was succeed
// fast-fail is essential for secure
throw new Error('数据库返回结果为空');
var y = [];
filtered.forEach(function(row) {
y.push(row.ANALOGYVALUE);
});
ys.push(y);
titles.push(filtered[0].DEVICENAME + ': ' + filtered[0].DEVICECODE);
});
res.render('graph', {
titles: titles,
dataX: x,
dataY: ys,
height: req.query.height == undefined ? 200 : req.query.height,
width: req.query.width == undefined ? 300 : req.query.width,
});
}
function resFilter(resolve, reject, connection, resultSet, numRows, filtered) {
resultSet.getRows(
numRows,
function (err, rows)
{
if (err) {
console.log(err.message);
reject(err);
} else if (rows.length == 0) {
resolve(filtered);
process.nextTick(function() {
oracle.releaseConnection(connection);
});
} else if (rows.length > 0) {
filtered.push(rows[0]);
resFilter(resolve, reject, connection, resultSet, numRows, filtered);
}
}
);
}
function createQuerySingleDeviceDataPromise(req, res, device_id, start_time, end_time) {
return oracle.getConnection()
.then(function(connection) {
return oracle.execute(
"SELECT\
DEVICE.DEVICEID,\
DEVICECODE,\
DEVICENAME,\
UNIT,\
ANALOGYVALUE,\
DEVICEHISTROY.RECTIME\
FROM\
DEVICE INNER JOIN DEVICEHISTROY\
ON\
DEVICE.DEVICEID = DEVICEHISTROY.DEVICEID\
WHERE\
DEVICE.DEVICEID = :device_id\
AND DEVICEHISTROY.RECTIME\
BETWEEN :start_time AND :end_time\
ORDER\
BY RECTIME",
[
device_id,
start_time,
end_time
],
{
outFormat: oracle.OBJECT,
resultSet: true
},
connection
)
.then(function(results) {
var filtered = [];
var filterGap = Math.floor(
(end_time - start_time) / (120 * 100)
);
return new Promise(function(resolve, reject) {
resFilter(resolve, reject,
connection, results.resultSet, filterGap, filtered);
});
})
.catch(function(err) {
res.status(500).json({
status: 'error',
message: err.message
});
process.nextTick(function() {
oracle.releaseConnection(connection);
});
});
});
}
function secureCheck(req, res) {
let qry = req.query;
if (
qry.device_ids == undefined
|| qry.start_time == undefined
|| qry.end_time == undefined
) {
throw new Error('device_ids或start_time或end_time参数为undefined');
}
if (req.query.end_time < req.query.start_time) {
throw new Error('终止时间小于起始时间');
}
};
router.get('/', function(req, res, next) {
try {
var device_ids;
var queryPromises = [];
secureCheck(req, res);
device_ids = req.query.device_ids.toString().split(';');
for(let i=0; i<device_ids.length; i++) {
queryPromises.push(createQuerySingleDeviceDataPromise(
req, res, device_ids[i], req.query.start_time, req.query.end_time));
};
Promise.all(queryPromises)
.then(function(filtereds) {
renderGraph(req, res, filtereds);
}).catch(function(err) {
res.status(500).json({
status: 'error',
message: err.message
});
})
} catch(err) {
res.status(500).json({
status: 'error',
message: err.message
});
}
});</code></pre>
<p>这是生成指定N个传感器在一段时间内的折线图的逻辑。显然,剖析业务可知,我们需要在数据库中查询N次传感器,获得N个值对象数组,然后才能去用N组数据渲染出图表的HTML页面。 可以看到,外部核心的Promise控制的流程只集中于下面的几行之中:<code>Promise.all(queryPromises()).then(renderGraph()).catch()</code>。即,只有获取完N个传感器的数值之后,才会去渲染图表的HTML页面,但是这N个传感器的获取过程却又是并发进行的,由Promise.all()来实现的,合理地利用了有限的机器性能资源。</p>
<p>然而,推入queryPromises数组中的每个Promise对象,又构成了自己的一条Promise逻辑链,只有这些子Promise逻辑链被处理完了,才可以说整个all()函数都被执行完了。子Promise逻辑链大致地可以总结为以下形式:</p>
<pre><code class="js">function() {
return new Promise().then().catch();
}</code></pre>
<p>其中的难点在于:</p>
<ol>
<li><p>合理地切分整套业务逻辑到不同的then()函数中,且一个then()中只能有一个异步过程。</p></li>
<li><p>函数体内的异步过程所产生的新的Promise逻辑链必须被通过return的方式挂载到父函数的Promise逻辑链中,否则即可能形成一个有先有后的控制流程。</p></li>
<li><p>catch()函数必须要做好捕捉和输出错误的处理,否则代码编写过程中的错误即不可能被发现,异步编程的整个过程也就无从继续下去了。</p></li>
<li><p>值得一提的是Promise模式的引入。Node.js 自身不带有Promise,可以引入标准的ECMAScript的Promise实现,然而其功能较为简陋,对于各种API的实现过于匮乏,因此最后选择了bluebird库来引入Promise模式的语言支持。</p></li>
</ol>
<p>由此我们可以看到,没有无缘无故的高性能。Node.js 的高并发的优良表现,是用异步编程的高复杂度换来的。当然,你也可以选择不要编程复杂度,即不采用 Promise,Asnyc 等等异步编程模式,任由代码沦入回调地狱之中,那么这时候的代价就是维护复杂度了。其中取舍,见仁见智。</p>
<h3>客户端层</h3>
<p>客户端主要功能如下表所示:</p>
<p><img src="/img/remote/1460000007083097?w=1144&h=1404" alt="功能设计表" title="功能设计表"></p>
<p>接下来简单介绍下几个主要页面。可以发现 iOS 明显比 Android 要来的漂亮,因为只对 iOS 做了视觉上的细调,直接迁移到 Android 上,就会由于屏幕显示的色差问题,显得非常粗糙。所以,对于跨平台的 React Native App 来说,做两套色值配置文件,以供两个平台使用,还是很有必要的。</p>
<p><img src="/img/remote/1460000007083098?w=854&h=774" alt="当前数据界面" title="当前数据界面"></p>
<p>上图即是土壤墒情底栏的当前数据页面,分别在Android和iOS上的显示效果,默认展示所有当前的传感器的数值,可以通过选择传感器种类或监测站编号进行筛选,两个条件可以分别设置,选定后再点击查找,即向服务器发起请求,得到数据后刷新页面。由于React Native 的组件化设计,刷新将只刷新下侧的DashBoard部分,且,若有上次已经渲染过的MonitorView,则会复用他们,不再重复渲染,从而实现了降低CPU占用的性能优化。MonitorView,即每一个传感器的展示小方块,自上至下依次展示了传感器种类,传感器编号,当前的传感器数值以及该传感器显示数值的单位。MonitorView和Dashboard均被抽象为一个一般化,可复用的组件,使之能够被利用在气象信息、病虫害监测之中,提升了开发效率,降低了代码的重复率。</p>
<p><img src="/img/remote/1460000007083099?w=904&h=816" alt="查询历史界面" title="查询历史界面"></p>
<p>上图是土壤墒情界面的历史数据界面,分别在Android和iOS上的展示效果,默认不会显示数据,直到输入了传感器种类和监测站编号,选择了年月日时间后,再点击查找,才会得到结果并显示出来。该界面并非如同当前数据界面一样,Android和iOS代码完全共用。原因在于选择月日和选择时间的控件,Android和iOS系统有各自的控件,它们也被封装为React Native中不同的控件,因此,两条绿色的选择时间的按钮,被封装为HistoricalDateSelectPad,分别放在componentIOS和componentAndroid文件夹中。界面下侧的数据监测板,即代码中的Dashboard,是复用当前数据中的Dashboard。</p>
<p><img src="/img/remote/1460000007083100?w=922&h=840" alt="图表界面" title="图表界面"></p>
<p>上图是土壤墒情界面的图表生成界面,分别在Android和iOS上的展示效果。时间选择界面,查找按钮,选择框,均可复用前两个界面的代码,因此无需多提。值得说的是,生成的折线图,事实上是通过内嵌的WebView来显示一个网页的。图表网页的生成,则依靠的百度Echarts 第三方库,然后服务端提供了一个预先写好的前端模板,Express框架填入需要的数据,最后下发到移动客户端上,渲染生成图表。图表支持了多曲线的删减,手指选取查看具体数据点,放大缩小等功能。</p>
<p><img src="/img/remote/1460000007083101?w=442&h=730" alt="Screen Shot 2016-10-06 at 8.54.14 P" title="Screen Shot 2016-10-06 at 8.54.14 P"></p>
<p>上图则是实际项目应用中的Redux相关文件的结构。stores中存放全局数据store相关的实现。</p>
<p>actions中则存放根据模块切割开的各类action生成函数集合。在 Redux 中,改变 State 只能通过 action。并且,每一个 action 都必须是 Javascript Plain Object。事实上,创建 action 对象很少用这种每次直接声明对象的方式,更多地是通过一个创建函数。这个函数被称为Action Creator。</p>
<p>reducers中存放许多reducer的实现,其中RootReducer是根文件,它负责把其他reducer拼接为一整个reducer,而reducer就是根据 action 的语义来完成 State 变更的函数。Reducer 的执行是同步的。在给定 initState 以及一系列的 actions,无论在什么时间,重复执行多少次 Reducer,都应该得到相同的 newState。</p>
<h2>性能测试</h2>
<h3>服务端</h3>
<p>测试工具:OS X Activity Monitor(http_load)</p>
<p><img src="/img/remote/1460000007082972?w=936&h=202" alt="serve" title="serve"></p>
<h3>客户端</h3>
<h5>iOS</h5>
<p>测试工具:Xcode 7.3</p>
<p><img src="/img/remote/1460000007082973?w=936&h=206" alt="iOS" title="iOS"></p>
<h5>Android</h5>
<p>测试工具:Android Studio 1.2.0</p>
<p><img src="/img/remote/1460000007082974?w=936&h=206" alt="Android" title="Android"></p>
<h3>代码量相关</h3>
<p><img src="/img/remote/1460000007082975?w=936&h=304" alt="code" title="code"></p>
<h4>简单总结</h4>
<p>React Native 尽管在开发上具有这样那样的坑,但是因其天生的跨平台,和优于 Html5的移动性能表现,使得他在写一些不太复杂的 App 的时候,开发速度非常快,自带两倍 buff。</p>
enum、static final 与 IntDef:Android 中实现枚举的方案选择
https://segmentfault.com/a/1190000007073583
2016-10-05T00:07:46+08:00
2016-10-05T00:07:46+08:00
DesGemini
https://segmentfault.com/u/desgemini
5
<h2>前述</h2>
<p>曾经有一段时间,许多网上的 Android 性能调优的文章都提到,要尽量避免在 Android 中使用 enum,因为使用 enum 会引入较大的性能损失。</p>
<p>然而,最新的 Android 文档已经改变了这一说法。根据 Android VM 的开发者Elliot Hugues 的博客所述,过去的 Android 官网的性能优化指南并不准确,混杂了许多臆断。因此如今他们严格地依据事实,重写了<a href="https://link.segmentfault.com/?enc=dANIU2z5Km92HBzlhIMXHg%3D%3D.K5psAcdagP2s4bsHAeJ%2FgPLoJviu3TwU8WwRuyUXfn%2FlrTWcaZHpeZkTpNHgGRQ5vZal87wp0wAn4t%2Bifw6bYw%3D%3D" rel="nofollow">Android 性能优化指南</a>,开发者也应当以最新的文档为准。当然比较窘迫的是,Android 文档的更新并不是同时改口,事实上就在同个 training 目录下的 <a href="https://link.segmentfault.com/?enc=tI5mAEZZeGvKQ%2BtzbPd77A%3D%3D.GtpKptFQME4UukqKUeqXOhtsVT9F3p7h0ajfjcxNgkPhWTvOcuoF5UaHL7fi%2FwbY1dJGjaKBxYeW05Rg5pHXQQ%3D%3D" rel="nofollow">管理应用内存</a>一文中,就仍然保留了过时的避免使用 enum 的说法。</p>
<h2>最新的解释</h2>
<p>之所以重新鼓励使用 enum ,其解释是:</p>
<ol>
<li><p>Android 2.2 及以下系统上,使用 enum 的确会引发较大的性能损耗。主要是内存上的消耗,enum 远大于使用 static final int。</p></li>
<li><p>在 Android 2.3 及以后的系统中,之前的一些 enum 的性能问题已被 JIT 所优化。此时,虽然 enum 相比于 static final int,内存仍然有所增加,但已经是可以接受的了。加之 Android 2.2 到如今的 Android 7.0,Android 手机的内存配置突飞猛进,从256MB跃升至6GB,enum 所带来的内存增加已经可以忽略。</p></li>
</ol>
<h2>强内存依赖的应用的枚举实现</h2>
<p>尽管如此,在实际开发中仍然有可能遇到内存消耗较大的应用开发问题,那么此时,该如何优化枚举值的实现,以节约内存消耗呢?方案如下:</p>
<h3>直接使用 static final int</h3>
<p>然而,其问题在于,直接使用无法实现枚举变量赋值的类型安全。且无法把多个枚举值归纳到同一个枚举类型下。比如:</p>
<pre><code class="java">private static final int MONDAY = 0;
private static final int TUESDAT= 1;
private static final int WEDNESDAY = 2;
private static final int THURSDAY = 3;
private static final int FRIDAY = 4;
private static final int SATURDAY = 5;
private static final int SUNDAY = 6;
private static final int JANUARY = 7;
private int day = JANUARY;</code></pre>
<p>显然,此时 int 就未能实现赋值的类型检查,也未能把多个枚举值归纳到同一个枚举类型下。</p>
<h3>Android Proguard 优化</h3>
<p>在 Android Proguard 中,可以在 proguard.cfg 中加入参数 <code>-Doptimization class/unboxing/enum</code>,从而自动将 enum 替换为 static final int。这样,也就无需担心多余的内存问题了。</p>
<h3>使用 IntDef 注解替代 int</h3>
<p>IntDef 可以用于替代 int,其价值在于用<code>@IntDef int var</code>限定赋值范围,实现类型安全。还用<code> @IntDef({SUNDAY, MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY,SATURDAY})</code>归集了散乱的 static final int 变量,如下代码所示:</p>
<pre><code class="java">public class MainActivity extends Activity {
public static final int SUNDAY = 0;
public static final int MONDAY = 1;
public static final int TUESDAY = 2;
public static final int WEDNESDAY = 3;
public static final int THURSDAY = 4;
public static final int FRIDAY = 5;
public static final int SATURDAY = 6;
@IntDef({SUNDAY, MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY,SATURDAY})
@Retention(RetentionPolicy.SOURCE)
public @interface WeekDays {}
@WeekDays int currentDay = SUNDAY;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
setCurrentDay(WEDNESDAY);
@WeekDays int today = getCurrentDay();
switch (today){
case SUNDAY:
break;
case MONDAY:
break;
case TUESDAY:
break;
case WEDNESDAY:
break;
case THURSDAY:
break;
case FRIDAY:
break;
case SATURDAY:
break;
default:
break;
}
}
public void setCurrentDay(@WeekDays int currentDay) {
this.currentDay = currentDay;
}
@WeekDays
public int getCurrentDay() {
return currentDay;
}
}</code></pre>
<p>然而,IntDef 的缺点在于无法优雅地把 int 转为 IntDef,尤其在一个枚举值是服务端下发的时候。强行的实现会变的极为尴尬:</p>
<pre><code class="java">@WeekDays
public int getDay(int value) {
switch (value){
case 0:
return SUNDAY;
case 1:
return MONDAY;
case 2:
return TUESDAY;
case 3:
return WEDNESDAY;
case 4:
return THURSDAY;
case 5:
return FRIDAY;
case 6:
return SATURDAY;
}</code></pre>
<p>此时,在枚举值较少的时候还能忍,较多的时候代码就会变得非常丑陋。本质是因为,@IntDef 缺少像 <code>Enum.values()</code> <code>Enum.ordinal()</code> 等等 int 与 enum 与 String 互转的方法,因此在想换转换较多的场景下,不如采取第二种优化方法。</p>
<p>====================================</p>
<p><strong>如果您觉得我的文章对您有所启迪,请点击文末的推荐按钮,您的鼓励将会成为我坚持写作的莫大激励。 by DesGemini</strong></p>
非全屏 Weex 页面开发中的 Android 适配
https://segmentfault.com/a/1190000007073531
2016-10-05T00:02:49+08:00
2016-10-05T00:02:49+08:00
DesGemini
https://segmentfault.com/u/desgemini
5
<p>weex代码中的高度和宽度的单位均为px,然而,在手机屏幕上显示的高宽却不一定与代码指定的相同。原因是weex框架在底层做了针对不同屏幕的适配工作,具体计算公式为 实际高宽 = 代码高宽 * (屏幕宽度 / 750)。</p>
<p>举个例子,假设代码中是这么写的:</p>
<pre><code class="css"><style>
.button {
height: 100;
width: 200;
}
</style></code></pre>
<p>那么,在一款屏幕分辨率为1920*1280的Android手机上,此时的计算过程为:<br><strong>height</strong>: 100 * (1080 / 750) = 144;<br><strong>width</strong>: 200 * (1080 / 750) = 288。</p>
<p>如果我们开发的weex页面是全屏幕的,那么这个高宽的转换过程对我们而言是透明的,无需做额外的工作。然而一旦有一个业务场景,weex容器并非是全屏幕的,而是需要从外部传入weex容器的高度,那么,就不得不考虑这个转换的过程。</p>
<p>举一个我在开发weex弹窗时的例子。该weex弹窗的样式如下:</p>
<p><img src="/img/remote/1460000007073552?w=750&h=1334" alt="weex-blog" title="weex-blog"></p>
<p>可以看到,如果不考虑多屏幕适配,顶栏和底栏都是一个固定值,那么只需要用总容器高度 - 两个定高组件就可以了。那么需要解决的第一个问题,就是如何获取外部容器的高度。由于weex可以通过<code>$getConfig().env.deviceHeight</code>和<code>$getConfig().env.deviceWidth</code>的形式来获取手机屏幕的高度,因而,很自然地就想到,是否能在安卓中以屏幕的3/5的比例,约定容器高度,然后在weex代码中,同样通过3/5来计算容器高度。这样就避免了去写 Native Module 和 Method。</p>
<p>然而,这样的思路是不可行的。因为Android Native的总高度,事实上是可供显示的全屏高度,而不一定是物理屏幕的高度,因为有状态栏,虚拟按键栏,Smartbar等等安卓碎片化引入的额外显示元素,实际全屏高度很有可能小于物理屏幕高度。所以,仍然需要开发和注册Native Module,以获取外部容器高度。</p>
<p>再来看上文的计算公式:总容器高度 - 两个定高 = scroller高度。因为多屏幕适配的原因,上面的公式是不可行的,需要改为:</p>
<p>外部传入的总容器高度 - 两个定高组件的高度字面量 * 转换比例 = scroller实际高度</p>
<p>也就是说:外部传入的总容器高度 / 转换比例 - 两个定高组件的高度字面量 = scroller实际高度 / 转换比例 = scroller的字面量高度。</p>
<p>所以,最终的业务代码如下所示:</p>
<pre><code class="js"> ready:function() {
...
// 引入外部注册的 Native Module;Android 和 iOS 各有其实现
var AppInfo = require('@weex-module/MSOAFoundation');
if (this.$getConfig().env.platform != "iOS") {
// 适配 Android
this.mainExtra = "mainExtraAndroid";
AppInfo.getContainerHeight(function(params) {
ratio = this.$getConfig().env.deviceWidth / 750;
this.scrollerHeight = params.height / ratio - 200;
}.bind(this));
} else {
// 适配 iPhone 4S
if (this.$getConfig().env.deviceHeight < 1000) {
this.scrollerHeight = 700;
}
}
...
}</code></pre>
<p>这个坑非常的隐蔽,本质是因为:weex 默默做了A参考系转换到B参考系的过程,然而一旦我们自力更生,试图从B参考系获得一个测量得到的高度,用在A参考系,而没意识到这个隐蔽的转换过程的时候,就会陷入到一台机子上调好了,另一台又跪了的尴尬局面。而且,这种情况在Android上远较iOS要来的严重。因为iOS上,除了4S以外,5,5s,6,6p,6s,6sp,屏幕尺寸均为同一长宽比。因此,在一台上调整好后,可无缝等比例放大到其他机型上。然而在Android上,毋论碎片化的屏幕尺寸,光status bar,navigation bar,smartbar等等虚拟的占用实际显示区域的各类bar,就足够让weex的默认适配喝一壶的。因此,weex这种隐蔽适配的处理方式,在Android生态上是否真的合理方便,尚待商榷。</p>
<p>====================================</p>
<p><strong>如果您觉得我的文章对您有所启迪,请点击文末的推荐按钮,您的鼓励将会成为我坚持写作的莫大激励。 by DesGemini</strong></p>
React Native 蛮荒开发生存指南
https://segmentfault.com/a/1190000004910600
2016-04-10T19:09:29+08:00
2016-04-10T19:09:29+08:00
DesGemini
https://segmentfault.com/u/desgemini
15
<h2>引言</h2>
<p>React Native,在过去一年大红大紫,一下子成为了许多人追捧的新兴技术热点。然而,除却蜻蜓点水般运行一个 Hello World 式的 React Native 小 App,真正想要用 React Native 写一个商用的 App,却要面临很多困难。其中最主要的,就是缺少技术资料,缺少像 Android 这样发展七八年的技术一般,在博客和技术社区上存留的大量的技术资料。因而,<strong>面向百度编程,面向 Google 编程,面向 StackOverFlow 编程</strong>这三大杀手锏,均对 React Native 开发中遇到困惑表示无可奈何。加之 Facebook 开发组的文档更新速度远远跟不上开发的速度,使得 React Native 的工程化应用之路,恍若在蛮荒生存。笔者不才,为某一商业项目开发 React Native App 已近半年,以自己的踩坑和爬坑经验撰写此文,取名蛮荒开发生存指南XD。</p>
<h2>文档</h2>
<p><strong>1. 立足 React Native 英文文档,参考中文翻译的文档。再简陋的文档好歹也是文档。</strong></p>
<p><a href="https://link.segmentfault.com/?enc=an7ZHaNYlbPRg57xqGEqfw%3D%3D.pTUGu4QsQQPVkV0JLvu9KyJR6HDDksNLwU9h%2Bmq58xV1Sz%2BTJfaeuruihBlEdiDaNbxHtYIpT3i7JwO%2FWN0%2F4i%2Bl7Yw2euhPzjBY9OZvN%2BM%3D" rel="nofollow">官方英文文档地址在这里</a>。官方文档需要注意的是,左上角有一个蓝色的版本号,点击可以翻阅过去版本号的文档。文档中有写到的技术点肯定都已经在这个版本实现了,但文档没写的技术点,则有可能也实现了,只是没写上去。</p>
<p>目前找到的 React Native 中文文档有两份:一份是<a href="https://link.segmentfault.com/?enc=AGkwZxTt88CYARac5oAelA%3D%3D.fLNvUA8%2FfjU2lBb2RztjKAWGpjj3%2F%2BL%2FAUwMJeyDQvNzHCTZpj9anFtYO8K5U0ShLRi0Rn%2FNA5VbMY61JTshEw%3D%3D" rel="nofollow"> React Native 中文网</a>的,另一份是<a href="https://link.segmentfault.com/?enc=jdme9PV50JaiMX%2FnwxT0rA%3D%3D.Vy3qI4fr2XCczbS46s7hmvQvfNxxM2A7e8wiJBn6BJ%2FPr%2FAW5mV9%2BmK8r9SwI8PSYHTPuDHI8HOsiC0iOvM7ZA%3D%3D" rel="nofollow">极客学院</a>上的。前者一直在持续更新,后者似乎已经很久没有更新了。可在拿不准英文文档意思的时候作为参考。</p>
<p><strong>2. React 文档同样重要。</strong></p>
<p>由于 React Native 实质上是 React.js 的开发思想在移动端的实现,因此,许多如 Component, Props, State, flux 等等概念,在 React Native 的文档中均没有提及,相反在 React 的文档中有着详细的讲解。关于 React.js 和 React Native 之间的关系,知乎上<a href="https://link.segmentfault.com/?enc=Qn937J%2BRuQdsJ4kBFuQUIQ%3D%3D.0%2BKeMyGatVEBIb18%2FPNidBzgHq10UAREQjyMk9M%2BB7f5MNktoO%2BZ5Kzayla31bxVCXyCyCPHAKA5%2Fep%2B2BIlUQ%3D%3D" rel="nofollow">这篇回答</a>讲的鞭辟入里,值得深思。React 官方英文文档地址<a href="https://link.segmentfault.com/?enc=3iUTMat%2BqcXs0oaS%2FH2idw%3D%3D.11N9Q4QtXCQWOD7XVY9vO%2Fa7vULhZtSNyhb3uUfI2v530RSGY4Bp3UrE6XGlVD82Qvl3Fs%2ByaZBsTYaUGp7P1Q%3D%3D" rel="nofollow">在这里</a>,国内志愿者翻译的中文文档<a href="https://link.segmentfault.com/?enc=DsFY8PvWaJyamuJNAkMb7g%3D%3D.JaqEJwKezXCgyl5oXbdDpj%2BxzYwZNWattHpe%2F6FGTytFM2GiHbu1rIM%2FZQdyMNS8Mpjt%2FXe%2BdRgTbzqa8KdvFw%3D%3D" rel="nofollow">在这里</a>。</p>
<p><strong>3. 搜索文档的方法</strong></p>
<p>由于上述文档中除 React Native 文档以外,其他文档均无配置文档搜索框。因此,有必要使用 Google 或 Baidu 加上 site:url 来全局搜索文档。</p>
<h2>社区</h2>
<p><strong>4. 像对待官方文档一样认真阅读 React Native 所有版本的 release note。</strong></p>
<p>React Native 文档更新速度缓慢,且不能保证覆盖所有的 feature。与之相反,release note 则会告知你新的 feature 和 bug fix,虽然很多只有一句话,但是真正有帮助的是,release note 会给出相关的 commit 链接,从而我们可以阅读代码和注释,以此来了解该 feature 或 bug fix 的内容。但是同理,总会有一些改动没有统计到 release note 中。深深的怨念...</p>
<p><strong>5. 不要对 StackOverFlow 抱太大期望。</strong></p>
<p>截至目前,StackOverFlow 上 React Native 相关的,且得到了满意回答的问题寥寥无几,而且大多集中于 React Native 开发环境搭建等入门踩坑问题上。这种情况是完全可以预料到的,因为从本质来说,类似 StackOverFlow 这种问答社区的优质问答积累需要漫长的时间,何况技术的细节无穷无尽,非数年之功不能处处兼顾。</p>
<p>这是针对搜索现有问答来说,StackOverFlow 无法满足 React Native 的开发问题。同理,如果自己发帖提问,同样不能保证快速地得到满意的解答,我认为最关键的问题在于,现有的 React Native 开发者的活跃社区,不在 StackOverFlow,而在 Github 上。因此,引出第六条指南。</p>
<p><strong>6. 在 Github issue 中搜索出现的问题的关键字。</strong></p>
<p>react native 的<a href="https://link.segmentfault.com/?enc=%2BKH2F9%2F%2BBJJbGvjBZHbI6Q%3D%3D.LFi729WlcUVga1w6PMJX8x%2FFsGV3%2FzhGrHQGyLS0qR0NzhvK1wQ22vECO2ZeOsI3" rel="nofollow">Github Issue</a>中的问题和解决极为丰富,迄今已有4000多条 issue,与 StackOverFlow 判若云泥。然而由于其是论坛的性质,因此需要耐心阅读英文对话内容,才可能找到解决的方法。此外,如不能找到相关内容,另开一个 issue 寻求帮助也不失为一种良策。</p>
<p><strong>6. 遇到无法解决的问题,就升级 React Native 版本。</strong></p>
<p>虽然很无脑,但的确有时候很有奇效。比如笔者在实现 React Native 内嵌 WebView 时,React Native for Android v0.18.0中 WebView 的 javaScriptEnabled 属性即便设置为 true 也依然无效。升级到 v0.22.0 即解决该问题。<strong>然而,从v0.19.0 到 v0.22.0 的 release note</strong> 对该 bug fix 根本没提 T_T。</p>
<p><strong>7. 阅读源代码,是求生的最后工具。</strong><br><strong>8. 源代码中的注释往往透露了非常关键的信息。</strong></p>
<p>与文档相比,React Native 的源代码结构非常清晰,代码风格干净,其注释也往往包含了使用的说明,而这些说明又往往是文档中未曾包含的。因此,阅读源代码,不失为无计可施情况下的一种解决方法。拙作<a href="https://segmentfault.com/a/1190000004422456">初窥基于 react-art 库的 React Native SVG</a>即是通过阅读 React Native 源代码而有所收获的。</p>
<h2>工具</h2>
<p><strong>8. 快速运行他人代码的神器 - iOS RNPlayNative</strong></p>
<p><a href="https://link.segmentfault.com/?enc=BwceLnYg6m2nj%2F%2FiwaHiWw%3D%3D.jylRlMd27%2B0xNfOYZNiSz6mf2G1fjww0Nd1lbNnMH04%3D" rel="nofollow">https://rnplay.org</a> 实现了令人惊叹的 React Native 实时运行的效果,即,你可以在网页上输入 React Native 代码,然后在网页上的模拟器中直接运行代码。你也可以在 iPhone 上安装RNPlayNative 应用,扫描网站上的二维码,然后直接就可以在自己的 iPhone 上运行该代码了,完全免除了 NPM 的依赖下载 和 Xcode 编译的冗长时间。此外,该网站还提供了 React Native 框架版本的切换,Amazing!</p>
<p>rnplay 可以帮助 React Native 开发者快速地运行和体验他人的代码,同时也可以用于排除自身环境的错误,还可以用于快速排除旧版本引入的 bug。如此神器,然而国内却很少有人知道,希望经笔者介绍后,能被更多的 React Native 开发者所用。</p>
<p>和 Web 一样的代码部署速度,却有着远超 Web 的流畅手感,既让人感到不可思议,仔细想想 React Native 的早已宣传的快速部署特性,却又在情理之中。只能感慨老外们应用新技术的速度太快了。</p>
<p><strong>9. react-native-logcat</strong></p>
<p><a href="https://link.segmentfault.com/?enc=ruAXHuwxX6pUHFBZ%2BggSRg%3D%3D.vyDxffK%2BiRd6Eg0SuD%2FKxS0nJ0tZKrPFXHEZ4nfHunAEdeiqKcxb%2BFqFCGRlE18UTcFvc%2FNWz1f%2FSRiZhVgJOA%3D%3D" rel="nofollow">一个开源的 React Native Android Log输出工具</a>,免去了繁杂的adb命令。</p>
<h2>调试</h2>
<p><strong>10. 注释调试法:虽然很 Low 但是很有效。</strong></p>
<p>这里不得不提一个 React Native 在捕捉错误上的一个设计缺陷。如果错误是在 ComponentDidMount 之前出现的,那么 backtrace 上只会有一堆神神叨叨的 React Native 库函数,完全无法定位到你的代码中,即便只是一些小语法错误。</p>
<p>那么此时,除了肉眼复查代码,唯一的方法也就是注释调试了。逐行注释掉新加入的代码,观察 bug 是否会复现。</p>
<p>====================================<br><strong>如果您觉得我的文章对您有所启迪,请点击文末的推荐按钮,您的鼓励将会成为我坚持写作的莫大激励。 by DesGemini </strong></p>
探究 React Native 中 Props 驱动的 SVG 动画和 Value 驱动动画
https://segmentfault.com/a/1190000004711886
2016-03-29T14:30:23+08:00
2016-03-29T14:30:23+08:00
DesGemini
https://segmentfault.com/u/desgemini
2
<h2>引言</h2>
<p>一直以来,动画都是移动开发中极为特殊的一块。一方面,动画在交互体验上有着不可替代的优越处,然而另一方面,动画的开发又极为的耗时,需要消耗工程师大量的时间用于开发和调试。再来看前端,前端的动画实现,经过多年的发展,已分为 CSS3 动画和 JavaScript 动画。</p>
<p>React Native 作为一个复用前端思想的移动开发框架,并没有完整实现CSS,而是使用JavaScript来给应用添加样式。这是一个有争议的决定,可以参考这个<a href="https://link.segmentfault.com/?enc=P5Vs%2F5nuDLfMoStZKzRiXA%3D%3D.%2BMMLMLKHZbVRYXrakZsLyMGSDD9Hh4QBwMvRY2BInNQ%3D" rel="nofollow">幻灯片</a>来了解 Facebook 做的理由。自然,在动画上,因为缺少大量的 CSS 属性,React Naive 中的动画均为 JavaScript 动画,即通过 JavaScript 代码控制图像的各种参数值的变化,从而产生时间轴上的动画效果。</p>
<p>React Native 的<a href="https://link.segmentfault.com/?enc=DDRM7EgPppYOq4NwhUIocg%3D%3D.DwTX5X3G5%2Fy%2F5HAHaiKqFVE5BiGZBa%2BamDK4Kufx2R9BU3NjxiwqaZMtSu6Lowq1Po5Xf2KyWvKLGUoR%2BiFzCMt2AU4woJ7hxJdqcqZjSDo%3D" rel="nofollow">官方文档</a>已经详细地介绍了 React Native 一般动画的使用方法和实例,在此不再赘述。然而阅读官方文档后可知,官方的动画往往是给一个完整的物体添加各种动画效果,如透明度,翻转,移动等等。但是对于物体的自身变化,比如如下这个进度条,明显是在旋转的同时也在伸缩,则缺乏必要的实现方法。这是因为,动画的本质既是图形的各种参数的数值变化的过程,文档中的 Animated.Value 就是用作被驱动的参数,可以,想要让一个圆环能够伸缩,就必须让数值变化的过程,深入到图形生成的过程中,而不是如官方文档的例子一样,仅仅是施加于图形生成完毕后的过程,那么也就无法实现改变图形自身的动画效果了。</p>
<p>拙作<a href="https://segmentfault.com/a/1190000004422456">初窥基于 react-art 库的 React Native SVG</a>已讨论了 React Native 中静态 SVG 的开发方法,<strong>本文则致力于探究 React Native 中 SVG 与 Animation 结合所实现的 SVG 动画。</strong>也就是可以改变图形自身的动画效果。此外还探究了 Value 驱动动画在实现方法上的不同之处。</p>
<h2>Props 驱动的 SVG 动画</h2>
<p>本节即以实现一个下图所示的旋转的进度条的例子,讲述 React Native SVG 动画的开发方法。</p>
<p><img src="/img/bVtVUn" alt="图片描述" title="图片描述"></p>
<p>Wedge.art.js 位于 react-art 库下 lib/ 文件夹内,提供了 SVG 扇形的实现,然而缺乏对 cx, cy 属性的支持。另外拙作之前也提到了,Wedge中的扇形较为诡异,只有一条半径,为了实现进度条效果我把另一条半径也去掉了。我将 Wedge.art.js 拷贝到工程中,自行小修改后的代码如下。</p>
<pre><code class="js">// wedge.js
/**
* Copyright 2013-2014 Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule Wedge.art
* @typechecks
*
* Example usage:
* <Wedge
* outerRadius={50}
* startAngle={0}
* endAngle={360}
* fill="blue"
* />
*
* Additional optional property:
* (Int) innerRadius
*
*/
'use strict';
var React = require('react-native');
var ReactART = React.ART;
var $__0 = React,PropTypes = $__0.PropTypes;
var Shape = ReactART.Shape;
var Path = ReactART.Path;
/**
* Wedge is a React component for drawing circles, wedges and arcs. Like other
* ReactART components, it must be used in a <Surface>.
*/
var Wedge = React.createClass({displayName: "Wedge",
propTypes: {
outerRadius: PropTypes.number.isRequired,
startAngle: PropTypes.number.isRequired,
endAngle: PropTypes.number.isRequired,
innerRadius: PropTypes.number,
cx: PropTypes.number,
cy: PropTypes.number
},
circleRadians: Math.PI * 2,
radiansPerDegree: Math.PI / 180,
/**
* _degreesToRadians(degrees)
*
* Helper function to convert degrees to radians
*
* @param {number} degrees
* @return {number}
*/
_degreesToRadians: function(degrees) {
if (degrees !== 0 && degrees % 360 === 0) { // 360, 720, etc.
return this.circleRadians;
} else {
return degrees * this.radiansPerDegree % this.circleRadians;
}
},
/**
* _createCirclePath(or, ir)
*
* Creates the ReactART Path for a complete circle.
*
* @param {number} or The outer radius of the circle
* @param {number} ir The inner radius, greater than zero for a ring
* @return {object}
*/
_createCirclePath: function(or, ir) {
var path = Path();
path.move(this.props.cx, or + this.props.cy)
.arc(or * 2, 0, or)
.arc(-or * 2, 0, or);
if (ir) {
path.move(this.props.cx + or - ir, this.props.cy)
.counterArc(ir * 2, 0, ir)
.counterArc(-ir * 2, 0, ir);
}
path.close();
return path;
},
/**
* _createArcPath(sa, ea, ca, or, ir)
*
* Creates the ReactART Path for an arc or wedge.
*
* @param {number} startAngle The starting degrees relative to 12 o'clock
* @param {number} endAngle The ending degrees relative to 12 o'clock
* @param {number} or The outer radius in pixels
* @param {number} ir The inner radius in pixels, greater than zero for an arc
* @return {object}
*/
_createArcPath: function(startAngle, endAngle, or, ir) {
var path = Path();
// angles in radians
var sa = this._degreesToRadians(startAngle);
var ea = this._degreesToRadians(endAngle);
// central arc angle in radians
var ca = sa > ea ? this.circleRadians - sa + ea : ea - sa;
// cached sine and cosine values
var ss = Math.sin(sa);
var es = Math.sin(ea);
var sc = Math.cos(sa);
var ec = Math.cos(ea);
// cached differences
var ds = es - ss;
var dc = ec - sc;
var dr = ir - or;
// if the angle is over pi radians (180 degrees)
// we will need to let the drawing method know.
var large = ca > Math.PI;
// TODO (sema) Please improve theses comments to make the math
// more understandable.
//
// Formula for a point on a circle at a specific angle with a center
// at (0, 0):
// x = radius * Math.sin(radians)
// y = radius * Math.cos(radians)
//
// For our starting point, we offset the formula using the outer
// radius because our origin is at (top, left).
// In typical web layout fashion, we are drawing in quadrant IV
// (a.k.a. Southeast) where x is positive and y is negative.
//
// The arguments for path.arc and path.counterArc used below are:
// (endX, endY, radiusX, radiusY, largeAngle)
path.move(or + or * ss + this.props.cx, or - or * sc + this.props.cy) // move to starting point
.arc(or * ds, or * -dc, or, or, large) // outer arc
// .line(dr * es, dr * -ec); // width of arc or wedge
if (ir) {
path.counterArc(ir * -ds, ir * dc, ir, ir, large); // inner arc
}
return path;
},
render: function() {
// angles are provided in degrees
var startAngle = this.props.startAngle;
var endAngle = this.props.endAngle;
if (startAngle - endAngle === 0) {
return;
}
// radii are provided in pixels
var innerRadius = this.props.innerRadius || 0;
var outerRadius = this.props.outerRadius;
// sorted radii
var ir = Math.min(innerRadius, outerRadius);
var or = Math.max(innerRadius, outerRadius);
var path;
if (endAngle >= startAngle + 360) {
path = this._createCirclePath(or, ir);
} else {
path = this._createArcPath(startAngle, endAngle, or, ir);
}
return React.createElement(Shape, React.__spread({}, this.props, {d: path}));
}
});
module.exports = Wedge;</code></pre>
<p>然后就是实现的主体。其中值得关注的点是:</p>
<ol>
<li><p>并非任何 Component 都可以直接用 <code>Animated.Value</code> 去赋值 Props,而需要对 Component 做一定的改造。<code>Animated.createAnimatedComponent(Component component)</code>,是 Animated 库提供的用于把普通 Component 改造为 AnimatedComponent 的函数。阅读 React Native 源代码会发现,Animated.Text, Animated.View, Animated.Image,都是直接调用了该函数去改造系统已有的组件,如<code>Animated.createAnimatedComponent(React.Text)</code>。</p></li>
<li><p>Easing 库较为隐蔽,明明在<code>react-native/Library/Animated/</code>路径下,却又需要从React中直接引出。它为动画的实现提供了许多缓动函数,可根据实际需求选择。如 <code>linear()</code> 线性,<code>quad()</code> 二次(quad明明是四次方的意思,为毛代码实现是t*t....),<code>cubic()</code> 三次等等。官方文档中吹嘘 Easing 中提供了 tons of functions(成吨的函数),然而我数过了明明才14个,233333。</p></li>
<li><p>该动画由起始角度和终止角度两个变化的参数来控制,因此,两个<code>Animated.Value</code>需要同时启动,这涉及到了动画的组合问题。React Native 为此提供了 <code>parallel</code>, <code>sequence</code>,<code>stagger</code> 和 <code>delay</code> 四个函数。其主要实现均可在react-native/Library/Animated/Animate中找到,官方文档中亦有<a href="https://link.segmentfault.com/?enc=sxmPZ43gcqwpgG4Nq0JZYQ%3D%3D.2GVqL3nTrh0O0TICKwybF1%2B22S3iDMy%2F2als2FT3RZCFQv2kXrY2LPhiZbcQtpV%2FhE8ozqA0Qdo%2BYxa7X51q90HAUVs6tCxRMgQhJZpl2rM%3D" rel="nofollow">说明</a>。这里用的是<code>Animated.parallel</code>。</p></li>
</ol>
<p>开发中遇到的问题有:</p>
<ol>
<li><p>该动画在 Android 上可以运行,但是刷新频率看上去只有两帧,无法形成一个自然过渡的动画,笔者怀疑是 React Native Android 对 SVG 的支持仍有缺陷。</p></li>
<li><p>SVG 图形和普通 React Native View 的叠加问题,目前我还没有找到解决方法。感觉只能等 React Native 开发组的进一步支持。</p></li>
<li><p>动画播放总会有一个莫名其妙的下拉回弹效果,然而代码上没有任何额外的控制。</p></li>
</ol>
<pre><code class="js">// RotatingWedge.js
'use strict';
var React = require('react-native');
var {
ART,
View,
Animated,
Easing,
} = React;
var Group = ART.Group;
var Surface = ART.Surface;
var Wedge = require('./Wedge');
var AnimatedWedge = Animated.createAnimatedComponent(Wedge);
var VectorWidget = React.createClass({
getInitialState: function() {
return {
startAngle: new Animated.Value(90),
endAngle: new Animated.Value(100),
};
},
componentDidMount: function() {
Animated.parallel([
Animated.timing(
this.state.endAngle,
{
toValue: 405,
duration: 700,
easing: Easing.linear,
}
),
Animated.timing(
this.state.startAngle,
{
toValue: 135,
duration: 700,
easing: Easing.linear,
})
]).start();
},
render: function() {
return (
<View>
<Surface
width={700}
height={700}
>
{this.renderGraphic()}
</Surface>
</View>
);
},
renderGraphic: function() {
console.log(this.state.endAngle.__getValue());
return (
<Group>
<AnimatedWedge
cx={100}
cy={100}
outerRadius={50}
stroke="black"
strokeWidth={2.5}
startAngle={this.state.startAngle}
endAngle={this.state.endAngle}
fill="FFFFFF"/>
</Group>
);
}
});
module.exports = VectorWidget;</code></pre>
<h2>Value 驱动的动画</h2>
<p>接下来看 Value 驱动的 SVG 动画。先解释一下 Value 和 Props 的区别。<code><Text color='black'></Text></code>,这里的 color 就是 Props,<code><Text>black</Text></code>这里的 black 就是 value。</p>
<p>为什么要特意强调这一点呢,如果我们想要做一个如下图所示的从10到30变动的数字,按照上节所述的方法,直接调用 <code>Animated.createAnimatedComponent(React.Text)</code>所生成的 Component ,然后给 Value 赋值一个Animated.Value(),然后Animated.timing...,是无法产生这样的效果的。</p>
<p><img src="/img/bVtVWj" alt="图片描述" title="图片描述"></p>
<p>必须要对库中的<code>createAnimatedComponent()</code>函数做一定的改造。改造后的函数如下:</p>
<pre><code class="js">var AnimatedProps = Animated.__PropsOnlyForTests;
function createAnimatedTextComponent() {
var refName = 'node';
class AnimatedComponent extends React.Component {
_propsAnimated: AnimatedProps;
componentWillUnmount() {
this._propsAnimated && this._propsAnimated.__detach();
}
setNativeProps(props) {
this.refs[refName].setNativeProps(props);
}
componentWillMount() {
this.attachProps(this.props);
}
attachProps(nextProps) {
var oldPropsAnimated = this._propsAnimated;
/** 关键修改,强制刷新。
原来的代码是:
var callback = () => {
if (this.refs[refName].setNativeProps) {
var value = this._propsAnimated.__getAnimatedValue();
this.refs[refName].setNativeProps(value);
} else {
this.forceUpdate();
}
};
**/
var callback = () => {
this.forceUpdate();
};
this._propsAnimated = new AnimatedProps(
nextProps,
callback,
);
oldPropsAnimated && oldPropsAnimated.__detach();
}
componentWillReceiveProps(nextProps) {
this.attachProps(nextProps);
}
render() {
var tmpText = this._propsAnimated.__getAnimatedValue().text;
return (
<Text
{...this._propsAnimated.__getValue()}
ref={refName}
>
{Math.floor(tmpText)}
</Text>
);
}
}
return AnimatedComponent;
}</code></pre>
<p>为了获取必须要用到的AnimatedProps,笔者甚至违背了道德的约束,访问了双下划线前缀的变量<code>Animated.__PropsOnlyForTests</code>,真是罪恶啊XD。</p>
<p>言归正传,重要的修改有:</p>
<ol>
<li><p>修改了 attachProps 函数。对于任何变动的 props,原来的代码会试图使用 setNativeProps 函数进行更新,若 setNativeProps 函数为空,才会使用 forceUpdate() 函数。对于 props,setNativeProps 函数是可行的,然而对 value 无效。我猜测,setNativeProps 方法在 Android 底层可能就是 setColor() 类似的 Java 方法,然而并没有得到实证。目前这种 forceUpdate,由注释知,是彻底更新了整个 Component,相当于先从 DOM 树上取下一个旧节点,再放上一个新节点,在性能的利用上较为浪费。</p></li>
<li><p>使用 PropTypes.xxx.isRequired 来进行参数的类型检查。PropTypes 检查支持的类型可在 <code>react-native/node_modules/react/lib/ReactPropTypes.js</code> 中看到,在此不再赘述。</p></li>
<li><p>Animated.value() 从10到30变化的过程是一个随机采样的过程,并不一定会卡在整数值上,因此还需要做一些小处理。</p></li>
</ol>
<p>值得注意的是,该动画在 Android 上虽然可以正常运行,但也存在丢帧的问题,远远不能如 iOS 上流畅自然。对于这一点,只能等待 Facebook 的进一步优化。</p>
<p>全部的代码如下:</p>
<pre><code class="js">// RisingNumber.js
'use strict';
var React = require('react-native');
var {
Text,
Animated,
Easing,
PropTypes,
View,
StyleSheet,
} = React;
var AnimatedText = createAnimatedTextComponent();
var AnimatedProps = Animated.__PropsOnlyForTests;
function createAnimatedTextComponent() {
var refName = 'node';
class AnimatedComponent extends React.Component {
_propsAnimated: AnimatedProps;
componentWillUnmount() {
this._propsAnimated && this._propsAnimated.__detach();
}
setNativeProps(props) {
this.refs[refName].setNativeProps(props);
}
componentWillMount() {
this.attachProps(this.props);
}
attachProps(nextProps) {
var oldPropsAnimated = this._propsAnimated;
var callback = () => {
this.forceUpdate();
};
this._propsAnimated = new AnimatedProps(
nextProps,
callback,
);
oldPropsAnimated && oldPropsAnimated.__detach();
}
componentWillReceiveProps(nextProps) {
this.attachProps(nextProps);
}
render() {
var tmpText = this._propsAnimated.__getAnimatedValue().text;
return (
<Text
{...this._propsAnimated.__getValue()}
ref={refName}
>
{Math.floor(tmpText)}
</Text>
);
}
}
return AnimatedComponent;
}
var RisingNumber = React.createClass({
propTypes: {
startNumber: PropTypes.number.isRequired,
toNumber: PropTypes.number.isRequired,
startFontSize: PropTypes.number.isRequired,
toFontSize: PropTypes.number.isRequired,
duration: PropTypes.number.isRequired,
upperText: PropTypes.string.isRequired,
},
getInitialState: function() {
return {
number: new Animated.Value(this.props.startNumber),
fontSize: new Animated.Value(this.props.startFontSize),
};
},
componentDidMount: function() {
Animated.parallel([
Animated.timing(
this.state.number,
{
toValue: this.props.toNumber,
duration: this.props.duration,
easing: Easing.linear,
},
),
Animated.timing(
this.state.fontSize,
{
toValue: this.props.toFontSize,
duration: this.props.duration,
easing: Easing.linear,
}
)
]).start();
},
render: function() {
return (
<View>
<Text style={styles.kind}>{this.props.upperText}</Text>
<AnimatedText
style={{fontSize: this.state.fontSize, marginLeft: 15}}
text={this.state.number} />
</View>
);
},
});
var styles = StyleSheet.create({
kind: {
fontSize: 15,
color: '#01A971',
},
number: {
marginLeft: 15,
},
});
module.exports = RisingNumber;
</code></pre>
<p>====================================<br><strong>如果您觉得我的文章对您有所启迪,请点击文末的推荐按钮,您的鼓励将会成为我坚持写作的莫大激励。 by DesGemini </strong></p>
初窥基于 react-art 库的 React Native SVG
https://segmentfault.com/a/1190000004422456
2016-02-08T17:38:53+08:00
2016-02-08T17:38:53+08:00
DesGemini
https://segmentfault.com/u/desgemini
6
<h2>技术背景</h2>
<p>在移动应用的开发过程中,绘制基本的二维图形或动画是必不可少的。然而,考虑到Android和iOS均有一套各自的API方案,因此采用一种更普遍接受的技术方案,更有利于代码的双平台兼容。</p>
<p><a href="https://link.segmentfault.com/?enc=SSPto02VXT7p2vZ%2BcOWCGA%3D%3D.%2FrLs8S5T6fAOIkcNYwV1%2FJ%2BnPc8nYbuPG38ov2unGDNt2CfZMHSeJQjYFHM%2FdmFG" rel="nofollow">art</a>是一个旨在多浏览器兼容的Node style CommonJS模块。在它的基础上,Facebook又开发了<a href="https://link.segmentfault.com/?enc=rYl614smCOH6GD3F%2FRdLNg%3D%3D.L5cNMLUEydRybwkMuBI%2BcbVQM0GXq0lMkyJa85YMnjBnHiE68jIARz2BDd29icuN" rel="nofollow">react-art</a> ,封装art,使之可以被react.js所使用,即实现了前端的svg库。然而,考虑到react.js的JSX语法,已经支持将<code><cirle></code> <code><svg></code>等等svg标签直接插入到dom中(当然此时使用的就不是react-art库了)此外还有HTML canvas的存在,因此,在前端上,react-art并非不可替代。</p>
<p>然而,在移动端,考虑到跨平台的需求,加之web端的技术积累,react-art成为了现成的绘制图形的解决方案。react-native分别在0.10.0和0.18.0上添加了iOS和Android平台上对react-art的支持,当然,没有文档。在文档基本等于没有的情况下,笔者苦逼地翻源代码,为大家带来了(全球首发?=_=)的入门文档。</p>
<h2>示例代码</h2>
<p>推荐大家采用react-art自带的Example: <a href="https://link.segmentfault.com/?enc=CpEQO31LGxdVX3gW0MGrug%3D%3D.NQ6j3Udf1R3cIlN%2Fjn2xPRoaNUJLlLF71j5qmx28hLpMYoUfkQEIYOXaw%2B5PLjzd5EaisBuqDbiimvh3Lx1kKIt4VbORJinCx0l%2BBSi4SFk%3D" rel="nofollow">Vector-Widget</a>。React.js和React-Native的区别,只在于下文所述的ART获取上,然后该例子就可以同时应用在Web端和移动端上了。</p>
<p>成功运行Vector-Widget后的效果图:<br><img src="/img/bVsJlj" alt="图片描述" title="图片描述"></p>
<p>Vector-Widget额外实现了旋转,以及鼠标点击事件的旋转加速响应。Web端可以看到点击加速,但是在移动端无效,原因是React Native并未对Group中onMouseDown和onMouseUp属性作处理。本文着重于静态svg的实现,暂时无视动画部分效果即可。</p>
<p>此外还可以用该例子感受一下叹为观止svg动画的性能占用(摊手)。</p>
<h2>原理和调用</h2>
<h3>获取ART</h3>
<p>package.json中需要引入art库,笔者的版本设置是<code>react-art: 0.14.0</code>。</p>
<h4>Android与iOS</h4>
<pre><code class="js">var React = require('react-native');
var ReactART = React.ART;</code></pre>
<p>或者使用ES6的Destructuring特性:</p>
<pre><code class="js">var {
...,
ART,
...,
} = React;</code></pre>
<h4>Web端</h4>
<pre><code class="js">var React = require('react');
var ReactART = React.ART;</code></pre>
<h3>基本组件</h3>
<h4>获取方式</h4>
<p>接下来的所述的代码,web端和移动端都是通用的,这也是React Native的诱惑所在。</p>
<pre><code class="js">var {
Shape,
Group,
Transform,
Surface,
...,
} = React.ART;</code></pre>
<h4>Surface</h4>
<p>所有的svg component必须被一个Surface标签所包含。<br>Props如下:</p>
<ul>
<li><p>width: Surface的宽度。</p></li>
<li><p>height: Surface的高度。</p></li>
<li><p>style: margin系列和padding系列都生效。</p></li>
</ul>
<h4>Group</h4>
<p>Group用于组合art component。比如在一个函数中返回多个svg component的情况,此时就必须要用<code><Group></code>包一下,否则即报错。<code><Group></code>可以嵌套使用。</p>
<p>style:margin和padding系列均无效,我怀疑不接受style。</p>
<pre><code class="js">function _render() {
return (
<Group>
<Shape d={"M160 160 A 45 45, 0, 0, 1, 115 205"} stroke="#000000" strokeWidth={3} />
<Shape d={"M160 160 A 45 45, 0, 0, 1, 115 205"} stroke="#000000" strokeWidth={3} />
</Group>
);
}</code></pre>
<h4>Shape</h4>
<p>Shape用于生成路径,语法与svg中的<path>很相似。Shape的Props如下:</p>
<ul>
<li><p>d: 语法与svg规范相同</p></li>
<li><p>stroke: 线条颜色,"#FFFFFF"的形式</p></li>
<li><p>strokeWidth: 线条宽度,{3}的形式</p></li>
<li><p>transform:接受 new ART.Transform()生成的object,具体见下文Transform条目。</p></li>
</ul>
<h4>Path</h4>
<p>语法更近似于移动端。使用方法:</p>
<pre><code class="js">var ReactART = require('./ReactART');
var Path = ReactART.Path;
function _render() {
// 除close以外的所有方法都返回修改后的自身,因此支持链式调用
var path = Path().moveTo(0, -radius)
.arc(0, radius * 2, radius)
.arc(0, radius * -2, radius)
.close();
// path可以直接赋值给d
return <Shape d={path} />
}</code></pre>
<p>可以看到,取出的Path是一个构造函数。Path对象的中的函数功能如下,大多与svg规范一致,我就再啰嗦一遍了。svg规范中<path>的d属性请参见<a href="https://link.segmentfault.com/?enc=5tzHDlarFFaqnCZbHJLDDg%3D%3D.7JMKr%2B%2B1Btin66S9K9pfwUZXlGiCYboIL4vbCsCVAIY6BhMz1aloE6YH6tYjKTYS7E6RC4vH26G8TpUYMaL5%2Bw%3D%3D" rel="nofollow">https://developer.mozilla.org/en-US/docs...</a> :</p>
<ul>
<li><p>push():</p></li>
<li><p>reset(): 清空Path</p></li>
<li><p>move(x, y): 等同于'm',移动到目的坐标,参数x和y是相对目标下的目的坐标</p></li>
<li><p>moveTo(x, y): 等同于'M',与move只差别在x和y是绝对坐标。</p></li>
<li><p>line(x, y): 等同于'l',从一个坐标点到另一个坐标点画直线,参数x和y是相对坐标下的目的坐标</p></li>
<li><p>lineTo(x, y): 等同于'L',与line只差别在x和y是绝对坐标。</p></li>
<li><p>arc(x, y, rx, ry, outer): 等同于'a',从一个坐标点向另一个坐标点画椭圆曲线,x和y是相对坐标下的目的坐标,rx和ry是椭圆的长轴半径和短轴半径,outer只有0和1两个数字,代表是大角度还是小角度。</p></li>
<li><p>arcTo(x, y, rx, ry, outer): 等同于'A',与arc只差别在x和y是绝对坐标。</p></li>
<li>
<p>curve(2个,4个或6个参数): 从一个坐标点向另一个坐标点画贝塞尔曲线。</p>
<ul>
<li><p>当参数为两个时,等同于't',绘制光滑二次贝塞尔曲线。</p></li>
<li><p>当参数为4个时,等同于'q',绘制二次贝塞尔曲线。</p></li>
<li><p>当参数为6个时,等同于'c',绘制三次贝塞尔曲线。</p></li>
<li><p>有些精通SVG的同学这时候可能就要问我了,不对啊,二次贝塞尔曲线和光滑三次贝塞尔曲线的参数都是4个,你这里没有光滑三次啊?因为开发的同学留坑没写了呀(微笑)。</p></li>
</ul>
</li>
</ul>
<h4>Transform</h4>
<p>实现代码路径:art/core/transform.js</p>
<p>Transform对象中的函数:</p>
<ul>
<li><p>transform(xx, yx, xy, yy, x, y): transform的相对坐标版本</p></li>
<li><p>transformTo: 完整的矩阵变换,把这张位图上所有的点都做一次矩阵乘法,得到的新位图,公式如下图所示$$ \begin{Bmatrix} a & b & c \\ d & e & f \\ 0 & 0 & 1 \\ \end{Bmatrix} \times \begin{Bmatrix} x \\ y \\ 1 \end{Bmatrix}$$</p></li>
<li><p>translate(x, y): 位移</p></li>
<li><p>move: 相对于原参考点坐标,增减参考点的x,y坐标</p></li>
<li><p>moveTo: 等同于translateTo,容易误以为是move的绝对左边版本,吐糟不能</p></li>
<li><p>scale(x, y): 将一个元素拉伸或者压缩指定的倍数,x是宽度缩放比例,y是长度缩放比例,如果只有一个参数,则x = y。如</p></li>
<li><p>scaleTo: 在缩放的同时保持原来的长宽比。例子和效果图如下:</p></li>
</ul>
<pre><code class="html"><Surface
width={700}
height={700}>
<Rectangle
width={100}
height={200}
stroke="red"
strokeWidth={10}
fill="FFFFFF"
transform={new Transform().translate(150, 150).scale(2)}
/>
<Rectangle
width={100}
height={200}
stroke="yellow"
strokeWidth={10}
fill="FFFFFF"
transform={new Transform().translate(150, 150).scaleTo(1, 2)}
/>
<Rectangle
width={100}
height={200}
stroke="blue"
strokeWidth={10}
fill="FFFFFF"
transform={new Transform().translate(150, 150)}
/>
</Surface></code></pre>
<p><img src="/img/bVsJk8" alt="图片描述" title="图片描述"></p>
<ul><li><p>rotate(deg, x, y): 将一个元素旋转角度deg。x和y则是用于指定旋转的原点。</p></li></ul>
<pre><code class="html"><Surface
width={700}
height={700}>
<Rectangle
width={100}
height={200}
stroke="red"
strokeWidth={10}
fill="FFFFFF"
transform={new Transform().translate(150, 150)}
/>
<Rectangle
width={100}
height={200}
stroke="red"
strokeWidth={10}
fill="FFFFFF"
transform={new Transform().translate(150, 150).rotate(30, 100, 100)}
/>
</Surface></code></pre>
<pre><code class="html"><Surface
width={700}
height={700}>
<Rectangle
width={100}
height={200}
stroke="red"
strokeWidth={10}
fill="FFFFFF"
transform={new Transform().translate(150, 150)}
/>
<Rectangle
width={100}
height={200}
stroke="red"
strokeWidth={10}
fill="FFFFFF"
// 差别在这里
transform={new Transform().translate(150, 150).rotate(30)}
/>
</Surface></code></pre>
<p>效果如下图所示:<br><img src="/img/bVsJk9" alt="图片描述" title="图片描述"><br><img src="/img/bVsJla" alt="图片描述" title="图片描述"></p>
<h3>react-art中的lib</h3>
<h4>引入lib中的module</h4>
<p>在react-art的库中,有个神奇的lib文件夹,下面除了ReactART.js以外,还有Circle.art.js,Rectangle.art.js,Wedge.art.js等,其中Circle和Rectangle分别对应于svg规范中的圆形<code><circle></code>,矩形<code><rect></code>,而Wedge则是用于生成扇形。当然,这些module都很不完善,很有可能需要二次开发,如果这样,推荐拷贝库文件到工程中再行修改。</p>
<p>引入它们的语句为<code>var Circle = require('react-art/lib/Circle.art');</code>。这种不同寻常地引用方式是Facebook开发和维护的<a href="https://link.segmentfault.com/?enc=Z5OXqWMGwiFuNyfrweybWQ%3D%3D.RYVKGJ3%2FCAmZ1b7atCAztU6wqSlsyFiBkT4U6pX40QOIU9DI1ZPrGfzglYjbtK8X" rel="nofollow">fbjs</a>引入的,react-native依赖了fbjs,而所有需要被输出的js文件的头部都会以<code>// @providesModule Circle.art</code><br>的形式标明。</p>
<h4>Circle</h4>
<p>使用示例:</p>
<pre><code class="html"><Circle
radius={10}
stroke="green"
strokeWidth={3}
fill="blue"
/></code></pre>
<p>值得一提的是,Circle.art.js是个半成品,可以看到它根本没有实现svg规范中的cx和cy,因此画出来的圆的圆心始终在左上角,显示出来的也就只有半个圆。请在实际应用中自行实现,或者使用笔者提供的<a href="https://gist.github.com/CauT/cb524c1f25db567c24b5">修改版本</a>。</p>
<p>当然,不用cx和cy的话,也可以设置tranform来实现平移。如:</p>
<pre><code class="html"><Circle
radius={10}
stroke="green"
strokeWidth={3}
fill="blue"
transform={new Transform().translate(100, 100)}
/></code></pre>
<p>两种方法都可以达到平移效果,但是cx, cy的方式相对来说更简洁,可读性也更好。</p>
<h4>Rectangle</h4>
<p>使用示例:</p>
<pre><code class="html"><Rectangle
width={200}
height={400}
stroke="red"
strokeWidth={10}
fill="FFFFFF"
/></code></pre>
<p>使用上述代码,就很直观看到这个module的缺陷了,矩形四条边不等宽(扶额)。<br><img src="/img/bVsIDY" alt="图片描述" title="图片描述"><br>此外还接受的props有:</p>
<ul>
<li><p>radius</p></li>
<li><p>radiusTopLeft</p></li>
<li><p>radiusTopRight</p></li>
<li><p>radiusBottomLeft</p></li>
<li><p>radiusBottomRight</p></li>
</ul>
<p>这里的radius指的是圆角矩形的圆角半径,接受的值类型为数字,radius为四个角的通用半径,但如果设置了具体某个角的半径,则用后者。</p>
<h4>Wedge</h4>
<p>Wedge是楔子的意思,然而在这里却是生成各种角度的扇形=_=。<br>使用示例:</p>
<pre><code class="js"><Wedge
outerRadius={50}
stroke="red"
startAngle={0}
endAngle={100}
fill="FFFFFF"
/></code></pre>
<p>生成的图形如下图:<br><img src="/img/bVsID3" alt="图片描述" title="图片描述"><br>可选Props为innerRadius,用于生成一个圆环扇形,如下图。<br><img src="/img/bVsID4" alt="图片描述" title="图片描述"><br>呐,一看这个module也是半成品,如果stroke有颜色而fill为白色就露馅了,曲线没闭合。因此,如有需求,请自行弥补。</p>
<h2>技术缺陷</h2>
<p>除了之前所提到的各种各样的实现上的不完善以外,svg规范在React Native上应用的最大问题在于,有一大批web上的svg支持的css属性,还没有在React Native上实现。</p>
<p>以stroke类的一大批属性为例,目前可用的只有线条颜色stroke和strokeWidth,而如stroke-dasharray,stroke-dashoffset这样的可以用之实现神奇的描边等效果的CSS属性,目前还没有被支持。</p>
<p>在性能占用方面, 静态svg尚可接受,但如果是svg annimation,实测红米Note 2上的CPU占用率即可达50%左右,故其在生产环境的大规模应用,恐怕还需进一步的性能优化。</p>
<p>====================================<br><strong>如果您觉得我的文章对您有所启迪,请点击文末的推荐按钮,您的鼓励将会成为我坚持写作的莫大激励。 by DesGemini </strong></p>
多React Native项目时依赖管理的最佳实践
https://segmentfault.com/a/1190000004278414
2016-01-09T20:51:28+08:00
2016-01-09T20:51:28+08:00
DesGemini
https://segmentfault.com/u/desgemini
9
<p>在实际开发过程中,经常需要同时运行和修改多个React Native工程,比如运行github上的开源项目以观察某种控件的实际效果。那么此时,各项目下的初始化(npm install)就会非常的痛苦,因为React Native的文件非常大,以0.17.0为例,安装后达到309MB。尽管,我们可以通过阿里npm等镜像站的方式加速下载的过程,但是下载后的进一步编译也非常地耗时。</p>
<p>此外,多React Native工程还带来了React Native自身的冗余,如果创建了十几个工程,那么多占用的空间轻松达到3GB以上,非常地不友好。</p>
<h2>npm link原理</h2>
<p>我的解决思路是:用npm link替代npm install。<code>npm link [package-name]</code>命令的原理是,去[prefix]/lib/node_modules/下检索是否已经全局安装了当前的package,如果是,则直接用软链接的方法在本地路径指向全局package。如果没检索到,则会先在全局路径下安装该package,再去建立软链接。npm获取全局路径的命令是:<code>npm config get prefix</code>。</p>
<p>需要注意的是,有package.json的路径下,不要类比<code>npm install</code>,就这么执行<code>npm link</code>。此时npm link会把当前路径作为一个本地package,在全局路径下创建一个软链接。由此可知,npm link并不会像npm install一样,读取package.json中的依赖并自动配置。</p>
<h2>配置过程</h2>
<pre><code>npm install -g react-native
cd [program_path]
npm link react-native</code></pre>
<p>简单三步搞定。然后运行<code>react-native run-android</code>,打个Android包检测一下。</p>
<p>纳尼,报错如下:</p>
<p>Looks like you installed react-native globally, maybe you meant react-native-cli?<br>To fix the issue, run:<br>npm uninstall -g react-native<br>npm install -g react-native-cli</p>
<p>原因很简单,react-native框架其实由两个部分组成:react-native和react-native-cli,前者用于提供编译环境,后者则是封装了react-native开发过程中所要用到的命令,如<code>react-native start</code>,实质就是封装了<code>sh ./node_modules/react-native/packager/packager.sh</code>。</p>
<p>官方文档要求全局安装react-native-cli,但是局部安装react-native,这是有原因的。如果你先全局安装了react-native-cli,会在/usr/local/bin下生成一个名为react-native的软链接,其指向为:<code>react-native -> ../lib/node_modules/react-native-cli/index.js*</code>。而随后再次"全局"安装react-native的时候,又会生成一个名为react-native的软链接,覆盖了react-native-cli安装时生成的软链接,其指向是:<code>../lib/node_modules/react-native/local-cli/wrong-react-native.js</code>。由此可见,React Native官方已经意识到了这个问题,然而不知何原因并不推荐全局安装React Native。然而笔者从节约硬盘空间和加快初始化的角度,认为还是有必要全局安装React Native,从而快速npm link的,所以有必要研究该报错的原理。</p>
<p>因此,针对这个报错,两种解决方法:</p>
<ol>
<li><p>先<code>npm install -g react-native</code>,再<code>npm install -g react-native-cli</code>。然而,如果以后使用过程中又升级了全局React Native,此时需看方案2。</p></li>
<li><p><code>cd /usr/local/bin</code>,<code>ln -s ../lib/node_modules/react-native-cli/index.js react-native</code>,即可重新创建一个指向react-native-cli的软链接。如果prefix的地址不是默认的,则<code>ln -s prefix/lib/node_modules/react-native-cli/index.js react-native</code>。</p></li>
</ol>
<h2>缺陷</h2>
<p>当前这个自动添加统一依赖的方法,存在一个问题。所有依赖于全局路径下的React Native都必须是一个版本的,npm link并没有提供多版本号依赖的解决方法。因此,还是建议选择一个常用的React Native版本安装在全局路径,个别需求其他版本号的React Native的项目,使用<code>npm install</code>来配置局部依赖。</p>
<p>插说一句,npm自身的依赖管理设计还是非常优秀的,然而React Native实在是太大了,而且我们完全有理由相信,他会更大。他其实应该是与Android SDK, Java SDK一般重量级的开发SDK,因此更应该借鉴rvm,设计一个React Native Version Manager。然而却委身于node_modules,因而产生了这种无奈的冗余。</p>
<p>====================================<br><strong>如果您觉得我的文章对您有所启迪,请点击文末的推荐按钮,您的鼓励将会成为我坚持写作的莫大激励。 by DesGemini </strong></p>