Author Archives: airfly

Is that message authentication code (MAC) causes The resource cannot be found?

Is that message authentication code (MAC) causes The resource cannot be found?

This two days I deploy a .Net 4.0 website onto a web farm (multi-server environment) hosting in USA. I was got error after HttpModule, see below:

Server Error in ‘/’ Application.


The resource cannot be found.

Description: HTTP 404. The resource you are looking for (or one of its dependencies) could have been removed, had its name changed, or is temporarily unavailable.  Please review the following URL and make sure that it is spelled correctly. 

Requested URL: /aspx/main/index.aspx

I tried everything, but never got the problem solved. I was tried to give up. Then I went to try the login function. Then I got the MAC failed.

After I generated a machineKey for the website I found that ‘The resource cannot be found.‘ just went away.

So I just think is that MAC problem causes this ‘The resource cannot be found.‘ problem?

It’s very easy to generate a machineKey for your own. Just see the steps below:

  1. Open IIS Managment & select a website.
  2. You’ll see a Machine Key icon & click on it.
  3. Click on ‘Generate Keys’ you’ll see the keys.
  4. Copy the Keys into your Web.config files, it looks like:
    <system.web>
        <machineKey validationKey=”96…” decryptionKey=”99…” validation=”SHA1″ />

 

过去的一年以来,我做了进口葡萄酒行业

我自工作以来,一直从事IT及编码行业工作,做得相当快乐和辛苦,也非常有成就感,但是很多事情光有成就感已经远远不足以生存在我朝,人生需要有很多东东,很多体验,于是我过去的一年开始,做起了进口葡萄酒行业,直接销售了来自法国、美国、德国、西班牙、智利、阿根廷、澳洲等世界葡萄酒。

欢迎大家支持我的新事业,在这儿,我保证我销售的进口葡萄酒是一批高品质,而价格非常公道的原瓶进口葡萄酒。如果您能够买到品质比我的好,价格比我的低的同款行货(非假货,非仿货,非次货。。。),请告诉我它们的出处,我将非常乐意为您补单,或是给您意外惊喜。

欢迎查看我所销售的进口葡萄酒:请猛击这里。

同时也非常欢迎您直接跟我联系,我的手机:13132914138,我的微信号和QQ号:1272000

同时您也可以直接打开我的微店:请猛击这里。

HTML5演示Demo

Just clone some html5 js code from lightapp & make a demo. It’s used to build simple app for book, manual and posters. It supports audio, video & baidu map, etc.
刚刚从lightapp上克隆了一些javascript代码,用于手机端展示html5动态页面的,可以非常方便的制作一些简单的宣传类应用,它支持声音,视频,百度地图展示等功能。

It’s here. 猛击这里展示。 直接输入:http://airflypan.com/ttpogx/ 也行。

iOS日期处理

Dates

        NSDate类提供了创建date,比较date以及计算两个date之间间隔的功能。Date对象是不可改变的。

        如果你要创建date对象并表示当前日期,你可以alloc一个NSDate对象并调用init初始化:

  1. NSDate *now = [[NSDate alloc] init];  

        或者使用NSDate的date类方法来创建一个日期对象。如果你需要与当前日期不同的日期,你可以使用NSDate的initWithTimeInterval…或dateWithTimeInterval…方法,你也可以使用更复杂的calendar或date components对象。

        创建一定时间间隔的NSDate对象:

  1. NSTimeInterval secondsPerDay = 24 * 60 * 60;  
  2.   
  3. NSDate *tomorrow = [[NSDate alloc] initWithTimeIntervalSinceNow:secondsPerDay];  
  4.   
  5. NSDate *yesterday = [[NSDate alloc] initWithTimeIntervalSinceNow:-secondsPerDay];  
  6.   
  7. [tomorrow release];  
  8. [yesterday release];  


        使用增加时间间隔的方式来生成NSDate对象:

  1. NSTimeInterval secondsPerDay = 24 * 60 * 60;  
  2.   
  3. NSDate *today = [[NSDate alloc] init];  
  4. NSDate *tomorrow, *yesterday;  
  5.   
  6. tomorrow = [today dateByAddingTimeInterval: secondsPerDay];  
  7. yesterday = [today dateByAddingTimeInterval: -secondsPerDay];  
  8.   
  9. [today release];  


        如果要对NSDate对象进行比较,可以使用isEqualToDate:, compare:, laterDate:和 earlierDate:方法。这些方法都进行精确比较,也就是说这些方法会一直精确比较到NSDate对象中秒一级。例如,你可能比较两个日期,如果他们之间的间隔在一分钟之内则认为这两个日期是相等的。在这种情况下使用,timeIntervalSinceDate:方法来对两个日期进行比较。下面的代码进行了示例:

  1. if (fabs([date2 timeIntervalSinceDate:date1]) < 60) …  

 

