1

探索 iOS 编码对包大小的影响

 2 years ago
source link: https://www.51cto.com/article/701465.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

本文讲述的技术点属于比较极致和新颖的包大小优化技术,文章会从二进制、汇编指令的层面来分析 oc 代码对包大小的影响。接下来会从以下三个方面进行讲述:

  1. 二进制层面分析 oc 代码对包大小的影响
  2. 编码上优化包大小的 tips
  3. 总结各种 tips 的收益

从二进制文件层面来分析编码对包大小影响

以分析属性为例子,介绍一种“从二进制文件层面来分析编码对包大小影响”的分析方法。

实验背景:用真机 iphone11,iOS13.5.1,release,build setting 默认设置,linkmap file 使用 arm64 进行实验。

.o 级别对比

通过对比 ViewControler 没有属性、有一个属性、两个属性的情况:

@interface ViewController : UIViewController
@property (nonatomic, strong) NSString *myString1;
@property (nonatomic, strong) NSString *myString2;
@end

interface ViewController : UIViewController@property (nonatomic, strong) NSString *myString1;@property (nonatomic, strong) NSString *myString2;@end

ViewController.o占用字节

  • 没有属性 0.36k
  • 一个属性 0.67k
  • 两个属性 0.92k

当一个类减少一个属性,会减少 0.25k,这里有一个前提,这个类还有其他属性。

因此,我们可以得到一个结论:一个属性占用 0.25K。

查看 linkmap file 中的符号属于哪个 section 的 tips

# Sections:
# Address        Size            Segment        Section
0x10000623C        0x00000224        __TEXT        __text
0x100006460        0x00000090        __TEXT        __stubs

__TEXT.__text 的地址范围是 0x10000623C - 0x100006460,那么 linkmap file 中的 Symbols 对应的地址,如果是在这个范围内,就属于__TEXT__.text。

0x10000623C        0x00000034        [  1] -[ViewController viewDidLoad]
0x100006270        0x00000010        [  1] -[ViewController myString1]
0x100006280        0x00000014        [  1] -[ViewController setMyString1:]
0x100006294        0x00000010        [  1] -[ViewController myString2]
0x1000062A4        0x00000014        [  1] -[ViewController setMyString2:]
0x1000062B8        0x00000040        [  1] -[ViewController .cxx_destruct]
0x1000062F8        0x00000008        [  2] -[AppDelegate application:didFinishLaunchingWithOptions:]
0x100006300        0x0000008C        [  2] -[AppDelegate application:configurationForConnectingSceneSession:options:]
0x10000638C        0x00000004        [  2] -[AppDelegate application:didDiscardSceneSessions:]
0x100006390        0x00000080        [  3] _main
0x100006410        0x00000004        [  4] -[SceneDelegate scene:willConnectToSession:options:]
0x100006414        0x00000004        [  4] -[SceneDelegate sceneDidDisconnect:]
0x100006418        0x00000004        [  4] -[SceneDelegate sceneDidBecomeActive:]
0x10000641C        0x00000004        [  4] -[SceneDelegate sceneWillResignActive:]
0x100006420        0x00000004        [  4] -[SceneDelegate sceneWillEnterForeground:]
0x100006424        0x00000004        [  4] -[SceneDelegate sceneDidEnterBackground:]
0x100006428        0x00000010        [  4] -[SceneDelegate window]
0x100006438        0x00000014        [  4] -[SceneDelegate setWindow:]
0x10000644C        0x00000014        [  4] -[SceneDelegate .cxx_destruct]
0x100006460        0x0000000C        [  5] _NSStringFromClass

0x10000623C 到 0x100006460 的符号都属于__TEXT.__text。

探索一个属性会产生哪些符号

将 section(节) size 不同拎出来,数字的单位:字节

__TEXT 段:

c392712607393211eec29924038c8a73f23b01.jpg

__DATA 段:

21cec33801bc7dbcb9b794c4bb34400f9f1751.jpg

  • $1 表示没有属性和一个属性的对比
  • $2 表示一个属性和两个属性的对比
TEXT.text 分析

d58234f50eaf5b47d2898208a47128939fd29d.jpg

Linkmap File 差异:

