編輯:關於Android編程
前言
Runtime是一套比較底層的純C語言API,包含了很多底層的C語言API。在我們平時編寫的OC代碼中,程序運行時,其實最終都是轉成了Runtime的C語言代碼。Runtime是開源的,你可以去 這裡 下載Runtime的源碼。本文主要分為兩個章節,第一部分主要是理論和原理,第二部分主要是使用實例。文章的最後會附上本文的demo下載鏈接。
描述Objective-C對象所用的數據結構定義都在Runtime的頭文件裡,下面我們逐一分析。
運行期系統如何知道某個對象的類型呢?對象類型並不是在編譯期就知道了,而是要在運行期查找。Objective-C有個特殊的類型id,它可以表示Objective-C的任意對象類型,id類型定義在Runtime的頭文件中:
struct objc_object { Class isa; } *id;由此可見,每個對象結構體的首個成員是Class類的變量。該變量定義了對象所屬的類,通常稱為isa指針。
2.Class
Class對象也定義在Runtime的頭文件中:
typedef struct objc_class *Class; struct objc_class { Class isa OBJC_ISA_AVAILABILITY; #if !__OBJC2__ Class super_class OBJC2_UNAVAILABLE; const char *name OBJC2_UNAVAILABLE; long version OBJC2_UNAVAILABLE; long info OBJC2_UNAVAILABLE; long instance_size OBJC2_UNAVAILABLE; struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; struct objc_method_list **methodLists OBJC2_UNAVAILABLE; struct objc_cache *cache OBJC2_UNAVAILABLE; struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; #endif }下面說下Class的結構體中的幾個主要變量: 1.isa:結構體的首個變量也是isa指針,這說明Class本身也是Objective-C中的對象。2.super_class:結構體裡還有個變量是super_class,它定義了本類的超類。類對象所屬類型(isa指針所指向的類型)是另外一個類,叫做“元類”。3.ivars:成員變量列表,類的成員變量都在ivars裡面。4.methodLists:方法列表,類的實例方法都在methodLists裡,類方法在元類的methodLists裡面。methodLists是一個指針的指針,通過修改該指針指向指針的值,就可以動態的為某一個類添加成員方法。這也就是Category實現的原理,同時也說明了Category只可以為對象添加成員方法,不能添加成員變量。5.cache:方法緩存列表,objc_msgSend(下文詳解)每調用一次方法後,就會把該方法緩存到cache列表中,下次調用的時候,會優先從cache列表中尋找,如果cache沒有,才從methodLists中查找方法。提高效率。 看圖說話:
上圖中:superclass指針代表繼承關系,isa指針代表實例所屬的類。類也是一個對象,它是另外一個類的實例,這個就是“元類”,元類裡面保存了類方法的列表,類裡面保存了實例方法的列表。實例對象的isa指向類,類對象的isa指向元類,元類對象的isa指針指向一個“根元類”(root metaclass)。所有子類的元類都繼承父類的元類,換而言之,類對象和元類對象有著同樣的繼承關系。
PS: 1.Class是一個指向objc_class結構體的指針,而id是一個指向objc_object結構體的指針,其中的isa是一個指向objc_class結構體的指針。其中的id就是我們所說的對象,Class就是我們所說的類。2.isa指針不總是指向實例對象所屬的類,不能依靠它來確定類型,而是應該用isKindOfClass:方法來確定實例對象的類。因為KVO的實現機制就是將被觀察對象的isa指針指向一個中間類而不是真實的類。SEL是選擇子的類型,選擇子指的就是方法的名字。在Runtime的頭文件中的定義如下:
typedef struct objc_selector *SEL;
它就是個映射到方法的C字符串,SEL類型代表著方法的簽名,在類對象的方法列表中存儲著該簽名與方法代碼的對應關系,每個方法都有一個與之對應的SEL類型的對象,根據一個SEL對象就可以找到方法的地址,進而調用方法。
Method代表類中的某個方法的類型,在Runtime的頭文件中的定義如下:
[代碼]objc代碼:typedef struct objc_method *Method;objc_method的結構體定義如下:
struct objc_method { SEL method_name OBJC2_UNAVAILABLE; char *method_types OBJC2_UNAVAILABLE; IMP method_imp OBJC2_UNAVAILABLE; }1.method_name:方法名。2.method_types:方法類型,主要存儲著方法的參數類型和返回值類型。3.IMP:方法的實現,函數指針。(下文詳解)
class_copyMethodList(Class cls, unsigned int *outCount)可以使用這個方法獲取某個類的成員方法列表。
Ivar代表類中實例變量的類型,在Runtime的頭文件中的定義如下:
typedef struct objc_ivar *Ivar;objc_ivar的定義如下:
struct objc_ivar { char *ivar_name OBJC2_UNAVAILABLE; char *ivar_type OBJC2_UNAVAILABLE; int ivar_offset OBJC2_UNAVAILABLE; #ifdef __LP64__ int space OBJC2_UNAVAILABLE; #endif }class_copyIvarList(Class cls, unsigned int *outCount) 可以使用這個方法獲取某個類的成員變量列表。
objc_property_t是屬性,在Runtime的頭文件中的的定義如下:
typedef struct objc_property *objc_property_t;class_copyPropertyList(Class cls, unsigned int *outCount) 可以使用這個方法獲取某個類的屬性列表。
IMP在Runtime的頭文件中的的定義如下:
typedef id (*IMP)(id, SEL, ...);IMP是一個函數指針,它是由編譯器生成的。當你發起一個消息後,這個函數指針決定了最終執行哪段代碼。
Cache在Runtime的頭文件中的的定義如下:
typedef struct objc_cache *Cacheobjc_cache的定義如下:
struct objc_cache { unsigned int mask OBJC2_UNAVAILABLE; unsigned int occupied OBJC2_UNAVAILABLE; Method buckets[1] OBJC2_UNAVAILABLE; };每調用一次方法後,不會直接在isa指向的類的方法列表(methodLists)中遍歷查找能夠響應消息的方法,因為這樣效率太低。它會把該方法緩存到cache列表中,下次的時候,就直接優先從cache列表中尋找,如果cache沒有,才從isa指向的類的方法列表(methodLists)中查找方法。提高效率。
在Objective-C中,調用方法是經常使用的。用Objective-C的術語來說,這叫做“傳遞消息”(pass a message)。消息有“名稱”(name)或者“選擇子”(selector),也可以接受參數,而且可能還有返回值。如果向某個對象傳遞消息,在底層,所有的方法都是普通的C語言函數,然而對象收到消息之後,究竟該調用哪個方法則完全取決於運行期決定,甚至可能在運行期改變,這些特性使得Objective-C變成一門真正的動態語言。給對象發送消息可以這樣來寫:
id returnValue = [someObject message:parm];someObject叫做“接收者”(receiver),message是“選擇子”(selector),選擇子和參數結合起來就叫做“消息”(message)。編譯器看到此消息後,將其轉換成C語言函數調用,所調用的函數乃是消息傳遞機制中的核心函數,叫做objc_msgSend,其原型如下:
id objc_msgSend (id self, SEL _cmd, ...);後面的...表示這是個“參數個數可變的函數”,能接受兩個或兩個以上的參數。第一個參數是接收者(receiver),第二個參數是選擇子(selector),後續參數就是消息中傳遞的那些參數(parm),其順序不變。
編譯器會把上面的那個消息轉換成:
id returnValue objc_mgSend(someObject, @selector(message:), parm);
傳遞消息的幾種函數:
objc_msgSend:普通的消息都會通過該函數發送。
objc_msgSend_stret:消息中有結構體作為返回值時,通過此函數發送和接收返回值。
objc_msgSend_fpret:消息中返回的是浮點數,可交由此函數處理。
objc_msgSendSuper:和objc_msgSend類似,這裡把消息發送給超類。
objc_msgSendSuper_stret:和objc_msgSend_stret類似,這裡把消息發送給超類。
objc_msgSendSuper_fpret:和objc_msgSend_fpret類似,這裡把消息發送給超類。
編譯器會根據情況選擇一個函數來執行。
objc_msgSend發送消息的原理:
第一步:檢測這個selector是不是要被忽略的。第二步:檢測這個target對象是不是nil對象。(nil對象執行任何一個方法都不會Crash,因為會被忽略掉)第三步:首先會根據target對象的isa指針獲取它所對應的類(class)。第四步:優先在類(class)的cache裡面查找與選擇子(selector)名稱相符,如果找不到,再到methodLists查找。第五步:如果沒有在類(class)找到,再到父類(super_class)查找,再到元類(metaclass),直至根metaclass。第六步:一旦找到與選擇子(selector)名稱相符的方法,就跳至其實現代碼。如果沒有找到,就會執行消息轉發(message forwarding)。(下節會詳解)上面說了消息的傳遞機制,下面就來說一下,如果對象在收到無法解讀的消息之後會發生上面情況。當一個對象在收到無法解讀的消息之後,它會將消息實施轉發。轉發的主要步驟如下:
消息轉發步驟 第一步:對象在收到無法解讀的消息後,首先調用resolveInstanceMethod:方法決定是否動態添加方法。如果返回YES,則調用class_addMethod動態添加方法,消息得到處理,結束;如果返回NO,則進入下一步;第二步:當前接收者還有第二次機會處理未知的選擇子,在這一步中,運行期系統會問:能不能把這條消息轉給其他接收者來處理。會進入forwardingTargetForSelector:方法,用於指定備選對象響應這個selector,不能指定為self。如果返回某個對象則會調用對象的方法,結束。如果返回nil,則進入下一步;第三步:這步我們要通過methodSignatureForSelector:方法簽名,如果返回nil,則消息無法處理。如果返回methodSignature,則進入下一步;第四步:這步調用forwardInvocation:方法,我們可以通過anInvocation對象做很多處理,比如修改實現方法,修改響應對象等,如果方法調用成功,則結束。如果失敗,則進入doesNotRecognizeSelector方法,拋出異常,此異常表示選擇子最終未能得到處理。/** 消息轉發第一步:對象在收到無法解讀的消息後,首先調用此方法,可用於動態添加方法,方法決定是否動態添加方法。如果返回YES,則調用class_addMethod動態添加方法,消息得到處理,結束;如果返回NO,則進入下一步; */ + (BOOL)resolveInstanceMethod:(SEL)sel { return NO; } /** 當前接收者還有第二次機會處理未知的選擇子,在這一步中,運行期系統會問:能不能把這條消息轉給其他接收者來處理。會進入此方法,用於指定備選對象響應這個selector,不能指定為self。如果返回某個對象則會調用對象的方法,結束。如果返回nil,則進入下一步; */ - (id)forwardingTargetForSelector:(SEL)aSelector { return nil; } /** 這步我們要通過該方法簽名,如果返回nil,則消息無法處理。如果返回methodSignature,則進入下一步。 */ - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { if ([NSStringFromSelector(aSelector) isEqualToString:@"study"]) { return [NSMethodSignature signatureWithObjCTypes:"v@:"]; } return [super methodSignatureForSelector:aSelector]; } /** 這步調用該方法,我們可以通過anInvocation對象做很多處理,比如修改實現方法,修改響應對象等,如果方法調用成功,則結束。如果失敗,則進入doesNotRecognizeSelector方法。 */ - (void)forwardInvocation:(NSInvocation *)anInvocation { [anInvocation setSelector:@selector(play)]; [anInvocation invokeWithTarget:self]; } /** 拋出異常,此異常表示選擇子最終未能得到處理。 */ - (void)doesNotRecognizeSelector:(SEL)aSelector { NSLog(@"無法處理消息:%@", NSStringFromSelector(aSelector)); }
接收者在每一步中均有機會處理消息,步驟越靠後,處理消息的代價越大。最好在第一步就能處理完,這樣系統就可以把此方法緩存起來了。
有時我們需要在對象中存放相關信息,Objective-C中有一種強大的特性可以解決此類問題,就是“關聯對象”。 可以給某個對象關聯許多其他對象,這些對象通過“鍵”來區分。存儲對象值時,可以指明“存儲策略”,用以維護相應地“內存管理語義”。存儲策略由名為“objc_AssociationPolicy” 的枚舉所定義。下表中列出了該枚舉值得取值,同時還列出了與之等下的@property屬性:假如關聯對象成為了屬性,那麼他就會具備對應的語義。
下列方法可以管理關聯對象:
// 以給定的鍵和策略為某對象設置關聯對象值。 objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) // 根據給定的鍵從某對象中獲取對應的對象值。 id objc_getAssociatedObject(id object, void *key) // 移除指定對象的全部關聯對象。 void objc_removeAssociatedObjects(id object)
在Objective-C中,對象收到消息之後,究竟會調用哪種方法需要在運行期才能解析出來。查找消息的唯一依據是選擇子(selector),選擇子(selector)與相應的方法(IMP)對應,利用Objective-C的動態特性,可以實現在運行時偷換選擇子(selector)對應的方法實現,這就是方法交換(method swizzling)。
類的方法列表會把每個選擇子都映射到相關的IMP之上
我們可以新增選擇子,也可以改變某個選擇子所對應的方法實現,還可以交換兩個選擇子所映射到的指針。
class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)2.method_exchangeImplementations交換兩個方法的實現。
method_exchangeImplementations(Method m1, Method m2)3.method_setImplementation設置一個方法的實現
method_setImplementation(Method m, IMP imp)
先說下這三個方法的區別:
class_replaceMethod:當類中沒有想替換的原方法時,該方法調用class_addMethod來為該類增加一個新方法,也正因如此,class_replaceMethod在調用時需要傳入types參數,而其余兩個卻不需要。method_exchangeImplementations:內部實現就是調用了兩次method_setImplementation方法。再來看看他們的使用場景:
+ (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ SEL originalSelector = @selector(willMoveToSuperview:); SEL swizzledSelector = @selector(myWillMoveToSuperview:); Method originalMethod = class_getInstanceMethod(self, originalSelector); Method swizzledMethod = class_getInstanceMethod(self, swizzledSelector); BOOL didAddMethod = class_addMethod(self, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (didAddMethod) { class_replaceMethod(self, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { method_exchangeImplementations(originalMethod, swizzledMethod); } }); } - (void)myWillMoveToSuperview:(UIView *)newSuperview { NSLog(@"WillMoveToSuperview: %@", self); [self myWillMoveToSuperview:newSuperview]; }
使用實例1.class_replaceMethod,當需要替換的方法有可能不存在時,可以考慮使用該方法。2.method_exchangeImplementations,當需要交換兩個方法的時使用。3.method_setImplementation是最簡單的用法,當僅僅需要為一個方法設置其實現方式時實現。
前面講的全部是理論知識,比較枯燥,下面說一些實際的栗子。
一、動態的創建一個類// 創建一個名為People的類,它是NSObject的子類 Class People = objc_allocateClassPair([NSObject class], "People", 0); // 為該類添加一個eat的方法 class_addMethod(People, NSSelectorFromString(@"eat"), (IMP) eatFun, "v@:"); // 注冊該類 objc_registerClassPair(People); // 創建一個People的實例對象p id p = [[People alloc] init]; // 調用eat方法 [p performSelector:@selector(eat)];二、動態的給某個類添加方法
+ (BOOL)resolveInstanceMethod:(SEL)sel { if ([NSStringFromSelector(sel) isEqualToString:@"doSomething"]) { class_addMethod(self, sel, (IMP) doSomething, "v@:@"); } return YES; }動態的給某個類添加方法,class_addMethod的參數:self:給哪個類添加方法sel:添加方法的方法編號(選擇子)IMP:添加方法的函數實現(函數地址)types 函數的類型,(返回值+參數類型) v:void @:對象->self :表示SEL->_cmd
類別不可以添加屬性,我們可以在類別中設置關聯,舉個栗子:
Person+Category.h 文件
#import "Person.h" @interface Person (Category) @property (nonatomic, copy) NSString *name; @end
Person+Category.m 文件
#import "Person+Category.h" #import @implementation Person (Category) static char *key; - (void)setName:(NSString *)name { objc_setAssociatedObject(self, key, name, OBJC_ASSOCIATION_COPY_NONATOMIC); } - (NSString *)name { return objc_getAssociatedObject(self, key); } @end當然你也可以這麼寫
Person+Category.m 文件
#import "Person+Category.h" #import @implementation Person (Category) - (void)setName:(NSString *)name { objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC); } - (NSString *)name { return objc_getAssociatedObject(self, _cmd); } @endobjc_setAssociatedObject和objc_getAssociatedObject傳入的參數key:要求是唯一並且是常量,可以使用static char,然而一個更簡單方便的方法就是:使用選擇子。由於選擇子是唯一並且是常量,你可以使用選擇子作為關聯的key。(PS:_cmd表示當前調用的方法,它就是一個方法選擇器SEL,類似self表示當前對象)
四、方法交換 1.如果我現在想檢查一下項目中有沒有內存循環,怎麼辦?是不是要重寫dealloc函數,看下dealloc有沒有執行,項目小的時候,一個一個controller的寫,還不麻煩,如果項目大,要是一個一個的寫,估計你會瘋掉的。這時候方法交換就派上用場了,你就可以嘗試用自己的方法交換系統的dealloc方法,幾句代碼就搞定了。
#import "UIViewController+Dealloc.h" #import @implementation UIViewController (Dealloc) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ Method method1 = class_getInstanceMethod(self, NSSelectorFromString(@"dealloc")); Method method2 = class_getInstanceMethod(self, @selector(my_dealloc)); method_exchangeImplementations(method1, method2); }); } - (void)my_dealloc { NSLog(@"%@銷毀了", self); [self my_dealloc]; } @end2.數組越界,向數組中添加一個nil對象等等,都會造成閃退,我們可以用自己的方法交換數組相對應的方法。下面是一個交換數組addObject:方法的栗子:
#import "NSMutableArray+Category.h" #import @implementation NSMutableArray (Category) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ SEL originalSelector = @selector(addObject:); SEL swizzledSelector = @selector(lj_AddObject:); // NSMutableArray是類簇,真正的類名是__NSArrayM Method originalMethod = class_getInstanceMethod(objc_getClass("__NSArrayM"), originalSelector); Method swizzledMethod = class_getInstanceMethod(objc_getClass("__NSArrayM"), swizzledSelector); BOOL didAddMethod = class_addMethod(self, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (didAddMethod) { class_replaceMethod(self, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { method_exchangeImplementations(originalMethod, swizzledMethod); } }); } - (void)lj_AddObject:(id)object { if (object != nil) { [self lj_AddObject:object]; } } @end
五、歸檔PS:我不太建議大家平時開發的時候使用這類數組安全操作的做法,不利於代碼的調試,如果真的加入了nil對象,你可能就不會那麼容易找出問題在哪,還是在項目發布的時候使用比較合適。
大家都知道在歸檔的時候,需要先將屬性一個一個的歸檔,然後再將屬性一個一個的解檔,3-5個屬性還好,假如100個怎麼辦,那不得寫累死。有了Runtime,就不用擔心這個了,下面就是如何利用Runtime實現自動歸檔和解檔。NSObject+Archive.h文件:
#import @interface NSObject (Archive) /** * 歸檔 */ - (void)encode:(NSCoder *)aCoder; /** * 解檔 */ - (void)decode:(NSCoder *)aDecoder; /** * 這個數組中的成員變量名將會被忽略:不進行歸檔 */ @property (nonatomic, strong) NSArray *ignoredIvarNames; @end
NSObject+Archive.m文件:
#import "NSObject+Archive.h" #import @implementation NSObject (Archive) - (void)encode:(NSCoder *)aCoder { unsigned int outCount = 0; Ivar *ivars = class_copyIvarList([self class], &outCount); for (unsigned int i = 0; i < outCount; i++) { Ivar ivar = ivars[i]; NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)]; if ([self.ignoredIvarNames containsObject:key]) { continue; } id value = [self valueForKey:key]; [aCoder encodeObject:value forKey:key]; } free(ivars); } - (void)decode:(NSCoder *)aDecoder { unsigned int outCount = 0; Ivar *ivars = class_copyIvarList([self class], &outCount); for (unsigned int i = 0; i < outCount; i++) { Ivar ivar = ivars[i]; NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)]; if ([self.ignoredIvarNames containsObject:key]) { continue; } id value = [aDecoder decodeObjectForKey:key]; [self setValue:value forKey:key]; } free(ivars); } - (void)setIgnoredIvarNames:(NSArray *)ignoredIvarNames { objc_setAssociatedObject(self, @selector(ignoredIvarNames), ignoredIvarNames, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (NSArray *)ignoredIvarNames { return objc_getAssociatedObject(self, _cmd); } @end然後再去需要歸檔的類實現文件裡面寫上這幾行代碼:
@implementation Person - (void)encodeWithCoder:(NSCoder *)aCoder { [self encode:aCoder]; } - (instancetype)initWithCoder:(NSCoder *)aDecoder { if (self = [super init]) { [self decode:aDecoder]; } return self; } @end這幾行代碼都是固定寫法,你也可以把它們定義成宏,這樣就可以實現一行代碼就歸檔了,思路源自MJExtension!
利用Runtime,遍歷模型中所有成員變量,根據模型的屬性名,去字典中查找key,取出對應的value,給模型的屬性賦值,實現的思路主要借鑒MJExtension。NSObject+Property.h文件:
#import @protocol KeyValue @optional /** * 數組中需要轉換的模型類 * * @return 字典中的key是數組屬性名,value是數組中存放模型的Class(Class類型或者NSString類型) */ + (NSDictionary *)objectClassInArray; /** * 將屬性名換為其他key去字典中取值 * * @return 字典中的key是屬性名,value是從字典中取值用的key */ + (NSDictionary *)replacedKeyFromPropertyName; @end @interface NSObject (Property) + (instancetype)objectWithDictionary:(NSDictionary *)dictionary; @endNSObject+Property.m文件:
#import "NSObject+Property.h" #import @implementation NSObject (Property) + (instancetype)objectWithDictionary:(NSDictionary *)dictionary { id obj = [[self alloc] init]; // 獲取所有的成員變量 unsigned int count; Ivar *ivars = class_copyIvarList(self, &count); for (unsigned int i = 0; i < count; i++) { Ivar ivar = ivars[i]; // 取出的成員變量,去掉下劃線 NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)]; NSString *key = [ivarName substringFromIndex:1]; id value = dictionary[key]; // 當這個值為空時,判斷一下是否執行了replacedKeyFromPropertyName協議,如果執行了替換原來的key查值 if (!value) { if ([self respondsToSelector:@selector(replacedKeyFromPropertyName)]) { NSString *replaceKey = [self replacedKeyFromPropertyName][key]; value = dictionary[replaceKey]; } } // 字典嵌套字典 if ([value isKindOfClass:[NSDictionary class]]) { NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)]; NSRange range = [type rangeOfString:@"\""]; type = [type substringFromIndex:range.location + range.length]; range = [type rangeOfString:@"\""]; type = [type substringToIndex:range.location]; Class modelClass = NSClassFromString(type); if (modelClass) { value = [modelClass objectWithDictionary:value]; } } // 字典嵌套數組 if ([value isKindOfClass:[NSArray class]]) { if ([self respondsToSelector:@selector(objectClassInArray)]) { NSMutableArray *models = [NSMutableArray array]; NSString *type = [self objectClassInArray][key]; Class classModel = NSClassFromString(type); for (NSDictionary *dict in value) { id model = [classModel objectWithDictionary:dict]; [models addObject:model]; } value = models; } } if (value) { [obj setValue:value forKey:key]; } } // 釋放ivars free(ivars); return obj; } @end
log類的繼承結構Logpublic final class Log extends Object java.lang.Object ? android.uti
記得之前京東首頁有一個效果,有一個畫軸,然後可以滾動畫軸,去打開畫(不知道怎麼去形容這個效果,就叫做畫軸效果吧- -!),然後去做相關操作,剛開始看到這個效果,想法是動態
Android中有個我們熟悉又陌生的對象Context(上下文),當我們啟動Activity的時候需要上下文,當我們使用dialog的時候我們需要上下文,但是上下文對象到
設計思路:1.畫柱狀圖2.畫豎線3.畫頂部橫線4.畫文字1.畫柱狀圖畫柱狀圖的方法很簡單,就是使用canvas.drawRect(float left, float to