NSCalendar & NSDateComponents

        日历对象封装了对系统日期的计算,包括这一年开始,总天数以及划分。你将使用日历对象对绝对日期与date components(包括年,月,日,时,分,秒)进行转换。

        NSCalendar定义了不同的日历,包括佛教历,格里高利历等(这些都与系统提供的本地化设置相关)。NSCalendar与NSDateComponents对象紧密相关。

        你可以通过NSCalendar对象的currentCalendar方法来获得当前系统用户设置的日历。

  1. NSCalendar *currentCalendar = [NSCalendar currentCalendar];  
  2.   
  3. NSCalendar *japaneseCalendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSJapaneseCalendar];  
  4.   
  5. NSCalendar *usersCalendar = [[NSLocale currentLocale] objectForKey:NSLocaleCalendar];  

        usersCalendar和currentCalendar对象是相等的,尽管他们是不同的对象。

        你可以使用NSDateComponents对象来表示一个日期对象的组件——例如年,月,日和小时。如果要使一个NSDateComponents对象有意义,你必须将其与一个日历对象相关联。下面的代码示例了如何创建一个NSDateComponents对象:

  1. NSDateComponents *components = [[NSDateComponents alloc] init];  
  2.   
  3. [components setDay:6];  
  4. [components setMonth:5];  
  5. [components setYear:2004];  
  6.   
  7. NSInteger weekday = [components weekday]; // Undefined (== NSUndefinedDateComponent)  


        要将一个日期对象解析到相应的date components,你可以使用NSCalendar的components:fromDate:方法。此外日期本身,你需要指定NSDateComponents对象返回组件。

  1. NSDate *today = [NSDate date];  
  2.   
  3. NSCalendar *gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];  
  4.   
  5. NSDateComponents *weekdayComponents = [gregorian components:(NSDayCalendarUnit | NSWeekdayCalendarUnit) fromDate:today];  
  6.   
  7. NSInteger day = [weekdayComponents day];  
  8. NSInteger weekday = [weekdayComponents weekday];  
  9.   
  10. 同样你也可以从NSDateComponents对象来创建NSDate对象:  
  11. NSDateComponents *components = [[NSDateComponents alloc] init];  
  12.   
  13. [components setWeekday:2]; // Monday  
  14. [components setWeekdayOrdinal:1]; // The first Monday in the month  
  15. [components setMonth:5]; // May  
  16. [components setYear:2008];  
  17.   
  18. NSCalendar *gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];  
  19.   
  20. NSDate *date = [gregorian dateFromComponents:components];  


        为了保证正确的行为,您必须确保使用的组件在日历上是有意义的。指定“出界”日历组件,如一个-6或2月30日在公历中的日期值产生未定义的行为。

        你也可以创建一个不带年份的NSDate对象,这样的操作系统会自动生成一个年份,但在后面的代码中不会使用其自动生成的年份。

  1. NSDateComponents *components = [[NSDateComponents alloc] init];  
  2.   
  3. [components setMonth:11];  
  4. [components setDay:7];  
  5.   
  6. NSCalendar *gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];  
  7.   
  8. NSDate *birthday = [gregorian dateFromComponents:components];  


        下面的示例显示了如何从一个日历置换到另一个日历:

  1. NSDateComponents *comps = [[NSDateComponents alloc] init];  
  2.   
  3. [comps setDay:6];  
  4. [comps setMonth:5];  
  5. [comps setYear:2004];  
  6.   
  7. NSCalendar *gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];  
  8.   
  9. NSDate *date = [gregorian dateFromComponents:comps];  
  10.   
  11. [comps release];  
  12. [gregorian release];  
  13.   
  14. NSCalendar *hebrew = [[NSCalendar alloc] initWithCalendarIdentifier:NSHebrewCalendar];  
  15.   
  16. NSUInteger unitFlags = NSDayCalendarUnit | NSMonthCalendarUnit | NSYearCalendarUnit;  
  17.   
  18. NSDateComponents *components = [hebrew components:unitFlags fromDate:date];  
  19.   
  20. NSInteger day = [components day]; // 15  
  21. NSInteger month = [components month]; // 9  
  22. NSInteger year = [components year]; // 5764  

 

历法计算

        在当前时间加上一个半小时:

  1. NSDate *today = [[NSDate alloc] init];  
  2.   
  3. NSCalendar *gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];  
  4.   
  5. NSDateComponents *offsetComponents = [[NSDateComponents alloc] init];  
  6.   
  7. [offsetComponents setHour:1];  
  8. [offsetComponents setMinute:30];  
  9.   
  10. // Calculate when, according to Tom Lehrer, World War III will end  
  11. NSDate *endOfWorldWar3 = [gregorian dateByAddingComponents:offsetComponents toDate:today options:0];  


        获得当前星期中的星期天(使用格里高利历):

  1. NSDate *today = [[NSDate alloc] init];  
  2.   
  3. NSCalendar *gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];  
  4.   
  5. // Get the weekday component of the current date  
  6. NSDateComponents *weekdayComponents = [gregorian components:NSWeekdayCalendarUnit fromDate:today];  
  7.   
  8. /*  
  9. Create a date components to represent the number of days to subtract from the current date.  
  10.   
  11. The weekday value for Sunday in the Gregorian calendar is 1, so subtract 1 from the number of days to subtract from the date in question.  (If today is Sunday, subtract 0 days.)  
  12. */  
  13.   
  14. NSDateComponents *componentsToSubtract = [[NSDateComponents alloc] init];  
  15.   
  16. [componentsToSubtract setDay: 0 – ([weekdayComponents weekday] – 1)];  
  17.   
  18. NSDate *beginningOfWeek = [gregorian dateByAddingComponents:componentsToSubtract toDate:today options:0];  
  19.   
  20. /*  
  21. Optional step:  
  22. beginningOfWeek now has the same hour, minute, and second as the original date (today).  
  23.   
  24. To normalize to midnight, extract the year, month, and day components and create a new date from those components.  
  25. */  
  26.   
  27. NSDateComponents *components = [gregorian components:(NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit) fromDate: beginningOfWeek];  
  28.   
  29. beginningOfWeek = [gregorian dateFromComponents:components];  

        如何可以计算出一周的第一天(根据系统的日历设置):

  1. NSDate *today = [[NSDate alloc] init];  
  2.   
  3. NSDate *beginningOfWeek = nil;  
  4.   
  5. BOOL ok = [gregorian rangeOfUnit:NSWeekCalendarUnit startDate:&beginningOfWeek interval:NULL forDate: today];  


        获得两个日期之间的间隔:

  1. NSDate *startDate = …;  
  2. NSDate *endDate = …;  
  3.   
  4. NSCalendar *gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];  
  5.   
  6. NSUInteger unitFlags = NSMonthCalendarUnit | NSDayCalendarUnit;  
  7.   
  8. NSDateComponents *components = [gregorian components:unitFlags fromDate:startDate toDate:endDate options:0];  
  9.   
  10. NSInteger months = [components month];  
  11. NSInteger days = [components day];  

        使用Category来计算同一时代(AD|BC)两个日期午夜之间的天数:

  1. @implementation NSCalendar (MySpecialCalculations)  
  2.   
  3. -(NSInteger)daysWithinEraFromDate:(NSDate *) startDate toDate:(NSDate *) endDate {  
  4.      NSInteger startDay=[self ordinalityOfUnit:NSDayCalendarUnit inUnit: NSEraCalendarUnit forDate:startDate];  
  5.   
  6.      NSInteger endDay=[self ordinalityOfUnit:NSDayCalendarUnit inUnit: NSEraCalendarUnit forDate:endDate];  
  7.   
  8.      return endDay-startDay;  
  9. }  
  10.   
  11. @end  


        使用Category来计算不同时代(AD|BC)两个日期的天数:

  1. @implementation NSCalendar (MyOtherMethod)  
  2.   
  3. -(NSInteger) daysFromDate:(NSDate *) startDate toDate:(NSDate *) endDate {  
  4.   
  5.      NSCalendarUnit units=NSEraCalendarUnit | NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit;  
  6.   
  7.      NSDateComponents *comp1=[self components:units fromDate:startDate];  
  8.      NSDateComponents *comp2=[self components:units fromDate endDate];  
  9.   
  10.      [comp1 setHour:12];  
  11.      [comp2 setHour:12];  
  12.   
  13.      NSDate *date1=[self dateFromComponents: comp1];  
  14.      NSDate *date2=[self dateFromComponents: comp2];  
  15.   
  16.      return [[self components:NSDayCalendarUnit fromDate:date1 toDate:date2 options:0] day];  
  17. }  
  18.   
  19. @end  


        判断一个日期是否在当前一周内(使用格里高利历):

  1. -(BOOL)isDateThisWeek:(NSDate *)date {  
  2.   
  3.      NSDate *start;  
  4.      NSTimeInterval extends;  
  5.   
  6.      NSCalendar *cal=[NSCalendar autoupdatingCurrentCalendar];  
  7.      NSDate *today=[NSDate date];  
  8.   
  9.      BOOL success= [cal rangeOfUnit:NSWeekCalendarUnit startDate:&start interval: &extends forDate:today];  
  10.   
  11.      if(!success)  
  12.         return NO;  
  13.   
  14.      NSTimeInterval dateInSecs = [date timeIntervalSinceReferenceDate];  
  15.      NSTimeInterval dayStartInSecs= [start timeIntervalSinceReferenceDate];  
  16.   
  17.      if(dateInSecs > dayStartInSecs && dateInSecs < (dayStartInSecs+extends)){  
  18.           return YES;  
  19.      }  
  20.      else {  
  21.           return NO;  
  22.      }  
  23. }  