// ViewController没有属性,差异点是没有-[ViewController .cxx_destruct]符号
// 一个属性: 16 + 20 + 20 = 56(与上方的 $1是对应上的)
0x100006318        0x00000010        [  1] -[ViewController myString1]
0x100006328        0x00000014        [  1] -[ViewController setMyString1:]
0x10000633C        0x00000014        [  1] -[ViewController .cxx_destruct]
// 两个属性 16 + 20 + 16 + 20 +64 = 136
// 一个属性与两个属性比较 136 - 56 = 80(与上方的 $2是对应上的)
0x100006270        0x00000010        [  1] -[ViewController myString1]
0x100006280        0x00000014        [  1] -[ViewController setMyString1:]
0x100006294        0x00000010        [  1] -[ViewController myString2]
0x1000062A4        0x00000014        [  1] -[ViewController setMyString2:]
0x1000062B8        0x00000040        [  1] -[ViewController .cxx_destruct]

疑惑点:.cxx_destruct的大小不一样,以下用两个截图回答这个问题,因为.cxx_destruct 内部的实现不一样,导致汇编的指令数量也不一样。

一个属性 .cxx_destruct内部实现的汇编代码:

82aa01a54ea3d0ddd9977461aef186eef8b326.jpg

两个属性 .cxx_destruct内部实现的汇编代码:

f73539c20508165974c37874afe20f9a6d2ad2.jpg

TEXT.objc_methname 分析

386af0f80b39fb77aa9065e66fcb5ad97d729b.jpg

// 比较字节方法如上
// 一个属性
0x1000065FC        0x0000000A        [  1] literal string: myString1
0x100006606        0x0000000E        [  1] literal string: setMyString1:
0x100006614        0x0000000E        [  1] literal string: .cxx_destruct
0x100006622        0x0000000B        [  1] literal string: _myString1
// 两个属性
0x1000065A4        0x0000000A        [  1] literal string: myString1
0x1000065AE        0x0000000E        [  1] literal string: setMyString1:
0x1000065BC        0x0000000A        [  1] literal string: myString2
0x1000065C6        0x0000000E        [  1] literal string: setMyString2:
0x1000065D4        0x0000000E        [  1] literal string: .cxx_destruct
0x1000065E2        0x0000000B        [  1] literal string: _myString1
0x1000065ED        0x0000000B        [  1] literal string: _myString2

这里补充额外的一个点Dead Stripped Symbols相关的知识点:

// Text.text
0x1000062B8        0x00000040        [  1] -[ViewController .cxx_destruct]
0x10000644C        0x00000014        [  4] -[SceneDelegate .cxx_destruct]
// TEXT.objc_methname
0x1000065D4        0x0000000E        [  1] literal string: .cxx_destruct
<<dead>>         0x0000000E            [  4] literal string: .cxx_destruct

Dead Stripped Symbols的意思是.o 文件中不必要的符号去掉;

例如[ 4] literal string: .cxx_destruct就是不必要的符号,[ 4]是指SceneDelegate.o之所以不必要是因为对于objc_methname节来说,不需要有同名的符号。

// linkfile map内容,[ 4]指SceneDelegate.o
[  4] /Users/xxxx/Library/Developer/Xcode/DerivedData/PacketSize-athynpqwkehwhtfsynewxvtjyvdz/Build/Intermediates.noindex/PacketSize.build/Release-iphoneos/PacketSize.build/Objects-normal/arm64/SceneDelegate.o

Xcode 中Dead Stripped Symbols相关的设置:

211618316d6a789e83968296ce1bdb34810aeb.jpg

TEXT.objc_methtype

58f3ad8503fdda8ae9c3859906d0ca81402df9.jpg

// 有NSString属性,有以下符号
0x1000073CF        0x00000008        [  1] literal string: @16@0:8
0x1000073D7        0x0000000B        [  1] literal string: v24@0:8@16
0x1000073E2        0x0000000C        [  1] literal string: @"NSString"
// 同样Dead Stripped Symbols中也有@16@0:8、v24@0:8@16

TEXT.cstring 分析

f1ec05c80f1a6403193704d2273210161b5dac.jpg

