Cách tốt nhất để đặt c-struct trong NSArray là gì?


89

Cách thông thường để lưu trữ cấu trúc c trong an là NSArraygì? Ưu nhược điểm, xử lý bộ nhớ?

Đáng chú ý, sự khác biệt giữa valueWithBytesvalueWithPointer - được nuôi bởi justin và catfish dưới đây.

Đây là một liên kết đến cuộc thảo luận của Apple valueWithBytes:objCType:dành cho độc giả trong tương lai ...

Đối với một số tư duy bên và xem xét nhiều hơn về hiệu suất, Evgen đã đặt ra vấn đề sử dụng STL::vectortrong C ++ .

(Điều đó đặt ra một vấn đề thú vị: liệu có một thư viện c nhanh, không khác STL::vectornhưng nhẹ hơn nhiều, cho phép "xử lý các mảng" ...?

Vì vậy, câu hỏi ban đầu ...

Ví dụ:

typedef struct _Megapoint {
    float   w,x,y,z;
} Megapoint;

Vậy: cách bình thường, hay nhất, thành ngữ để lưu trữ cấu trúc của chính mình như thế trong an NSArray, và bạn xử lý trí nhớ trong thành ngữ đó như thế nào?

Xin lưu ý rằng tôi đang đặc biệt tìm kiếm thành ngữ thông thường để lưu trữ cấu trúc. Tất nhiên, người ta có thể tránh vấn đề này bằng cách tạo ra một lớp nhỏ mới. Tuy nhiên, tôi muốn biết thành ngữ thông thường thực sự đặt cấu trúc trong một mảng như thế nào, cảm ơn.

BTW đây là cách tiếp cận NSData có lẽ là? không tốt nhất ...

Megapoint p;
NSArray *a = [NSArray arrayWithObjects:
    [NSData dataWithBytes:&p length:sizeof(Megapoint)],
    [NSData dataWithBytes:&p length:sizeof(Megapoint)],
    [NSData dataWithBytes:&p length:sizeof(Megapoint)],
        nil];

BTW như một điểm tham khảo và cảm ơn Jarret Hardie, đây là cách lưu trữ CGPointsvà tương tự trong một NSArray:

NSArray *points = [NSArray arrayWithObjects:
        [NSValue valueWithCGPoint:CGPointMake(6.9, 6.9)],
        [NSValue valueWithCGPoint:CGPointMake(6.9, 6.9)],
        nil];

(xem Làm cách nào để thêm các đối tượng CGPoint vào NSArray một cách dễ dàng? )


mã của bạn để chuyển đổi nó thành NSData sẽ ổn .. và không bị rò rỉ bộ nhớ .... tuy nhiên, người ta cũng có thể sử dụng mảng cấu trúc C ++ chuẩn Megapoint p [3];
Swapnil Luktuke

Bạn không thể thêm tiền thưởng cho đến khi câu hỏi được hai ngày.
Matthew Frederick

1
valueWithCGPoint không khả dụng cho OSX. Đó là một phần của UIKit
lppier

@Ippier valueWithPoint khả dụng trên OS X
Schpaencoder

Câu trả lời:


159

NSValue không chỉ hỗ trợ cấu trúc CoreGraphics - bạn cũng có thể sử dụng nó cho riêng mình. Tôi khuyên bạn nên làm như vậy, vì lớp có thể nhẹ hơn so NSDatavới cấu trúc dữ liệu đơn giản.

Chỉ cần sử dụng một biểu thức như sau:

[NSValue valueWithBytes:&p objCType:@encode(Megapoint)];

Và để lấy lại giá trị:

Megapoint p;
[value getValue:&p];

4
@Joe Blow @Catfish_Man Nó thực sự sao chép cấu trúc p, không phải là một con trỏ tới nó. Các @encodechỉ thị cung cấp tất cả các thông tin cần thiết về lớn như thế nào cấu trúc được. Khi bạn giải phóng NSValue(hoặc khi mảng), bản sao cấu trúc của nó sẽ bị phá hủy. Nếu bạn đã sử dụng getValue:trong thời gian chờ đợi, bạn vẫn ổn. Xem phần "Sử dụng giá trị" của "Chủ đề lập trình số và giá trị": developer.apple.com/library/ios/documentation/Cocoa/Conceptual/…
Justin Spahr-Summers

1
@Joe Blow Hầu như đúng, ngoại trừ việc nó sẽ không thể thay đổi trong thời gian chạy. Bạn đang chỉ định loại C, loại này luôn phải được biết đầy đủ. Nếu nó có thể trở nên "lớn hơn" bằng cách tham chiếu nhiều dữ liệu hơn, thì bạn có thể sẽ triển khai điều đó với một con trỏ và @encodesẽ mô tả cấu trúc với con trỏ đó, nhưng không mô tả đầy đủ dữ liệu trỏ đến, điều này thực sự có thể thay đổi.
Justin Spahr-Summers

1
Sẽ NSValuetự động giải phóng bộ nhớ của cấu trúc khi nó được phân bổ? Tài liệu là một chút không rõ ràng về điều này.
defos1

1
Vì vậy, chỉ cần hoàn toàn rõ ràng, NSValuesở hữu dữ liệu mà nó sao chép vào chính nó và tôi không phải lo lắng về việc giải phóng nó (theo ARC)?
devos1

1
@devios Đúng. NSValuekhông thực sự thực hiện bất kỳ “quản lý bộ nhớ” nào - bạn có thể nghĩ nó chỉ là một bản sao của giá trị cấu trúc bên trong. Ví dụ: nếu cấu trúc chứa các con trỏ lồng nhau NSValuesẽ không biết giải phóng hoặc sao chép hoặc làm bất cứ điều gì với những con trỏ đó — nó sẽ khiến chúng không bị ảnh hưởng, sao chép địa chỉ như hiện tại.
Justin Spahr-Summers

7

Tôi sẽ đề nghị bạn giữ nguyên NSValuetuyến đường, nhưng nếu bạn thực sự muốn lưu trữ các structkiểu dữ liệu đơn giản trong NSArray của mình (và các đối tượng thu thập khác trong Cocoa), bạn có thể làm như vậy - mặc dù gián tiếp, sử dụng Core Foundation và bắc cầu miễn phí .

CFArrayRef(và đối tác có thể thay đổi của nó, CFMutableArrayRef) cho phép nhà phát triển linh hoạt hơn khi tạo một đối tượng mảng. Xem đối số thứ tư của trình khởi tạo được chỉ định:

CFArrayRef CFArrayCreate (
    CFAllocatorRef allocator,
    const void **values,
    CFIndex numValues,
    const CFArrayCallBacks *callBacks
);

Điều này cho phép bạn yêu cầu CFArrayRefđối tượng sử dụng các quy trình quản lý bộ nhớ của Core Foundation, hoàn toàn không hoặc thậm chí là các quy trình quản lý bộ nhớ của riêng bạn .

Ví dụ bắt buộc:

// One would pass &kCFTypeArrayCallBacks (in lieu of NULL) if using CF types.
CFMutableArrayRef arrayRef = CFArrayCreateMutable(kCFAllocatorDefault, 0, NULL);
NSMutableArray *array = (NSMutableArray *)arrayRef;

struct {int member;} myStruct = {.member = 42};
// Casting to "id" to avoid compiler warning
[array addObject:(id)&myStruct];

// Hurray!
struct {int member;} *mySameStruct = [array objectAtIndex:0];

Ví dụ trên hoàn toàn bỏ qua các vấn đề liên quan đến quản lý bộ nhớ. Cấu trúc myStructđược tạo trên ngăn xếp và do đó bị phá hủy khi hàm kết thúc - mảng sẽ chứa một con trỏ đến một đối tượng không còn ở đó. Bạn có thể giải quyết vấn đề này bằng cách sử dụng các thói quen quản lý bộ nhớ của riêng mình - do đó, tùy chọn được cung cấp cho bạn - nhưng sau đó bạn phải thực hiện công việc khó khăn như đếm tham chiếu, cấp phát bộ nhớ, phân bổ nó, v.v.

Tôi sẽ không đề xuất giải pháp này, nhưng sẽ giữ nó ở đây trong trường hợp nó được bất kỳ ai khác quan tâm. :-)


