Đôi khi phát triển ứng dụng chúng ta sẽ phải lưu lại những dữ liệu nhạy cảm như thông tin người dùng, thẻ ngân hàng … Vậy lúc đó chúng ta sẽ phải cần đến Keychain để lưu các dữ liệu đó. Vậy hôm nay mình sẽ giới thiệu với các bạn phương pháp lưu dữ liệu sử dụng Keychain.
Tại sao không dùng các phương pháp lưu dữ liệu khác?
Có thể các bạn đã biết, những thông tin nhạy cảm của người dùng như: thông tin tài khoản(username, pass, token …), thông tin thẻ ngân hàng … nếu để lộ ra ngoài sẽ làm ảnh hưởng rất lớn tời người dùng. Vì vậy nếu chúng ta sử dụng các cách lưu dữ liệu thông thường không có tính bảo mật như: File, Property List(UserDefaults), CoreData, Realm … thì người ta hoàn toàn có thể lấy được nội dung của ứng dụng một cách đơn giản. Về cơ bản các kiểu lưu dữ liệu kia đều sử dụng 1 file để lưu trữ data ở đó mà không sử dụng bất kỳ phương pháp mã hóa nào, vì vậy nó quá dễ dàng để các tin tặc có thể lấy được những dữ liệu bạn lưu trong các file đó.
Sử dụng Keychain Services API
Để lưu trữ những dữ liệu nhạy cảm cần tính bảo mật Apple đã cung cấp cho chúng ta Security Services. Keychain services API sẽ giúp chúng ta lưu trữ các dữ liệu này ở trong một encrypted database được gọi là Keychain.
Để lưu Password vào keychain mình sẽ làm như sau:
func save(_ password: String, for account: String) {
let password = password.data(using: String.Encoding.utf8)!
let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: account,
kSecValueData as String: password]
let status = SecItemAdd(query as CFDictionary, nil)
guard status == errSecSuccess else { return print("save error")}
}
Keychain service cung cấp một số kiểu để xác định nơi lưu trữ phù hợp. kSecClassGenericPassword là một trong số đó, keychain services sẽ hiểu items này là password và cần được mã hóa. Ngoài ra còn có các kiểu khác các bạn tìm hiểu ở ĐÂY.
SecItemAdd(_:_:) để lưu dữ liệu vào keychain
Để lấy lại dữ liệu chúng ta đã lưu chúng ta làm như sau:
func getPassword(for account: String) -> String? {
let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: account,
kSecMatchLimit as String: kSecMatchLimitOne,
kSecReturnData as String: kCFBooleanTrue]
var retrivedData: AnyObject? = nil
let _ = SecItemCopyMatching(query as CFDictionary, &retrivedData)
guard let data = retrivedData as? Data else {return nil}
return String(data: data, encoding: String.Encoding.utf8)
}
Để lấy dữ liệu ra chúng ta sử dụng SecItemCopyMatching() truyền vào đó query mà mình muốn.
Đối với các bạn lần đầu làm việc với Keychain API thì nó cũng khá lằng nhằng. Nhưng hiện nay đã có khá nhiều thư viện ngon hỗ trợ việc code của các bạn trở nên dễ dàng hơn như: Keychain-swift, Keychain Access
Mã hóa thông tin
Ngoài Password ra đôi khi chúng ta cũng cần phải mã hóa một số loại thông tin nhạy cảm khác. Việc mã hóa bằng tay khá phức tạp, vì vậy để hỗ trợ cho việc mã hóa mình thường dùng CryptoSwift.
Tổng kết
Mình hi vọng bài viết có thể giúp các bạn khi các bạn gặp phải các bài toán cần phải lưu những dữ liệu nhạy cảm. Khi đó nhớ chọn Keychain nhé. Chúc các bạn thành công.
Bài viết tiếp theo mình sẽ chia sẻ thêm một tính năng rất hay của keychain đó là Keychain Sharing
Cách lưu dữ liệu sử dụng file Property List khá phổ biến trong lập trình ứng dụng iOS, nó thường được dùng để lưu các dữ liệu nhỏ và không cần tính bảo mật cao. Bài viết này mình sẽ chia sẻ với các bạn một số cách để thao tác với Property List.
Info.plist
Tất cả các project khi bạn mới tạo XCode sẽ tự tạo ra một file Info.plist, nó làm nhiệm vụ lưu lại các thông tin dự án của bạn. Từ project name, version cho đến các setting cũng như mô tả API của Apple.
Các thông tin trong file Info.plist sẽ được các API của Apple truy cập vào để lấy thông tin hiển thị lên ứng dụng. (VD: Hiển thị message cho pop up xin quyền truy cập Camera …) cũng như việc xác nhận thông tin của Apple khi bạn Submit ứng dụng lên App Store Connect. Vì vậy file này chỉ nên lưu những thông tin cài đặt của dự án. Nếu bạn muốn lưu dữ liệu dạng này ta có thể tạo một file mới và lưu vào đó.
Tạo file Property List mới
Bước 1: Chuột phải vào thư mục bạn muốn lưu file -> New File…
Bước 2: Đánh ô tìm kiếm phía trên với keyword “Property List” -> Next -> Đặt tên file
Vậy là bạn đã tạo thành công file Property List. Giờ bạn có thể mở file và điền thông tin mình muốn lưu vào file đó.
Tuy nhiên nếu sử dụng XCode thì chúng ta chỉ có thể lưu được các dữ liệu như:
String
Number
Bool
Data
Date
Array
Dictionary
Chúng ta cũng có thể lưu kiểu custom object vào file Property List nhưng phải sử dụng code để encode dữ liệu cần lưu sang Data rồi khi sử dụng thì chúng ta decode nó về dữ liệu ban đầu.
Tạo file Property List bằng code
Chúng ta có thể tạo file Property List một cách đơn giản hơn bằng cách như sau:
func save(key: String, value: Any) {
let myData: NSDictionary = [key: value]
let fileManager = FileManager.default
// Đường dẫn lưu file và tên file của bạn
let path = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("MyInfo3.plist")
// Viết vào file nếu file không tồn tại nó sẽ tự tạo file mới.
myData.write(to: path, atomically: true)
print(path)// in ra console để thấy đường dẫn lưu file
}
Truy cập vào file Property List bằng code
Lấy data từ file Property list
func getMyPlist(key: String) -> Any? {
// Lấy ra đường dẫn vào file Property List của bạn
let path = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("MyInfo.plist")
// Sử dụng NSDIctionary để lấy nội dung từ URL
guard let myDict = NSDictionary(contentsOf: path) else { return nil }
print(path)
// Trả về giá trị theo key trong Dictionary
return myDict[key]
}
Lưu dữ liệu vào file Property List
func saveMyPlist(key: String, value: Any) {
let fileManager = FileManager.default
// Tạo đường dẫn file
let path = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("MyInfo.plist")
guard let myDict = NSMutableDictionary(contentsOf: path) else {
// nếu file chưa tồn tại chúng ta sẽ ghi vào file với bộ key/value đầu tiên.
let myData: NSDictionary = [key: value]
myData.write(to: path, atomically: true)
return
}
// nếu file đã tồn tại chúng ta sẽ update dữ liệu
myDict[key] = value
myDict.write(to: path, atomically: true)
print(path)
}
Xóa một key trong Property List
func removeMyPlist(key: String) {
let fileManager = FileManager.default
// Tạo đường dẫn file
let path = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("MyInfo.plist")
guard let myDict = NSMutableDictionary(contentsOf: path) else { return }
// nếu file đã tồn tại chúng ta sẽ update dữ liệu
myDict.removeObject(forKey: key)
myDict.write(to: path, atomically: true)
}
Để tương tác dễ hơn với plist bắt buộc chúng ta phải tạo ra các func để quản lý các file đó. Nhưng nếu các bạn không muốn làm những thứ lằng nhằng đó thì chúng ta có thể chuyển qua dùng một file Property List có sẵn mà Apple đã cung cấp đó là sử dụng UserDefault
UserDefault
UserDefault là một singleton class nhằm mục đích giúp các nhà phát triển có thể dễ dàng lưu dữ liệu vào file Plist hơn. Mặc định khi bạn sử dụng UserDefault nó sẽ tạo ra một file Plist trong dự án. Từ đó ta có thể thêm, sửa, xóa các key nằm trong file Plist đó dựa trên các hàm mà UserDefault cung cấp.
UserDefault tương tác trên Property List nên nó cũng phải tuân thủ theo quy tắc của file đó. Vì vậy thông thường chung ta chỉ có thể gán được các giá trị mà file Plist cho phép.
Để lưu/lấy giá trị bằng UserDefault ta làm như sau:
let udf = UserDefaults.standard//Tạo instance
udf.set("11", forKey: "1")// gán giá trị 11 cho key "1"
udf.synchronize()//nó block thread đang gọi cho đến khi giá trị được gán hoàn tất
print(udf.value(forKey: "1"))// lấy dữ liệu của key 1 và in ra console
Như vậy với UserDefault chúng ta có thể làm việc với file Plist một cách đơn giản. Tuy nhiên, đôi khi chúng ta cũng phải lưu lại kiểu dữ liệu mà Plist không hỗ trợ. Lúc này chúng ta cần phải encode nó về Data để có thể lưu được dạng này.
Lưu ý
Chúng ta không nên lưu những dữ liệu quá nặng trong Plist, vì file này sẽ tiêu tốn bộ nhớ RAM khi bạn chạy ứng dụng.
Lưu custom object
Tạo mới class
class Employee: NSObject, NSCoding {
let name: String
let dob: String
// hàm khởi tạo
init(name: String, dob: String) {
self.name = name
self.dob = dob
}
// init với NSCoder bước này để decode
required convenience init(coder aDecoder: NSCoder) {
let name = aDecoder.decodeObject(forKey: "name") as! String
let dob = aDecoder.decodeObject(forKey: "dob") as! String
self.init(name: name, dob: dob)
}
// hàm này để encode
func encode(with aCoder: NSCoder) {
aCoder.encode(name, forKey: "name")
aCoder.encode(dob, forKey: "dob")
}
}
Lưu Employee vào UserDefault sử dụng NSKeyedArchiver
let employee = Employee(name: "Dino", dob: "19/09/1999")
let udf = UserDefaults.standard
do {
let encodeData = try NSKeyedArchiver.archivedData(withRootObject: employee, requiringSecureCoding: false)
udf.set(encodeData, forKey: "emp")
udf.synchronize()
} catch {
print(error)
}
Lấy ra Employee từ UserDefault
let decode = udf.data(forKey: "emp")
let decodeEmp = NSKeyedUnarchiver.unarchiveObject(with: decode!) as? Employee
Tổng kêt
Mình hi vọng bài viết này có thể giúp các bạn sử dụng Property List tốt hơn. Chúc các bạn thành công.
Đối với các ứng dụng iOS, việc lưu dữ liệu là việc bắt buộc. Việc lưu dữ liệu rất cần thiết đối với các ứng dụng từ lớn cho tới nhỏ. Bạn có thể lưu dữ liệu lớn như các dữ liệu lấy được từ các API, cho đến các trạng thái hoặc các cài đặt của ứng dụng. Bài viết này mình sẽ giới thiệu tổng quát về các cách chúng ta có thể lưu dữ liệu trong iOS. Bài viết này chỉ mang tính giới thiệu cho từng loại thường dùng cho việc gì, các cách thao tác trên từng loại mình sẽ tách ra làm các bài riêng cho các bạn dễ hiểu và đỡ bị ngợp.
1. Lưu dữ liệu bằng file
Các dữ liệu cơ bản thường gặp khi sử dụng ứng dụng như String(VD: json, xml ..) và Data(VD: image)
Đối với những kiểu dữ liệu cơ bản này chúng ta có thể thực hiện thao tác lưu(save), lấy(get) dữ liệu thông qua các hàm đã được cung cấp sẵn trong class đó.
Chúng ta thường dùng cách này để lưu những dữ liệu như:
Lưu lại log của ứng dụng
Lưu lại hình ảnh, cache data
Lưu lại nội dung có dữ liệu lớn
Lưu lại nội dung dạng json hoặc xml
Về cơ bản thì với cách này các bạn có thể lưu được rất nhiều thứ và nó có thể được quản lý, sắp xếp tùy ý. Do đuôi file không bị cố định nên bạn có thể lưu nội dung lên file có đuôi mình muốn và mở bằng các ứng dụng hỗ trợ.
Để quản lý thư mục lưu file, chúng ta nên sử dụng NSFileManager. Ngoài ra NSFileManager cũng cung cấp cho chúng ta các hàm để save(lưu), get(lấy) dữ liệu. Nhưng các hàm này bản thân các class như String và Data cũng có rồi nên NSFileManager thường được dùng để quán lý thư mục.
Hướng dẫn lưu dữ liệu ra file
2. Lưu dữ liệu phân cấp
Property Lists
Là các file có đuôi .plist, dùng để lưu dữ liệu có dạng như một hệ thông phân cấp kiểu Array hoặc Dictionary với dung lượng không quá lớn.
Trong iOS, Property List chỉ lưu được một số kiểu dữ liệu cơ bản như:
String
Data
Date
Number
Bool
Array
Dictionary
Ngoài ra chúng ta cũng có thể lưu được các object. Để làm điều đó chúng ta cần mã hóa(encode) nó về dạng Data rồi khi cần dùng thì giải mã(decode) dữ liệu trở lại.
NSUserDefault
NSUserdefault là một file Property List, đã được Apple viết sẵn 1 class dạng singleton. Nó giúp các iOS developer thuận tiện và tiết kiệm thời gian hơn trọng việc lưu/lấy dữ liệu cần thiết ở file mà NSUserdefault đã tạo sẵn cho mỗi project của bạn.
Property List thường được dùng để:
Lưu dữ liệu đơn giản có dung lượng thấp
Lưu lại các setting của ứng dụng
Lưu lại các trạng thái của ứng dụng
3. Lưu dữ liệu sử dụng SQLite
Đối với các ứng dụng có nhiều dữ liệu phức tạp, với các mối quan hệ chồng chéo giữa các dữ liệu thì việc sử dụng cách 1 với 2 không thể đáp ứng được các yêu cầu về tốc độ tìm kiếm, sắp xếp và thậm chí là hiệu năng về việc dọc và lưu dữ liệu mà ứng dụng yêu cầu. Với những dạng dữ liệu như này chúng ta nên trang bị cho ứng dụng một phương thức tốt hơn đó chính là SQLite.
SQLite
SQLite là một hệ thống cơ sở dữ liệu quan hệ nhỏ gọn,hoàn chỉnh có thể cài đặt bên trong các ứng dụng khác.
Đặc điểm của SQLite là gọn, nhẹ và đơn giản. Không cần cài đặt, không cần cấu hình hay khởi động mà có thể sử dụng ngay. Dữ liệu database cũng được lưu ở một file duy nhất. Đối với các ứng dụng mobile SQLite rất thích hợp để sử dụng.
SQLite thường được sử dụng khi nào:
Khi bạn có một khối dữ liệu lớn cần phải lưu lại
Khi bạn có một tập các dữ liệu liên quan móc nối với nhau
Khi bạn muốn lưu lại các dữ liệu mà API trả về
Một số thư viện hay sử dụng
FMBD(Flying Meat Database): Là một thư viện hỗ trợ bạn trong việc thao tác với SQLite . Nói cách khác FMDB là một Objecttive-C wrapper của SQLite. Công việc của FMDB là giúp bạn thoải mái hơn trọng việc thực hiện các câu lệnh truy vấn trong SQLite.
CoreData: Là một framework được Apple xây dụng để hỗ trợ chúng ta thao tác với SQLite Database theo hướng đối tượng mà không phải quan tâm tới các câu lệnh truy vấn của SQLite. Nó sẽ coi các bản ghi trong SQLite Database như một object, table như class.
Nên sử dụng FMBD(Flying Meat Database) hay CoreData
CoreData là một Framework được Apple phát triển để làm việc với SQLite Database vì vậy việc sử dụng CoreData sẽ được khuyến khích hơn cả. CoreData giúp việc viết và đọc code rõ ràng hơn việc sử dụng FMDB để làm việc trực tiếp với SQLite. Nó tốt cho việc bảo trì cũng như phát triển code sau này. Ngoài ra Apple còn hỗ trợ cả việc cache dữ liệu, tích hợp CoreData với UITableView, một class mà hầu hết các ứng dụng đều sử dụng.
Bạn đọc dữ liệu từ database lấy ra một list các object và hiển thị nó bằng UITableView nó thực sự rất tiện lợi đối với các iOS Dev.
4. Keychain
Keychain là một lưu trữ bảo mật với những dữ liệu nhỏ gọn, nhạy cảm như mật khẩu, số tài khoản ngân hàng, mã bảo mật hay một số thông tin mà ta muốn bảo mật không muốn cho người khác biết được.
Ngoài ra Apple cũng cung cấp một dịch vụ Keychain Sharing để chia sẻ keychain giữa các ứng dụng của cùng một nhà phát triển. Ví dụ như Facebook và Messenger, nếu ta đăng nhập ở Facebook và lưu password vào Keychain thì sang Messenger chúng ta có thể sử dụng password để đăng nhập nhanh ở ứng dụng này.
5. Realm
Realm là một database sử dụng core C++ với tham vọng thay thế SQLite với các ưu điểm về tốc độ và dễ sử dụng. Hiện nay Realm rất được ưa chuộng trên thế giới vì nó có rất nhiều ưu điểm vuợt trội so với những cách còn lại.
Ưu điểm của Realm so với CoreData:
Dễ sử dụng hơn
Tốc độ query nhanh hơn
Quản lý dữ liệu dễ dàng trực quan hơn
Tài liệu tham khảo tốt
Open source
Khi nào nên sử dụng Realm cho dự án của bạn:
Khi project của bạn cần lưu lại số lượng lớn các bản ghi
Khi project của bạn có nhiều dữ liệu móc nối với nhau, có mối quan hệ phức tạp
Khi dự án của bạn cần lưu lượng lớn các thông tin từ API
Tổng kết
Bài viết này mình đã giới thiệu với các bạn về một số cách lưu dữ liệu phổ biến trong swift. Mỗi khi các bạn muốn lưu một loại dữ liệu nào hãy cân nhắc kiểu dữ liệu nó là gì, mức độ quan trọng của nó như nào. Từ đó bạn có thể chọn được cách lưu dữ liệu tối ưu nhất với loại dữ liệu đó. Các bài viết tiếp theo mình sẽ giới thiệu với các bạn cách triển khai lưu dữ liệu cho từng cách.
Cảm ơn các bạn đã theo dõi bài viết! Chúc các bạn thành công.
Việc quản lí bộ nhớ trong mỗi ứng dụng là rất quan trọng. Nếu ứng dụng của bạn sử dụng dữ liệu bừa bãi 1 cách không cần thiết thì sẽ làm memory của ứng dụng tăng cao, và trong trường hợp xấu có thể dẫn đến crash.
Swift sử dụng ARC để hỗ trợ developer quản lí ứng dụng của app. Ở bài viết này mình sẽ nói về ARC trong swift.
Contents:
ARC là gì
Cách ARC hoạt động
Strong reference
Xử lí strong reference
weak reference
ARC là gì
ARC là viết tắt của Automatic Reference Counting. Swift sử dụng ARC để giúp bạn việc quản lý bộ nhớ của app.
ARC sẽ tự động giải phóng bộ nhớ được sử dụng bởi 1 biến khi biến đó không còn được sử dung nữa.
Tuy nhiên, để ARC có thể biết được khi nào biến đó không còn dc sử dụng, thì ARC phải biết về các relations giữa biến đó vs các biến khác trong code của bạn.
Note: ARC chỉ áp dụng cho các biến kiểu class(reference type).
Cách ARC hoạt động
Mỗi khi bạn tạo 1 biến, ARC sẽ tự chia sẻ 1 phần bộ nhớ để lưu trữ biến đó. Khi biến đó không còn được sử dụng, ARC giải phóng biến, lấy lại bộ nhớ để dùng cho những mục đích khác
Để xác định 1 biến không còn được sử dụng, ARC sẽ đếm số lượng strong reference hiện tại của biến đó đối với các biến khác. Chỉ khi số lượng strong reference = 0 thì biến đó mới được giải phóng bộ nhớ.
Vậy strong reference là gì?
Strong reference
Mỗi khi bạn gán 1 biến kiểu class cho 1 property, constant hoặc variable, thì những thứ đó tạo 1 strong reference đến biến đó.
class Car {
var price: Double
init(price: Double) {
self.price = price
}
deinit {
print("Car is being deinitialized")
}
}
var hondaCar: Car? = Car(price: 50000.0)
Ở đây, bạn gán biến Car(price: 50000.0) cho varialbe hondaCar
-> hondaCar đã tạo 1 strong reference đến Car(price: 50000.0)
Thêm dòng code sau và run thử trên playground:
hondaCar = nil
Kết quả thu được ở màn hình console như sau:
Nếu bạn set honedaCar = nil -> Khi đó Car(price:50000) sẽ không còn strong reference nào trỏ đến -> ARC sẽ giải phóng bộ nhớ cho Car.
Tiếp tục thử chạy đoạn code sau và quan sát màn hình console:
Chạy đoạn code trên, bạn sẽ không thấy được print ra "Car is being deinitialized"…
Vì các biến car1, car2, car3 là reference type, nên ta có sơ đồ relation ship như sau:
Khi đó nếu chỉ set car1, car2 = nil, thì Car vẫn còn 1 strong reference từ car3 trỏ đến -> ARC sẽ không giải phóng bộ nhớ cho Car trong trường hợp này.
Strong reference giữa các class
Trong ví dụ trên, sẽ tạo ra 1 sơ đồ relation giữa các instance như sau:
Việc set property department và owner của 2 instance tuan và gotham đã tạo ra 2 strong reference trỏ đến nhau.
Khi đó Person và Departmen, mỗi thứ sẽ có 2 strong refernce trỏ đến. Vì vậy, khi bạn chỉ set tuan = nil và gotham = nil thì Person và Departmen vẫn sẽ không được xóa khỏi bộ nhớ dù không cần dùng đến nữa.
-> Trường hợp này gọi là retain cycle -> Tạo ra rò rỉ bộ nhớ (memory leaks).
Xử lí strong reference
Swift cung cấp 2 cách để xử lí strong reference cycle khi bạn làm việc với các biến kiểu class:
weak reference
unowned reference
Weak reference
weak refernce: thay vì tạo 1 strong reference, nó sẽ chỉ tạo 1 weak -> ARC sẽ không làm tăng số lượng strong reference trỏ tới biến đó.
Sử dụng weak reference khi biến còn lại có thời gian tồn tại ngắn hơn(thường bị giải phóng bộ nhớ trước)
class Person {
weak var department: Department?
deinit {
print("Person is being deinitialized")
}
}
Sửa lại ví dụ trên bằng cách sửa department thành kiểu weak( vì department có thời gian tồn tại ngắn hơn người).
Khi đó, sơ đồ relation ship sẽ thành:
set gotham = nil -> Departmen k còn strong reference nào trò đến -> được giải phóng khỏi bộ nhớ, khi đó strong reference của Departmen trò đến Person cũng sẽ bị mất.
set tuan = nil -> Person không còn strong refernce nào trỏ đến -> ARC giải phóng khỏi bộ nhớ.
Kết quả thu được:
Unowned reference:
Tương tự như weak refernce, nó sẽ không tạo ra 1 strong reference.
Khác weak reference ở chỗ, unowned dùng cho những biến có thời gian tồn tại lâu hơn.
Note: nếu bạn truy cập đến 1 biến unowned sau khi nó đã được giải phóng khỏi bộ nhớ -> app sẽ crash. Vì vậy hãy cẩn thận khi dùng unowned.
Sửa ví dụ trên thành như sau và sẽ vẫn thu được kết quả tương tự so với khi dùng weak:
class Department {
unowned var owner: Person?
deinit {
print("Department is being deinitialized")
}
}
Hi vọng bài viết trên đã đem lại cho bạn cái nhìn tổng quan về ARC và cách sử dụng weak/unowned để tránh retain cycle giữa các class.
Singleton là một design pattern rất phổ biến trong phát triển phần mềm. Singleton rất đơn giản, phổ biến và dễ sử dụng. Chính vì những ưu điểm đó mà nó được rất nhiều người sử dụng.
Singleton pattern là gì?
Singleton pattern là một pattern đảm bảo rằng một (class) chỉ có một thể hiện(instance) duy nhất.
Trong Swift các bạn có thể đã được sử dụng khá nhiều các singleton khi làm việc với các framework của Apple. Dưới đây là một số ví dụ cho singleton:
// Shared URL Session
let sharedURLSession = URLSession.shared
// Default File Manager
let defaultFileManager = FileManager.default
// Standard User Defaults
let standardUserDefaults = UserDefaults.standard
Ưu điểm và nhược điểm của Singleton pattern
Ưu điểm:
Đảm bảo một class chỉ có 1 instance duy nhất được khởi tạo.
Ẩn các các hàm khởi tạo của class. (Đảm bảo không thể khởi tạo từ bên ngoài)
Có thể truy cập ở bất cứ nơi nào trên project
Nhược điểm
Khó kiểm soát và quản lý
Hạn chế số lượng instance của class
Khi chỉ cho phép một trường hợp cụ thể của một class
Nên sử dụng Singleton pattern khi nào?
Singleton pattern là một pattern rất hữu ích. Đôi khi bạn muốn chắc chắn rằng chỉ có một instance của một class được khởi tạo và ứng dụng của bạn chỉ sử dụng instance đó.
Ví dụ: Ứng dụng của bạn có tính năng bật tắt nhạc nền khi người dùng mở ứng dụng thì nhạc nền sẽ tự động chạy và nếu người dùng muốn tắt thì phải vào setting để tắt nó. Bạn chắc chắn phải sử dụng duy nhất một instance trong trường hợp này, bởi vì bạn không thể tạo một instance để mở và một instance để tắt vì nó là 2 instance độc lập nên không thực hiện việc cho nhau được. Vì vậy chúng ta sẽ cần đến Singleton pattern trong trường hợp này để giải quyết bài toán gọn gàng và nhẹ nhàng.
Tuy rằng Singleton pattern khá dễ sử dụng và hữu dụng nhưng bạn cần cân nhắc khi sử dụng nó tránh lạm dụng dẫn đến việc khó kiểm soát code cũng như debug.
Các bạn nên sử dụng Singleton khi mà bài toán của các bạn yêu cầu phải có một đối tượng để quản lí các nguồn tài nguyên hoặc theo dõi trạng thái của hệ thống mà nó ảnh hưởng đến nhiều nơi trong dự án.
Sử dụng class thông thường
class LocationManager{
//MARK: - Location Permission
func requestForLocation(){
//Code Process
print("Location granted")
}
}
// Truy cập vào class
let location = LocationManager() // khởi tạo class
location.requestForLocation() // gọi func
Đây là trường hợp thông thường của 1 class không sử dụng Singleton pattern để truy cập vào các func trong class. Mỗi lần muốn sử dụng một func trong class chúng ta phải khởi tạo một instance cho nó sau đó mới có thể gọi func đó được. Để giải quyết vấn đề này chúng ta tạo ra một Singleton class với static instance.
Tạo Singleton class
class LocationManager {
static let shared = LocationManager()
init(){}
func requestForLocation() {
//Code Process
print("Location granted")
}
}
// Truy cập func trong class sử dụng Singleton Pattern
LocationManager.shared.requestForLocation()
// Bạn vẫn có thể truy cập vào class một cách thông thường
let location = LocationManager()
location.requestForLocation()
Cách tốt hơn để tạo Singleton class
class LocationManager {
static let shared = LocationManager()
var locationGranted: Bool?
// thay đổi hàm init thành private để chỉ có thể khởi tạo trong class này.
private init(){}
func requestForLocation() {
//Code Process
locationGranted = true
print("Location granted")
}
}
// Gọi hàm trong class bằng việc sử dụng đoạn code dưới
LocationManager.shared.requestForLocation()
Lúc này chúng ta đã thay đổi access level của hàm khởi tạo từ internal sang private. Vì vậy nếu chúng ta cố tính sử dụng LocationManager() XCode sẽ báo lỗi: ‘LocationManager’ initializer is inaccessible due to ‘private’ protection level
Sử dụng Singleton class
Bây giờ, bất kể chỗ nào trong project bạn muốn sử dụng hàm requestForLocation của LocationManager bạn cũng chì cần sử dụng như sau:
LocationManager.shared.requestForLocation()
Sử dụng biến toàn cục (Global variables)
Đây là cách đơn giản nhất để ứng dụng Singleton pattern:
let sharedNetworkManager = NetworkManager(baseURL: API.baseURL)
class NetworkManager {
// MARK: - Properties
let baseURL: URL
// Initialization
init(baseURL: URL) {
self.baseURL = baseURL
}
}
Bằng cách khai báo một biến toàn cục, bấn kì object nào cũng có thể truy cập dến biến toàn cục đó. Trong Swift, biên toàn cục được khai báo một cách trì hoãn cho đến khi nó được gọi ra để sử dụng lần đầu tiên. Việc khởi tạo trong swift cũng dựa trên hàm dispatch_one, điều này sẽ có ích khi bạn chỉ muốn khởi tạo object của mình một lần duy nhất.
Tuy nhiên khi sử dụng cách này bạn sẽ gặp vấn đề là code của bạn sẽ khó hiểu và khó để debug hơn. Và hàm khởi tạo của class NetworkManager thì không thể khai báo là private.
Lưu ý
Mình không khuyến khích các bạn sử dụng cách tạo global variable vì các nhược điểm của nó.
Tổng kết
Vậy là giờ các bạn đã hiểu cách tạo Singleton class trong dự án của bạn. Nó khá đơn giản, dễ sử dụng và tốn ít thời gian để tạo. Nó có một số ưu điểm và nhược điểm. Nếu bạn sử dụng nhiều Singleton pattern trong dự án của mình thì sẽ khó có thể quản lý vòng đời của Singleton class của bạn. Ngoài ra, nó duy trì một trạng thái chia sẻ toàn cầu có thể thay đổi. Tuy rằng nó có các ưu điểm rõ rệt, nhưng hãy tránh lạm dụng Singleton pattern thay vào đó tốt hơn chúng ta nên sử dụng Dependency injection.
Ngày xưa khi sử dụng đồng hồ đeo tay kết nối với điện thoại di động, mỗi khi có tin nhắn hay cuộc gọi đến thì các thiết bị này đều hiển thị được thông tin đó lên màn hình. Lúc đó mình cũng khá băn khoăn tại sao đồng hồ không có sim mà sao nó lại nhận được tin nhắn, cuộc gọi đến. Cho đến gần đây mình có làm một dự án liên quan đến tính năng này mình mới hiểu được tại sao nó lại làm được điều đó. Bài viết này mình sẽ giới thiệu với các bạn về một dịch vụ của Apple có thể làm được điều đó Apple Notification Center Service.
Giới thiệu
Mục đích của Apple Notification Center Service (ANCS) là cung cấp cho các phụ kiên Bluetooth (kết nối với thiết bị iOS thông qua Bluetooth low-energy) một cách đơn giản và thuận tiện để truy cập nhiều loại thông báo được tạo trên thiết bị iOS.
ANCS được thiết kế theo ba nguyên tắc: đơn giản, hiệu quả và khả năng mở rộng. Do dó các phụ kiện khác nhau từ đền LED đơn giản đến phức tạp.
Sự phụ thuộc(Dependencies)
ANCS không có phụ thuộc, ngoài bộ tiêu chuẩn phụ của Generic Attribute Profile(GATT). Một phụ kiện sẽ đóng vai trò là GATT Client có thể truy cập và sử dụng các dịch vụ khác do thiết bị iOS cung cấp trong khi sử dụng ANCS.
Thuật ngữ
Apple Notification Center Service được viết tắt là ANCS
Notification Provider: nhà xuất bản dịch vụ ANCS ở đây là các thiết bị iOS được viết tắt là NP
Notification Consumer: Là những thiết bị ngoại vi được viết tắt là NC
Một thông báo hiển thị trên thiết bị iOS trong Notification center se được gọi là iOS Notification
Một thông báo được gửi bởi GATT characteristic đưới dạng tin nhắn không đồng bộ sẽ được gọi là GATT Notification.
Apple Notification Center Service
APNS là dịch vụ chính có UUID là 7905F431-B5CE-4E99-A40F-4B1E122D00D0. Chỉ có duy nhất một instance của ANCS có thể đại diện cho NP. Do tính chất của iOS, ANCS không được đảm bảo luôn hiển thị. Do đó, NC nên tìm kiếm và đăng ký(subcribe) Service Changed characteristic của GATT để theo dõi việc publishing và unpublishing của ANCS bất cứ lúc nào.
Control Point: UUID 69D1D8F3-45E1-49A8-9821-9BBDFDAAD9D9 (writeable with response)
Data Source: UUID 22EAC6E9-24D6-4BB5-BE44-B36ACE7C7BFB (notifiable)
Tất cả các characteristic này yêu cầu phải có authorization để truy cập. Hỗ trợ cho Notification Source characteristic là bắt buộc, trong khi Control Point characteristic làm nhiệm vụ hỗ trợ, còn Data Source characteristic thì là tùy chon (optional).
Lưu ý:
Có thể có nhiều hơn 3 characteristics trong ANCS đã kể ở trên. Nó có nghĩa là NC có thể bỏ qua bất kể characteristic nào mà nó không nhận ra.
Notification Source
Notification Source characteristic là characteristic mà NC được thông báo:
Sự xuất hiện iOS Notification mới trên NP
Sự sửa đổi một iOS Notification trên NP
Sự xóa bỏ một iOS Notification trên NP
GATT Notification có thể được gửi ngay khi NC đăng ký Notification Source characteristic. Do đó, NC phải ở trong trạng thái có thể châpps nhận và xử lý đúng các message này trước khi đăng kí vào characteristic này.
Định dạng của GATT Notification được thể hiện theo hình dưới đây:
GATT Notification được gửi thông qua Notification Source characteristic nó chứa các thông tin như sau:
EventID: Trường này thông báo cho phụ kiện xem iOS Notification đã được thêm, chỉnh sửa hoặc được xóa. Các giá trị được định nghĩa trong EventID Values.
EventFlags: Một bitmask có bit được đặt thông báo cho một đặc thù của NC với iOS Notification. Ví dụ: Nếu một iOS Notification được coi là quan trọng thì NC có thể muốn hiển thị giao diện người dùng tích cực hơn để đảm bảo người dùng được cảnh báo chính xác. Các bit được liệt kê cho trường này được định nghĩa trong Event Flags.
CategoryID: Một giá trị số cung cấp một danh mục trong đó iOS Notification có thể được phân loại. NP sẽ nỗ lực hết sức để cung cấp danh mục chính xác cho mỗi iOS Notification. Nó được định nghĩa trong CategoryID Values.
CategoryCount: Số lượng iOS notification đang hoạt động trong category đã cho. Ví dụ: Nếu hai email chưa đọc đang nằm trong hòm thư đến email của người dùng và một email mới được đẩy lến thiết bị iOS của người dùng thì giá trị của CategoryCount = 3.
NotificationUID: 32 bit số định danh duy nhất(UID) cho iOS notification. Giá trị này được sử dụng để điều khiển trong các lệnh được gửi đến Control Point characteristic để tương tác với iOS Notification.
Thời gian tồn tại của iOS Notification có thể được suy đoán ngầm thông qua chuỗi Notification Source. GATT notifications được tạo ra bởi NP:
Control Point
Một NC có thể muốn tương tác với một iOS Notification. Nó có thể muốn lấy thêm thông tin về nó bao gồm nội dung của nó hoắc nó có thể muốn thự hiện hành động trên nó. Việc truy suất các thuộc tính này được thực hiện thông qua Control Point và Data Source characteristic.
Một NC có thể đưa ra yêu cầu để nhận thêm thông tin về iOS Notification bằng việc viết lệnh cụ thể tới Control Point characteristic. Nếu việc ghi vào Control Point characteristic thành công, NP sẽ phản hồi kịp thời yêu cầu đó thông qua luồng của GATT notification trên Data Source characteristic.
Một NC có thể thực hiện các hành động được xác đinh trước trên iOS Notification bằng cách viết các lênh cụ thể vào Control Point characteristic.
Get notification Attributes
Lệnh lấy Notification Atributes cho phép NC truy xuất các thuộc tính của iOS Notification cụ thể.
Nội dung của lệnh lấy Notification Attribute như sau:
CommandID: nên được gán là 0 (CommandIDGetNotificationAttributes)
NotificationUID: 32 bit đại diện cho UID của iOS Notification mà khách hàng muốn biết thông tin.
AttributeIDs: Là danh sách các thuộc tính mà NC muốn lấy. Một số thuộc tính có thể cần phải được theo dõi bởi tham số độ dài 16 bit chỉ định số byte tối ta của thuộc tính NC muốn truy xuất.
Định dạng của phản hồi từ lệnh Get Notification Attributes như sau:
CommandID: để là 0(CommandIDGetNotificationAttributes)
NotificationUID: 32 bit định nghĩa UID của iOS Notification
AttributeList: Danh sách các AttributionID có độ dài 16 bit. Một thuộc tính là một cuỗi có độ dài tính bằng byte được cung cấp tring bộ dữ liệu, Không được NULL. Nếu thuộc tính được yêu cầu là trống(empty) hoặc thiếu cho iOS Notification, độ dài của nó được đặt = 0. Dữ liệu sẽ luôn theo thứ tự các AttributionID của lệnh Get Notification Attributes
Nếu phản hồi lớn hơn đơn vị truyền tối đâ GATT(MTU) đã đàm phán, nó sẽ được NP chia thành nhiều mảnh. NC phải tổng hợp lại phản hồi bằng cách ghép từng đoạn. Phản hồi hoàn tất khi các bộ dữ liệu hoàn chỉnh cho từng thuộc tính được yêu cầu đã được nhận.
Get App Attribute
Lệnh Get App Atrributes cho phép NC truy xuất các thuộc tính của một ứng dụng cụ thể được cài đặt trên NP, định dạng của nó thể hiện như hình dưới:
Nội dung của lênh Get App Attribute như sau:
CommandID: cần gán là 1 (CommandIDGetAppAttributes)
AppIdentifier: là chuỗi định danh ứng dụng muốn lấy thông tin. Không được null
AttributeIDs: Danh sác các thuộc tính mà NC muốn truy xuất.
Nội dung phản hồi của lệnh Get App Attributes như sau:
CommandID: là 1(CommandIDGetAppAttributes)
AppIdentifier: là chuỗi định danh ứng dụng muốn lấy thông tin. Không được null
AttributeList: Danh sách AttributeIDs có 16 bit. Một thuộc tính là một cuỗi có độ dài tính bằng byte được cung cấp trong bộ dữ liệu, Không được NULL. Nếu thuộc tính được yêu cầu là trống(empty) hoặc thiếu cho App, độ dài của nó được đặt = 0. Thứ tự dữ liệu sẽ giống với lệnh.
Cúng giống như lệnh Get Notification Attributes. Nếu phản hồi lớn hơn đơn vị truyền tối đâ GATT(MTU) đã đàm phán, nó sẽ được NP chia thành nhiều mảnh. NC phải tổng hợp lại phản hồi bằng cách ghép từng đoạn. Phản hồi hoàn tất khi các bộ dữ liệu hoàn chỉnh cho từng thuộc tính được yêu cầu đã được nhận.
Perform Notification Action
Lệnh thực hiện hành động Notification cho phép NC thực hiện hành động được xác định trước trên một Notification cụ thể của iOS Notification. Lệnh thực hiện Notification Action có các trường sau:
Bytes
Name
Description
1
CommandID
là 2 (CommandIDPerformNotificationAction).
2-5
NotificationUID
32-bit đại diện cho UID của iOS Notification
6
ActionID
ID của hành động mà bạn muốn thực hiện trên iOS Notification
Không có dữ liệu nào được tạo ra trên Data Source characteristic khi lệnh này gặp vấn đề, cho dù nó có thành công hay không.
Notification Actions
Bắt đầu từ iOS 8.0, NP có thể thông báo cho NC về các hành động tềm năng có liên quan đến iOS Notification. Thay mặt người dùng, NC có thể yêu cầu NP để thực hiện một hành động liên quan tới một iOS Notification cụ thể.
NC được thông báo về sự tồn tại của các hành động có thể thực hiện trên một iOS Notification bằng việc phát hiện sự hiện diện của các cờ(flag) được đặt trong trường EventFlags của GATT Notification được tạo bởi Notification Source characteristic.
EventFlagPositiveAction: là hành động tích cực lên iOS notification
EventFlagNegativeAction: là hành động tiêu cực lên iOS notification
Các hành động thực thế được thực hiên bởi NP thay mặt NC được xác định bơi NP và thay đổi tùy thuộc vào iOS Notification mà chúng được thực hiện trên nó. Ví dụ: Có một cuộc gọi đến, hành động Trả lời thì là hành động tích cực(EventFlagPositiveAction), còn Từ Chối là hành động tiêu cực dựa trên iOS notification đó.
NC phải giả định hay cố gắng đoán trước hành động chính xác được thực hiện trên iOS Notification, bởi vì những hành động này dựa trên thông tin không có sẵn cho nó, cũng như cá yếu tố khác như phiên bản ANCS do NP triển khai. NP đảm bảo rằng hành động tích cực và tiêu cực có liên quan đến kết quả, mà nó không làm cho người dùng bất ngờ.
Nếu iOS Notification được hiển thị, hành động tích cực và tiêu cưc có thể hiển thị cho người dùng dựa trên kí tự X hoặc màu sắc.
NC có thể truy suất các nhãn mô tả ngắn gọn các thành động thực tế được liên kết với một iOS Notification bằng việc truy xuất thuộc tính thông báo mới được giới thiệu trong iOS 8.0
NotificationAttributeIDPositiveActionLabel: nhãn(label) được sử dụng để mô tả hành động tích cực có thể được thực hiện trên iOS Notification.
NotificationAttributeIDNegativeActionLabel: nhãn(label) được sử dụng để mô tả hành động tiêu cực có thể được thực hiện trên iOS Notification.
Sessions
Một ANCS session bắt đầu khi NC đăng ký Notification Source characteristic trên NP và kết thúc khi NC hủy đăng ký khỏi dùng một chẩcteristic hoặc mất kết nối tới NP. Vì ANCS không được thiết kế để trở thành dich vụ đồng bộ hoàn chỉnh, nên nó không giữ lại các trạng thái qua các session(phiên). Do đó, tất cả các Identifiers (như NotificationUID, và AppIdentifier) và tất cả dữ liệu được trao đổi giữa NC và NP chỉ có hiệu lực trong một session cụ thể.
Khi một session cụ thể kết thúc, NC nên xóa bỏ bất kể identifier và dữ liệu mà nó thu thập và lưu lại trong session đó. Khi một session mới được bắt đầu, NP sẽ cố gắng hết sức để thông báo cho NC về bất kỳ iOS Notification hiện có nào trên hệ thống. NC có thể sử dụng thông tin này để xây dụng mô hình để sử dụng phần còn lại của session.
Attribute Fetching và Caching
Apple đặc biệt khuyến nghị NC chủ fetch các thuộc tính khi cần và có thể đáp ứng với hành động của người dùng. Ví dụ nếu một NC chọn hiển thị iOS Notification đang hoạt động trong một danh sách đơn giản và chỉ hiển thị chi tiết cụ thể của iOS Notification khi được người dùng chọn, sau đó việc truy xuất thuộc tính của iOS notification này có thể được kích hoạt lazily.
Tring một session, Apple khuyến nghị NC xây dựng bộ đệm(cache) của App Attribute cho mỗi app identifier mà nó gặp phải. Việc xây dựng cache cho phép NC tránh truy xuất cùng một App Attribute nhiều lần, vì vậy nó giúp tiết kiệm thời gian và mức tiêu hao pin.
Error codes
Unknown command (0xA0): NP không nhận ra CommandID
Invalid command (0xA1): command không đúng định dạng
Invalid parameter (0xA2): một trong những tham số không tồn tại trên NP
Action failed (0xA2): hành động không được thực hiện
Nếu NP trả lời với một lỗi, nó sữ không tạo ra bất kỳ GATT Notification nào trên Data Source characteristic cho command(lệnh) tương ứng.
Example Diagrams
Hai hình dưới đây cho thấy các ví dụ về tương tác phổ biến giữa NP và NC.
Hình dưới đây cho thấy chuỗi các command và phản hồi điển hình cần thiết để thiết lập ANCS trên NC:
Hình dưới đây thì hiển thị các chuỗi command và phản hồi cần thiết để có thêm thông tin về iOS Notification để hiển thị trên NC:
Tổng kết
Vậy là bài viết này chúng ta đã đi qua một loạt các thông tin của ANCS như ANCS dùng làm gì, nó có những service characteristic nào, các characteristic có định dạng như nào và rất nhiều thông tin liên quan đến việc triển khai ANCS.
Bài viết tiếp theo liên quan đến ANCS mình sẽ đi vào ví dụ code cụ thể để các bạn có thể hiểu rõ hơn về cách hoạt động cũng như chức năng của ANCS.
Hôm nay mình sẽ giới thiệu với các bạn về công nghệ NFC. Công nghệ từng được dự đoán có thể thay đổi thế giới :D. Vậy NFC là gì? Nó được sử dụng như nào? Chúng ta sẽ đi vào chi tiết dưới đây.
NFC là gì?
NFC là viết tắt của Near-Field communications được hiểu là chuẩn kết nối không dây trong khoảng cách gần. NFC được phát triển dựa trên nguyên lý nhận dạng bằng tín hiệu tần số vô tuyến có tốc độ truyền tải dữ liệu tối đa 424 Kbps. Khoảng cách truyền dữ liệu tối đa của nó là khoảng 4 cm.
Công dụng của NFC là để truyền dữ liệu ở phạm vi gần một cách nhanh chóng và an toàn. Hiện nay một số nước người ta cũng ứng dụng vào việc thanh toán hàng hóa cũng như đọc thông tin sản phẩm.
Để sử dụng NFC chúng ta cần có thiết bị đọc (thường là điện thoại di động) và thiết bị đích(có thể là một điện thoại khác, 1 thẻ NFC…)
NFC rất phổ biến ở các dòng điện thoại chạy hệ điều hành Android. Ngày NFC nổi đình nổi đám Apple vẫn không tin tưởng công nghệ này. Cho đến tận Iphone 7 NFC mới được apple thêm vào thiết bị của họ.
Cách sử dụng CoreNFC Framework để quét NFC Tag
Cho đến gần đây thì có vẻ Apple đã chú ý hơn tới công nghệ này. Vì vậy nó chỉ được hỗ trợ từ Iphone 7 trở lên và iOS >= 11.
CoreNFC có nhiệm vụ phát hiện thẻ NFC và đọc thông điệp có chứa dữ liệu NDEF và lưu dữ liệu vào thẻ có thể ghi dữ liệu.
Cài đặt môi trường
Enable Near Field Communication Tag Reading
Bấm vào Capability > Tìm Near Field Communication Tag Reading và add
Lưu ý bạn nên để CoreNFC Framework là optional. Nếu không nó sẽ bị crash app khi chạy trên các thiết bị không hỗ trợ.
Thêm NFC description trong Info.plist
Việc này nhằm mục đích mô tả cho người dùng biết sơ qua về tính năng này. Và khi upload ứng dụng lên store không bị apple reject.
Vậy là việc setup môi trường đã xong. Chúng ta sẽ đi vào code thôi.
Đầu tiên để sử dụng được chúng ta cần Import CoreNFC vào ViewController của bạn.
import CoreNFC
Thêm một biến global để lưu thông tin session reader
var session: NFCNDEFReaderSession?
Thêm 2 dòng code dưới đây vào viewDidLoad() trong trường hợp này mình đang để mỗi lần vào màn hình này là nó bắt đầu scan luôn. Các bạn có thể để 2 dòng này trong đoạn code mà các bạn mong muốn để chủ động hơn:
Lúc này XCode sẽ báo lỗi ViewController của bạn. Nó yêu cầu bạn phải conform NFCNDEFReaderSessionDelegate protocol.
class ViewController: UIViewController, NFCNDEFReaderSessionDelegate
XCode tiếp tục báo lỗi, nó báo bạn đang thiếu các phương thức bắt buộc của NFCNDEFReaderSessionDelegate vì vậy ta thêm 2 func dưới đây:
func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]) {
for message in messages {
for record in message.records {
if let string = String(data: record.payload, encoding: .ascii) {
print(string)
}
}
}
}
func readerSession(_ session: NFCNDEFReaderSession, didInvalidateWithError error: Error) {
}
didDetectNDEFs messages: Hàm này được gọi khi thiết bị của bạn tìm thấy và đọc được thẻ NFC ở gần đó. Từ hàm này chúng ta có thể sử dụng dữ liệu mà thẻ NFC cung cấp.
didInvalidateWithError: Hàm này trả về khi có lỗi xảy ra
Vậy là chúng ta đã hoàn thành việc coding đọc thẻ NFC. Hãy build ứng dụng lên và trải nghiệm 😀
Lưu ý: Trong quá trình test cũng như trải nghiệm tính năng này mình thấy các thiết bị của Apple có tầm khá ngắn. Và để quét được NFC tag các bạn phải dùng mặt trước của điện thoại. Mặt sau quét rất khó thành công.
Để tăng trải nghiệm của người dùng, khiến người dùng cảm thấy ứng dụng của chúng ta chạy nhanh hơn thì một trong những việc đó là giảm thời gian khởi động của chúng ta. Để người dùng có thể trải nghiệm tính năng chính một cách nhanh nhất.
Apple khuyến cáo chúng ta nên lập trình sao cho tổng thời gian khởi động ứng dụng dưới 400ms và bạn phải thực hiện trong vòng chưa đầy 20s nếu không muốn hệ thống tự dừng ứng dụng của bạn. Tuy nhiên thì vì nhiều lý do app chúng ta thường khởi động lâu hơn thế. Bên cạnh việc xử lý những tác vụ tiền khởi động ở trong AppDelegate thì chúng ta cũng cần phải biết cách debug việc khởi động chậm và biết được những gì đã xảy ra trước khi app chạy vào AppDelegate. Dưới đây là một số mẹo từ hội nghị WWDC 2016.
Pre-main time
Rất nhiều xảy ra trước khi hệ thống thực hiện chạy hàm main() của ứng dụng và gọi các hàm trong App Delegate như applicationWillFinishLaunching. Trước iOS 10, rất khó để chúng ta tìm hiểu tại sao một ứng dụng lại khởi động chậm vì những lí do khác ngoài code của bạn.
Hiện tại từ iOS chúng ta có thể làm việc đó dễ dàng hơn băng việc thêm DYLD_PRINT_STATISTICS = 1 vào trong Environment variables của project scheme
Chạy thử ứng dụng trên iPhone 8 ios 13 ta được kết quả như sau:
Khi kiểm tra việc khởi động chậm chúng ta nên chọn thiết bị chậm nhất mà ứng dụng hỗ trợ.
Kết quả cho chúng ta thấy tổng thời gian tính đến thời điểm hệ thông gọi hàm main() của ứng dụng theo sau đó là breakdown của các main steps.
Dưới đây là một số lưu ý mà mình đã đúc kết từ các tài liệu và clip trên để cải thiện thời gian khởi động ứng dụng:
dylib loading time
dynamic loader có nhiệm vụ tìm và đọc các thư viện(dylibs) được ứng dụng sử dụng. Mỗi thư viện có thể có các phụ thuộc(Dependencies). Cơ chế load framework hệ thống của Apple là rất tối ưu, tuy nhên thì khi load những thư viện được nhúng vào ứng dụng của bạn thì lại không như vậy. Để tăng tốc các dylib thì Apple khuyến cáo bạn bên hạn chế sử dụng các thư viện bên ngoài hoặc gộp chúng lại với nhau làm một framework.
Rebase/binding time
Là khoảng thời gian ứng dụng rebale và binding các pointer.
Ứng dụng chứa nhiều Objective-C class, selector hay category có thể tốn thêm thời gian cho việc khởi động ứng dụng.
Để tăng tốc rebase/binding time bạn cần sử dụng ít các pointer fix-ups hơn.
Nếu bạn sử dụng C++ thì nên sử dụng ít đi các virtual functions.
Trong Swift sử dụng Struct cũng thường nhanh hơn
ObjC setup time
Objective-c runtime cần thiết lập một vài tác vụ cho việc đăng ký class, category và phân biệt các selector. Vì vậy bất kể những cải tiến nào bạn thực hiện để rebase/binding time cũng sẽ áp dụng cho thời gian thiết lập này.
Initializer time
Là lúc các hàm khởi tạo chạy, nếu bạn sử dụng phương thức Objective-C +load(deprecated) hãy thay thế nó bằng +initialize.
Các bạn cũng có thể sử dụng Instruments để theo dõi các chỉ số này.
Sau cùng thệ thống sẽ gọi hàm main() tiếp đến gọi UIApplicationMain() và các phương thức trong AppDelegate.
Loading framework làm tăng thời gian khởi động
Để kiểm chứng chúng ta cùng theo dõi thử nghiệm dưới đây:
Thời gian dylib loading time tăng lên từ 380ms đến 631ms. Các bạn có thể trực tiếp thử nghiệm trên chính ứng dụng của mình để thấy sự khác biệt.
Tổng kết
Bài viết trên mình đã chia sẻ cho các bạn về một số mẹo làm giảm thời gian khởi động ứng dụng. Giúp tăng trải nghiệm người dùng. Chúc các bạn thành công.
Đa số với mỗi lập trình viên đều đã gặp phải những vấn đề về memory leaks.
Ở bài viết này, mình sẽ đi sâu vào memory Leaks và cách xử lí.
Bài viết này đòi hỏi sự hiểu biết về weak/strong reference, retain cycle và ARC trong swift.
Contents:
Memory leaks là gì?
Xử lí memory leaks bằng weak/unowned
Non-escaping closure vs escaping closure
Delay Deallocation
Optional self vs Unwrapped self
Example
Memory leaks là gì?
Memory leaks là 1 phần bộ nhớ bị chiếm vĩnh viễn và không được giải phóng mặc dù không cần dùng đến -> Dẫn đến không thể tái sử dụng phần bộ nhớ này.
Thường xảy ra do retain cycle.
Memory leaks gây ra những gì:
memory của ứng dụng tăng cao không cần thiết -> dẫn đến memory warning và có thể crash.
Những object bị leaks sẽ không bị hủy bỏ -> object đó sẽ luôn lắng nghe thông báo và sẽ thực hiện phản ứng mỗi khi nhận thông báo -> Dẫn đến sai lệch kết quả, có thể đặc biệt nghiêm trọng nếu ảnh hưởng đến database.
Memory leaks demo
Khởi tạo 2 View Controller như sau:
VC1 có 1 button để push sang VC2.
VC2 có hàm deinit để print ra "VC2 was deallocate from memory" khi VC2 được giải phóng bộ nhớ.
Build thử, tap vào button để push sang VC2, và tap vào nút back để quay lại VC1. Quan sát màn hình console, có thể thấy không có gì được print ra.
Ở đây, VC2 không được giải phóng bộ nhớ bởi VC2 giữ 1 strong reference đến closure completion, closure completion cũng giữ 1 strong reference đến VC2 -> Tạo ra 1 retain cycle -> Memory leaks.
Xử lí memory leaks bằng weak/unowned
Cách giải quyết mà mọi người thường dùng nhất là sử dụng weak/unowned.
Vậy sự khác biệt giữa weak và unowned là gì?
Giống:
Weak và unonwed tạo ra 1 weak reference thay vì 1 strong reference để loại bỏ retain cycle -> Bộ nhớ sẽ dc giải phóng ngay lập tức khi không cần dùng đến.
Khác:
Weak
Unowned
– Có thể nil
Không thể nil
Tuy nhiên, unowned cũng giống như việc force unwrapping self và cố gắng truy cập đến contents của nó ngay cả sau khi self đã được giải phóng -> dẫn đến crash.
Crash do truy cập đến self trong khi self đã được xóa khỏi bộ nhớ
Vì vậy ta thường thấy weak self được sử dụng nhiều hơn.
Thay đoạn code viewDidLoad ở VC2 bằng:
Build và chạy thử. Sau khi pop từ VC2 về VC1, màn hình console đã hiện "VC2 was dellocated from memory" -> Memory leaks đã được giải quyết.
Tuy nhiên câu hỏi ở đây là, liệu có cần sử dụng weak cho mọi closure?
Non-escaping closure vs escaping closure
Để trả lời câu hỏi trên, có 1 vài điều trước hết bạn cần phải biết.
non-escaping closure (ví dụ higher-order functions như compactMap): Được thực hiện trong 1 phạm vi thân hàm nhất định, được thực hiện ngay lập tức và sau khi thực hiện thì được giải phóng, không được lưu lại.
escaping closure: Được lưu lại, có thể truyền đi như 1 biến, và có thể được thực hiện lại vào 1 thời điểm khác trong tương lai.
Closure sẽ tạo ra 1 strong reference đối với những thứ được đóng gói bên trong closure, trong ví dụ ở đây là self.
Đối với non-escaping closure: strong reference này sẽ chỉ tồn tại trong thời gian closure đó dc thực hiện -> Khi closure thực hiện xong, strong reference biến mất -> self không còn strong reference nào trỏ đến nên được giải phóng khỏi bộ nhớ.
Đối với escaping closure: Nếu escaping closure này đóng gói 1 self bên trong, thì strong reference này sẽ tồn tại mãi mãi -> tạo ra retain cycle. -> self không được giải phóng.
Demo 2:
Thay đoạn code viewDidLoad của VC2 thành và run thử.
Tuy closure này đóng gói self bên trong nhưng khi back về VC1, VC2 vẫn được giải phóng khỏi bộ nhớ.
Ở đây bạn tạo ra 1 non-escaping closure, nó tạo ra 1 strong reference tới self, khi kết thúc thân hàm, strong reference này sẽ biến mất -> Không bị retain cycle. Vì vậy, không cần dùng weak self trong trường hợp này.
Delay Deallocation
Gỉa sử có 1 func download image từ internet như sau:
Ở đây URLSession.shared.dataTask là 1 non-escaping closure cần nhiều thời gian để chạy.
Non-escaping closure không yêu cầu bạn phải dùng weak self để tránh retain cycle, tuy nhiên với những closure cần nhiều thời gian chạy như trên, thì sẽ tạo ra 1 khoảng thời gian delay trước khi VC2 được deallocate rất lớn -> Nếu ng dùng push và pop vào VC2 liên tục thì vẫn tạo ra tăng memory.
Note: Cân nhắc việc sử dụng weak self đối với non-escaping closure!
Optional self vs Unwrapped self
Dùng weak self sẽ làm cho self thành kiểu optional. Khi đó sẽ thường có 2 kiểu giải quyết:
Dùng optional self: self?
Dùng unwrapped self
Vậy 2 cách này có khác gì nhau?
Khi unwrapped self, thì sẽ chỉ kiểm tra xem self có tồn tại 1 lần duy nhất ở đầu thân hàm, nếu self khác nil thì sẽ tạo 1 strong reference tồn tại trong thời gian chạy hàm. Khi hàm kết thúc, strong reference biến mất, khi đó VC2 mới được deallocated mặc dù đã pop về VC1. -> Vẫn tạo ra 1 delay deallocated, không có retain cycle.
Dùng self? thì sẽ check mỗi lần gọi đến self. Nếu self đã dc giải phóng khỏi bộ nhớ thì trình biên dịch sẽ bỏ qua dòng code đó. -> Không tạo ra delay deallocated, không có retain cycle.
Examples
Grand Central Dispatch
GCD được khởi tạo và thực hiện ngay lập tức, không được lưu lại nên là non-escaping closure, không cần dùng weak self.
-> Đó là lí do không cần dùng weak self ở main.async
Trong Swift, chúng ta có khá nhiều cách để truyền dữ liệu qua lại giữa các đối tượng. Bài viết này mình muốn chia sẻ với các bạn về một số kỹ thuật phổ biến và ưu nhược điểm của nó. Bài viết này mình sẽ đề cập đến 3 cách phổ biến đó là Delegation, Closure và NotificationCenter Observation
Bài toán
Màn hình của mình cần làm có một UITableView, trong UITableView này chứa các UITableViewCell các cell thì có chứa 2 UIButton Dark và Light. Việc của mình phải làm là mỗi khi người dùng tương tác với button ở trong cell thì UIViewController sẽ bắt được sự kiện và hiển thị lên màn hình thông tin cell và hành động mà người dùng vừa tương tác.
Các bạn tải về project bắt đầu này để tiện hơn trong quá trình thực hành nhé:
Delegation là một design pattern cho phép đối tượng gửi thông điệp(data, message …) đến đối tượng khác khi có một sự kiện xảy ra. Ví dụ ta có 2 đối tượng A và B, trên B thực hiện hành động gửi thông điệp sang A để A thực hiện hành động dựa trên kết quả hành động trên B.
Do chúng bài toán của chúng ta cần bắt hành động của người dùng khi họ action lên các button trên cell vì thế trong trường hợp này trong UITableViewCell chúng ta cần tạo protocol cho MyTableViewCell.swift cụ thể như sau:
Ở đây mình tạo ra protocol định nghĩa các action trên cell. Khi người dùng bấm vào button Dark nó sẽ call protocol này tương tự với Button Light.
Tiếp đến chúng ta tạo thêm 2 variables để lưu delegate và indexPath:
var indexPath: IndexPath?
weak var delegate: ActionOnCell?
indexPath để lưu lại cell mà ngươi dùng tương tác.
delegate để biết đối tượng nào đang conform protocol của MyTableViewCell
Đối với delegate chúng ta cần khai báo weak để tránh tham chiếu strong dẫn đến Retain cycle, không giải phóng được các object này vì nó đang tham chiếu tới nhau.
Tiếp đến chúng ta cần gọi các protocol tương ứng khi người dùng tương tác lên các button trên cell:
@IBAction func dark(_ sender: Any) {
if let delegate = delegate {
delegate.didTapDark(indexPath: indexPath)
}
}
@IBAction func light(_ sender: Any) {
if let delegate = delegate {
delegate.didTapLight(indexPath: indexPath)
}
}
Vậy là chúng ta đã setup protocol cho MyTableViewCell.swift giờ tất cả những UIViewController nào sử dụng cell này đều có thể conform protocol của nó để bắt được event khi người dùng tương tác lên các button cell.
Giờ chúng ta quay lại file ViewController.swift kéo xuống hàm cellForRowAt indexPath của tableview và update như sau:
Chúng ta gán lại giá trị indexPath cho các cell. Và set delegate cho Cell là self(viewController hiện tại) Lúc này XCode sẽ báo lỗi Cannot assign value of type ‘ViewController’ to type ‘ActionOnCell?’ vì chúng ta chưa conform protocol ActionOnCell trên ViewController.
Vì vậy chúng ta sẽ tạo 1 extension của ViewController và conform ActionOnCell
Khi người dùng tap vào button trên cell nó sẽ call protocol tương ứng và trả dữ liệu về cho viewController vì vậy bạn có thể sử dụng dữ liệu mà bạn muốn để update dữ liệu lên màn hình.
Các bạn Build project sẽ nhận được kết quả như sau:
NotificationCenter observation
Là một cơ chế gửi thông báo cho phép truyền phát thông tin đến các đối tượng đã đăng ký quan sát notification đó.
Chúng ta sẽ dựa trên 2 cơ chế post và observer của NotificationCenter để gửi và nhận dữ liệu giữa các đối tượng. Đối với notification thì chúng ta có thể sử dụng gửi thông tin từ một đối tượng đến nhiều đối tượng khác đã đăng ký theo dõi notification đó.
Cụ thể trong bài toán này chúng ta sẽ làm như sau: Tạo 2 notification Name đinh nghĩa việc người dùng bấm vào Button Dark và Light:
extension Notification.Name {
static let didTapDarkNotification = NSNotification.Name(rawValue: "didTapDarkNotification")
static let didTapLightNotification = NSNotification.Name(rawValue: "didTapLightNotification")
}
Mở file MyTableViewCell.swift thực hiện việc phát(post) thông báo tại 2 event của 2 button trên cell cụ thể như dưới đây:
Nhiệm vụ của 2 hàm này là để khi nào người dùng bấm vào nut Dark hoặc Light thì NotificationCenter sẽ gửi đi một notification, những đối tượng nào đang lắng nghe các notification name đó sẽ nhận được thông điệp.
Vì vậy chúng ta cần add observer cho ViewController.swift để nó nhận được thông tin khi người dùng tương tác lên các button trên cell. Chúng ta sẽ làm như sau:
Chúng ta cần đăng ký theo dõi notification ở viewWillApper và Remove theo dõi notification khi viewWillDisappear vì nếu chúng ta không remove view controller sẽ luôn nhận được thông báo khi mà nó còn trong view hierarchy dẫn đến những lỗi không mong muốn.
Ý tưởng ở đây chúng ta sẽ tạo 1 closure callback có 2 tham số là action type và dữ liệu truyền lại là index path.
Mở file MyTableViewCell.swift và thêm 1 enum định nghĩa các kiểu action trên cell
enum ActionType {
case dark
case light
}
Tạo closure cho MyTableViewCell
var didTapButton: ((ActionType, IndexPath?) -> Void)?
Gọi closure khi người dùng bấm vào các nút trên cell
@IBAction func dark(_ sender: Any) {
if let didTapButton = didTapButton {
didTapButton(.dark, indexPath)
}
}
@IBAction func light(_ sender: Any) {
if let didTapButton = didTapButton {
didTapButton(.light, indexPath)
}
}
Tiếp đến để nhận được event khi người dùng bấm vào các button chúng ta cần gán giá trị cho closure ở nơi mà bạn muốn. Cụ thể trong trường hợp này là ViewController.swift
Giờ run ứng dụng chúng ta sẽ được kết quả như 2 cách trên.
Đánh giá
Delegation: là cách dùng rất phổ biến và đễ sử dụng nhât khi truyền dữ liệu giữa các đối tượng. Nó dựa vào các protocol để truyền dữ liệu qua lại giữa các đối tượng. Thường dùng trong các trường hợp truyền dữ liệu dạng 1 – 1. Tuy nhiên nếu bạn muốn sử dụng delegate dạng 1 – n hãy tham khảo bài viết này https://magz.techover.io/2019/08/09/swift-design-patterns-multicast-delegate/
NotificationCenter: Là dạng truyền nhận dữ liệu 1 – n hoặc 1 – n. Và khi một thông báo được đẩy đi tất cả những đối tượng đã đăng ký nhận thông báo đó sẽ nhận được thông báo. Khi bạn dùng cần phải kiểm soát chặt các notification. Trong mỗi trường hợp khác nhau chúng ta lại có cách đăng ký notification và remove nó ở các chỗ khác nhau. VD: – Đối với các trường hợp bình thường chúng ta sẽ phải thêm notification ở ViewWillAppear và remove nó ở viewWillDisappear để đảm bảo lần viewWillAppear tiếp theo nó không đăng ký lại dẫn đến bị lặp nhiều lần. – Chúng ta không thể để ở viewDidLoad vì nếu remove ở ViewWillDisappear thì khi pop lại sẽ mất notification, nếu không remove thì khi nó vẫn còn trong view hierarchy chưa bị giải phóng nó sẽ vẫn nhận được notification mặc dù chungs ta không mong muốn điều đó. Trong tất cả thì cách này là cách rắc rối nhất. Bạn chỉ nên dùng khi muốn thực hiện dạng truyền dữ liệu 1-n
Closure: Là cách dùng khá phổ biến hiện tại, vì nó là cách viết khá gọn gàng vì vậy tiết kiệm được thời gian code. Tuy nhiên cách viết của nó khá khó hiểu với người mới. Các bạn cũng có thể nhận thấy sự phổ biến của nó thông qua các hàm completion, callback mà Apple đã sử dụng rất nhiều trong các API của họ.
Tổng kết
Mình hi vọng bài viết giúp cho các bạn có thể lựa chọn các cách truyền dữ liệu phù hợp với bài toán của mình. Chúc các bạn thành công!
Nếu bài viết có vấn đề gì các bạn hãy comment xuống dưới để mình update nhé!