Objective-C runtime một thư viện low level hữu ích khi lập trình

Ban đầu mình dự định là sẽ viết tiếp phần 2 của loạt bài về làm việc với model và dictionary trong nhưng mình quyết định viết thêm bài này trước để mọi người không bị khó hiểu với mấy cái kiến thức của Objective-C runtime khi xây dựng model.

Objective-C runtime

Objective-C runtime, hẳn là các bạn rất lạ khi nghe tới cụm từ này. Dĩ nhiên rồi, vì nếu lập trình bình thường thì không mấy khi phải đụng tới nó. Bình thường chúng ta chỉ dừng lại ở việc đọc các thư viện có sẵn của Apple như UIKit, Foundation... rồi cách làm việc với các thư viện đó. Nhưng đôi khi chúng ta gặp các bugs, các hàm rất lạ trong open source như objc_msgSend hay objc_setAssociatedObject không biết từ đâu ra. Việc tìm hiểu Objective-C runtime sẽ giúp bạn hiểu sâu hơn về cách thức hoạt động của ngôn ngữ lập trình Objective-C, nếu như trước khi bạn chỉ hiểu tới mức biến được khai báo, khởi tạo dữ liệu, gán giá trị, gọi hàm thì nay bạn sẽ hiểu thêm về cách mà chúng hoạt động, cách chúng tiếp nhận và xử lý các lời gọi hàm cũng như alloc và release objects. Tìm hiểu những thứ sâu hơn như GDC hoặc Objective-C runtime sẽ giúp bạn nâng mức độ hiểu biết về ngôn ngữ của mình hơn từ đó nâng cao khả năng lập trình, debug, nhìn stack trace. Nói chung là muốn làm được, chúng ta chỉ cần học cách sử dụng nhưng muốn làm chủ và hiểu rõ được vấn đề chúng ta phải hiểu cặn kẽ về nó (kể cả đang học Swift thì cũng nên biết vì Swift được build trên top của những thư viện này).

Mình viết bài này để tạo cảm hứng cho các bạn muốn tìm hiểu thêm về Objective-C thôi chứ mình không thể nào ngồi viết chi tiết ra về Objective-C cũng như Objective-C runtime được, trên mạng đã có quá nhiều bài viết như vậy rồi, nên nếu đọc bài này chắc chắc các bạn sẽ không thể nào hiểu hết về nó được, phải đọc thêm từ nhiều nguồn khác nữa. Vậy sơ qua, đầu tiên Objective-C là một ngôn ngữ hướng đối tượng runtime (thực tế, các lời gọi hàm thông thường sẽ được biên dịch ra thành các lời gọi hàm runtime rồi biên dịch ra mã máy). Có nghĩa là trong lúc thực thi, các objects có quyền từ chối hoặc chuyển (forwarding) lời gọi hàm đó sang đối tượng khác. (Đó là lý do mà chúng ta làm được mấy trò method swizzling trong Objective-C). Để dễ hình dung, các bạn tưởng tượng như thế này: khi load một class (bản chất khi class được load lên cũng tạo thành một đối tượng, và nó có các class functions)/khởi tạo một đối tượng thì các định danh hàm (selector) được lưu trong một bảng, và các selector này nó sẽ ánh xạ xuống các hàm trong khối thực thi (vùng nhớ mà hàm thực được load lên). Khi gọi một phương thứ tức là ta sẽ gửi một message tới đối tượng/class đó, message đó bao gồm thông tin về con trỏ của object/class rồi parameters và selector. Từ đó, đối tượng tiếp nhận và thực thi lời gọi hàm. Với cách lập trình thông thường, các bạn sẽ không biết/không can thiệp được vào quá trình load/gửi message đó. Nhưng nếu sử dụng thư viện runtime thì chúng ta hoàn toàn có thể replace/swap các selector đó để chúng trỏ tới các hàm khác mà chúng ta mong muốn.

Điều gì xảy ra khi ta gọi một hàm

Khi gọi một hàm của một class/đối tượng thì tức là ta đang gửi một message tới class/đối tượng đó.

// Call function: talkToDude with string parameter.
[self talkToDude:@"Hey dude"];

được trình biên dịch dịch ra runtime function:

objc_msgSend(self,@selector(talkToDude:), @"Hey dude");  