Sử dụng cấu trúc của bạn như được phân bổ trên heap (thay cho ngăn xếp) được minh họa ở đây:

typedef struct {
    float w, x, y, z;
} Megapoint;

// One would pass &kCFTypeArrayCallBacks (in lieu of NULL) if using CF types.
CFMutableArrayRef arrayRef = CFArrayCreateMutable(kCFAllocatorDefault, 0, NULL);
NSMutableArray *array = (NSMutableArray *)arrayRef;

Megapoint *myPoint = malloc(sizeof(Megapoint);
myPoint->w = 42.0f;
// set ivars as desired..

// Casting to "id" to avoid compiler warning
[array addObject:(id)myPoint];

// Hurray!
Megapoint *mySamePoint = [array objectAtIndex:0];

Mảng có thể thay đổi (ít nhất là trong trường hợp này) được tạo ở trạng thái trống và do đó không cần con trỏ đến các giá trị được lưu trữ bên trong. Điều này khác với mảng không thay đổi trong đó nội dung được "đóng băng" khi tạo và do đó các giá trị phải được chuyển cho quy trình khởi tạo.
Sedate Alien

@Joe Blow: Đó là một điểm tuyệt vời mà bạn thực hiện về quản lý bộ nhớ. Bạn đúng là nhầm lẫn: mẫu mã mà tôi đã đăng ở trên sẽ gây ra lỗi bí ẩn, tùy thuộc vào thời điểm ngăn xếp của hàm bị ghi đè. Tôi bắt đầu giải thích chi tiết về cách giải pháp của tôi có thể được sử dụng, nhưng nhận ra rằng tôi đang thực hiện lại việc đếm tham chiếu của chính Objective-C. Tôi xin lỗi vì mã nhỏ gọn - vấn đề không phải là năng khiếu mà là sự lười biếng. Không có điểm nào trong việc viết mã mà người khác không thể đọc được. :)
Sedate Alien