iOS将字符串转换为日期时间格式

1、如何如何将一个字符串如“ 20110826134106”装化为任意的日期时间格式,下面列举两种类型:
   NSString* string = @”20110826134106″;
    NSDateFormatter *inputFormatter = [[[NSDateFormatter alloc] init] autorelease];
    [inputFormatter setLocale:[[[NSLocale alloc] initWithLocaleIdentifier:@”en_US”] autorelease]];
    [inputFormatter setDateFormat:@”yyyyMMddHHmmss”];
    NSDate* inputDate = [inputFormatter dateFromString:string];
    NSLog(@”date = %@”, inputDate);
    
    NSDateFormatter *outputFormatter = [[[NSDateFormatter alloc] init] autorelease]; 
    [outputFormatter setLocale:[NSLocale currentLocale]];
    [outputFormatter setDateFormat:@”yyyy年MM月dd日 HH时mm分ss秒”];
    NSString *str = [outputFormatter stringFromDate:inputDate];
    NSLog(@”testDate:%@”, str);
两次打印的结果为:
    date = 2011-08-26 05:41:06 +0000
    testDate:2011年08月26日 13时41分06秒

说明:上面的时间是美国时间,下面的没有设置

   NSString* string = @”Wed, 05 May 2011 10:50:00 +0800″;
    NSDateFormatter *inputFormatter = [[[NSDateFormatter alloc] init] autorelease];
    [inputFormatter setLocale:[[[NSLocale alloc] initWithLocaleIdentifier:@”en_US”] autorelease]];
    [inputFormatter setDateFormat:@”EEE, d MMM yyyy HH:mm:ss Z”];
    NSDate* inputDate = [inputFormatter dateFromString:string];
    NSLog(@”date = %@”, inputDate);

2、以前一直为这个事情纠结,无奈只能拼接字符串:

NSString *str=@”20120403000000″;

NSString *dateStr=[NSString stringWithFormat:@”有效期至:%@年%@月%@日”,
                           [str substringWithRange:NSMakeRange(0, 4)],
                           [str substringWithRange:NSMakeRange(4, 2)],
                           [str substringWithRange:NSMakeRange(6, 2)]];
这个方法笨,可是没办法,查了好多资料,都没明白,今天突然明白了,呵呵,只要把那个[inputFormatter setDateFormat:@”EEE, d MMM yyyy HH:mm:ss Z”];@“”里面的格式转化为你字符串的格式一切就OK了,不知道我说明白了吗?


3、iOS-NSDateFormatter 格式说明:

G: 公元时代,例如AD公元
    yy: 年的后2位
    yyyy: 完整年
    MM: 月,显示为1-12
    MMM: 月,显示为英文月份简写,如 Jan
    MMMM: 月,显示为英文月份全称,如 Janualy
    dd: 日,2位数表示,如02
    d: 日,1-2位显示,如 2
    EEE: 简写星期几,如Sun
    EEEE: 全写星期几,如Sunday
    aa: 上下午,AM/PM
    H: 时,24小时制,0-23
    K:时,12小时制,0-11
    m: 分,1-2位
    mm: 分,2位
    s: 秒,1-2位
    ss: 秒,2位
    S: 毫秒

常用日期结构:
yyyy-MM-dd HH:mm:ss.SSS
yyyy-MM-dd HH:mm:ss
yyyy-MM-dd
MM dd yyyy

