使用RunLoop来确定首屏渲染时间的方案

如何真正精确地测量应用打开到首屏渲染完成的时间呢?
如何侵入性最少?

想看最终解决方案,下拉到底。

Runloop 与绘制的联系

__CFRunLoopDoObservers(kCFRunLoopBeforeWaiting); //即将进入休眠,会重绘一次界面.在这里将之前视图显示相关的 信息, 通过 CATransaction 提交到 Render Server中。我们能干预的到此为止,可以简单的理解为,即将 显示视图

绘制过程

  1. 当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。
  2. 当Oberver监听的事件到来时,回调执行函数中会遍历所有待处理的UIView/CAlayer 以执行实际的绘制和调整,打包提交到Render Server 中,更新 UI 界面。

回调函数内部的调用栈大概是这样的:

1
2
3
4
5
6
7
8
9
10
11
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
QuartzCore:CA::Transaction::observer_callback:
CA::Transaction::commit();
CA::Context::commit_transaction();
CA::Layer::layout_and_display_if_needed();
CA::Layer::layout_if_needed();
[CALayer layoutSublayers];
[UIView layoutSubviews];
CA::Layer::display_if_needed();
[CALayer display];
[UIView drawRect];

视图何时被绘制到屏幕上?

beforewaiting之前 会触发 runloop observe 的回调,视图信息发送改变, 被标记为需要重绘. Observer 将信息提交到 Render Server 中,我们可以简单的理解为 这时候 已经绘制在屏幕上。
didFinshLaunch 之后改变了视图内容, 被标记为需要重绘。不过不一定要在 CoreAnimation 注册在beforewaiting的回调才会commit

初步想法,利用 CoreAnimation beforeWaiting 回调

在回调时,dispatch_async 一个block 然后,记录block执行的时间。
为什么这样做呢?
dispatch_async 提交的block 会在 beforewaiting 之后的 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ 执行 如果在beforewaiting 回调时机提交的话,这种方式是可行的 :)

如何验证 dispatch_after callback 执行和__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ 的关系 ?

在一次runloop中, 处理 main_dispatch_queue 中block的时机

1
2
3
4
5
6
7
8
9
10
11
// Received mach_msg, wake up
__CFRunLoopDoObservers(kCFRunLoopAfterWaiting);
// Handle msgs
if (wakeUpPort == timerPort) {
__CFRunLoopDoTimers();
} else if (wakeUpPort == mainDispatchQueuePort) {
//GCD当调用dispatch_async(dispatch_get_main_queue(),block)时,libDispatch会向主线程的runloop发送mach_msg消息唤醒runloop,并在这里执行。这里仅限于执行dispatch到主线程的任务,dispatch到其他线程的仍然是libDispatch来处理。
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
} else {
__CFRunLoopDoSource1(); //CADisplayLink是source1的mach_msg触发?
}

dispatch_after 的时候,我们的block 被提交到main_dispatch_queue

  1. 在 didFinishLaunchingWithOptions 设置 breakpoints
  2. dispatch_after 回调设置 breakpoints
  3. 设置__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
  4. __CFRunLoopDoBlocks 设置breakpoints

当走到 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ 之后就会 调用 dispatch_after回调

因为存在 source 1 跳转到第九步的情况,真正可以确定 视图更新的之后的时刻 就是berforewaiting回调 向main_dispatch_queue 添加的block执行时的时间

然而

beforewaiting 之前还是会有很多 runloop 循环, 但是 viewdidappear时 视图内容的确已经显示了啊.
打了断点才发现 不仅在 CoreAnimation 的 beforewaiting 回调中才会 commit , 也可以自行commit.
didfinshlaunchingwithoptions所在的runloop会完成keywindow根视图控制器的渲染。
那我们就在didfinshlaunchingwithoptions 之后 添加个 timer 事件源。就可以搞定


参考

Apple Core Animationa
At a Glance
You may never need to use Core Animation directly, but when you do you should understand the role that Core Animation plays as part of your app’s infrastructure.
Core Animation Manages Your App’s Content
Core Animation is not a drawing system itself. It is an infrastructure for compositing and manipulating your app’s content in hardware. At the heart of this infrastructure are layer objects, which you use to manage and manipulate your content. A layer captures your content into a bitmap that can be manipulated easily by the graphics hardware. In most apps, layers are used as a way to manage the content of views but you can also create standalone layers depending on your needs.

Apple CATransaction
Overview
CATransaction is the Core Animation mechanism for batching multiple layer-tree operations into atomic updates to the render tree Every modification to a layer tree must be part of a transactioni. Nested toransactions are supported.
Core Animation supports two types of transactions: implicit transactions and explicit transactions. Implicit transactions are created automatically when the layer tree is modified by a thread without an active transaction and are committed automatically when the thread’s runloop next iteratesa.(对一次循环的思考) Explicit transactions occur when the the application sends the CATransaction class a begin() message before modifying the layer tree, and a commit() message afterwards.

Apple CGContext
The CGContext type represents a Quartz 2D drawing destination. A graphics context contains drawing parameters and all device-specific information needed to render the paint on a page to the destination, whether the destination is a window in an application, a bitmap image, a PDF document, or a printer.

YYKit保持界面流畅的技巧 Core Animation 在 RunLoop 中注册了一个 Observer,监听了 BeforeWaiting 和 Exit 事件。这个 Observer 的优先级是 2000000,低于常见的其他 Observer。当一个触摸事件到来时,RunLoop 被唤醒,App 中的代码会执行一些操作,比如创建和调整视图层级、设置 UIView 的 frame、修改 CALayer 的透明度、为视图添加一个动画;这些操作最终都会被 CALayer 捕获,并通过 CATransaction 提交到一个中间状态去(CATransaction 的文档略有提到这些内容,但并不完整)。当上面所有操作结束后,RunLoop 即将进入休眠(或者退出)时,关注该事件的 Observer 都会得到通知。这时 CA 注册的那个 Observer 就会在回调中,把所有的中间状态合并提交到 GPU 去显示;如果此处有动画,CA 会通过 DisplayLink 等机制多次触发相关流程。

深入理解RunLoop

iOS 事件处理机制与图像渲染过程
Observer事件,runloop中状态变化时进行通知。(微信卡顿监控就是利用这个事件通知来记录下最近一次main runloop活动时间,在另一个check线程中用定时器检测当前时间距离最后一次活动时间过久来判断在主线程中的处理逻辑耗时和卡主线程)。这里还需要特别注意,CAAnimation是由RunloopObserver触发回调来重绘,接下来会讲到。
Block事件,非延迟的NSObject PerformSelector立即调用,dispatch_after立即调用,block回调。
Main_Dispatch_Queue事件:GCD中dispatch到main queue的block会被dispatch到main loop执行。
Timer事件:延迟的NSObject PerformSelector,延迟的dispatch_after,timer事件。
Source0事件:处理如UIEvent,CFSocket这类事件。需要手动触发。触摸事件其实是Source1接收系统事件后在回调 __IOHIDEventSystemClientQueueCallback() 内触发的 Source0,Source0 再触发的 _UIApplicationHandleEventQueue()。source0一定是要唤醒runloop及时响应并执行的,如果runloop此时在休眠等待系统的 mach_msg事件,那么就会通过source1来唤醒runloop执行。
Source1事件:处理系统内核的mach_msg事件。(推测CADisplayLink也是这里触发)

渲染时机

上面已经提到过:Core Animation 在 RunLoop 中注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件 。当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。当Oberver监听的事件到来时,回调执行函数中会遍历所有待处理的UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。

这个函数内部的调用栈大概是这样的:

1
2
3
4
5
6
7
8
9
10
11
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
QuartzCore:CA::Transaction::observer_callback:
CA::Transaction::commit();
CA::Context::commit_transaction();
CA::Layer::layout_and_display_if_needed();
CA::Layer::layout_if_needed();
[CALayer layoutSublayers];
[UIView layoutSubviews];
CA::Layer::display_if_needed();
[CALayer display];
[UIView drawRect];

CATransaction in Depth
In Core Animation, transactions are a way to group multiple animation-related changes together. Transactions ensure that the desired animation changes are committed to Core Animation at the same time:

CATransaction.begin()

backingLayer1.opacity = 1.0
backingLayer2.position = CGPoint(x: 50.0, y: 50.0)
backingLayer3.backgroundColor = UIColor.red.cgColor

CATransaction.commit()
Creating a transaction
In the trivial example above, no animations will actually occur. The changes made to layers in this way will be reflected immediately.

As the documentation explains, Core Animation has two types of transactions: implicit and explicit. On threads with a run loop (e.g., the main thread), all changes to a layer tree during a run loop cycle will be implicitly placed in a transaction as long as an explicit transaction isn’t already specified. Note that an implicit transaction is not created for changes to backing layers.1

For standalone layers, explicit transactions aren’t needed to make animated changes:

layer1.opacity = 1.0
layer2.position = CGPoint(x: 50.0, y: 50.0)
layer3.backgroundColor = UIColor.red.cgColor
Implicit transaction involving standalone layers
At the beginning of the run loop cycle before that code is executed, Core Animation will have created a transaction implicitly. After running that code, those standalone layer changes will automatically be encoded as animations. At the end of the run loop cycle, Core Animation commits the implicit transaction, and any enqueued animations created within that time are executed.

牛逼同事的讨论欢迎大佬加入今日头条带我飞

macho&fishhook

導入された

In iOS development, sometimes we hook calls in libSystem for debugging/tracing purposes.
fishhook is a very simple library that enables dynamically rebinding symbols in Mach-O binaries running on iOS in the simulator and on device.
Understanding fishhook’s implementation is great for learning macho and symbol.

Relate Knowledge

Indirect Addressing