Nếu bạn vui khi "rò rỉ" (vì muốn có một từ hay hơn) struct, bạn chắc chắn có thể phân bổ nó một lần và không miễn phí nó trong tương lai. Tôi đã bao gồm một ví dụ về điều này trong câu trả lời đã chỉnh sửa của mình. Ngoài ra, nó không phải là lỗi đánh máy myStructvì nó là một cấu trúc được phân bổ trên ngăn xếp, khác biệt với một con trỏ đến một cấu trúc được phân bổ trên heap.
Sedate Alien

4

Một phương pháp tương tự để thêm c struct là lưu trữ con trỏ và hủy tham chiếu con trỏ như vậy;

typedef struct BSTNode
{
    int data;
    struct BSTNode *leftNode;
    struct BSTNode *rightNode;
}BSTNode;

BSTNode *rootNode;

//declaring a NSMutableArray
@property(nonatomic)NSMutableArray *queues;

//storing the pointer in the array
[self.queues addObject:[NSValue value:&rootNode withObjCType:@encode(BSTNode*)]];

//getting the value
BSTNode *frontNode =[[self.queues objectAtIndex:0] pointerValue];

3

nếu bạn cảm thấy khó hiểu hoặc thực sự có nhiều lớp để tạo: đôi khi hữu ích khi tạo động một lớp objc (ref class_addIvar:). bằng cách này, bạn có thể tạo các lớp objc tùy ý từ các kiểu tùy ý. bạn có thể chỉ định từng trường hoặc chỉ chuyển thông tin của cấu trúc (nhưng thực tế đó là sao chép NSData). đôi khi hữu ích, nhưng có lẽ là một 'sự thật thú vị' đối với hầu hết người đọc.

Làm thế nào tôi sẽ áp dụng điều này ở đây?

bạn có thể gọi class_addIvar và thêm một biến cá thể Megapoint vào một lớp mới hoặc bạn có thể tổng hợp một biến thể objc của lớp Megapoint trong thời gian chạy (ví dụ: một biến thể hiện cho mỗi trường Megapoint).

trước đây tương đương với lớp objc đã biên dịch:

@interface MONMegapoint { Megapoint megapoint; } @end

cái sau tương đương với lớp objc đã biên dịch:

@interface MONMegapoint { float w,x,y,z; } @end

sau khi bạn đã thêm ivars, bạn có thể thêm / tổng hợp các phương thức.

để đọc các giá trị được lưu trữ ở đầu nhận, sử dụng các phương pháp tổng hợp của bạn object_getInstanceVariablehoặc valueForKey:(thường sẽ chuyển đổi các biến phiên bản vô hướng này thành các biểu diễn NSNumber hoặc NSValue).

