Blog

  • The Good, The Bad and the Ugly

    (Một bộ phim kinh điển mà mình cực kỳ thích nên lấy nó làm title cho bài viết này)
    Đây là bản “hồi ký” khi mình tìm solution cho một bài toán, hi vọng nó sẽ giúp ai đó định hướng được đường phải đi.

    Bài toán: mix 2 file audio trên android mà không dùng thêm library nào

    • Suy nghĩ đầu tiên là tìm xem android có chìa API nào ra để mix file ko – bỏ đi, các cậu ko cần search, android ko chìa api nào ra để làm việc này đâu
    • Suy nghĩ thứ 2: Đưa dữ liệu âm thanh(mp3..etc) về dạng raw data và thử mix các bit vào nhau? có vẻ khả thi

    The bad:

    
    private byte[] mBufData1 = null;
    private byte[] mBufData2 = null;
    private ArrayList mBufMixedData = new ArrayList<>();
    
    private void loadMixedData() {
            int length1 = mBufData1.length;
            int length2 = mBufData2.length;
            int max = Math.max(length1, length2);
            int tempSplitSize;
            if (length1 == length2) {
                for (int i = 0; i < length1; i++) {
                    mBufMixedData.add((byte) (mBufData1[i] + mBufData2[i]));
                }
            } else {
                if (length2 > length1) {
                    tempSplitSize = length1;
                } else {
                    tempSplitSize = length2;
                }
                for (int i = 0; i < tempSplitSize; i++) {
                    mBufMixedData.add((byte) (mBufData1[i] + mBufData2[i]));
                }
                if (length2 > length1) {
                    for (int i = tempSplitSize; i < max; i++) {
                        mBufMixedData.add((mBufData2[i]));
                    }
                } else {
                    for (int i = tempSplitSize; i < max; i++) {
                        mBufMixedData.add((mBufData1[i]));
                    }
                }
    
            }
        }

    Chạy được thật, âm thanh đã được mix lại như file karaoke ngoài hàng. Nhưng nhìn source code thì chỉ có đứa mù dở mới ko nhìn thấy vấn đề OOM chắc chắn phát sinh.
    Nếu dừng ở đây – Ok, nó chạy được, chúng ta sẽ giấu đi đoạn OOM kia, kệ cho dự án hót shit vì còn lâu họ mới test ra issue ý, lúc phát hiện chúng ta đã cao chạy xa bay rồi. Ka ka ka
    Chúng ta là những kẻ tồi tệ

    The Ugly

    Cải tiến hơn 1 chút, để tránh OOM, mình ko load hết dữ liệu lên ram nữa mà đưa vào DataOutputStream để write từng bit xuống file

    private void createWaveMixing(String p1, String p2, String p3) throws IOException {
            int size1 = 0;
            int size2 = 0;
            int size1;
            int size2;
    
            FileInputStream fis1 = null;
            FileInputStream fis2 = null;
            try {
                fis1 = new FileInputStream(p1);
                fis2 = new FileInputStream(p2);
                size1 = fis1.available();
                size2 = fis2.available();
                long totalAudioLen = size1;
                if (size1 < size2) {
                    totalAudioLen = size2;
                }
                long totalDataLen = totalAudioLen + WavUtil.LENGTH_EXTENDED;
                long longSampleRate = WavUtil.getSampleRate(p1);//44100
                long totalDataLen = totalAudioLen + WavUtils.LENGTH_EXTENDED;
                long longSampleRate = MediaCodecUtils.getSampleRate(p1);//44100
                int channels = 2;
                long byteRate = WavUtil.RECORDER_BPP * longSampleRate * channels / 8;
                long byteRate = WavUtils.RECORDER_BPP * longSampleRate * channels / 8;
                DataOutputStream out = null;
                try {
                    out = new DataOutputStream(new FileOutputStream(p3));
                    WavUtil.writeWaveFileHeader(out, totalAudioLen, totalDataLen, longSampleRate, channels, byteRate);
                    WavUtils.writeWaveFileHeader(out, totalAudioLen, totalDataLen, longSampleRate, channels, byteRate);
                    out.write(toByteArray(mBufMixedData));
                } catch (Exception e) {
                    Log.e(TAG, "#createWaveMixing():" + e.getMessage(), e);
                } finally {
                    if (out != null) {
                        out.close();
                    }
                }
            } finally {
                if (fis1 != null) {
                    fis1.close();
                }
                if (fis2 != null) {
                    fis2.close();
                }
            }
    
        }
    
        private byte[] toByteArray(ArrayList in) {
            byte[] data = new byte[in.size()];
            for (int i = 0; i < data.length; i++) {
                data[i] = in.get(i);
            }
            return data;
        }
    }

    Cách làm này tránh được OOM, nhưng phải nói là nó chậm, thực sự chậm, chậm kinh khủng. Đâu đó mất khoảng 50 -60 s cho 2 file music raw dài 3 phút.
    Nói chung là chạy được, ko có issue gì cả, chỉ chậm thôi. Chậm thì tự tìm cách mà improve đi, kêu gì – đúng ko các cậu? – lại chả phải quá.
    Nếu dừng ở đây, chúng ta là những gã lừa đảo.

    Nhưng dù sao mình cũng là người vừa đẹp trai lại tốt tính, nên mới xuất hiện tình huống thứ 3

    The Good

    Lần này mình optimize bằng cách sử dụng FileChannel, thay vì handle từng bit một, thì mình bóc một nhóm lớn ra để mix(buôn sỉ mới nhanh giầu)

    private void createWaveMixing(String p1, String p2, String p3) throws IOException {
            int size1;
            int size2;
    
            FileInputStream fis1 = null;
            FileInputStream fis2 = null;
            try {
                fis1 = new FileInputStream(p1);
                fis2 = new FileInputStream(p2);
                size1 = fis1.available();
                size2 = fis2.available();
                long totalAudioLen = size1;
                if (size1 < size2) {
                    totalAudioLen = size2;
                }
                long totalDataLen = totalAudioLen + WavUtils.LENGTH_EXTENDED;
                long longSampleRate = MediaExtractorUtils.getSampleRate(p1);//44100
                int channels = 2;
                long byteRate = WavUtils.RECORDER_BPP * longSampleRate * channels / 8;
                DataOutputStream out = null;
    
                FileChannel fc1 = fis1.getChannel();
                FileChannel fc2 = fis2.getChannel();
                long length1 = fc1.size();
                long length2 = fc2.size();
                try {
                    out = new DataOutputStream(new FileOutputStream(p3));
                    WavUtils.writeWaveFileHeader(out, totalAudioLen, totalDataLen, longSampleRate, channels, byteRate);
                    {
                        ByteBuffer buff1 = ByteBuffer.allocate(BUFFER_SIZE);
                        ByteBuffer buff2 = ByteBuffer.allocate(BUFFER_SIZE);
    
                        ByteBuffer mixedBuffer = null;
                        if (length1 == length2) {
                            while (fc1.read(buff1) > 0) {
                                fc2.read(buff2);
                                mixedBuffer = mixByteBuffer(buff1, buff2);
                                out.write(mixedBuffer.array());
                                buff1.clear();
                                buff2.clear();
                                mixedBuffer.clear();
                            }
                            if (mixedBuffer != null) {
                                mixedBuffer.clear();
                            }
                        } else {
                            if (length2 > length1) {
                                while (fc1.read(buff1) > 0) {
                                    fc2.read(buff2);
                                    mixedBuffer = mixByteBuffer(buff1, buff2);
                                    out.write(mixedBuffer.array());
                                    buff1.clear();
                                    buff2.clear();
                                    mixedBuffer.clear();
                                }
                                while (fc2.read(buff2) > 0) {
                                    out.write(buff2.array());
                                    buff2.clear();
                                }
                            } else {
                                while (fc2.read(buff2) > 0) {
                                    fc1.read(buff1);
                                    mixedBuffer = mixByteBuffer(buff1, buff2);
                                    out.write(mixedBuffer.array());
                                    buff1.clear();
                                    buff2.clear();
                                    mixedBuffer.clear();
                                }
                                while (fc1.read(buff1) > 0) {
                                    out.write(buff1.array());
                                    buff1.clear();
                                }
                            }
    
                        }
    
                    }
                } catch (Exception e) {
                    Log.e("test", "#createWaveMixing():" + e.getMessage(), e);
                } finally {
                    if (out != null) {
                        out.close();
                    }
                    fc1.close();
                    fc2.close();
                }
            } finally {
                if (fis1 != null) {
                    fis1.close();
                }
                if (fis2 != null) {
                    fis2.close();
                }
            }
    
        }

    Kết quả tốc độ tăng gấp 6-8 lần mà lại ko có issue gì cả
    Lần này(dường như) mình sẽ có thể là người tốt

    Cùng 1 bài toán, sẽ có các cách giải khác nhau
    Khoảng thời gian cho phép khác nhau, chúng ta cũng sẽ phải chọn các giải pháp khác nhau(chấp nhận được, chạy có issue, chậm…etc)

    1 ngày làm được ko? được – the bad
    1 tuần làm được ko? được – the ugly
    1 tháng làm được ko? được – the good

    Cái gì cũng sẽ có giá của nó, tùy vào deadline, tùy vào thời điểm, chúng ta hãy cố chọn ra được cách làm tốt nhất

    1 phút dành cho quảng cáo, GDK đã có 1 phiên bản có thể record, mix, nối audio nhé, anh em có thể search gdk-soundutilities

  • [AWS] Remote Debugging ứng dụng Lambda viết bằng Java với Visual Studio Code

    [AWS] Remote Debugging ứng dụng Lambda viết bằng Java với Visual Studio Code

    Debug cũng quan trọng giống như lúc bạn code vậy. Với những bạn làm quen với Lambda thì không phải ai cũng biết làm sao để có thể debug được. Đa phần các bạn sẽ chọn cách in dữ liệu ra màn hình để debug. Hôm nay tôi sẽ hướng dẫn các bạn cách debug ứng dụng viết bằng Lamba nhé.

    Remote Debugging

    Như các bạn đều biết thì để có thể debug được ứng dụng Java thì bạn cần phải Remote tới cổng Debug của trình thực thi Java. Quá trình này được gọi là Remote Debugging. Với ứng dụng Java bình thường các bạn có thể dễ dàng sử dụng các IDE có hỗ trợ Remote Debugging một cách dễ dàng. Với các ứng dụng Lambda bằng Java thì sao?

    Khởi động ứng dụng Lambda với chế độ Remote Debugging

    Trong bài viết Phát triển ứng dụng Lambda bằng Java, tôi đã hướng dẫn các bạn cách sử dụng SAM để chạy các ứng dụng Lambda viết bằng ngôn ngữ Java. Các bạn theo dõi bài viết trên sẽ thấy ứng dụng được chạy trên một máy ảo Java trông một docker container. Để khời động chế để Remote Debugging thì các bạn gõ lệnh sau(các bạn nhớ khởi động Docker trước khi khởi động SAM nhé):

    hieunv@HieuNV sam-app % sam local start-api -d 5858
    Mounting HelloWorldFunction at http://127.0.0.1:3000/hello [GET]
    You can now browse to the above endpoints to invoke your functions. You do not need to restart/reload SAM CLI while working on your functions, changes will be reflected instantly/automatically. You only need to restart SAM CLI if you update your AWS SAM template
    2020-03-30 20:10:33  * Running on http://127.0.0.1:3000/ (Press CTRL+C to quit)
    

    Các bạn sẽ thấy SAM được khởi động và lắng nghe ở cổng 3000. Còn cổng 5858 thì sao? Tại thời điểm này nó chưa được khởi động. Khi bạn access vào http://127.0.0.1:3000/hello thì cổng Remote Debugging 5858 mới được khởi động.

    Invoking helloworld.App::handleRequest (java11)
    
    Fetching lambci/lambda:java11 Docker container image......
    Mounting /Users/hieunv/Projects/hieunv/sam-app/.aws-sam/build/HelloWorldFunction as /var/task:ro,delegated inside runtime container
    Picked up _JAVA_OPTIONS: -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,quiet=y,address=*:5858 -XX:MaxHeapSize=2834432k -XX:MaxMetaspaceSize=163840k -XX:ReservedCodeCacheSize=81920k -XX:+UseSerialGC -XX:-TieredCompilation -Djava.net.preferIPv4Stack=true
    

    Cấu hình Remote Debug trong Visual Studio Code

    Các bạn quay lại Visual Studio Code, vào Tab Debug sau đó chọn create a launch.json file. Tại mục chọn kiểu Debug bạn chon Add Configuration và chọn

    add configuration

    Sau đó các bạn chon Attach To Remote Program

    Attach To Remote Program

    Tiếp đó các bạn sửa lại cấu hình hostName thành localhostport thành 5858(đấy là cổng Remote Debug của trình thực thi Java)

    {
      // Use IntelliSense to learn about possible attributes.
      // Hover to view descriptions of existing attributes.
      // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
      "version": "0.2.0",
      "configurations": [
        {
          "type": "java",
          "name": "Debug (Attach) - Remote",
          "request": "attach",
          "hostName": "localhost",
          "port": 5858
        }
      ]
    }
    

    Đặt break point

    Các bạn quay lại Visual Studio Code và mở mã nguồn muốn debug sau đó đặt break point

    break point

    Khởi động Visual Studio Code Debug bằng các click vào nut Start

    Xem output log bạn sẽ thấy thông báo sau:

    START RequestId: 4f69214b-9a3a-19ef-0137-5081d7caccea Version: $LATEST
    END RequestId: 4f69214b-9a3a-19ef-0137-5081d7caccea
    REPORT RequestId: 4f69214b-9a3a-19ef-0137-5081d7caccea	Init Duration: 58932.30 ms	Duration: 10421.29 ms	Billed Duration: 10500 ms	Memory Size: 512 MB	Max Memory Used: 73 MB
    2020-03-30 20:39:18 127.0.0.1 - - [30/Mar/2020 20:39:18] "GET /hello HTTP/1.1" 500 -
    ``
    

    Sau đó bạn access http://127.0.0.1:3000/hello bằng Postman và quay lại Visual Studio Code

    debug mode

    Như vậy là chúng ta đã debug thành công vào hàm Lambda rồi.

    Cám ơn các bạn đã theo dõi bài viết. Hy vọng bài viết sẽ giúp ích với dự án của các bạn. Chúc các bạn thành công.

  • iOS: Một số cách lưu dữ liệu phổ biến

    iOS: Một số cách lưu dữ liệu phổ biến

    Lời mở đầu

    Đố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.

  • ARC in Swift

    ARC in Swift

    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 departmentowner của 2 instance tuangotham đã 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 = nilgotham = 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.

  • Design Pattern: Singleton trong Swift

    Design Pattern: Singleton trong Swift

    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.

  • Các bước hướng dẫn chi tiết submit ứng dụng lên Google Store

    Các bước hướng dẫn chi tiết submit ứng dụng lên Google Store

    Khi bạn đã có trong tay một sản phẩm tốt và đầy tâm huyết. Vậy làm cách nào để có thể giới thiệu ứng dụng của mình đến với nhiều người dùng? Để mọi người biết đến và có thể sử dụng nó? Để nhận được những góp ý giúp cải thiện hơn, hay cũng là một nguồn thu nhập của bạn, thì bạn sẽ phải làm gì?. Chắc chắn, với một developer phát triển về mobile thì bạn sẽ có những câu hỏi như vậy, phải không?

    Để giải đáp cho câu hỏi đó thì iOS bạn sẽ sử dụng App Store để có sumit ứng dụng của bạn, App Store chính là market chính và có nhiều người iOS biết tới nhất.
    Với Android, đó chính là Google Play Store. Google Play Store cũng là market chính và có nhiều người dùng Android biết tới nhất. Vậy làm cách nào để có thể submit ứng dụng lên Google Store? Đừng lo, bài viết này mình sẽ giới thiệu tới các bạn luôn đây. Let’s start…

    Nội dung chính của bài viết:

    • Bạn cần chuẩn bị gì trước khi submit ứng dụng lên Google Store?
    • Cần tối ưu kích thước APK file
    • Từng bước submit ứng dụng lên Google Store
    • Đăng kí tài khoản Google Developer
    • Tạo ứng dụng mới và điền thông tin mô tả ứng dụng
    • Upload logo và screenshot ứng dụng
    • Tiến hành upload APK lên Store
    • Hoàn thành đánh giá Content rating
    • Đăng kí ứng dụng miễn phí hay trả phí

    Bạn cần chuẩn bị gì trước khi submit ứng dụng lên Google Store?

    Khác với Apple, khi mà App Store có quá trình review rất chặt chẽ. Các ứng dụng khi submit lên App Store đều trải qua quá trình review thủ công. Điều này sẽ đảm bảo ứng dụng trên App Store có chất lượng tốt nhất trước khi tới tay người dùng.

    Google Play Store thì quá trình review ứng dụng thường làm bằng máy. Do vậy, ứng dụng của bạn có nhiều cơ hội được approve hơn rất nhiều.

    Tuy nhiên, để ứng dụng thành công với hàng nghìn, thậm chí hàng triệu lượt tải thì bạn cần bỏ công sức ra tối ưu cũng như chuẩn bị kĩ càng trước khi submit.

    Những điều bắt buộc phải làm khi submit ứng dụng lên Google Store

    • Tạo một Bundle ID cho ứng dụng
    • Tạo một APK or Android App Bundles có sign key
    • Và tất nhiên là phải có một tài khoản Google Developer( chi phí để tạo là 25$)

    Cần tối ưu kích thước APK file

    Nếu ứng dụng của bạn có kích thước lớn thì nên chia ra thành nhiều module. Cách làm giống như các Game hay làm vậy. Các bạn chỉ đưa phần chính của ứng dụng lên Store. Sau khi người dùng tải ứng dụng về thì sẽ tiếp tục tải data.

    Tuy nhiên, mình khuyến khích là kích thước APK càng nhỏ càng tốt vì điều đó tốt cho ASO( App Store Optimization). Google Play Store ưu tiên các ứng dụng nhỏ nhẹ, nhưng chất lượng tốt.

    Lưu ý: Google Play supports compressed app downloads of only 150 MB or less.

    Đăng kí tài khoản Google Developer

    Đầu tiên, bạn cần đăng kí trở thành nhà phát triển ứng dụng ở đây: Google Play Console.

    Bạn đăng nhập bằng tài khoản Google như bình thường. Tick vào ô bên dưới Developer Agreement để chuyển sang màn hình thanh toán.

    Sau khi thanh toán bằng thẻ VISA/ MasterCard xong thì bạn cần điền các thông tin cần thiết cho nhà phát triển như: developer name, email address, website, phone number.

    Cuối cùng là nhấn vào nút COMPLETE REGISTRATION.

    Tạo ứng dụng mới và điền thông tin mô tả ứng dụng

    Như vậy là bạn đã trở thành nhà phát triển ứng dụng rồi đấy. Công việc tiếp theo là tạo ứng dụng mới bằng cách nhấn vào nút CREATE APPLICATION

    Lưu ý: Tiêu đề là phần hiển thị trên Google Play chứ không phải tên ứng dụng khi cài vào điện thoại. Nên bạn có thể khéo léo đưa từ khóa vào tiêu đề để tối ưu ASO.

    Tiếp theo, chúng ta cần điền thông tin mô tả ứng dụng: short description và full description.

    Rồi nhấn nút SAVE DRAFT. khi đã điền xong.

    Upload logo và screenshot ứng dụng

    Phần này chúng ta sẽ cần upload ảnh logo, screenshot của ứng dụng. Lưu ý là logo cần có kích thước là 512x512px.

    Nhấn vào Add high-res icon để tải logo lên.

    Chọn Add feature graphic. Đây là ảnh promo được hiển thị trên đỉnh của trang ứng dụng trên Google play.

    Nói chung, bạn có ảnh hay video nào có thể promo được cho ứng dụng thì upload hết lên đây nhé. Càng nhiều càng tốt…

    Cuối cùng là nhấn SAVE DRAFT. để tiếp tục.

    Phía dưới của màn hình có phần chọn kiểu ứng dụng là : App hay Game. Chọn Category phù hợp với ứng dụng: Tool, Productivity, Entertainment…

    Ở màn này có một mục là Content rating. Phần này mình sẽ hướng dẫn chi tiết ở phía dưới bài viết nhé.

    Điền URL tới file privacy policy. Nếu bạn chưa biết cách viết privacy policy như thế nào thì có thể sử dụng công cụ sau để tạo tự động App Privacy Policy Generator

    Bạn sẽ được chuyển đến màn hình như bên dưới.

    Tiến hành upload APK lên Store

    Bạn chọn MANAGE PRODUCTION để tiếp tục hoàn thành các bước tiếp theo.

    Ngoài ra, bạn cũng có thể lựa chọn chạy alpha hay beta testing trước khi thực sự publish ứng dụng cho tất cả người dùng.

    Ở bài viết này, mình không đề cập đển việc chạy alpha hay beta testing. Chúng ta publish chính luôn.

    Nhấn nút CREATE RELEASE.

    Nếu bạn đã tạo APK có sẵn sign key rồi thì không cần phải làm gì cả, chọn luôn opt out.

    Tiếp tục là chọn browse files để upload APK từ máy tính lên.

    Bạn có thể sửa release name, nhưng mình thì cứ để mặc định là số phiên bản của ứng dụng.

    Điền các thông tin chính cho bản apk này (thường thì người ta hay điền các tính năng mới mà cho lần upgrade ứng dụng) rồi nhấn REVIEW.

    APK cũng đã upload lên. Việc tiếp theo như mình nói ở trên là hoàn thành đánh giá Content rating.

    Hoàn thành đánh giá Content rating

    Phần Content rating này, bạn cứ trả lời thật với những câu hỏi của họ là ổn. Các câu hỏi kiểu như: Ứng dụng có liên quan đến SEX không? Ứng dụng có kích động, phản động hay liên quan đến Phát xít không? … Cứ trả lời thật nhé

    Đăng kí ứng dụng miễn phí hay trả phí

    Phần cuối cùng là Pricing & distribution

    Bạn cần cân nhắc là ứng dụng của bạn sẽ phát hành miễn phí hay là bán cho người dùng.

    Mình chỉ lưu ý là: Một khi đã chọn là ứng dụng miễn phí thì bạn không thể chuyển thành ứng dụng trả phí được nữa. Nhưng ngược lại thì được.

    Nếu bạn chọn là ứng dụng trả phí thì cần phải cài đặt phương thức nhận tiền để Google còn thanh toán cho bạn chứ.

    Tất cả đã xong. Việc của bạn bây giờ là quay trở lại App releases và nhấn nút START ROLLOUT TO PRODUCTION và chờ đợi Google review và appove cho ứng dụng của bạn.

    Theo kinh nghiệm của mình thì thời gian review sẽ tầm khoảng 4 giờ. Trong lúc chờ đợi thì hãy đón đọc các bài tiếp theo cũng mình nhé :))

  • iOS: Apple Notification Center Service

    iOS: Apple Notification Center Service

    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.

    Service characteristics

    Cơ bản thì ANCS có 2 characteristics như sau:

    • Notification Source: UUID 9FBF120D-6301-42D9-8C58-25E699A21DBD (notifiable)
    • 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:

    BytesNameDescription
    1CommandIDlà 2 (CommandIDPerformNotificationAction).
    2-5NotificationUID32-bit đại diện cho UID của iOS Notification
    6ActionIDID 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.

  • iOS/Swift: NFC là gì? Cách quét thẻ NFC sử dụng CoreNFC

    iOS/Swift: NFC là gì? Cách quét thẻ NFC sử dụng CoreNFC

    Lời mở đầu

    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 ý: Near Field communication Tag Reading chỉ hỗ trợ cho các tài khoản trả tiền. Chi tiêt xem tại: https://help.apple.com/developer-account/#/dev21218dfd6

    Thêm CoreNFC Framework

    Lưu ý bạn nên để CoreNFC Frameworkoptional. 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:

    session = NFCNDEFReaderSession(delegate: self, queue: DispatchQueue.main, invalidateAfterFirstRead: false)
    session?.begin()

    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.

    Chúc các bạn thành công!

  • iOS: Giảm thời gian khởi động ứng dụng

    iOS: Giảm thời gian khởi động ứng dụ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.

    Refer

    Lời mở dầu

    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.

    Các bạn xem clip này để tìm hiểu sâu hơn  WWDC 2016 Session 406 Optimizing App Startup Time

    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:

    Trước khi thêm thư viện:

    Total pre-main time: 408.97 milliseconds (100.0%)
         dylib loading time: 383.84 milliseconds (93.8%)
        rebase/binding time:   7.86 milliseconds (1.9%)
            ObjC setup time:   6.82 milliseconds (1.6%)
           initializer time:  10.36 milliseconds (2.5%)
           slowest intializers :
             libSystem.B.dylib :   2.33 milliseconds (0.5%)

    Sau khi thêm 10 thư viện Swift sử dụng Cocoapods

    Total pre-main time: 682.90 milliseconds (100.0%)
         dylib loading time: 631.17 milliseconds (92.4%)
        rebase/binding time:  17.06 milliseconds (2.4%)
            ObjC setup time:  17.47 milliseconds (2.5%)
           initializer time:  17.09 milliseconds (2.5%)
           slowest intializers :
             libSystem.B.dylib :   6.05 milliseconds (0.8%)

    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.

  • Deep dive into Memory Leaks Swift

    Deep dive into Memory Leaks Swift

    Đ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:
    WeakUnowned
    – Có thể nilKhô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:

    override func viewDidLoad() {
        super.viewDidLoad()
        completion = { [weak self] in
            self?.view.backgroundColor = .red
        }
    }

    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ử.
    override func viewDidLoad() {
        super.viewDidLoad()
        let nonEscapingClosure = {
            self.view.backgroundColor = .red
        }
    }
    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
    DispatchQueue.main.async {
        self.view.backgroundColor = .yellow
    }

    UIView.Animate

    UIView.animate(withDuration: 0, animations: {
        self.view.backgroundColor = .yellow
    }, completion: nil)

    Tương tự như GCD, closure trong UIView.animate cũng là 1 non-escaping closure nên không cần weak self.

    Timer

    let _ = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { (_) in
         self.view.backgroundColor = .yellow
    }

    Timer sẽ tạo ra 1 strong reference và retain cycle nếu thỏa mãn 2 điều kiện:

    • Timer chạy lặp lại liên tục.
    • Timer capture lại self trong block closure

    -> Nếu thoả mãn 2 điều kiện trên thì phải dùng weak self đối với Timer.