iOS开发UITableView中行的操作

文章写得很好,转载自:http://my.oschina.net/plumsoft/blog/53271

这篇文章主要讲的表格的操作包括:标记行、移动行、删除行、插入行。

这次就不从头建立工程了,在http://www.oschina.net/code/snippet_164134_9876下载工程。这个工程就是最简单的产生一个表格并向其中写入数据。用Xcode 4.2打开它,在这个工程基础上实现以上操作。

1、标记行

这里讲的标记行指的是单击此行,可以实现在此行右边出现一个勾,如下图所示:

为了实现标记功能,在ViewController.m中@end之前添加代码:

#pragma mark -
#pragma mark Table Delegate Methods
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { 
    UITableViewCell *oneCell = [tableView cellForRowAtIndexPath: indexPath];
    if (oneCell.accessoryType == UITableViewCellAccessoryNone) {
        oneCell.accessoryType = UITableViewCellAccessoryCheckmark;
    } else 
        oneCell.accessoryType = UITableViewCellAccessoryNone;
    [tableView deselectRowAtIndexPath:indexPath animated:YES]; 
}

该代码实现:单击某行时,若此行未被标记,则标记此行;若此行已经被标记,则取消标记。

运行效果如上图。

上面的代码实际上就是修改某行的accessoryType属性,这个属性可以设为四个常量:

UITableViewCellAccessoryCheckmark
UITableViewCellAccessoryDetailDisclosureButton
UITableViewCellAccessoryDisclosureIndicator
UITableViewCellAccessoryNone

效果依次如下图所示:

            

   UITableViewCellAccessoryCheckmark            UITableViewCellAccessoryDetailDisclosureButton

                 

UITableViewCellAccessoryDisclosureIndicator                   UITableViewCellAccessoryNone

注意,上面第二张图片中的蓝色圆圈不仅仅是一个图标,还是一个控件,点击它可以触发事件,在上一篇博客《iOS开发16:使用Navigation Controller切换视图》使用过。

2、移动行

想要实现移动或者删除行这样的操作,需要启动表格的编辑模式。使用的是setEditing:animated:方法。

2.1 打开ViewController.xib,将其中的表格控件映射成Outlet到ViewController.h,名称为myTableView。

2.2 打开ViewController.m,在viewDidLoad方法最后添加代码:

//启动表格的编辑模式
[self.myTableView setEditing:YES animated:YES];

2.3 在@end之前添加代码:

//打开编辑模式后,默认情况下每行左边会出现红的删除按钮,这个方法就是关闭这些按钮的
- (UITableViewCellEditingStyle)tableView:(UITableView *)tableView
           editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath { 
    return UITableViewCellEditingStyleNone; 
} 

//这个方法用来告诉表格 这一行是否可以移动
- (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath { 
    return YES; 
}

//这个方法就是执行移动操作的
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)
        sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath {
    NSUInteger fromRow = [sourceIndexPath row]; 
    NSUInteger toRow = [destinationIndexPath row]; 
    
    id object = [list objectAtIndex:fromRow]; 
    [list removeObjectAtIndex:fromRow]; 
    [list insertObject:object atIndex:toRow]; 
}

editingStyleForRowAtIndexPath这个方法中用到了常量UITableViewCellEditingStyleNone,它表示不可编辑,这里的编辑指的是删除和插入。表示表格行的编辑模式的常量有:

UITableViewCellEditingStyleDelete
UITableViewCellEditingStyleInsert
UITableViewCellEditingStyleNone

顾名思义,第一个表示删除,第二个表示插入,第三个表示不可编辑。

若将editingStyleForRowAtIndexPath方法中的UITableViewCellEditingStyleNone依次换成上面三个值,则它们运行的效果依次如下图所示:

      

2.4 运行,从下图可以看到实现了行的移动:

但是也会发现,现在无法对每行进行标记了。这说明,在编辑模式下,无法选择行,从而didSelectRowAtIndexPath这个方法不会执行。

3、删除行

从第2步过来,实现删除某行,其实比较简单了。

3.1将editingStyleForRowAtIndexPath方法中的UITableViewCellEditingStyleNone修改成UITableViewCellEditingStyleDelete。

3.2 在@end之前添加代码:

//这个方法根据参数editingStyle是UITableViewCellEditingStyleDelete
//还是UITableViewCellEditingStyleDelete执行删除或者插入
- (void)tableView:(UITableView *)tableView commitEditingStyle:
    (UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
    NSUInteger row = [indexPath row];
    if (editingStyle == UITableViewCellEditingStyleDelete) {
        [self.list removeObjectAtIndex:row]; 
        [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
                         withRowAnimation:UITableViewRowAnimationAutomatic]; 
    }
}

在这个方法中又出现了一个常量:UITableViewRowAnimationAutomatic,它表示删除时的效果,类似的常量还有:

UITableViewRowAnimationAutomatic
UITableViewRowAnimationTop
UITableViewRowAnimationBottom
UITableViewRowAnimationLeft
UITableViewRowAnimationRight
UITableViewRowAnimationMiddle
UITableViewRowAnimationFade
UITableViewRowAnimationNone

它们的效果就不一一介绍了,可以在实际使用时试试。

3.3 运行,看看效果:

      

刚运行时显示如左边的图片,点击某一行左边的圆圈图标,会显示如中间图片所示。然后点击Delegate按钮,那一行就会被删除掉,如右边的那张图片所示,它显示的是删除时的效果。

4、插入行

这个与删除行类似。

4.1 首先将editingStyleForRowAtIndexPath方法中的UITableViewCellEditingStyleDelete修改成UITableViewCellEditingStyleInsert。

4.2在3.2添加的方法中添加代码:

else {
    //我们实现的是在所选行的位置插入一行,因此直接使用了参数indexPath
    NSArray *insertIndexPaths = [NSArray arrayWithObjects:indexPath,nil];
    //同样,将数据加到list中,用的row
    [self.list insertObject:@"新添加的行" atIndex:row];
    [tableView insertRowsAtIndexPaths:insertIndexPaths withRowAnimation:UITableViewRowAnimationRight];
}