btw: tất cả các câu trả lời bạn nhận được đều hữu ích, một số câu trả lời tốt hơn / tệ hơn / không hợp lệ tùy thuộc vào ngữ cảnh / kịch bản. các nhu cầu cụ thể liên quan đến bộ nhớ, tốc độ, dễ bảo trì, dễ chuyển hoặc lưu trữ, v.v. sẽ xác định cái nào là tốt nhất cho một trường hợp nhất định ... nhưng không có giải pháp 'hoàn hảo' nào là lý tưởng về mọi mặt. không có 'cách tốt nhất để đặt c-struct vào NSArray', chỉ là 'cách tốt nhất để đặt c-struct vào NSArray cho một kịch bản, trường hợp hoặc tập hợp yêu cầu cụ thể ' - mà bạn sẽ có để cụ thể hóa.

hơn nữa, NSArray là một giao diện mảng thường có thể tái sử dụng cho các kiểu con trỏ có kích thước (hoặc nhỏ hơn), nhưng có những vùng chứa khác phù hợp hơn với c-struct vì nhiều lý do (std :: vector là một lựa chọn điển hình cho c-struct).


nguồn gốc của mọi người cũng phát huy tác dụng ... cách bạn cần sử dụng cấu trúc đó thường sẽ loại bỏ một số khả năng. 4 float là khá tốt, nhưng bố cục cấu trúc thay đổi theo kiến ​​trúc / trình biên dịch quá nhiều để sử dụng biểu diễn bộ nhớ liền kề (ví dụ: NSData) và mong đợi nó hoạt động. serializer objc của người nghèo có thể có thời gian thực thi chậm nhất, nhưng nó tương thích nhất nếu bạn cần lưu / mở / truyền Megapoint trên bất kỳ thiết bị OS X hoặc iOS nào. Theo kinh nghiệm của tôi, cách phổ biến nhất là đặt cấu trúc trong một lớp objc. nếu bạn đang trải qua tất cả những điều này chỉ để (tiếp)
Justin

(tt) Nếu bạn đang trải qua tất cả những rắc rối này chỉ để tránh học kiểu tập hợp mới - thì bạn nên học kiểu tập hợp mới =) std::vector(ví dụ) phù hợp với việc nắm giữ các kiểu, cấu trúc và lớp C / C ++ hơn NSArray. Bằng cách sử dụng một NSArray của các loại NSValue, NSData hoặc NSDictionary, bạn đang mất rất nhiều tính an toàn về kiểu trong khi thêm hàng đống phân bổ và chi phí thời gian chạy. Nếu bạn muốn gắn bó với C, thì họ thường sử dụng malloc và / hoặc mảng trên ngăn xếp ... nhưng std::vectorẩn hầu hết các biến chứng khỏi bạn.
justin

trên thực tế, nếu bạn muốn thao tác / lặp mảng như bạn đã đề cập - stl (một phần của thư viện chuẩn c ++) là rất tốt cho điều đó. bạn có nhiều kiểu hơn để lựa chọn (ví dụ: nếu chèn / xóa quan trọng hơn thời gian truy cập đọc) và rất nhiều cách hiện có để thao tác các vùng chứa. Ngoài ra - nó không phải là bộ nhớ trống trong c ++ - các vùng chứa và các hàm mẫu được nhận biết kiểu và được kiểm tra khi biên dịch - an toàn hơn nhiều so với việc kéo một chuỗi byte tùy ý ra khỏi các biểu diễn NSData / NSValue. họ cũng có kiểm tra giới hạn và chủ yếu là quản lý tự động bộ nhớ. (tt)
justin

(tt) nếu bạn mong đợi rằng bạn sẽ có nhiều công việc ở cấp độ thấp như thế này, thì bạn chỉ nên học nó ngay bây giờ - nhưng sẽ mất thời gian để học. bằng cách gói tất cả điều này lại trong các biểu diễn objc, bạn đang mất rất nhiều hiệu suất và an toàn khi nhập, trong khi khiến bản thân phải viết nhiều mã soạn sẵn hơn để truy cập và diễn giải các vùng chứa và giá trị của chúng (Nếu 'Do đó, rất cụ thể ...' là chính xác những gì bạn muốn làm).
justin