Indirect Addressing

Indirect addressing is the name of the code generation technique that allows symbols defined in one file to be referenced from another file, without requiring the referencing file to have explicit knowledge of the layout of the file that defines the symbol. Therefore, the defining file can be modified independently of the referencing file. Indirect addressing minimizes the number of locations that must be modified by the dynamic linker, which facilitates code sharing and improves performance.

When a file uses data that is defined in another file, it creates symbol references. A symbol reference identifies the file from which a symbol is imported and the referenced symbol. There are two types of symbol references: non lazy and lazy.

Non-lazy symbol references are resolved (bound to their definitions) by the dynamic linker when a module is loaded.
A non-lazy symbol reference is essentially a symbol pointer—a pointer-sized piece of data. The compiler generates non-lazy symbol references for data symbols or function addresses.

Lazy symbol references are resolved by the dynamic linker the first time they are used (not at load time). Subsequent calls to the referenced symbol jump directly to the symbol’s definition.
Lazy symbol references are made up of a symbol pointer and a symbol stub, a small amount of code that directly dereferences and jumps through the symbol pointer. The compiler generates lazy symbol references when it encounters a call to a function defined in another file.

lazy_symbol_pointers & non_lazy_symbol_pointers

lazy_symbol_pointers

lazy_symbol_pointers (S_LAZY_SYMBOL_POINTERS)
A lazy_symbol_pointers section contains 4-byte symbol pointers that eventually contain the value of the indirect symbol associated with the pointer. These pointers are used by symbol stubs to lazily bind undefined function calls at runtime. A lazy symbol pointer initially contains an address in the symbol stub of instructions that cause the symbol pointer to be bound to the function definition (in the example in symbol_stubs (S_SYMBOL_STUBS), the lazy pointer L_foo$lazy_ptr initially contains the address for dyld_stub_binding_helper but gets overwritten with the address for _foo). The dynamic link editor binds the indirect symbol associated with the lazy symbol pointer by overwriting it with the value of the symbol.

The static link editor places a copy of a lazy pointer in the output file only if the corresponding symbol stub is in the output file. Only the corresponding symbol stub can make a reference to a lazy symbol pointer, and no global symbols can be defined in this type of section. There must be one indirect symbol associated with each lazy symbol pointer. An example of a lazy_symbol_pointers section is one in which the compiler has generated calls to undefined functions, each of which can be bound lazily at the time of the first call to the function.

non_lazy_symbol_pointers (S_NON_LAZY_SYMBOL_POINTERS)
A non_lazy_symbol_pointers section contains 4-byte symbol pointers that contain the value of the indirect symbol associated with a pointer that may be set at any time before any code makes a reference to it. These pointers are used by the code to reference undefined symbols. Initially these pointers have no interesting value but get overwritten by the dynamic link editor with the value of the symbol for the associated indirect symbol before any code can make a reference to it.

The static link editor places only one copy of each non-lazy pointer for its indirect symbol into the output file and relocates all references to the pointer with the same indirect symbol to the pointer in the output file. The static link editor further can fill in the pointer with the value of the symbol if a definition of the indirect symbol for that pointer is present in the output file. No global symbols can be defined in this type of section. There must be one indirect symbol associated with each non-lazy symbol pointer. An example of a non_lazy_symbol_pointers section is one in which the compiler has generated code to indirectly reference undefined symbols to be bound at runtime—this preserves the sharing of the machine instructions by allowing the dynamic link editor to update references without writing on the instructions.

Symbol_stubs

symbol_stubs

A symbol_stubs section contains symbol stubs, which are sequences of machine instructions (all the same size) used for lazily binding undefined function calls at runtime. If a call to an undefined function is made, the compiler outputs a call to a symbol stub instead, and tags the stub with an indirect symbol that indicates what symbol the stub is for. On transfer to a symbol stub, a program executes instructions that eventually reach the code for the indirect symbol associated with that stub. Here’s a sample of assembly code based on a function func() containing only a call to the undefined function foo():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.text
.align 2
.globl _func
_func:
b L_foo$stub
.symbol_stub
L_foo$stub: ;
.indirect_symbol _foo ;
lis r11,ha16(L_foo$lazy_ptr) ;
lwz r12,lo16(L_foo$lazy_ptr)(r11) ; the symbol stub
mtctr r12 ;
addi r11,r11,lo16(L_foo$lazy_ptr) ;
bctr ;
.lazy_symbol_pointer
L_foo$lazy_ptr: ;
.indirect_symbol _foo ; the symbol pointer
.long dyld_stub_binding_helper ; to be replaced by _foo's address

Fishhook Analysis

1. determine symbol
stub -> symbol stubs -> lazy_symbol_pointers -> indirect symbol table -> symbol table

2. hook symbol
change lazy_symbol_pointers

fishhook Code analysis

First, you should know how the macho refers to the external symbols, Fishhook change the contents of target pointer.

How it works
dyld binds lazy and non-lazy symbols by updating pointers in particular sections of the __DATA segment of a Mach-O binary. fishhook re-binds these symbols by determining the locations to update for each of the symbol names passed to rebind_symbols and then writing out the corresponding replacements.

For a given image, the DATA segment may contain two sections that are relevant for dynamic symbol bindings: nl_symbol_ptr and la_symbol_ptr. nl_symbol_ptr is an array of pointers to non-lazily bound data (these are bound at the time a library is loaded) and la_symbol_ptr is an array of pointers to imported functions that is generally filled by a routine called dyld_stub_binder during the first call to that symbol (it’s also possible to tell dyld to bind these at launch). In order to find the name of the symbol that corresponds to a particular location in one of these sections, we have to jump through several layers of indirection. For the two relevant sections, the section headers (struct sections from ) provide an offset (in the reserved1 field) into what is known as the indirect symbol table. The indirect symbol table, which is located in the LINKEDIT segment of the binary, is just an array of indexes into the symbol table (also in LINKEDIT) whose order is identical to that of the pointers in the non-lazy and lazy symbol sections. So, given struct section nl_symbol_ptr, the corresponding index in the symbol table of the first address in that section is indirect_symbol_table[nl_symbol_ptr->reserved1]. The symbol table itself is an array of struct nlists (see ), and each nlist contains an index into the string table in LINKEDIT which where the actual symbol names are stored. So, for each pointer nl_symbol_ptr and la_symbol_ptr, we are able to find the corresponding symbol and then the corresponding string to compare against the requested symbol names, and if there is a match, we replace the pointer in the section with the replacement.

Small Example Help you understand lazy/non_lazz symbol
The process of looking up the name of a given entry in the lazy or non-lazy pointer tables looks like this:

As you see, call imp___stubs___dyld_get_image_vmaddr_slide rather than _dyld_get_image_vmaddr_slide.

callmethod.png

In __stubs section.

stubimpaddress.png

__la_symbol_ptr section
la_symbol_ptr.png

stup_helper jmp dyld_stub_binder determine symbol address
stub_helper.png
stup_helper.png