上面的代码中也可以不用insertRowsAtIndexPaths方法,而直接使用[tableView reloadData];语句,但是这样就没有添加的效果了。

4.3 好了,运行一下:

      

刚运行时如上面左图所示,单击了某个加号后,新的一行就从右边飞进来了,因为在insertRowsAtIndexPaths中用了参数UITableViewRowAnimationRight。

Objective-C中的Class(类类型),Selector(选择器SEL),函数指针(IMP)

看到了一篇牛文“Objective-C 2.0 with Cocoa Foundation— 5,Class类型,选择器Selector以及函数指针”,讲得十分精彩,忍不住把它的代码加上注释整理于此,以便日后查看。
个人体会:obj-C中的“Class类型变量”比c#中的Object基类还要灵活,可以用它生成任何类型的实例(但是它又不是NSObject)。而选择器SEL与函数指针IMP,如果非要跟c#扯上关系的话,这二个结合起来,就点类似c#中的反射+委托,可以根据一个方法名称字符串,直接调用方法。
“牛”的基类 Cattle.h
1
2
3
4
5
6
7
8
#import <Foundation/Foundation.h>
@interface Cattle : NSObject {
    int legsCount;
}
- (void)saySomething;
- (void)setLegsCount:(int) count;
@end
 Cattle.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#import "Cattle.h"
@implementation Cattle
-(void) saySomething
{
    NSLog(@"Hello, I am a cattle, I have %d legs.", legsCount);
}
-(void) setLegsCount:(int) count
{
    legsCount = count;
}
@end
子类“公牛” Bull.h
1
2
3
4
5
6
7
8
9
10
#import <Foundation/Foundation.h>
#import "Cattle.h"
@interface Bull : Cattle {
    NSString *skinColor;
}
- (void)saySomething;
- (NSString*) getSkinColor;
- (void) setSkinColor:(NSString *) color;
@end
Bull.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#import "Bull.h"
@implementation Bull
-(void) saySomething
{
    NSLog(@"Hello, I am a %@ bull, I have %d legs.", [self getSkinColor],legsCount);
}
-(NSString*) getSkinColor
{
    return skinColor;
}
- (void) setSkinColor:(NSString *) color
{
    skinColor = color;
}
@end
代理类DoProxy.h (关键的代码都在这里)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#import <Foundation/Foundation.h>
//定义几个字符串常量
#define SET_SKIN_COLOR @"setSkinColor:"
#define BULL_CLASS @"Bull"
#define CATTLE_CLASS @"Cattle"
@interface DoProxy : NSObject {
    BOOL notFirstRun;
    
    id cattle[3];
    //定义二个选择器
    SEL say;
    SEL skin;
    
    //定义一个函数指针(传统C语言的处理方式)
    void(*setSkinColor_Func)(id,SEL,NSString*);
    
    //定义一个IMP方式的函数指针(obj-C中推荐的方式)
    IMP say_Func;
    
    //定义一个类
    Class bullClass;
}
-(void) doWithCattleId:(id) aCattle colorParam:(NSString*) color;
-(void) setAllIVars;
-(void) SELFuncs;
-(void) functionPointers;
@end
DoProxy.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#import "DoProxy.h"
#import "Cattle.h"
#import "Bull.h"
@implementation DoProxy
//初始化所有变量
- (void) setAllIVars
{  
    cattle[0] = [Cattle new];
    
    bullClass = NSClassFromString(BULL_CLASS);
    //即cattle[1],cattle[2]都是Bull类的实例
    cattle[1] = [bullClass new];
    cattle[2] = [bullClass new];
    
    say = @selector(saySomething);
    skin = NSSelectorFromString(SET_SKIN_COLOR);
}
//初始化id
- (void) doWithCattleId:(id) aCattle colorParam:(NSString*) color
{
    //第一次运行的时候
    if(notFirstRun == NO)
    {
        NSString *myName = NSStringFromSelector(_cmd);//取得当前正在执行的方法的名字
        NSLog(@"Running in the method of %@", myName);
        notFirstRun = YES;//修改初次运行标志位
    }
    
    NSString *cattleParamClassName = [aCattle className];//取得aCattle的"类名称"
    
    //如果aCattle是Bull或Cattle类的实例
    if([cattleParamClassName isEqualToString:BULL_CLASS] || [cattleParamClassName isEqualToString:CATTLE_CLASS])
    {
        [aCattle setLegsCount:4];//设置牛的4条腿
        if([aCattle respondsToSelector:skin])//如果aCattle对应的是类中,有定义方法"setSkinColor"
        {
            [aCattle performSelector:skin withObject:color];//则调用setSkinColor方法
        }
        else
        {
            NSLog(@"Hi, I am a %@, have not setSkinColor!", cattleParamClassName);//否则输出相应的提示信息
        }
        [aCattle performSelector:say];//最后执行saySomething方法(这二个方法在Bull与Cattle类中都有,所以肯定能运行)
    }
    else //如果aCattle即不是Bull类也不是Cattle类的实例
    {
        NSString *yourClassName = [aCattle className];
        NSLog(@"Hi, you are a %@, but I like cattle or bull!", yourClassName);//显示这个"异类"的相关信息
    }
}
//初始化选择器以及相应函数
- (void) SELFuncs
{
    [self doWithCattleId:cattle[0] colorParam:@"brown"];
    [self doWithCattleId:cattle[1] colorParam:@"red"];
    [self doWithCattleId:cattle[2] colorParam:@"black"];
    [self doWithCattleId:self colorParam:@"haha"];//这里故意传入一个异类self(即DoProxy本身),DoProxy当然不是Bull或Cattle
}
//函数指针测试
- (void) functionPointers
{
    //取得函数指针的第一种方式
    setSkinColor_Func=(void (*)(id, SEL, NSString*)) [cattle[1] methodForSelector:skin];
    //上面的语句其实等效于下面这种方法
    //IMP setSkinColor_Func = [cattle[1] methodForSelector:skin];
    
    //用第二种方法取得saySomething的函数指针
    say_Func = [cattle[1] methodForSelector:say];
    
    //用函数指针的形式调用setSkinColor
    setSkinColor_Func(cattle[1],skin,@"verbose");
    
    NSLog(@"Running as a function pointer will be more efficiency!");
    
    //调用saySomething方法
    say_Func(cattle[1],say);
}
@end
测试主函数main()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#import <Foundation/Foundation.h>
#import "DoProxy.h"
int main (int argc, const char * argv[]) {
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    DoProxy *doProxy = [DoProxy new];
    
    [doProxy setAllIVars];
    [doProxy SELFuncs];
    [doProxy functionPointers];
    
    [doProxy release];
    [pool drain];
    return 0;
}