nó chỉ là một công cụ khác theo ý của bạn. có thể có phức tạp khi tích hợp objc với c ++, c ++ với objc, c với c ++ hoặc bất kỳ kết hợp nào khác. thêm các tính năng ngôn ngữ và sử dụng nhiều ngôn ngữ trong mọi trường hợp đều phải trả một khoản chi phí nhỏ. nó đi theo mọi cách. ví dụ: thời gian xây dựng tăng lên khi biên dịch dưới dạng objc ++. đồng thời, những nguồn này cũng không dễ dàng được sử dụng lại trong các dự án khác. chắc chắn, bạn có thể thực hiện lại các tính năng ngôn ngữ ... nhưng đó không phải là giải pháp tốt nhất. tích hợp c ++ trong một dự án objc là tốt, nó cũng 'lộn xộn' như việc sử dụng các nguồn objc và c trong cùng một dự án. (tiếp
justin

3

tốt nhất là sử dụng trình tuần tự objc của người nghèo nếu bạn đang chia sẻ dữ liệu này trên nhiều abis / kiến ​​trúc:

Megapoint mpt = /* ... */;
NSMutableDictionary * d = [NSMutableDictionary new];
assert(d);

/* optional, for your runtime/deserialization sanity-checks */
[d setValue:@"Megapoint" forKey:@"Type-Identifier"];

[d setValue:[NSNumber numberWithFloat:mpt.w] forKey:@"w"];
[d setValue:[NSNumber numberWithFloat:mpt.x] forKey:@"x"];
[d setValue:[NSNumber numberWithFloat:mpt.y] forKey:@"y"];
[d setValue:[NSNumber numberWithFloat:mpt.z] forKey:@"z"];

NSArray *a = [NSArray arrayWithObject:d];
[d release], d = 0;
/* ... */

... đặc biệt nếu cấu trúc có thể thay đổi theo thời gian (hoặc theo nền tảng được nhắm mục tiêu). nó không nhanh như các tùy chọn khác, nhưng ít có khả năng bị hỏng trong một số điều kiện (mà bạn chưa xác định là quan trọng hay không).

nếu biểu diễn tuần tự hóa không thoát khỏi quy trình, thì kích thước / thứ tự / căn chỉnh của các cấu trúc tùy ý sẽ không thay đổi và có các tùy chọn đơn giản hơn và nhanh hơn.

trong cả hai trường hợp, bạn đã thêm một đối tượng được đếm lại (so với NSData, NSValue), vì vậy ... tạo một lớp objc chứa Megapoint là câu trả lời đúng trong nhiều trường hợp.


@Joe Blow thứ gì đó thực hiện tuần tự hóa. để tham khảo: en.wikipedia.org/wiki/Serialization , parashift.com/c++-faq-lite/serialization.html , cũng như "Hướng dẫn lập trình lưu trữ và tuần tự hóa" của Apple.
justin

giả sử tệp xml đại diện chính xác cho một cái gì đó, thì có - đó là một dạng phổ biến của tuần tự hóa có thể đọc được.
justin

0

Tôi khuyên bạn nên sử dụng std :: vector hoặc std :: list cho các loại C / C ++, vì lúc đầu nó chỉ nhanh hơn NSArray, và thứ hai nếu không đủ tốc độ cho bạn - bạn luôn có thể tạo trình phân bổ cho các vùng chứa STL và làm cho chúng nhanh hơn nữa. Tất cả các công cụ Trò chơi, Vật lý và Âm thanh trên điện thoại di động hiện đại đều sử dụng vùng chứa STL để lưu trữ dữ liệu nội bộ. Chỉ vì chúng thực sự nhanh.

Nếu nó không phải dành cho bạn - có những câu trả lời tốt từ những người về NSValue - tôi nghĩ nó dễ chấp nhận nhất.


STL là một thư viện được đưa một phần vào Thư viện Chuẩn C ++. en.wikipedia.org/wiki/Standard_Template_Library cplusplus.com/reference/stl/vector
Evgen Bodunov

Đó là một tuyên bố thú vị. Bạn có liên kết đến một bài báo về lợi thế tốc độ của các vùng chứa STL so với các lớp vùng chứa Ca cao không?
Sedate Alien

đây là một đọc thú vị về NSCFArray vs std :: vector: ridiculousfish.com/blog/archives/2005/12/23/array trong ví dụ trong bài viết của bạn, sự mất mát lớn nhất là (thường) tạo ra một đại diện đối tượng objc mỗi yếu tố (ví dụ , NSValue, NSData, hoặc loại Objc có chứa Megapoint yêu cầu phân bổ và chèn vào hệ thống đếm lại). bạn thực sự có thể tránh điều đó bằng cách sử dụng cách tiếp cận của Sedate Alien để lưu trữ Megapoint trong một CFArray đặc biệt sử dụng một kho dự phòng riêng gồm các Megapoint được phân bổ liền kề (mặc dù không có ví dụ nào minh họa cách tiếp cận đó). (tt)
justin,

nhưng sau đó sử dụng NSCFArray so với vectơ (hoặc loại stl khác) sẽ phát sinh thêm chi phí cho điều phối động, các lệnh gọi hàm bổ sung không được nội tuyến, rất nhiều kiểu an toàn và nhiều cơ hội để trình tối ưu hóa bắt đầu ... điều đó bài viết chỉ tập trung vào chèn, đọc, đi bộ, xóa. rất có thể, bạn sẽ không nhanh hơn một mảng c được căn chỉnh 16 byte Megapoint pt[8];- đây là một tùy chọn trong c ++ và các vùng chứa c ++ chuyên biệt (ví dụ std::array:) - cũng lưu ý rằng ví dụ này không thêm căn chỉnh đặc biệt (16 byte đã được chọn bởi vì nó là kích thước của Megapoint). (tt)
justin

std::vectorsẽ thêm một lượng nhỏ chi phí cho điều này và một phân bổ (nếu bạn biết kích thước bạn sẽ cần) ... nhưng điều này gần với kim loại hơn 99,9% trường hợp cần. thông thường, bạn chỉ sử dụng một vectơ trừ khi kích thước được cố định hoặc có mức tối đa hợp lý.
justin

0

Thay vì cố gắng đặt cấu trúc c trong một NSArray, bạn có thể đặt chúng trong NSData hoặc NSMutableData dưới dạng mảng cấu trúc ac. Để truy cập chúng, bạn sẽ làm

const struct MyStruct    * theStruct = (const struct MyStruct*)[myData bytes];
int                      value = theStruct[2].integerNumber;

hoặc để đặt sau đó

struct MyStruct    * theStruct = (struct MyStruct*)[myData mutableBytes];
theStruct[2].integerNumber = 10;


0

Đối với cấu trúc của bạn, bạn có thể thêm một thuộc tính objc_boxable và sử dụng @()cú pháp để đưa cấu trúc của bạn vào phiên bản NSValue mà không cần gọi valueWithBytes:objCType::

typedef struct __attribute__((objc_boxable)) _Megapoint {
    float   w,x,y,z;
} Megapoint;

NSMutableArray<NSValue*>* points = [[NSMutableArray alloc] initWithCapacity:10];
for (int i = 0; i < 10; i+= 1) {
    Megapoint mp1 = {i + 1.0, i + 2.0, i + 3.0, i + 4.0};
    [points addObject:@(mp1)];//@(mp1) creates NSValue*
}

Megapoint unarchivedPoint;
[[points lastObject] getValue:&unarchivedPoint];
//or
// [[points lastObject] getValue:&unarchivedPoint size:sizeof(Megapoint)];

-2

Đối tượng obj C chỉ là một cấu trúc C với một số phần tử được thêm vào. Vì vậy, chỉ cần tạo một lớp tùy chỉnh và bạn sẽ có kiểu cấu trúc C mà NSArray yêu cầu. Bất kỳ cấu trúc C nào không có thêm mấu chốt mà một NSObject đưa vào trong cấu trúc C của nó sẽ không thể tiêu hóa được đối với NSArray.

Việc sử dụng NSData làm trình bao bọc có thể chỉ lưu trữ bản sao của cấu trúc chứ không phải cấu trúc gốc, nếu điều đó tạo ra sự khác biệt cho bạn.


-3

Bạn có thể sử dụng các lớp NSObject khác với C-Structures để lưu trữ thông tin. Và bạn có thể dễ dàng lưu trữ NSObject đó vào NSArray.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.