// 一个属性 10 + 29 = 39
0x100007EE2        0x0000000A        [  1] literal string: myString1
0x100007EEC        0x0000001D        [  1] literal string: T@"NSString",&,N,V_myString1
// 两个属性
0x100007EAF        0x0000000A        [  1] literal string: myString1
0x100007EB9        0x0000001D        [  1] literal string: T@"NSString",&,N,V_myString1
0x100007ED6        0x0000000A        [  1] literal string: myString2
0x100007EE0        0x0000001D        [  1] literal string: T@"NSString",&,N,V_myString2

TEXT.objc_classname 分析

985ae6605e03914ebe70936c5b1aa7bc0db0a5.jpg

两个属性,比其他多占用了两个字节,目前还不知道这两个字节是怎么排列出来的,字节对齐?
0x100007322        0x0000000F        [  1] literal string: ViewController
0x100007331        0x00000002        [  1] literal string: 
0x100007333        0x0000000C        [  2] literal string: AppDelegate
0x10000733F        0x00000016        [  2] literal string: UIApplicationDelegate
0x100007355        0x00000009        [  2] literal string: NSObject
0x10000735E        0x0000000E        [  4] literal string: SceneDelegate
0x10000736C        0x00000016        [  4] literal string: UIWindowSceneDelegate
0x100007382        0x00000010        [  4] literal string: UISceneDelegate
0x100007392        0x00000002        [  4] literal string: 

DATA.objc_const 分析

844467404376de9bd61082b4c7b538791db00c.jpg

没有属性
0x100008110        0x00000020        [  1] __OBJC_$_INSTANCE_METHODS_ViewController

一个属性 40 + 24 + (104 - 32) = 136
0x100008178        0x00000028        [  1] __OBJC_$_INSTANCE_VARIABLES_ViewController
0x100008110        0x00000068        [  1] __OBJC_$_INSTANCE_METHODS_ViewController
0x1000081A0        0x00000018        [  1] __OBJC_$_PROP_LIST_ViewController

两个属性 (152 - 104) + (72 - 40) + (40 - 24) = 96
0x100008110        0x00000098        [  1] __OBJC_$_INSTANCE_METHODS_ViewController
0x1000081A8        0x00000048        [  1] __OBJC_$_INSTANCE_VARIABLES_ViewController
0x1000081F0        0x00000028        [  1] __OBJC_$_PROP_LIST_ViewController

DATA.objc_ivar 分析

25d996c33decc2ee91488153b134122ad93180.jpg

一个属性
0x1000094B0        0x00000004        [  1] _OBJC_IVAR_$_ViewController._myString1
两个属性
0x100009510        0x00000004        [  1] _OBJC_IVAR_$_ViewController._myString1
0x100009514        0x00000004        [  1] _OBJC_IVAR_$_ViewController._myString2

总结:通过以上的介绍,我们掌握了“从二进制文件层面分析编码对包大小的影响”的分析方法,用此方法我们可以继续分析“函数、direct、block”对包大小的影响。

编码上优化包大小的 Tips

属性动态化

探索对属性添加@dynamic 会对二进制文件产生的影响,探索方式跟上面一样,还是通过 link file map 的符号分析:

@interface ViewController()

// 对比myString1属性有无添加@dynamic修饰的区别
@property (nonatomic, copy) NSString *myString1;

@end
TEXT.text 分析

b50528421acc293e8bd598fd6c297d6c53b91f.jpg

// 添加dynamic修饰没有以下部分
// 16 + 20 + 20 = 56
0x100006318        0x00000010        [  1] -[ViewController myString1]
0x100006328        0x00000014        [  1] -[ViewController setMyString1:]
0x10000633C        0x00000014        [  1] -[ViewController .cxx_destruct]

说明添加了@dynamic修饰不会生成 getter、setter,并且没有实例变量,自然也不需要在dealloc中执行置 nil 的代码。

TEXT.objc_methname 分析

d9c85aa078e8e650fc993484051856f299c6f9.jpg

// 添加dynamic修饰没有以下部分
// 10 + 14 + 11 = 35
0x1000065FC        0x0000000A        [  1] literal string: myString1
0x100006606        0x0000000E        [  1] literal string: setMyString1:
0x100006622        0x0000000B        [  1] literal string: _myString1

说明添加了@dynamic修饰不会生成 getter、setter,并且没有实例变量,自然就不需要生成对应的常量符号。