运行结果:

2011-02-28 21:40:33.240 HelloSelector[630:a0f] Running in the method of doWithCattleId:colorParam:
2011-02-28 21:40:33.245 HelloSelector[630:a0f] Hi, I am a Cattle, have not setSkinColor!
2011-02-28 21:40:33.247 HelloSelector[630:a0f] Hello, I am a cattle, I have 4 legs.
2011-02-28 21:40:33.248 HelloSelector[630:a0f] Hello, I am a red bull, I have 4 legs.
2011-02-28 21:40:33.250 HelloSelector[630:a0f] Hello, I am a black bull, I have 4 legs.
2011-02-28 21:40:33.251 HelloSelector[630:a0f] Hi, you are a DoProxy, but I like cattle or bull!
2011-02-28 21:40:33.252 HelloSelector[630:a0f] Running as a function pointer will be more efficiency!
2011-02-28 21:40:33.254 HelloSelector[630:a0f] Hello, I am a verbose bull, I have 4 legs.

作者:菩提树下的杨过
出处:http://yjmyzz.cnblogs.com
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

Objective-c的动态调用函数的方法

最近在看关于selector的资料,感觉很好玩。

首先我能理解什么叫selector已经不容易了,查阅了很多资料。
其次是要动态调用,首先网上找到的方法是,这样的用法:

-(void)traceThem:(int)a traceThem2:(int)b{

NSLog(@”hello:%d 你好%d”,a,b);

}

[self performSelector:@selector(traceThem:traceThem2:) withObject:(id)1 withObject:(id)2];

上面代码的意思要调用函数名为traceThem:traceThem2:的函数,参数分别是1,2

(OC就叫发消息,它的函数说法是有一条traceThem:traceThem2:的消息,reciver是self,其实编译的时候还不是自己改成调用函数的概念。);

performSelector这样的调用函数方法,最多只能支持2个参数,你可以把参数放到NSDictionary传递。

搞到百度到的一大堆blog文都是自己写了一大段代码来支持performSelector多个参数的调用。。。

其实还有个好方法objc_msgSend:

objc_msgSend(self,@selector(traceThem:traceThem2:traceThme3:),参数1,参数2,参数3);

但是动态调用,就是要求@selector()的参数能动态输入,例如是配置到一个配置表中的字符串(NSString),方法如下:

SEL function = NSSelectorFromString(@”traceThem:traceThem2:traceThem3:”);

objc_msgSend(self,function,1,2,3);

BTW:

由于objc_msgSend是运行时的方法,所以要加入头文件,用open quickly可帮到大忙,如下:

,即需要的头文件:#import <objc/message.h>

 

objective-c的动态调用函数的方法 - Sylar_Lin - 低调做人高调做事

WWDC2014之App Extensions

一、关于App Extensions

extension是iOS8新开放的一种对几个固定系统区域的扩展机制,它可以在一定程度上弥补iOS的沙盒机制对应用间通信的限制。

extension的出现,为用户提供了在其它应用中使用我们应用提供的服务的便捷方式,比如用户可以在Todaywidgets中查看应用展示的简略信息,而不用再进到我们的应用中,这将是一种全新的用户体验;但是,extension的出现可能会减少用户启动应用的次数,同时还会增大开发者的工作量。

几个关键词

  • extension point

系统中支持extension的区域,extension的类别也是据此区分的,iOS上共有TodayShareActionPhoto EditingStorage ProviderCustom keyboard几种,其中Today中的extension又被称为widget

每种extension point的使用方式和适合干的活都不一样,因此不存在通用的extension。

  • app extension

即为本文所说的extension。extension并不是一个独立的app,它有一个包含在app bundle中的独立bundle,extension的bundle后缀名是.appex。其生命周期也和普通app不同,这些后文将会详述。

extension不能单独存在,必须有一个包含它的containing app。

另外,extension需要用户手动激活,不同的extension激活方式也不同,比如: 比如Today中的widget需要在Today中激活和关闭;Custom keyboard需要在设置中进行相关设置;Photo Editing需要在使用照片时在照片管理器中激活或关闭;Storage Provider可以在选择文件时出现;ShareAction可以在任何应用里被激活,但前提是开发者需要设置Activation Rules,以确定extension需要在合适出现。

  • containing app

尽管苹果开放了extension,但是在iOS中extension并不能单独存在,要想提交到AppStore,必须将extension包含在一个app中提交,并且app的实现部分不能为空,这个包含extension的app就叫containing app。

extension会随着containing app的安装而安装,同时随着containing app的卸载而卸载。

  • host app

能够调起extension的app被称为host app,比如widget的host app就是Today

二、extension和containing app、host app

2.1 extension和host app

extension和host app之间可以通过extensionContext属性直接通信,该属性是新增加的UIViewController类别:

1
2
3
4
5
6
@interface UIViewController(NSExtensionAdditions) <NSExtensionRequestHandling>
// Returns the extension context. Also acts as a convenience method for a view controller to check if it participating in an extension request.
@property (nonatomic,readonly,retain) NSExtensionContext *extensionContext NS_AVAILABLE_IOS(8_0);
@end

