Mô hình ARC
Mô hình quản lý bộ nhớ hiện đại trên trình biên dịch di động
Mô hình quản lý bộ nhớ hiện đại trên trình biên dịch di động
Delphi đã triển khai mô hình quản lý bộ nhớ mới trên các trình biên dịch di động (iOS, Android). Trên các nền tảng này, delphi sử dụng mô hình ARC (Automatic Reference Counting - đếm tham chiếu tự động) để quản lý bộ nhớ. Mô hình này cũng được sử dụng trong ngôn ngữ Swift trên iOS.
Lưu ý: Các trình biên dịch cho máy tính để bàn cũng đã hỗ trợ ARC, nhưng chỉ áp dụng cho interface, mảng động và chuỗi, không sử dụng cho object và class được nên mình không nói đến.
So với bộ thu gom rác GC, thì ARC có hiệu suất tốt hơn nhiều, sử dụng ít bộ nhớ hơn (hầu như không có), không chiếm CPU.
Với ARC, thay vì quản lý bộ nhớ thủ công bằng try ... finally như thế này
var
Obj: TClass;
begin
Obj := TClass.Create;
try
...
finally
Obj.Free;
end;
end;
Bạn hãy để cho delphi tự động dọn dẹp các object khi không còn được sử dụng nữa (mình sẽ trình bày cơ chế hoạt động của nó kĩ hơn ở phần sau)
var
Obj: TClass;
begin
Obj := TClass.Create;
...
end;
Mặc định, khi Obj ra khỏi phạm vi (hàm) thì sẽ tự động bị hủy. Tuy nhiên, bạn cũng có thể hủy bỏ Obj thủ công bằng việc gán cho nó giá trị nil. Hoặc bạn muốn dùng Free, FreeAndNil vẫn không sao cả, khi ARC được bật thì Free hoặc FreeAndNil cũng sẽ gán nil cho object mà thôi.
var
Obj: TClass;
begin
Obj := TClass.Create;
Obj.DoSomeThing;
Obj := nil;
end;
Như cái tên của nó, Automatic Reference Counting sử dụng kĩ thuật đếm số tham chiếu của đối tượng, từ đó quyết định đối tượng đó có được hủy hay không. Số lượng tham chiếu của đối tượng được lưu trữ trong thuộc tính RefCount.
Mỗi đối tượng ban đầu có số RefCount là -1. Khi số tham chiếu tăng lên (do Create, hoặc object khác liên kết đến) thì RefCount tăng lên 1. Ngược lại, nếu số tham chiếu giảm xuống (do đặt object thành nil hoặc object khác hủy bỏ liên kết) thì RefCount giảm xuống 1. Nếu RefCount bằng 0, object sẽ được hủy, giải phóng bộ nhớ.
Một object có thể liên kết đến object khác bằng cách gán thuộc tính, trường object bằng object kia, hoặc thực hiện phương thức với tham số là object này. Hoặc có thể tham chiếu toàn bộ object như sau, cũng làm tăng RefCount
var
Obj: TClass;
DemoObj: TClass;
begin
Obj := TClass.Create;
DemoObj := Obj;
...
end;
Dòng DemoObj := Obj nghĩa là DemoObj là một tham chiếu đến Obj, dẫn đến Obj.RefCount tăng lên 1.
Lưu ý: Hiện tại delphi chỉ hỗ trợ ARC cho các trình biên dịch di động, thế nên bạn phải điều chỉnh nền tảng biên dịch thành Android hoặc iOS để có thể sử dụng.
type
TDemo = class
NewObject: TObject;
end;
var
Obj: TObject;
Demo: TDemo;
begin
// Ban đầu, Obj.RefCount = -1
Obj := TObject.Create; // Dùng Create, Obj.RefCount = 0
Demo := TDemo.Create; // Demo chưa liên kết, Obj.RefCount = 0
Demo.NewObject := Obj; // Liên kết Obj vào thuộc tính NewObject, Obj.RefCount = 1
Demo.NewObject := nil; // Hủy bỏ liên kết, Obj.RefCount = 0
// RefCount = 0 thì Obj bị hủy
end;
Khi Obj.RefCount = 0 thì Obj sẽ bị hủy (Destroy) tự động. Bạn cũng có thể làm điều này thủ công bằng cách gán Obj := nil;.
Đây là hiện tượng xảy ra khi hai đối tượng thực hiện tham chiếu lẫn nhau. Vì vậy, mặc dù hai đối tượng này không còn được sử dụng, nhưng ARC không thể loại bỏ nó do vẫn còn liên kết với nhau. Điều này sẽ dẫn đến lỗi memory leak.
Hãy cũng xem qua ví dụ sau
type
TParent = class
Child: TChild;
end;
TChild = class
Parent: TParent;
end;
var
Par: TParent;
Chi: TChild;
begin
Par := TParent.Create;
Chi := TChild.Create;
Par.Child := Chi;
Chi.Parent := Par; // Tham chiếu lẫn nhau
// Đến đây ra ngoài phạm vi thì Par và Chi phải bị hủy
// Nhưng Par và Chi vẫn còn duy trì liên kết với nhau
// Nên kết quả là vẫn không bị ARC hủy bỏ, dẫn đến memory leak
end;
Dephi và hầu hết các ngôn ngữ khác hỗ trợ ARC đều cung cấp một khái niệm tham chiếu yếu (weak). Đi kèm với tham chiếu yếu là tham chiếu mạnh (strong). Mặc định, tất cả các tham chiếu đều là tham chiếu mạnh. Tuy nhiên, bạn có thể chỉ định tham chiếu yếu bằng cụm [Weak].
Tham chiếu mạnh khi thực hiện sẽ làm tăng RefCount lên 1, trong khi tham chiếu yếu không làm tăng RefCount.
Hiện tượng tham chiếu vòng như trên có thể giải quyết như sau (mình sẽ ghi lại code ở trên)
type
TParent = class
Child: TChild;
end;
TChild = class
[Weak]
Parent: TParent;
end;
var
Par: TParent;
Chi: TChild;
begin
Par := TParent.Create; // Par.RefCount = 0
Chi := TChild.Create; // Chi.RefCount = 0
Par.Child := Chi; // Par.RefCount = 1 do có tham chiếu mạnh
Chi.Parent := Par; // Chi.RefCount = 0 do là tham chiếu yếu
// Vì Chi.RefCount = 0, Chi bị hủy bỏ trước
// Vì Chi đã bị hủy bỏ, nên liên kết Par đến Chi cũng bị hủy bỏ theo
// Nên Par.RefCount = 0, Par bị hủy bỏ sau
end;
Như thế, đến cuối cùng, thì cả hai đều bị ARC hủy bỏ, nên không làm memory leak.
Đôi khi, bạn không muốn đối tượng chịu sự quản lý của ARC, bạn muốn quản lý bộ nhớ thủ công cho nó, delphi cung cấp chỉ thị [Unsafe] để vô hiệu hóa ARC cho đối tượng này.
var
[Unsafe]
Obj1: TObject;
Obj2: TObject;
Chỉ thị [Unsafe] chỉ có tác dụng đến một đối tượng phía sau nó, nên Obj1 là unsafe, Obj2 thì không.
Khi ấy, Obj1 sẽ không bị ARC quản lý, bạn phải tự động quản lý bộ nhớ cho nó. Obj1.RefCount luôn trả về 0.
ARC với object chỉ sử dụng được trên các compiler di động, nhưng ARC với các đối tượng khác như interface, mảng, chuỗi có thể sử dụng với bất kì trình biên dịch nào. Do đó, bạn thấy khi sử dụng interface, mảng hay chuỗi thì đều không phải giải phóng nó bằng tay. ARC đã làm điều đó cho bạn.