Một class lúc được load lên thì bản thân nó cũng tạo thành một object gọi là meta class. Khi chúng ta gọi [Class alloc] có nghĩa là chúng ta đang gửi một message tới meta class đó và nếu hàm alloc được implement thì nó sẽ thực thi hàm này. Bản thân hàm load class này chỉ được chạy một lần (cho nên best practice khi làm function swizzling là viết trong hàm load class nếu không ta phải viết nó trong một singleton) nên dù có gọi [Class alloc] nhiều lần thì class đó cũng chỉ được load vào bộ nhớ một lần và chỉ có một object meta class mà thôi. Để hỗ trợ cho việc tìm và gọi đúng selector, meta class có một table để chứa selector cũng như cung cấp một cơ chế cached để gọi nhanh các function được gọi thường xuyên Class Cache.

Điều gì sẽ xảy ra nếu ta gửi message tới một class hoặc một object không tồn tại: Nó không làm gì cả. Vậy còn nếu gửi message tới một object có tồn tại nhưng selector không tồn tại, nó sẽ crash. Để hiểu rõ hơn về điều này ta đi sâu vào một chút:

  • Gửi một message tới nil: Đối với Objective-C nil là con trỏ NULL pointer, mọi lời gọi tới NULL pointer đều hợp lệ, nó không crash, nó chỉ đơn giản là không làm gì cả, đây là quy ước của ngôn ngữ. Nên phân biệt việc gửi message tới nil object và gửi message tới object bị dealloc (vùng nhớ đã được thu hồi).
  • Gửi một message tới một object với selector không tồn tại: Trong Objective-C runtime có cái gọi là Objective-C Message Forwarding Nó sẽ cố tìm trong tất cả bảng selector của meta class, meta của super class (hoặc object instance) nếu không tìm thấy nó sẽ tiến hành gọi hàm resolveInstanceMethod. Đây là nơi bạn có thể handle việc chuyển lời gọi selector đó sang một implement của một selector khác. Nếu không thì runtime sẽ tiếp tục gọi hàm forwardingTargetForSelector đây là nơi bạn có thể forward lời gọi hàm đó sang cho một object khác mà có thể response được lời gọi hàm này. Cuối cùng, nếu không có gì thoã mãn runtime sẽ tiếp tục gọi forwardInvocation đây có thể nói là cơ hội cuối cùng để bạn override lại hàm này nhằm forward message đó sang một object khác nếu không thì chương trình sẽ ném ra lỗi exception bởi vì mặc định của hàm forwardInvocation chỉ đơn giản gọi hàm doesNotRecognizeSelector.

Nhiều điều thú vị khác

Làm việc Objective-C runtime chính là làm việc với lập trình Objective-C low level. Ở đó ta có thể làm việc sâu hơn với classes, objects... Nó giúp hiểu sâu hơn vấn đề đồng thời làm được nhiều hacking/tricks (chủ yếu có tác dụng trong việc debug lỗi) và xây dựng các thư viện cực kì hữu ích, bên cạnh các vấn đề ở trên thư viện runtime còn cung cấp thêm nhiều phương thức khác để làm việc với đối tượng như:

  • Làm việc với class: Load class từ chuỗi string, lấy super/sub class, lấy name của class.
  • Làm việc với đối tượng: lấy tên class của đối tượng, gọi hàm, kiểm tra xem đối tượng có hồi đáp 1 lời gọi function không.
  • Tạo ra một lời gọi function bằng cách gửi message.
  • Làm việc với các thuộc tính của đối tượng(*).
  • Associated một đối tượng với một đối tượng khác (giúp cho việc thêm bớt một thuộc tính vào một class đã có sẵn mà ta không có source code của class đó).
  • Và các tính năng khác nữa...

Ý tưởng

Khi tìm hiểu qua Objective-C runtime thì mình nảy ra một ý tưởng. Nếu mình biết được các thuộc tính của đối tượng (mục *) trong thời gian thực thi thì mình hoàn toàn có thể set được giá trị cho các thuộc tính đó trong lúc khởi tạo với dữ liệu là một Dictionary. Và để sử dụng thư viện Objective-C runtime xây dựng nên một model có khả năng tự init data của mình bằng Dictionary mà không cần phải viết hàm init. Mình sẽ nói rõ trong bài sau.

Huy

I gave away my old skin to hold you, as a new me. Love you, Cà Rốt Sữa

Saigon, [email protected]