实际上extension和host app之间是通过IPC(interprocess communication)实现的,只是苹果把调用接口高度抽象了,我们并不需要关注那么底层的东西。

2.2 containing app和host app

他们之间没有任何直接关系,也从来不需要通信。

2.3 extension和containing app

这二者之间的关系最复杂,纠纠缠缠扯不清关系。

  • 不能直接通信

首先,尽管extension的bundle是放在containing app的bundle中,但是他们是两个完全独立的进程,之间不能直接通信。不过extension可以通过openURL的方式启动containing app(当然也能启动其它app),不过必须通过extensionContext借助host app来实现:

1
2
3
4
5
6
7
8
//通过openURL的方式启动Containing APP
- (void)openURLContainingAPP
{
    [self.extensionContext openURL:[NSURL URLWithString:@"appextension://123"]
                 completionHandler:^(BOOL success) {
                     NSLog(@"open url result:%d",success);
                 }];
}

extension中是无法直接使用openURL的。

  • 可以共享Shared resources

extension和containing app可以共同读写一个被称为Shared resources的存储区域,这是通过App Groups实现的,后文将会详述。

三者间的关系可以通过官网给的两张图片形象地说明:

detailed_communication

app_extensions_container_restrictions

  • containing app能够控制extension的出现和隐藏

通过以下代码,containing app可以让extension出现或隐藏(当然extension也可以让自己隐藏):

1
2
3
4
5
6
7
8
9
10
11
//让隐藏的插件重新显示
- (void)showTodayExtension
{
    [[NCWidgetController widgetController] setHasContent:YES forWidgetWithBundleIdentifier:@"com.wangzz.app.extension"];
}
//隐藏插件
- (void)hiddeTodayExtension
{
    [[NCWidgetController widgetController] setHasContent:NO forWidgetWithBundleIdentifier:@"com.wangzz.app.extension"];
}

三、App Groups

这是iOS8新开放的功能,在OS X上早就可用了。它主要用于同一group下的app共享同一份读写空间,以实现数据共享。

extension和containing app共同读写一份数据是很合理的需求,比如系统的股市应用,widget和app中都需要展示几个公司的股票数据,这就可以通过App Groups实现。

3.1 功能开启

为了便于后续操作,请先确保你的开发者账号在Xcode上处于登录状态。

  • 在app中开启

App Groups位于:

1
TARGETS-->AppExtensionDemo-->Capabilities-->App Groups

找到以后,将App Groups右上角的开关打开,然后选择添加groups,比如我的是group.wangzz,当然这是为了测试随便起得名字,正规点得命名规则应该是:group.com.company.app。

添加成功以后如下图所示:

app_group

  • 在extension中开启

我创建的是widget,target名称为TodayExtension,对应的App Groups位于:

1
TARGETS-->TodayExtension-->Capabilities-->App Groups

开启方式和app中一样,需要注意的是必须保证这里地App Groups名称和app中的相同,即为group.wangzz。

四、extension和containing app数据共享

App Groups给我们提供了同一group内app可以共同读写的区域,可以通过以下方式实现数据共享:

4.1 通过NSUserDefaults共享数据

  • 存数据

通过以下方式向NSUserDefaults中保存数据:

1
2
3
4
5
6
7
- (void)saveTextByNSUserDefaults
{
    NSUserDefaults *shared = [[NSUserDefaults alloc] initWithSuiteName:@"group.wangzz"];
    [shared setObject:_textField.text forKey:@"wangzz"];
    [shared synchronize];
}

需要注意的是:

1.保存数据的时候必须指明group id;

2.而且要注意NSUserDefaults能够处理的数据只能是可plist化的对象,详情见Property List Programming Guide

3.为了防止出现数据同步问题,不要忘记调用[shared synchronize];

  • 读数据

对应的读取数据方式:

1
2
3
4
5
6
7
- (NSString *)readDataFromNSUserDefaults
{
    NSUserDefaults *shared = [[NSUserDefaults alloc] initWithSuiteName:@"group.wangzz"];
    NSString *value = [shared valueForKey:@"wangzz"];
    return value;
}

4.2 通过NSFileManager共享数据

NSFileManager在iOS7提供了containerURLForSecurityApplicationGroupIdentifier方法,可以用来实现app group共享数据。

  • 保存数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (BOOL)saveTextByNSFileManager
{
    NSError *err = nil;
    NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.wangzz"];
    containerURL = [containerURL URLByAppendingPathComponent:@"Library/Caches/good"];
    NSString *value = _textField.text;
    BOOL result = [value writeToURL:containerURL atomically:YES encoding:NSUTF8StringEncoding error:&err];
    if (!result) {
        NSLog(@"%@",err);
    } else {
        NSLog(@"save value:%@ success.",value);
    }
    return result;
}
  • 读数据
1
2
3
4
5
6
7
8
9
- (NSString *)readTextByNSFileManager
{
    NSError *err = nil;
    NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.wangzz"];
    containerURL = [containerURL URLByAppendingPathComponent:@"Library/Caches/good"];
    NSString *value = [NSString stringWithContentsOfURL:containerURL encoding:NSUTF8StringEncoding error:&err];
    return value;
}

在这里我试着保存和读取的是字符串数据,但读写SQlite我相信也是没问题的。

  • 数据同步

两个应用共同读取同一份数据,就会引发数据同步问题。WWDC2014的视频中建议使用NSFileCoordination实现普通文件的读写同步,而数据库可以使用CoreData,Sqlite也支持同步。

五、extension和containing app代码共享

和数据共享类似,extension和containing app很自然地会有一些业务逻辑上可以共用的代码,这时可以通过iOS8中刚开放使用的framework实现。苹果在App Extension Programming Guide中是这样描述的:

In iOS 8.0 and later, you can use an embedded framework to share code between your extension and its containing app. For example, if you develop image-processing code that you want both your Photo Editing extension and its containing app to share, you can put the code into a framework and embed it in both targets.