TEXT.objc_methtype 分析

533074f86164ccd128e660b485159c9c37342d.jpg

// 添加dynamic修饰没有下面的符号
// 但是在计算添加dynamic收益时,不能将这个计算入内,原因是一个项目一般都是这些符号的
0x1000073E2        0x0000000C        [  1] literal string: @"NSString"
TEXT.cstring 分析

420daf9455fbe2ad6f4856cfca1d3a688a6077.jpg

一个属性
0x100007EEC        0x0000001D        [  1] literal string: T@"NSString",&,N,V_myString1
dynamic属性
0x100007EF9        0x00000013        [  1] literal string: T@"NSString",&,D,N

【T@"NSString",&,N,V_myString1】和【T@"NSString",&,D,N】是属性类型字符串, property type stringapple 的官方文档中有这个介绍

https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtPropertyIntrospection.html#//apple_ref/doc/uid/TP40008048-CH101 // 关于属性类型字符串介绍

DATA.objc_const 分析

435b61699eb6aa235ad750c1d505ad28a12a29.jpg

一个属性
0x100008110        0x00000068        [  1] __OBJC_$_INSTANCE_METHODS_ViewController
0x100008178        0x00000028        [  1] __OBJC_$_INSTANCE_VARIABLES_ViewController

dynamic属性
0x100008110        0x00000020        [  1] __OBJC_$_INSTANCE_METHODS_ViewController

(104 - 32) + 40 = 112

实例方法列表没有 myString1 的 getter、setter。

DATA.objc_ivar 分析

e8aa11f07d5d86fd4e86233e0f3e77d0105ad4.jpg

dynamic 属性没有以下符号
0x1000094B0        0x00000004        [  1] _OBJC_IVAR_$_ViewController._myString1

没有实例变量,自然就没有_OBJC_IVAR_$_ViewController._myString1符号。

结论:用@dynamic优化一个属性,收益是(229 - 12)= 217B,12 是__objc_methtype 不能算进来;应用场景是对 model 类添加@dynamic。

方法调用、函数调用、direct 方法调用

实验方法:
// 对照组
#define Test // 先空实现
void func(ViewController *mSelf)
{
  [mSelf method];
}

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
  [super viewDidLoad];
  [self test];
}

- (void)method
{
  NSLog(@"");
}

- (void)test
{
    // 2000个 Test 宏的调用,注意,对照组的宏是空实现
}

// 方法调用 的 实验
// 修改Test宏
#define Test [self method];

// 函数调用 的 实验
#define Test func(self);

// direct方法调用 的 实验
#define Test [self method];
- (void)test __attribute__((objc_direct));
使用函数的汇编指令:

39958c785ce688ed1b144212900da66123e764.jpg

使用方法的汇编指令:

67a0d2e64ba9ab7e1e60238d66461e753c7a87.gif35332f338f369682fdf65570372e4a499298a7.jpg

以上两个截图的汇编指令基本一样,说明在调用 func(self)函数时,因为函数内部实现过于简单,被编译优化了,直接优化为内敛函数。

修改 func 函数的实现,避开编译优化:

// 不影响调用逻辑的情况下,将函数实现改复杂点
void func(ViewController *mSelf)
{
  if ([mSelf isKindOfClass:[ViewController class]]) {
    [mSelf method];
  }
}

81005c7793d004967ad290e8a746c2cf764b6f.jpg

另外还做了一组方法名长度不同的实验:

  • 短方法名:method
  • 长方法名:methodmethodmethodmethodmethodmethodmethodmethod

62c1be906c571b5d297871917bcf03c0e166cc.jpg

短方法名和长方法名对二进制大小的影响?

分别查看短方法名和长方法名的 TEXT.text

// 短方法名
0x100004560        0x00005FB4        __TEXT        __text
0x1000045CC        0x00005DDC        [  1] -[ViewController test]
// 长方法名
0x100004534        0x00005FB4        __TEXT        __text
0x1000045A0        0x00005DDC        [  1] -[ViewController test]

大小都是0x00005FB4

c3eec4d07a7ce7422cc0071b51a2bd07c79762.gif86528cb09afb90d72065601186fd04b3c065cc.jpg

65b729e55320d7d6983186060e9ae04ec184e9.gifa1184ef20b84c47e0726952fb83b3d26504fa1.jpg