static void rebind_symbols_for_image(struct rebindings_entry rebindings,
const struct mach_header
header,
intptr_t slide) {

printf(“fishhookLog slide %ld \n”, slide);
Dl_info info;
if (dladdr(header, &info) == 0) {
return;
}

segment_command_t cur_seg_cmd;
segment_command_t
linkedit_segment = NULL;

/*

  • The symtab_command contains the offsets and sizes of the link-edit 4.3BSD
  • “stab” style symbol table information as described in the header files
  • and .
    /
    struct symtab_command
    symtab_cmd = NULL;

// This is the second set of the symbolic information which is used to support
//
the data structures for the dynamically link editor.
struct dysymtab_command* dysymtab_cmd = NULL;

uintptr_t cur = (uintptr_t)header + sizeof(mach_header_t);
printf(“fishhookLog cur %ld \n”, cur);
//macho header 之后 就是 load command
for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
cur_seg_cmd = (segment_command_t )cur;
//#define LC_SEGMENT_ARCH_DEPENDENT LC_SEGMENT_64
// LC_SEGMENT_ARCH_DEPENDENT LC_SYMTAB LC_DYSYMTAB 都是加载命令类型
// 根据加载命令 可以拿到对我们有用的信息(想清楚 macho segement 和 section 的关系)
// load command 可以 load segement 和 section
if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
if (strcmp(cur_seg_cmd->segname, SEG_LINKEDIT) == 0) {
linkedit_segment = cur_seg_cmd;
}
} else if (cur_seg_cmd->cmd == LC_SYMTAB) {
symtab_cmd = (struct symtab_command
)cur_seg_cmd;
} else if (cur_seg_cmd->cmd == LC_DYSYMTAB) {
dysymtab_cmd = (struct dysymtab_command*)cur_seg_cmd;
}
}

// release 去掉调试信息估计就拿不到了?
if (!symtab_cmd || !dysymtab_cmd || !linkedit_segment ||
!dysymtab_cmd->nindirectsyms) {
return;
}

// Find base symbol/string table addresses

// uint64_t fileoff; / file offset of this segment / ? 偏移量要减?
uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff;

// uint32_t symoff; / symbol table offset /
// 符号表
nlist_t symtab = (nlist_t )(linkedit_base + symtab_cmd->symoff);
// 字符串表
char strtab = (char )(linkedit_base + symtab_cmd->stroff);

// Get indirect symbol table (array of uint32_t indices into symbol table)
uint32_t indirect_symtab = (uint32_t )(linkedit_base + dysymtab_cmd->indirectsymoff);

cur = (uintptr_t)header + sizeof(mach_header_t);

// 遍历load commands 可以拿到segement 相关的信息
for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
cur_seg_cmd = (segment_command_t *)cur;
if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {

  // 跳过 _DATA _DATA_CONST segment
  if (strcmp(cur_seg_cmd->segname, SEG_DATA) != 0 &&
      strcmp(cur_seg_cmd->segname, SEG_DATA_CONST) != 0) {
    continue;
  }

  // 遍历segment 中的 sections
  for (uint j = 0; j < cur_seg_cmd->nsects; j++) {

    // section pointer
    section_t *sect =
      (section_t *)(cur + sizeof(segment_command_t)) + j;

    // flags 是 搞毛的?
    // 无论lazy / no_lazy 直接改 symtab 的
    if ((sect->flags & SECTION_TYPE) == S_LAZY_SYMBOL_POINTERS) {
      perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
    }
    if ((sect->flags & SECTION_TYPE) == S_NON_LAZY_SYMBOL_POINTERS) {
      perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
    }
  }
}

}
}

Questions

Why need stubs section ?

Shared library. We want multiple processes to share the same macho TEXT segment, but macho will reference the symbols of other macho. If TEXT is variable, we must use multiple TEXT segment.DATA is variable ,we can use stub in TEXT and have Stubs section in DATA, save Indirect symbol.

iOS中的Debug

Use lldb Debug

lldb

Use Command-Line Tool Debug

Mac OS App
Debug Running process

  1. lldb
  2. process attach --pid 250 / process attach --name neteasemusic

sample

Simulator App

  1. Run iOS Simulator
  2. lldb
  3. process attach -n <App name> --waitfor
  4. launch your app in the simulator(not use Xcode launch)

Set breakpoint

OC breakpoint set --selector alignLeftEdges:
C breakpoint set --name foo
C++ breakpoint set --method foo
File line breakpoint set --file foo.c --line 12

frame variables

frame variable

Getting Information About the Current Frame
frame info

Sample

1
2
3
lldb sample
breakpoint set --name main
run

Use lldb debug lldb

  1. build lldb
  2. lldb <lldb builded>
  3. breakpoint set …
  4. run
  5. use your builded lldb process attach --pid 501

iOS Debug info

helloworld.c

1
2
3
4
5
6
7
8
9
#include <stdio.h>
int globalVar = 5;
int main()
{
int a = 10;
int b = 9;
printf("Hello, World!");
return 0;
}

clang -g helloworld.c

Generate a.out.dSYM.

DSYM Information extraction

_debug_info

Global variable

1
2
3
4
5
6
7
0x0000002a: variable [2]
name( "globalVar" )
type( {0x0000003f} ( int ) )
external( true )
decl file( "/Users/xiejunyi/Library/Mobile Documents/com~apple~CloudDocs/学习记录/Debug学习/TestDebug_info/helloworld.c" )
decl line( 2 )
location( [0x0000000100001018] )

Local variable a

1
2
3
4
5
6
0x0000005f: variable [5]
location( fbreg -8 )
name( "a" )
decl file( "/Users/xiejunyi/Library/Mobile Documents/com~apple~CloudDocs/学习记录/Debug学习/TestDebug_info/helloworld.c" )
decl line( 5 )
type( {0x0000003f} ( int ) )

DWARF Debugging Format

dwarfdump -e --debug-info

Location

Introduction to the DWARF Debugging Format
Michael J. Eager, Eager Consulting February, 2007

WARF provides a very general scheme
to describe how to locate the data rep­ resented by a variable. A DWARF location expression contains a sequence of opera­ tions which tell a debugger how to locate the data. Figure 7 shows DIEs for three variables named a, b, and c. Variable a has a fixed location in memory, variable b is in register 0, and variable c is at offset –12 within the current function’s stack frame. Although a was declared first, the DIE to describe it is generated later, after all func­ tions. The actual location of a will be filled in by the linker.

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
Location xpressions
ED
Functions and Subprograms
D
fig7.c:
1: int a;
2: void foo()
3: {
4: register int b;
5: int c;
6: }
<1>: DW_TAG_subprogram DW_AT_name = foo
<2>: DW_TAG_variable DW_AT_name = b
DW_AT_type = <4>
DW_AT_location =
(DW_OP_reg0)
<3>: DW_TAG_variable
DW_AT_name = c
DW_AT_type = <4>
DW_AT_location =
(DW_OP_fbreg: -12)
<4>: DW_TAG_base_type
DW_AT_name = int
DW_AT_byte_size = 4
DW_AT_encoding = signed
<5>: DW_TAG_variable DW_AT_name = a
DW_AT_type = <4>
DW_AT_external = 1
DW_AT_location =
(DW_OP_addr: 0)
Figure 7. DWARF description of variables a, b, and c.

Like the sample, so we try to get variable:(X86-64)

  1. global variable
  2. local variable
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
(lldb) br s -f helloworld.c -l 6
Breakpoint 1: where = a.out`main + 29 at helloworld.c:6, address = 0x0000000100000f6d
(lldb) run
Process 7965 launched: '/Users/xiejunyi/Library/Mobile Documents/com~apple~CloudDocs/学习记录/Debug学习/TestDebug_info/a.out' (x86_64)
Process 7965 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
frame #0: 0x0000000100000f6d a.out`main at helloworld.c:6
3 int main()
4 {
5 int a = 10;
-> 6 int b = 9;
7 printf("Hello, World!");
8 return 0;
9 }
Target 0: (a.out) stopped.
(lldb) register read
General Purpose Registers:
rax = 0x0000000100000f50 a.out`main at helloworld.c:4
rbx = 0x0000000000000000
rcx = 0x00007ffeefbff858
rdx = 0x00007ffeefbff738
rdi = 0x0000000100000faa "Hello, World!"
rsi = 0x00007ffeefbff728
rbp = 0x00007ffeefbff700
rsp = 0x00007ffeefbff6f0
r8 = 0x0000000000000000
r9 = 0xffffffff00000000
r10 = 0x00007fff900900c8 atexit_mutex + 24
r11 = 0x00007fff900900d0 atexit_mutex + 32
r12 = 0x0000000000000000
r13 = 0x0000000000000000
r14 = 0x0000000000000000
r15 = 0x0000000000000000
rip = 0x0000000100000f6d a.out`main + 29 at helloworld.c:6
rflags = 0x0000000000000206
cs = 0x000000000000002b
fs = 0x0000000000000000
gs = 0x0000000000000000
// fp - 8 = rbp -8 = 7ffeefbff6f8
(lldb) x/w 7ffeefbff6f8
0x7ffeefbff6f8: 0x0000000a
(lldb) p &a
(int *) $0 = 0x00007ffeefbff6f8
(lldb) x/w 0x0000000100001018
0x100001018: 0x00000005
(lldb) p &globalVar
(int *) $1 = 0x0000000100001018

Get Local Variable:

  1. get current stack frame
  2. add offset

Implementation Print In iOS Simulator

Feasibility test

  1. dump debug-info

    1
    dwarfdump -e --debug-info /Users/xiejunyi/Library/Developer/Xcode/DerivedData/PrintSample-hfwdmnmnyklbuaalffwqzfqzsgby/Build/Products/Debug-iphonesimulator/PrintSample.app.dSYM/Contents/Resources/DWARF > debug_info.txt
  2. find target ivar symbol

1
2
3
4
5
6
7
0x0003b04f: variable [111]
name( "test_global" )
type( {0x0003b064} ( int ) )
external( true )
decl file( "/Users/xiejunyi/Library/Mobile Documents/com~apple~CloudDocs/学习记录/Debug学习/PrintSample/PrintSample/ViewController.m" )
decl line( 12 )
location( [0x0000000100003e38] )
  1. get target ivar macho slide
    use _dyld_register_func_for_add_image(_rebind_symbols_for_image);

    fishhook

  2. get address
    pa = va + slide

  3. read value
    use pointer

We can use x86-64 function calling conventions.
In x86-64, function call will:

  1. push rbp
  2. rbp register get value from rsp register
1
2
pushq %rbp
movq %rsp, %rbp

so we can get the old rbp from stack!

Let’s try:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
(lldb) register read
General Purpose Registers:
rax = 0x615198fac76f0071
rbx = 0x000000010a0cc736 "count"
rcx = 0x000000000000010f
rdx = 0x000000010de6a340 libsystem_pthread.dylib`_thread
rdi = 0x00007f95be8326b0
rsi = 0x00007f95be832000
rbp = 0x00007ffee6e8cb10
rsp = 0x00007ffee6e8cad0
r8 = 0x00007ffee6e8c8e0
r9 = 0x0000000000000040
r10 = 0x0000000000000000
r11 = 0x0000000000000206
r12 = 0x0000000000000018
r13 = 0x00007f95bd507790
r14 = 0x000000010ba04904 UIKit`_UIApplicationLinkedOnVersion
r15 = 0x0000000109695940 libobjc.A.dylib`objc_msgSend
rip = 0x0000000108d707ec PrintSample`-[ViewController viewDidLoad] + 108 at ViewController.m:30
rflags = 0x0000000000000206
cs = 0x000000000000002b
fs = 0x0000000000000000
gs = 0x0000000000000000
(lldb) register read
General Purpose Registers:
rax = 0x615198fac76f0071
rbx = 0x000000010a0cc736 "count"
rcx = 0x000000000000010f
rdx = 0x000000010de6a340 libsystem_pthread.dylib`_thread
rdi = 0x00007f95bd507790
rsi = 0x0000000108d719a0 "test_method"
rbp = 0x00007ffee6e8cac0
rsp = 0x00007ffee6e8cac0
r8 = 0x00007ffee6e8c8e0
r9 = 0x0000000000000040
r10 = 0x003c7701003c4600
r11 = 0x0000000108d70850 PrintSample`-[ViewController test_method] at ViewController.m:38
r12 = 0x0000000000000018
r13 = 0x00007f95bd507790
r14 = 0x000000010ba04904 UIKit`_UIApplicationLinkedOnVersion
r15 = 0x0000000109695940 libobjc.A.dylib`objc_msgSend
rip = 0x0000000108d7085c PrintSample`-[ViewController test_method] + 12 at ViewController.m:39
rflags = 0x0000000000000246
cs = 0x000000000000002b
fs = 0x0000000000000000
gs = 0x0000000000000000
(lldb) x/w 0x00007ffee6e8cac0
0x7ffee6e8cac0: 0xe6e8cb10

work well.

  1. get offset frome dsym eh_frame debug_info
  2. get rbp
  3. get address
  4. get value
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)viewDidLoad {
[super viewDidLoad];
int test_a = 15;
[self easyRead];
}
- (void)easyRead {
long r_rbp;
asm ("movq %%rbp, %0\n"
: "=m"(r_rbp));
long *rbp_pointer = (long *)r_rbp;
long old_rbp = *rbp_pointer;
long address = old_rbp + -36;
int *ivar_p = (int *)address;
NSLog(@"%d", *ivar_p);
}

Log:
2018-01-21 18:26:01.638456+0800 PrintSample[37629:6251770] 15

But when you open llvm optimization, This method will not be able to work.

Use lldb read/write memory

1
2
3
4
5
(lldb) p &a
(int *) $1 = 0x00007ffeefbff808
(lldb) x/w 0x00007ffeefbff808
0x7ffeefbff808: 0x00000000
1
2
(lldb) x/2xw 0x00007ffee6e8cac0
0x7ffee6e8cac0: 0xe6e8cb10 0x00007ffe

more x command use can reference

Use lldb read/write register

register read/x(format)
register write r9 2

iOS Debug Practical

UITableView setDelegate: + dealloc Trap

  1. hook tableView setDelegate:
1
2
3
4
5
6
7
// TableView
- (void)hook_setDelegate:(id)delegate {
__weak weakSelf = self;
[self hook_setDelegate:delegate];
}

Can you find any mistakes?

crash: crash Cannot form weak reference to instance :(

First, look at my crash call stack,

callstack

As you see., After dealloc, Called my hooked method hmd_setDelegate:.

Then look at the assembly code or [UIScrollView dealloc]

dealloc
Comments:objc_msgSend and setDelegate:

At this time, your probaly understand why,In the object destruction phase, you are using…the object.

Reference

The LLDB Debugger
Apple’s “Lazy” DWARF Scheme
Introduction to the DWARF Debugging Format

Understand iOS rendering

In TouTiao interview, I did not fully understand the issue.

How to understand Core graphincs rendering?

Core Animation Pipeline

  1. Application Core Animation
    • Commit Transaction
  2. Render Server Core Animation
    • Decode
    • Draw Calls
  3. GPU
    • Render
  4. Display
    • Display

The application builds a view hierarchy that indirectly with UIKit or directly with Core Animation.

But the application process is actually not doing the actual rendering work for Core Animation.The view hierarchy is committed to the render server.Render server is a separate process.Core Animation uses OpenGL or metal render views.Once the view hierarchy is being rendered we can finally display it to the user.

Understand the Core Animation Render pipeline is helpful for our working.

The first thing that happens in the application, you receive an event probably because of a touch. In this case, we want to update a view hierarchy.We call the commit transaction on our application.The view hierarchy is then encoded and sent to the render server.

Render server first decode this view hierarchy.Then starts issuing draw calls for the GPU.

OpenGL or metal review sources and finally start rendering and so the GPU starts doing its rendering work.

If this rendering work finishes before the next resync, we can swap in the frame buffer and show the view hierarchy to the user.

Developers Can Affect

The commit transaction stage affects application developers the most.

  1. layout phase set up the views
  2. display phase draw the views
  3. prepare to commit phase we do some additional Core Animation work.??
  4. Package and Sent

In the Layout phase, the layoutSubviews overrides are invoked.View creation happens, Layers were added to the hierarchy.This phase is usually CPU bound or I/O bound.

The second phase is the Display phase.This is where the draw contents this drawRect if it’s overridden or does string drawing.This phase is actually CPU or memory bound.We use core graphics for this rendering.So we usually do this rendering with CGContext.We want to avoid a large performance set at this stage.

The next phase is the prepare phase.This is where image decoding and image conversion happens.

In the last phase, we package up the layer and sent them to the render server.

This process is recursive, and you have to reiterate over the whole layer tree, and this is expensive.So this is why we want to keep the layer tree as flat as possible to make sure that this part of phase and as efficient as it can be.

Animation Work

Animation themselves is a three-stage process.Two of those happen inside the application and the last stage happens on the render server.

  1. create animation
  2. prepare and commit your animation
  3. render server

The first stage is where we create the animation, update view hierarchy.The second stage is where we prepare and commit your animation.We don’t just commit the view hierarchy, we commit as well the animation.For a reason, because we would like to handle the animation work to the render server so that we can continue to update our animation without using interprocess communication to talk back to the application or force them back to the application.
Not only commit the view hierarchy but also commit as well animation(guess like animation information).

Render Concepts

  1. application built a view hierarchy with Core Animation
  2. committed to render server
  3. decoded it and render it use OpenGL or Metal
  4. generate with OpenGL command and submitted to a GPU
  5. Vertex Processing “Vertex Shader” and Tiling
  6. Output in Parameter Buffer
  7. Pixel Processing “Pixel Shader”
  8. Written to Render Buffer

CoreGraphics memory consumption

100px * 100px rect

use memory: 100 100 4byte = 40000byte

4byte is R,G,B,A

Conclusion

First, Remember Core Animation Draw Pipeline.
Second, Core Graphics use CPU and memory.I think CPU will compute values in memory.In GPU, use GPU and GPU memory.

Reference:
WWDC 2014 session 419

理解地址翻译

地址翻译过程

页面命中时:

  1. CPU生成虚拟地址,传送给MMU
  2. MMU从页表基址寄存器拿到页表的物理地址根据VA中的VPN得到对应页表项的地址,从内存中取回PTE给MMU
  3. MMU利用PTE中的PPN,加上VPO得到物理地址,从主存中取出数据返回给CPU

页面未命中时:

  1. CPU生成虚拟地址,传送给MMU
  2. MMU从页表基址寄存器拿到页表的物理地址根据VA中的VPN得到对应页表项的地址,从内存中取回PTE给MMU
  3. PTE有效位为0,MMU触发了一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序
  4. 缺页处理异常程序确定出物理内存中的牺牲页,如果这个页面已经被修改了,则把它换出到磁盘
  5. 缺页处理程序调入新的页面,并更新内存中的PTE
  6. 缺页处理程序返回到原来的进程,再次执行导致缺页的指令

结合高速缓存:
大多数系统是使用物理地址来访问高速缓存,使用物理寻址,多个进程同时在高速缓存中有存储块和共享来自相同虚拟页面的块成为很简单的事情。而且高速缓存无需处理保护问题,因为访问权限的检查是地址翻译过程的一部分。

主要的思路是,地址翻译发生在高速缓存查找之前

物理地址可以被分为:
CT: 标记,对应于物理地址的PPN, 避免PPO相同,导致CT,CO相同,CT用来做判断是否是确切的地址。
CI: 组索引
CO: 块偏移

理解
CT,CI,CO 利用这些东西来判断唯一性没毛病,因为他们是从唯一的PA拆分出来的。

可以确定数据在高速缓存中的位置。

利用TLB加速地址翻译:
TLB是一个小的 虚拟寻址的缓存,其中每一行都保存着一个由单个PTE组成的块。
VA中的VPN可以被分为TLBT和TLBI两个部分用于虚拟寻址。
TLBT: 标记,区别可能映射到同一个TLB组的不同VPN
TLBI: 组索引

CPU 发送 VA 给 MMU ,MMU 检查 TLB 是否缓存对应的 PTE
如果缓存,则利用相应的PTE计算PA 去高速缓存/内存中查找数据
如果不缓存,则计算PTE的物理地址,去内存/高速缓存中取出 PTE给MMU, MMU 计算PA,去内存中查找数据。

使用到的各种中介
页表:可以提供虚拟页和物理页的对应关系
TLB: 根据VPN查找缓存的PTE
SRAM: 根据PA查找缓存的物理内存中的数据

使用到的地址
VA: 虚拟地址,可以分为 VPN + VPO
PA: 物理地址, 可以分为 PPN + PPO, 虚拟地址对应的物理地址,VPO 和 PPO 相同,因为页面大小是相同的。可以分为 CI,CO,CT 用于查找高速缓存中的数据。
VPN: 虚拟页号,可以分为 TLBI,TLBT用于查找 TLB项。

思考 物理寻址和虚拟寻址 对于数据共享的影响
物理寻址:地址是唯一的,根据PA 就能判断是否是 同一份数据。前面VA->PA之后,就可以共享了。
虚拟寻址:使用VA,看TLB的处理,可能会导致多个不同进程的相同的虚拟地址得到同样的结果,不知道TLB在多个进程之间是怎么处理了。

虚拟地址做数据共享应该参考进程数据共享,判断是否已经映射对象随后改进程的页表项映射到同样的物理地址,对比物理寻址直接确定内存中的物理地址,是麻烦了一些。

共享对象
虚拟内存系统集成道传统的文件系统中。
进程1将一个共享对象映射到它的虚拟内存中的一个区域。进程2也将一个共享对象映射到它的虚拟内存中的一个区域。因为每个对象都有一个唯一的文件名,内核可以迅速的判断进程1已经映射了这个对象,而且可以使进程2中的页表条目指向相应的物理页面。

理解会随着经验和知识的积累 改变
Keep Update

理解线程安全

线程安全问题的来源

线程安全是编程中的术语,指某个函数、函数库在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。

可以看到这一切的诱因就是因为共享变量。如果我们的线程执行过程中,没有相互影响,就不会出现问题。

当我们的线程访问共享变量时,我们无法预测操作系统是否将为我们的线程选择一个正确的顺序。这就尴尬了!

对多个线程对资源的访问,我们称之为 竞争。

由于两个或者多个进程竞争使用不能被同时访问的资源,使得这些进程有可能因为时间上推进的先后原因而出现问题,这叫做竞争条件(Race Condition)。

竞争条件分为两类

  1. Mutex 不能被多个进程同时使用的资源
  2. Synchronization 两个或多个进程彼此指针存在内在的制约关系
  • 消费者生产者问题就是同步问题,它需要调度对共享资源的访问,是 Synchronization。
  • 读者写者问题是互斥问题的一个概括。

消费者生产者问题

因为插入和取出项目都涉及更新共享变量,所以我们必须保证对缓冲区的访问是互斥的。但是只保证互斥访问是不够的,我们还需要调度对缓冲区的访问。如果缓冲区是满的,那么生产者必须等待直到有一个槽位变为可用。与之相似,如果缓冲区是空的,那么消费者必须等待直到有一个项目变为可用。

读者-写者问题

读者写者问题是互斥问题的一个概括。一组并发的线程要访问一个共享对象。写者必须拥有对对象独占的访问。

如何保证线程安全

在我们需要访问共享变量的情况下,我们需要保证线程执行顺序的正确性。或者我们尽量避免使用共享变量。

进度图
如果我们用 纵横坐标分别表示两个进程执行的指令顺序,那么操作共享变量的指令会构成一个二维的不安全区,当两条线程的执行轨迹会同时访问不安全区时,我们就认为执行是不安全的。(局限性:无法描述多处理器并发执行)

我们可以选择安全的轨迹线,或者使用信号量实现互斥。

思考一下,锁做了什么?

lock是在执行多线程时用于强行限制资源访问的同步机制,即用于在并发控制中保证对互斥要求的满足.
对于竞争条件中的 Mutex 我们可以使用互斥锁处理。Synchronization 可以使用条件锁。
使用lock是可以保证线程安全的,但不能保证线程的执行顺序。

原子性

原子性(Atomic),一个事务包含多个操作,这些操作要么全部执行,要么全都不执行。

OC 中一个很经典的面试题是, property 设置 atomic 能保证线程安全吗?
很多人都回答可以,这是压根没理解线程安全的体现。
原子性不能保证线程安全
原子性可以保证写操作一小块代码段是互斥的,但是并不能保证线程安全。

设置atomic之后,只是保证了 属性的赋值操作是互斥的,可惜只是该属性..
不能保证我们整个代码的线程安全。

考虑一段代码.

1
2
3
4
5
6
self.a = 0;//1
if (self.a == 0) {
print("safe");
} else {
print("unsafe");
}

当 self.a = 0; 执行完毕后

处理机调度,切换到另一个线程执行

1
self.b = 0;

再次切换为原先的线程
此时,结果是
unsafe.
我们原先的值被其他线程篡改了。并不能保证线程安全。

会想一下 那经典的 进度图,原子性无法阻止多个线程访问不安全区。

加锁之后,就保证 代码块 不会被多个进程访问,保证了线程安全。

1
2
3
4
5
6
7
8
lock();
self.a = 0;//1
if (self.a == 0) {
print("safe");
} else {
print("unsafe");
}
unlock();

不可变性

不可变性,这个跟线程安全关系大吗?
显然,我们对共享变量的访问,会导致线程安全问题是因为我们对其进行了写操作。不可变可以避免代码编写中因为疏忽导致的问题。真正处理线程安全的时候,你遇到的,会是可变的共享变量!
所以,这个对线程安全问题,没有用处。

理解会随着经验和知识的积累 改变
Keep Update

iOS 开源项目的Xcode项目结构构建

最近在维护自己的开源项目XJYChart,希望使用Xcode 构建起一套方便维护,调试,发布的项目结构。

目标是,在一个workplace下,包含 开源项目的project,Demo的project,Demo的project中含有Swift和OC的两个demo Target.让多个target embed 同一个 framework,并且支持改变framework的代码,编译后自动更新embeded framework.

1
2
3
4
5
6
-- workspace
-- XJYChart.xcodeproj
-- XJYChartDemo.xcodeproj
-- XJYChartDemo-OC
-- XJYChartDemo-Swift
-- Pods.xcodeproj

制作框架的static framework

教程很多,一搜一大把,就不详细说了。

  1. 新建framework
  2. Mach-O Type 设置为 static libiary
  3. 让framework 支持多个平台

编译,生成 framework

Demo project

新建工程,随后添加一个target。
这里我们Embedded Binaries中添加 我们上一步生成的framework就可以了

workspace

新建一个workspace,将上面创建的两个project 拖进workspace.
如果demo target 依赖其他库,可以使用cocoapods.

框架依赖其他第三方

  1. 在同一个project 内,建立XJYChart Target , 和 Colours Target
  2. linked Frameworks and libiraries, XJYChart 添加 Colours.framework
  3. Demo 工程中,embeded XJYChart.framework 和 Colours.framework

Apps with Dependencies Between Frameworks

直接拖framework 随后 link, 模拟器没有问题。iOS11.1 也没有问题,iOS 11.2 不行。于是我改成了这种方法。

结果

这时候,我的开源库的项目结构支持

  1. Swift demo 和 OC demo ,使用同一份框架编译的产物
  2. 改动框架的代码,在demo编译后会自动更新framework产物。
  3. 框架的代码脱离demo环境
  4. 打开workspace,在一个窗口下维护demo和框架的代码

什么是Flutter的革命【翻译】

什么是Flutter?

Flutter移动应用程序SDK是一种构建快速,美观的移动应用程序的新方式,可帮助开发人员摆脱过去常见的“cookie切割”应用程序。 试过Flutter的人真的喜欢它; 例如,看这个这个,或者这个。 或者,这是第三方编辑的文章和视频列表

就像任何新的系统一样,人们想知道Flutter的不同,或者换句话说,“Flutter有什么新鲜和令人兴奋的东西?”这是一个公平的问题,本文将从技术角度来回答它 - 而不是 只是什么是令人兴奋的,但为什么。
但是,首先,有一点历史。

移动应用程序开发的简要历史

移动应用程序开发是一个相对较新的领域。 第三方开发人员已经能够在不到十年的时间内开发移动应用程序,所以工具仍在不断发展并不奇怪。

OEM SDKs

Apple iOS SDK于2008年发布,2009年发布Google Android SDK。这两个SDK分别基于不同的语言:Objective-C和Java。
nativeApp

您的应用程序会与平台进行交谈,以创建widgets或访问相机等服务。 widgets呈现给屏幕画布,并且事件被传回给widgets。 这是一个简单的架构,但是您几乎必须为每个平台创建单独的应用程序,因为这些widgets是不同的,更不用说语言。

WebView

第一个跨平台框架基于JavaScript和WebViews。 例如Titanium和一系列相关的框架:PhoneGap,Apache Cordova,Ionic等等。 在苹果发布iOS SDK之前,他们鼓励第三方开发者为iPhone构建webapps,所以用web技术构建跨平台的应用程序是一个显而易见的步骤。
webviewApp

您的应用程序创建HTML并将其显示在平台上的WebView中。 请注意,像JavaScript这样的语言很难直接与本地代码(like thee services)交谈,所以它们会经历一个在JavaScrip领域和Native 领域之间进行上下文切换的“Bridge”。 因为平台服务通常不是经常被调用的,所以这不会导致太多的性能问题。

Reactive Views

ReactJS(和其他)这样的响应式Web框架已经变得流行,主要是因为它们通过使用从响应式编程中借用的编程模式来简化Web视图的创建。 2015年,创建了React Native将响应式视图的诸多好处带给移动应用程序。
React native

React Native非常受欢迎(并且是值得的),但是由于JavaScript领域访问Native领域的OEM widgets,因此它也必须通过这个桥梁。 通常访问widgets的频率非常高(在动画,转换过程中,或者用户用手指在屏幕上滑动某些东西时,每秒可达60次),因此可能会导致性能问题。 正如一篇关于React Native的文章所说:

这里是理解React Native性能的主要关键之一。 每个领域本身都非常快。 当我们从一个领域转移到另一个时,性能瓶颈往往会发生。 为了构建高性能的React Native应用程序,我们必须保持桥梁通过最低限度。

Flutter

像React Native一样,Flutter也提供响应式风格的视图。 Flutter使用编译的编程语言即Dart的方法来避免JavaScript桥引起的性能问题,。 Dart被“提前编译”(AOT)编译成多个平台的本地代码。 这使得Flutter可以与平台进行通信,而无需通过执行了上下文切换的JavaScript Bridge。 编译为本机代码也可以提高应用程序的启动时间。
Flutter是唯一提供响应式视图而不需要JavaScript Bridge的移动SDK的事实应该足以让Flutter变得有趣并且值得尝试,但是Flutter还有一些更具革命性的地方,那就是它如何实现widgets。

Widgets

Widgets是影响和控制应用程序的视图和界面的元素。 说Widgets是移动应用程序最重要的部分之一,这并不是夸大其词。 事实上,Widgets可以成就或挫败一个应用程序。
gif1

  • Widgets的外观和感觉是最重要的。 Widgets需要看起来不错,包括各种屏幕尺寸。 他们也需要感觉自然。
  • Widgets必须快速执行:创建Widgets树,inflate the Widgets(实例化他们的子项),将它们放在屏幕上,渲染它们,或者(特别是)使它们动画化。
  • 对于现代应用程序,Widgets应该是可扩展的和可定制的。 开发人员希望能够添加令人愉快的新Widgets,并自定义所有Widgets以匹配应用程序的品牌。

Flutter有一个新的架构,包括外观和感觉不错,快速,可定制和可扩展的Widgets。 没错,Flutter不使用OEM Widgets(或DOM WebViews),它提供了自己的Widgets。

flutterapp

Flutter将Widgets和渲染器从平台移动到应用程序中,从而使其可以自定义和扩展。 Flutter对平台的需求平台是一个画布,在这个画布中,Widgets可以呈现在设备屏幕上,并可以访问事件(触摸,定时器等)和服务(位置,摄像机等)。
Dart程序(绿色)和本地平台代码(iOS或Android蓝色)之间仍然存在一个接口,可以进行数据编码和解码,但这可能比JavaScript Bridge 快几个数量级。

将Widgets和渲染器移动到应用程序中会影响应用程序的大小。 Android上Flutter应用程序的最小大小约为6.7MB,与使用类似工具构建的最小应用程序类似。 由您决定是否Flutter的好处是值得的权衡,所以本文的其余部分讨论这些好处。

布局

Flutter最大的改进之一就是它的布局。布局根据一组规则(也称为约束)来确定widgets的大小和位置。
传统上,布局使用一堆可应用于(虚拟)任何widgets的规则。规则实现了多种布局方法。让我们以CSS布局为例,因为它是众所周知的(尽管和Android和iOS的布局基本相似)。 CSS具有属性(规则),这些属性应用于HTML元素(widgets)。 CSS3定义了375个属性。
CSS包含许多布局模型,包括(多个)框模型,浮动元素,表格,多列文本,分页媒体等等。其他的布局模型,如flexbox和grid,后来被添加,因为开发人员和设计师需要更多的控制布局,并使用表和透明图像来获得他们想要的。在传统布局中,开发人员无法添加新的布局模型,因此必须将flexbox和网格添加到CSS并在所有浏览器上实施。
传统布局的另一个问题是规则可以相互影响(甚至相互冲突),并且元素通常应用了许多规则。这使布局变慢。更糟的是,布局表现通常是N阶有序的,所以随着元素数量的增加,布局变得更加缓慢。
Flutter是由Google的Chrome浏览器团队成员进行的一项实验开始的。如果我们忽略了传统的布局模型,我们想看看是否可以建立更快的渲染器。几周后,我们取得了显着的业绩增长。我们发现:

  • 大多数布局相对简单,比如:滚动页面上的文本,大小和位置仅取决于显示大小的固定矩形,以及一些表格,浮动元素等等。
  • 大多数布局对于widgets子树是局部的,并且该子树通常使用一个布局模型,因此这些widgets只需要少量的规则支持。
    我们意识到,如果我们使用如下的要点,布局可以被大大简化:
  • 每个widget都可以指定自己的简单布局模型,而不必拥有大量可应用于任何widget的布局规则。
  • 因为每个widget都有一个小得多的布局规则,布局可以大大优化。
  • 为了进一步简化布局,我们把几乎所有东西都变成了一个widget

这里是Flutter代码来创建一个布局简单的widget树:

1
2
3
4
5
6
7
new Center(
child: new Column(
children: [
new Text('Hello, World!')),
new Icon(Icons.star, color: Colors.green)
]
)

这个代码的语义是足够的,你可以很容易地想象它会产生什么,但是这里得到的结果是:

Center

在这个代码中,一切都是widget,包括布局。Center widget将其中心放置在其父级(例如屏幕)中。Colume布局widget垂直排列其子元素(widget列表)。该列包含一个文本widget和一个图标widget(它有一个属性,它的颜色)。
在Flutter中,居中和填充是widget。主题是widget,适用于他们的孩子。甚至应用程序和导航都是widget。
Flutter包含了很多用于布局的widget,不仅包括列,还包括行,网格,列表等。此外,Flutter还有一个独特的布局模型,我们称之为“sliver layout model”,用于滚动。 Flutter中的布局非常快,可以用于滚动。想一想。滚动必须是瞬间发生的和平滑的,以至于用户感觉像屏幕图像在他们拖过物理屏幕时被附着到他们的手指。
通过使用滚动布局,Flutter可以实现高级类型的滚动,如下所示。请注意,这些动画GIF图像,Flutter更平滑。你可以(也应该)自己运行这些应用程序;请参阅本文末尾的参考资料部分。

gif2_1
gif2_2
git2_3

大多数时候,Flutter可以一次完成布局,这意味着线性时间,所以它可以处理大量的widgets。 Flutter也做缓存和其他事情,所以可以避免布局。

定制设计

因为widgets现在是应用程序的一部分,所以可以添加新的widgets,并且可以定制现有的widgets以使其具有不同的外观或感觉,或匹配公司的品牌。 移动设计的趋势远离几年前常见的cookie应用程序,并且朝向取悦用户并赢得奖项的定制设计。
Flutter为Android,iOSMaterial Design提供了丰富的,可自定义的widgets集(事实上,我们已经知道,Flutter是Material Design中最高保真实现之一)。 我们使用Flutter的可定制性来构建这些widgets集,以匹配多个平台上的本机widgets的外观和风格。 应用程序开发人员可以使用相同的可定制性来进一步调整窗口widgets,以满足他们的需求。

更多关于 Reactive Views

用于reactive web views的库引入了virtual DOM。 DOM是HTML文档对象模型(HTML Document Object Model),一个使用JavaScript用来处理HTML文档的API,用一个元素树来表示。 虚拟DOM是使用编程语言中的对象创建的DOM的抽象版本,在这种情况下是JavaScript。

在 reactive web views(由ReactJS等系统实现)中,虚拟DOM是不可变的,每当有任何变化时,都会从头开始重建。 将虚拟DOM与真实的DOM进行比较,生成一组最小的更改,然后执行这些更改以更新真实的DOM。 最后,平台重新渲染真实的DOM并将其绘制到画布中。

reactive_web_views_update

这可能听起来是很多额外的工作,但它是非常值得的,因为操纵HTML DOM是非常昂贵的
React Native做类似的事情,但对于移动应用程序。 它不是DOM,而是操纵移动平台上的原生widgets。 它不是虚拟DOM,而是构建一个widgets的虚拟树,并将其与本机widgets进行比较,只更新那些已更改的widgets。

rn_update

请记住,React Native必须通过Bridge与Native widgets进行通信,因此widgets的虚拟树有助于将Bridge过程消耗保持在最低限度,同时仍允许使用本机窗口小widgets。 最后,一旦Native widgets被更新,平台将把它们呈现在画布上。
React Native是移动开发的一大胜利,是Flutter的灵感来源,但Flutter更进一步。

flutter_update

回想一下,在Flutter中,widgets和渲染器已经从平台上升到用户的应用程序中。没有原生的OEM widget tree可以操作,那么virtual widget tree现在是widget tree。 Flutter渲染widget tree并将其绘制到平台画布上。这是很好,简单(和快速)。另外,动画发生在用户空间中,所以应用程序(以及开发者)对其有更多的控制。

Flutter渲染器本身很有趣:它使用几个内部树结构来渲染那些需要在屏幕上更新的小widgets。例如,渲染器使用“structural repainting using compositing”(”structural”意味着是通过widget,比通过屏幕上的矩形区域更有效)。不变的widgets,甚至是那些已经移动的小widgets,都是从高速缓存中“bit blited”的。这是Flutter中即使在高级滚动(在上面讨论和示出)中滚动如此执行的事情之一。
为了仔细了解Flutter渲染器,我推荐这个视频。你也可以看看代码,因为Flutter是开源的。当然,您可以自定义甚至替换整个堆栈,包括渲染器,合成器,动画,手势识别器和(当然)widgets。

Dart编程语言

因为Flutter就像使用反应视图的其他系统一样,为每个新帧刷新视图树,所以它创建了许多只能存活一帧(六十分之一秒)的对象。 幸运的是,Dart使用对这类系统非常有效的“分代垃圾收集”,因为对象(尤其是短命的)相对cheap。 另外,对象的分配可以用single pointer bump来完成,这是快速的并且不需要lock。 这有助于避免UI jank和stutter。
Dart也有一个“tree shaking”的编译器,其中只包括您的应用程序需要的代码。 即使只需要其中的一个或两个,也可以使用大型widgets库。

Hot reload

Flutter最受欢迎的功能之一是其快速,有状态的Hot reload。 您可以在Flutter应用程序运行时对其进行更改,并重新加载已更改的应用程序的代码,并让代码从停止的位置继续,通常不到一秒钟。 如果您的应用程序遇到错误,您通常可以修复错误,然后继续,如同错误从未发生过。 即使你必须做一个完整的重载,速度也很快。
hot_fix

开发人员告诉我们,这可以让他们“绘制”他们的应用程序,一次做出一个更改,然后几乎立即看到结果,而不必重新启动应用程序。

兼容性

由于widgets(以及这些widgets的渲染器)是您应用程序的一部分,而不是平台的一部分,因此不需要“兼容库”。您的应用程序不仅可以正常工作,而且在最近的操作系统版本(Android Jelly Bean和更新的版本以及iOS 8.0和以上的版本)上也可以发挥同样的作用。这大大减少了在较旧的OS版本上测试应用程序的需要。另外,您的应用很可能会在未来的操作系统版本上运行。
我们被问到一个潜在的问题。由于Flutter不使用OEM native widgets,当新版本的iOS或Android支持新类型的OEM native widgets或更改现有OEM native widgets的外观或行为时,Flutter widgets 是否需要更新才会更新?

  • 首先,Google是Flutter的内部用户,所以我们有强烈的动机去更新这些Widget集合,以保持它们的当前状态,并尽可能接近当前的OEMwidgets。
  • 如果有一段时间我们在更新widget时速度太慢,那么Google并不是Flutter的唯一有激励让widget保持最新状态的用户。 Flutter的widgets是可扩展和可定制的,任何人都可以更新它们,甚至是你。甚至不必提交一个拉请求。你永远不用等待Flutter本身的更新。
  • 以上几点仅适用于您希望在应用程序中反映新的变化。如果您不希望更改影响您的应用的外观或行为方式,那就太好了。widget是你的应用程序的一部分,所以一个widget将永远不会在你的操作外发生改变,使你的应用程序看起来不好(或更糟的是,打破你的应用程序)。
  • 作为一个额外的好处,你可以编写你的应用程序,使它使用新的widget甚至在旧的操作系统版本中。

其他Benifits

Flutter的简单性使其变得更加快速,但是普遍的可定制性和可扩展性使其变得强大。
Dart有一个软件包的存储库,所以你可以扩展你的应用程序的功能。 例如,有许多软件包可以轻松访问Firebase,因此您可以构建“serverless”应用程序。 外部贡献者已经创建了一个包,允许您访问Redux数据存储。 还有一些名为“plugins”的软件包,可以以独立于操作系统的方式轻松访问平台服务和硬件,如加速度计或摄像头。

当然,Flutter也是开源的,加上Flutter渲染栈是你的应用程序的一部分,意味着你可以自定义几乎任何你想要的一个单独的应用程序。 这个图中的所有绿色都可以定制:

framework_Ebgub

所以,“Flutter有什么新的和令人兴奋的?”

如果有人问你有关Flutter,现在你知道如何回答他们:

  • 反应式视图的优点,没有JavaScript Bridge
  • 快速,流畅,可预测; 代码将AOT编译为本机(ARM)代码
  • 开发人员可以完全控制widgets和布局
  • 带有漂亮,可定制的widgets
  • 极好的的开发人员工具,惊人的hot reload
  • 性能更高,兼容性更多,更有趣

你有没有注意到我从这个清单中删除了什么? 当人们谈论Flutter时,这通常是人们首先提到的,但对于我来说这是Flutter最不感兴趣的事情之一。
事实上,Flutter可以从一个代码库为多个平台构建美丽而快速的应用程序。 当然,这应该是一个给定的! 这是可定制和可扩展性,可以轻松地将Flutter目标锁定到多个平台,而不会放弃性能或功耗。

翻译原文链接

Method Swizzling 安全性分析 和 RSSwizzle解决方案分析

iOS 平台开发,有时会使用到Method Swizzling, 但Method Swizzling 在使用过程中有许多需要注意的问题,本文将介绍将会产生的问题,并且分析 RSSwizzle 是如何解决这些问题的。

在Objective-C 中方法交换有什么危险

What are the Dangers of Method Swizzling in Objective C?
stackoverflow 上的这个回答十分精彩。

- Method swizzling 并不是原子操作
- 改变了不是我们自己代码的行为
- 有可能出现命名冲突
- Swizzling 改变方法的参数
- Swizzles 顺序问题
- 难于理解
- 难于Debug

RSSwizzle

RSSwizzle 实现方式

1. 根据block生成NEW IMP
2. replace 目标方法的实现
3. block可以获取原来的IMP

核心交换代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
static void swizzle(Class classToSwizzle,
SEL selector,
RSSwizzleImpFactoryBlock factoryBlock)
{
Method method = class_getInstanceMethod(classToSwizzle, selector);
NSCAssert(NULL != method,
@"Selector %@ not found in %@ methods of class %@.",
NSStringFromSelector(selector),
class_isMetaClass(classToSwizzle) ? @"class" : @"instance",
classToSwizzle);
NSCAssert(blockIsAnImpFactoryBlock(factoryBlock),
@"Wrong type of implementation factory block.");
__block OSSpinLock lock = OS_SPINLOCK_INIT;
// To keep things thread-safe, we fill in the originalIMP later,
// with the result of the class_replaceMethod call below.
__block IMP originalIMP = NULL;
// This block will be called by the client to get original implementation and call it.
RSSWizzleImpProvider originalImpProvider = ^IMP{
// It's possible that another thread can call the method between the call to
// class_replaceMethod and its return value being set.
// So to be sure originalIMP has the right value, we need a lock.
OSSpinLockLock(&lock);
IMP imp = originalIMP;
OSSpinLockUnlock(&lock);
if (NULL == imp){
// If the class does not implement the method
// we need to find an implementation in one of the superclasses.
Class superclass = class_getSuperclass(classToSwizzle);
imp = method_getImplementation(class_getInstanceMethod(superclass,selector));
}
return imp;
};
RSSwizzleInfo *swizzleInfo = [RSSwizzleInfo new];
swizzleInfo.selector = selector;
swizzleInfo.impProviderBlock = originalImpProvider;
// We ask the client for the new implementation block.
// We pass swizzleInfo as an argument to factory block, so the client can
// call original implementation from the new implementation.
id newIMPBlock = factoryBlock(swizzleInfo);
const char *methodType = method_getTypeEncoding(method);
NSCAssert(blockIsCompatibleWithMethodType(newIMPBlock,methodType),
@"Block returned from factory is not compatible with method type.");
IMP newIMP = imp_implementationWithBlock(newIMPBlock);
// Atomically replace the original method with our new implementation.
// This will ensure that if someone else's code on another thread is messing
// with the class' method list too, we always have a valid method at all times.
//
// If the class does not implement the method itself then
// class_replaceMethod returns NULL and superclasses's implementation will be used.
//
// We need a lock to be sure that originalIMP has the right value in the
// originalImpProvider block above.
OSSpinLockLock(&lock);
// originIMP get value from here
originalIMP = class_replaceMethod(classToSwizzle, selector, newIMP, methodType);
OSSpinLockUnlock(&lock);
}

具体过程解析

如果hook的方法在hook的类中有实现

1. block生成新的IMP
2. 替换IMP, 这时候拿到原始的ORIGINIMP
3. block接受了一个RSSwizzleInfo参数,从参数中可以拿到当时保存的获得IMP的block
4. 由于block中存储的是originIMP ,所以获得的是原始的实现

如果hook的方法在子类中无实现

1. block生成新的IMP
2. 替换IMP(由于没有实现,相当于add了IMP), 原始的实现为nil
3. block 中我们调用calloriginIMP,这个方法实际调用了一个block originalImpProvider
4. 这个block的从父类找到相应的实现(注意,这里实际上是在 调用方法 时才会触发)

这种情况是调用时,动态获得 当时的方法实现,所以可以避免hook顺序带来的问题。

那么影响Swizzle的结果到底是是什么呢?

Swizzle实现方式本质上就是改变方法的IMP 为 NewIMP, 并调用原先的originIMP

只hook一个是没问题,但是涉及到多次hook, 并且hook的方法可能为一个时, 他们的顺序就会导致不同的结果,因为顺序不同,Swizzle时,Method 相应的 IMP 不相同。
这里我们更关注,父子类+hook 同一个方法产生的问题。

那么RSSwizzle 怎么解决问题的呢?

父类有method, 子类没有实现method.
我们有如下的IMP:superIMP,superNewIMP,subNewIMp
此时,我们Swizzle 父类的method 为 superNewIMP
Swizzle子类的method 为 subNewIMp

首先关注我们期望的调用顺序

1. subNewIMP
2. superNewIMP
3. superIMP

我们先hook父类,再hook子类后,的调用顺序

1. subNewImp
2. superNewIMP
3. superIMp

先hook子类再hook父类

1. subNewIMP
2. superIMP

为什么会有差异?
因为当我们在hook子类方法时,原先的方法实现是不同的。

解决问题
那就要保证,即使hook的顺序不同,也能正确取到相应的IMP
那我们保证,子类在调用相应方法的时候,取到的IMP是父类当前的IMP就可以(这样就和Swizzle的时间顺序没有了关系)

RSSwizzle 加锁,保证线程安全

originalImpProvider 的代码

1
2
3
4
5
6
7
8
9
10
RSSWizzleImpProvider originalImpProvider = ^IMP{
OSSpinLockLock(&lock);
IMP imp = originalIMP;
OSSpinLockUnlock(&lock);
if (NULL == imp){
Class superclass = class_getSuperclass(classToSwizzle);
imp = method_getImplementation(class_getInstanceMethod(superclass,selector));
}
return imp;
}

Swizzle 方法的某个部分

1
2
3
OSSpinLockLock(&lock);
originalIMP = class_replaceMethod(classToSwizzle, selector, newIMP, methodType);
OSSpinLockUnlock(&lock);

这两个方法有个共享变量 originalIMP,这就意味着,可能会出现线程安全问题。再仔细看下代码

1
2
3
4
OSSpinLockLock(&lock);
IMP imp = originalIMP;
OSSpinLockUnlock(&lock);
if (NULL == imp){

这个共享变量,和条件判断相关。敏锐的同学一眼就能看出来,在不加锁的情况下,当不同的线程对 这两段代码进行执行的时候,就会出现,即使if (NULL == imp){通过了,但实际上,另一条线程执行了class_replaceMethod()。这时就会出现问题。

在加锁之后,在同一时间段内,只有一个线程能访问改变这个变量的代码。避免了共享变量导致的线程安全问题。

采用Block添加实现,没有命名冲突问题

连命名的机会都没有…

1
2
3
4
5
6
7
8
9
10
11
12
13
RSSwizzleInstanceMethod(classToSwizzle,
@selector(calculate:),
RSSWReturnType(int),
RSSWArguments(int number),
RSSWReplacement(
{
// The following code will be used as the new implementation.
// Calling original implementation.
int res = RSSWCallOriginal(number);
// Returning modified return value.
return res + 1;
}), 0, NULL);

采用block 添加实现,只是改变了原来的IMP ,Selector没有改变,实现的_cmd并没有改变

参数_cmd是当前方法的selector

swizzle method 可能会导致的_cmd 参数改变,例如
我们有originMethodnewMethod,他们分别对应着 OriginIMP,和NewIMP.
当我们交换方法实现后:

1
2
originmethod -> NewIMP
newMethod -> OriginIMP

要想调用原来的实现,我们需要调用 newMethod 这就导致了 相同的IMP 但是_cmd 却改变了

iOS Hook 方案解析

最近研究了一下iOS平台上几个hook框架的hook方案,写文记录一下分析的过程

现有hook框架

  1. AnyMethodLog
  2. Aspects

AnyMethodLog hook 方案分析

Hook 代码

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
//替换方法
BOOL qhd_replaceMethod(Class cls, SEL originSelector, char *returnType) {
Method originMethod = class_getInstanceMethod(cls, originSelector);
if (originMethod == nil) {
return NO;
}
const char *originTypes = method_getTypeEncoding(originMethod);
IMP msgForwardIMP = _objc_msgForward;
#if !defined(__arm64__)
if (qhd_isStructType(returnType)) {
//Reference JSPatch:
//In some cases that returns struct, we should use the '_stret' API:
//http://sealiesoftware.com/blog/archive/2008/10/30/objc_explain_objc_msgSend_stret.html
//NSMethodSignature knows the detail but has no API to return, we can only get the info from debugDescription.
NSMethodSignature *methodSignature = [NSMethodSignature signatureWithObjCTypes:originTypes];
if ([methodSignature.debugDescription rangeOfString:@"is special struct return? YES"].location != NSNotFound) {
msgForwardIMP = (IMP)_objc_msgForward_stret;
}
}
#endif
IMP originIMP = method_getImplementation(originMethod);
if (originIMP == nil || originIMP == msgForwardIMP) {
return NO;
}
//把原方法的IMP换成_objc_msgForward,使之触发forwardInvocation方法
class_replaceMethod(cls, originSelector, msgForwardIMP, originTypes);
//把方法forwardInvocation的IMP换成qhd_forwardInvocation
class_replaceMethod(cls, @selector(forwardInvocation:), (IMP)qhd_forwardInvocation, "v@:@");
//创建一个新方法,IMP就是原方法的原来的IMP,那么只要在qhd_forwardInvocation调用新方法即可
SEL newSelecotr = qhd_createNewSelector(originSelector);
BOOL isAdd = class_addMethod(cls, newSelecotr, originIMP, originTypes);
if (!isAdd) {
DEV_LOG(@"class_addMethod fail");
}
return YES;
}

阐述一下具体的过程:

  1. 如何让方法每次都走_objc_msgForward呢?把原来的 sel的IMP改成_objc_msgForward.

  2. 这时我们需要保存原来的 IMP 然后hook forwardInvocation … 换成自己的实现,调用原来的IMP和新增的代码

从代码很明显的可以看出,这是利用OC的消息转发机制,选择了合适的时机,进行打桩。
相较于传统的Swizzle方法,这种方法打主桩,是有可行性的。
并且在ForwardInvocation: 处理,虽然相较其余两个转发机制调用的方法的消耗大,但是更灵活一些,最切合问题。

Aspects

Aspects 的代码我看的比较仔细,相对于AnyMethodLog, Aspects 对Hook的处理更成熟,各种情况都做了考虑,这里来重点分析下。

相较于AnyMethodLog, Aspects 不仅可以hook类,也可以对实例进行hook, 粒度更小,适用的场景更加多样化。

这是Aspects Hook 的代码,可以看到对实例和类的处理是不同的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
static Class aspect_hookClass(NSObject *self, NSError **error) {
NSCParameterAssert(self);
Class statedClass = self.class;
Class baseClass = object_getClass(self);
NSString *className = NSStringFromClass(baseClass);
// Already subclassed
if ([className hasSuffix:AspectsSubclassSuffix]) {
return baseClass;
// We swizzle a class object, not a single object.
}else if (class_isMetaClass(baseClass)) {
return aspect_swizzleClassInPlace((Class)self);
// Probably a KVO'ed class. Swizzle in place. Also swizzle meta classes in place.
}else if (statedClass != baseClass) {
return aspect_swizzleClassInPlace(baseClass);
}
// Default case. Create dynamic subclass.
const char *subclassName = [className stringByAppendingString:AspectsSubclassSuffix].UTF8String;
Class subclass = objc_getClass(subclassName);
if (subclass == nil) {
subclass = objc_allocateClassPair(baseClass, subclassName, 0);
if (subclass == nil) {
NSString *errrorDesc = [NSString stringWithFormat:@"objc_allocateClassPair failed to allocate class %s.", subclassName];
AspectError(AspectErrorFailedToAllocateClassPair, errrorDesc);
return nil;
}
aspect_swizzleForwardInvocation(subclass);
aspect_hookedGetClass(subclass, statedClass);
aspect_hookedGetClass(object_getClass(subclass), statedClass);
objc_registerClassPair(subclass);
}
object_setClass(self, subclass);
return subclass;
}
```
先看看对实例的处理
```objectivec
subclass = objc_allocateClassPair(baseClass, subclassName, 0);
aspect_swizzleForwardInvocation(subclass);
aspect_hookedGetClass(subclass, statedClass);
aspect_hookedGetClass(object_getClass(subclass), statedClass);
objc_registerClassPair(subclass);
object_setClass(self, subclass);

熟悉kvo原理的同学,一眼就应该看明白了,这是做了什么事情。
这里可谓是相当巧妙的避免了父类和子类实例hook相同的IMP可能导致的循环调用问题。(下一部分会说明如何避免的)

对类的hook和AnyMethodLog十分类似。就不再多阐述了。网上相关介绍 使用 forwardInvocation+hook类 的资料很多。

Aspects和AnyMethodLog都是利用了forwardInvocation进行处理,这是一致的。

现行Hook方案的问题

自己经常hook的同学可能会发现,在hook时,会出现调用循环的问题。

无论是AnyMethodLog 和 Aspects 都无法同时hook 父类和子类的同一个方法到一个相同的IMP上。为什么呢?

思考一下为什么会出现循环调用? 那必定是,调用方又被调用者调用了一次,在iOS Hook 中,如果我们hook 了 父类和子类的同一个方法,让他们拥有相同的实现,就会出现这种问题。

看一下阿里🌟的阐述,我觉得还是很清晰的
基于桥的全量方法Hook方案 - 探究苹果主线程检查实现
假设我们现在对UIView、UIButton都Hook了initWithFrame:这个方法,在调用[[UIView alloc] initWithFrame:]和[[UIButton alloc] initWithFrame:]都会定向到C函数qhd_forwardInvocation中,在UIView调用的时候没问题。但是在UIButton调用的时候,由于其内部实现获取了super initWithFrame:,就产生了循环定向的问题。

Aspects 中,Hook 之前,是要对能否hook 进行检查了,对于类,有严格的限制,对于实例则没有限制。

类为什么要限制,上面已经阐释了,那么实例为什么可以呢?

这就是 实例Hook 实现方式所产生的结果。

来理一下实例hook怎么实现的:

  1. 生成子类
  2. hook 子类的forwardInvocation(这是一系列操作,不过这个尤为重要)
  3. 对实例的类信息进行伪装

如果我们有 ClassA 的 实例 a, SubClassA 的 实例 suba.
对他们进行hook viewdidload 方法, 那么会生成两个子类,我们记为prefix_ClassA, prefix_SubClassA,我们对forwardInvocation IMP的替换,实际上是在这两个类上进行的。

当方法调用时:
suba -> forwardInvocation(我们替换的IMP) ->self viewdidload(SubClassA 的IMP) -> super viewdidload(ClassA的实现)
这显然不会导致循环的问题。

如果不采用对生成的子类hook,就会出现问题,可以过一遍方法调用的流程。

总之,还是父类子类的相同方法是否是同一个IMP的核心问题。

随后,Aspects 做了 如果是真正的消息转发响应的处理,有兴趣的同学可以看一下。
JSPatch 的方法替换也是利用了 forwardInvocation进行处理。

方法混写和ISA混写

今天无意在 feiox 留给我的书上发现了这个定义。
再把 Aspects 和 AnyMethodLog 的实现相结合,发现书上说的真的贴切。

  • 方法混写
    • 影响一个类的所有实例
    • 高度透明,所有对象的类都不变
    • 需要特殊的覆盖方法实现
  • ISA混写
    • 只影响目标实例
    • 对象的类会变化(不过可以通过覆盖class方法隐藏)
    • 覆盖方法使用标准的子类技术实现的

实战中犯过的错误

  1. 父子类 使用category hook 同一个方法,并且hook的新方法名还是一样的

// TableViewSetDelegate call
// tableView.setdelegate -> scrollView.setdelegate

// HOOK
// TableView.hmd_set -> TABLEOriginSETIMP
// TableView.set -> TABLEhmdSETIMP
// ScrollView.hmd_set -> SCROLLoriginSETIMP
// ScrollView.set -> SCROLLhmd_setIMP

// tableView.setdelegate -> TABLEhmdSETIMP -> TABLOEriginSETIMP -> ScrollView.hmd_set -> TABLEOriginSETIMP

// tableview category TABLEOriginSETIMP 覆盖了 UIScrollView category hmd_set 的实现

// 导致 ScrollView.hmd_set 和 TableView.hmd_set 指向了 同一个 IMP

callstack