即将framework分别嵌入到extension和containing app的target中实现代码共享。但这样岂不是需要分别要将framework分别copy到extension和containing app的main bundle中?

参考extension和containing app数据共享,我试想能不能将framework只保存一份放在App Groups区域?

5.1 copy framework到App Groups

在app首次启动的时候将framework放到App Groups区域:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- (BOOL)copyFrameworkFromMainBundleToAppGroup
{
    NSFileManager *manager = [NSFileManager defaultManager];
    NSError *err = nil;
    NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.wangzz"];
    NSString *sorPath = [NSString stringWithFormat:@"%@/Dylib.framework",[[NSBundle mainBundle] bundlePath]];
    NSString *desPath = [NSString stringWithFormat:@"%@/Library/Caches/Dylib.framework",containerURL.path];
    BOOL removeResult = [manager removeItemAtPath:desPath error:&err];
    if (!removeResult) {
        NSLog(@"%@",err);
    } else {
        NSLog(@"remove success.");
    }
    BOOL copyResult = [[NSFileManager defaultManager] copyItemAtPath:sorPath toPath:desPath error:&err];
    if (!copyResult) {
        NSLog(@"%@",err);
    } else {
        NSLog(@"copy success.");
    }
    return copyResult;
}

5.2 使用framework:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (BOOL)loadFrameworkInAppGroup
{
    NSError *err = nil;
    NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.wangzz"];
    NSString *desPath = [NSString stringWithFormat:@"%@/Library/Caches/Dylib.framework",containerURL.path];
    NSBundle *bundle = [NSBundle bundleWithPath:desPath];
    BOOL result = [bundle loadAndReturnError:&err];
    if (result) {
        Class root = NSClassFromString(@"Person");
        if (root) {
            Person *person = [[root alloc] init];
            if (person) {
                [person run];
            }
        }
    } else {
        NSLog(@"%@",err);
    }
    return result;
}

经过测试,竟然能够加载成功。

需要说明的是,这里只是说那么用是可以成功加载framework,但还面临不少问题,比如如果用户在启动app之前去使用extension,这时framework还没有copy过去,怎么处理;另外iOS的机制或者苹果的审核是否允许这样使用等。

在一切确定下来之前还是乖乖按文档中的方式使用吧。

六、生命周期

extension和普通app的最大区别之一是生命周期。

  • 开始

在用户通过host app点击extension时,系统就会实例化extension应用,这是生命周期的开始。

  • 执行任务

在extension启动以后,开始执行它的使命。

  • 终止

在用户取消任务,或者任务执行结束,或者开启了一个长时后台任务时,系统会将其杀掉。

由此可见,extension就是为了任务而生!

下图来自官方文档,它将生命周期划分的更详细:

app_extensions_lifecycle

通过打印日志发现,Today中的widget在将Today切换到全部或者未读通知时都会被杀掉。

七、 调试

extension和普通app的调试方式差不多,开始调试前先选中extension对应的target,点击run,就会弹出下图所示选择框:

extension_debug

需要选择一个host app,这里选择Today

然后即可和普通app一样调试了,不过我在实际使用过程中,发现有各种奇怪的事情,比如NSLog无法在控制台输出,应该是bug吧。

八、 iOS8应用文件系统

发现iOS8的文件系统发生了变化,新的文件系统将可执行文件(即原来的.app文件)从沙盒中移到了另外一个地方,这样感觉更合理。

  • 测试代码

下述代码用于打印App Groups路径、应用的可执行文件路径、对应的Documents路径:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)logAppPath
{
    //app group路径
    NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.wangzz"];
    NSLog(@"app group:\n%@",containerURL.path);
    //打印可执行文件路径
    NSLog(@"bundle:\n%@",[[NSBundle mainBundle] bundlePath]);
    //打印documents
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *path = [paths objectAtIndex:0];
    NSLog(@"documents:\n%@",path);
}
  • containing app执行结果
1
2
3
4
5
6
2014-06-23 19:35:03.944 AppExtensionDemo[7471:365131] app group:
/private/var/mobile/Containers/Shared/AppGroup/89CCBFB1-CA5E-4C7F-80CB-A3EB9E841816
2014-06-23 19:35:03.946 AppExtensionDemo[7471:365131] bundle:
/private/var/mobile/Containers/Bundle/Application/1AC73797-A3BB-4BDE-A647-3D083DA6871A/AppExtensionDemo.app
2014-06-23 19:35:03.948 AppExtensionDemo[7471:365131] documents:
/var/mobile/Containers/Data/Application/E5E6E516-0163-4754-9D10-A5F6C33A6261/Documents
  • extension执行结果
1
2
3
4
5
6
Jun 23 19:37:49 autonavis-iPad com.foogry.AppExtensionDemo.TodayExtension[7638] <Warning>: app group:
  /private/var/mobile/Containers/Shared/AppGroup/89CCBFB1-CA5E-4C7F-80CB-A3EB9E841816
Jun 23 19:37:49 autonavis-iPad com.foogry.AppExtensionDemo.TodayExtension[7638] <Warning>: bundle:
  /private/var/mobile/Containers/Bundle/Application/596717B7-7CB8-4F53-BCD4-380F34ABD30F/AppExtensionDemo.app/PlugIns/com.foogry.AppExtensionDemo.TodayExtension.appex
Jun 23 19:37:49 autonavis-iPad com.foogry.AppExtensionDemo.TodayExtension[7638] <Warning>: documents:
  /var/mobile/Containers/Data/PluginKitPlugin/57581433-3DBD-4930-971F-78D30C150E8A/Documents

由此可见,不管是extension还是containing app,他们的可执行文件和保存数据的目录都是分开存放的,即所有app的可执行文件都放在一个大目录下,保存数据的目录保存在另一个大目录下,同样,AppGroup放在另一个大目录下。

说明

  • 本文用到的demo已经上传到github上。

  • 文中可能有理解有误的地方,还请指出。

参考文档