短方法名和长方法名对二进制大小的影响非常小,TEXT.text 的大小是一样的,原因是方法调用主要是操作函数指针,此时方法名长度对此没有影响;

那么主要影响是TEXT.objc_methname

// 短方法名
0x10000A664        0x00000D96        __TEXT        __objc_methname
// 长方法名
0x10000A638        0x00000DC0        __TEXT        __objc_methname

本实验中,函数调用和方法调用的区别?

先对比下方法调用和函数调用,在二进制上的区别:

// 函数和方法的区别
// 函数调用
0x100006448        0x000040CC        __TEXT        __text
// 方法调用
0x100004560        0x00005FB4        __TEXT        __text

区别还是在 TEXT.text 上。

函数调用的汇编:

a4d284953c5da3b254f9725cb2748963564790.jpg

方法调用的汇编:

278819525e4d9e4f1ef9699dba2062810fd327.gifb5cd8a034cd2d09544f3937f22262eacc17dbb.jpg

方法调用需要更多的指令,因此将方法调用改为函数调用,随着调用处越多,包大小的收益越大。

函数调用和 direct 方法调用的区别?

// 函数和direct的区别
// 函数调用
0x100006448        0x000040CC        __TEXT        __text
0x10000A664        0x00000D96        __TEXT        __objc_methname
0x10000BF90        0x00000070        __TEXT        __unwind_info
0x10000C0F0        0x00001358        __DATA        __objc_const
0x10000D448        0x00000038        __DATA        __objc_selrefs
0x10000D480        0x00000018        __DATA        __objc_classrefs
// direct调用
0x1000064C4        0x00004060        __TEXT        __text
0x10000A674        0x00000D8F        __TEXT        __objc_methname
0x10000BF9C        0x00000064        __TEXT        __unwind_info
0x10000C0F0        0x00001340        __DATA        __objc_const
0x10000D430        0x00000028        __DATA        __objc_selrefs
0x10000D458        0x00000010        __DATA        __objc_classrefs

// direct方法调用TEXT.text中没有[ViewController method]符号,但是当method方法内部的实现比较多时,就会有[ViewController method]符号;原因是method过于简单被编译器优化为内联了
// 因为method被内联了,所以__objc_selrefs少了一个符号;但是当method实现复杂了,会多一个method方法的调用,__objc_selrefs就多一个符号

// 函数在TEXT.objc_methname多了以下两个符号
0x10000A664        0x00000006        [  1] literal string: class
0x10000A66A        0x0000000F        [  1] literal string: isKindOfClass:

// 函数调用实例方法列表尺寸比direct方法调用大,原因direct没有实例方法
// 函数调用
0x10000C138        0x00000050        [  1] __OBJC_$_INSTANCE_METHODS_ViewController
// direct方法调用
0x10000C138        0x00000038        [  1] __OBJC_$_INSTANCE_METHODS_ViewController

结论:direct 方法收益和函数调用基本一致,每一处调用优化收益是 3.93b。

函数调用、direct 方法调用为何可以减少二进制文件大小?

正常方法调用:

21288dc45c0386b4b9f7551635ef1712a3eefd.jpg021298b9469d5033552164856b7001f1a0305f.gif

函数调用:

f596ab914b489936b256539e431e62874af84d.jpgd15912220332cb4726b499e6168e359b8d0f06.gif

direct 方法调用:

c92a61e378e21613d8d29589fd50a532d4ff6a.jpgb43cb3d78d054e76fdd331dbfb74cecc70b2cf.gif

x0是储存函数的第一个参数,msgSend的第一个参数肯定是self,所以x0是self
cbz先判断self是否存在,如果不存在,goto 0x1000063ac->ret,ret是return;

- (void)method
{
  NSLog(@"");
}
如果self存在,则执行 NSLog

因为method方法实现过于简单,被编译器优化为内联了。

当方法实现比较简单,使用 direct 修饰,可以优化调用效率,但是包大小收益可能是负收益;因为很多处调用 direct 方法被内联。

回到一开始的问题:函数调用、direct 方法调用可以减少二进制文件大小的原因是因为相比方法调用,指令变少了。

去 Block

实验内容:

