ARC, vòng tham chiếu, truy cập độc quyền và các lối thoát unsafe
Swift quản lý thực thể class bằng đếm tham chiếu tự động. Không phải bộ thu gom rác dạng truy vết: không luồng chạy nền, không khựng, và bộ nhớ được giải phóng ngay khoảnh khắc tham chiếu cuối cùng biến mất. Đổi lại, việc đếm là công việc thật, và ARC không tự gỡ được vòng tham chiếu.
ARC chèn lệnh retain và release quanh mọi chỗ một tham chiếu được lưu hoặc truyền đi. Phần lớn
bị trình tối ưu xoá bỏ, nhưng những lệnh sống sót chính là lý do một vòng lặp chặt trên các thực thể
class có thể chậm hơn nhiều lần so với cùng vòng lặp đó trên struct — đếm tham chiếu là thao tác
nguyên tử, nên mỗi lần retain là một phép đồng bộ hoá, kể cả trong code đơn luồng.
Đây là lập luận cụ thể cho việc dùng kiểu giá trị ở đường nóng, và cho việc dùng final:
final class Node { var value = 0 }
final loại bỏ khả năng có lớp con, nhờ đó trình biên dịch khử ảo hoá được lời gọi phương thức và
đôi khi đưa hẳn thực thể lên stack.
Hai đối tượng giữ tham chiếu mạnh tới nhau sẽ giữ nhau sống mãi mãi:
final class Parent { var child: Child? }
final class Child { var parent: Parent? } // ← rò rỉ
Cách sửa là làm cho một chiều không sở hữu:
final class Child { weak var parent: Parent? }
weak — trở thành nil khi đối tượng đích chết, nên bắt buộc phải là Optional và var.
Tốn một mục trong bảng phụ và một lần kiểm tra ở mỗi lần truy cập.unowned — không trở thành nil; truy cập sau khi đích đã chết là sập chương trình. Rẻ hơn,
và đúng đắn khi tham chiếu thật sự không thể sống lâu hơn đích của nó.Hãy ưu tiên weak trừ khi bạn nói được vì sao vòng đời được bảo đảm. “Nó sập trên máy người dùng”
là kết cục tệ hơn “nó nil và mình đã xử lý”.
Vòng tham chiếu phổ biến nhất trong ứng dụng thật là một closure bị chính đối tượng mà nó bắt giữ nắm lại:
final class ImageLoader {
var onFinish: (() -> Void)?
func load() {
fetch { data in
self.store(data) // self bị bắt giữ mạnh; self lại giữ onFinish
}
}
}
Danh sách bắt giữ phá vỡ vòng đó:
fetch { [weak self] data in
guard let self else { return }
self.store(data)
}
guard let self else { return } là cách viết chuẩn — nó giữ một tham chiếu mạnh trong suốt thân
closure, nhờ vậy self không thể bị giải phóng giữa chừng.
Mẹo
Không phải closure nào cũng cần [weak self]. Một closure chạy xong rồi được giải phóng ngay —
array.map { self.transform($0) } — không thể tạo ra vòng tham chiếu, vì chẳng ai lưu nó lại.
Thêm [weak self] ở đó chỉ là nhiễu, và tập cho người đọc thói quen bỏ qua chú thích ấy đúng
chỗ nó thật sự quan trọng.
Swift bắt buộc một biến không được truy cập để ghi trong khi nó đang được truy cập bởi việc khác. Một phần được bắt lúc biên dịch, phần còn lại lúc chạy:
var total = 0
func accumulate(into value: inout Int, from other: Int) { value += other }
accumulate(into: &total, from: total) // ổn: `from` được đọc trước khi gọi
// swap(&total, &total) // lỗi: truy cập chồng lấn
Luật này tồn tại để trình tối ưu có quyền giả định rằng tham số inout không bị trùng bí danh —
chính điều đó làm cho việc sửa tại chỗ các kiểu giá trị chạy nhanh. Kiểm tra độc quyền lúc chạy bật
ở bản debug và, từ Swift 5, bật cả ở bản release — nếu bạn từng thấy dòng “Simultaneous accesses to
0x…”, chính là nó.
Đôi khi bạn cần chính những byte đó. Họ Unsafe*Pointer trao chúng cho bạn, kèm một quy ước đặt tên
nói rõ bạn đang cầm sợi dây dài tới đâu:
var samples = [Float](repeating: 0, count: 1024)
samples.withUnsafeMutableBufferPointer { buffer in
for index in buffer.indices {
buffer[index] *= 0.5
}
}
Dạng closure withUnsafe… là cách an toàn để làm chuyện không an toàn: con trỏ chỉ hợp lệ trong
phạm vi closure, không dài hơn một lệnh nào. Luật tối thượng là không bao giờ để nó thoát ra ngoài —
var escaped: UnsafeMutableBufferPointer<Float>?
samples.withUnsafeMutableBufferPointer { escaped = $0 } // treo lơ lửng ngay khi closure trả về
— vì mảng có toàn quyền cấp phát lại vùng đệm ngay khi closure kết thúc, và trình biên dịch sẽ không ngăn bạn.