// 有block和无block的代码区别
- (void)blockTest
{
    [self.blockProvider blockInterface1:1];
}
- (void)blockTest
{
    [self.blockProvider blockInterface:^{

    }];
}

block 调用会产生哪些符号?

// 一个block调用与两个block调用的区别
// __DATA.const(没有初始化过的常量) 多一个调用就会多一个___block_literal_global符号,这个占用32个字节
0x100008070        0x00000020        [  1] ___block_descriptor_32_e5_v8?0l
0x100008090        0x00000020        [  1] ___block_literal_global
0x1000080B0        0x00000020        [  1] ___block_literal_global.12

// ___block_literal_global就是Block_layout结构体对象
struct Block_layout {
void *isa; int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor *descriptor;
/* Imported variables. */
};

每多一处 block 的调用,会多一个___block_literal_global 符号,占用 32b。

// 无block调用
0x1000060A4        0x00000044        [  1] -[ViewController blockTest]
// 一个block调用
0x100006094        0x00000048        [  1] -[ViewController blockTest]
0x1000060DC        0x00000004        [  1] ___27-[ViewController blockTest]_block_invoke
// 包含_block_invoke这个符号,则说明[ViewController blockTest]方法的实现中,有实现block

// 两个block调用
// 如果有两个block调用,会多出 ___29-[ViewController viewDidLoad]_block_invoke_2

// 有一个block调用比无block调用多出 36 + 4 = 40b
// 有两个block调用比有一个block调用多出24 + 4 = 28b

每多一处 block 的调用,会多一个_block_invoke_2 符号,占用 4b。

block 调用和方法调用,在调用处的汇编指令差别?

方法调用:
- (void)blockTest
{
  [self.blockProvider blockInterface1:1];
}

21d60ec23193c88fb6c267186494349a2d0db0.jpgf5cb2a8761129e55a8e83446610e6914e1a85a.gif

block 调用:

92c4b5e85f9405089b394760b2d9a4ec31ed09.jpg

在调用处的指令,block 调用只是比方法调用多了一个指令,多了 4 个字节(maplinkfile 计算出来的)。

去 Block 优化的示例

含有 block 参数方法的实现:
// 定义含有block的方法
- (void)blockInterface:(void(^)(void))block
{
  [self shill];

  !block ? : block();
}

- (void)shill
{
  NSString *string = @"123";
  if ([string isKindOfClass:[NSString class]]) {
    NSLog(@"123");
  }
}

// 去Block的优化
#define CallBlock(blockProvider,BLOCK)\
[blockProvider shill];\
BLOCK
实验对比:
- (void)blockTest
{
  [self.blockProvider blockInterface:^{
    NSLog(@"block");
  }];
}

- (void)blockTest
{
  CallBlock(self.blockProvider,{
    NSLog(@"block");
  })
}
// 去Block,相当于把block的实现内联了
0x100006064        0x00000050        [  1] -[ViewController blockTest]

// 调用含有block的方法
// TEXT.text
0x10000604C        0x00000048        [  1] -[ViewController blockTest]
0x100006094        0x0000001C        [  1] ___27-[ViewController blockTest]_block_invoke
// DATA.const
0x100008070        0x00000020        [  1] ___block_descriptor_32_e5_v8?0l
0x100008090        0x00000020        [  1] ___block_literal_global
调用含有 block 的方法:

08dd56f8953217524139277310e04be244be96.jpg

去 Block:

d7fdbcf773193a17d212073533d901822d82bb.jpg

结论:去 Block 的主要收益在于减少了___block_literal_global 和_block_invoke 符号,但是去 block 相当于把 block 的实现内联了,_block_invoke 主要大小的占用是 block 的实现,这块内联和_block_invoke 的差异差不多有 0x0000001C(28) - (0x00000050 - 0x00000048) = 20b;

___block_literal_global 固定 20b;

所以去 block 的收益是 40b。

总结介绍了从二进制文件层面分析编码对包大小的影响;

介绍了属性、方法调用、函数调用、direct 方法调用、block 调用在汇编下的差别,让我们在平时编码中,能有个大概的印象,该代码对应的汇编大概是什么样子;

得出一些收益数据:

922d09978b5aebb9f3a275fa6c5a38f35b6fc4.jpg


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK