Quantcast
Channel: CSDN博客移动开发推荐文章
Viewing all 5930 articles
Browse latest View live

Android官方开发文档Training系列课程中文版:如何避免ANR?

$
0
0

原文地址:http://android.xsoftlab.net/training/articles/perf-anr.html#anr

尽管你写代码可能通过了世界上所有的性能测试,但是它还是可能会让人感觉到卡顿。当应用卡的不成样子时,系统会给你弹一个”Application Not Responding”的对话框。

在Android中,系统会对那些长时间没有响应的应用采取一些措施:弹出一个对话框告诉用户APP已经停止了响应,如下图所示:

正出于这个原因,系统会在APP长时间没有响应的时候为用户提供一个退出APP的选项。所以使APP能够及时响应这一点是至关重要的,这样系统才不会向用户显示ANR对话框。

这节课我们会学习Android系统如何检测应用程序是否是未响应,以及应用程序如何保持响应能力的一些改进措施。

什么触发了ANR?

通常情况下,系统会在应用程序不再能够响应用户的输入时显示ANR对话框。比如,如果应用阻塞在了UI线程的IO操作上,那么系统就不能够处理用户的输入事件。或者应用花费了大量的时间在内存模型的构建上或者是在UI线程中计算了游戏的下一步动作。要记住:
即便是最高效的代码也需要花费时间来运行。

任何情况下都不要在UI线程中执行耗时操作,而是要将这些工作放在一个单独的线程中执行。这可以使UI线程保持流畅工作(UI线程负责驱动用户界面的事件循环)。

在Android中,应用程序的响应态由Activity Manager及Window Manager负责监控。系统会在侦测到以下状况时显示ANR对话框:

  • 对输入事件在5秒内没有作出响应。
  • BroadcastReceiver在10秒内没有执行完毕。

如何避免ANR?

Android应用程序默认运行在UI线程中。这意味着在UI线程中的任何耗时操作都会引发ANR问题,因为这会使应用程序给不到输入事件或者意图广播处理的机会。

因此,在UI线程中的每个方法都应当做尽可能少的工作,尤其是Activity的生命周期回调函数。像网络或数据库操作,或者大量的计算之类的耗时操作应当在工作线程中执行。

创建用于执行耗时操作的线程最便捷的方式莫过于使用AsyncTask了。只需要继承AsyncTask,然后重写doInBackground()就可以执行了。如果要向用户展示工作进度,你可以使用publishProgress()方法,它会回调onProgressUpdate()方法(该方法运行于UI线程)。
在onProgressUpdate()内我们可以更新进度条。

private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {
    // Do the long-running work in here
    protected Long doInBackground(URL... urls) {
        int count = urls.length;
        long totalSize = 0;
        for (int i = 0; i < count; i++) {
            totalSize += Downloader.downloadFile(urls[i]);
            publishProgress((int) ((i / (float) count) * 100));
            // Escape early if cancel() is called
            if (isCancelled()) break;
        }
        return totalSize;
    }
    // This is called each time you call publishProgress()
    protected void onProgressUpdate(Integer... progress) {
        setProgressPercent(progress[0]);
    }
    // This is called when doInBackground() is finished
    protected void onPostExecute(Long result) {
        showNotification("Downloaded " + result + " bytes");
    }
}

使用execute()方法启动工作线程:

new DownloadFilesTask().execute(url1, url2, url3);

如果不采用这种方式,我们还有另一种实现方法:创建自己的Thread或HandlerThread。如果采用这种方法,那么应该设置该线程的优先级为”background”:通过Process.setThreadPriority()方法及参数THREAD_PRIORITY_BACKGROUND设置。
如果没有设置该优先级,那么该线程会使应用感到变慢,因为该线程的优先级默认与UI线程的优先级一致,它们会互相抢占CPU资源。

如果实现了自己的Thread或HandlerThread,那么要确保在等待其它工作线程完成之前UI线程不被阻塞—不要调用Thread.wait()或Thread.sleep()。如果需要等待其它线程的执行结果,可以为UI线程创建一个Handler。这样做可以使UI线程还可以对
输入事件保持响应能力。这样就可以避免5秒内无响应的ANR对话框出现。

BroadcastReceiver在执行时间上有特殊的限制,这意味着在其内部的工作一定是轻量级的:比如在后台做一些保存设置或者发送通知的工作。所以与UI线程中执行的方法一样,广播接收器内也应当杜绝耗时操作的出现。

TIP: 你可以使用StrictMode来发现UI线程中意外出现的耗时操作。

ANR相关优化

一般来说,100~200毫秒是用户所能感知到应用卡顿的极限。下面列出了一些可以避免应用程序ANR的一些点,同样也有助于防止出现卡顿的情况:

  • 如果应用需要对用户输入做大量的后台工作,可以显示一个进度表示工作正在进行。
  • 对于游戏类的复杂计算,应该将这些工作放在工作线程中执行。
  • 如果应用在初始化阶段需要花费一些时间,可以考虑显示一个闪屏页面或者尽可能快的显示主界面:展示加载正在进行,并进行异步数据填充。在这些情况下都应当表明任务正在进行,以免让用户认为应用已经卡死。
  • 使用SystraceTraceview等性能工具检查APP的响应瓶颈。
作者:u011064099 发表于2016/10/9 10:42:03 原文链接
阅读:58 评论:0 查看评论

iOS小笔记

$
0
0


controller:连接二者的桥梁;
cocoa frameworks 有两个框架:
foundation
foundation  是cocoa中最基本的一些类;再mac应用程序中负责对象管理,内存管理,容器等相关数据;
uikit:
uikit:为程序提供可视化的底层构架,包括窗口,视图,控件类和管理这些对象的控制器。这一层中的其它框架允许你访问用户联系人和图片信息,以及设备上的加速器和其它硬件特征;


UI(User Interface)编程


9.3第八周,周一
创建ios项目
1.打开iOS中的application;


2.UIViewController:视图控件,通过点击,触摸来控制程序
3.UIView :视图;


4.在项目中创建一个文件。在 iOS的cocoa touch创建一个
  ulviewcontrollersubclass类然后在AppDelegate.m中找到application函数,在 self.window.backgroundColor = [UIColor whiteColor];后写如下代码
        RootViewCotroller *rvc=[[RootViewCotroller alloc]initWithNibName:@"RootViewCotroller" bundle:nil];
    self.window.rootViewController=rvc;
    [rvc release];
  代码含义:创建根控件,并把新创建的根控件赋值给窗口的根控件;注意释放;
5.label控件的创建和属性:
     可以用视图界面创建;然后修改属性;
6.代码实现label
  1.IBOutlet  :是个宏。控件和根界面控件内容建立联系
   
   2.atomic:原子操作;线成保护;就是在几个线程中调用同一段内容时导致所用内容混乱,进行枷锁机制
nonatomic:不加锁机制;能提高效率;
     把控件打包,并和界面添加的控件建立联系:
 
   3.在RootViewCotroller.m文件中在viewDidLoad(加载函数)函数中设置lable的属性;
   4.iPhone 横向320 竖向480 横条占20; 所以一般是320*460
      创建给label并在屏幕上定位
    CGRect rect={0,100,100,40};
         //创建定位结构体;
           坐标原点是左上角;四个参数分别是距左框的距离;距上框的距离;label控件的宽度,高度;
    UILabel *label=[[UILabel alloc] initWithFrame:rect];
    UILabel *label=[[UILabel alloc]
    可一直接生成UIrect;
initWithFrame:CGRectMake(<#CGFloat x#>, <#CGFloat y#>, <#CGFloat width#>, <#CGFloat height#>)]
 
   5.UIView  * 指所有可视控件;
   6.设置背景色
    [label1 setBackgroundColor:[UIColor blueColor]];
   7.设置label的字体;
       [label2 setFont:[UIFont systemFontOfSize:20]];
    label1.font=[UIFont italicSystemFontOfSize:30];
  
  8.设置字体的颜色
      label2.textColor=[UIColor redColor];
  9.设置字体的对齐方式;
     label1.textAlignment=UITextAlignmentLeft;
  10.自动调整字体
       label1.adjustsFontSizeToFitWidth=YES;
       当文字数量多时可以自动调整字体大小
       如果不设置就会变成省略;
 
       字体省略方式:
       label2.lineBreakMode=UILineBreakModeHeadTruncation;
    这个是截去头,还有尾截,和中间截去;
       label2.lineBreakMode=UILineBreakModeClip;
    表示在已有控件写多少是多少,不写省略符
 
         label1.lineBreakMode=UILineBreakModeCharacterWrap;
     字符换行,指只要需要换行就换行不考虑单词的整体性;
    label1.lineBreakMode=UILineBreakModeWordWrap;
        以一个整个单词换行,不允许把整个单词分开写到不同的行;保持单词完整性;
 
      设置自动调整的最小限制
    label1.minimumFontSize=10;
  11.设置行数:
       label1.numberOfLines=3;
    注:如果行高不够的话它不能显示完整;
   字体省略方式
 12.设置高亮颜色
     label1.highlightedTextColor=[UIColor whiteColor];
      设置高亮的状态时的颜色;
    label1.highlighted=YES;
     把label设置为高亮状态;
 13.设置阴影色
  label1.shadowColor=[UIColor blackColor];
  设置阴影颜色;
    label1.shadowOffset=CGSizeMake(-2,-2 );//只有长和宽 没有   起始位置
   设置阴影位置;是相对字体的位置;
 14.IOPhone的图标默认大小时57*57;
 
 
9.4周二
1. IBAction 时void型的,为了连接某个触发;
2.button创建
  button1=[UIButton buttonWithType:UIButtonTypeRoundedRect]
  注:这样创建的button不是alloc出来的不用释放;
           后面参数时button 的外观;
3.button设置位置:
  button1.frame=CGRectMake(120, 280, 80, 40);
4.button 设置title
  [button1 setTitle:@"click here" forState:UIControlStateNormal];
  注:由于button继承view control所以它可以设置状态;
          状态的作用?
       答:可以在不同状态下展示不同的button 特点;
   1.高亮状态下设置
     [button1 setTitle:@"click ready" forState:UIControlStateHighlighted];
   2.禁用状态设置
    [button1 setTitle:@"forbid using" forState:UIControlStateDisabled];
      button1.enabled=NO;
    button设置成不可用,但是还可以更改button 的属性
5.  设置button的title的属性;  
     button1.titleLabel.font=[UIFont systemFontOfSize:20];
  注:button是复合了label所以可以调用title label,设置它的 文本属性就是设置button的文本属性;
6设置       颜色
  [button1 setTitleColor:[UIColor yellowColor] forState:UIControlStateNormal];
7.给button添加事件
     [button1 addTarget:self action:@selector(onclick) forControlEvents:UIControlEventTouchUpInside];
   addTarget:指触发的对象; action:触发的事件; forControlEvets:是表示如何点击按钮触发;
8.局部声明button,如何使用button使其执行动作;
  在事件函数头传递button对象;谁调用它就传递谁的地址;
  -(void)onclick:(UIButton *)seder
    [button addTarget:self action:@selector(onclick:) forControlEvents:UIControlEventTouchUpInside];  
    注意:传递调用函数带参数有冒号;
9.添加图片
    UIImage *image=[UIImage imageNamed:@"03.gif"];
    [button setBackgroundImage:image forState:UIControlStateNormal];
    [button setBackgroundImage:image1 forState:UIControlStateHighlighted];
10.得到图片大小
    CGSize size=image.size;
    NSLog(@"%f %f",size.width,size.height);
 
11.让button一直处于选择状态;
     button.selected=YES;
   button会一直处于一个状态而不改变? 到底什么意思
12.  判断button状态执行操作;
if(button.state==UIControlStateNormal){
     
    }
13.移出事件
       [sender removeTarget:self action:@selector(onclick:) forControlEvents:UIControlEventTouchDragOutside];
14.点击滑动的妙用;可以在作程序时加入实现触屏效果;
     [button addTarget:self action:@selector(onclick:) forControlEvents:UIControlEventTouchDragInside];
    [button addTarget:self action:@selector(onclick:) forControlEvents:UIControlEventTouchDragOutside];




 
9.6周四
UIView (视图)
1.什么是uiview
  在应用程序中看的见摸的着的是视图。相当于java中容器;
  viewController相当于底板;
2.创建视图
  UIView *view=[[UIView alloc] initWithFrame:CGRectMake(20, 20, 280, 420)];
3.将视图添加到父视图
  [self.view addSubview:view];
   注意:子视图是父视图的子类,后添加的视图在最上曾;
4.在视图view中添加label(label 是view的子类)
 [view addSubview:label];
 注:由于ui中的控件都是视图,所以可以在任意视图加载其他视图;
 把一些视图装到另一个视图是为了保证视图的整体性;
 注:在button上添加label点击label就是点击button label相当于透明;而在上添button就是点谁谁响应;
5.
  CGRectframe :坐标
  CGRectbounds:边界
     button.bounds=CGRectMake(0, 0, 50, 50);
  CGPointcenter:中点
     该是图的中点在父视图的相对坐标;
   button.center=CGPointMake(160, 230);
   指视图的中心位置在父视图的相对位置
  得到视图的终点坐标:
     button.center.x;
    button.center.y;
6.视图层次
   视图子类是个数组;
   视图先添加的在下,后添加在上;
   交换视图位置:
  [button1 exchangeSubviewAtIndex:0 withSubviewAtIndex:1];
   1.遍历视图
 for (UILabel * l in button1.subviews) {
        l.text=[NSString stringWithFormat:@"%d",i++];
    }
   2.插入视图
   [button1 insertSubview:label3 atIndex:1];
     将视图label3插入到视图button1的第二位;
    [button1 insertSubview:label2 aboveSubview:label1];
     将视图label2插到label1上;
         [button1 insertSubview:label1 belowSubview:label3];
     
   3.将视图从父类视图中移出;
   [label2 removeFromSuperview];
   4.[button1 delete:label2];
   5.将视图放到最上层 ;
    [button1 bringSubviewToFront:label1];
    注:写代码时把子视图写到一块,父视图写到一块
   6.把视图放到最上下层
        [button1 sendSubviewToBack:label1];
   7.判断某个视图是某个视图的子视图 返回类型是bool类型;
     [label1 isDescendantOfView:button1];
7.tag 视图的标志; 可以标志视图。是set 和get函数,设置和取得可以用“.”操作
    button1.tag=1;
8.label1.superview 表示label1的父视图;
9. 隐藏视图 :label1.hidden=YES; 默认为no。
     应用:可以让视图重叠换灯片放映,或动画;如果父视图隐藏,子视图也随之消失
10.视图可能会超出父视图的范围;这时可以更改父视图边界属性裁减出边界  的子视图
   button1.clipsToBounds=YES;
 
  UIImageView
 
1.初始化:
     UIImageView *imageview=[[UIImageView alloc] initWithImage:[UIImage     imageNamed:@"2.jpg"]];
 
    imageview.frame=CGRectMake(20, 20, 140, 220);
    imageview.contentMode=UIViewContentModeScaleAspectFit;
 让添加的图片按比例填充;
3.imageview和label都不能触发事件 但是可以给他们添加事件
//点击事件   搞清楚是单击还是双击  来处理事件    
//声明点击手势   初始化点击手势识别器
    当单击判断失败 在触发单击;先判断,不知道是单击还是双击                    
 
    UITapGestureRecognizer *tgr=[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onclick)];//来添加事件
        tgr.numberOfTapsRequired=1;//区分单击还是双击
                tgr.numberOfTapsRequired=2;
    [tag addTarget:self action :@selecter(press:)];//事件
    //创建敲击事件的接收器;
    [imageview addGestureRecognizer:tgr];//这个视图来响应,把手势加到视图上面
4.
    imageview.UserInteractionEnabled=YES;
   //是否能和用户交互;
5.三中图片填充方式
     imageview.contentMode=UIViewContentModeScaleAspectFit;
     
    imageview.contentMode=UIViewContentModeScaleAspectFill;
   //按比例添满会超出边界。所以如果超出可以裁减
    imageview.clipsToBounds=YES;
    imageview.contentMode=UIViewContentModeScaleToFill;
    //添满
6.创建视图还可以在appDelegate.m中的application中声明实现
7.  子视图按某中形式随父视图扩大或缩小;不同的扩大缩小方式特点可以或   
  labelsub.autoresizingMask=UIViewAutoresizingFlexibleWidth;
  labelsub.autoresizingMask=UIViewAutoresizingFlexibleWidth |     UIViewAutoresizingFlexibleHeight;
      控制视图按比例的开关;
   labelsub.autoresizesSubviews=NO;
 
MVC:设计思想: model:内存数据 view :可视控件;controller:控制代码;
单例:从头到尾使用的类
RootController *rvc=[[RootController alloc]initWithNibName:@"RootController" bundle:nil];//有xib文件时创建连接文件,以后最好没有xib文件,xib文件降低速度
以后初始化要不创建xib文件初始化方法是:
RootController *rvc=[[RootController alloc]init];
kvo/kvc:?
notification:?
窗口:
iphono内存占用量不超过20Mb
全屏做法    1.记下此时的控件fram
                    2.修改控件的大小,使其全屏
                    3.把改图像拉到顶层;
                    4.修改tag
字体新设置:
label.font=[UIFont fontWithName:@"宋体" size:30];
 
timer
1.创建定时器,一但创建就开始运行;
 NSTimer *timer=[NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(changLabel) userInfo:nil repeats:YES];
2,定时器停止;一旦停止就释放,相当于release;
 [timer invalidate];
3,打开定时器
     [timer fire]
4.动画效果
   
 
'
UITextField:对话框  同样是个view
  
1.创建
UITextField *field=[[UITextField alloc] initWithFrame:CGRectMake(40, 20, 250, 60)];
 
  2.设置边框
    UITextField *field=[[UITextField alloc] initWithFrame:CGRectMake(40, 20, 250, 60)];
3.在文本框后面设置清除键
field.clearButtonMode=UITextFieldViewModeAlways;
4.设置初始文本
 field.placeholder=@"username";
5。设置文本变为隐藏;
 field.secureTextEntry=YES;
6.设置textfield的自定义函数
    首先在.h文件中提交协议  <UITextFieldDelegate>
  这样就可以自定义textfield 函数;注:自定义函数如果是返回bool类型的函数说明这个函数是一个监听函数;自定义都是进行某些动作时自动调用;
7.-(void)textFieldDidBeginEditing:(UITextField *)textField
  当文本框进入编辑模式时自动调用
8.退出编辑模式时调用函数;
  -(void)textFieldDidEndEditing:(UITextField *)textField
{
    ( (UILabel *)[self.view.subviews objectAtIndex:0]).text=textField.text;
 
}
9.点击键盘上return后调用的函数
-(BOOL)textFieldShouldReturn:(UITextField *)textField
10.关闭键盘函数;
 
   [textField resignFirstResponder];
    return NO;
 
11.限定输入字符的长度函数
-(BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{  //textfield表示谁调用该函数,
      
    if (string.length>0) //string 表示当前输入的字符;
        return textField.text.length<10;  //而textField.text的值是还没有加入刚输入的string
    return YES;
 
   }
 
 
9.10 第九周
多控制器操作:
 一.多控制器间的切换;
    思想:1.在每个视图控制器界面添加按钮点击触发事件创建新的视图控制器;然后把当前控制器交给新建的控制器;
                 2.在新建的视图控制器中等使用完该控制器后,把控制器释放;由于创建是在前一个控制器中释放的所以这里释放不是简单的release;
    方法:一  1.在父控制器中创建新控制器,并把但前的控制界面交给新控制器
               ViewController* vc2=[[ViewController2 alloc] init];
                   vc2.view.frame=CGRectMake(0,0,320,460);
            [self.view addSubview:vc2.view];
            [self addChildViewController:vc2];
                  注:由于这样vc2 是在原控制器上加载所以它会默认流出原视图导航栏,要想让新控制器全部占满屏幕就要更改视图的大小;
                2.  然后用完vc2在最后把vc2从原控制器中移出
            [self.view removeFromSuperview]; 
            [self removeFromParentViewController];
                      注:由于创建vc2时原有的控制器可vc2关联,原有的view和vc2的view关联;所以在清理时得把这两项分别清理;
                      注:这中添加清理的方法使用高版本;为了程序的适用性通常用第二种方法;
            二: 1.在父控制器中创建新控制器;并把当前的控制器设置为vc2,并添加动画效果
                       ViewController* vc2=[[ViewController2 alloc] init];
                        [self presentModalViewController:vc2 animated:YES];
           //添加新的controller而且带动画效果;animated:是否要动画效果
                    2 在用完vc2时移出该控制器;
                       [self dismissModalViewControllerAnimated:YES];
           //释放当前的controller
动画效果:只要把新的位置改变放到这些代码之间;
[UIView beginAnimations:nil context:nil];//动画设置开始;
    [UIView setAnimationDuration:0.2];//设置动画时间;
    text.frame=CGRectMake(<#CGFloat x#>, <#CGFloat y#>, <#CGFloat width#>, <#CGFloat height#>)//对象的新状态;
    [ UIView commitAnimations];//开始动画;




9。11周二
注:系统对象不开辟空间不能打印retaincount自己声明的类的对象不开辟空间可以打印retaincount
导航条UINavigationBar
1.导航条按钮初始化三种方法:
      1.普通初始化
      UIBarButtonItem *rigthButton=[[[UIBarButtonItem alloc] initWithTitle:@"next" style:UIBarButtonItemStylePlain target:self action:@selector(click:)] autorelease];
    第一个参数是按钮标题;第二个参数是按钮的样式;第三个参数是触发事件调用那个类中的方法第四个参数是触发的事件
   2.用图片初始化
     UIBarButtonItem *rightButtton= [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"5.png"] style:UIBarButtonItemStylePlain target:self action:@selector(click:)];
    注:这样初始化的按钮有按钮框,图片存在按钮框里要想只显示图片要自定义按钮然后用按钮初始化;如第三种方法;
   3.自定义控件初始化
 
//自定义的透明按钮  上面一般是要加上图片 让后点击图片
          UIButton *button=[UIButton buttonWithType:UIButtonTypeCustom];
    [button setImage:[UIImage imageNamed:@"5.png"] forState:UIControlStateNormal];
     创建button 按钮,样式设置为普通,也就是没有样式;
    button.frame=CGRectMake(0, 0, 30, 30);
    注:必须设置button 的frame否则不显示
   [button addTarget:self action:@selector(click:) forControlEvents:UIControlEventTouchUpInside];
    给button添加事件
    UIBarButtonItem *rightButton=[[UIBarButtonItem alloc] initWithCustomView:button];
    初始化导航条button
   4.运用系统功能按钮初始化uibarbutton
    UIBarButtonItem *rightButton=[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCamera target:self action:@selector(click:)];
 
2.添加副标题//导航栏添加副标题
   self.navigationItem.prompt=@"subtile"; 导航条高度是44;
3.把导航条按钮设置成数组;可多加按钮,按钮一多会把别的控件挤偏;但是不能覆盖;如果加的太多只能显示先加入的;
       self.navigationItem.rightBarButtonItems=[NSArray arrayWithObjects:<#(id), ...#>, nil]
    
  注:UIBarButtonItem 有些属性和unbutton不一样,UIBarButtonItem不能设置
//设置导航栏的颜色
4.设置导航条色调
  // tintColor:色调  控制器的导航条 的    色调
   self.navigationController.navigationBar.tintColor=[UIColor purpleColor];
5.颜色色透明度
    1.黑色透明度
self.navigationController.navigationBar.barStyle=UIBarStyleBlackTranslucent;
   2.其他颜色透明度
   self.navigationController.navigationBar.translucent=YES;
    self.navigationController.navigationBar.alpha=0.8
 
slider.frame=self.navigationController.navigationBar.bounds;//从边界开始设定大小
重点
6.更改导航条背景
   //更改背景图片ios5.0以后的方法
    //判断。如过是5.0以上的版本用以下方法;
    if (__IPHONE_OS_VERSION_MIN_REQUIRED>=__IPHONE_5_0) {
 
    [self.navigationController.navigationBar setBackgroundImage:[UIImage imageNamed:@""] forBarMetrics:UIBarMetricsDefault];
    }
   如果是5.0以下的可以创建类别
   //通过类别创建图像
     @implementation UINavigationBar(Custom)
      -(void)drawRect:(CGRect)rect{
      UIImage *image=[UIImage imageNamed:@""];
      [image drawInRect:rect];
     }
7.隐藏导航条
  [self.navigationController setNavigationBarHidden:YES animated:YES];
8.直接跳转到某页
    1.创建数组把控制页面全部加载进来;
   NSMutableArray *array=[NSMutableArray arrayWithArray:self.navigationController.viewControllers];
  2.设置跳转到哪页
          [self.navigationController popToViewController:[array objectAtIndex:0] animated:YES];
    能够设置的常项
        创建一个可变空数组;
     NSMutableArray *array1=[[NSMutableArray alloc]initWithCapacity:0];
   用数组设置navigationController的ViewControllers
   //[self.navigationController setViewControllers:array];
    self.navigationController.viewControllers=array;
9.在子导航条中去除自带得返回上一级得button
self.navigationItem.hidesBackButton=YES;
ToolBar工具条
1.创建工具条:为单个viewcontroller添加toolbar
     UIToolbar *toobar=[[UIToolbar alloc] initWithFrame:CGRectMake(0,372,320, 44)];
      注:一定要注意创建导航条后当前的viewcontroller的坐标原点已经改变是加上导航条后的的高度;导航条的高度是44;
   toolbar.tintColor=[UIColor purpleColor];
   toolbar的颜色设置;和导航条设置一样;
    toobar.hidden=NO;
      注:toolbar的hidden属性默认是yes,所以加toolbar要把hidden设置为no;
2.  在导航条中有toolbar属性;所以要想每页都加toolbar 就可以在这里把导航条的toolbar的hidden属性设置为no;
   ];
    UINavigationController *nv=[[UINavigationController alloc]initWithRootViewController:vc];
    nv.toolbarHidden=NO;
    nv.toolbar.tintColor=[UIColor purpleColor];
    [nv setNavigationBarHidden:YES animated:YES];
3.把按钮写成一个数组然后添加到toolbar
   [array addObject:button];
   //设置viewcontroller的toolbar以数组初始化;
    //toobar的button不能调整大小;
   // self.toolbarItems.
    [self setToolbarItems:array animated:YES];
 
4.验证两个coloer属性;
  tinkcoloer:
 
9.12 周三
//六个控件
 UISlider 滑条控件//滑动条控件
 滑条初始化:
    UISlider *sl=[[UISlider alloc]initWithFrame:CGRectMake(30, 60, 280, 30)];
    1.滑条的高度不用设置因为系统默认 ;但是可以设置背景图改变滑条的高度;具体看滑条的图片设置
 
       创建label用来显示滑动的数值
    UILabel *label=[[UILabel alloc]initWithFrame:CGRectMake(0, 0, 320, 40)];
     view控件的tag属性可以作为区分视图的标志想查找它时可以在另一个函数中写
     UILabel *label=(UILabel *)[self.view viewWithTag:100];
    label.tag=100;
  
  设置滑条的最大值和最小值;
    sl.minimumValue=0.0;
    sl.maximumValue=20.0;
 
  给滑条添加事件,由于滑条的值是可变的所以监听它时用值改变事件监听,以后一般遇到值可以不断改变的view控件要用值改变事件监听;
    [sl addTarget:self action:@selector(valuechang:) forControlEvents:UIControlEventValueChanged];
   // 设置默认滑条位置;
    sl.value=10.0;
    设置大小端图像;
   sl.maximumValueImage=[UIImage imageNamed:@"5.png"];
   设置滑条轨迹的颜色
    sl.minimumTrackTintColor=[UIColor purpleColor];
    sl.maximumTrackTintColor=[UIColor redColor];
    sl.thumbTintColor=[UIColor greenColor];
 
    //点击按钮分高亮状态和非高亮状态;所以要注意设置那个状态;
    [sl setThumbImage:[UIImage imageNamed:@"5.png"] forState:UIControlStateNormal];
 
    // 设置滑条的最大最小图像;注:1滑点滑动部分变大图像会被拉伸,如果图像到达原大小在压缩不会变化;2.如果滑条设置背景图片就会改变滑条的宽度,而且只 能是改变minimumValueImag此时的图片高度就是滑条的高度;原因:因为滑条的大端是开始的占空间大小;显示的滑动进度是在原有基础上添加的 所以大端改变整个滑条才会改变
    [sl setMinimumTrackImage:[UIImage imageNamed:@"5.png"] forState:UIControlStateNormal];
    [sl setMaximumTrackImage:[UIImage imageNamed:@"5.png"] forState:UIControlStateNormal];
     
简单的滑条触发事件测试:滑条值的改变显示在label控件上;  
-(void)valuechang:(UISlider *)sender
{
    UILabel *label=(UILabel *)[self.view viewWithTag:100];
    label.text=[NSString stringWithFormat:@"%.0f",sender.value];
}
 
UISwitch:开关控件//创建开关控件
 初始化switch对象
UISwitch * _swith=[[UISwitch alloc] initWithFrame:CGRectMake(100, 100, 0, 0)];
   给with添加事件注意 事件控制是UIControlEventValueChanged
    [_swith addTarget:self action:@selector(sw:) forControlEvents:UIControlEventValueChanged];
     设置with的打开时背景色
    _swith.onTintColor=[UIColor blackColor];
 注:不用设置switch的宽高。因为时系统默认;设置没有作用;
给switch添加switch事件:状态改变一次触发一次
-(void)sw:(UISwitch *)swith
{
    if (switch.on==YES)
        NSLog(@"yes");
        else
            NSLog(@"no");
     
}
 
 
 UIActivityIndicatorView:加载缓控件
  
//加载缓冲页面;初始化
    UIActivityIndicatorView *aiv=[[UIActivityIndicatorView alloc] initWithFrame:CGRectMake(100, 100, 0, 0)];
    
   设置当停止时隐藏
    aiv.hidesWhenStopped=NO;
    设置旋转图标的样式。一共有三种
    aiv.activityIndicatorViewStyle=UIActivityIndicatorViewStyleWhiteLarge;
    设置缓冲图标的颜色   注:要先设置样式在设置颜色,因为样式是带颜色,能覆盖以前的颜色;
    aiv.color=[UIColor blueColor];
    缓冲图标开始,停止动画;
    [aiv startAnimating];
    [aiv stopAnimating];
 
 
 UIProgressView:进度条
  初始化进度条
    UIProgressView *pgv=[[UIProgressView alloc] initWithFrame:CGRectMake(50, 100, 220, 0)];
   设置进度条的tag属性;
    pgv.tag=100;
    pgv.progressViewStyle=UIProgressViewStyleBar;
    //颜色设置
    pgv.progressTintColor=[UIColor purpleColor];
    pgv.trackTintColor=[UIColor blackColor];
    //进度条图片设置
    pgv.progressImage=[UIImage imageNamed:@""];
    pgv.trackImage=[UIImage imageNamed:@""];
   //用时间控制器模拟时间进度条;
    [NSTimer scheduledTimerWithTimeInterval:0.2 target:self selector:@selector(refresh:) userInfo:nil repeats:YES];
   //用函数对进度条做试验
-(void)refresh:(NSTimer *)timer
{
    UIProgressView *pv=(UIProgressView *)[self.view viewWithTag:100];
    [pv setProgress:pv.progress+0.1 animated:YES];
    if (pv.progress==1) {
        NSLog(@"%d",[timer retainCount]);
        [timer invalidate];
       // [timer release];
       //注意在ui中运用封装好的函数生成的控件不要给他们release它有他自动释放或需要调用释放函数;所以不必管它
        NSLog(@"%d",[timer retainCount]);
    }
 
 UITextView 文本编辑器
 文本编辑器和文本框一样有代理函数在.h文件开头需引入;
 @interface ViewController : UIViewController<UITextViewDelegate>
 它的代理函数和文本框的基本相同,不同的有两个
-(void)textViewDidChange:(UITextView *)textView:(UITextView *)textView
-(void)textViewDidChangeSelection:(UITextView *)textView
注:在调用代理函数时一定要textview的代理设置为调用函数所在的类;
文本框的一些属性设置:
     1.初始化文本框
   UITextView *textView=[[UITextView alloc]initWithFrame:CGRectMake(0, 0, 320, 100)];
   2.文本框的颜色,对齐方式,字体设置
    textView.textColor=[UIColor grayColor];
    textView.textAlignment=UITextAlignmentLeft;
    textView.font=[UIFont systemFontOfSize:30];
    3。自动纠错关闭;
    textView.autocorrectionType=NO;
   4.大写模式;除此之外还可以设置字母不大写;句首字母大写。单词开头字母大写;  textView.autocapitalizationType=UITextAutocapitalizationTypeAllCharacters;
   5.键盘模式:
   textView.keyboardType=UIKeyboardTypePhonePad;//打电话模式
    textView.returnKeyType=UIReturnKeyGo;返回模式变为go
     //根据输入框需要输入的内容不同要设置键盘的样式
 
 
 UIStepper 计步器//创建一个计步器
  1.初始化
//ValueChanged  点击是要用这种模式
    UIStepper *stepper=[[UIStepper alloc]initWithFrame:CGRectMake(100, 200, 100, 40)];
 2.给stepper添加触发事件,还是注意 forControlEvents:UIControlEventValueChanged的设置
    [stepper addTarget:self action:@selector(stepper:) forControlEvents:UIControlEventValueChanged];
 3.设置stepper的最大最小值
    stepper.minimumValue=0;
    stepper.maximumValue=50;
 4.
    //常按可以自动加默认是yes    长按就一直进行
  stepper.autorepeat=YES;//autorepeat是否连续的执行这一操作或者响应
    //最小和最大间循环;
   stepper.wraps=YES;
    //设置每次增长值
    stepper.stepValue=2;
    //设置按键时是否显示每次加的值;如果设为no常按键会加到走好几个步长的值
   stepper.continuous=NO;//continue单位时间内是否响应
UILabel *label=[[UILabel alloc]initWithFrame:CGRectMake(50, 50, 100, 50)];
    label.font=[UIFont systemFontOfSize:30];
    [self.view addSubview:label];
    label.tag=100;
 
-(void)stepper:(UIStepper *)stepper
{
    UILabel *label=(UILabel *)[self.view viewWithTag:100];
    label.text=[NSString stringWithFormat:@"%f",stepper.value];
 
}
 
 UISegmentedControl 分段控制器;//创建分段控制器
   1.分段控制器初始化
 UISegmentedControl *sc=[[UISegmentedControl alloc]initWithItems:[NSArray arrayWithObjects:@"a",@"b",@"c",[UIImage imageNamed:@"5.png"], nil]];
 2 分段控制器设置frame
    sc.frame=CGRectMake(100, 100, 150, 40);
 3. 为分段控制器添加监听事件;
    [sc addTarget:self action:@selector(sc:) forControlEvents:UIControlEventTouchUpInside];
    //注这个触发事件监听是ValueChanged,为了监听按钮
4。控制类型控制类型一共有四种;
    sc.segmentedControlStyle=UISegmentedControlStyleBordered;
 5。按钮自动弹起开启;默认是no;
    sc.momentary=YES;
 6。添加新按钮;可以是文字,或图像;
    [sc insertSegmentWithTitle:@"e" atIndex:3 animated:YES];
   [sc insertSegmentWithImage:<#(UIImage *)#> atIndex:<#(NSUInteger)#> animated:<#(BOOL)#>]
7.在段控制器中区分点击那个按钮的个属性
//一共有多少个按钮;
    int i=sc.numberOfSegments;
//当前点击的按钮位置;
    int j=sc.selectedSegmentIndex;
    //移出当前点击的段;
    [sc removeSegmentAtIndex:j animated:YES];
    //移出所有的段;
   [sc removeAllSegments];
 
UIGesture 手势事件
1.移动手势
     1.创建移动手势的对象:
 UIPanGestureRecognizer *pan=[[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(pan:)];
  2.把此对象添加到操作的对象中;
    [imagview addGestureRecognizer:pan];
  3.实现移动对象的方法
-(void)pan:(UIPanGestureRecognizer *)pan
{
    CGPoint point=[pan translationInView:self.view];
   //这个函数是得到触点在屏幕上与开始触摸点的位移值
    imagview.center=CGPointMake(imagview.center.x+point.x, imagview.center.y+point.y);
  //让图像的中心位置变化;中心变化图像位置就移动了 ,但是注意位移时如果不清空translation就会多移动一倍;所以每次要让pan的位置零;
    [pan setTranslation:CGPointMake(0, 0) inView:self.view];
     
}
2.扩大缩小手势
  1.创建扩大缩小手势的对象 在创建时就带了要触发的事件  手势器
UIPinchGestureRecognizer *pinch=[[UIPinchGestureRecognizer alloc]initWithTarget:self action:@selector(pinch:)];
 2. 记录下开始图像的大小,在扩大缩小函数时使用;原因是扩大尺寸是在图像的现有大小上乘,而图像的尺寸是变化的所以会使图像扩大缩小的太过火;所以纪录原图 像大小一直在这个基础上扩大 size=CGSizeMake(imagview.bounds.size.width,imagview.bounds.size.height );
3.图像添加扩大缩小手势;
    [imagview addGestureRecognizer:pinch];
4.图像扩大缩小手势触发的事件;
-(void)pinch:(UIPinchGestureRecognizer *)pinch
{
    改变图像的大小;pinch.scale是触摸点的相对于原有位置的位移量的倍数。
    imagview.bounds=CGRectMake(0, 0,size.width*pinch.scale, size.height*pinch.scale);
    为了防止每次扩大缩小都要从原图开始,所以操作结束释放触点一次就要把当前的尺寸记下,下次变化在这个基础上改变;
                                         
    if (pinch.state==UIGestureRecognizerStateEnded) {
        size=CGSizeMake(imagview.bounds.size.width, imagview.bounds.size.height);
    }
 
                                            gesture.scale=1.0f;//这个很重要
  
3.旋转手势
//电话在手机里面优先级是最高的
 
  1.旋转角度对象的初始化
    UIRotationGestureRecognizer *rotation=[[UIRotationGestureRecognizer alloc]initWithTarget:self action:@selector(rotation:)];
  2.给旋转图像添加旋转对象
    [imagview addGestureRecognizer:rotation];
  3.旋转时触发的函数
-(void)rotation:(UIRotationGestureRecognizer *)rotation

      让图像旋转某个角度,这个交度使相对原图旋转的角度。所以必须有个判断语句,如果释放一次触点就要把当前位置记下,否则图像还会回到原来状态;
    imagview.transform=CGAffineTransformMakeRotation(f+rotation.rotation);
    if (rotation.state==UIGestureRecognizerStateEnded) {
        f=f+rotation.rotation;
    }
}
4.tap的一些属性
  可以给image添加tap事件
  tap.numberOfTapsRequired=2;//点击几次触发事件,点击次数必须是连续点击
   tap.numberOfTouchesRequired=2;//设置触摸点数 只有两个触点存在时点击才能触发事件
5.触摸时自动调用的触摸函数一共有四个
     触点事件调用时UITouch对象产生后自动调用的属性;由于单击的话只有一共对象;所以不论有几个touch对象都时指当前鼠标触点?那为什么还要创建触点对象呢?而anyobject之外还能做什么
    1. 触摸开始触发的事件
//开始触屏的时候touchesBegan
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    NSLog(@"began");
   建立触摸对象 触摸对象是任意触点;但是事实是当前鼠标点击的点;但是怎么得到另一个点的坐标?
    UITouch *touch=[touches anyObject];//anyObject  任何触摸的东西都储存在里面
    if(touch.tapCount==1)
    {
    NSLog(@"这是一个单击事件");
    }
    else if(touch.tapCount==2)
    {
    NSLog(@"这是一个双击事件");
}
     
    CGPoint point=[touch locationInView:self.view];
    NSLog(@"%f %f",point.x,point.y);
   // 可以调用touch开始点和touch结束点做加减得到移动大小, 但是这样做很麻烦,ui有封装好的方法       UIPanGestureRecognizer
   // touch只调用第一触电大小,怎么得到第二个点?}
 
//手指离开屏幕的时候
   2.触摸事件结束触发事件
    -(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
   {
    NSLog(@"end");
    }
  3.cancell实在移动时突然被其他进程打断时调用。比如突来电话
      -(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
    {
    NSLog(@"cancel");
    }
  4.触点移动时触发的事件;
//手指移动发生的事件
   -(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
   {
        UITouch *touch=[touches anyObject];
    CGPoint point=[touch locationInView:self.view];
    UIImageView * imageView=(UIImage *)[self.view viewWithTag:101];
    CGRect
 
    NSLog(@"move");
   }
 
 [performSelector:@selector(single Tag)withObject:nil afterDelay:delaytime]//实现函数的调用,延时调用,相当于多线程
 
9.13周四
1.各个控制器间的关系:
window  的rootviewcontroller可以是uinavigationcontroller ,uitabbarcontroller ,uiviewcontroller
uinavigationconroller 上可以添加uiviewcontroller
uitabbarcontroller上可以添加uiviewcontrolller 也可以添加uinavigationcontroller
到此基本上控制器的关系弄清了;
 1.UITabBarController :标签控制器,分栏控制器
     当uitabbarcontroller含的uitabbaritem很多时,编译器会自动隐藏以省略号表示,点击省略号就可以查看不显示的item
    1.初始化:
   UITabBarController *tc=[[UITabBarController alloc]init];
  2.创建视图控制器然后创建 UITabBarItem并把它赋给视图控制器的 tabBarItem
   ViewController *vc=[[[ViewController alloc]init]autorelease];
    UITabBarItem *item=[[UITabBarItem alloc]initWithTabBarSystemItem:UITabBarSystemItemSearch tag:0];
    注:可以自定义item 也可以用系统中定义好的;
    vc.tabBarItem=item;
   3.创建导航条;用于放人UITabBarController 由此可知uitabbarcontronller和uinavigationcontroller的包含关系;
     UINavigationController *nc=[[UINavigationController alloc]initWithRootViewController:vc];
   4.添加导航控件和视图控件;
   tc.viewControllers= [NSArray arrayWithObjects:nc,vc1,vc2, nil];
   5.让根控制器为标签控制器
    self.window.rootViewController=tc;
2.解决不显示第二个以后的标签问题
      1.原因:如果把每个控制器的标签生成写在viewdidload里面由于为了节省内存开始只会加载一个控制器。所以只能显示第一个标签;
   ViewController1 *vc1=[[[ViewController1 alloc]init]autorelease];
    //注如不使用vc1显示是没有其图,因为还不加载它的程序
    vc1.view.backgroundColor=[UIColor blueColor];
     2.解决方法:1.init函数中生成控制器标签生成代码;在创建控制器时就会执行;
                            2. 在appdelegate中生成控制器的标签
                            3.在appdelegate 中调用控制器的函数
3.本地存储: NSUserDefaults:一旦运行数据就会被存储下来,每次运行程序以前的纪录下来的数据仍然有;
  //只能保存NSString  NSNumber NSData,NSDictionary; NSArray;这五种类型;(其他的是需要强转的  类似于 图片的)
  //保存到本地,一旦保存,再运行还有数据,所以可以达到数据在不通页面间的传递
    NSUserDefaults *userDefault=[NSUserDefaults standardUserDefaults];
   //为本地存储添加数据;类似于字典的用法
    [userDefault setObject:@"adba" forKey:@"key"];
  //同步操作,保存到本地;
     [userDefault synchronize];   同步了才可以保存到本地的
   //获取以前存储的数据;
    NSString * str=[userDefault objectForKey:@"key"];//根据关键字取出对应的用户默认数据
    NSLog(@"%@",str);
 
//实现UITabBarControllerDelegate代理的方法
- (void)tabBarController:(UITabBarController *)tabBarController didSelectViewController:(UIViewController *)viewController
{
    NSLog(@"You selected: %d",tabBarController.selectedIndex);
     
    //只是一个单例
    NSUserDefaults *ud=[NSUserDefaults standardUserDefaults];//用来存储数据的数据库 创建一个指针来存储和取出数据
    [ud setInteger:tabBarController.selectedIndex forKey:@"LAST_PAGE"];
    [ud synchronize];//将新添加的数据存盘
}
//下面4个方法  不是属于类的方法 是系统本来已经提供的方法    只跟视图控制器的出现和消失有关系的
 
 
//视图将要出现的时候
 
- (void)viewWillAppear:(BOOL)animated;    // Called when the view is about to made visible. Default does nothing
 
//视图已经出现的时候
- (void)viewDidAppear:(BOOL)animated;     // Called when the view has been fully transitioned onto the screen. Default does nothing
//视图将要消失的时候
- (void)viewWillDisappear:(BOOL)animated; // Called when the view is dismissed, covered or otherwise hidden. Default does nothing
//视图已经消失的时候
- (void)viewDidDisappear:(BOOL)animated;  // Called after the view was dismissed, covered or otherwise hidden. Default does nothing
 
 
4.数据字典:NSDictionary:用于存储数据可以和数组联合起来用;
1.字典的初始化方法
    1.直接用数值初始化:
   NSDictionary *dic1=[[NSDictionary alloc]initWithObjects:[NSArray arrayWithObjects:@"",@"", nil] forKeys:[NSArray arrayWithObjects:@"",@"", nil]] ;
  2.用数组初始化
    NSArray *array=[[NSArray alloc]initWithObjects:@"23",@"100",@"zhang" ,nil];
    NSArray *array2=[[NSArray alloc] initWithObjects:@"年龄",@"体重",@"姓名", nil];
    NSDictionary *dic1=[[NSDictionary alloc]initWithObjects:array forKeys:array2];
  3.用其他字典初始化
      NSDictionary *dic=[[NSDictionary alloc]initWithDictionary:dic1];
  4.用网络URL初始化
    NSDictionary *dic5=[NSDictionary alloc]initWithContentsOfURL:<#(NSURL *)#>
2.通过key来找到对应的值;
    NSLog(@"%@",[dic objectForKey:@"key"]);
   
3.创建动态字典 用法和普通字典一样;
    NSMutableDictionary *dic3=[NSMutableDictionary dictionaryWithCapacity:0];
4.为动态字典添加内容;
    [dic2 setObject:@"180" forKey:@"身高"];
     [dic2 setObject:@"56" forKey:@"weight"];
     [dic2 setObject:@"13" forKey:@"age"];
 
NSNumber 数值对象
1.初始化数值对象 用一个基础数据类型初始化,如整形,浮点,字符都可以;
  NSNumber *num=[NSNumber numberWithInt:vc.tabBarItem.tag ];
2.把数值对象添加到数组中;
    [array addObject:num];
3.把nsstring类型转换成值;和把数值类型转换成整形
     int i=1;
        NSString *str=[NSString stringWithFormat:@"%d",i];
        int a=[str intValue];//其他数值形式的也可以类似转换   如float b=[str floatValue];
 
UITabBarController的代理函数;
uitabbarcontroller 中当item很多时会有省略;还可以调整uitabbaritem的排放顺序调整在主页显示的页面;这时会需要tabbarcontroller 的代理类
1.当某个viewcontroller被选择时触发的事件;
   应用:每当页面显示时有些提示信息……..
-(void)tabBarController:(UITabBarController *)tabBarController didSelectViewController:(UIViewController *)viewController
{
    NSLog(@"didSelectViewController");
   // NSLog(@"%d",tabBarController.tabBarItem.tag);
    NSLog(@"%d",viewController.tabBarItem.tag);
}
2.在某个viewcontroller选择时触发的事件
-(BOOL)tabBarController:(UITabBarController *)tabBarController shouldSelectViewController:(UIViewController *)viewController
{
         
    NSLog(@"shouldSelectViewController:");
    NSLog(@"%d",viewController.tabBarItem.tag);
    return YES;
}
3 将要结束用户自定义item时;既点击导航栏中的done时调用用法:可以提示用户确实要这样更改吗。
-(void)tabBarController:(UITabBarController *)tabBarController willEndCustomizingViewControllers:(NSArray *)viewControllers changed:(BOOL)changed
{
    NSLog(@"willEndCustomizingViewControllers");
    //NSLog(@"%d",viewControllers)
    NSLog(@"%d",changed);
}
4.将开始进行用户自定义item时调用;既点击了导航栏中的edit时调用此函数;
-(void)tabBarController:(UITabBarController *)tabBarController willBeginCustomizingViewControllers:(NSArray *)viewControllers
{
    }
5.结束用户自定义item后调用既单击了done ;用法可以纪录此时的新的item用于永久性更改,下次运行此程序保持这个状态;
-(void)tabBarController:(UITabBarController *)tabBarController didEndCustomizingViewControllers:(NSArray *)viewControllers changed:(BOOL)changed
{
    NSMutableArray *array=[NSMutableArray arrayWithCapacity:0];
    for (UIViewController *vc in viewControllers) {
        NSNumber *num=[NSNumber numberWithInt:vc.tabBarItem.tag ];
        [array addObject:num];
       // NSLog(@"%d",vc.tabBarItem.tag);
         
        //把nsstring转换成数据类型
//        int i=1;
//        NSString *str=[NSString stringWithFormat:@"%d",i];
//        int a=[str intValue];
    }
    NSUserDefaults *userdefault=[NSUserDefaults standardUserDefaults];
    [userdefault setObject:array forKey:@"num"];
 
    NSLog(@"didEndCustomizingViewController");
}
如何保存用户更改的uitabbaritem位置?
 1. 在用户用编辑item顺序后执行done,这时在调用的didEndCustomizingViewControllers 函数传递的参数viewControllers 纪录了此时新的排列顺序;这时要用本地存储记下这个顺序;注意因为每个viewcontroller在初始化时设置了自己的tag值,所以只记下tag值 就可以找到对应的viewcontroller
-(void)tabBarController:(UITabBarController *)tabBarController didEndCustomizingViewControllers:(NSArray *)viewControllers changed:(BOOL)changed
{
    初始化可变数组用于存储tag值
    NSMutableArray *array=[NSMutableArray arrayWithCapacity:0];
    遍历新设置的控制器顺序;并把tag值按顺序存储到可变数组中
    for (UIViewController *vc in viewControllers) {
        NSNumber *num=[NSNumber numberWithInt:vc.tabBarItem.tag ];
        [array addObject:num];
}   定义本地存储对象;
    NSUserDefaults *userdefault=[NSUserDefaults standardUserDefaults];
    把可辨数组存入到本地存储中
    [userdefault setObject:array forKey:@"num"];
    NSLog(@"didEndCustomizingViewController");
}
2. 在加载函数中可以先判断本地存储是否有数据;如果没有就是用户还未设置过,那item顺序要按加载顺序排列;如果非空的话就把本地存储的数据的赋值给viewcontrollers;
    NSMutableArray *array=[NSMutableArray arrayWithCapacity:0];
    for (int i=0; i<6; i++) {
        UIViewController *vc=[[UIViewController alloc]init];
       [array addObject:vc];
 
        UITabBarItem *item=[[UITabBarItem alloc]initWithTitle:[NSString stringWithFormat:@"vc%d",i+1] image:[UIImage imageNamed:@"1.png"] tag:i];
        vc.tabBarItem=item;
        [item release];
        [vc release];
           }
    UITabBarController *tabbar=[[UITabBarController alloc]init];
   tabbar.viewControllers=array;
注:没有alloc的对象一定不要release;如button一般创建不用alloc;注意注注意
 
9.14 周五
实现键盘弹起时页面也跟着弹起键盘的高度
1.基本思想:
     1. 创建两个通知用于监听键盘弹起和落下
              //面试重点:通知;通知中心是管理通知的;
     [[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];
    //监听者一般是哪个控件创建谁监听器就谁监听
         //name是监听的信号;
   //selecto监听到信号后触发的事件;
    //object 指监听某个对象,一般是全局监听所以是nil
    [[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil];
    2.在监听键盘弹起的事件中记下键盘高度,然后把view提高键盘高度
         
    -(void)keyboardWillShow:(NSNotification *)noti
{    键盘弹起时触发的事件;
    //获得键盘的高度, 将传递过来的消息信息纪录达字典;从字典中找到关键字为键盘框架的数据让然后把它转换成cgrect类型读取它的高度
    NSDictionary *dic=[noti userInfo];
    CGRect rect=[[dic objectForKey:UIKeyboardFrameEndUserInfoKey]CGRectValue];
    float h=rect.size.height;
    NSLog(@"%f",h);
    //设置动画 使键盘高度提高;
    [UIView beginAnimations:nil context:nil];
    [UIView setAnimationDuration:0.25];
    self.view.frame=CGRectMake(0, 20-h, 320, 460);
    //注意是20减去键盘的高度;因为view是加载在window上的它的frame是相对于window的。这就要注意了,a控件添加到b控件上a的frame就是相对于b的frame
    [UIView commitAnimations];

  3.收起键盘   键盘弹出和收起是随文本编辑开始和退出自动弹出和收起,所以要想让文本框退出编辑模式必须有个事件触发,而在touchesbegan的系统函数中只要鼠标点击就能触发,所以一般在这个里面让textfield退出编辑模式;
[ resignFirstResponder]; 就是textfield退出编辑模式的函数;
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITextField * textfield=(UITextField *)[self.view viewWithTag:100];
    [textfield resignFirstResponder];
}
 
 4.不使用键盘时让view以动画跳下来;
   -(void)keyboardWillHide:(NSNotification *)noti

    [UIView beginAnimations:nil context:nil];
    [UIView setAnimationDuration:0.20];
    self.view.frame=CGRectMake(0, 20, 320, 460);
    [UIView commitAnimations];
}
 
弹出消息框 
注:弹出消息框所用代理为UIAlertViewDelegate   这是提示框,  这个不用添加到窗口的!!!
  1.创建消息框对象
UIAlertView *alertview=[[UIAlertView alloc]initWithTitle:@"error" message:@"帐号错误" delegate:self cancelButtonTitle:@"取消" otherButtonTitles:@"确定", nil];
 让消息框展示出来
    [alertview show];//这是系统自动调用的函数
    [alertview release];
 2.在消息框对象的代理函数中执行操作 消息框有四个代理函数,点击函数最常用
-(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
      通过传递过来的button序号知道那个button进行了操作执行相应的命令
     //按钮从左边计数;
    if (buttonIndex==0) {
        self.view.backgroundColor=[UIColor yellowColor];
    }
    else
        self.view.backgroundColor=[UIColor brownColor];
}
 
从相册和相机获取图片
思想: 1.创建获取图片按钮和接收图像的的视图控件
                imageview =[[UIImageView alloc]initWithFrame:CGRectMake(100, 80, 100, 100)];
    [self.view addSubview:imageview];
    UIButton *button=[UIButton buttonWithType:UIButtonTypeRoundedRect];
    button.frame=CGRectMake(100, 190, 120, 20);
    [button addTarget:self action:@selector(buttonClick) forControlEvents:UIControlEventTouchUpInside];
   [self.view addSubview:button];
 
//提示框
     2.在button触发的事件中生成UIActionSheet对象去选择从哪里获取图片
       -(void)buttonClick
{
    UIActionSheet *as=[[UIActionSheet alloc]initWithTitle:@"选择图片来源" delegate:self  cancelButtonTitle:@"取消" destructiveButtonTitle:@"相机" otherButtonTitles:@"相册", nil];
  //可以生成多个按钮
    [as showInView:self.view];
   展示这个动作页面;
    [as release];
}
      3.通过调用UIActionSheet的代理函数获取图片;所以头文件必须声明他的代理一般用到是点击函数,通过传递按钮的排列序号知道那个按钮被点击触发相应的事件
       -(void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex
{
    if (buttonIndex==2) {
        return;
    }
// 取消按钮序号是2,所以如果指针是2,表明触发了取消,注意对应的序号要和创建时一致;
    UIImagePickerController *pc=[[UIImagePickerController alloc]init];
     
   //创建图片获取控制器;根据点击按钮不同从不同的资源获取图片;
    // 防止触发虚拟机不能完成的事件发生崩溃,提前做一下判断,如果不是虚拟机就执行操作;
     if(buttonIndex==0 && TARGET_IPHONE_SIMULATOR)
     {
         [pc release];
         return;
     }
    if(buttonIndex==0){
       pc.sourceType=UIImagePickerControllerSourceTypeCamera;
    }
    else if(buttonIndex==1)
    {
        //pc.sourceType=UIImagePickerControllerSourceTypePhotoLibrary;
         
        pc.sourceType=UIImagePickerControllerSourceTypeSavedPhotosAlbum;
    //区分这两个属性的不同第一个
    }
 
    pc.delegate=self;
    //这个对象设置代理需要遵从两个协议UIImagePickerControllerDelegate, 和导航控制协议 因为图片控制器也是个导航控制器UINavigationControllerDelegate
//把控制器交给图像资源获取控制器
     [self presentModalViewController:pc animated:YES];
    [pc release];
}
在系统中的图片获取器中点击按钮可以触发的事件
//点击图片调用的函数
-(void)imagePickerController:(UIImagePickerController *)picker didFinishPickingImage:(UIImage *)image editingInfo:(NSDictionary *)editingInfo
{
    imageview.image=image;
    [picker dismissModalViewControllerAnimated:YES];
}
//点击取消触发的事件
-(void)imagePickerControllerDidCancel:(UIImagePickerController *)picker
{
    [picker dismissModalViewControllerAnimated:YES];
 
}
 
9.17 第十周 周一
 
UIScrollView 滚动视图
注:苹果的内存复用机制:视图很大超过了屏幕显示的范围,在显示的只是屏幕上的内容,为了减少内存使用把不现实的那用从内存中去除;
1.滚动视图的初始化
   UIScrollView *sv=[[UIScrollView alloc]initWithFrame:CGRectMake(0, 0, 320, 460)];
   frame:是滚屏在视图上占用的大小;
2.给滚动视图添加控件时,如果控件有事件响应的话,系统设置了一个定时器,如果在限定事件内拖动的话,该控件就失去了响应机会,拖动视图也会跟着拖动;
UIButton *button=[UIButton buttonWithType:UIButtonTypeRoundedRect];
    button.frame=CGRectMake(100, 100, 100, 50);
    //注意按钮的响应时间,如果按钮在没响应前滑动,此时就相当于视图滑动
    
3.设置滚屏的大小,这个属性很重要   显示的是内容的大小   体现了滚动视图的经典
    sv.contentSize= CGSizeMake(320, 460*2);
   滚屏的真实大小;
4.初始位置设置    //偏移量   是一个点  有x值与  y值
    sv.contentOffset=CGPointMake(90, 90);  // contentOffset.x 与 contentOffset。y
//注:x,y是屏幕的框架原点相对于滚屏的原点的位置;当滚屏超出手机框架时这两个值比为正数;
5.竖直和水平滚动条是否显示;默认时显示滚动条//这条很重要 
    sv.showsVerticalScrollIndicator=NO;//是否显示竖直方向的滚动条
    sv.showsHorizontalScrollIndicator=NO;//是否显示水平方向的滚动条,默认是显示的 ,不显示设置成NO
6.反弹效果的设置;设为yes后可以反弹;设为no就不能反弹;
    sv.bounces=YES;
    //总是反弹,即使大小和边框一样也可以有反弹效果;
    sv.alwaysBounceHorizontal=YES;
    sv.alwaysBounceVertical=YES;
7.设置为不可滚动;即使大于框架
   sv.scrollEnabled=NO;
 
//添加图片视图,视图用addSubview的方法来添加,把图片添加到滚动视图上面
        [sv addSubview:imageView];
//再把滚动视图添加到  总试图上面
    [self.view addSubview:sv ];
 
//设置显示内容尺寸
    sv.contentSize=CGSizeMake(260*4, 400);
    //设置按照页码滚动
    sv.pagingEnabled=YES;//这样设置是保证 一页一页的显示出来 不可能只显示一半,用户不喜欢看到这个效果;
    //设置边缘弹动效果//主要是给用户感觉的
    sv.bounces=YES;//可以弹动的      也可以sv.bounces=NO;此时到边缘就不能滚动,不会出现一些不该出现的空白
    //改变滚动条偏移量  当前显示内容的偏移量;
    sv.contentOffset=CGPointMake(100, 0);
 
使用:代理的三部曲,每一个协议方法都是这么来做的
遵守协议(什么控制器遵守什么协议  写在尖括号里面)
委托代理(A.delegate=self)
实现方法(完成的方法);
 
8.滚动条的代理函数  
   1.当屏幕滚动时调用的函数
    -(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    NSLog(@"%f %f",scrollView.contentOffset.x,scrollView.contentOffset.y);
}
      2. 将要开始拖动时调用
-(void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
 
    3.滚动结束后调用  手指抬起开始减速的瞬间 开始回调  //用这个方法来实现拖动的时候 页的对应 
-(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
    NSLog(@"endDrag");
}
   4.滚屏结束时调用 
-(void)scrollViewDidEndDragging:(UIScrollView *)scrollView
 
 
   5.滚屏将开始减速时调用
-(void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView
 
 
用一个类来封装一下,封装就是派生出一个类,写自己的类,
 
9.用滚动视图来显示图片的缩放;注:滚屏只能执行单一功呢,要么让他做缩放功呢,要么让他实现滚屏;
  1.创建滚动视图
    super viewDidLoad];
    UIScrollView *sv=[[UIScrollView alloc]initWithFrame:CGRectMake(0, 0, 320, 460)];
  2.设置最大最小缩放
    //缩放和滑动止只能选一
    sv.maximumZoomScale=5.0;
    sv.minimumZoomScale=0.5;
  3. 创建视图控件,然后添加到滚屏中。
  imageview=[[UIImageView alloc]initWithImage:[UIImage imageNamed:[NSString stringWithFormat:@"5.png"]]];
    imageview.frame=CGRectMake(100, 100, 50, 50);
    [sv addSubview:imageview];
    [imageview release];
  4.用到滚屏的函数要给它设置代理
    sv.delegate=self;
 
处理用户响应事件,需要重载那三个触摸的方法,开始,触摸到,末尾,
 
  5.在滚屏的放大缩小函数中写实现功能
 -(UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
{
     
        static int i=0;
     
    if (scrollView!=myScrollView) {
        return [scrollView.subviews objectAtIndex:i];
        i++;
        
         
    }
    else
        return NULL;
}
 
 
封装一个类达到的效果 简化代码,很有好处
 
 
10.运用滚动屏实现广告条展示;
    设计思想:滚动广告起始和最后广告条实现不间断的无限循环;所以要多家一个视图,最后一个视图和第一个一样,这样可以保证不间断;
   UIScrollView *sv=[[UIScrollView alloc]initWithFrame:CGRectMake(0, 0, 320, 200)];
    sv.contentSize=CGSizeMake(320, 200*9);
    sv.scrollEnabled=NO;
  //视图的可滚动属性设为no不可滚动;
    sv.tag=100;
    [self.view addSubview:sv];
    sv.showsVerticalScrollIndicator=NO;
    for (int i=0; i<9; i++) {
        UIImageView *imageview=[[UIImageView alloc]initWithFrame:CGRectMake(0, i*200, 320, 200)];
        imageview.image=[UIImage imageNamed:[NSString stringWithFormat:@"17_%d.jpg",i]];
        [sv addSubview:imageview];
        [imageview release];
    }
//创建计时器来控制图片定时更替
    NSTimer *timer=[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(timer:) userInfo:nil repeats:YES];
}
-(void)timer:(NSTimer *)timer
{
    UIScrollView *sv=(UIScrollView *)[self.view viewWithTag:100];
     //当滚屏滚到最后一幅图时,让滚屏从头开始
    if(sv.contentOffset.y==200*8)//偏移量contentOffset,画布向左偏移是正的,向右偏移是负的,这是原则;
    {
        sv.contentOffset=CGPointMake(0, 0);
    }
    [sv setContentOffset:CGPointMake(0, sv.contentOffset.y+200) animated:YES];
 
}
UIPageControl 分页显示  页面控制器
1.初始化对象
 UIPageControl *pc=[[UIPageControl alloc]initWithFrame:CGRectMake(0, 440, 80, 20)];
2.设置页数
    pc.numberOfPages=4;//设置小点的个数
    pc.tag=100;
3.为分页点添加事件
    [pc addTarget:self action:@selector(pc:) forControlEvents:UIControlEventTouchUpInside];
4.在滑动页面时,为每页定位      开发者无法判断用户判断到哪一页,所以只有委托UIScrollView的方法
使用这个函数之前要实现UIScrollViewDelegate代理
-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    UIPageControl *pc=(UIPageControl *)[self.view viewWithTag:100];
    pc.currentPage=scrollView.contentOffset.x/320;//改变原点的颜色
}
5.注意要想给滚屏分页,除了创建分页对象还要让滚屏设置分页属性
  sv.pagingEnabled=YES;
 
//遵从UIScrollViewDelegate协议,实现UIScrollViewDelegate里面的方法
-(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{//当页面开始减速的瞬间回调函数
    int pageNumber=scrollView.contentOffset.x/320;//contentoffset 内容的偏移量
     
    myPageontrol.currentPage=pageNumber;
}
 
 
 
 
9.18周二
 
必须实现的
@required
 
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section;
 
// Row display. Implementers should *always* try to reuse cells by setting each cell's reuseIdentifier and querying for available reusable cells with dequeueReusableCellWithIdentifier:
// Cell gets various attributes set automatically based on table (separators) and data source (accessory views, editing controls)
 
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
 
UITableView 表视图
1.初始化表格控件
    UITableView * tableview=[[UITableView alloc]initWithFrame:CGRectMake(0, 0, 320, 460-44) style:UITableViewStyleGrouped];
    tableview.tag=100;
2.给表格控件添加代理。注意它有两个代理//必须是两个代理
    tableview.delegate=self;
    tableview.dataSource=self;
3.表格控件的代理函数
//delegate 行数和单元格函数必须实现。否则程序无法运行;
 
1.行数
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
        return dataarray.count;//有的时候每一行都是一样的
}
 
1,复用机制;要重点了解;
2,只有一列,两列的话就是有两个tableView;
3,有数据源对象,代理对象,tableView本身的对象;
 
2.cell单元格//多少个格子要完成;每个格子里面是什么东西;  一定要完成这两个协议,否则UITableView会崩溃
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
   为每个单元个加一个标志,便于取得//每个格子的标题
    NSString *strID=@"ID";//代表的是某种类型的cell;字不是写的一样,但都是一种
       从表格对象中得到移出屏幕得对象;
    UITableViewCell *cell=[tableView dequeueReusableCellWithIdentifier:strID];//用队列的方式去找cell
    if (cell==nil) {
    //空值的情况,重现创建一个
        //顶多分配20个,然后不会再去分配了。然后反复的使用,0到10 是可以被替换的,不是一下子分配一万个,内存考虑
        cell=[[[UITableViewCell alloc]initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:strID]autorelease];
      //     UILabel *label=[[UILabel alloc]initWithFrame:CGRectMake(10, 10, 40, 20)];
       // label.text=[NSString stringWithFormat:@"%d",indexPath.row];
//        [cell addSubview:label];
//        label.tag=100;
         
    }
//    indexPath.row; 行数      
////    indexPath.section 段数     section代表的是组数,分组的时候就出来了
//    UILabel *label=(UILabel *)[self.view viewWithTag:100];
//    label.text=[dataarray objectAtIndex: indexPath.row];
   //添加图片
    cell.imageView.image=[UIImage imageNamed:@"5.png"];
    //添加细节名称表明可以展开cell
    cell.detailTextLabel.text=@"open";
    //表示有更多内容的展示风格//表格栏的尖尖  一半的中括号
    cell.accessoryType=UITableViewCellAccessoryDisclosureIndicator;
    //选择时变色设置
    cell.selectionStyle=UITableViewCellSelectionStyleNone;
//  设置cell文本  从数组中取出来   可以把文字统一放到数组里面去
    cell.textLabel.text = [dataarray objectAtIndex:indexPath.row];//主标题
     //添加细节名称表明可以展开cell      副标题
    cell.detailTextLabel.text=@"open";
 
 
    return cell;
}
 
删除或添加cell   先删除数据,再添加数据源 ,分为两步,  因为它有两个代理 ,其中一个就是数据源代理
 
 
 3. 段数
-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return 3;
}
4.行高
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
  //  可以设置每个行行高
//    if (indexPath.row==1) {
//        return 20;
//    }
    return 50;
}
5.段头标题
-(NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
  return @"abc";
 
}
6.段尾标题
-(NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section
{
   return @"tial";
 
}
7.删除和添加
设置每个单元格的可编辑属性默认是yes所以当让它处于编辑模式时这个函数可以不写
//设置元素的可编辑性
-(BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath
{
    return YES;
}
//更改单元格的编辑属性;可以设置为删除和插入
- (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath{
    return UITableViewCellEditingStyleInsert;//插入模式;
}
 
//可以滑动弹出删除键删除,也可以设置编辑按钮,点击编辑按钮删除
-(void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
     删除时既要从屏幕上删除,又要从数组中删除,
    [dataarray removeObjectAtIndex:indexPath.row];
    [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationRight];
   }
注意:如果表格分很多段,就必须分段操作;
1.分几段,就创建几个数组,以防删除不同段内容相互影响,导致崩溃;
2.在给每段单元格添加标题时也要分段添加;返回行数时也要按段来返回数组的行数;
3.在删除单元格时要根据段不同删除不同的数组元素;
//删除按钮改名为
-(NSString *)tableView:(UITableView *)tableView titleForDeleteConfirmationButtonForRowAtIndexPath:(NSIndexPath *)indexPath
{
   return @"2334";
}
7.为选择cell添加操作;下面操作为点击就会跳到新页面;作为选择cell操作的时候来用的
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    UIViewController *vc=[[UIViewController alloc]init];
    vc.title=[NSString stringWithFormat:@"%d",indexPath.row];
    [self.navigationController pushViewController:vc animated:YES];//push之后再去加载;
}
8.//didSelectRowAtIndexPath作为cell选择操作的时候用
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    UISearchBar * search = (UISearchBar *)[self.view viewWithTag:102];
    //是否是第一响应事件
    if ([search isFirstResponder]) {
    //若果是的画 就响应
        [search resignFirstResponder];
    }
}
 
9.为表格添加索引,点击就会跳到对应位置,分段的话每个标记代表一段
-(NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView
{
   return [NSArray arrayWithObjects:@"a",@"b",@"c", nil];
}
10.移动某个cell
-(BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath
{
    return YES;
}
-(void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath
{
    //注意从前往后和从后往前移动不同
    NSString *str=[dataarray objectAtIndex:sourceIndexPath.row];
   // NSString *str1=[dataarray objectAtIndex:destinationIndexPath.row];
    if(sourceIndexPath.row<destinationIndexPath.row){
        [ dataarray insertObject:str atIndex:destinationIndexPath.row+1 ] ;
        [dataarray removeObjectAtIndex:sourceIndexPath.row];
        
    }
    else
    {
        [dataarray insertObject:str atIndex:destinationIndexPath.row];
        [dataarray removeObjectAtIndex:sourceIndexPath.row+1];
    }
 
}
注意有导航条时table的高要减掉导航条的高;
//@required里面的方法是必须要实现的;@optional里面的方法可以实现也可以不实现
 
@interface VC01 : UIViewController <UITableViewDelegate, UITableViewDataSource>
//UITableView实现不了的,就用代理,让别人帮他实现,实现里面需要的方法
 
9.19 周二
搜索条一般搜索条
-(void)viewDidLoad
{
    [super viewDidLoad];
    dataArray=[[NSArray alloc]initWithObjects:@"dwew",@"dfas",@"aads",@"we", nil];
           注:在搜索设置时数组创建一定要用alloc;而且release 要在最后的dealloc中;因为数组用于更新数据,不断的使用如果不用alloc创建可能会被释放导致数组不可用;
     _tableview=[[UITableView alloc]initWithFrame:CGRectMake(0, 0, 320, 460)];
    _tableview.delegate=self;
    _tableview.dataSource=self;
     注:写tableview 的协议;
    [self.view addSubview:_tableview];
//创建搜索条
    UISearchBar *search=[[UISearchBar alloc]initWithFrame:CGRectMake(0, 0, 320, 88)];
    search.delegate=self;
    注:注意添加uisearchbar的协议;
  //添加搜索栏
    mSearchBar = [[UISearchBar alloc] initWithFrame:CGRectMake(0, 0, 320, 44)];
    [mSearchBar setTag:102];
    _mTableView.tableHeaderView = mSearchBar;
 
 
 
 
    search.showsScopeBar=YES;
    search.scopeButtonTitles=[NSArray arrayWithObjects:@"a",@"b",@"c", nil];
    注:添加范围条;两个函数必须连用;要对其添加事件要在下面函数中添加,这个事件是在点击button时触发
-(void)searchBar:(UISearchBar *)searchBar selectedScopeButtonIndexDidChange:(NSInteger)selectedScope
 
    search.showsCancelButton=YES;
    注:添加取消键
    search.showsSearchResultsButton=YES;
    注:设置搜索键 出发事件要和下面的函数连用 一般用于撤销键盘实现搜索功能;
-(void)searchBarSearchButtonClicked:(UISearchBar *)searchBar
 
         注:将搜索条添加到tableview中;
    _tableview.tableHeaderView=search;  
}
  // 添加取消按钮,点击取消按钮时触发;
-(void)searchBarCancelButtonClicked:(UISearchBar *)searchBar
{
    [searchBar resignFirstResponder];
    searchBar.text=@"";
}
// 如果设置搜索列表键为真,将添加了搜索列表键到搜索列表的后面,点击时触这个事件;
-(void)searchBarResultsListButtonClicked:(UISearchBar *)searchBar{
   searchBar.text=@"resultslistbutton";
 
}
//点击键盘上的搜索键时触发;
-(void)searchBarSearchButtonClicked:(UISearchBar *)searchBar
{
    [self searchBar:searchBar textDidChange:searchBar.text];
}
//当添加了范围界定条时点击上面的按钮触发
-(void)searchBar:(UISearchBar *)searchBar selectedScopeButtonIndexDidChange:(NSInteger)selectedScope
{
    if (selectedScope==0) {
        searchBar.text=@"a";
    }
    else if(selectedScope==1){
        searchBar.text=@"b";
    }
    else
        searchBar.text=@"c";
    [self searchBar:searchBar textDidChange:searchBar.text];
//调用自己的函数;对数据进行搜索;从此也得知,非键盘输入的数据要搜索必须通过调用搜索函数出发,或者在键盘状态加入非键盘输入的数据;
}
//返回表格的行数;根据是否是搜索状态还非搜索状态反会不同的数据
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    if (isSearch==YES) {
        return searchArray.count;
    }
    else
    return dataArray.count;
}
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell=[tableView dequeueReusableCellWithIdentifier:@"ID"];
    if (cell==nil) {
        cell=[[[UITableViewCell alloc]initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:@"ID"]autorelease];
    }
     
    if (isSearch==YES) {
        NSLog(@"%@",searchArray);
        cell.textLabel.text=[searchArray objectAtIndex:indexPath.row];
    }
    else{
    cell.textLabel.text=[dataArray objectAtIndex:indexPath.row];
    }
    return cell;
}
 
 
 
 
 
 
//搜索条中的内容改变时调用;但有个问题就是一旦开始搜索取消后原内容不显示因为这个函数只有搜索条中输入值时才调用;所以搜索取消不显示原有数据;但是如果在结束编辑中调用此函数就不会出现此情况;
但是此时如果有取消键的话又必须在取消键中添加内容让取消后清空搜索框并加载原又数据;所以下面的三个函数是联系起来的
-(void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
{
    if (searchBar.text==nil||[searchBar.text isEqualToString:@""]) {
        isSearch=NO;
        [_tableview reloadData];//自动刷新,  自动刷新数据
    }
NSPredicate *predicate=[NSPredicate predicateWithFormat:@"SELF contains [cd] %@",searchBar.text];
   self.searchArray=[dataArray filteredArrayUsingPredicate:predicate];
        isSearch=YES;
    [_tableview reloadData];
 
}
-(void)searchBarTextDidEndEditing:(UISearchBar *)searchBar{
 
    [self searchBar:searchBar textDidChange:searchBar.text];
}
-(void)searchBarCancelButtonClicked:(UISearchBar *)searchBar
{  searchBar.text=@"";
    isSearch=NO;
    [_tableview reloadData];
    [searchBar resignFirstResponder];  
}
运用搜索控制器来实现搜索功能
-(void)viewDidLoad
{  
    dataarray=[[NSArray alloc]initWithObjects:@"da",@"dee",@"ded",@"rtt", @"esdf",@"effy",@"qqo",@"ett",@"sdfa",@"dfg",nil];
  注:创建搜索数组,数组必须用alloc初始化而且不能被release; 
    tableview=[[UITableView alloc]initWithFrame:CGRectMake(0, 0, 320, 460) style:UITableViewStylePlain];
    tableview.delegate=self;
    tableview.dataSource=self;
    tableview.tag=100;
    注:要为tableview添加tag属性便于知道搜索状态应为处于搜索状态时搜索控制器就会覆盖住表格框;所以通过判断此时是的tableview是那个可以知道更新那个数组数据;
    [self.view addSubview:tableview];
    [tableview release];
    UISearchBar *searchbar=[[UISearchBar alloc]initWithFrame:CGRectMake(0, 0, 320, 44)];
    tableview.tableHeaderView=searchbar;
       注:创建搜索展示控制器 把导航条添加进去;把文本控制设置为本类;
    searchdisplay=[[UISearchDisplayController alloc]initWithSearchBar:searchbar contentsController:self];
    注:设置代理,和搜索资源搜索代理;
    searchdisplay.delegate=self;
    searchdisplay.searchResultsDataSource=self;
    searchdisplay.searchResultsDelegate=self;
   注意:此时搜索控制器不用释放,因为这个搜索器需要不断被调用;
    [searchbar release];
}
   根据此时那个tableview被使用返回数据;
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    if (tableView.tag==100) {
        return dataarray.count;
    }
    return searcharray.count;
}
//设置单元格;
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell=[tableView dequeueReusableCellWithIdentifier:@"ID"];
    if (cell==nil) {
        cell=[[UITableViewCell alloc]initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:@"ID"];
    }
    if (tableView.tag==100) {
        cell.textLabel.text=(NSString *)[dataarray objectAtIndex:indexPath.row];
    }
    else
        cell.textLabel.text=(NSString *)[searcharray objectAtIndex:indexPath.row];
    
    return cell;
}
-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return 3;
}
//更新数据前对数据进行过滤;得到过滤的数据;此函数是个实时监控函数;
-(BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchString:(NSString *)searchString{
    NSPredicate *predicate=[NSPredicate predicateWithFormat:@"SELF contains [cd] %@",searchString];
    self.searcharray=(NSArray *)[dataarray filteredArrayUsingPredicate:predicate];
    NSLog(@"%@",searcharray);
    return YES;
}
//返回tableview中和对应段指针相对应的section的title;相当于为每段设置了一个名字
-(NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView
{
    return [NSArray arrayWithObjects:UITableViewIndexSearch,@"a",@"b",@"c",nil];
//注意这里添加了一个搜索,所以搜索指针对应发生改变;在下面的函数返回时要注意
}
//返回和title和index一致的section
-(NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index
{
    if (index==0) {
        [tableView setContentOffset:CGPointMake(0, 0)];
    }
    return index-1;
}
//设置section的头标题;
-(NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
    return [NSString stringWithFormat:@"%d",section];
}
注意:有些对象没有被释放要写dealloc函数;
 
设置控件的收起和放下
(void)viewDidLoad
{
    [super viewDidLoad];
    array=[[NSArray alloc]initWithObjects:@"小红",@"小梅",@"小李", nil];
    
    tableview=[[UITableView alloc]initWithFrame:CGRectMake(0, 0, 320, 460) style:UITableViewStylePlain];
    tableview.delegate=self;
    tableview.dataSource=self;
tableview.sectionHeaderHeight=30;
//设置tableview的section头标题的高;便于以后添加控件和控件大小相适应
    [self.view addSubview:tableview];
    [tableview release];
    p[0]=1;//p是有两个元素的整形数组做为开关;如果设置几段需要几个开关,就让数组元素个数是几
    p[1]=1;
   
}
//根据数组元素的值确定段是打开还是收起;
//简称设置行数
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    if (p[section]==0) {
        return 0;
    }
    return array.count;//直接返回
}
//设置组数
-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return 2;
}
//创建表格单元格
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell=[tableView dequeueReusableCellWithIdentifier:@"ID"];
    if (cell==nil) {
        cell=[[[UITableViewCell alloc]initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:@"ID"]autorelease];
    }
    cell.textLabel.text=[array objectAtIndex:indexPath.row];
    return cell;
}
//为表格的段头添加view让此view加载label和button,并为button添加事件用于控制开关的打开和关闭;既置0和置1;
-(UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
    UIView *view=[[UIView alloc]initWithFrame:CGRectMake(0, 0, 320, 30)];
    view.backgroundColor=[UIColor blueColor];
    UILabel *label=[[UILabel alloc]initWithFrame:CGRectMake(0, 0, 80, 30)];
    label.backgroundColor=[UIColor clearColor];
     
    if (section==0) {
        label.text=@"我的好友";
    }
   else
       label.text=@"黑名单";
     
    [view addSubview:label];
     
    UIButton *button=[UIButton buttonWithType:UIButtonTypeCustom];
    button.tag=section;
//注意添加button的tag用于鉴别是哪个button;
    button.frame=CGRectMake(0, 0, 320, 30);
    [button addTarget:self action:@selector(buttonclick:) forControlEvents:UIControlEventTouchUpInside];
    [view addSubview:button];
     
    return view;      
}
//button事件用于控制开关状态;
-(void)buttonclick:(UIButton *)sender
{
    if (p[sender.tag]==0) {
        p[sender.tag]=1;
    }
    else
        p[sender.tag]=0;
     
    [tableview reloadSections:[NSIndexSet indexSetWithIndex:sender.tag] withRowAnimation:UITableViewRowAnimationFade];
   //重新加载section根据button的不同来决定加载那个section 之 所以可以这样适应为button。tag是根据section设置的;此时加载section需要的数据是nsindexset类型搜一要把button的tag转换成此类型;
}
 
为解决问题:怎么让section头宽度变大?
解决方法:设置tableview的section的头高 设置代码为:tableview.sectionHeaderHeight=30;
cell的层次 view在cell上contentview又在view上其他控件又加在了content view上
 
cell的contentView   cell上面的内容视图
 
 
9.20周四
和xib文件连用创建表格
创建表格框,然后创建xib文件,并把xib文件中的默认视图删除;添加tableviewcell 可以在里面添加其他控件;
-(void)viewDidLoad
{    [super viewDidLoad];
        UITableView * tableView=[[UITableView alloc]initWithFrame:CGRectMake(0, 0, 320,460) style:UITableViewStylePlain];
        tableView.delegate=self;
        tableView.dataSource=self;
        [self.view addSubview:tableView];
        [tableView release];
}
//设置行数
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return 20;
}/
//在创建cell的函数中将xib创建的文件添加到tableview中;
-(UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell * cell=[tableView dequeueReusableCellWithIdentifier:indexPath.row%2==0?@"ID":@"ID2"];
    if(cell==nil){
        //cell=[[UITableViewCell alloc]initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:@"ID"];
        cell=[[[NSBundle mainBundle] loadNibNamed:indexPath.row%2==0?@"MYcell":@"MYCell2" owner:self options:nil]objectAtIndex:0];
        //和xib中的文件绑定;
    }
//设置cell颜色要注意是设置contentview的颜色应为它覆盖在cell上;
  //  cell.contentView.backgroundColor=[UIColor redColor];  //cell也是个view 上有contentview 其实label也是加再contentview上
     
    return cell;
}
 
//自定义的cell注意在创建自定义cell类在写继承时是继承UITableViewCell;
   创建cell类后在初始化文件中写上初始化的cell特性,既需要更改什么属性,加在其他控件;
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
    if (self) {
        self.myLabel=[[UILabel alloc]initWithFrame:CGRectMake(0, 0, 320, 40)];
        myLabel.textAlignment=UITextAlignmentCenter;
        myLabel.backgroundColor=[UIColor greenColor];
        [self addSubview:myLabel];
        [myLabel release];
         
    }
     
    return self;
}
//在控制器中创建cell时创建自定义cell
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
 
 
//复用池,每次显示满屏dequeueReusableCellWithIdentifier方法  复用机制
    CustomCell *cell=(CustomCell *)[tableView dequeueReusableCellWithIdentifier:@"ID"];
    if (cell==nil) {
        cell=[[CustomCell alloc]initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:@"ID"];
         
    }
//加载文本从资源文件中获取;
    NSString *str=[NSString stringWithContentsOfFile:[[NSBundle mainBundle]pathForResource:@"ui" ofType:@"rtf"] encoding:NSUTF8StringEncoding error:nil ];   // NSString *str=[NSString stringWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"c" ofType:@"rtf"] encoding:NSUTF8StringEncoding error:nil];
    cell.myLabel.text=str;
    return  cell;
}
 
 
 
9.24 周一 第十一周(网络
在使用不同的网络协议传输时都要先加入协议文件;如果用到他们的对应函数时要写协议,设置代理
1.json 格式网络传输
    在网络中传输请求时用的时是字符串。他们用{}[]链接起来;其中{}表示字典;[]表示数组;解析时只要分清层次需要从哪取数据就用相应的数组或对象取数据;
1.网络数据解析
   //{ "title":"harry pottery", "price":"999$"}
    NSString *json=@"{\"title\":\"harry pottery\",\"price\":\"99999\"}";
    //双引号里面不能在有双引号,所以加双引号前要加反斜杠;
    NSDictionary *dic=[json JSONValue];
    NSString *str=[dic objectForKey:@"title"];
    NSLog(@"%@",str);
    //[{"a","b","c"}]
    json=@"[\" a\", \"b\",\"c\"]";
    NSArray *array=[json JSONValue];
    for (NSString *s in array) {
        NSLog(@"%@",s);
    }
 
/*    { "books":[
 {
       "title": "harry1"
 },
 {
       "title":"harry2"
 }
        ]
      "num":"7"
 }
 */
    /*
     }
     */  
    json=@"{\"key1\":[{\"key2\":\"value2\"},\"value3\" ]}";
    NSDictionary *dic1=[json JSONValue];
   array=[dic1 objectForKey:@"key1"];
    dic=[array objectAtIndex:0];
    NSString *st=[dic objectForKey:@"key2"];
    NSLog(@"%@",st );
 
2 .SBJson网络传输数据:要加入第三方协议:SBJson 冰岛入SBJson.h头文件
 NSData *data=[NSData dataWithContentsOfURL:[NSURL URLWithString:@"http://localhost/1.gif"]];
    UIImage *image=[UIImage imageWithData:data];
    UIImageView *imageview=[[UIImageView alloc]initWithFrame:CGRectMake(0, 0, 320, 200)];
    imageview.image=image;
    [self.view addSubview:imageview];
 
3.SDWebImage 网络传输
     //使用SDWebImage 传输数据,而且它是个自带缓存打开过,再打开时还存在;
   在使用图片按钮传输时要引人头文件
#import "UIImageView+WebCache.h"
#import "UIButton+WebCache.h"
     NSURL *url=[NSURL URLWithString:@"http://localhost/1.gif"];
    UIImageView *image =[[UIImageView alloc]initWithFrame:CGRectMake(0, 0, 320, 200)];
    [image setImageWithURL:url];
    [self.view addSubview:image];
    [image release];
4.ASIHTTPRequest网络传输???????
首先要导人四个库文件libz.1.2.5.dylib,CFNetwork.framework,
SystemConfiguration.framework,MobileCoreServices.framework
引人头文件 #import "ASIHTTPRequest.h" 同时要添加ASIHTTPRequest.h协议;设置代理
   NSURL *url=[NSURL URLWithString:@"http://localhost/1.gif"];
//使ASIHTTPRequest 传输数据;注意这个需要加第三方协议
    ASIHTTPRequest *request=[ASIHTTPRequest requestWithURL:url];
    request.delegate=self;
    [request startSynchronous];
//开始同步
   [UIApplication sharedApplication].networkActivityIndicatorVisible=YES;
//网络请求,如果为yes,就一直旋转
-(void)requestFinished:(ASIHTTPRequest *)request
{
    UIImageView *imageview=[[UIImageView alloc]initWithFrame:CGRectMake(0, 0, 320, 200)];
    imageview.image=[UIImage imageWithData:request.responseData];
    [self.view addSubview:imageview];
}
 
5.异步传输  这种异步传输需要代理  <NSURLConnectionDataDelegate>
 
  /异步传输数据:优点:在传输数据没有完成前,可以对网页上的其他东西进行操作;
    [super viewDidLoad];
    NSURL *url=[NSURL URLWithString:@"http://localhost/1.gif"];
    //创建请求
    NSURLRequest *request=[NSURLRequest requestWithURL:url];
    //发送请求
    [NSURLConnection connectionWithRequest:request delegate:self];
    data1=[[NSMutableData alloc]init];
   imageview=[[UIImageView alloc]initWithFrame:CGRectMake(0, 0, 320, 200)];
    [self.view  addSubview:imageview];
        
}
//异步传输链接的代理函数:
//链接响应时调用
-(void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
    
    NSLog(@"didreceiveresponse");
}
//加载完成时调用
-(void)connectionDidFinishLoading:(NSURLConnection *)connection
{
     
    imageview.image=[UIImage imageWithData:data1];
    NSLog(@"connectiondidFinishload");
     
}
//反复调用不断接收新数据
-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
     [data1 appendData:data];
    NSLog(@"didreceivedata");
}
//链接发生错误时响应;
-(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
    NSLog(@"error");
}
 
 
xml
首 先导人GData 文件, 然后通过在项目头文件中Build Phases中第三个选项中数添加libxml2 文件并将其移动到项目下面, 然后在 BuildSettings中在search paths中在 Header Search Paths 中添加路径/use/include/libxml2 这样就可用xml 读取网络文件了;
1.将网络编码文件存入字符串;
 NSString *xmlstr=[NSString stringWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"xml" ofType:@"txt"] encoding:NSUTF8StringEncoding error:nil];
 2.将字符串转变成GDataXMLDocument 类行的文件;
获取文件
    GDataXMLDocument *xmldoc=[[GDataXMLDocument alloc]initWithXMLString:xmlstr options:0 error:nil];
3.从文件中获取根元素;
    //获取跟元素
    GDataXMLElement * rootEle=[xmldoc rootElement];
4.获取元素的值
  rootEle.stringValue
5.获取根元素下面的某项值,用所在位置获取;也可以通过数组把他们全都放到数组,在从数组中获取;
    GDataXMLElement *sysele=(GDataXMLElement *)[rootEle childAtIndex:0];
    GDataXMLElement cityele=[sysele childAtIndex:0];
    NSArray *array=[sysele children];
6.获取元素的 XMLString属性值; 获取属性stringValue值 属性名值name
    array =[intel children];
    for (GDataXMLElement * itemele in array) {
        //itemele.attributes 是元素得属性 是个数组;
        for (GDataXMLElement * item in itemele.attributes) {
            //打印元素属性;会将每个属性和值一起打印出来;
            NSLog(@"%@",item.XMLString);
            //打印属性得值
            NSLog(@"%@",item.stringValue);
            //打印属性名字
            NSLog(@"%@",item.name);
        }
    }
7.通过元素名获取元素;
    //通过元素名获取元素
 GDataXMLElement *codeele=[[sysele elementsForName:@"areaCode"] objectAtIndex:0];
8.通过路径获取获取元素;
    //XPATH 通过路径获取元素  注意通过路径取出的元素因为不知道保函几个元素,所以是数组类行要想取得得用objectindexat 根据位置获取其中得某一个;
    //绝对路径
    GDataXMLElement *cityele=[[xmldoc nodesForXPath:@"root/systemConfig/CityName" error:nil] objectAtIndex:0];
    NSLog(@"%@",cityele.stringValue);
    //通过层次写;
    cityele=[[xmldoc nodesForXPath:@"//CityName" error:nil] objectAtIndex:0];
    NSLog(@"%@",cityele.stringValue);
    //获取在这个层次得所以item
    NSArray *array=[xmldoc nodesForXPath:@"//Item" error:nil];
    //获取某个目录下得item并通过item[i]打印它的元素;注意是从元素标号1开始;
    array=[xmldoc nodesForXPath:@"//intentionLevel/Item[1]" error:nil];
   GDataXMLElement *tem =[[xmldoc nodesForXPath:@"//Item" error:nil] o  bjectAtIndex:0];
    GDataXMLElement *ele =[tem.attributes objectAtIndex:0];
//元素得属性指针开始从0位置;
    NSLog(@"%@",ele.stringValue);
 
    //打印最后一个元素
   array=[xmldoc nodesForXPath:@"//intentionLevel/Item[last()]" error:nil];
    //打印某个范围内的元素通过位置号判断
  array=[xmldoc nodesForXPath:@"//intentionLevel/Item[position()<3]" error:nil];
    //获取不同节点下的元素
     array=[xmldoc nodesForXPath:@"//intentionLevel/Item[position()<3]|//  ****/item[2]" error:nil];
    //通过关键字获取值;
     array=[xmldoc nodesForXPath:@"//intentionLevel/Item[@key=2]" error:nil];
 array=[xmldoc nodesForXPath:@"//intentionLevel/Item[@key<3]" error:nil];
   
 在接收取出来的元素时,一般取出来的是数组,要用数组接,如果是单个元素要用数组的object 0去转换成对象;然后再取其中的属性;
 
    网页刷新
准备工作:
  1.再刷新网页时用到了第三方代理,在引入函数前要先添加文件
    EGOTableViewPullRefresh
  2。添加库文件:QuartzCore.framework
  3.加入代理:1.UITableViewDelegate,
             2.UITableViewDataSource,
             3.EGORefreshTableHeaderDelegate
viewdidload函数内容:
   1.创建获得内容得数组;
  dataArray=[[NSMutableArray alloc]initWithObjects:@"a",@"b",@"c",@"d" ,nil];
  2.创建table 并设置代理;
    table=[[UITableView alloc]initWithFrame:CGRectMake(0, 0, 320, 460) style:UITableViewStylePlain];
    table.delegate=self;
    table.dataSource=self;
    [self.view addSubview: table];
    [table release];
   3.创建刷新视图
    refreashview=[[EGORefreshTableHeaderView alloc]initWithFrame:CGRectMake(0, -460, 320, 460)];
    refreashview.delegate=self;
   4.添加刷新视图到table中;
    [table addSubview:refreashview];
    //更新上次刷新时间,如果没有只返回date化上次更新时间不跟着刷新
  [refreashview refreshLastUpdatedDate];
     
}
代理函数内容
 加载时调用是table 它是个bool值,时时自动调用
-(BOOL)egoRefreshTableHeaderDataSourceIsLoading:(EGORefreshTableHeaderView *)view
{
 NSLog(@"isloading");
    return isloading;
    //是否正在刷新,如果返回no就会在次加载数据,会让数据加载过多
}
时间函数timer调用得方法
-(void)refresdata
{   //更新数据;
    [dataArray addObject:@"new"];
    [table reloadData];
    [refreashview egoRefreshScrollViewDataSourceDidFinishedLoading:table];
    一但更新完以后就要把加载函数反回得bool值改变为no便于下次加载;
    isloading=NO;
}
//加载更新出发得更新函数;
-(void)egoRefreshTableHeaderDidTriggerRefresh:(EGORefreshTableHeaderView *)view
{
    isloading=YES;
    [NSTimer scheduledTimerWithTimeInterval:3 target:self selector:@selector(refresdata) userInfo:nil repeats:NO];
    //注意这里得重复要设置为no,调用一次这个函数才添加一个timer,
     NSLog(@"didTrigger");
     
}
//屏幕滚动时调用
-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
// 调用这个函数用于调用加载函数并根据不同的状态设置位置;
    [refreashview egoRefreshScrollViewDidScroll:scrollView];
}
-(void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{   根据现在更新的状态,打开加载的控制器;
    [refreashview egoRefreshScrollViewDidEndDragging:scrollView];
}
//添加上次刷新时间;
-(NSDate *)egoRefreshTableHeaderDataSourceLastUpdated:(EGORefreshTableHeaderView *)view
{
    return [NSDate date];
}
//创建tableview的基本函数;
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return dataArray.count;
}
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell=[tableView dequeueReusableCellWithIdentifier:@"ID"];
    if ( cell==nil) {
        cell=[[[UITableViewCell alloc]initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:@"ID"] autorelease];
    }
     
    cell.textLabel.text=[dataArray objectAtIndex:indexPath.row];
    return  cell;
}
-(void)dealloc
{
    [dataArray release];
    [super dealloc];
}
 
9.26 周三
数据库操作;
进入数据库
sqlite3 data.db
退出数据库
.quit
创建表
create table Students(name, score);
插入数据前看是否有表,没表就创建
create table if not exists Students(name,score);
插入数据
insert into Students values('kety',89);
删除表
drop  table Stufents
删除某项数据
delete from Students where name='hau';
修改数据
update Students set score=55 where name='hahan';
排序: 降序排; 默认升序排
 select * from Book order by price desc;
排序后显示前几项
select * from Book order by price desc limit 5;
查询某个条件范围的全部数据
select * from Book where price>40;
查询内容中的某一项
select name from Book;
两个表关联查询
select Students.name, Student1.pro, Students.score from Students join Student1 on Students.name=Student1.name;
查询纪录数量;
select count(*)from Students;
 
前台操作和数据库链接
注意:在操作前要导入文件libsqlite3.dylib   导入头文件#import "sqlite3.h";
self.PATH=[[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0] stringByAppendingPathComponent:@"data.db"];
    注意: PATH 是接收路径的字符串变量,注意前面要加上self;
    //路径的创建,stringByAppendingPathComponent 给字符串拼接时加斜杠;
    //NSDocumentDirectory 永久保存数据;
在出发事件中进行后台操作;
增加纪录
-(void)add
{
    //打开数据库
    sqlite3 *sql;
    int res =sqlite3_open([PATH UTF8String], &sql);
    if (res!=SQLITE_OK) {
        NSLog(@"open error");
        return;
    }
    //创建表
    NSString *sqlStr=@"create table if not exists Students(name,score)";
    res=sqlite3_exec(sql, [sqlStr UTF8String], NULL, NULL, NULL);
    if (res!=SQLITE_OK) {
        NSLog(@"Create error");
        sqlite3_close(sql);
        return;
    }
    sqlStr =[NSString stringWithFormat:@"insert into Students values('%@','%@')",namefield.text,scorefield.text];
    res=sqlite3_exec(sql, [sqlStr UTF8String], NULL, NULL, NULL);
    if (res!=SQLITE_OK) {
        NSLog(@"insert error");
    }
    sqlite3_close(sql);
}
 
删除纪录
-(void)del
{
    sqlite3 *sql;
    int res =sqlite3_open([PATH UTF8String], &sql);
    if (res!=SQLITE_OK) {
        NSLog(@"open error");
        return;
    }
    NSString *sqlStr=[NSString stringWithFormat:@"delete from Students where name='%@'",namefield.text];
    res=sqlite3_exec(sql, [sqlStr UTF8String], NULL, NULL, NULL);
    if (res!=SQLITE_OK) {
        NSLog(@"delete error");
    }
    sqlite3_close(sql);
}
 
更新纪录
-(void)upd
{
    sqlite3 * sql;
    int res =sqlite3_open([PATH UTF8String],&sql );
    if (res!=SQLITE_OK) {
        NSLog(@"open error");
        return;
    }
    NSString *sqlStr=[NSString stringWithFormat:@"update Students set score='%@' where name='%@'",scorefield.text,namefield.text];
    res=sqlite3_exec(sql, [sqlStr UTF8String], NULL, NULL, NULL);
    if (res!=SQLITE_OK) {
        NSLog(@"UPDATE ERROR");
        return;
    }
    sqlite3_close(sql);
}
 
查询纪录
-(void)sel
{
    sqlite3 *sql;
    int res=sqlite3_open([PATH UTF8String], &sql);
    if (res!=SQLITE_OK) {
        NSLog(@"open error");
        return;
    }
    创建预处理
    sqlite3_stmt *stmt;
    NSString *sqlStr=@"select *from Students";
    res=sqlite3_prepare_v2(sql, [sqlStr UTF8String], -1, &stmt, NULL);
    if (res!=SQLITE_OK) {
        NSLog(@"prepare error");
        return;
    }
    while (sqlite3_step(stmt)==SQLITE_ROW) {
        char *name=(char *)sqlite3_column_text(stmt, 0);
        char *score=(char *)sqlite3_column_text(stmt, 1);
        NSLog(@"%s %s",name,score);
    }
    sqlite3_close(sql);
}
 
创建新的线程:作用在主线程执行时同时调用另一个线程使不同操作同时执行;用于网络和数据库传送数据时不影响执行其他内容
创建新线程:
//开辟新线程;把读取网络和数据库的内容可以开辟新线程,
[NSThread detachNewThreadSelector:@selector(thread ) toTarget:self withObject:nil];
想要在新线程中执行的内容可以可以写到新线程调用的函数中
-(void)thread
{
    //通过函数跳回主线程;
    [self performSelectorOnMainThread:@selector(main1) withObject:nil waitUntilDone:nil];//延时调用函数
 

计时器也可以开辟新线程;所以在网络和数据库传输数据时可以用计时器来替代进程
 //timer也会开辟新线程;可以用来代替线程函数;
    [NSTimer scheduledTimerWithTimeInterval:0.0 target:self selector:@selector(timer) userInfo:nil repeats:YES];
 
在数据库操作时添加,删除,更新的操作思想:
 1.打开数据库;
 2.创建操作语句;
 3.执行语句;
 4.关闭数据库;
 
 
9.27 周四
引入第三方协议和后台数据库链接;
1.先引入数据库文件libsqlite3.dylib 再添加第三方文件FMDB, 引入头文件
   #import "FMDatabase.h" 
  
2.实现增,删,改,查功能
    -(void)add
{   //绑定数据库
    FMDatabase *db=[FMDatabase databaseWithPath:PATH];
    BOOL res =[db open];
    if (res==NO) {
        NSLog(@"open error");
        return;
    }
//执行语句;
    res=[db executeUpdate:@"create table if not exists Students (name,score)"];
    if (res==NO) {
        NSLog(@"creat error");
        [db close];
        return;
    }
    res=[db executeUpdate:@"insert into Students values(?,?)",namefield.text,scorefield.text];
    if (res==NO) {
        NSLog(@"insert error");
    }
    [db close];
  //  NSData *data=[NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"" ofType:@"png"]  ];
}
//delete
-(void)del
{
    FMDatabase *db=[FMDatabase databaseWithPath:PATH];
    BOOL res =[db open];
    if (res==NO) {
        NSLog(@"open error");
        return;
    }
    res=[db executeUpdate:@"delete from Students where name =?",namefield.text];
    if (res==NO) {
        NSLog(@"delete error");
    }
    [db close];
}
-(void)upd
{
    FMDatabase *db=[FMDatabase databaseWithPath:PATH];
    BOOL res=[db open];
    if (res==NO) {
        NSLog(@"open error");
        return;
    }
    res=[db executeUpdate:@"update set score=77 from Students where name=?",namefield.text];
    if (res==NO) {
        NSLog(@"update error");
    }
    [db close];
}
-(void)sel
{
    FMDatabase *db=[FMDatabase databaseWithPath:PATH];
    BOOL res=[db open];
    if (res==NO) {
        NSLog(@"open error");
        return;
    }
预处理数据
    FMResultSet *set=[db executeQuery:@"select * from Students"];
遍历数据
    while ([set next]) {
        NSString *name=[set stringForColumn:@"name"];
        NSString *score=[set stringForColumnIndex:1];
        NSLog(@"%@ %@",name ,score);
    }
如果图片经过编码转换成data类型存入数据库取出来的方式如下;
1.从预处理中取出图片的二进制编码
    NSData *data=[set objectForColumnIndex:1];
2.创建存放图片的控件;  
    UIImageView *image=[[UIImageView alloc]initWithFrame:CGRectMake(50, 300, 100, 100)];
3.将数据转换成image添加到控件中;
    image.image=[UIImage imageWithData:data];
    [self.view addSubview:image];
    [db close];
 
}
sdafggfdhfjgsdfhgjfgkhljkgjfhgfdsaDFGH
其他类型数据存入数据库
在数据库存储中,可以存储nsstring,nsnumber ,nsdata ;如果想储存其他类型的数字需要将其内容都转换成nsdata类型;
将image转换成data类型;然后通过获取data再将其转换成image类型:
1.创建image
UIImage *image= [UIImage imageNamed:@"5.png" ];
2.创建data用于存入数据;
    NSMutableData *data=[NSMutableData dataWithCapacity:0];
3.创建编码器
    NSKeyedArchiver *arch= [[NSKeyedArchiver alloc]initForWritingWithMutableData:data];
4.编码图片;
    [arch encodeObject:image forKey:@"image"];
5.完成编码
    [arch finishEncoding];
6.释放;
    [arch release];
7.创建解码器
    NSKeyedUnarchiver *unaarev=[[NSKeyedUnarchiver alloc]initForReadingWithData:data];
8.解码图片;
    UIImage *image1=[unaarev decodeObjectForKey:@"image"];
9.解码完成
    [unaarev finishDecoding];
10.释放
    [unaarev release];
11.展示图片
    UIImageView * image2=[[UIImageView alloc]initWithFrame:CGRectMake(30, 300, 100, 100)];;
    image2.image=image1;
    [self.view addSubview:image2];
 
聊天对话框的的气泡拉伸思想: 为了达到不矢真,横向延伸要用用竖向的一条像素按所需长度复制;竖向延伸要拿横向的一条像素按所需长度延伸;
具体实现:
 //聊天气泡拉伸
1.创建气泡
    image =[UIImage imageNamed:@"5.png"]
cap:帽子扩展延伸;
 2。拉伸气泡函数
    image=[image stretchableImageWithLeftCapWidth:14 topCapHeight:14];
    image2.image=image;
    NSString *str=@"abcdet";
    //得到str的尺寸;
   CGSize size=[str sizeWithFont:[UIFont systemFontOfSize:12.0] constrainedToSize:CGSizeMake(200, 1000) lineBreakMode:UILineBreakModeWordWrap];
 
10.8 周一
向服务器上传数据
 1.先添加第三方文件ASIHTTPRequst 导人第三方文件:#import "ASIFormDataRequest.h"  然后添加四个数据库文件 libz.1.2.5.dylib ,MobileCoreServices.framework, Systemconfiguration.framework;, CFNetwork.framework
   2. 实现上传:
    1.创建与服务器链接对象;//192.168 .88.8  局域网    获得局域网服务器的网址
      NSURL *url=[NSURL URLWithString:@"http://169.254.59.31/pk.php"];
     2.创建向服务器发出请求的对象;
     ASIFormDataRequest *asiform=[ASIFormDataRequest requestWithURL:url];
     3.设置上传内容:上传内容可以是文件,也可以是二进制数据
               一种函数 [asiform setFile:[[NSBundle mainBundle]pathForResource:@"5" ofType:@"jpg"] forKey:@"image"];
      第二种函数:[asiform setFile:[[NSBundle mainBundle]pathForResource:@"5" ofType:@"jpg"] withFileName:@"2.png" andContentType:@"image/jpg" forKey:@"image"];
     不同点:第二种会在服务器端显示你所设置的文件名,如果是二进制数据后者会显示类型,如果是图片会显示图片的缩略图,如果用第一种不能显示其缩略图;
         4.设置代理,这样就可以使用其函数了;包括上传成功失败的函数
         5.最后让其上传执行
      [asiform startSynchronous];
-(void)viewDidLoad
{
    [super viewDidLoad];
   /*
      网络接口路径
      169.254.59.31/pk.php
     图像上传 post
    参数
    图像 image
    返回:不同内容代表不同的是否成功
    {}
     
   */
    //创建链接
    NSURL *url=[NSURL URLWithString:@"http://169.254.59.31/pk.php"];
    //创建数据请求
    ASIFormDataRequest *asiform=[ASIFormDataRequest requestWithURL:url];
     
   1. [asiform setFile:[[NSBundle mainBundle]pathForResource:@"5" ofType:@"jpg"] forKey:@"image"];
   2. [asiform setFile:[[NSBundle mainBundle]pathForResource:@"5" ofType:@"jpg"] withFileName:@"2.png" andContentType:@"image/jpg" forKey:@"image"];
    NSData *data=[NSData dataWithContentsOfFile:[[NSBundle mainBundle]pathForResource:@"5" ofType:@"jpg" ]];
     //上传data类型的内容显示类型;
    [asiform setData:data forKey:@"image"];
    //上传data类型以文件命名,在服务器端显示上传内容;
    [asiform setData:data withFileName:@"5.jpg" andContentType:@"image/jpg" forKey:@"image"];
    asiform.delegate=self;
    [asiform startSynchronous];
}
//用于提示上传是否成功或失败;
-(void)requestFailed:(ASIHTTPRequest *)request
{
    NSLog(@"error");
 
}
-(void)requestFinished:(ASIHTTPRequest *)request
{
    NSLog(@"ok");
}
 
10.9周二
mp3播放器制作
1.加载库文件 AVFoundation.framework和QuartzCore.framework;( 没弄明白这个文件的作用,因为加不加他没什么作用)然后引入头文件
                #import <AVFoundation/AVFoundation.h>
       #import <QuartzCore/QuartzCore.h>
2.mp3实现代码
-(void)viewDidLoad
{
    [super viewDidLoad];
    //创建播放按钮,并分别为添加事件
    UIButton *play=[UIButton buttonWithType:UIButtonTypeRoundedRect];
    play.frame=CGRectMake(20, 100, 80, 30);
    [play addTarget:self action:@selector(play) forControlEvents:UIControlEventTouchUpInside ];
    [play setTitle:@"play" forState:UIControlStateNormal];
    [self.view addSubview:play];
    UIButton *pause=[UIButton buttonWithType:UIButtonTypeRoundedRect];
    pause.frame=CGRectMake(120, 100, 80, 30);
    [pause setTitle:@"pause" forState:UIControlStateNormal];
    [pause addTarget:self action:@selector(pause) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:pause];
    UIButton *stop=[UIButton buttonWithType:UIButtonTypeRoundedRect];
    stop.frame=CGRectMake(220, 100, 80, 30);
    [stop addTarget:self action:@selector(stop) forControlEvents:UIControlEventTouchUpInside];
    [stop setTitle:@"stop" forState:UIControlStateNormal];
    [self.view addSubview:stop];
//创建数据链接
    NSURL *url=[NSURL fileURLWithPath:[[NSBundle mainBundle]pathForResource:@"withyou" ofType:@"mp3"]];
    player=[[AVAudioPlayer alloc]initWithContentsOfURL:url error:Nil];
    //提前载入随时准备播放
    [player prepareToPlay];
//创建滑条用于控制音量,声道和播放速率
    UISlider *volslider=[[UISlider alloc]initWithFrame:CGRectMake(50, 50, 200, 0)];
    //音量控制
    volslider.minimumValue=0.0;
    volslider.maximumValue=1.0;
    //声道
    volslider.minimumValue=-1.0;
    volslider.minimumValue=1.0;
    // 变速
    player.enableRate=YES;// 开启加速功能;
    volslider.minimumValue=0.5;
    volslider.maximumValue=2.0;
    volslider.value=1.0;
    //为滑条添加滑动事件,注意出发事件是滑条值改便时;
    [volslider addTarget:self action:@selector(volslider:) forControlEvents:UIControlEventValueChanged];
    [self.view addSubview:volslider];
    [volslider release];
//创建进度条,控制音频播放进度
    UISlider *proslider=[[UISlider alloc]initWithFrame:CGRectMake(50, 20, 220, 0)];
    //设置进度条最大最小播放百分比
    proslider.minimumValue=0.0;
    proslider.maximumValue=1.0;
    proslider.value=0.0;
    proslider.tag=100;
    [proslider addTarget:self action:@selector(progressSlider:) forControlEvents:UIControlEventValueChanged];
    [self.view addSubview:proslider];
    [proslider release];
    //e ,j 是防止暂停,停止点击超过一次时多次释放timer导致崩溃
    e=1;
    j=1;
//设置代理
    player.delegate=self;
//设置音频频率显示条
    for (int i=0; i<4; i++) {
        pro[i]=[[UIProgressView alloc]initWithFrame:CGRectMake(50*i, 440, 200, 0)];
        [self.view addSubview:pro[i]];
        [pro[i] release];
                //旋转进度条,因为要的是弧度所以参数要改变成弧度制
        pro[i].transform=CGAffineTransformMakeRotation(270 *M_PI/180);
        //设置铆点用于设置旋转围绕点;
        pro[i].layer.anchorPoint=CGPointMake(0.0, 0.0);
    }
    //音频可控用于进度条随音频变动
    player.meteringEnabled=YES;
     
}
 
//播放完成时调用
-(void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag
{
    NSLog(@"success");
 
}
//进度条随音频改变
-(void)progressSlider:(UISlider *)slider
{
    player.currentTime=player.duration*slider.value;
 
}
-(void)volslider:(UISlider *)slider
{
    //声音大小控制
    player.volume=slider.value;
    //声道控制
    player.pan=slider.value;
    //播放速度控制
    player.rate=slider.value;
 
}
//时间控制器调用的函数
-(void)refresh
{
    UISlider *proslider=(UISlider *)[self.view viewWithTag:100];
    proslider.value=player.currentTime/player.duration;
    [player updateMeters];
    //刷新频率
    pro[0].progress=[player peakPowerForChannel:0]/-100;
    pro[1].progress=[player peakPowerForChannel:1]/-100;
    pro[2].progress=[player averagePowerForChannel:0]/-100;
    pro[3].progress=[player averagePowerForChannel:1]/-100;
}
//播放控制 用添加时间器来控制
-(void)play
{
    [player play];
    timer=[NSTimer scheduledTimerWithTimeInterval:0.05 target:self selector:@selector(refresh) userInfo:nil repeats:YES];
    e=1;
    j=1;
}
//停止控制 注意e,j是分别用于控制停止按钮,暂停时间器释放,避免时间器重复释放导致的崩溃;
-(void)stop
{
    if (e==1&&j==1) {
    [player stop]; 
    [timer invalidate];
//如果按停止键,控制进度条也停止
    UISlider *proslider=(UISlider *)[self.view viewWithTag:100];
    player.currentTime=0.0;
    proslider.value=player.currentTime;
        e=0;
        j=0;
    }
    else if(e==1&&j==0)
    {
        UISlider *proslider=(UISlider *)[self.view viewWithTag:100];
        player.currentTime=0.0;
        proslider.value=player.currentTime;
        e=0;
    }
}
//控制暂停
-(void)pause
{
    if (j==1&&e==1) {
        [player pause];
        [timer invalidate];
        j=0;
    }
}
-(void)dealloc
{
    [player release];
    [super dealloc];
 
}
mp4播放器
1.导人库文件 MeduaPlayer.framework. 并导人头文件#import <MediaPlayer/MediaPlayer.h>
2. 代码实现解析
-(void)viewDidLoad
{
  //创建按钮用于播放控制
    UIButton *button=[UIButton buttonWithType:UIButtonTypeRoundedRect];
    button.frame=CGRectMake(120, 400, 70, 40);
    [button addTarget:self action:@selector(buttonclick) forControlEvents:UIControlEventTouchUpInside];
  // 注意播放控制器又两种一种是播放控制器,MPMoviePlayerController 一种是带view的播放控制器;MPMoviePlayerViewController 二者的不同之处是前者要播放时是将其视图加入主控制视图中[self.view addSubview:play.view];,播放是调用播放函数[play play];后者是将播放控制交给MPMoviePlayerViewController:[self presentModalViewController:play animated:YES]; 这样一开始播放会全屏播放;
//初始化MPMoviePlayerViewControlle,资源是当前文件
    play1=[[MPMoviePlayerViewController alloc]initWithContentURL:[NSURL fileURLWithPath:[[NSBundle mainBundle]pathForResource:@"4" ofType:@"mp4"]]];
   // play=[[MPMoviePlayerViewController alloc]initWithContentURL:[NSURL fileURLWithPath:[[NSBundle mainBundle]pathForResource:@"3" ofType:@"mp4"]]];
//初始化MPMoviePlayerController 资源是当前文件
    play=[[MPMoviePlayerController alloc]initWithContentURL:[NSURL fileURLWithPath:[[NSBundle mainBundle]pathForResource:@"4" ofType:@"mp4"]]];
    //设置播放器的 frame属性
    play.view.frame=CGRectMake(0, 0, 320, 300);
    //UIImageView *image= [[UIImageView alloc]initWithImage:[UIImage imageNamed:@"1.bmp"]];
//设置播放器背景色,虽然能设置,但是不支持使用此属性;
    play.backgroundColor=[UIColor colorWithPatternImage:[UIImage imageNamed:@"1.bmp"]];
//播放器的控制属性,用于控制播放模式默认使嵌入模式;
    play.controlStyle=MPMovieControlStyleEmbedded;
//播放重复模式,可以用于设置重复播放;
    play.repeatMode=MPMovieRepeatModeOne;
    [self.view addSubview:play.view];
     
      
   // play.allowsAirPlay=NO;
    [self.view addSubview:button];
}
//控制播放器播放;
-(void)buttonclick
{
    [self presentModalViewController:play animated:YES];
    [play play];
}
10.11周四   画图
1.画图工具要新建一个uiview文件 然后在这个文件的.m文件中的- (void)drawRect:(CGRect)rect函数中画图
2.画各中图的方法
   //图片的绘制
    UIImage* image = [UIImage imageNamed:@"5.png"];
    [image drawInRect:CGRectMake(100, 100, 100, 100)];
    // 文字
    NSString* str = @"ABC";
    [str drawInRect:CGRectMake(100, 100, 100, 50) withFont:[UIFont systemFontOfSize:30.0] lineBreakMode:UILineBreakModeClip alignment:UITextAlignmentLeft];
  
   //画直线
    创建画纸
    CGContextRef ref = UIGraphicsGetCurrentContext();
    画起点:
    CGContextMoveToPoint(ref, 20, 100);
    添加点
    CGContextAddLineToPoint(ref, 300, 100);
    CGContextAddLineToPoint(ref, 150, 300);
    封闭图形
    CGContextClosePath(ref);
 
    //线条颜色
    CGContextSetStrokeColorWithColor(ref, [UIColor blueColor].CGColor);
    画路径 结束画图
    CGContextStrokePath(ref);
     
    //线宽
    CGContextSetLineWidth(ref, 20.0);
    //虚线
    float length[] = {40,20,40};
   CGContextSetLineDash(ref, 0, length, 2);
   //注意:参数二是从多少像素开始;
                        参数三是用于实线和间隔循环的数组;
                        参数四是用于要去数组的前几个数循环;
        //线段的样式
    CGContextSetLineCap(ref, kCGLineCapSquare);
    //线段连接处的连接样式
    CGContextSetLineJoin(ref, kCGLineJoinRound);
    CGContextStrokePath(ref);
 
    //矩形
    CGContextRef ref = UIGraphicsGetCurrentContext();
    //设置矩形的fram
    CGContextAddRect(ref, CGRectMake(100, 100, 200, 100));
    //设置线宽
    CGContextSetLineWidth(ref, 10.0);
   //设施线的颜色
    CGContextSetStrokeColorWithColor(ref, [UIColor greenColor].CGColor);
     //设置填充色;
    CGContextSetFillColorWithColor(ref, [UIColor blueColor].CGColor);
//注意下面的三个生成图像的方法的不同点
         1.第一个只画轮廓
    //CGContextStrokePath(ref);
    2.只填充图像的内部,不现实边框
    //CGContextFillPath(ref);
    3.边框和填充都显示,注意后面的参数式fill和stroke都有的,一般fill是内部填充的属性,而stroke是线的属性
    CGContextDrawPath(ref, kCGPathFillStroke);
  
    //圆
    CGContextRef ref = UIGraphicsGetCurrentContext();
    设置圆的框架大小
    CGContextAddEllipseInRect(ref, CGRectMake(100, 100, 200, 100));
    CGContextStrokePath(ref);
     
    CGContextRef ref = UIGraphicsGetCurrentContext();
    画自由圆
         设置圆心位置
    CGContextMoveToPoint(ref, 150, 150);
          画圆,第二,三个 是圆心位置,第四个参数是半径 第五,六个参数数是开始和结束的角,角是弧度制,最后一个参数是顺时针和逆时针旋转1表示顺时针,而表示逆时针
    CGContextAddArc(ref, 150, 150, 100, 0, 270 * M_PI / 180, 1);
    设置图形填充色
    CGContextSetFillColorWithColor(ref, [UIColor purpleColor].CGColor);
    填充图形;
    CGContextFillPath(ref);
    重新设置圆点 ,相当于在新的图层上画图,这样可以为每个图形设置不同的颜色;
    CGContextMoveToPoint(ref, 150, 150);
    CGContextAddArc(ref, 150, 150, 100, 0, 120 * M_PI / 180, 0);
    CGContextSetFillColorWithColor(ref, [UIColor orangeColor].CGColor);
    CGContextFillPath(ref);
    CGContextMoveToPoint(ref, 150, 150);
    CGContextAddArc(ref, 150, 150, 100, 120 * M_PI / 180, 270 * M_PI / 180, 0);
    CGContextSetFillColorWithColor(ref, [UIColor blueColor].CGColor);
    CGContextFillPath(ref);
  
    //画曲线
    CGContextRef ref = UIGraphicsGetCurrentContext();
    设置初试点
    CGContextMoveToPoint(ref, 20, 400);
    画曲线 第二,三个参数是曲线始末两点切线的焦点坐标;后两个参数是末点的坐标;
    CGContextAddQuadCurveToPoint(ref, 0, 100, 300, 400);
    CGContextStrokePath(ref);
     
     //设置图形的透明度和阴影
    CGContextRef ref = UIGraphicsGetCurrentContext();
    CGContextAddRect(ref, CGRectMake(100, 100, 150, 150));
    CGContextSetLineWidth(ref, 10.0);
    CGContextSetStrokeColorWithColor(ref, [UIColor blueColor].CGColor);
    CGContextSetFillColorWithColor(ref, [UIColor redColor].CGColor);
    //设置图形的透明度
    CGContextSetAlpha(ref, 0.5);
        //设置图形阴影 第二个参数是阴影向又向下的偏移量 ,最后一个参数是羽化程度
    CGContextSetShadow(ref, CGSizeMake(20, 20), 10);
    CGContextDrawPath(ref, kCGPathFillStroke);
动画效果总结
  1.以前给uiview 添加动画效果;  
//    [UIView beginAnimations:nil context:nil];
//    [UIView setAnimationDuration:1];
//    //设置动画快慢
             设置动画的方向
//    [UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
 
//    [self.view exchangeSubviewAtIndex:0 withSubviewAtIndex:1];
//    [UIView setAnimationTransition:UIViewAnimationTransitionCurlUp forView:self.view cache:YES];
//    [UIView setAnimationDelegate: self];
      设置动画各阶段的调用的函数;
//    [UIView setAnimationWillStartSelector:@selector(start)];
//    [UIView setAnimationDidStopSelector:@selector(stop)];
//    [UIView commitAnimations];
 CATransition 用时要导人库文件QuartzCore.framework 在加入头文件#import <QuartzCore/QuartzCore.h>
  初始化
   CATransition *trans=[CATransition animation];
   设置动画时间;
    trans.duration=1.0;
  设置动画进入曲线
    trans.timingFunction=UIViewAnimationCurveEaseInOut;
    设置动画的类型 类型可以直接用字符串;类型为:
    1.   1.  pageCurl            向上翻一页 
    2.   pageUnCurl          向下翻一页 
    3.   rippleEffect        滴水效果 
    4.   suckEffect          收缩效果,如一块布被抽走 
    5.   cube                立方体效果 
    6.   oglFlip             上下翻转效果  
    trans.type=@"rippleEffect";
   动画进入类型
   // trans.type=kCATransitionFromLeft;
    trans.subtype=kCATransitionFromTop;
    设置次动画;
   // trans.subtype=kCATransitionFromBottom;
    设置动画代理
    trans.delegate=self;
    //动画从多少开始;是整个动画的播放百分比
    //trans.startProgress=0.5;
    //动画结束动画从多少结束
    //trans.endProgress=0.8;
    [self.view.layer addAnimation:trans forKey:nil];
    [self.view exchangeSubviewAtIndex:0 withSubviewAtIndex:1];
 
}
-(void)animationDidStart:(CAAnimation *)anim
{
    NSLog(@"strat");
}
-(void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
{
    //flag 得值表示本次动画是否执行完了
    NSLog(@"stop %d",flag);
     
}
注:CATransition动画只能在图层上加,所以要切换页view时也要加到view.layer上;如在切换控制器时做法self.navigationController.view.layer addAnimation:tran forKey:nil];
    [self.navigationController pushViewController:rvc animated:NO];
    其中animated的yes或no没关系;
 
 
 
10.12日 地图
   1.创建地图需要添加库文件:CoreLocation.framework //服务的  定位的 库文件       
                         MapKit.framework
     然后添加创建头文件地图类#import <MapKit/MapKit.h>
             因为用到了类的代理函数需添加协议    <CLLocationManagerDelegate,MKMapViewDelegate>
 2.代码实线
         创建两个类  MKMapView *map;
                 CLLocationManager *localmanager;
     -(void)viewDidLoad
{
    //创建地图
    map=[[MKMapView alloc]initWithFrame:CGRectMake(0, 0, 320, 460)];
    [self.view addSubview:map];
    //定位经纬
    CLLocationCoordinate2D coordinate=CLLocationCoordinate2DMake(40.035731, 116.351008);
    //范围 显示比例  越小越精确
    MKCoordinateSpan span=MKCoordinateSpanMake(0.5, 0.5);
    将经纬度和显示比例添加到 原始方位中,当打开地图时地图上显示此位置;
    MKCoordinateRegion region=MKCoordinateRegionMake(coordinate, span);
      map.region=region;
       //设置地图的显示类型,卫星图,普通地图,修改过的卫星图;
    //map.mapType=MKMapTypeSatellite;
    //map.mapType=MKMapTypeHybrid;
 
 
    //设置map代理
    map.delegate=self;
       //创建位置管理对象;
    localmanager=[[CLLocationManager alloc]init];
    // 地图显示精确度。越精确越费电
    localmanager.desiredAccuracy=kCLLocationAccuracyBest;
    //移动100米更新位置一次
    localmanager.distanceFilter=100;
    [localmanager startUpdatingLocation];
      
    localmanager.delegate=self;
 
 
 
    //添加大头针   大头针类是单独的文件;是普通的继承nsobject文件 用还需添加代理      <MKAnnotation>
 实线代理类中的函数
//设置标题
-(NSString *)title
{
     return @"标题";
}
//设置副标题
-(NSString *)subtitle
{
     return @"副标题";
}
//设置将大头针插到的位置;
-(CLLocationCoordinate2D)coordinate
{
    CLLocationCoordinate2D cooridinate=CLLocationCoordinate2DMake(40.035731, 116.351008);
    return cooridinate;
}
 
为了能多插入几个大头针可以更改他们的属性,位置,我们可以创建一个初始化函数,通过创建不同的对象,实现多大头针;
-(id)initWithTitle:(NSString *)title subTitle:(NSString *)subtitle coordinate:(CLLocationCoordinate2D)coordinate
{
    self=[super init];
    if (self) {
        _title=title;   
        _subtitle=subtitle;
        _coordinate=coordinate;
       这三个参数都是自定义属性;在返回函数中可以直接分别返回这几个属性
    }
    return self;
}
 
 
 
     
     回到根控制文件创建大头针,用自定义初始化函数初始化大头针;
    MyAnnotion *myann=[[MyAnnotion alloc]initWithTitle:@"标题" subTitle:@"副标题" coordinate:coordinate];
    给地图添加大头针
 
    [map addAnnotation:myann];
 
 
   创建一个长压手势,在手势函数中
    UILongPressGestureRecognizer *press=[[UILongPressGestureRecognizer alloc]initWithTarget:self action:@selector(longpress:)];
     给地图添加手势
    [map addGestureRecognizer:press];
    [map release];
}
 
 
长按触发的手势函数;
-(void)longpress:(UILongPressGestureRecognizer *)press
{
    为了防治一直按键不断的产生大头针 要对按键状态进行判断,只有第一次长按才添加大头针
    if (press.state==UIGestureRecognizerStateBegan) {
        CGPoint piont=[press  locationInView:self.view ];
    CLLocationCoordinate2D coordinate=[map convertPoint:piont toCoordinateFromView:map];
    MyAnnotion * annontion=[[MyAnnotion alloc]initWithTitle:@"title" subTitle:@"subtite" coordinate:coordinate];
 
     //为大头针出现时添加动作;
        [map addAnnotation:annontion];
    }
     
     
}
 
//这是地图的协议方法
//自定义大头针
-(MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation
{
    //为了节省内存创建大头针队列,只生成能看见的大头针; 
    MKPinAnnotationView *pinView=(MKPinAnnotationView *)[mapView dequeueReusableAnnotationViewWithIdentifier:@"ID"];
    //判断是否是用户定义;为了防治把系统的定位标志也改成大头针样式,由于是定位,定位时刻进行,所以回不断的产生大头针,和我们自己定义的大头针没有了区分
    if ([annotation isKindOfClass:[mapView.userLocation class]]) {
        return nil;
    }
    if(pinView ==nil){
    //创建大头针使用我们定义加入地图的大头针;
        pinView=[[[MKPinAnnotationView alloc]initWithAnnotation:annotation reuseIdentifier:@"ID"]autorelease];
    }
    //展示标题
    pinView.canShowCallout=YES;
    //设置动画
    pinView.animatesDrop=YES;
    //设置针头颜色
    pinView.pinColor=MKPinAnnotationColorPurple;
    创建视图用于添加到大头针细节视图中
    UIView *leftview=[[UIView alloc]initWithFrame:CGRectMake(0, 0, 20, 20)];
    leftview.backgroundColor=[UIColor greenColor];
    pinView.leftCalloutAccessoryView=leftview;
    //添加按钮用于添加到大头针视图细节的右面;
    UIButton *button=[UIButton buttonWithType:UIButtonTypeDetailDisclosure]; 
    pinView.rightCalloutAccessoryView=button;
     return pinView;
}
 
 
//定位函数
-(void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation

     把用户的位置属性设为yes来展示用户的位置;
    map.showsUserLocation=YES;
    设置显示比例
    MKCoordinateSpan span=MKCoordinateSpanMake(0.5, 0.5);
    创建原始位置属性,此时经纬可以用函数的参数来设置,当前所处的位置;
    MKCoordinateRegion region=MKCoordinateRegionMake(newLocation.coordinate, span);
     动画添加位置
    [map setRegion:region animated:YES];
    更新位置
    [localmanager startUpdatingLocation];
}
  
 
 
游戏开发
cocos2d oc
cocos2d-x( 中国人维护  不如cocos2d多) 2d-x c++
cocos2d-android java
cocos2d-html5
cocos2d-flash
cocos2d流程:和电影流程差不多
游戏开始:   
     1。初始化   
              libs:游戏引擎;
              加库:opencles.framework(计算机图形学)
               注:gpu:是数
 
      注意:选择白库时选最短得
plist图片要和png图片名称相同
注:在游戏当中一定要注意产生对象的释放;因为内容比较多,如不释放内存泄漏非常严重;
游戏一:发射打击;
 1。 加载所需文件:    openal.framework   ,openGLES.framework,quartacore.framework,audiotoolbox.faramework,avfoundation.framwork,libz.dylib;
添加libs文件cocos2d包,再加上所需图片和音乐;
  2.游戏界面只有一个导演,一个舞台,每个界面相当于一个节目;界面切换靠的是导演指导哪个节目上场;每个节目上要添加层靠曾来展示内容;
    具体操作是:
        1.创建导演,并判断是否能运行最新的导演
          BOOL ret = [CCDirector setDirectorType:kCCDirectorTypeDisplayLink];
//这是功能比较强大的导演;
   if (ret == FALSE) {
        [CCDirector setDirectorType:kCCDirectorTypeDefault];
        CCDirector *director = [CCDirector sharedDirector];
//创建导演,他是个共享函数,程序始终只有一个导演;
             这个导演适于所有版本的系统;
    2.创建舞台
        CGRect rect = self.window.bounds;//得到舞台的尺寸
      创建舞台view
    EAGLView *glView = [[EAGLView alloc] initWithFrame:rect]
   3. 导演让屏幕横屏
    [director setDeviceOrientation:kCCDeviceOrientationLandscapeLeft];
    // 设置屏幕为 风景
    // kCCDeviceOrientationLandscapeLeft
    // 竖屏  Portrait 肖像
4. 关联导演和glview
    // 让导演知道哪里是舞台
    [director setOpenGLView:glView];
5. 设置游戏刷新率
    // 刷新周期  60HZ
    // [director setAnimationInterval:1/60];
    [director setAnimationInterval:1/60.0f];
6.把舞台添加到窗口 
    [self.window addSubview:glView];
    [glView release];
    // glview计数器是多少  2
7.运行第一个节目
     CCScene *s = [StartLayer scene];
调用start layer的类函数用于切换节目  [StartLayer scene] 的类函数要些在StartLayer中方便使用
    [director runWithScene:s];
    让导演运行第一个节目;
在运行一个节目时需要注意的点:
   1。创建切换节目的类函数;
         + (id) scene {
    CCScene *s = [CCScene node];
    CCLayer *l = [StartLayer node];
    [s addChild:l];
    return s;
}
  node函数是继承node的 初始化函数是[[[self alloc] init] autorelease];用这个函数比较简便;但是初始化时如果对象就以两个可以这样初始化;如果对象很多就不要这样初始化;因为自动释放时间不确定容易占用内存;
   2.初始化层;
        注:一开始加载的东西要放在初始化中;
        创建菜单
         CCMenuItemFont *startItem = [[CCMenuItemFont alloc] initFromString:@"开始游戏" target:self selector:@selector(beginGame)];
      CCMenuItemFont是文字菜单项;有图片菜单项,文本项等等
                点击菜单项都会出发事件;点击开始游戏菜单项就要出发新节目;在出发函数中要
        CCMenuItem *helpItem = [[CCMenuItemFont alloc] initFromString:@"帮助" target:self selector:@selector(helpGame)];
        // CCMenuItemFont创建一个菜单项
        // cocos2d
        CCMenu *menu = [CCMenu menuWithItems:startItem, helpItem, nil];  
        创建菜单,同时加入菜单项;
        [menu alignItemsVertically];
        // menuitem竖直对齐
        // NSArray
        [self addChild:menu];
        // 把menu加入到self
        // addChild: 类似addSubview:
 
 点击菜单项都会出发事件;点击开始游戏菜单项就要出发新节目;在触发函数中要要切换节目
    2.创建新节目:
    CCScene *s = [LoadingLayer scene];//这个函数同样是类函数,
    // 第2个 任何地方都是这样
    CCDirector *d = [CCDirector sharedDirector];获取导演;
    // s 1
    [d replaceScene:s];
     让新节目代替旧节目; 这个函数的好处是把旧节目释放;不再占用内存
//    [s release];
    // 把当前剧场删掉  然后用新的剧场s
    // 不遵守规矩
    // 在LoadingLayer alloc 在StartLayer中release
登陆节目:
       节目同样要些创建节目的类函数,和上面的写法一样
       登陆时显示的是一个进度条;通过扩大其中一个进度条的长度来模仿数据加载;
     初始化函数:
       在cocos2d中的精灵相当于ui中的图片
         CCSprite *redSprite = [[CCSprite alloc] initWithFile:@"progressbar1.png"];
        // 用图片progressbar1.png来创建一个精灵
        CCDirector *d = [CCDirector sharedDirector];
        CGSize winSize = [d winSize];
        // 取得屏幕当前的宽高
                 设置锚点;锚点相当于图片的基准点;
        redSprite.anchorPoint = ccp(0, 0);
        CGSize redSpriteSize = [redSprite contentSize];
        // 取得精灵自己的宽高
        CGFloat x = (winSize.width-redSpriteSize.width)/2.0f;
        CGFloat y = (winSize.height-redSpriteSize.height)/2.0f;
        设置精灵的位置;
        redSprite.position = ccp(x, y);
//        redSprite.position = ccp(winSize.width/2, winSize.height/2);
        // 指定它中心位置
        // 设置锚点在父视图中的坐标
        将精灵添加到图层;
        [self addChild:redSprite];
        [redSprite release];
         注意局部对象及时的销毁;
        yellowSprite = [[CCSprite alloc] initWithFile:@"progressbar2.png"];
//        yellowSprite.position = ccp(winSize.width/2, winSize.height/2);
        yellowSprite.anchorPoint = ccp(0, 0);
        yellowSprite.position = ccp(x, y);
//scale是缩放的意思;可以对整体缩放,也可以缩放x和y;
        yellowSprite.scaleX = 0.2f;
        yellowSprite.tag = 100;
        [self addChild:yellowSprite];
//        [NSTimer scheduledTimerWithTimeInterval:<#(NSTimeInterval)#> target:<#(id)#> selector:<#(SEL)#> userInfo:<#(id)#> repeats:<#(BOOL)#>
         //图层也可以添加计时器;不用再创建timer;
        [self schedule:@selector(timer:) interval:0.5f];
        // 每隔0.5s来调用self timer:
 每隔0。5秒调用的函数;
- (void) timer:(double)dt {
//    CCSprite *yellowSprite = (CCSprite *)[self getChildByTag:100];
    // getChildByTag:100根据100tag来找ccnode
    // 如果有就返回 否则 nil
    // 取得s的在x上的放大比例 < 1 缩小
    CGFloat scalex = [yellowSprite scaleX];
    scalex += 0.1f;
    [yellowSprite setScaleX:scalex];
    if (scalex >= 1.0f) {
        // 取消定时器
        [self unschedule:@selector(timer:)];
        [[CCDirector sharedDirector] replaceScene:[GameLayer scene]];
         
    }
}
 
游戏界面
     游戏界面同样是通过类函数创建节目;在初始化函数种需要做的工作是加载背景图,和游戏者。创建随时间调用的函数;
  CCSprite *bgSprite = [[CCSprite alloc] initWithFile:@"bg.png"];
        bgSprite.anchorPoint = ccp(0, 0);
        bgSprite.position = ccp(0, 0);
        [self addChild:bgSprite];
        [bgSprite release];
        // 放一个人
         
        CCSprite *player = [[CCSprite alloc] initWithFile:@"Player.png"];
        CGSize playerSize = [player contentSize];
        CGSize winSize = [[CCDirector sharedDirector] winSize];
        player.position = ccp(playerSize.width/2.0f, winSize.height/2.0f);
        [self addChild:player];
        [player release];
        tagetarray=[[CCArray alloc]init];
        bulletarray=[[CCArray alloc]init];
 
        [self addTarget];
        [self schedule:@selector(targetTimer:) interval:1.0f];
        [self schedule:@selector(gameLogic)];
        // 如果不写多长时间调用就是每隔1/60.0f 秒调用
        // 缺省touch是关闭的
        self.isTouchEnabled = YES;
        发子弹需要触摸所以要让屏幕可触摸;
创建打击目标调用的函数;
-(void)addTarget
{ 创建精灵;
    CCSprite * taget=[[CCSprite alloc]initWithFile:@"Target.png"];
    将精灵添加到数组;用于测试是否被子弹打中;
    [tagetarray addObject:taget];
 
    CGSize winsize=[[CCDirector sharedDirector] winSize];
    CGSize tagetsize=[taget contentSize];
          通过随机数获设置精灵的随机高度;注:随机数很大通过取余来控制随机数的范围;
    CGFloat y= tagetsize.height/2.0f+ arc4random()%(int)(winsize.height-tagetsize.height);
    CGFloat x=winsize.width;
    taget.position=ccp(x,y);
    [self addChild:taget];
    [taget release];
    CGPoint dispoint=ccp(0, y);
    设置动作点,精灵会在设置的时间内从起点运动到目地点;
    CCMoveTo *move=[[CCMoveTo alloc]initWithDuration:2 position:dispoint];
    CGPoint destpoint2=ccp(300,0 );
    动作点可设置跳跃点
    CCJumpTo *move2=[[CCJumpTo alloc]initWithDuration:2 position:destpoint2  height:200 jumps:1];
    用来调用某个函数;一般是用来做善后工作;
    CCCallFuncN *finish= [[CCCallFuncN alloc]initWithTarget:self selector:@selector(finish:)]; 
     创建队列,调用者会按队列顺序执行;
    CCSequence *squence=[CCSequence actions:move,move2,finish, nil];
     让对象按队列运行;
    [taget runAction:squence];
    [move2 release];
    [move release];
  
}
触摸函数用于添加子弹;注意触摸函数的写法;函数touch后带es的和ui的触摸函数一样
-(void)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event

      创建音乐引擎;
    [[SimpleAudioEngine sharedEngine] playEffect:@"pew-pew-lei.caf"];
    CCSprite *bulletsprite=[[CCSprite alloc]initWithFile:@"Bullet.png"];
    bulletsprite.tag=10;
    创建触摸对象
    UITouch *onetouch=[touches anyObject];
    获取触摸对象的位置;
    CGPoint touchpoint=[onetouch locationInView:onetouch.view ];
    //注意获取的触摸点时竖屏时的所以得把该点转换成游戏横屏时的触摸点;
    CGPoint glpoint=[[CCDirector sharedDirector]convertToGL:touchpoint];
    CGFloat y=[[CCDirector sharedDirector]winSize].height/2.0f;
    bulletsprite.position=ccp(20,y);
    [self addChild: bulletsprite];
    CCMoveTo * move=[[CCMoveTo alloc]initWithDuration:1 position:glpoint];
    int i=480/glpoint.x;
    CGPoint point=ccp(490, glpoint.y*i);
     
    //int time=sqrt(glpoint.x*glpoint.x+glpoint.y*glpoint.y)*sqrt((480-glpoint.x)*(480-glpoint.x)+(glpoint.y*i)*(glpoint.y*i));
     
    CCMoveTo *move2=[[CCMoveTo alloc]initWithDuration:1 position:point];
    CCCallFuncN *finish=[[CCCallFuncN alloc]initWithTarget:self selector:@selector(finish:)];
    把子弹添加到数组;用于判断是否打中目标;
    [bulletarray addObject:bulletsprite];
  CCSequence *sequence=[CCSequence actions:move,move2,finish, nil];
    [bulletsprite runAction:sequence];
     
    [bulletsprite release];
    [move release];
  //  [finish release];
     
}
判断是否大中目标的函数;用循环数组比较比较范围来获取;
-(void)gamelogic
{
    for (CCSprite* bullet in bulletarray) {
       // NSLog(@"%@",bulletarray);
        CGSize buletsize=[bullet  contentSize];
        for (CCSprite *taget in tagetarray) {
        CGSize tagetsize=[taget contentSize];
            float x1=buletsize.width;
            float x2=tagetsize.width;
            float max=x1*0.4f+x2*0.4f;
            CGFloat lon =ccpDistance(bullet.position,taget.position );
            通过比较接触范围来判断是否打中;
            if (lon<=max) {
                //停止动画函数;在这里没有什么作用;
                [taget stopAllActions];
                 [bullet stopAllActions];
               调用释放函数;
                [self finish:taget];
                [self finish:bullet];
                   
                return;
                 
            }
        }
         
            }
}
-(void)addTarget
 
 
从屏幕上释放精灵;
-(void)finish:(CCNode *)node
{ 由于数组中添加了对象,释放时也要从数组中去除;
    if (node.tag==10) {
        [bulletarray removeObject:node];
    }
    else
       [tagetarray removeObject:node];
     
    [node removeFromParentAndCleanup:YES];
     
}
做游戏地图
要学习 : tieldmap:地图编译器  
         opengl
         Box2D 物理引擎
网络:http gel post1
     http   post2
     weibo分享 腾讯微博,uauth2 协议
   domain    协议:1.http://
                                   2.ftp://
                                   3.
             baidu.com是真正的域名; www.baidu.com是一台机器是二级域名
            参数间用&隔开,参数名=参数值
             http://www.baidu.com  是缺省的 http://www.baidu.com  /index.html/
   网络访问模式
        客户端      发出请求                apache2      调用             访问程序
   要点:
 
    网络请求处理 nignx(大量连接时), httplight(联接少的时候),apache2
 
  服务器默认路径是/library/web server/document / document 是放网页的文件夹
     读取这个目录下的iOS1时写法:http://localhost/iOS1
     XXXX.CGi 是服务器程序      cgi-executables 是放可执行程序的文件 在服务器访问中映射是cgi-bin 访问时写成http://localhost/cgi-bin/iOS
post1真实名字是:application/x-www-form-urlencoded
功能强大顺序;
post2>post1>get
他们都是上行协议;都是客户端传给服务器端的, 而服务器传给客户端的没有区分也叫响应;
什么时get协议:
     在网址上写的直接回车能出来的是get协议 get 时传入到网上的东西.又可以定义为只有请求头没有请求体的请求,
    入网址是http://api.douban.com/cgi-bin?srlaction=232
    发到网上的时 只有请求头: 
                                get :  /cgi-bin?srlaction=232\r\n
                                  请求路径和内容
                                host:api.douban.com\r\n
                                  请求机器
                                \r\n     :请求头的结束标志;
post1 发送格式:
请求头:
            post /cgi-bin/get.cgi\r\n
            请求路径
           host:localhost\r\n
            请求的机器
          content-type:application/x-www-form-unlencoded\r\n
            请求协议:post1
          content-length :78\r\n
          请求体长度
           r\n
请求体:
        srclatitude=40.02999&srclongitude=116.3466
  get 缺陷:1.网址不可太长<1024
 
                    2.不能带密码;
                     3.不能传文件
   post1特点:1.网址小于4g
                       2.可一带密码;
 
 read(0,input ,lengthjl) :0 指从apache2中读数据
 
套接字:socket
  
tcp:用在稳定的连接:可靠的,速度快
   用于:图片,录音,文字
 
udp不可靠的网络连接;非常快,
用于:视频,文件,文字
 
 
tcp:网络传输;
传输思想:
1.服务器端:
             1.生成服务器并并设置监听接口
             2.在代理的确实接收到套接字函数中 要调用等待读取函数;这里的等待不是新建立的套接字等待而是让服务器底层新开一个线程等待传人数据;
             3.在代理的确实接收到数据函数中取得接收到的数据,同时要在建立等待接收数据,等待下一次传输;这时的tag还用先前的;因为tcp网络传输一次建立联系,后面就无须在建立联系;所以省去再和服务器建立联系这个环节;
2.客户端:
             1.生成客户端套接字 ;
              2. 连接服务器,并设置网络接入入口
              3.调用代理的以服务器建立连接成功函数。在这里吧连接开关设置为已连连接
              4.将数据发送到服务器
 
- (void) createServer {
    allClientSockets = [[NSMutableArray alloc] init];
    serverSocket = [[AsyncSocket alloc] initWithDelegate:self];
    [serverSocket acceptOnPort:0x1234 error:nil];
    // serverSocket在端口0x1234来等待
    // 也是异步的bind端口
    // serverSocket自动会在0x1234上等待有人来连接我
}
 
- (void) onSocket:(AsyncSocket *)sock didAcceptNewSocket:(AsyncSocket *)newSocket {
    // 只要有人来连接我 这个函数就会被调用
    // newSocket就是系统自动创建的 socket
    // 如果有10个人连接端口 0x1234那么这个函数就会执行10次
    // newSocket
    // newSocket是自动autorelease
    NSLog(@"new connection is coming new %@",newSocket);
    [allClientSockets addObject:newSocket];
    int index = [allClientSockets indexOfObject:newSocket];
    [newSocket readDataWithTimeout:-1 tag:index+100];
    // newSocket来告诉系统来接收到达newSocket的数据
    // -1 一直等待 100 表示
    // 异步读取数据
}
- (void) onSocket:(AsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
     
    int index = [allClientSockets indexOfObject:sock];
    NSString *s = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    recvMsgView.text = [NSString stringWithFormat:@"%@\n%@", recvMsgView.text, s];
    [s release];
     
    // 继续发起一个读的操作
    [sock readDataWithTimeout:-1 tag:tag];
}
 
 
#pragma mark -
#pragma mark Client Part
- (void) createClient {
    clientSocket = [[AsyncSocket alloc] initWithDelegate:self];
    isConnected = NO;
}
- (void) connectToHost:(NSString *)ip {
    static int index;
    if (index++)
        return;
    // 异步连接ip:0x1234的机器
    [clientSocket connectToHost:ip onPort:0x1234 withTimeout:10 error:nil];
    // 还不知道到底有没有连接成功
}
- (void) onSocket:(AsyncSocket *)sock didConnectToHost:(NSString *)host port:(UInt16)port {
     NSLog(@"已经连接上了 %@:%d", host, port);
    isConnected = YES;
}
- (void) sendMessage:(id)arg {
    NSString *ip = ipField.text;
    // 192.168.1.23 api.douban.com
    NSString *msg = msgField.text;
    NSData *msgData = [msg dataUsingEncoding:NSUTF8StringEncoding];
    /*  1. 文字 2. 图片  3 图片+文字 4 文字+语音
     //  文字 1 图片 2 语音 3
        type: 1
        subtype: 1
        len :
        文字内容
      
         
        type:2
        subtype:2
        len: 10240
        图片内容
      
        type:3
        subtype:1
        len:100
        文字内容
        subtype:2
        len:200000
        图片内容
      
        type:4
        subtype:3
        len:1000000
        语音内容
        subtype:1
        文字
         
     */
    // 端口
    // 第一次发数据就来调用 连接
    // 第二次之后就不用连接了
     [self connectToHost:ip];
     
    if(isConnected == YES) {
        [clientSocket writeData:msgData withTimeout:10 tag:111];
        // 给clientsocket发送数据 msgData,
    } else {
         
    }
}
- (void) onSocket:(AsyncSocket *)sock didWriteDataWithTag:(long)tag {
    // 一旦发送成功 该函数就会得到调用
    if (tag == 111) {
         
    }
}
 
udp 网络传输
    基本思想:
      1。接收端端:
                     1.创建接收端套接字;
                      2.绑定网络端口
                      3.等待接收数据;
                       4.调用代理接收到数据的函数;取出数据,并开始调用等待函数等待下次数据传输;
     2. 发送端
                    1.创建发送端套接字
                     2.得到接收端的ip和接收数据的网络端口;将数据发送到该地址;
                      3.通过是否发送成功函数通过判断返回的tag值来判断是否发送成功;
     
- (void)viewDidLoad
{
    [super viewDidLoad];
 
    ipField = [[UITextField alloc] initWithFrame:CGRectMake(50, 10, 200, 30)];
    ipField.borderStyle = UITextBorderStyleRoundedRect;
    [self.view addSubview:ipField];
     
    msgField = [[UITextField alloc] initWithFrame:CGRectMake(50, 50, 150, 30)];
    msgField.borderStyle = UITextBorderStyleRoundedRect;
    [self.view addSubview:msgField];
     
    UIButton *b = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    b.frame = CGRectMake(220, 50, 80, 30);
    [b setTitle:@"发送" forState:UIControlStateNormal];
    [b addTarget:self action:@selector(sendMessage:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:b];
     
    recvMsgView = [[UITextView alloc] initWithFrame:CGRectMake(10, 100, 300, 250)];
    recvMsgView.backgroundColor = [UIColor groupTableViewBackgroundColor];
    [recvMsgView setEditable:NO];
    [self.view addSubview:recvMsgView];
     
    // 1. 创建接收socket
    recvMsgSocket = [[AsyncUdpSocket alloc] initWithDelegate:self];
    // 2. 接收的对象要负责绑定bind到本机的一个port
    [recvMsgSocket bindToPort:0x1234 error:nil];
    // recvMsgSocket只能接收 本机的端口为0x1234的数据包
    // 192.168.1.11:0x1234
    // 什么时候绑定成功
    // 3. 接收数据
    [recvMsgSocket receiveWithTimeout:-1 tag:200];
    //-1 表示让让服务器无限等待,去接受数据;如是正数就表示让服务器等待多长时间去接收数据;
    // 这个函数不会blocked
    //  这个函数不会亲自阻塞
    // 操作系统来等 10
    // 告诉系统接收一次
    // 4. ? 数据什么时候来了
     
    recv2 = [[AsyncUdpSocket alloc] initWithDelegate:self];
    [recv2 bindToPort:0x1235 error:nil];
//    [recv2 receiveWithTimeout:-1 tag:201];
     
    // 1. 创建发送套接字 只是创建一次
    senderUdpSocket = [[AsyncUdpSocket alloc] initWithDelegate:self];
   // [senderUdpSocket bindToPort:9876 error:nil];
    // 发送一方绑定端口是没有意义的
    // [senderUdpSocket bindToPort:0x7654 error:nil];
    //对于senderUdpSocket如果没有调用bindToPort那么系统会自动给你选择一个没用的随机的端口
}
- (BOOL) onUdpSocket:(AsyncUdpSocket *)sock didReceiveData:(NSData *)data withTag:(long)tag fromHost:(NSString *)host port:(UInt16)port {
    if (tag == 200) {
        // 证明端口0x1234上有数据来了
        // data对方传过来的数据
        // host表示是谁传给我的 port不是0x1234是发送方的端口
        // host:port是对方的端口和ip
        // tag就是200
        NSString *s = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
        NSString *s2 = [NSString stringWithFormat:@"%@:%d %@", host, port, s];
        [s release];
        recvMsgView.text = [NSString stringWithFormat:@"%@\n%@", recvMsgView.text, s2];
        [recvMsgSocket receiveWithTimeout:-1 tag:200];
    }
    return YES;
}
 
 
 
- (void) sendMessage:(id)arg {
    NSString *ip = ipField.text;
    // 192.168.1.23 api.douban.com
    NSString *msg = msgField.text;
    NSData *msgData = [msg dataUsingEncoding:NSUTF8StringEncoding];
    // 端口
    short port = 0x1234;
    // 给ip:port发送消息msg
    [senderUdpSocket sendData:msgData toHost:ip port:port withTimeout:10 tag:100];
    // 给ip地址的机器上的一个端口port 发送消息msgData
    // 10 timeout 超时 失败
    // tag 100 表示本次数据包
    // sendData:该函数不会blocked
    // 该函数不会负责真正的发送
    // 只会告诉系统我要发送 tag=100的数据包msgData
    NSLog(@"aa");
}
 
- (void) onUdpSocket:(AsyncUdpSocket *)sock didSendDataWithTag:(long)tag {
    // 名字为tag的数据包已经发送完成
    if (tag == 100) {
        NSLog(@"数据包100已经发送成功");
    }
}
 
广播:又叫消息,观察者,通知
ARC:内存自动管理 不须要release;
   2。不用[super dealloc]
       3.不能用aotorelease;
 单例:一个对象,一个全局对象;
    1.写一个单例
    2.单例什么时候释放;不用释放,因为时全局变量
    3。如何避免实例化 :将init alloc 函数返回空;
 单例命名规则:currentxxxxx, sharedxxxx, aplication xxx
 
 
arrayWithContentOfFile:    数组解析文件的方法
 
//创建三个分栏控制器
    UITabBarItem *barItem1=[[UITabBarItem alloc]initWithTitle:@"首页" image:[UIImage imageNamed:@"关于.png"] tag:0];
    self.vc01.tabBarItem=barItem1;
    //消息图标
    self.vc01.tabBarItem.badgeValue=@"3";//badgeValue是一个属性,指的是消息的提示
    self.vc02.tabBarItem.badgeValue=nil;//这样就显示为空
    [barItem1 release];
     
     
     
    //系统UITableBarItem的图标
    UITabBarItem *barItem2=[[UITabBarItem alloc]initWithTabBarSystemItem:UITabBarSystemItemMostViewed tag:1];
    self.vc02.tabBarItem=barItem2;
    [barItem2 release];
     
     
    //系统UITableBarItem的图标
 
    UITabBarItem *barItem3=[[UITabBarItem alloc]initWithTabBarSystemItem:UITabBarSystemItemFeatured tag:3];
    self.vc03.tabBarItem=barItem3;
    [barItem3 release];
     
 
//将三个视图控制器装入到数组中 
  //  NSArray *arrayVC=[NSArray arrayWithObjects:_vc01,_vc02,_vc03 nil];
     
    NSArray *arrayVC=[NSArray arrayWithObjects:self.vc01,self.vc02,self.vc03, nil];
    //将控制器赋给tabBarControl
     
    UITabBarController *tabBar=[[UITabBarController alloc]init];
    //接收数组viewControllers
    tabBar.viewControllers=arrayVC;
   
     
    //将tabBar设置根视图控制器
 
    self.window.rootViewController=tabBar;
    [arrayVC release];
     
只要是添加的视图控制器 或是什么管理器的 话 都是加载 根视图控制器rootViewController上面 
加上UIlable  UIButton  view等等都是直接添加到window或是view上面的 addSubview方法 
//添加图片视图,视图用addSubview方法来添加  特别是视图
        [sv addSubview:imageView];
 
 
 
//在视图上面添加图片的一般方法;
 for (int i=0; i<9; i++) {
        //获取每一个图片的 文件名  并且转化成字符串
        NSString *strName=[NSString stringWithFormat:@"17_%d.jpg",i+1];
        //加载图片
        UIImage *image=[UIImage imageNamed:strName];
        UIImageView *imageView=[[UIImageView alloc]initWithImage:image];
        imageView.frame=CGRectMake(20+ 97*(i%3), 20+130*(i/3), 85,120);
        //add方法是添加视图的
        [self.view addSubview:imageView];
        [imageView release];
    }
 
 
//把图片添加到数组里面去  addObject
 NSString *sirName=[NSString stringWithFormat:@"%d.png",i+1];
        UIImage *image=[UIImage imageNamed:sirName];
        [imageArray addObject:image ];
 
//随机数    后面是数组元素的个数
int random=arc4random()%[imageArray count];
 
 
随机数一般都是需要取模的  相当于是确定生成随机数的范围  不至于太大的数
取模规律  8  取模生成的是0,1,2,3,4,5,6,7;
 
 
往按钮上面添加图片很重要的方法   使得每一个图片都是可以点的 实际实际上点的都是button
  [btn setImage:image forState:UIControlStateNormal];
//设置按钮的大小  相当于是确定了图片的大小   和 图片视图是差不多的
 btn.frame=CGRectMake(40*i , 40*j, 40, 40);
 
 
objectAtIndex 只适用于集合(数组)。可根据索引获取对象。如:
NSArray *array=[NSArray arrayWithObjects:@"zhangsan",@"lisi",@"wangwu",nil];
NSLog("%@",[array objectAtIndex:0]);
这时输出的值是'zhangsan' .可以看出objectAtIndex 消息是从下标0开始。
 
 
objectAtIndex从0开始 即第一个元素的位置是0
objectAtIndex 一般在数组操作中用到。但是这个数组必须是你能控制的,就是说你必须清楚地知道第几个元素是什么内容。
如果你不清楚你需要的元素在什么位置,请用tag标注。
 
 
//两个宝石 相同的点了之后 就都隐藏了  不同  的点了没反应    这是函数的实现
-(void)pressButton:(UIButton *)btn
{
    //只被执行一次,以后都不执行了,记录上次的值;如果没有,每次都会执行,避免出现空指针直接nil
     
    static UIButton * lastBtn=nil;
    if (lastBtn==nil) {
        lastBtn=btn;
        lastBtn.enabled=NO;//点击  是否可以点击
    }
    else
    {
        if (lastBtn.tag==btn.tag) {
            lastBtn.hidden=YES;
            btn.hidden=YES;
            lastBtn=nil;
        }
        else
        {
            lastBtn.enabled=YES;
            lastBtn=nil;
        }
    }
 
 
}
 
//横着滚动的情况  特别要注意滚动条的偏移量 横着的画最好与图片的宽度一致,竖着的画最好与图片的宽度一致
  //初始化
    UIScrollView *sv=[[UIScrollView alloc]initWithFrame:CGRectMake(30, 30, 260, 400)];
    //设置滚动条最后移动的背景颜色
    sv.backgroundColor=[UIColor redColor];
    for (int i=0; i<3; i++) {
        UIImageView *imageView=[[UIImageView alloc]initWithImage:[UIImage imageNamed:[NSString stringWithFormat:@"17_%d.jpg",i+1]]];
        imageView.frame=CGRectMake(0+i*260, 0, 260,400);
         
        //添加图片视图,视图用addSubview的方法来添加,把图片添加到滚动视图上面
        [sv addSubview:imageView];
        [imageView release];
         
    }
     
    //设置显示内容尺寸
    sv.contentSize=CGSizeMake(260*3, 400);
//    设置按照页码滚动
    sv.pagingEnabled=YES;
  //  设置边缘弹动效果,拉到极端的时候可以弹起来
    sv.bounces=YES;
    //改变滚动条偏移量
    sv.contentOffset=CGPointMake(260, 0);//最好设置为CGPointMake(0, 0);上下  横着都不会首影响
    //再把滚动视图添加到  总试图上面
    [self.view addSubview:sv ];
 
 
//竖着着滚动的情况  特别要注意滚动条的偏移量 横着的画最好与图片的宽度一致,竖着的画最好与图片的宽度一致
 //初始化
    UIScrollView *sv=[[UIScrollView alloc]initWithFrame:CGRectMake(30, 30, 260, 400)];
    //设置滚动条最后移动的背景颜色
    sv.backgroundColor=[UIColor redColor];
    for (int i=0; i<3; i++) {
        UIImageView *imageView=[[UIImageView alloc]initWithImage:[UIImage imageNamed:[NSString stringWithFormat:@"17_%d.jpg",i+1]]];
        imageView.frame=CGRectMake(0, 0+i*400, 260,400);
         
        //添加图片视图,视图用addSubview的方法来添加,把图片添加到滚动视图上面
        [sv addSubview:imageView];
        [imageView release];
         
    }
     
    //设置显示内容尺寸
    sv.contentSize=CGSizeMake(0, 400*3);
//    设置按照页码滚动
    sv.pagingEnabled=YES;
  //  设置边缘弹动效果,拉到极端的时候可以弹起来
    sv.bounces=YES;
    //改变滚动条偏移量
    sv.contentOffset=CGPointMake(0, 400);//最好设置为CGPointMake(0, 0);上下  横着都不会首影响
    //再把滚动视图添加到  总试图上面
    [self.view addSubview:sv ];
 
 
//销毁对象,程序结束的时候调用,销毁控制器
- (void)dealloc
{
    //棋盘销毁,首先把棋子都拿掉
    self.vc01=nil;
    self.vc02=nil;
    self.vc03=nil;
    self.vc04=nil;
    self.vc05=nil;
    self.vc06=nil;
    [_window release];
    [super dealloc];
}
 
self.navigationItem.leftBarButtonItem=barBtnLef;//导航左按钮
 self.navigationItem.rightBarButtonItem=barBrnRight;//导航右按钮
 
 //创建左导航按钮
    UIButton *btnLeft=[UIButton buttonWithType:UIButtonTypeCustom];
    btnLeft.frame = CGRectMake(0, 0, 33, 30);
    [btnLeft addTarget:self action:@selector(pressLeftNav) forControlEvents:UIControlEventTouchUpInside];
    UIImage *imageLeft=[UIImage imageNamed:@"main_left_nav"];
    [btnLeft setImage:imageLeft forState:UIControlStateNormal];
     
    UIBarButtonItem *barBtnLef=[[UIBarButtonItem alloc]
                                initWithCustomView:btnLeft];
    self.navigationItem.leftBarButtonItem=barBtnLef;
     
     
    //创建右导航按钮,button也有frame  起始位置都是默认的 只需要宽和高就可以了,不一定是添加在window上面,可以添加在视图上面
    UIButton *btnRight = [UIButton buttonWithType:UIButtonTypeCustom];
    btnRight.frame = CGRectMake(0, 0, 48, 29);
    [btnRight addTarget:self action:@selector(pressRightNav) forControlEvents:UIControlEventTouchUpInside];
    UIImage *imageRight = [UIImage imageNamed:@"main_right_nav"];
    [btnRight setImage:imageRight forState:UIControlStateNormal];
    //导航栏 有自己特定的按钮的
    UIBarButtonItem *barBrnRight=[[UIBarButtonItem alloc]initWithCustomView:btnRight];
    self.navigationItem.rightBarButtonItem=barBrnRight;
 
 
navigationItem指的是导航的各种属性   可以引用导航按钮  标题视图 等等
 
 
 //导航栏的标题视图titleView
    UIImageView *ivLogo = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"logo_title"]];
    ivLogo.frame = CGRectMake(0, 0, 60, 35);
    self.navigationItem.titleView = ivLogo;//标题视图
    [ivLogo release];
 
 
 
    UIView *bgView=[[UIView alloc]initWithFrame:CGRectMake(0, 0, 320, 460)];
    UIImageView *bgImageView=[[UIImageView alloc]initWithFrame:CGRectMake(0, 0, 320, 460)];
    [bgView addSubview:bgImageView];
 
 imageview.UserInteractionEnabled=YES;//是否能和用户交互;
    
 
//特别注意的一点是  button上面同时有标题与  小图片的时候  默认是图片在前面 ,字在后面 
 NSArray *arrayTopButtonTitle = [NSArray arrayWithObjects:@"照片", @"状态", @"报到", nil];
    NSArray *arrayTopBtnImages = [NSArray arrayWithObjects:@"rr_pub_takephoto", @"rr_pub_status", @"rr_pub_checkin", nil];
    for (int i=0; i<arrayTopButtonTitle.count; i++) {
        UIButton *btn = [UIButton buttonWithType:UIButtonTypeRoundedRect];
        btn.frame = CGRectMake(5+105*i, 2, 100, 30);
        [btn setTitle:[arrayTopButtonTitle objectAtIndex:i] forState:UIControlStateNormal];
        //把图片放在按钮上面
        [btn setImage:[UIImage imageNamed:[arrayTopBtnImages objectAtIndex:i]] forState:UIControlStateNormal];
        [topButtonImageView addSubview:btn];
    }
 
 
经常想做的效果,主界面设置一个好看的图片  让后再添加button  等其他一些东西
 
//设置主界面的背景 用图片视图作为背景
    UIView *bgView=[[UIView alloc]initWithFrame:CGRectMake(0, 0, 320, 460)];
     
    UIImageView *bgImageView=[[UIImageView alloc]initWithImage:[UIImage imageNamed:@"rr_main_background"]];
    bgImageView.frame=CGRectMake(0, 0, 320, 460);
//把图片视图添加到视图 实际上就是把图片添加到视图
    [bgView addSubview:bgImageView];
 
 
//是否可以与用户交互  不能交互就不能够点击按钮
    topButtonImageView.userInteractionEnabled=YES;
     
    [bgImageView addSubview:topButtonImageView];
    bgImageView.userInteractionEnabled=YES;   
注意的是:最外层view 设置的button不用设置与用户交互 就是可以点击的   而当视图上面还有小视图的时候  而且小视图上面还有button的时候,这个时候必须设置与用户交互YES
topButtonImageView.userInteractionEnabled=YES;
 
否则虽然有button  但是点不动  呵呵;
 
 
 
 
//张贝贝经典改变颜色的程序
 
-(void)viewDidLoad
{
    [super viewDidLoad];
    self.navigationItem.prompt=@"移动滑块后将改变画面颜色";
     
    slider =[[UISlider alloc]init];
    slider.frame=self.navigationController.navigationBar.bounds;
    slider.minimumValue=0.0;
    slider.maximumValue=1.0;
    slider.value=0.0;
    [slider addTarget:self action:@selector(sliderDidChange) forControlEvents:UIControlEventTouchUpInside];
    self.navigationItem.titleView=slider;
     
    label =[[UILabel alloc]init];
    label.frame=CGRectInset(self.view.bounds, 10, 10);
//表示自动拉伸对齐
    label.autoresizingMask=UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight;
    label.backgroundColor=[UIColor blackColor];
    [self.view addSubview:label];
     
}
 
-(void)viewWillAppear:(BOOL)animated
{
  //  [super viewWillAppear:<#animated#>];
    [self.navigationController setNavigationBarHidden:NO animated:YES];
    [self.navigationController setToolbarHidden:YES animated:YES];
    [self.navigationItem setHidesBackButton:YES animated:NO];
     
}
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    [self.navigationItem setHidesBackButton:NO animated:YES];
}
 
 
//关于颜色的调配
-(void)sliderDidChange
{
    UIColor *color =[[UIColor alloc]initWithRed:(1-slider.value) green:slider.value blue:(1-slider.value)+0.4 alpha:1.0];
    label.backgroundColor=color;
}
 
 
 
//static的用法  交替进行
-(void)changColor
{
    //保留每一次的值  然后在开始  没有static就每次都是针对 黑色的将不会出现
    //布尔类型的变量  定义了一个变量
    static bool isChange=true;
    if (isChange) {
        self.view.backgroundColor=[UIColor redColor];
    }
    else
    {
        self.view.backgroundColor=[UIColor blackColor];
    }
    //此时必须还要赋过来  然后才可以 红色与黑色交替变换
    isChange=!isChange;
}
 
//通知中心  把方法通过一个控制器传递给另外一个控制器  
[[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(changeBackColor) name:@"CHANGE_COLOR" object:nil];
两个控制器都必须有同样的通知  都是同一个方法的实现才是可以的  颜色是可以不一样的
 
//滚动条下面的脚视图
 _tableView.tableFooterView = titleView;
 
 
 /*
         UITableViewCellAccessoryDetailDisclosureButton为蓝色小园按钮,**可点击**
         */
 
 
上百个视图的画 大项目  用Single View Application来做做设计  很号的理清自己的思路  视图太多了 不好弄  刚开始尽量不要和代码连接在一起!  拖空间
 
工作的前两年 还是手写代码
 
 
//%2d表示显示两位数字 %02d表示只有各位的时候  十位是0
    NSString *str = [NSString stringWithFormat:@"%02d:%02d",random1,random2];
 
 
//查找的第一响应事件
- (BOOL)searchBarShouldBeginEditing:(UISearchBar *)searchBar
{
    //创建UIControl一层界面
    _searchBg = [[UIControl alloc] initWithFrame:CGRectMake(0, 0, 320, 240)];
    [_searchBg addTarget:self action:@selector(backgroundTap:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:_searchBg];
    [_searchBg release];
    return YES;
}
- (void)backgroundTap:(id)sender {
    UISearchBar *bar = (UISearchBar *)[self.view viewWithTag:110];
    [bar resignFirstResponder];
    if (_searchBg) {
        [_searchBg removeFromSuperview];
    }
}
 
 
//一般这种写法是表示自动拉伸对齐
mTableView.autoresizingMask = UIViewAutoresizingFlexibleWidth |UIViewAutoresizingFlexibleHeight;
 
 
 
在应用UIBUtton的时候我们有时需要同时设置图片和文字,下面代码仅供参考:
 
  UIButton *_backButton = [UIButton buttonWithType:UIButtonTypeCustom];
    [_backButton setFrame:CGRectMake(12, 8, 64, 28)];
    [_backButton setTitle:@"返回" forState:UIControlStateNormal];        //设置button在没有选中的时候显示的字体
    _backButton.titleLabel.font = [UIFont systemFontOfSize:14.0f];         //设置button显示字体的大小
    [_backButton setBackgroundImage:[UIImage imageNamed:@"backButton.png"] forState:UIControlStateNormal];    //设置button背景显示图片
    [self.view addSubview:_backButton];
 
 
出了上面的操作意外,我们还可以同时设置button在选中(按下之后)以及正常状态下(没有选中)显示文字和图片的不同,
 
    UIButton *_backButton = [UIButton buttonWithType:UIButtonTypeCustom];
    [_backButton setFrame:CGRectMake(12, 8, 64, 28)];
    [_backButton setTitle:@"返回" forState:UIControlStateNormal];
    [_backButton setTitle:@"Down" forState:UIControlStateHighlighted];
    [_backButton setBackgroundColor:[UIColor clearColor]];
    [_backButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
    _backButton.titleLabel.font = [UIFont systemFontOfSize:14.0f];
    [_backButton setBackgroundImage:[UIImage imageNamed:@"backButton.png"] forState:UIControlStateNormal];
     [_backButton setBackgroundImage:[UIImage imageNamed:@"DownButton.png"] forState:UIControlStateNormal];
    [_toolBar addSubview:_backButton];
 
 
 
 
 //自定义导航条添加按钮,并且添加事件
    UIBarButtonItem *btn=[[UIBarButtonItem alloc]initWithTitle:@"返回" style:UIBarButtonItemStyleBordered  target:self action:@selector(btns:)];
    //放在左边
    self.navigationItem.leftBarButtonItem=btn;
 
 
不管有多少行 都是可以用来代替的。。。。
 
//一般试图器之间发生的关系  都是用一个button  点击事件  然后用导航推出下一个视图
-(void)nextPage:(id)sender
{
//创建下一个视图控制器的对象
    thirdViewController *third = [[thirdViewController alloc]init];
//然后用导航来推出
    [self.navigationController pushViewController:third animated:YES];
    [third release];
}
 
 
 
同步声明一次 就应该析构一次
@property (strong, nonatomic) UISplitViewController *splitViewController;
@synthesize splitViewController = _splitViewController;
 
 
这是必须的 因为声明一次  就会出现一个strong  计数器加上1  就需要释放一次
- (void)dealloc
{
     
     
    [_splitViewController release];
    [super dealloc];
}
 
 
 
//需要牢记的内存管理知识
@property (nonatomic, copy) NSString *descrition;//字符串的声明,里面最好是用copy
//同步的标准写法  加上下划线
@synthesize descrition = _descrition;
 
//立马还需要调用dealoc函数 析构  自己手动写出    高手的指点
- (void)dealloc
{
    self.descrition = nil;
     
    [super dealloc];
}
 
 
另外需要注意的是
@property (assign, nonatomic) NSInteger i;//assign是弱引用,计数器不用加1
而且整形的变量是不需要分配内存空间的  没有指针   所以不能给强引用 ,此时不能用到strong的
 
 
 
//本类中声明成员变量的方法  只是为了本类来使用的 其他的类是用不着的
@implementation Vc02
{
    //只允许本类来使用的成员变量
    NSDictionary *_dict;
}
 
 
//成员变量只要是指针变量  就需要调用dealloc来释放    顺序是先release  然后再放空    声明的全局变量需要   self.descrition = nil;  直接放空 就可以了  需要特别的牢记
 
- (void)dealloc
{
    //成员变量是先释放 然后在放空
    [_dict release];
    _dict = nil;
     
     
    [super dealloc];
}
 
 
 //设置内容尺寸,能够显示内容的尺寸  根据所有图片的大小来确定的
    sv03.contentSize=CGSizeMake(0, 1000);
    //    设置按照页码滚动
    sv03.pagingEnabled=YES;
    //  设置边缘弹动效果,拉到极端的时候可以弹起来
    sv03.bounces=YES;
    //改变滚动条偏移量,从第三张图片开始    ,从顶部第一张图片开始的,就是0与0,这个偏移量挺重要的
    sv03.contentOffset=CGPointMake(0, 320);
    //再把滚动视图添加到  总试图上面
    [self.view addSubview:sv03 ];
    [sv03 release];
 
 
 
//添加右边的图片
    UIImageView *imageView01=[[UIImageView alloc]initWithImage:[UIImage imageNamed:@"衣装"]];
    imageView01.frame=CGRectMake(170, 0, 150, 75);
    imageView01.userInteractionEnabled =YES;
     
    UIButton *button01=[UIButton buttonWithType:UIButtonTypeCustom];
    button01.frame=CGRectMake(0, 0, 150, 75);
    [button01 addTarget:self action:@selector(press01) forControlEvents:UIControlEventTouchUpInside];
    //加一个nil  表示透明的button   与下面一句话的意思差不多
    //[button setImage:nil forState:UIControlStateNormal];
    [button01 setBackgroundColor:[UIColor clearColor]];
    [imageView01 addSubview:button01];
     
    [self.view addSubview:imageView01];
    [imageView01 release];
 
 
 
UIAlertView  是不用添加到试图上面 的
 
但是UIActionSheet  是必须要添加到试图上面
 
 
 
全局的成员变量 需要用dealloc函数来释放的  记住;
@property(strong,nonatomic)UILabel *lab01;
@synthesize lab01;
 
self.lab01=[[[UILable alloc]init]autorelease];//这里用到了点语法,所以计数器是加1的;后面需要一个自动释放池才可以,程序快结束的时候来调用,用点语法才是正常调用了@property与@synthesize   要不然只是声明。。。。全局的成员变量还要用到dealloc来析构一次才可以
 
一般不建议这样用  ,最好还是用[lab01  release];
 
 
//UIActionSheet是最外层的  所以一般不能直接添加到试图view上面  记住!!
UIImageView *imageview = (UIImageView *)[self.view viewWithTag:33333];
    actionsheet=[[UIActionSheet alloc]initWithTitle:nil delegate:self  cancelButtonTitle:@"取消" destructiveButtonTitle:@"分享到新浪微博" otherButtonTitles:@"分享到腾讯微博", nil];
    actionsheet.delegate=self;
    [actionsheet showInView:imageview];
 
    [actionsheet release];
 
 
 
 
//滚动试图在下面 点哪张 加上哪张
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch=[touches anyObject];
    //鼠标点击的位置 确定下来
    CGPoint pt=[touch locationInView:scroll02];
    //点击一次的时候
    if (touch.tapCount==1) {
        for (int i=0; i<18; i++) {
            //根据tag  找到那个图片视图
            UIImageView *image=(UIImageView *)[scroll02 viewWithTag:i+1];
            //设置与用户交互
            image.userInteractionEnabled=YES;
            //
           // int tag=image.tag;
            scroll02.userInteractionEnabled=YES;
            //判断是否点击到了图片
            if (CGRectContainsPoint(image.frame, pt)) {
                //点到了哪张  就把图片的文件名赋给他
                imageName=[NSString stringWithFormat:@"17_%d.jpg",image.tag];
                 
                UIImage *image=[UIImage imageNamed:imageName];
                UIImageView *imageView=[[UIImageView alloc]initWithImage:image];
                imageView.frame=CGRectMake(0, 0, 320, 190);
                [self.view addSubview:imageView];
                [imageView release];
 
            }
             
        }
    }
     
}
 
UITouch 
 
处理用户响应事件,需要重载那三个触摸的方法,开始,触摸到,末尾,
 
 
 
 
 
 
 
 
 
 
 
 
 //用下载的数据 实例化  xml解析器
    GDataXMLDocument *doc=[[GDataXMLDocument alloc]initWithData:downloadData
                                                        options:0 error:nil];
    //解析器 创建成功 (下载的数据  是合法的xml文件)
    if (doc) {
         
        //从解析器中用xpath  语法   查找  我要找的节点   返回值不管怎么样 都是数组类型
        NSArray *usersArray=[doc nodesForXPath:@"//user" error:nil];
         
         
         
         
        //xml文档中  的所有的  节点 都是   GDataXMLElement类的  实例(对象)
        for (GDataXMLDocument *element in usersArray) {
            // [element childAtIndex: ];//返回的第几个子节点
            //            [element childCount];
            //返回element  节点的子节点  为数组
             
            //[element children];
            //            [element attributeForName:@""];   获得element  节点的指定名称  的属性 节点  
            //获得element节点的所以的属性节点
            //            [element attribute];
             
            //实例化  模型类  对象
            UserItem *item=[[[UserItem alloc]init] autorelease];
            //从当前节点中查找 指定名称的子节点  elementsForName  所有的节点 都是element
            //            NSArray *subArray=[element elementsForName:@"uid"];
            //            //uid节点
            //           
            //            GDataXMLElement *uidElement=[subArray lastObject];
            //           
            //            //获取节点的 文本信息
            //            item.uid=[uidElement  stringValue];
            item.uid=[self elementString:element name:@"uid"];
            item.username=[self elementString:element name:@"username"];
            item.realname=[self elementString:element name:@"realname"];
            item.headimage=[NSString stringWithFormat:@"http://192.168.88.8/sns%@",[self elementString:element name:@"headimage"]] ;
             
             
            [dataArray addObject:item];
        }
         
        isLoading=NO;
        [self.tableView reloadData];
    }
     
 
 
 
 
 
高亮情况下的button的实现方式(相当于设置两张图片,一张是为默认准备的,一张是高亮准备的)
 
//默认的情况是出现一张图片
    UIButton * rightbtn = [UIButton buttonWithType:UIButtonTypeCustom];
    [rightbtn setImage:[UIImage imageNamed:@"NextMenuButton.png"] forState:UIControlStateNormal];
    //高亮的时候是出现另外一张图片
    [rightbtn setImage:[UIImage imageNamed:@"NextMenuButtonSelected.png"] forState:UIControlStateHighlighted];
    rightbtn.frame = CGRectMake(200, 0, 40, 40);
    [rightbtn addTarget:self action:@selector(pressrightbtn) forControlEvents:UIControlEventTouchUpInside];
    [navbar addSubview:leftbtn];
    [navbar addSubview:rightbtn];
 
 
自定义导航栏以及导航栏上面的 按钮   的基本方法
  
    //自己定义导航栏
    UINavigationBar * navbar = [[UINavigationBar alloc] initWithFrame:CGRectMake(0, 0, 320, 40)];
    //导航栏设置背景图片
    [navbar setBackgroundImage:[UIImage imageNamed:@"NavigatorBarBg"] forBarMetrics:UIBarMetricsDefault];
 
    //创建这个对象很重要的
        UINavigationItem * item =[[UINavigationItem alloc] init];
 
 
 //导航栏上面的返回按钮
    UIBarButtonItem * btn1 = [[UIBarButtonItem alloc] initWithTitle:@"返回" style:UIBarButtonItemStyleDone target:self action:@selector(pressbtn1)];
    btn1.tintColor = [UIColor yellowColor];
    item.leftBarButtonItem = btn1;
    [btn1 release];
 
 
//没有这句话 回来的时候就推不过去的
    [navbar pushNavigationItem:item animated:YES];
    [item release];
    [self.view addSubview:navbar];
    [navbar release];
 
 
 
//这么重要的函数老师居然没有讲   我靠!!
//此函数  如果是  return YES则就是四个方向都是支持的  竖着,倒着,左横,右横;  但系统默认只是正着的状态
 
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
    // Return YES for supported orientations
   return (interfaceOrientation == UIInterfaceOrientationPortrait);
//    return YES;
}

















作者:prl18353364833 发表于2016/10/9 10:47:50 原文链接
阅读:35 评论:0 查看评论

iOS各种小理论知识

$
0
0

page1image1016

Objective-C 部分

1. 你如何理解 OC 的内存管理
OC 内存管理是基于引用计数。谁想使用某个对象 B,就要把对象 B 的计数器+1,如果不

使用这个对象了,那么就把对象 B 计数器-1,如果 B 对象计数器减到 0,那么 B 对象自动会调用自己的 dealloc 函数,也就是这个对象被销毁。

一句话就是 谁拥有对象,谁负责释放该对象,谁 alloc 谁应该 release 或者 autorelease

系统有一个自动释放池,对于一些对象我们可以放在自动释放池中。如果自动释放池本身销毁,那么里面的内容会被全部调用 release.

  1. assign, retain, copy, autorelease, release, dealloc 的不同assign 就是基本的赋值,就是 C 的基本的赋值retain 就是让对象引用计数+1,表示拥有了这个对象copy 把对象复制一份。copy 是深拷贝,浅拷贝auorelease 把对象放在自动释放池里面

    release 表示对象计数器-1 如果减到 0 那么对象自动 dealloc

  2. self.name= yang”name= “yang”的关系

    self.name 表示调用 setter 调用一个函数

    name=”year”表示给字段 field 赋值

  3. 什么是深拷贝和浅拷贝 copy

    浅拷贝只是拷贝对象本身,不递归的拷贝里面的子对象深拷贝是把对象以及对象的对象递归拷贝。注意强调 mutableCopy 不是深拷贝如果要对自己对象 BookModel 实现拷贝 那么必须实现 NSCopying 协议实现里面的 - (id) copyWithZone:(NSZone *)zone;

  4. copy mutableCopy 是什么
    mutableCopy 可变拷贝 NSString -- mutableCopy- NSMutableStringNSArray -- mutableCopy- NSMutableArray;
    NSMutableString -
    copy---NSString

  5. #import #include 区别

#import 对于包 多个相同文件只是包 一次 防止了重复包

#include 需要加上#ifndef __QF_Header__#define __QF_Header__

#endif
7. OC
有没有 GC/Gabbage Collector 垃圾回收

iPhone/OC 没有 GC 当时 Mac 本地的 OC 是有 GC 8. KVC 是什么,有什么好处

KVC 就是 key value coding. 主要用来大量的对象赋值上。比如网络下载 json 数据进行解析,然后要给数据模型赋值。

bookModel.name = [dict objectForKey:@”name”];

bookModel.author = [dict objectForKey:@”author”];如果要用 kvc 那么就可以简写成

NSArray *arr = [NSArray arrayWithObjects:@”name”, @”author”, nil];for(NSString *field in arr) {

NSString *v = [dict objectForKey:field];

bookModel setValue:v forKey:field];}

kvc 2 个方法 setValue:forKey:valueForKey:方法分别对应 setter getter 方法KVO 主要用在属性的监 上。KVO 比如用在播放视频的时候我想知道当前的播放进度

协议和代理的关系和区别协议就是规范接口(让每个对象都有相同的函数),主要是用来在多个对方中数据传输的统一接口。类似于 java 中得接口。OC 协议的所有方法不一定要全部实现。@optional,当时java 中得接口全部需要实现。
代理是一种设计模式。在
iOS 中主要是用来做反向传值。

OC 里面的 NSArray, NSDictionary 如何实现的NSArray 使用链表实现, NSDictionary 使用 hash

什么是单例?单例就是一个全局对象,就是只有一个对象,目的是为了数据共享。单例在多线程重要注意什么? 需要加锁 OC ,避免多个线程同时创建单例

+(id) sharedInstance {static id _s;@synchronized(self) {

if (_s == nil) _s = [[[self class] alloc] init];}

return _s;}

界面之间传值有哪些方法?
正向传就是属性,方法
反向就是代理。用单例,用
blocks,用数据库,通知中心

NSNotification 原理是啥?原理就是接收端把接受函数注册到一个数组中,发送端发送数据就直接调用之前存得函


NSNotification 是同步的。不是异步的。

1. NSNotification是同步还是异步的

是同步的. 如果需要异步 必须使用 NSNotificationQueue 实现
A notification center delivers notifications to observers synchronously. In other words, thepostNotification: methods do not return until all observers have received and processed thenotification. To send notifications asynchronously use NSNotificationQueue.

In a multithreaded application, notifications are always delivered in the thread in which the

page2image12528

notification was posted, which may not be the same thread in which an observer registereditself.

Hope it helps you.

类别 Category 和继承区别类别是用在给一个类扩展方法,不用保存变量,因为类别不能保存变量集成扩展方法和变量。比较重一些。类别 Category 相对轻量一些。当时不能完全替代集成。

你如何理解 ScrollView 复用, TableView 才有下拉刷新原理是?
EGO

iOS5, iOS6, iOS7 适配if (isIOS7) {

// iOS7 导航控制器不透明self.navigationController.navigationBar.translucent = NO;self.edgesForExtendedLayout = UIRectEdgeNone;
// 可以认为上 2 让它恢复成iOS6的经典模式

}

对于图 需要 如实现 个宏来取得正确的版本的图 iphone5 度适配,要动态的获取 度

网络部分

2. 你如何理解线程
iOS 线程是为了界面流畅,防止界面假死.比如解析一个大文件,比如一个地区详情文件,解析时间比如是 10s,那么不用线程的话,界面就会假死/10s,用户体验差,那么解决方法就是很快把界面暂时,启动线程去后台解析。
再比如,做图片滤镜。图像操作会耗费很多时间,比如
5s,那么界面会假死 5s,那么用户体验差,解决方法是启动线程在后台使用滤镜算法处理图像,一旦处理完就通知主线程刷新界面.在比如:网络下载数据,由于网络状态好坏时间会不确定,那么我们需要使用线程来处理这种不确定的关系,一定数据接收完成,那么就可以通知主线程处理。再比如读取数据全国电话号码数据库,时间很长,那么就需要通过线程来做特别强调的是,线程不是快。是为了给用户一个快得假象

page3image8536


3. 你如何理解线程池线程池就是一堆项目的集合,就是一个线程队列来管理这些线程对象。如果需要处理一

些任务,可以从线程池中直接取线程,而不是临时创建线程。比如 NSOperationQueue 就是一个线程池,管理 NSOperation 线程对象

线程锁 NSLock
线程锁是多个线程要访问同一个资源引入的一种机制。比如 2 个线程同时写数据文件,那么需要用锁 NSLock 来保证同一个时刻只能有一个线程写。写结束后另外一个线程才能接着写。比如创建单例的时候,为了保证哪怕多个线程同时创建单例,也只能创建一个单例对象。

4. 你如何理解图片下载
图片下载一般使用
Lazy 懒加载和图片缓存的模式。懒加载就是需要的时候才去临时记载。

主要是节省内存空间和用户流量。缓存就是把 url 地址做成 md5 存在沙盒中以便下次直接使用。

3. ASI 的原理是啥??
ASI 使用 Apple 底层的 CFNetwork 框架实现的。而不是用 BSD Socket 实现的。他是一个基于 NSOperation 的线程处理网络框架。CFNetwork 是基于 InputStream/Outstream 流的方式管理数据。它内部使用了多线程异步模式进行数据的通讯。比如数据的上传进度,下载进度,缓存的管理机制。大文件下载,大文件上传,安全机制等。

4. SDWebImage 原理是什么?
SDWebImage
是一个基于 NSOperation/NSInvokeOperation 的一个多线程图片缓存框架framework,
原理是每次通过[uiimageview setImageWithUrl:]就启动一个异步线程,负责下载网络图片,一旦下载完成就把网址使用 md5 做一个校验 checksum,然后存在一个沙盒 sandbox 管理目录中,以便下次可以直接从 sandbox 中读取,而不需要从网络上下载,目的是为了节省用户流量,加快图片访问速度。

5. HTTP 原理是?HTTP是基于TCP/IP的一个网络连接协议,它是客户端和服务器通讯的常用方式。 他

是使用请求,响应模型,客户端请求,服务端响应的方式。HTTP 协议有请求头,请求体,响应头,响应体。XML/Json 是一般作为响应的数据格式传输。GET/POST 是和请求的 2 中常见方式。还有 2 PUT, DELETE

TCP/IP 的原理是?
TCP/IP 是网络底层的二进制协议。它是网络传输的标准协议。 TCP 是稳定,可靠的点对

点传输,UDP 是不可靠的,高速的网络传输协议。在手机客户单中,一般用在聊天类IM(Instance Message),或者视频流,语音等大数据中。TCP 可以和服务器保持长连接。一般使用Socket来进行TCP/IP开发程序,常见的套接字是AsyncSocket, BSD Socket。或者CFNetwork 都可以

page4image10104


HTTP 协议头上加上 Range 字段就可以使用断点下载,每次取得文件大小,然后设置

Range 字段,然后下载的数据追加到文件的最后。这种方式就是断点下载。

Socket 原理是什么?
AsyncSocket iOS 中一个 TCP/IP 通讯的开源的套接字框架。比 BSD Socket 好用一些。

AsyncSocket 使用了 CFNetwork 中异步回调模型。所有的收发数据都是使用代理来通知。内部基于 CFNetwork 里面的 Stream 流的模式。他支持 TCP/UDP,支持 UDP 广播,组播,比如 XMPP 里面就用了 AsyncSocket 来进行底层通讯。

Cookie 原理
Cookie 类似于 Token,主要是为了让服务端能够识别当前用户。在手机上一般使用 token模式。在浏览器上比较喜欢 cookie, iOS NSUrlConnection ASI 都支持 Cookie, Cookie 就是一块小的数据存储空间。

MVC
MVC Model, View, Controller 的简称,主要是为了解决 Model View 界面之间耦合度的问题(保证低耦合)。保证数据模型和刷新界面相对独立。Controller 就是在中间把 ModelView 耦合在一起。因为大型软件要求 Model 尽量和界面进行独立一些。比如很多界面有列表模式,地图模式。界面有 2 分,当时数据模型就一份。

比如:iOS 常见的开发大多基于 MVC 模型,比如 Controller 去启动网络,数据处理,然后解析数据,然后建立数据模型,然后把数据模型关联到 TableViewCell 上。这种就是典型的 MVC

6. weibo/OAuth 分享原理是什么
Weibo 使用了 OAuth 网络协议。目前使用 OAuth2.0 协议。他是基于 HTTP 的一个网络认

证协议,通过 AppKey 等调用 weibo 的页面,然后用户输入账号,weibo 密码服务器返回成功后的 token

7. 微信,支付宝,银联原理是什么微信,支付宝,银联都是在真机上使用。我们在程序中调用微信,支付宝,银联等程序,

然后给他们传入相应地参数接口,这个就是 2 个程序之间项目的传输数据。是通过 URL 进行识别的。具体他们协议都没有公开,所以我们需要他们自己的 SDK 进行开发。

8. XMPP 原理?
XMPP
是基于 Socket 套接字的一个专门聊天 IM 的协议。他支持注册,登陆,以及单聊,群组聊天功能。对图片,语音,文件支持不好,一般使用 HTTP 来进行转发一下。或者做一个 base64 编码。XMPP 是一个即时聊天框架,支持和 googletalk msn 早期的 qq 进行通讯。他内部支持重连机制(断线后自动重连)。心跳机制。内部使用 CoreData 进行数据的管理。

CoreData 是什么?
CoreData Apple 自己的一个数据库管理框架,它好处是可以避免使用 sql 语句,是一个图形化的操作框架,当时从开发者角度来说,不然 FMDB/sql 直接使用 sql 语句更方便.

page5image12264

HTTP 断点下载原理


FMDB 可以支持支持标准的 sql 语句。支持事务 Transition,回滚 roolback 等。

CoreText 是什么?
CoreText Apple 官方一个标准专业排版框架。它可以精确的处理图片,问题的位置。一般用 CoreText 用在图文混排上。

FMDB 的原理是?
FMDB 是一个开源的封装了 Apple 底层 C sqlite3 数据库,他使用方便。支持标准 sql 语句,支持多线程处理,支持是事务,回滚等。

page6image3160
作者:prl18353364833 发表于2016/10/9 10:52:25 原文链接
阅读:35 评论:0 查看评论

Android7.0 PackageManagerService (3) APK安装

$
0
0

在本篇博客中,我们分析一下Android中的APK是如何安装的,以及PKMS在这个过程中进行了哪些工作。

APK的安装方式有很多,我们先来看看如何用adb命令进行安装。
我们从adb install开始分析,该命令有多个参数,这里仅考虑最基本的adb install xxxx.apk。

一、adb命令
看看system/core/adb/commandline.cpp中的adb_commandline函数:

int adb_commandline(int argc, const char **argv) {
    ...........
    else if (!strcmp(argv[0], "install")) {
        if (argc < 2) return usage();
        FeatureSet features;
        std::string error;
        if (!adb_get_feature_set(&features, &error)) {
            fprintf(stderr, "error: %s\n", error.c_str());
            return 1;
        }

        if (CanUseFeature(features, kFeatureCmd)) {
            //支持FeatureCmd时调用install_app
            return install_app(transport_type, serial, argc, argv);
        }
        //否则,利用install_app_legacy
        return install_app_legacy(transport_type, serial, argc, argv);
    }
    ...........
}

1、install_app_legacy
我看先看看传统的install_app_legacy:

static int install_app_legacy(TransportType transport, const char* serial, int argc, const char** argv) {
    //待安装的APK目前还在源机器上,现在需要把APK的文件复制到手机里
    //如果安装在手机内部存储,那么目的地址为DATA_DEST
    //如果安装在SD卡上,则目的地址为SD_DEST
    static const char *const DATA_DEST = "/data/local/tmp/%s";
    static const char *const SD_DEST = "/sdcard/tmp/%s";
    .........
    //默认安装到手机内部
    const char* where = DATA_DEST;
    for (i = 1; i < argc; i++) {
        //携带参数-s时,才安装到SD卡
        if (!strcmp(argv[i], "-s")) {
            where = SD_DEST;
        }
    }

    //解析参数,判断adb命令中是否携带了有效的apk文件名
    ...........

    //取出apk名
    std::vector<const char*> apk_file = {argv[last_apk]};
    //构造apk目的地址
    std::string apk_dest = android::base::StringPrintf(
            where, adb_basename(argv[last_apk]).c_str());

    //do_sync_push将此APK文件传输到手机的目标路径,失败的话将跳转到clenaup_apk
    if (!do_sync_push(apk_file, apk_dest.c_str())) goto cleanup_apk;

    //执行pm_command
    result = pm_command(transport, serial, argc, argv);

cleanup_apk:
    //删除刚才传输的文件
    //PKMS在安装过程中会将该APK复制一份到/data/app目录下,所有data/local/tmp目录下对应的文件可以删除
    delete_file(transport, serial, apk_dest);
    return result;
}

从代码来看,传统的安装方式就是将源机器中的APK文件拷贝到目的手机的tmp目录下,然后调用pm_command进行处理。

2、install_app
我们再看看支持FeatureCmd的机器,如何安装APK:

static int install_app(TransportType transport, const char* serial, int argc, const char** argv) {
    //利用参数创建出本地文件的名称
    const char* file = argv[argc - 1];

    //解析参数,判断adb命令中是否携带了有效的apk文件名
    .........

    //adb_open中将创建出这个file对应的文件
    int localFd = adb_open(file, O_RDONLY);
    ............
    std::string cmd = "exec:cmd package";
    //添加cmd参数
    ............
    //连接源端,获取源APK文件的描述符
    int remoteFd = adb_connect(cmd, &error);
    ............
    //将remoteFd中的数据写入到localFd
    copy_to_file(localFd, remoteFd);
    //得到结果
    read_status_line(remoteFd, buf, sizeof(buf));

    adb_close(localFd);
    adb_close(remoteFd);
    ..........
    return 0;
}

从代码来看install_app就是将源机器的文件复制到了目的机器中,并没有进行额外的操作。猜想可能是支持特殊FeatureCmd的机器,PKMS能够监听到这个拷贝,然后触发后续的扫描工作。这个过程没有研究过对应代码,暂时不做深入分析。

对于传统的安装方式,我们需要继续往下看看pm_command。

二、pm_command
我们先看看pm_command函数:

static int pm_command(TransportType transport, const char* serial, int argc, const char** argv) {
    std::string cmd = "pm";

    //构造pm cmd
    while (argc-- > 0) {
        cmd += " " + escape_arg(*argv++);
    }

    //发送shell命令给adbd
    return send_shell_command(transport, serial, cmd, false);
}

我们跟进下send_shell_command:

// Connects to the device "shell" service with |command| and prints the
// resulting output.
static int send_shell_command(TransportType transport_type, const char* serial,
                              const std::string& command,
                              bool disable_shell_protocol,
                              std::string* output=nullptr,
                              std::string* err=nullptr) {
    ...........
    while (true) {
        bool attempt_connection = true;

        // Use shell protocol if it's supported and the caller doesn't explicitly disable it.
        if (!disable_shell_protocol) {
            .......
            if (adb_get_feature_set(&features, &error)) {
                //如果定义了feature,则替换shell protocol
                use_shell_protocol = CanUseFeature(features, kFeatureShell2);
            } else {
                // Device was unreachable.
                attempt_connection = false;
            }
        }

        if (attempt_connection) {
            std::string error;
            //此时command中携带的就是以pm开头的命令
            std::string service_string = ShellServiceString(use_shell_protocol, "", command);

            //向shell服务发送命令
            fd = adb_connect(service_string, &error);
            if (fd >= 0) {
                break;
            }
        }
        ............
    }

    //读取返回结果
    int exit_code = read_and_dump(fd, use_shell_protocol, output, err);
    if (adb_close(fd) < 0) {
        ..........
    }

    return int exit_code;
}

从上面的代码来看,pm_command就是向shell服务发送pm命令。

pm是一个可执行脚本,我们在终端上调用adb shell,然后执行pm,可以得到以下结果:

root:/ # pm
usage: pm list packages [-f] [-d] [-e] [-s] [-3] [-i] [-u] [--user USER_ID] [FILTER]
       pm list permission-groups
       pm list permissions [-g] [-f] [-d] [-u] [GROUP]
       pm list instrumentation [-f] [TARGET-PACKAGE]
..........

pm脚本定义在frameworks/base/cmds/pm中:

base=/system
export CLASSPATH=$base/framework/pm.jar
exec app_process $base/bin com.android.commands.pm.Pm "$@"

在编译system.img时,会根据Android.mk将该脚本复制到system/bin目录下。
从脚本的内容来看,当调用pm时,将向app_process目录的main函数传入Pm对应的参数:

我们看看对应的定义于app_main.cpp的main函数(前面的博客分析过,这个其实也是zygote启动的函数):

//app_process的main函数
int main(int argc, char* const argv[]) {
    ........
    //解析参数
    while (i < argc) {
        const char* arg = argv[i++];
        if (strcmp(arg, "--zygote") == 0) {
            zygote = true;
            niceName = ZYGOTE_NICE_NAME;
        } else if (strcmp(arg, "--start-system-server") == 0) {
            startSystemServer = true;
        } else if (strcmp(arg, "--application") == 0) {
            application = true;
        } else if (strncmp(arg, "--nice-name=", 12) == 0) {
            niceName.setTo(arg + 12);
        } else if (strncmp(arg, "--", 2) != 0) {
            //此时我们有参数,进入该分支设置className
            className.setTo(arg);
            break;
        } else {
            --i;
            break;
        }
    }
    ...........
    if (zygote) {
        runtime.start("com.android.internal.os.ZygoteInit", args, zygote);
    } else if (className) {
        //此时不再是启动zygote,而是启动className对应的类
        runtime.start("com.android.internal.os.RuntimeInit", args, zygote);
    } else {
        .........
    }
    ........
}

我们跟进AndroidRuntime.cpp的start函数:

void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote)
{
    ..........
    jmethodID startMeth = env->GetStaticMethodID(startClass, "main",
            "([Ljava/lang/String;)V");
    if (startMeth == NULL) {
        ALOGE("JavaVM unable to find main() in '%s'\n", className);
    } else {
        //反射调用main函数,从native层进入java世界
        env->CallStaticVoidMethod(startClass, startMeth, strArray);
    }
    .........
}

于是流程会进入到RuntimeInit的main函数:

public static final void main(String[] argv) {
    ........
    //进行一些常规的初始化工作
    commonInit();
    /*
    * Now that we're running in interpreted code, call back into native code
    * to run the system.
    */
    nativeFinishInit();
    .........
}

native函数定义在framework/base/core/jni/AndroidRuntime.cpp中,对应的函数为:

static void com_android_internal_os_RuntimeInit_nativeFinishInit(JNIEnv* env, jobject clazz)
{
    //gCurRuntime保存AndroidRuntime,实际上是AndroidRuntime的子类
    gCurRuntime->onStarted();
}

App_main.cpp中定义的AppRuntime继承AndroidRuntime,实现了onStarted函数:

virtual void onStarted()
{
    //binder通信相关的
    sp<ProcessState> proc = ProcessState::self();
    ALOGV("App process: starting thread pool.\n");
    proc->startThreadPool();

    AndroidRuntime* ar = AndroidRuntime::getRuntime();
    //调用AndroidRuntime.cpp的callMain函数,参数与Pm.java相关
    ar->callMain(mClassName, mClass, mArgs);

    IPCThreadState::self()->stopProcess();
}
status_t AndroidRuntime::callMain(const String8& className, jclass clazz,
        const Vector<String8>& args) {
    ..........
    env = getJNIEnv();
    ..........
    methodId = env->GetStaticMethodID(clazz, "main", "([Ljava/lang/String;)V");
    ..........
    const size_t numArgs = args.size();
    stringClass = env->FindClass("java/lang/String");
    strArray = env->NewObjectArray(numArgs, stringClass, NULL);

    for (size_t i = 0; i < numArgs; i++) {
        jstring argStr = env->NewStringUTF(args[i].string());
        env->SetObjectArrayElement(strArray, i, argStr);
    }
    ...........
    //最终调用了Pm.java的main函数
    env->CallStaticVoidMethod(clazz, methodId, strArray);
    return NO_ERROR;
}

这里自己初次看时,认为这里没有fork新的进程,那么APK安装运行在zygote进程中。
实际上这是一个错误的理解,说明自己的理解还不到位。
init创建zygote进程时,是fork出一个子进程,然后才调用app_main中的函数,此时整个zygote严格来讲只是一个native进程;当app_main函数最终通过AndroidRuntime等反射调用zygoteInit.java的main函数后,才演变成了Java层的zygote进程。
这里的情况是类似的,adb进程发送消息给Shell服务,Shell服务执行Pm脚本,由于exec函数并未创建出新的进程,因此调用app_main后整个代码仍然是运行在Shell服务对应的native进程中,同样通过反射后演变为Java层中的进程。

这里自己花了很多的笔墨来分析如何从执行脚本文件,到启动Java进程。
主要是弄懂这个机制后,我们实际上完全可以学习pm的写法,依葫芦画瓢写一个脚本文件,然后定义对应的Java文件。
通过脚本命令,来让Java层的进程提供服务。

最后,我们通过一个图来总结一下这个过程:

三、Pm中的流程
现在我们进入了Pm.java的main函数:

public static void main(String[] args) {
    int exitCode = 1;
    try {
        //别被写法欺骗了,Pm并没有继承Runnable
        exitCode = new Pm().run(args);
    } catch (Exception e) {
        .......
    }
    System.exit(exitCode);
}

//根据参数进行对应的操作,现在我们仅关注APK安装
public int run(String[] args) throws RemoteException {
    ...........
    //利用Binder通信,得到PKMS服务端代理
    mPm = IPackageManager.Stub.asInterface(ServiceManager.getService("package"));

    //保存参数
    mArgs = args;
    String op = args[0];
    mNextArg = 1;

    ............
    //返回PKMS中保存的PackageInstallerService
    mInstaller = mPm.getPackageInstaller();
    ........
    if ("install".equals(op)) {
        //安装APK将调用runInstall
        return runInstall();
    }
    .......
}

我们跟进runInstall函数:

private int runInstall() throws RemoteException {
    //根据参数创建InstallParams,其中包含了SessionParams,标志为MODE_FULL_INSTALL
    final InstallParams params = makeInstallParams();
    //1 创建Session
    final int sessionId = doCreateSession(params.sessionParams,
            params.installerPackageName, params.userId);
    try {
        //inPath对应于安装的APK文件
        final String inPath = nextArg();
        .......
        //2 wirite session
        if (doWriteSession(sessionId, inPath, params.sessionParams.sizeBytes, "base.apk",
                false /*logSuccess*/) != PackageInstaller.STATUS_SUCCESS) {
            return 1;
        }
        //3 commit session
        if (doCommitSession(sessionId, false /*logSuccess*/)
                != PackageInstaller.STATUS_SUCCESS) {
            return 1;
        }
        System.out.println("Success");
        return 0;
    } finally {
        ........
    }
}

从上面的代码来看,runInstall主要进行了三件事,即创建session、对session进行写操作,最后提交session。
接下来,我们来看看每一步究竟在干些什么:

1、 create session

private int doCreateSession(SessionParams params, String installerPackageName, int userId)
        throws RemoteException {
    //通过ActivityManagerService得到"runInstallCreate"(作为Context对应的字符串)对应的uid
    userId = translateUserId(userId, "runInstallCreate");
    if (userId == UserHandle.USER_ALL) {
        userId = UserHandle.USER_SYSTEM;
        params.installFlags |= PackageManager.INSTALL_ALL_USERS;
    }

    //通过PackageInstallerService创建session
    final int sessionId = mInstaller.createSession(params, installerPackageName, userId);
    return sessionId;
}

跟进一下PackageInstallerService的createSession函数:

@Override
public int createSession(SessionParams params, String installerPackageName, int userId) {
    try {
        return createSessionInternal(params, installerPackageName, userId);
    } catch (IOException e) {
        throw ExceptionUtils.wrap(e);
    }
}

private int createSessionInternal(SessionParams params, String installerPackageName, int userId)
        throws IOException {
    //安装权限检查
    .......
    //修改SessionParams的installFlags
    if ((callingUid == Process.SHELL_UID) || (callingUid == Process.ROOT_UID)) {
        params.installFlags |= PackageManager.INSTALL_FROM_ADB;
    } else {
        .........
    }
    ..........
    // Defensively resize giant app icons
    //调整app图标大小,这里应该是不同安装方式共用的代码
    //通过adb安装apk时,应该还没有解析到app图标
    if (params.appIcon != null) {
        ........
    }

    //根据SessionParams的installFlags进行一些操作
    ..........
    } else {
        // For now, installs to adopted media are treated as internal from
        // an install flag point-of-view.
        //adb安装应该进入这个分支(不添加参数指定安装在sd card时),为SessionParams设置InstallInternal Flag,后文会用到
        params.setInstallFlagsInternal();
        ...........
    }

    final int sessionId;
    final PackageInstallerSession session;
    synchronized (mSessions) {
        // Sanity check that installer isn't going crazy
        //确保同一个uid没有提交过多的Session,MAX_ACTIVE_SESSIONS为1024
        final int activeCount = getSessionCount(mSessions, callingUid);
        if (activeCount >= MAX_ACTIVE_SESSIONS) {
            throw new IllegalStateException(
                    "Too many active sessions for UID " + callingUid);
        }

        //同样确保同一个uid没有提交过多的Session,MAX_HISTORICAL_SESSIONS为1048576
        final int historicalCount = getSessionCount(mHistoricalSessions, callingUid);
        if (historicalCount >= MAX_HISTORICAL_SESSIONS) {
            throw new IllegalStateException(
                    "Too many historical sessions for UID " + callingUid);
        }
        ........
        //sessionId是个随机值
        sessionId = allocateSessionIdLocked();

        // We're staging to exactly one location
        File stageDir = null;
        String stageCid = null;
        //根据installFlags,决定安装目录,前文已经提到,过默认将安装到internal目录下
        if ((params.installFlags & PackageManager.INSTALL_INTERNAL) != 0) {
            final boolean isEphemeral =
                    (params.installFlags & PackageManager.INSTALL_EPHEMERAL) != 0;
            //此处将会在临时性的data目录下创建出file,应该是作为copy的目的地址
            stageDir = buildStageDir(params.volumeUuid, sessionId, isEphemeral);
        } else {
            stageCid = buildExternalStageCid(sessionId);
        }

        session = new PackageInstallerSession(mInternalCallback, mContext, mPm,
                mInstallThread.getLooper(), sessionId, userId, installerPackageName, callingUid,
                params, createdMillis, stageDir, stageCid, false, false);
                mSessions.put(sessionId, session);
        mSessions.put(sessionId, session);
    }
    //进行回调
    mCallbacks.notifySessionCreated(session.sessionId, session.userId);
    //在mSessionsFile中进行记录
    writeSessionsAsync();

    return sessionId;
}

从代码来看,上述代码的目的就是为APK安装做好准备工作,例如权限检查、目的临时文件的创建等, 最终创建出PackageInstallerSession对象。PackageInstallerSession可以看做是”安装APK”这个请求的封装,其中包含了处理这个请求需要的一些信息。
这种设计方式,大致可以按照命令模式来理解。

实际上PackageInstallerSession不仅是分装请求的对象,其自身还是个服务端:

public class PackageInstallerSession extends IPackageInstallerSession.Stub 

2、write session
创建出PackageInstallerSession后,我们看看Pm.java中的doWriteSession函数:

 private int doWriteSession(int sessionId, String inPath, long sizeBytes, String splitName,
        boolean logSuccess) throws RemoteException {
    if ("-".equals(inPath)) {
        inPath = null;
    } else if (inPath != null) {
        //此时file指向了待安装的APK文件(adb执行拷贝后的目的地址)
        final File file = new File(inPath);
        if (file.isFile()) {
            sizeBytes = file.length();
        }
    }
    ......
    //取出PackageInstallerSession中的SessionInfo
    final SessionInfo info = mInstaller.getSessionInfo(sessionId);

    PackageInstaller.Session session = null;
    InputStream in = null;
    OutputStream out = null;
    try {
        //1 获取PackageInstallerSession的调用接口
        session = new PackageInstaller.Session(
                mInstaller.openSession(sessionId));

        if (inPath != null) {
            //定义输入端,待安装APK对应文件的源地址
            in = new FileInputStream(inPath);
        } else {
            in = new SizedInputStream(System.in, sizeBytes);
        }

        //2 定义输出端,对应拷贝后的目的地址
        out = session.openWrite(splitName, 0, sizeBytes);

        int total = 0;
        byte[] buffer = new byte[65536];
        int c;
        //进行文件的拷贝
        while ((c = in.read(buffer)) != -1) {
            total += c;
            out.write(buffer, 0, c);

            if (info.sizeBytes > 0) {
                final float fraction = ((float) c / (float) info.sizeBytes);
                //只是更新进度而已
                session.addProgress(fraction);
            }
        }
        session.fsync(out);
        ......
        return PackageInstaller.STATUS_SUCCESS;
    } catch (IOException e) {
        ........
    } finally {
        IoUtils.closeQuietly(out);
        IoUtils.closeQuietly(in);
        IoUtils.closeQuietly(session);
    }
}

从doWriteSession的代码来看,此处进行的主要工作就是通过Session将源端的数据拷贝到目的端。
其实从整个对象的命名和执行过程来看,这里整个是基于C/S架构的通信过程,Pm作为PackageInstallerService 的客户端,利用PackageInstallerSession来封装每一次完整的通信过程。

2.1 得到PackageInstallerSession的代理对象
我们看看上面代码调用的PackageInstaller.Session的构造函数:

//参数传入的是PackageInstallerService.openSession函数的返回结果,即实际PackageInstallerSession的代理端
public Session(IPackageInstallerSession session) {
    mSession = session;
}

我们看看PackageInstallerService.openSession函数:

@Override
public IPackageInstallerSession openSession(int sessionId) {
    try {
        return openSessionInternal(sessionId);
    } catch (IOException e) {
        throw ExceptionUtils.wrap(e);
    }
}

private IPackageInstallerSession openSessionInternal(int sessionId) throws IOException {
    synchronized (mSessions) {
        //根据sessionId得到之前创建的PackageInstallerSession
        final PackageInstallerSession session = mSessions.get(sessionId);
        if (session == null || !isCallingUidOwner(session)) {
            throw new SecurityException("Caller has no access to session " + sessionId);
        }
        //调用其open函数
        session.open();

        //PacakgeInstallerSession转化为IPackageInstallerSession返回
        return session;
    }
}

//open函数就是准备好待拷贝的目录
public void open() throws IOException {
    .......
        //PackageInstallerService创建出PackageInstallerSession时,传入的prepared参数为false
        if (!mPrepared) {
            if (stageDir != null) {
                prepareStageDir(stageDir);
            } else if (stageCid != null) {
                prepareExternalStageCid(stageCid, params.sizeBytes);
                .....
            } else {
                //throw exception
                ......
            }
            ........
        }
    }
}

2.2 得到客户端
PacakgeInstaller.Session的openWrite函数:

public @NonNull OutputStream openWrite(@NonNull String name, long offsetBytes,
        long lengthBytes) throws IOException {
    try {
        //mSession是PacakgeInstallerSession,这里发生了Binder通信
        final ParcelFileDescriptor clientSocket = mSession.openWrite(name,
            offsetBytes, lengthBytes);
        //引入了FileBridge对象,后文分析
        return new FileBridge.FileBridgeOutputStream(clientSocket);
    } catch (RuntimeException e) {
        ExceptionUtils.maybeUnwrapIOException(e);
        throw e;
    } catch (RemoteException e) {
        throw e.rethrowFromSystemServer();
    }
}

我们看看PacakgeInstallerSession的openWrite函数:

@Override
public ParcelFileDescriptor openWrite(String name, long offsetBytes, long lengthBytes) {
    try {
        return openWriteInternal(name, offsetBytes, lengthBytes);
    } catch (IOException e) {
        throw ExceptionUtils.wrap(e);
    }
}

private ParcelFileDescriptor openWriteInternal(String name, long offsetBytes, long lengthBytes)
    throws IOException {
    // Quick sanity check of state, and allocate a pipe for ourselves. We
    // then do heavy disk allocation outside the lock, but this open pipe
    // will block any attempted install transitions.
    //FileBrige建立了客户端和服务端的管道
    final FileBridge bridge;
    synchronized (mLock) {
        ......
        bridge = new FileBridge();
        mBridges.add(bridge);
    }

    try {
        // Use installer provided name for now; we always rename later
        if (!FileUtils.isValidExtFilename(name)) {
            throw new IllegalArgumentException("Invalid name: " + name);
        }

        //打开文件,定义权限
        final File target = new File(resolveStageDir(), name);
        // holding open FDs into containers.
        final FileDescriptor targetFd = Libcore.os.open(target.getAbsolutePath(),
                O_CREAT | O_WRONLY, 0644);
        Os.chmod(target.getAbsolutePath(), 0644);

        //定义文件内存格式及分配内存
        // If caller specified a total length, allocate it for them. Free up
        // cache space to grow, if needed.
        if (lengthBytes > 0) {
            final StructStat stat = Libcore.os.fstat(targetFd);
            final long deltaBytes = lengthBytes - stat.st_size;
            // Only need to free up space when writing to internal stage
            if (stageDir != null && deltaBytes > 0) {
                mPm.freeStorage(params.volumeUuid, deltaBytes);
            }
            Libcore.os.posix_fallocate(targetFd, 0, lengthBytes);
        }

        //定义起始偏移量
        if (offsetBytes > 0) {
            Libcore.os.lseek(targetFd, offsetBytes, OsConstants.SEEK_SET);
        }

        bridge.setTargetFile(targetFd);
        bridge.start();

        //返回了bridge的client socket
        return new ParcelFileDescriptor(bridge.getClientSocket());
    } catch (ErrnoException e) {
        throw e.rethrowAsIOException();
    }
}

2.2.1 FileBridge
为了更好的理解上述过程,我们需要看看FileBridge:

public class FileBridge extends Thread {
    .......
    private final FileDescriptor mServer = new FileDescriptor();
    private final FileDescriptor mClient = new FileDescriptor();
    .......
    public FileBridge() {
        try {
            //构造函数建立的mServer和mClient之间的管道
            Os.socketpair(AF_UNIX, SOCK_STREAM, 0, mServer, mClient);
        } catch (ErrnoException e) {
            throw new RuntimeException("Failed to create bridge");
        }
    }
    .......
    public void setTargetFile(FileDescriptor target) {
        mTarget = target;
    }

    public FileDescriptor getClientSocket() {
        return mClient;
    }

    @Override
    public void run() {
        final byte[] temp = new byte[8192];
        try {
            //取出mServer中的数据,并进行处理
            //注意mSever和mClient通道绑定,于是读出的数据是mClient写入的,向mServer写数据也会递交给mClient
            while (IoBridge.read(mServer, temp, 0, MSG_LENGTH) == MSG_LENGTH) {
                final int cmd = Memory.peekInt(temp, 0, ByteOrder.BIG_ENDIAN);
                if (cmd == CMD_WRITE) {
                    // Shuttle data into local file
                    int len = Memory.peekInt(temp, 4, ByteOrder.BIG_ENDIAN);
                    while (len > 0) {
                        int n = IoBridge.read(mServer, temp, 0, Math.min(temp.length, len));
                        .......
                        IoBridge.write(mTarget, temp, 0, n);
                        len -= n;
                    }
                } else if (cmd == CMD_FSYNC) {
                    // Sync and echo back to confirm
                    Os.fsync(mTarget);
                    IoBridge.write(mServer, temp, 0, MSG_LENGTH);
                } else if (cmd == CMD_CLOSE) {
                    // Close and echo back to confirm
                    Os.fsync(mTarget);
                    Os.close(mTarget);
                    mClosed = true;
                    IoBridge.write(mServer, temp, 0, MSG_LENGTH);
                    break;
                }
            }
        } catch (ErrnoException | IOException e) {
            ........
        } finally {
            forceClose();
        }
}

通过调用PackageInstallerSession的openWrite函数,Pm将得到与PackageInstallerSession通信的client端,同时PackageInstallerSession启动FileBridge准备接收数据。

在上文中进行文件拷贝时,最终就是利用FileBridge的管道来完成实际的工作。

3、 commit session
根据上面的代码,我们知道doWriteSession结束后,如果没有出现任何错误,那么APK源文件已经copy到目的地址了。
接下来我们看看doCommitSession进行的工作。

private int doCommitSession(int sessionId, boolean logSuccess) throws RemoteException {
    PackageInstaller.Session session = null;
    try {
        session = new PackageInstaller.Session(
                mInstaller.openSession(sessionId));

        //receiver用于接收结果
        final LocalIntentReceiver receiver = new LocalIntentReceiver();
        //提交session
        session.commit(receiver.getIntentSender());
        .....
    } finally {
        IoUtils.closeQuietly(session);
    }
}

PackageInstaller.Session中的commit函数,将通过Binder通信调用PackageInstallerSession的commit函数:

public void commit(@NonNull IntentSender statusReceiver) {
    try {
        mSession.commit(statusReceiver);
    } catch (RemoteException e) {
        throw e.rethrowFromSystemServer();
    }
}

我们跟进PackageInstallerSession的commit函数:

@Override
public void commit(IntentSender statusReceiver) {
    .......
    final boolean wasSealed;
    synchronized (mLock) {
        //初始时mSealed为false
        wasSealed = mSealed;
        if (!mSealed) {
            // Verify that all writers are hands-off
            //前面的doWriteSession传输数据的结尾,会关闭bridge
            for (FileBridge bridge : mBridges) {
                if (!bridge.isClosed()) {
                    throw new SecurityException("Files still open");
                }
            }
            mSealed = true;
        }
    }
    ............
    final PackageInstallObserverAdapter adapter = new PackageInstallObserverAdapter(mContext,
            statusReceiver, sessionId, mIsInstallerDeviceOwner, userId);
    mHandler.obtainMessage(MSG_COMMIT, adapter.getBinder()).sendToTarget();
}

PackageInstallerSession被创建时,指定了mHandler对应callback:

private final Handler.Callback mHandlerCallback = new Handler.Callback() {
    @Override
    public boolean handleMessage(Message msg) {
        synchronized (mLock) {
            if (msg.obj != null) {
                //其实就是存储Pm.java中的结果接收器
                mRemoteObserver = (IPackageInstallObserver2) msg.obj;
            }

            try {
                //因此,commit发送消息后,最终将触发commitLocked
                commitLocked();
            } catch (PackageManagerException e) {
                .......
            }

            return true;
        }
    }
};

private void commitLocked() throws PackageManagerException {
    .......
    try {
        //解析安装地址,即前文APK文件copy后的目的地址
        resolveStageDir();
    } catch (IOException e) {
        ........
    }

    // Verify that stage looks sane with respect to existing application.
    // This currently only ensures packageName, versionCode, and certificate
    // consistency.
    //将利用PKMS检查APK文件是否满足要求,主要是保证各个文件是否具有一致性
    validateInstallLocked();

    //检查权限等
    ........

    if (stageCid != null) {
        // Figure out the final installed size and resize the container once
        // and for all. Internally the parser handles straddling between two
        // locations when inheriting.
        final long finalSize = calculateInstalledSize();
        resizeContainer(stageCid, finalSize);
    }

    // Inherit any packages and native libraries from existing install that
    // haven't been overridden.
    if (params.mode == SessionParams.MODE_INHERIT_EXISTING) {
        //如果新的APK文件继承某些已安装的Pacakge,此处将copy需要的native库文件等
        .........
    }

    ......
    // Unpack native libraries
    //解压缩native库文件
    extractNativeLibraries(mResolvedStageDir, params.abiOverride);

    // Container is ready to go, let's seal it up!
    if (stageCid != null) {
        //针对安装在sdcard的操作,根据uid、gid调用fixSdPermissions
        finalizeAndFixContainer(stageCid);
    }

    .........
    //调用PKMS的installStage,进入安装的下一步操作
    mPm.installStage(mPackageName, stageDir, stageCid, localObserver, params,
            installerPackageName, installerUid, user, mCertificates);
}

代码看到这里,我们终于明白了APK安装过程中,Pm.java进行的操作其实就是将adb拷贝的文件,拷贝到系统内或sdcard的目录中,然后进行初步的权限检查等工作,最后通知PKMS进入Install Stage。
整个代码引入了PackageInstallerSession,个人认为这里涉及了Binder通信、类似于Java网络通信的架构及命令模式,写的非常巧妙,有值得学习和模仿的地方。

我们同样用一张图来为这一部分做个总结:

大图链接

四、installStage
几经波折,APK的安装流程终于进入到了PKMS,我们看看installStage函数:

void installStage(String packageName, File stagedDir, String stagedCid,
        IPackageInstallObserver2 observer, PackageInstaller.SessionParams sessionParams,
        String installerPackageName, int installerUid, UserHandle user,
        Certificate[][] certificates) {
    ............
    //verificationInfo主要用于存储权限验证需要的信息
    final VerificationInfo verificationInfo = new VerificationInfo(
            sessionParams.originatingUri, sessionParams.referrerUri,
            sessionParams.originatingUid, installerUid);

    //origin中主要存储的APK文件的路径信息
    final OriginInfo origin;
    if (stagedDir != null) {
        origin = OriginInfo.fromStagedFile(stagedDir);
    } else {
        origin = OriginInfo.fromStagedContainer(stagedCid);
    }

    final Message msg = mHandler.obtainMessage(INIT_COPY);
    //准备安装所需的参数
    final InstallParams params = new InstallParams(origin, null, observer,
            sessionParams.installFlags, installerPackageName, sessionParams.volumeUuid,
            verificationInfo, user, sessionParams.abiOverride,
            sessionParams.grantedRuntimePermissions, certificates);
    .........
    msg.obj = params;
    .........
    //发送INIT_COPY消息,驱动处理流程
    mHandler.sendMessage(msg);
}

PKMS中实际的消息处理函数为doHandleMessage:

void doHandleMessage(Message msg) {
    switch (msg.what) {
        case INIT_COPY: {
            //这里取出的其实就是InstallParams,其继承HandlerParams
            HandlerParams params = (HandlerParams) msg.obj;
            //idx为当前等待处理处理的安装请求的个数
            int idx = mPendingInstalls.size();
            ............
            // If a bind was already initiated we dont really
            // need to do anything. The pending install
            // will be processed later on.
            //初始时,mBound的值为false
            if (!mBound) {
                ............
                // If this is the only one pending we might
                // have to bind to the service again.
                //连接实际的安装服务,后文介绍
                if (!connectToService()) {
                    ..................
                } else {
                    // Once we bind to the service, the first
                    // pending request will be processed.
                    //绑定服务成功后,将新的请求加入到mPendingIntalls中,等待处理
                    mPendingInstalls.add(idx, params);
                }
            } else {
                //如果之前已经绑定过服务,同样将新的请求加入到mPendingIntalls中,等待处理
                mPendingInstalls.add(idx, params);
                // Already bound to the service. Just make
                // sure we trigger off processing the first request.
                if (idx == 0) {
                    //如果是第一个请求,则直接发送事件MCS_BOUND,触发处理流程
                    mHandler.sendEmptyMessage(MCS_BOUND);
                }
            }
            break;
        }
    }
}

上面代码的处理逻辑实际上是比较简单的,我们就看看connectToService的操作,来寻找一下实际进行安装工作的服务:

private boolean connectToService() {
    ........
    //Component的包名为"com.android.defcontainer";类名为"com.android.defcontainer.DefaultContainerService"
    Intent service = new Intent().setComponent(DEFAULT_CONTAINER_COMPONENT);
    Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT);
    if (mContext.bindServiceAsUser(service, mDefContainerConn,
            Context.BIND_AUTO_CREATE, UserHandle.SYSTEM)) {
        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
        mBound = true;
        return true;
    }
    Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
    return false;
}

从代码可以看出实际进行安装工作的服务是DefaultContainerService,当绑定服务成功后:

class DefaultContainerConnection implements ServiceConnection {
    public void onServiceConnected(ComponentName name, IBinder service) {
        ........
        //获得与服务端通信的代理对象
        IMediaContainerService imcs =
                IMediaContainerService.Stub.asInterface(service);
        //发送MCS_BOUND消息触发流程
        mHandler.sendMessage(mHandler.obtainMessage(MCS_BOUND, imcs));
    }
    .......
}

现在我们知道了当服务绑定成功后,也会发送MCS_BOUND消息触发接下来的流程。

MCS_BOUND对应的处理流程同样定义于doHandleMessage中:

void doHandleMessage(Message msg) {
    .......
    case MCS_BOUND: {
        ........
        if (msg.obj != null) {
            mContainerService = (IMediaContainerService) msg.obj;
            .......
        }
        if (mContainerService == null) {
            if (!mBound) {
                // Something seriously wrong since we are not bound and we are not
                // waiting for connection. Bail out.
                ............            
            } else {
                Slog.w(TAG, "Waiting to connect to media container service");
            }
        } else if (mPendingInstalls.size() > 0) {
            HandlerParams params = mPendingInstalls.get(0);
            if (params != null) {
                ........
                //调用参数的startCopy函数
                if (params.startCopy()) {
                    ........
                    // Delete pending install
                    if (mPendingInstalls.size() > 0) {
                        mPendingInstalls.remove(0);
                    }
                    if (mPendingInstalls.size() == 0) {
                        if (mBound) {
                            ..........
                            removeMessages(MCS_UNBIND);
                            Message ubmsg = obtainMessage(MCS_UNBIND);
                            // Unbind after a little delay, to avoid
                            // continual thrashing.
                            sendMessageDelayed(ubmsg, 10000);
                        }
                    } else {
                        // There are more pending requests in queue.
                        // Just post MCS_BOUND message to trigger processing
                        // of next pending install.
                        ......
                        mHandler.sendEmptyMessage(MCS_BOUND);
                    }
                }
                .........
            }
        } else {
            // Should never happen ideally.
            Slog.w(TAG, "Empty queue");
        }
        break;
    }
.......
}

这一段代码写的非常清晰,就是处理完一个安装请求后,接着处理下一个;如果队列为空,则等待一段时间后,发送MCS_UNBIND消息断开与安装服务的绑定。

顺着流程,我们现在看看HandlerParams的startCopy函数:

final boolean startCopy() {
    boolean res;
    try {
        ........
        //处理安装失败,MAX_RETRIES = 4
        if (++mRetries > MAX_RETRIES) {
            .........
            mHandler.sendEmptyMessage(MCS_GIVE_UP);
            handleServiceError();
            return false;
        } else {
            //先调用handleStartCopy进行实际的copy工作
            handleStartCopy();
            res = true;
        }
    } catch (RemoteException e) {
        if (DEBUG_INSTALL) Slog.i(TAG, "Posting install MCS_RECONNECT");
        mHandler.sendEmptyMessage(MCS_RECONNECT);
        res = false;
    }
    //然后根据结果做相应处理
    handleReturnCode();
    return res;
}


如上图所示,从这段代码来看,PKMS将先后调用handleStartCopy和handleReturnCode来完成主要的工作。接下来,我们分别介绍一下这两个函数的工作流程。

五、handleStartCopy
HandlerParams为PKMS的内部抽象类,上面代码中的实际处理函数由其子类InstallParams来实现,我们看看与实际安装相关的handleStartCopy函数:

public void handleStartCopy() throws RemoteException {
    int ret = PackageManager.INSTALL_SUCCEEDED;

    // If we're already staged, we've firmly committed to an install location
    //根据参数决定是安装在手机内还是sdcard中,设置对应标志位
    if (origin.staged) {
        if (origin.file != null) {
            installFlags |= PackageManager.INSTALL_INTERNAL;
            installFlags &= ~PackageManager.INSTALL_EXTERNAL;
        } else if (origin.cid != null) {
            installFlags |= PackageManager.INSTALL_EXTERNAL;
            installFlags &= ~PackageManager.INSTALL_INTERNAL;
        } else {
            throw new IllegalStateException("Invalid stage location");
        }
    }

    final boolean onSd = (installFlags & PackageManager.INSTALL_EXTERNAL) != 0;
    final boolean onInt = (installFlags & PackageManager.INSTALL_INTERNAL) != 0;
    final boolean ephemeral = (installFlags & PackageManager.INSTALL_EPHEMERAL) != 0;
    PackageInfoLite pkgLite = null;

    //检查APK的安装位置是否正确
    if (onInt && onSd) {
        // Check if both bits are set.
        ...........
        //APK不能同时安装在内部存储空间和SD card上
        ret = PackageManager.INSTALL_FAILED_INVALID_INSTALL_LOCATION;
    } else if (onSd && ephemeral) {
        .......
        //APK不能短暂地安装在SD card上
        ret = PackageManager.INSTALL_FAILED_INVALID_INSTALL_LOCATION;
    }  else {
        //1、利用ContainerService获取PackageInfoLite,应该判断了能否进行安装
        pkgLite = mContainerService.getMinimalPackageInfo(origin.resolvedPath, installFlags,
                packageAbiOverride);
        .........

        /*
        * If we have too little free space, try to free cache
        * before giving up.
        */
        //对于安装在SD card上的APK,当存储空间过小导致安装失败时
        if (!origin.staged && pkgLite.recommendedInstallLocation
                == PackageHelper.RECOMMEND_FAILED_INSUFFICIENT_STORAGE) {
            final StorageManager storage = StorageManager.from(mContext);
            //利用StroageManager得到设备内部存储空间允许的最小余量
            final long lowThreshold = storage.getStorageLowBytes(
                    Environment.getDataDirectory());

            //利用ContainerService得到安装APK的大小
            final long sizeBytes = mContainerService.calculateInstalledSize(
                    origin.resolvedPath, isForwardLocked(), packageAbiOverride);
            try {
                //利用Installer释放缓存,试图将缓存释放到大于等于(最小余量与APK大小之和)
                mInstaller.freeCache(null, sizeBytes + lowThreshold);
                //再次试图得到PackageInfoLite,判断是否满足安装条件
                pkgLite = mContainerService.getMinimalPackageInfo(origin.resolvedPath,
                        installFlags, packageAbiOverride);
            } catch (InstallerException e) {
                Slog.w(TAG, "Failed to free cache", e);
            }

            /*
            * The cache free must have deleted the file we
            * downloaded to install.
            *
            * TODO: fix the "freeCache" call to not delete
            *       the file we care about.
            */
            if (pkgLite.recommendedInstallLocation
                    == PackageHelper.RECOMMEND_FAILED_INVALID_URI) {
                //试图释放cache还是无法安装,只能设置标志位为失败
                pkgLite.recommendedInstallLocation
                        = PackageHelper.RECOMMEND_FAILED_INSUFFICIENT_STORAGE;
            }
        }
    }

    if (ret == PackageManager.INSTALL_SUCCEEDED) {
        //recommendedInstallLocation中记录了安装的路径信息,即APK保存在终端内部还是Sd card中,此外也可以记录安装失败的信息
        int loc = pkgLite.recommendedInstallLocation;
        if (loc == PackageHelper.RECOMMEND_FAILED_INVALID_LOCATION) {
            ret = PackageManager.INSTALL_FAILED_INVALID_INSTALL_LOCATION;
        } else if (loc == PackageHelper.RECOMMEND_FAILED_ALREADY_EXISTS) {
            ret = PackageManager.INSTALL_FAILED_ALREADY_EXISTS;
        } else if (loc == PackageHelper.RECOMMEND_FAILED_INSUFFICIENT_STORAGE) {
            ret = PackageManager.INSTALL_FAILED_INSUFFICIENT_STORAGE;
        } else if (loc == PackageHelper.RECOMMEND_FAILED_INVALID_APK) {
            ret = PackageManager.INSTALL_FAILED_INVALID_APK;
        } else if (loc == PackageHelper.RECOMMEND_FAILED_INVALID_URI) {
            ret = PackageManager.INSTALL_FAILED_INVALID_URI;
        } else if (loc == PackageHelper.RECOMMEND_MEDIA_UNAVAILABLE) {
            ret = PackageManager.INSTALL_FAILED_MEDIA_UNAVAILABLE;
        } else {
            // Override with defaults if needed.
            //2、installLocationPolicy主要判断终端上是否有已经安装过该APK,同一个APK一般只能用新版的替换旧版
            loc = installLocationPolicy(pkgLite);
            //根据loc调整installFlag
            ......
        }
    }

    //3、createInstallArgs用于创建一个安装参数对象
    final InstallArgs args = createInstallArgs(this);
    mArgs = args;
    if (ret == PackageManager.INSTALL_SUCCEEDED) {
        // Apps installed for "all" users use the device owner to verify the app
        UserHandle verifierUser = getUser();
        if (verifierUser == UserHandle.ALL) {
            verifierUser = UserHandle.SYSTEM;
        }
        /*
        * Determine if we have any installed package verifiers. If we
        * do, then we'll defer to them to verify the packages.
        */
        final int requiredUid = mRequiredVerifierPackage == null ? -1
                : getPackageUid(mRequiredVerifierPackage, MATCH_DEBUG_TRIAGED_MISSING,
                        verifierUser.getIdentifier());
        if (!origin.existing && requiredUid != -1
                && isVerificationEnabled(verifierUser.getIdentifier(), installFlags)) {
            //如果存在Package检查者,同时满足启动检查的条件,那么将利用Pacakge检查者来检查安装包
            //其实就是构造一个intent,action为"android.intent.action.PACKAGE_NEEDS_VERIFICATION"
            //在intent中添加需要多信息后,发送给接收者处理
            .........
        } else {
            //4、调用安装参数对象的copyApk函数
            ret = args.copyApk(mContainerService, true);
        }
    }
    mRet = ret;
}

handleStartCopy函数整体来看还是比较复杂的,内容比较多,我们需要分4步介绍其中主要的内容。

1、getMinimalPackageInfo
getMinimalPackageInfo实际定义于DefaultContainerService中,其代码如下:

@Override
public PackageInfoLite getMinimalPackageInfo(String packagePath, int flags,
        String abiOverride) {
    final Context context = DefaultContainerService.this;
    final boolean isForwardLocked = (flags & PackageManager.INSTALL_FORWARD_LOCK) != 0;

    PackageInfoLite ret = new PackageInfoLite();
    ........
    final File packageFile = new File(packagePath);
    final PackageParser.PackageLite pkg;
    final long sizeBytes;
    try {
        //如同PKMS的构造函数,利用PackageParser来解析APK文件,得到PackageInfoLite
        pkg = PackageParser.parsePackageLite(packageFile, 0);
        sizeBytes = PackageHelper.calculateInstalledSize(pkg, isForwardLocked, abiOverride);
    } catch (PackageParserException | IOException e) {
        .................
    }

    ret.packageName = pkg.packageName;
    ret.splitNames = pkg.splitNames;
    ret.versionCode = pkg.versionCode;
    ret.baseRevisionCode = pkg.baseRevisionCode;
    ret.splitRevisionCodes = pkg.splitRevisionCodes;
    ret.installLocation = pkg.installLocation;
    ret.verifiers = pkg.verifiers;
    //利用resolveInstallLocation来得到一个合理的安装位置
    ret.recommendedInstallLocation = PackageHelper.resolveInstallLocation(context,
            pkg.packageName, pkg.installLocation, sizeBytes, flags);
    ret.multiArch = pkg.multiArch;

    return ret;
}

从代码可以看出,getMinimalPackageInfo的代码比较简单,其实就是利用PackageParser解析出APK对应Pacakge的基本信息,然后利用resolveInstallLocation得到适合APK安装的路径。

1.1 resolveInstallLocation
我们看看resolveInstallLocation函数:

public static int resolveInstallLocation(Context context, String packageName,
        int installLocation, long sizeBytes, int installFlags) {
    ApplicationInfo existingInfo = null;
    try {
        //如果之前该APK之前安装过,那么将获取到之前记录的ApplicationInfo信息
        existingInfo = context.getPackageManager().getApplicationInfo(packageName,
                PackageManager.GET_UNINSTALLED_PACKAGES);
    } catch (NameNotFoundException ignored) {
        .........
    }

    final int prefer;
    final boolean checkBoth;
    boolean ephemeral = false;
    //以下其实就是根据installFlags决定安装倾向的路径prefer
    if ((installFlags & PackageManager.INSTALL_EPHEMERAL) != 0) {
        prefer = RECOMMEND_INSTALL_INTERNAL;
        ephemeral = true;
        checkBoth = false;
    } else if ((installFlags & PackageManager.INSTALL_INTERNAL) != 0) {
        prefer = RECOMMEND_INSTALL_INTERNAL;
        checkBoth = false;
    } else if ((installFlags & PackageManager.INSTALL_EXTERNAL) != 0) {
        prefer = RECOMMEND_INSTALL_EXTERNAL;
        checkBoth = false;
    } else if (installLocation == PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY) {
        prefer = RECOMMEND_INSTALL_INTERNAL;
        checkBoth = false;
    } else if (installLocation == PackageInfo.INSTALL_LOCATION_PREFER_EXTERNAL) {
        prefer = RECOMMEND_INSTALL_EXTERNAL;
        checkBoth = true;
    } else if (installLocation == PackageInfo.INSTALL_LOCATION_AUTO) {
        //一般APK安装的路径就是auto
        // When app is already installed, prefer same medium
        if (existingInfo != null) {
            // TODO: distinguish if this is external ASEC
            if ((existingInfo.flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE) != 0) {
                prefer = RECOMMEND_INSTALL_EXTERNAL;
            } else {
                prefer = RECOMMEND_INSTALL_INTERNAL;
            }
        } else {
            //可以看到一般默认条件下是安装在手机内部的
            prefer = RECOMMEND_INSTALL_INTERNAL;
        }
        //auto时,checkBoth为true
        checkBoth = true;
    } else {
        prefer = RECOMMEND_INSTALL_INTERNAL;
        checkBoth = false;
    }

    boolean fitsOnInternal = false;
    if (checkBoth || prefer == RECOMMEND_INSTALL_INTERNAL) {
        //fitsOnInternal和下面的fitsOnExternal应该就是用于检查对应路径是否有足够的空间来安装APK的
        fitsOnInternal = fitsOnInternal(context, sizeBytes);
    }

    boolean fitsOnExternal = false;
    if (checkBoth || prefer == RECOMMEND_INSTALL_EXTERNAL) {
        fitsOnExternal = fitsOnExternal(context, sizeBytes);
    }

    if (prefer == RECOMMEND_INSTALL_INTERNAL) {
        // The ephemeral case will either fit and return EPHEMERAL, or will not fit
        // and will fall through to return INSUFFICIENT_STORAGE
        if (fitsOnInternal) {
            return (ephemeral)
                    ? PackageHelper.RECOMMEND_INSTALL_EPHEMERAL
                    : PackageHelper.RECOMMEND_INSTALL_INTERNAL;
        }
    } else if (prefer == RECOMMEND_INSTALL_EXTERNAL) {
        if (fitsOnExternal) {
            return PackageHelper.RECOMMEND_INSTALL_EXTERNAL;
        }
    }

    //个人感觉这里就是容错用的吧,在前面的代码中正常情况下,prefer已经被取值为RECOMMEND_INSTALL_INTERNAL或RECOMMEND_INSTALL_EXTERNAL了
    if (checkBoth) {
        if (fitsOnInternal) {
            return PackageHelper.RECOMMEND_INSTALL_INTERNAL;
        } else if (fitsOnExternal) {
            return PackageHelper.RECOMMEND_INSTALL_EXTERNAL;
        }
    }

    //没有足够空间,返回对应消息
    return PackageHelper.RECOMMEND_FAILED_INSUFFICIENT_STORAGE;
}

上述代码相对比较简单,我们主要以fitsOnInternal为例,看看如何判断存储空间是否足够:

public static boolean fitsOnInternal(Context context, long sizeBytes) {
    final StorageManager storage = context.getSystemService(StorageManager.class);
    final File target = Environment.getDataDirectory();
    //APK安装所需的空间,小于等于可用空间时,就返回true                
    return (sizeBytes <= storage.getStorageBytesUntilLow(target));
}

//StorgeManager中的函数,其实就是用总的可用空间,减去已经使用的空间,得到剩余空间
public long getStorageBytesUntilLow(File path) {
    return path.getUsableSpace() - getStorageFullBytes(path);
}

2、installLocationPolicy
当成功得到APK对应的PacakgeInfoLite,并判断安装路径有足够的剩余空间时,将调用installLocationPolicy函数:

private int installLocationPolicy(PackageInfoLite pkgLite) {
    String packageName = pkgLite.packageName;
    int installLocation = pkgLite.installLocation;
    boolean onSd = (installFlags & PackageManager.INSTALL_EXTERNAL) != 0;

    synchronized (mPackages) {
        // Currently installed package which the new package is attempting to replace or
        // null if no such package is installed.
        //判断终端上之前是否安装过同样的APK
        PackageParser.Package installedPkg = mPackages.get(packageName);

        // Package which currently owns the data which the new package will own if installed.
        // If an app is unstalled while keeping data (e.g., adb uninstall -k), installedPkg
        // will be null whereas dataOwnerPkg will contain information about the package
        // which was uninstalled while keeping its data.
        //当一个APK卸载时,那么installedPkg为null
        PackageParser.Package dataOwnerPkg = installedPkg;
        if (dataOwnerPkg  == null) {
            //但是如果APK卸载时,保留了数据,那么PKMS将取出对应的PacakgeSettings
            PackageSetting ps = mSettings.mPackages.get(packageName);
            if (ps != null) {
                //从PacakgeSettings中取出Pacakge
                dataOwnerPkg = ps.pkg;
            }
        }

        //存在旧APK对应的信息时
        if (dataOwnerPkg != null) {
            final boolean downgradeRequested =
                    (installFlags & PackageManager.INSTALL_ALLOW_DOWNGRADE) != 0;
            final boolean packageDebuggable =
                    (dataOwnerPkg.applicationInfo.flags
                            & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
            //当安装一个重复的APK时,新装的APK版本一般要比旧APK的版本高
            //除非满足以下要求,例如显示要求装入旧版本、在debug模式下等
            final boolean downgradePermitted =
                (downgradeRequested) && ((Build.IS_DEBUGGABLE) || (packageDebuggable));

            //默认模式下,即仅能安装高版本时
            if (!downgradePermitted) {
                try {
                    //比较两个Package信息中的VersionCode
                    checkDowngrade(dataOwnerPkg, pkgLite);
                } catch (PackageManagerException e) {
                    Slog.w(TAG, "Downgrade detected: " + e.getMessage());
                    return PackageHelper.RECOMMEND_FAILED_VERSION_DOWNGRADE;
                }
            }
        }

        //旧有的APK还存在终端上时
        if (installedPkg != null) {
            //installFlags中必须携带REPLACE_EXISTING,否则将报错
            if ((installFlags & PackageManager.INSTALL_REPLACE_EXISTING) != 0) {
                // Check for updated system application.
                if ((installedPkg.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) {
                    //系统APK不应该存在SD card上
                    if (onSd) {
                        Slog.w(TAG, "Cannot install update to system app on sdcard");
                        return PackageHelper.RECOMMEND_FAILED_INVALID_LOCATION;
                    }
                    return PackageHelper.RECOMMEND_INSTALL_INTERNAL;
                } else {
                    // If current upgrade specifies particular preference
                    if (installLocation == PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY) {
                        // Application explicitly specified internal.
                        return PackageHelper.RECOMMEND_INSTALL_INTERNAL;
                    } else if (installLocation == PackageInfo.INSTALL_LOCATION_PREFER_EXTERNAL) {
                        // App explictly prefers external. Let policy decide
                    } else {
                        // Prefer previous location
                        //未指定安装路径时,与之前的安装路径保持一致
                        if (isExternal(installedPkg)) {
                            return PackageHelper.RECOMMEND_INSTALL_EXTERNAL;
                        }
                        return PackageHelper.RECOMMEND_INSTALL_INTERNAL;
                    }
                }
            } else {
                // Invalid install. Return error code
                return PackageHelper.RECOMMEND_FAILED_ALREADY_EXISTS;
            }
        }
    }
    // All the special cases have been taken care of.
    // Return result based on recommended install location.
    if (onSd) {
        return PackageHelper.RECOMMEND_INSTALL_EXTERNAL;
    }
    return pkgLite.recommendedInstallLocation;
}

3、createInstallArgs
处理完潜在的重复安装APK的风险后,PKMS调用createInstallArgs生成安装参数对象:

private InstallArgs createInstallArgs(InstallParams params) {
    if (params.move != null) {
        return new MoveInstallArgs(params);
    } else if (installOnExternalAsec(params.installFlags) || params.isForwardLocked()) {
        return new AsecInstallArgs(params);
    } else {
        return new FileInstallArgs(params);
    }
}

这部分的代码较为简单,就是利用参数决定创建哪个InstallArgs的子类,我们主要关注在终端安装APK时,将要使用的FileInstallArgs,后文介绍其功能。

4、copyApk
如果不需要进行安装包检查,对于安装在终端内部的APK而言,将调用FileInstallArgs的copyAPK函数:

int copyApk(IMediaContainerService imcs, boolean temp) throws RemoteException {
    Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "copyApk");
    try {
        //doCopyApk负责进行实际的工作
        return doCopyApk(imcs, temp);
    } finally {
        Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
    }
}

private int doCopyApk(IMediaContainerService imcs, boolean temp) throws RemoteException {
    //与之前版本不同的时,Android7.0中,已经通过Session进行了文件拷贝
    //当进入到前文所述的PKMS的installStage时,OriginInfo.fromStagedFile或OriginInfo.fromStagedContainer均会将staged变量置为true
    if (origin.staged) {
        if (DEBUG_INSTALL) Slog.d(TAG, origin.file + " already staged; skipping copy");
            codeFile = origin.file;
            resourceFile = origin.file;
            return PackageManager.INSTALL_SUCCEEDED;
        }
    }

    //当使用其它方式安装APK时,将进入到以下流程
    try {
        //当需要临时安装时,创建一个临时安装目录
        final boolean isEphemeral = (installFlags & PackageManager.INSTALL_EPHEMERAL) != 0;
        final File tempDir =
                mInstallerService.allocateStageDirLegacy(volumeUuid, isEphemeral);
        codeFile = tempDir;
        resourceFile = tempDir;
    } catch (IOException e) {
        Slog.w(TAG, "Failed to create copy file: " + e);
        return PackageManager.INSTALL_FAILED_INSUFFICIENT_STORAGE;
    }

    //定义回调接口
    final IParcelFileDescriptorFactory target = new IParcelFileDescriptorFactory.Stub() {
        @Override
        public ParcelFileDescriptor open(String name, int mode) throws RemoteException {
            if (!FileUtils.isValidExtFilename(name)) {
                throw new IllegalArgumentException("Invalid filename: " + name);
            }
            try {
                //当接口被回调时,需要创建并打开文件,同事赋予相应的权限
                final File file = new File(codeFile, name);
                final FileDescriptor fd = Os.open(file.getAbsolutePath(),
                        O_RDWR | O_CREAT, 0644);
                Os.chmod(file.getAbsolutePath(), 0644);
                return new ParcelFileDescriptor(fd);
            } catch (ErrnoException e) {
                throw new RemoteException("Failed to open: " + e.getMessage());
            }
        }
    };

    //调用DefaultContainerService进行copyPackage的操作,传入了回调的接口
    int ret = PackageManager.INSTALL_SUCCEEDED;
    ret = imcs.copyPackage(origin.file.getAbsolutePath(), target);
    if (ret != PackageManager.INSTALL_SUCCEEDED) {
        Slog.e(TAG, "Failed to copy package");
        return ret;
    }

    //拷贝APK对应的Native库文件
    final File libraryRoot = new File(codeFile, LIB_DIR_NAME);
    NativeLibraryHelper.Handle handle = null;
    try {
        handle = NativeLibraryHelper.Handle.create(codeFile);
        ret = NativeLibraryHelper.copyNativeBinariesWithOverride(handle, libraryRoot,
                abiOverride);
    } catch (IOException e) {
        Slog.e(TAG, "Copying native libraries failed", e);
        ret = PackageManager.INSTALL_FAILED_INTERNAL_ERROR;
    } finally {
        IoUtils.closeQuietly(handle);
    }

    return ret;
}

对于非adb安装的APK,我们看看DefaultContainerService对应的copyPackage是如何进行处理的:

@Override
public int copyPackage(String packagePath, IParcelFileDescriptorFactory target) {
    if (packagePath == null || target == null) {
        return PackageManager.INSTALL_FAILED_INVALID_URI;
    }

    PackageLite pkg = null;
    try {
        final File packageFile = new File(packagePath);
        //解析出PackageFile
        pkg = PackageParser.parsePackageLite(packageFile, 0);
        //利用copyPackageInner进行实际的处理
        return copyPackageInner(pkg, target);
    } catch (PackageParserException | IOException | RemoteException e) {
        Slog.w(TAG, "Failed to copy package at " + packagePath + ": " + e);
        return PackageManager.INSTALL_FAILED_INSUFFICIENT_STORAGE;
    }
}

private int copyPackageInner(PackageLite pkg, IParcelFileDescriptorFactory target)
        throws IOException, RemoteException {
    //copyFile负责实际的拷贝
    copyFile(pkg.baseCodePath, target, "base.apk");
    if (!ArrayUtils.isEmpty(pkg.splitNames)) {
        for (int i = 0; i < pkg.splitNames.length; i++) {
            //对于多APK文件的情况,需依次拷贝所有的子文件
            copyFile(pkg.splitCodePaths[i], target, "split_" + pkg.splitNames[i] + ".apk");
        }
    }

    return PackageManager.INSTALL_SUCCEEDED;
}

private void copyFile(String sourcePath, IParcelFileDescriptorFactory target, String targetName)
        throws IOException, RemoteException {
    Slog.d(TAG, "Copying " + sourcePath + " to " + targetName);
    InputStream in = null;
    OutputStream out = null;
    try {
        //源文件作为输入
        in = new FileInputStream(sourcePath);
        //目的文件作为输出,这里进行了多层封装
        //上文提到过,回调接口target调用open函数后,将创建并打开目的端文件,然后赋予相应的写权限
        //ParcelFileDescriptor.AutoCloseOutputStream利用文件描述符构造出一个可自动关闭的输出流
        out = new ParcelFileDescriptor.AutoCloseOutputStream(
                target.open(targetName, ParcelFileDescriptor.MODE_READ_WRITE));
        //进行实际的数据拷贝
        Streams.copy(in, out);
    } finally {
        IoUtils.closeQuietly(out);
        IoUtils.closeQuietly(in);
    }
}

至此整个handleStartCopy流程介绍完毕,可以看出当利用adb安装时,handleStartCopy实际上并没有完成什么实际的操作;对于其它方式安装APK时,handleStartCopy才会进行真正的数据拷贝工作。

整个过程的大致流程如下:

六、handleReturnCode
copy过程结束后,将调用InstallParams的handleReturnCode:

void handleReturnCode() {
    if (mArgs != null) {
        processPendingInstall(mArgs, mRet);
    }
}

private void processPendingInstall(final InstallArgs args, final int currentStatus) {
    mHandler.post(new Runnable() {
        public void run() {
            mHandler.removeCallbacks(this);

            // Result object to be returned
            PackageInstalledInfo res = new PackageInstalledInfo();
            res.setReturnCode(currentStatus);
            res.uid = -1;
            res.pkg = null;
            res.removedInfo = null;
            if (res.returnCode == PackageManager.INSTALL_SUCCEEDED) {
                //1、安装在终端上的APK,将调用FileInstallArgs的doPreInstall进行处理
                args.doPreInstall(res.returnCode);

                synchronized (mInstallLock) {
                    //2、调用installPackageTracedLI进行安装
                    installPackageTracedLI(args, res);
                }

                //3、调用FileInstallArgs的doPostInstall
                args.doPostInstall(res.returnCode, res.uid);
            }

            // A restore should be performed at this point if (a) the install
            // succeeded, (b) the operation is not an update, and (c) the new
            // package has not opted out of backup participation.
            //判断是否需要备份恢复
            final boolean update = res.removedInfo != null
                    && res.removedInfo.removedPackage != null;
            final int flags = (res.pkg == null) ? 0 : res.pkg.applicationInfo.flags;
            boolean doRestore = !update
                    && ((flags & ApplicationInfo.FLAG_ALLOW_BACKUP) != 0);

            // Set up the post-install work request bookkeeping.  This will be used
            // and cleaned up by the post-install event handling regardless of whether
            // there's a restore pass performed.  Token values are >= 1.
            int token;
            if (mNextInstallToken < 0) mNextInstallToken = 1;
            token = mNextInstallToken++;

            PostInstallData data = new PostInstallData(args, res);
            mRunningInstalls.put(token, data);

            if (res.returnCode == PackageManager.INSTALL_SUCCEEDED && doRestore) {
                //调用BackupManager的接口进行恢复工作
                .......
            }

            if (!doRestore) {
                .......
                //4、生成一个POST_INSTALL消息,触发后续操作
                Message msg = mHandler.obtainMessage(POST_INSTALL, token, 0);
                mHandler.sendMessage(msg);
            }
        }
    });
}

从上面的代码可以看出,handleReturnCode主要做了4件事:
*调用InstallArgs的doPreInstall函数,对于安装在终端内部的APK而言,将调用FileInstallArgs的doPreInstall函数;
*调用PKMS的installPackageTracedLI函数进行APK安装;
*调用InstallArgs的doPostInstall函数;
*利用结果构造PostInstallData,然后发送POST_INSTALL消息触发后续处理流程

现在我们分别介绍这几部分工作:
1、doPreInstall

int doPreInstall(int status) {
    if (status != PackageManager.INSTALL_SUCCEEDED) {
        cleanUp();
    }
    return status;
}

private boolean cleanUp() {
    if (codeFile == null || !codeFile.exists()) {
        return false;
    }

    removeCodePathLI(codeFile);

    if (resourceFile != null && !FileUtils.contains(codeFile, resourceFile)) {
        resourceFile.delete();
    }

    return true;
}

从代码来看,正常流程下doPreInstall并不会进行实际的工作,只是当handleStartCopy出现问题时,doPreInstall将清理拷贝的文件。

2、installPackageTracedLI

private void installPackageTracedLI(InstallArgs args, PackageInstalledInfo res) {
    try {
        Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "installPackage");
        installPackageLI(args, res);
    } finally {
        Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
    }
}

private void installPackageLI(InstallArgs args, PackageInstalledInfo res) {
    //定义一些变量
    .........
    // Result object to be returned
    res.setReturnCode(PackageManager.INSTALL_SUCCEEDED);

    //定义parseFlags
    .......
    PackageParser pp = new PackageParser();
    .......
    final PackageParser.Package pkg;
    try {
        //解析APK文件,形成Package对象
        pkg = pp.parsePackage(tmpPackageFile, parseFlags);
    } catch (PackageParserException e) {
        res.setError("Failed parse during installPackageLI", e);
        return;
    } finally {
        Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
    }

    / If we are installing a clustered package add results for the children
    if (pkg.childPackages != null) {
        //在需要的情况下,解析Child Package信息
        ........
    }

    try {
        // either use what we've been given or parse directly from the APK
        if (args.certificates != null) {
            try {
                //如果参数中定义了权限信息,就用参数中的权限信息配置Package对象
                PackageParser.populateCertificates(pkg, args.certificates);
            } catch (PackageParserException e) {
                // there was something wrong with the certificates we were given;
                // try to pull them from the APK
                PackageParser.collectCertificates(pkg, parseFlags);
            }
        } else {
            //否则,就从AndroidManifest.xml文件中解析出权限信息
            PackageParser.collectCertificates(pkg, parseFlags);
        }
    } catch (PackageParserException e) {
        res.setError("Failed collect during installPackageLI", e);
        return;
    }

    //当安装重复的APK时,根据权限、签名信息、版本等条件,判断能否进一步操作
    .............

    ........
    //根据Package中的信息,修改拷贝文件时,临时赋予的名称
    //此处将利用FileInstallArgs的doRename
    if (!args.doRename(res.returnCode, pkg, oldCodePath)) {
        res.setError(INSTALL_FAILED_INSUFFICIENT_STORAGE, "Failed rename");
        return;
    }
    .........
    try (PackageFreezer freezer = freezePackageForInstall(pkgName, installFlags,
            "installPackageLI")) {
        if (replace) {
            //用新的package信息替换旧的
            replacePackageLIF(pkg, parseFlags, scanFlags | SCAN_REPLACING, args.user,
                    installerPackageName, res);
        } else {
            //将新的pacakge信息加入到PKMS中
            installNewPackageLIF(pkg, parseFlags, scanFlags | SCAN_DELETE_DATA_ON_FAILURES,
                    args.user, installerPackageName, volumeUuid, res);
        }
    }
    ..........
}

从代码来看,installPackageTracedLI的主要工作就是解析APK文件,形成对应的Package对象;生成对应的权限信息后,根据Package中的信息,更改存储路径对应目录的名称。

3、doPostInstall

int doPostInstall(int status, int uid) {
    if (status != PackageManager.INSTALL_SUCCEEDED) {
        cleanUp();
    }
    return status;
}

可以看出,FileInstallArgs中定义的doPostInstall函数和doPreInstall函数完全一样,正常流程下不需要进行任何操作;当之前的处理流程出现问题时,利用cleanUp清楚创建的文件和资源。

4、处理POST_INSTALL消息
在PackageHandler的doHandleMessage中处理POST_INSTALL消息:

.....
case POST_INSTALL: {
    ............
    PostInstallData data = mRunningInstalls.get(msg.arg1);
    final boolean didRestore = (msg.arg2 != 0);
    mRunningInstalls.delete(msg.arg1);

    if (data != null) {
        ............
        // Handle the parent package
        handlePackagePostInstall(parentRes, grantPermissions, killApp,
                grantedPermissions, didRestore, args.installerPackageName,
                args.observer);

        // Handle the child packages
        final int childCount = (parentRes.addedChildPackages != null)
                ? parentRes.addedChildPackages.size() : 0;
        for (int i = 0; i < childCount; i++) {
            //同样利用handlePackagePostInstall处理child Package
            ........
        }
        ........
    } else {
        .........
    }
}
break;
......

我们跟进一下handlePackagePostInstall函数:

private void handlePackagePostInstall(PackageInstalledInfo res, boolean grantPermissions,
        boolean killApp, String[] grantedPermissions,
        boolean launchedForRestore, String installerPackage,
        IPackageInstallObserver2 installObserver) {
    if (res.returnCode == PackageManager.INSTALL_SUCCEEDED) {
        // Send the removed broadcasts
        if (res.removedInfo != null) {
            res.removedInfo.sendPackageRemovedBroadcasts(killApp);
        }

        //调用grantRequestedRuntimePermissions等,赋予Package权限
        ...........

        //发送ACTION_PACKAGE_ADDED等广播消息
        ...........

        ...........
    }

    // If someone is watching installs - notify them
    //将安装的结果通知给Pm.java中观察者
    if (installObserver != null) {
        try {
            Bundle extras = extrasForInstallResult(res);
            installObserver.onPackageInstalled(res.name, res.returnCode,
                    res.returnMsg, extras);
        } catch (RemoteException e) {
            Slog.i(TAG, "Observer no longer exists.");
        }
    }
}

从上面的代码来看,处理POST_INSTALL的主要工作其实还是通过广播、回调接口通知系统中的其它组件,有新的Pacakge安装或发生了改变。

最后整理一下handleReturnCode的流程,如下所示:

七、总结
从上面的代码来看,整个APK的安装过程极其琐碎复杂,但核心思想还是比较简单的:就是将待安装的APK文件拷贝到手机的指定位置,然后利用PackageParser来解析出对应的Package对象,最终将Package对象加入到PKMS中。
整体流程的主干大体如下图所示:

大图链接

作者:Gaugamela 发表于2016/10/9 10:53:25 原文链接
阅读:47 评论:0 查看评论

XML 文件解析总结

$
0
0

一.基础概念的介绍

    XML在各种开发中都广泛应用,Android也不例外。作为承载数据的一个重要角色,如何读写XML成为Android开发中一项重要的技能。今天就由我向大家介绍一下在Android平台下几种常见的XML解析和创建的方法。

            .

(一)SAX解析器:

    SAX(Simple API for XML)解析器是一种基于事件的解析器,它的核心是事件处理模式,主要是围绕着事件源以及事件处理器来工作的。当事件源产生事件后,调用事件处理器相应的处理方法,一个事件就可以得到处理。在事件源调用事件处理器中特定方法的时候,还要传递给事件处理器相应事件的状态信息,这样事件处理器才能够根据提供的事件信息来决定自己的行为。
SAX解析器的优点是解析速度快,占用内存少。非常适合在Android移动设备中使用。

                        .

(二)DOM解析器:

    DOM是基于树形结构的的节点或信息片段的集合,允许开发人员使用DOM API遍历XML树、检索所需数据。分析该结构通常需要加载整个文档和构造树形结构,然后才可以检索和更新节点信息。
由于DOM在内存中以树形结构存放,因此检索和更新效率会更高。但是对于特别大的文档,解析和加载整个文档将会很耗资源。

        .

(三)PULL解析器:

    PULL解析器的运行方式和SAX类似,都是基于事件的模式。不同的是,在PULL解析过程中,我们需要自己获取产生的事件然后做相应的操作,而不像SAX那样由处理器触发一种事件的方法,执行我们的代码。PULL解析器小巧轻便,解析速度快,简单易用,非常适合在Android移动设备中使用,Android系统内部在解析各种XML时也是用PULL解析器。
以上三种解析器,都是非常实用的解析器,我将会一一介绍。我们将会使用这三种解析技术完成一项共同的任务。

  .
  .

二.文件解析的实现

.

(一)创建XMl文件


在项目的assets目录中创建一个XML文档cars.xml,如图所示:

         .

创建cars.xml文件
.
双击打开xml文件,编辑内容如下:

<Car id="1" >
    <mark>宝马</mark>
    <color>蓝色</color>
</Car>

<Car id="2" >
    <mark>奥迪</mark>
    <color>白色</color>
</Car>

<Car id="3" >
    <mark>保时捷</mark>
    <color>黄色</color>
</Car>



写完XML文件,布局文件不写,直接写代码,解析xml文件。

        .

(二)三种读取xml文件的方法的代码

package com.example.resource;

import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import org.xmlpull.v1.XmlPullParser;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.util.Xml;

public class MainActivity extends Activity {

    // 要解析文件的输入流
    InputStream is;
    // 创建一个List集合存放从XML文件获取到的数据
    List<Car> list = null;
    Car car = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        pullXml();// 使用pull解析
        saxXml();// 使用Sax解析
        domXml();// 使用DOM解析
    }


----------//开始解析数据


    // 使用DOM解析数据
    private void domXml() {
        list = new ArrayList<Car>();
        try {
            // 从assets里面获取文件,
            // 通过getAssets获取到的是读取流
            is = getAssets().open("cars.xml");
            // 取得DocumentBuilderFactory实例
            DocumentBuilderFactory factory = DocumentBuilderFactory
                    .newInstance();
            // 从factory获取DocumentBuilder实例
            DocumentBuilder builder = factory.newDocumentBuilder();
            // 解析输入流 得到Document实例
            org.w3c.dom.Document doc = builder.parse(is);
            Element root = doc.getDocumentElement();
            NodeList items = root.getElementsByTagName("Car");
            for (int i = 0; i < items.getLength(); i++) {
                car = new Car();
                Element item = (Element) items.item(i);
                // 获取id属性
                car.id = item.getAttribute("id");
                NodeList properties = item.getChildNodes();
                for (int j = 0; j < properties.getLength(); j++) {
                    Node property = properties.item(j);
                    String nodeName = property.getNodeName();
                    // 获取节点值的具体代码
                    if (nodeName.equals("mark")) {
                        car.mark = property.getFirstChild().getNodeValue();
                    } else if (nodeName.equals("color")) {
                        car.color = property.getFirstChild().getNodeValue();
                    }
                }
                list.add(car);
            }
            for (Car c : list) {
                Log.e("DOM", c.toString());
            }

        } catch (Exception e) {
            e.printStackTrace();
        }

    }


----------
    //上面是DOM解析数据的过程

----------


    // 使用SAX方法解析的结果
    private void saxXml() {
        try {
            // 从assets里面获取文件,
            // 通过getAssets获取到的是读取流
            is = getAssets().open("cars.xml");
            // 取得SAXParserFactory实例
            SAXParserFactory factory = SAXParserFactory.newInstance();
            // 从factory获取SAXParser实例
            SAXParser parser = factory.newSAXParser();
            // 实例化定义的Handler
            Myhandler handler = new Myhandler();
            // 根据Handler规则解析输入流
            parser.parse(is, handler);
            // 打印信息
            handler.showMessage();

        } catch (Exception e) {
            e.printStackTrace();
        }
        // 实例话

    }
//实现SAX方法解析数据必须要借助DefaultHandler 类的实现
    class Myhandler extends DefaultHandler {

        StringBuffer sb = null;

        // 根标签开始时
        @Override
        public void startDocument() throws SAXException {
            super.startDocument();
            list = new ArrayList<MainActivity.Car>();
            sb = new StringBuffer();
        }

        // 节点开始时
        @Override
        public void startElement(String uri, String localName, String qName,
                Attributes attributes) throws SAXException {
            super.startElement(uri, localName, qName, attributes);
            // 当遍历到Car节点就新建Car对象
            if (localName.equals("Car")) {
                car = new Car();
                car.id = attributes.getValue(0);// 获取第一个属性值,节点值在后面获取
            }
            // 将字符长度设置为零,以便从新开始读取元素内就字符节点
            sb.setLength(0);
        }

        // 读取字符流
        @Override
        public void characters(char[] ch, int start, int length)
                throws SAXException {
            super.characters(ch, start, length);
            sb.append(ch, start, length);

        }

        // 节点结束时
        @Override
        public void endElement(String uri, String localName, String qName)
                throws SAXException {
            super.endElement(uri, localName, qName);
            if (localName.equals("mark")) {
                car.mark = sb.toString();
            } else if (localName.equals("color")) {
                car.color = sb.toString();
            } else if (localName.equals("Car")) {
                list.add(car);
            }

        }

        public void showMessage() {
            // 打印获得的数据
            for (Car c : list) {
                Log.e("SAX", c.toString());
            }

        }

    }


----------
//上面是SAX方法解析数据的过程

----------


    // 使用pull方法解析文件的具体操作
    private void pullXml() {
        try {
            // 从assets里面获取文件,
            // 通过getAssets获取到的是读取流
            is = getAssets().open("cars.xml");
            // 创建一个pull解析对象,并获取示例
            XmlPullParser pull = Xml.newPullParser();
            // 设置解析格式
            pull.setInput(is, "utf-8");

            // 开始解析
            // 产生事件用来循环
            int type = pull.getEventType();

            // 实例化List集合存放从XML文件获取到的数据
            list = new ArrayList<Car>();
            // 通过判断事件,来获取相应的数据
            while (type != XmlPullParser.END_DOCUMENT) {// 没到结束的根标签就一直循环获取
                // 通过判断事件,
                switch (type) {
                case XmlPullParser.START_TAG:// 标签开始
                    // 获取节点值节点属性值的具体代码
                    if (pull.getName().equals("Car")) {// 当遍历到标签开始是Car
                        // 新建Car对象
                        car = new Car();
                        // 获取标签属性id,属性值和节点值的取法是不一样的
                        car.id = pull.getAttributeValue(0);

                    } else if (pull.getName().equals("mark")) {
                        // 获取标签的信息
                        car.mark = pull.nextText();
                    } else if (pull.getName().equals("color")) {
                        car.color = pull.nextText();
                    }
                    break;
                case XmlPullParser.END_TAG:// 标签结束时
                    // 如果是Car标签结束,就对数据对象进行存储
                    if (pull.getName().equals("Car")) {
                        list.add(car);
                    }
                    break;

                default:
                    break;
                }

                // 继续下一个标签
                pull.next();
                type = pull.getEventType();

            }

            // 打印一下集合的数据
            for (Car car2 : list) {
                Log.e("pull", car2.toString());
            }

        } catch (Exception e) {
            e.printStackTrace();
        }

    }



----------
    //上面的使用pull方法解析文件的过程

----------


//定义一个对象类,用来存放和显示数据
    static class Car {
        String id;
        String color;
        String mark;

        @Override
        public String toString() {
            return "Car [id=" + id + ", color=" + color + ", mark=" + mark
                    + "]";
        }

    }

}


程序运行后显示的Log内容如下:

xml文件解析后


   上面就是三种解析XML数据的方法,其中值得注意的是截取节点的值和截取节点的属性的值用的方法是不一样的。也要区分根节点和普通节点要执行的相关操作。
   本示例主要数实现数据的解析过程,相关数据的处理没有很好的表现出来,但是结果都是能解析出正确的数值的。
   对于这三种解析器各有优点,一般推荐使用PULL解析器,因为SAX解析器操作起来太笨重,DOM不适合文档较大,内存较小的场景,唯有PULL轻巧灵活,速度快,占用内存小, 使用非常顺手。大家也可以根据自己的喜好选择相应的解析技术。
   上面是解析xml文件的三种基本方式,后面也会接触到很多其他封装类的解析方法。但是都是在上面基础上设计的。

作者:wenzhi20102321 发表于2016/10/10 8:54:03 原文链接
阅读:58 评论:0 查看评论

关于Activity的一些个人总结,比较散乱。

$
0
0
一、Activity简介
1. 什么是Activity?
首先来看看Google Android开发官网上对于Activity的定义:Activity 是一个应用组件,用户可与其提供的屏幕进行交互,以执行拨打电话、拍摄照片、发送电子邮件或查看地图等操作。 每个 Activity 都会获得一个用于绘制其用户界面的窗口。窗口通常会充满屏幕,但也可小于屏幕并浮动在其他窗口之上。
通俗的来说呢Activity是一个可以包含用户界面的组件,一个应用程序中可以包含零个或多个Activity(包含零个的应用,属于无法被看到的应用这种应用很少见),一般会指定其中一个Activity作为MainActivity,而每个Activity均可以启动另一个Activity。Activity是用户接口程序,会提供给用户一个交互式的接口功能,是Activity应用程序的基本功能单元。
2.如何创建Activity?
同样的,在开发官网上也做了比较详细的解释:要创建 Activity,您必须创建 Activity 的子类(或使用其现有子类)。您需要在子类中实现 Activity 在其生命周期的各种状态之间转变时(例如创建 Activity、停止 Activity、恢复 Activity 或销毁 Activity 时)系统调用的回调方法。 两个最重要的回调方法是:
onCreate()
您必须实现此方法。系统会在创建您的 Activity 时调用此方法。您应该在实现内初始化 Activity 的必需组件。 最重要的是,您必须在此方法内调用 setContentView(),以定义 Activity 用户界面的布局。
onPause()
系统将此方法作为用户离开 Activity 的第一个信号(但并不总是意味着 Activity 会被销毁)进行调用。 您通常应该在此方法内确认在当前用户会话结束后仍然有效的任何更改(因为用户可能不会返回)。
从上面的解释中可以看出三点信息:a.要创建Activity,首先需要创建Activity的子类(即创建类并extends Activity)。b.Activity在被创建时,首先调用的是onCreate()方法,当用户离开Activity时,调用的是onPause()方法[具体的这两个回调方法会在下一章的生命周期中详细讲解] 。c.Activity本身并没有界面用于显示,它只是创建了一个窗口[窗口通常充满屏幕,也可以小于屏幕而浮于其他界面之上],开发者在Activity中重要的回调方法onCreate()方法内调用setContentView()方法来显示用户界面。
而上文提到的setContentView()方法中需要的是.xml文件,所以我们对于Activity的界面布局就需要写到在此Activity中onCreate()中调用的setContentView()中参数一致的xml文件中,这样子在Activity被创建之后,才会将你想要显示的界面元素一一显示在用户界面中。而简单来说的xml是可扩展标记性语言而具体的情况可以去这里看看,里面介绍的很详细而且还有举例说明{http://blog.csdn.net/fengbingchun/article/details/38978591}
3.xml文件的几种加载方式
上文提到的我们可以在xml文件中对Activity显示的界面元素进行布局,那么如何将xml文件加载到Activity中就成为了最重要的问题。一般情况下我们可以通过如下几种方法来实现加载。
①setContentView(),这种加载一般在activity中的onCreat()方法中用于加载应用的布局内容显示和标题显示。也就是说它加载的俩个部分,一个是标题部分,一个是内容部分。
②LayoutInflater实例化的对象,通过inflate()方法可以获取布局文件,这个获取的布局文件和findViewById()的方式不同,inflate()方法是在res/layout中获取xml格式的文件,同时返回的是view对象,可以让我们动态添加布局文件。findViewById(,这种方式是在某一个布局中根据id获取一个控件,同样也可以获取一个布局控件。
③继承PreferenceActivity的类加载xml文件,使用方法addPreferencesFromResource(R.xml.preferences)来加载xml文件。
④布局中使用<include>标签,此方法是在一个xml文件中加载另一个xml中的布局,而将xml加载入Activity来显示的方法可以使用上文所说的任一种方法。
在我们将xml文件加载入Activity准备用于显示后,我们就需要进行最重要的这一步了,即在manifest中声明此Activity。
4.在manifest中声明Activity
Manifest属于AndroidManifest.xml文件,这个文件里面包含了你的应用的所有信息[http://my.oschina.net/weiCloudS/blog/367709 这个是关于AndroidManifest文件的详细介绍],所以创建的Activity还需要在AndroidManifest.xml文件中进行声明,这样才能保证你的应用在运行之前所有的信息被系统所了解以便于显示出你想要显示的内容。具体的声明方式如下:
<manifest ... >
<application ... >
<activity android:name=".ExampleActivity" />
...
</application ... >
...
</manifest >
就这样子一个Activity就已经被创建好了,在IDE中run起来之后就可以看到自己所定义的界面了。
5. AndroidManifest文件的详细介绍
每个应用的根目录中都必须包含一个 AndroidManifest.xml 文件(且文件名精确无误)。 清单文件为 Android 系统提供有关您的应用的基本信息,系统必须获得这些信息才能运行任意应用代码。 此外,清单文件还可执行以下操作:
• 为应用的 Java 软件包命名。软件包名称充当应用的唯一标识符
• 描述应用的各个组件,即:构成应用的 Activity、服务、广播接收器和内容提供程序。 为实现每个组件的类命名并发布其功能(例如,它们可以处理的 Intent 消息)。根据这些声明,Android 系统可以了解这组件具体是什么,以及在什么条件下可以启动它们
• 确定将托管应用组件的进程
• 声明应用必须具备哪些权限才能访问 API 中受保护的部分并与其他应用交互
• 还声明其他应用与该应用组件交互所需具备的权限
• 列出 Instrumentation 类,这些类可在应用运行期间提供分析和其他信息。这些声明只会在应用处在开发和测试阶段时出现在清单文件中;它们会在应用发布之前被删除
• 声明应用所需的最低 Android API 级别
• 列出应用必须链接到的库
一般情况下在AndroidManifest文件的结构如下[已经包含了所有的元素]:
<?xml version="1.0" encoding="utf-8"?>

<manifest>

<uses-permission />
<permission />
<permission-tree />
<permission-group />
<instrumentation />
<uses-sdk />
<uses-configuration />
<uses-feature />
<supports-screens />
<compatible-screens />
<supports-gl-texture />

<application>

<activity>
<intent-filter>
<action />
<category />
<data />
</intent-filter>
<meta-data />
</activity>

<activity-alias>
<intent-filter> . . . </intent-filter>
<meta-data />
</activity-alias>

<service>
<intent-filter> . . . </intent-filter>
<meta-data/>
</service>

<receiver>
<intent-filter> . . . </intent-filter>
<meta-data />
</receiver>

<provider>
<grant-uri-permission />
<meta-data />
<path-permission />
</provider>

<uses-library />

</application>

</manifest>
在AndroidManifest文件中只有<manifest>和<application>是必须的,并且只能出现一次,其余大部分元素可以出现多次或不出现。同一级别的元素出现的顺序并没有什么格式,但是<activity-alias>必须跟在别名所指的 <activity>之后。并且在编程过程中我们都可以借用以上的元素其中之一或一部分来实现我们所需要实现的功能和需求。关于详细的讲解还需要进行自己动手编程来体验和理解其工作的原理。在此我们只对其中一部分进行讲解。
①action:动作标签,存在intent-filter标签中,如果与一个activity绑定,可以表示该活动具有该动作,如果有一个意图发出来,并且带有这种一个Activity定义的动作,那么,它就有可能定位到该activity,一般与category一起用。eg:

②activity:活动标签,定义大量的属性来设置Activity的一些特性,譬如:android:configChanges、android:label、android:launchMode等,这些属性的详细的内容可以在官方文档中进行查看。
③activity-alias:活动标签替代者,一般用于对Activity来设置别名,其最重要的属性是android:targetActivity,通过这个属性来设置指向的Activity。
④application:应用程序声明,具有大量的属性,并且Activity、Service、Receiver均定义在此标签内部,并且还可以进行一种类似于全局的配置作用,譬如设定主题。
⑤intent-filter:意图过滤器,定义了Activity,Service或者广播可以接收到的Intent,必须包含action标签。
⑥meta-data:元数据标签,用于定义一些系统可以用到的数据。但是必须为元数据定义一个独一无二的名字。
⑦manifest:主配置标签,类似于网页的<html>标签一样。
6.如何启动Activity?
经过上面的两步我们就已经有一个拥有界面元素的Activity了,接下来研究的就是如何将Activity启动起来了。通常情况下我们启动MainActivity时只需要在IDE中将你的应用Run起来之后就可以在虚拟机或者设备上就可以看到了,但是一般来说一个应用总是由多个Activity松耦合来一起组成的,这就需要我们能够从MainActivity或者任意一个Activity去跳转到所需要的Activity去。一般在启动Activity时有三种不同的方式来启动一个Activity:
①显示启动:通俗来讲就是通过代码来直白的启动一个Activity。代码如下:
Intent intent = new Intent(this, SignInActivity.class);
startActivity(intent);
②隐式启动:与显示启动相对立,通俗地说就是不那么直接的启动一个Activity,而是通过在manifest文件中添加inten-filter,再代码中启动这个intent即可。代码如下:
manifest中的配置:
Activity中的启动:

③设置别名启动:这种启动方式其实主要是在Activity标签中设置activity-alias标签,并设置intent-filter,此时运行到模拟器或者真机上之后会生成两个入口图标,一个是原始Activity的入口,另一个是设置别名之后的入口。
7.如何结束Activity?
在启动Activity之后,我们不能一直将其开启,这样子会占用很大一部分的内存资源,所以就需要在恰当的时候讲开启的Activity结束掉。就可以通过调用 Activity 的 finish() 方法来结束该 Activity。您还可以通过调用 finishActivity() 结束您之前启动的另一个 Activity。{但是一般情况下不建议使用此方法直接结束掉Activity,应该在下文介绍的Activity的生命周期中对应的回调方法中进行}
8.Intent和IntentFilter简介
Intent负责对操作的动作、动作涉及数据、附加数据进行描述,Android则根据此Intent的描述,负责找到对应的组件,将 Intent传递给调用的组件,并完成组件的调用。因此,Intent起着一个媒体中介的作用,专门提供组件互相调用的相关信息,实现调用者与被调用者之间的解耦。在应用中,我们可以以两种形式来使用Intent:
  直接Intent:指定了component属性的Intent(调用setComponent(ComponentName)或者setClass(Context, Class)来指定)。通过指定具体的组件类,通知应用启动对应的组件。
  间接Intent:没有指定component属性的Intent。这些Intent需要包含足够的信息,这样系统才能根据这些信息,在在所有的可用组件中,确定满足此Intent的组件。
对于直接Intent,Android不需要去做解析,因为目标组件已经很明确。
Android需要解析的是那些间接Intent,通过解析,将 Intent映射给可以处理此Intent的Activity、IntentReceiver或Service。Intent解析机制主要是通过查找已注 册在AndroidManifest.xml中的所有IntentFilter及其中定义的Intent,最终找到匹配的Intent。
Intent Filter 描述了一个组件愿意接收什么样的 Intent 对象,Android 将其抽象为 android.content.IntentFilter 类。在 Android 的 AndroidManifest.xml 配置文件中可以通过 <intent-filter >节点为一个 Activity 指定其 Intent Filter,以便告诉系统该 Activity 可以响应什么类型的 Intent。
  当使用 startActivity(intent) 来启动另外一个 Activity 时,如果直接指定 intent 对象的 Component 属性,那么 Activity Manager 将试图启动其 Component 属性指定的 Activity。否则 Android 将通过 Intent 的其它属性从安装在系统中的所有 Activity 中查找与之最匹配的一个启动,如果没有找到合适的 Activity,应用程序会得到一个系统抛出的异常。
9.如何判断是Activity还是控件?
当我们不确定时候,就要想办法去验证,这里主要是介绍验证的方法。
①使用hierarchyview工具,工具上会显示root的手机正在运行的activity或者控件
②使用grep “关键字” -inrs ./ 命令。查找界面上的关键字定位具体的类。
总之当我们确定显示的代码是什么,是控件还是Activity就很清楚了。
二、Activity生命周期
1.生命周期介绍
生命周期,从字面意思都能看来它对于Activity的影响是最多的,所以对于生命周期的学习是整个Activity的学习中最重要的部分。整体来说Activity的生命周期总共有7个,分别是:onCreate()、onStart()、onResume()、onPause()、onStop()、onDestroy()、onRestart()。具体的生命周期的流程见图2.1。可以看出来除了onRestart()之外其他的生命周期都是两两相对应的。所以在记忆和学习的时候就可以两两相互对应比对着学习。
2.onCreate()
在每个Activity中都会重写这个方法的,它会在Activity第一次被创建的时候调用,应该在此方法中执行所有正常的静态设置—创建视图、将数据绑定到列表等等。系统向此方法传递一个 Bundle 对象,其中包含 Activity 的上一状态,不过前提是捕获了该状态,始终后接 onStart()。
3.onStart()
在 Activity 即将对用户可见之前调用。如果 Activity 转入前台,则后接 onResume(),如果 Activity 转入隐藏状态,则后接 onStop()。
4.onResume()
在 Activity 即将开始与用户进行交互之前调用。 此时,Activity 处于 Activity 堆栈的顶层,并具有用户输入焦点。
始终后接 onPause()。
5.onPause()
当系统即将开始继续另一个 Activity 时调用。 此方法通常用于确认对持久性数据的未保存更改、停止动画以及其他可能消耗 CPU 的内容,诸如此类。 它应该非常迅速地执行所需操作,因为它返回后,下一个 Activity 才能继续执行。
如果 Activity 返回前台,则后接 onResume(),如果 Activity 转入对用户不可见状态,则后接 onStop()。
6.onStop()
Activity 对用户不再可见时调用。如果 Activity 被销毁,或另一个 Activity(一个现有 Activity 或新 Activity)继续执行并将其覆盖,就可能发生这种情况。如果 Activity 恢复与用户的交互,则后接 onRestart(),如果 Activity 被销毁,则后接 onDestroy()。
7.onDestroy()
在 Activity 被销毁前调用。这是 Activity 将收到的最后调用。 当 Activity 结束(有人调用 Activity 上的 finish()),或系统为节省空间而暂时销毁该 Activity 实例时,可能会调用它。 您可以通过 isFinishing() 方法区分这两种情形。
8.onRestart()
在 Activity 已停止并即将再次启动前调用。始终后接onStart()
9.Activity的三种生存期
在本章第一小节时已经说过Activity的生命周期基本是两两对应的,由此就可以将其分为三个阶段:A、完整生存期:即在onCreate()和onDestroy()之间经历的就是完整生存期,一般而言Activity会在onCreate()中完成所有的初始化,在onDestroy()中完成内存释放等操作。B、可见生存期:即在onStart()和onStop()之间经历就是可见生存期,在此期间内整个Activity对于用户是可见的,即便有时无法和用户进行交互。我们可以利用这一特性从而开发出更加节省空间的Activity,例如:在onStart()中加载资源,在onStop()中释放资源,就可以节省Activity在停止过程中不会占用过多的资源。C、前台生存期:即活动在onResume()和onPasue()之间经历的就是前台生存期,Activity总是处于活动状态,此期间的Activity总是和用户进行交互的,平时接触最多的也是这个状态期间的Activity。
10.Activity状态保存
通常情况下在Activity暂停或者停止时,Activity 的状态会得到保留。因为当 Activity 暂停或停止时,Activity 对象仍保留在内存中—有关其成员和当前状态的所有信息仍处于 Activity 状态。 因此,用户在 Activity 内所做的任何更改都会得到保留,这样一来,当 Activity 返回前台(当它“继续”)时,这些更改仍然存在。
但是,当Activity进入暂停或停止状态时,系统内存不足时会优先将此状态的Activity销毁掉,其Activity对象也被销毁,当用户返回Activity时,系统无法保存其完好状态返回此Activity,只能重新创建Activity对象,但是对于用户来说,他可能会认为此Activity无任何变化,在这种情况下就需要通过onSaveInstanceState()方法来保存有关于Activity的状态信息,确保有关 Activity 状态的重要信息得到保留。系统会先调用 onSaveInstanceState(),然后再使 Activity 变得易于销毁。系统会向该方法传递一个 Bundle,可以在其中使用 putString()和 putInt()等方法以名称-值对形式保存有关 Activity 状态的信息。然后,如果系统终止了应用进程,并且用户返回到了此Activity,则系统会重建该Activity,并将Bundle同时传递给onCreate()和 onRestoreInstanceState()。您可以使用上述任一方法从 Bundle 提取您保存的状态并恢复该 Activity 状态。如果没有状态信息需要恢复,则传递给您的 Bundle 是空值(如果是首次创建该 Activity,就会出现这种情况)。图2.2展示了当Activity不可见时,被kill掉重新返回和直接reStart Activity时所需要实现的方法。

图2.2:Activity中断、恢复、kill掉返回时流程
11.横竖屏切换时Activity生命周期的变换
学习生命周期的调用顺序和调用时机最好的方法是将七个回调方法都重写一遍在其中打上log输出在logcat中查看是最好的,所以就先给你们看看横竖屏切换时Activity生命周期log输出。
从中可以看出每次横竖屏切换时,都会重新调用一次完整的生命周期,相当于是重新创建了一次Activity。这样就会导致有时在Activity中的数据由于横竖屏切换的原因而导致数据的丢失问题,用户体验就会降低,但是从图中可以看出每次切换时,在Activity不可见之前总是会调用onSaveInstanceState()这个方法,并且在Activity重新可见之前会调用onRestoreInstanceState()方法,所以在横竖屏切换时可以用onSaveInstanceState()方法来保存一些Activity的状态信息、数据信息等。而用onRestoreInstanceState()方法来恢复Activity的状态信息、数据信息等,以提高用户体验。
也可以通过设置将Activity固定为横屏或竖屏,就不会出现横竖屏切换时生命周期的影响了。可以通过下列两种方式来设置:a. 在主配置清单中直接通过android:screenOrientation属性来设置。b.在onCreate()方法中通过setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);或setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);来设置。
如果必须要有横竖屏的切换时,也可以在manifest文件中的activity标签下加入android:configChanges="orientation|keyboardHidden|screenSize"属性来进行控制。在这个属性中orientation代表重力方向,keyboardHidden表示将输入键盘隐藏,screenSize表示适应整个屏幕。
注:在设置android:configChanges属性时,Google给我们提供了一共13个值来进行我们需要的设置。设置时,这13个值是可以一起进行设置的,只需要在中间用“|”进行隔开即可,如:android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize"对于这13个值官网上具体的解释如下:

12.finish()的调用
在前文的介绍中已经提到了finish()这个方法。主要要作用是结束Activity。但是在Activity的生命周期中也有结束Activity的方法,本节主要是讲述在Activity的生命周期的个方法中调用finish方法之后,Activity生命周期的走向。
①onCreate():
log:

②onStart():
log:

③onResume:
log:

④onPause:
log:

⑤onStop:
log:

⑥onDestroy:
log:

⑦onRestart:
log:

⑧onRestoreIntanceState:
log:

⑨onSavedInstanceState:
log:

从整个的log中可以看出生命周期是两两相互对应的,例如在onCreate中执行finish,则只执行onDestroy;在onStart中执行finish,则只执行onStop和onDestroy。从这就可以看出有先必有后,执行了第一个生命周期的方法,必定会执行与之对应的生命周期方法。但是其中onSavedInstanceState和onRestoreIntanceState这两个方法并不是完全相对应的,因为这两个方法是用于保存Activity状态和恢复状态的方法,并且onRestoreIntanceState仅仅会在横竖屏切换时才会执行此方法,这一点需要注意。
onDestroy()方法的作用也是销毁Activity的作用,所以在onDestroy中调用finish方法的时候需要注意的是要首先判断下当前Activity是否已经被destroy掉,否则在某种条件下是会报错的。
三、Activity之间的通信、栈式管理
1.Activity之间传递消息
在第一章已经简单的介绍了startActivityForResult()方法来进行Activity之间消息的相互传递。但是有时候我们需要传递消息的Activity实例可能运行在不同的进程之中,因此在传递消息时,就需要通过Intent对象来进行消息的传递,Android 中通过 Intent 对象来表示一条消息,一个 Intent 对象不仅包含有这个消息的目的地,还可以包含消息的内容。对于这个Intent 对象来说目的地是必须的,而内容是可选的。关于Intent的详细介绍和使用已经在第一章中做过介绍了,此处就不再赘述。
2.Activity的栈式管理

在上文中,我已经提到过了栈这个东西。简单来说呢,栈是一种先进后出的数据结构[关于栈的详细介绍可以去搜索下“数据结构中栈的介绍”去了解下]。在一开始的时候我就已经说过一个应用可以有多个Activity组成,而在Android中,就是通过栈来管理众多的Activity的。也就是说,在某一时刻,只能有一个Activity位于栈顶,即只有一个Activity能够和用户进行交互,只有当这个Activity被暂停或者销毁后,下面的Activity才有可能浮到栈顶,或者有一个新的Activity被创建出来,则旧的Activity就被压栈沉下去了。Activity是Android程序的表现层。程序的每一个显示屏幕就是一个Activity。正在运行的Activity处在栈的最顶 端,它是运行状态的。

从此图中也可以看出Activity在非活动状态时是极易被kill掉的,上图的颜色表示一种被kill掉的可能性的大小,越在栈底的越容易被kill掉,但是具体的情况和设备的内存状况有关。
四、Activity的启动模式以及用法
有时候在项目中我们需要根据特殊的需求来为Activity指定合适的启动模式。启动模式一共分为四种,分别是standard、singleTop、singleTask和singleInstance,可以在主配置清单文件中<activity>标签下设定android:launchMode来选择启动模式。默认情况下启动方式为standard。
1.standard
为默认的启动模式,简单来说就是在这种模式下每次启动一个Activity都会创建一个新的实例,入栈并处于栈顶。
2.SingleTop
当启动模式为singleTop时,每当启动Activity时,若发现栈顶已经是此Activity则直接使用此实例,若栈顶不是同一Activity则重新创建实例,并置入栈顶。
3.SingleTask
当启动模式为SingleTask时,每次启动Activity时系统首先会在栈中检查是否存在着该Activity的实例,若存在则直接使用此实例,并将此Activity之上的所有Activity出栈,若不存在,则创建新的实例。
4.SingleInstance
SingleInstance这种启动模式是最为复杂的模式,以SingleInstance模式启动Activity时,会启用一个新的栈来管理这个活动,这样的方式就会使得之外的任一程序来访问此Activity时,都会共用同一个栈来进行处理,也就能够实现共享Activity实例的功能。
注:在SingleInstance模式下实现共享的前提是所有的Activity都没有被销毁。

====================================================================================================================================

中间有一部分内容是从别的博客中参考的,之前写的时候没有及时注明出处。我的失误。有的log的图片我不知道怎么上传。回头等我会用这个了再更新下这个文档。共勉。


作者:Joseph_wkq 发表于2016/10/10 9:18:29 原文链接
阅读:25 评论:0 查看评论

Android常用的面试题

$
0
0

1.什么是Service以及描述下它的生命周期。Service有哪些启动方法,有什么区别,怎样停用Service?


Android Service是运行在后台的代码,不能与用户交互,可以运行在自己的进程,也可以运行在其他应用程序进程的上下文里。

需要通过某一个Activity或者Context对象来调用。Service有两个启动方法,分别是Context.startService()和Context.bindService()。如果在Service执行耗时的操作需要启动一个新线程来执行。

Android Service只继承了onCreate(), onStart(),onDestroy()三个方法,当我们第一次启动Service时,先后调用onCreate(), onStart()这两个方法,当停止Service时,则执行onDestroy()方法时。如果Service已经启动了,当我们再次启动Service时,不会再执行onCreate()方法,而是直接执行onStart()方法。



2.Error与Exception的区别

Error一般是指恢复不是不可能但是很困难的情况下的一张严重问题,比如内存溢出,不可能是指指望程序能处理这样的情况。
Exception是指程序的设计或实现问题,一般是比较容易恢复的,如果程序运行正常是不会出现在这样的错误的。

3.接口与抽象类的区别
接口是公开的,里面不能有私有的方法或变量,是用于让别人使用的,而抽象类是可以有私有方法或私有变量的,另外,实现接口的一定要实现接口里定义的所有方法,而实现抽象类可以有选择地重写需要用到的方法,一般的应用里,最顶级的是接口,然后是抽象类实现接口,最后才到具体类实现。还有,接口可以实现多重继承,而一个类只能继承一个超类,但可以通过继承多个接口实现多重继承,接口还有标识(里面没有任何方法,如Remote接口)和数据共享(里面的变量全是常量)的作用.

3.Get和Post的区别
1.get是从服务器上获取数据,post是向服务器传送数据;
2.对于get方式,服务器端用Request.QueryString获取变量的值,对于post方式,服务器端用Request.From获取提交的数据。
3.get安全性较低,post安全性较好,但是get的执行效率比post方式要好;
4.get传送的数据量较小,而post传送的数据量较大;

4.Activity的生命周期

相信不少朋友也已经看过这个流程图了,也基本了解了Activity生命周期的几个过程,我们就来说一说这几个过程。

1.启动Activity:系统会先调用onCreate方法,然后调用onStart方法,最后调用onResume,Activity进入运行状态。

2.当前Activity被其他Activity覆盖其上或被锁屏:系统会调用onPause方法,暂停当前Activity的执行。

3.当前Activity由被覆盖状态回到前台或解锁屏:系统会调用onResume方法,再次进入运行状态。

4.当前Activity转到新的Activity界面或按Home键回到主屏,自身退居后台:系统会先调用onPause方法,然后调用onStop方法,进入停滞状态。

5.用户后退回到此Activity:系统会先调用onRestart方法,然后调用onStart方法,最后调用onResume方法,再次进入运行状态。

6.当前Activity处于被覆盖状态或者后台不可见状态,即第2步和第4步,系统内存不足,杀死当前Activity,而后用户退回当前Activity:再次调用onCreate方法、onStart方法、onResume方法,进入运行状态。

7.用户退出当前Activity:系统先调用onPause方法,然后调用onStop方法,最后调用onDestory方法,结束当前Activity。

但是知道这些还不够,我们必须亲自试验一下才能深刻体会,融会贯通。

下面是一张经典的Activity生命周期图


5.简单描述一下你对service的理解

service与activity的地位是并列的,它代表一个单独的安卓组件。service与activity的区别在与:service通常位于后台运行,它一般不需要与用户交互,因此service组件没有图形用户界面。

与activity组件需要继承Activity基类相似,service组件需要继承service基类。一个service组件被运行起来后,它将拥有自己独立的生命周期,service组件通常用于为其他的组件提供后台服务或监控其他组件的运行状态。

6. handler机制的原理 
  andriod提供了Handler 和 Looper 来满足线程间的通信。Handler先进先出原则。Looper类用来管理特定线程内对象之间的消息交换(MessageExchange)。

  1)Looper: 一个线程可以产生一个Looper对象,由它来管理此线程里的MessageQueue(消息队列)。 

  2)Handler: 你可以构造Handler对象来与Looper沟通,以便push新消息到MessageQueue里;或者接收Looper从Message Queue取出)所送来的消息。

  3) Message Queue(消息队列):用来存放线程放入的消息。 

  4)线程:UIthread 通常就是main thread,而Android启动程序时会替它建立一个MessageQueue。
 
7.说说mvc模式的原理,它在android中的运用 
 MVC(Model_view_contraller)”模型_视图_控制器”。 MVC应用程序总是由这三个部分组成。Event(事件)导致Controller改变Model或View,或者同时改变两者。只要Controller改变了Models的数据或者属性,所有依赖的View都会自动更新。

8. 请解释下在单线程模型中Message、Handler、MessageQueue、Looper之间的关系。
Handler简介:
一个Handler允许你发送和处理Message和Runable对象,这些对象和一个线程的MessageQueue相关联。每一个线程实例和一个单独的线程以及该线程的MessageQueue相关联。当你创建一个新的Handler时,它就和创建它的线程绑定在一起了。这里,线程我们也可以理解为线程的MessageQueue。从这一点上来看,Handler把Message和Runable对象传递给MessageQueue,而且在这些对象离开MessageQueue时,Handler负责执行他们。

Handler
有两个主要的用途:(1)确定在将来的某个时间点执行一个或者一些Message和Runnable对象。(2)在其他线程(不是Handler绑定线程)中排入一些要执行的动作。

Scheduling Message
,即(1),可以通过以下方法完成:
post(Runnable):Runnable
在handler绑定的线程上执行,也就是说不创建新线程。
postAtTime(Runnable,long):
postDelayed(Runnable,long):
sendEmptyMessage(int):
sendMessage(Message):
sendMessageAtTime(Message,long):
sendMessageDelayed(Message,long):
post
这个动作让你把Runnable对象排入MessageQueue,MessageQueue受到这些消息的时候执行他们,当然以一定的排序。sendMessage这个动作允许你把Message对象排成队列,这些Message对象包含一些信息,Handler的hanlerMessage(Message)会处理这些Message.当然,handlerMessage(Message)必须由Handler的子类来重写。这是编程人员需要作的事。

当posting或者sending到一个Hanler时,你可以有三种行为:当MessageQueue准备好就处理,定义一个延迟时间,定义一个精确的时间去处理。后两者允许你实现timeout,tick,和基于时间的行为。

当你的应用创建一个新的进程时,主线程(也就是UI线程)自带一个MessageQueue,这个MessageQueue管理顶层的应用对象(像activities,broadcast receivers等)和主线程创建的窗体。你可以创建自己的线程,并通过一个Handler和主线程进行通信。这和之前一样,通过post和sendmessage来完成,差别在于在哪一个线程中执行这么方法。在恰当的时候,给定的Runnable和Message将在Handler的MessageQueue中被Scheduled。


Message
简介:
Message
类就是定义了一个信息,这个信息中包含一个描述符和任意的数据对象,这个信息被用来传递给Handler.Message对象提供额外的两个int域和一个Object域,这可以让你在大多数情况下不用作分配的动作。
尽管Message的构造函数是public的,但是获取Message实例的最好方法是调用Message.obtain(),或者Handler.obtainMessage()方法,这些方法会从回收对象池中获取一个。


MessageQueue
简介:
这是一个包含message列表的底层类。Looper负责分发这些message。Messages并不是直接加到一个MessageQueue中,而是通过MessageQueue.IdleHandler关联到Looper。
你可以通过Looper.myQueue()从当前线程中获取MessageQueue。


Looper
简介:
Looper
类被用来执行一个线程中的message循环。默认情况,没有一个消息循环关联到线程。在线程中调用prepare()创建一个Looper,然后用loop()来处理messages,直到循环终止。

大多数和message loop的交互是通过Handler。

下面是一个典型的带有Looper的线程实现。
  class LooperThread extends Thread {
      public Handler mHandler;
      
      public void run() {
          Looper.prepare();
          
          mHandler = new Handler() {
              public voidhandleMessage(Message msg) {
                  // process incomingmessages here
              }
          };
          
          Looper.loop();
      }
  }


9. 简要解释一下activity、 intent 、intent filter、service、Broadcase、BroadcaseReceiver
  答:一个activity呈现了一个用户可以操作的可视化用户界面 

  一个service不包含可见的用户界面,而是在后台无限地运行 

  可以连接到一个正在运行的服务中,连接后,可以通过服务中暴露出来的借口与其进行通信 

  一个broadcast receiver是一个接收广播消息并作出回应的component,broadcastreceiver没有界面 

  intent:content provider在接收到ContentResolver的请求时被激活。 

  activity, service和broadcast receiver是被称为intents的异步消息激活的。 

一个intent是一个Intent对象,它保存了消息的内容。对于activity和service来说,它指定了请求的操作名称和待操作数据的URI 

  Intent对象可以显式的指定一个目标component。如果这样的话,android会找到这个component(基于manifest文件中的声明)并激活它。但如果一个目标不是显式指定的,android必须找到响应intent的最佳component。

  它是通过将Intent对象和目标的intent filter相比较来完成这一工作的。一个component的intent filter告诉android该component能处理的intent。intent filter也是在manifest文件中声明的。

10.handler机制的原理 
  andriod提供了Handler 和 Looper 来满足线程间的通信。Handler先进先出原则。Looper类用来管理特定线程内对象之间的消息交换(MessageExchange)。

  1)Looper: 一个线程可以产生一个Looper对象,由它来管理此线程里的MessageQueue(消息队列)。 

  2)Handler: 你可以构造Handler对象来与Looper沟通,以便push新消息到MessageQueue里;或者接收Looper从Message Queue取出)所送来的消息。

  3) Message Queue(消息队列):用来存放线程放入的消息。 

  4)线程:UIthread 通常就是main thread,而Android启动程序时会替它建立一个MessageQueue。 

11.简述Android应用程序的组成
(1)Activites  应用程序的表示层。

应用程序的每个界面都将是Activity类的扩展。Acitvities用视图(View)构成GUI来显示信息、响应用户操作。就桌面开发而言,一个活动(Activity)相当于一个窗体(Form)。

(2)Services  应用程序中的隐形工作者。

Service组件在后台运行,更新你的数据源和可见的Activities,触发通知(Notification)。在应用程序的Activities不激活或不可见时,用于执行依然需要继续的长期处理。

(3)Content Providers  可共享的数据存储。

Content Providers用于管理和共享应用程序数据库。是跨应用程序边界数据共享的优先方式。

(4)Intents  一个应用程序间(inter-application)的消息传递框架。

使用Intents你可以在系统范围内广播消息或者对一个目标Activity或Service发送消息,来表示你要执行一个动作。

(5)Widgets  可以添加到主屏幕界面(home screen)的可视应用程序组件。

作为Broadcase Receiver的特殊变种,widgets让你可以为用户创建可嵌入到主屏幕界面的动态的、交互的应用程序组件。


12.基础变量与引用变量的存放

1.寄存器:最快的存储区, 由编译器根据需求进行分配,我们在程序中无法控制. 
2. 栈:存放基本类型的变量数据和对象的引用,但对象本身不存放在栈中,而是存放在堆(new 出来的对象)或者常量池中(字符串常量对象存放在常量池中。) 
3. 堆:存放所有new出来的对象。 
4. 静态域:存放静态成员(static定义的) 
5. 常量池:存放字符串常量和基本类型常量(public static final)。 
6. 非RAM存储:硬盘等永久存储空间 

13.Android中listview出现卡顿的原因

1..Adapter的getView方法里面convertView没有使用setTag和getTag方式;
2.在getView方法里面ViewHolder初始化后的赋值或者是多个控件的显示状态和背景的显示没有优化好,抑或是里面含有复杂的计算和耗时操作;
3.在getView方法里面 inflate的row 嵌套太深(布局过于复杂)或者是布局里面有大图片或者背景所致;
4.Adapter多余或者不合理的notifySetDataChanged;
5.listview 被多层嵌套,多次的onMessure导致卡顿,如果多层嵌套无法避免,建议把listview的高和宽设置为fill_parent. 如果是代码继承的listview,那么也请你别忘记为你的继承类添加上LayoutPrams,注意高和宽都是fill_parent的;

14.左连接与右连接的区别

左连接是已左边表中的数据为基准,若左表有数据右表没有数据,则显示左表中的数据右表中的数据显示为空。
左联接的结果集包括 LEFT 子句中指定的左表的所有行,而不仅仅是联接列所匹配的行。如果左表的某行在右表中没有匹配行,则在相关联的结果集行中右表的所有选择列表列均为空值。
右联接是左向外联接的反向联接。将返回右表的所有行。如果右表的某行在左表中没有匹配行,则将为左表返回空值。


例子:

A表(a1,b1,c1) B表(a2,b2) 
a1 b1 c1 a2 b2 
01 数学 95 01 张三 
02 语文 90 02 李四 
03 英语 80 04 <a target=_blank target="_blank" href="https://www.baidu.com/s?wd=%E7%8E%8B%E4%BA%94&tn=44039180_cpr&fenlei=mv6quAkxTZn0IZRqIHckPjm4nH00T1YLnWmYmhR3n1Tduj03myF90ZwV5Hcvrjm3rH6sPfKWUMw85HfYnjn4nH6sgvPsT6K1TL0qnfK1TL0z5HD0IgF_5y9YIZ0lQzqlpA-bmyt8mh7GuZR8mvqVQL7dugPYpyq8Q1T1PjDsPWTz" class="baidu-highlight" rel="nofollow" style="text-decoration: none; color: rgb(45, 100, 179);">王五</a> 

select A.*,B.* from A 
left outer join B on(A.a1=B.a2) 
结果是: 
a1 b1 c1 a2 b2 
01 数学 95 01 张三 
02 语文 90 02 李四 
03 英语 80 NULL NULL 

select A.*,B.* from A 
right outer join B on(A.a1=B.a2) 
结果是: 
a1 b1 c1 a2 b2 
01 数学 95 01 张三 
02 语文 90 02 李四 
NULL NULL NULL 04 <a target=_blank target="_blank" href="https://www.baidu.com/s?wd=%E7%8E%8B%E4%BA%94&tn=44039180_cpr&fenlei=mv6quAkxTZn0IZRqIHckPjm4nH00T1YLnWmYmhR3n1Tduj03myF90ZwV5Hcvrjm3rH6sPfKWUMw85HfYnjn4nH6sgvPsT6K1TL0qnfK1TL0z5HD0IgF_5y9YIZ0lQzqlpA-bmyt8mh7GuZR8mvqVQL7dugPYpyq8Q1T1PjDsPWTz" class="baidu-highlight" rel="nofollow" style="text-decoration: none; color: rgb(45, 100, 179);">王五</a>

15.HashMap和Hashtable的区别是什么?

HashMap:非线程安全,高效,支持null;Hashtable:线程安全,低效,不支持null;

16.List set map的区别是什么?

list、set继承collection接口,而map不是;

Map没有继承Collection接口,Map提供key到value的映射。一个Map中不能包含相同key,每个key只能映射一个value。Map接口提供3种集合的视图,Map的内容可以被当做一组key集合,一组value集合,或者一组key-value映射。(Map接口有三个实现类:HashMap,HashTable,LinkeHashMap 

List接口有三个实现类:LinkedList,ArrayList,Vector 

ArrayList和Vector的区别:ArrayList是非线程安全的,效率高;Vector是基于线程安全的,效率低 


list:有放入顺序,元素可重复;

set :无放入顺序,元素不可重复;

map:无放入顺序,元素按键值对存储;

17.什么是嵌入式系统?

嵌入式系统是指当外部数据或事件产生时能够接受并以足够快的速度予以处理,其处理的结果又能在规定的时间之内来控制生产过程或对处理系统作出快速响应,并且控制所有实时任务协调一致运行的系统。

18.MVC模式是什么,有何优缺点?

MVC指的是模型、视图、控制器,一般MVC应用程序都是由这三个部分组成,事件导致控制器改变模型或视图,或者同时改变两者,控制器改变模型的数据或属性后,所有依赖的视图都会自动更新数据,同样,控制器改变视图,视图也会从隐藏的模型中获取最新的数据。


优点:

1、开发人员可以只关注整个结构中的其中某一层;
2、可以很容易的用新的实现来替换原有层次的实现;
3、可以降低层与层之间的依赖;
4、有利于标准化;
5、利于各层逻辑的复用。

缺点:

1.降低了整体的性能;

2.有时会导致级联的修改;

作者:Fang_Jaexi 发表于2016/10/10 9:22:19 原文链接
阅读:26 评论:0 查看评论

android studio 2.2 cmake ffmpeg 简单的播放器(这里只有视频解码)

$
0
0

背景

在上一次的初次使用cmake编译jni后,这次cmake文件再次升级。利用网上的代码来编译一个简单的播放器。本文参照 Android+FFmpeg+ANativeWindow视频解码播放 一文,在cmake下重新编译。特将过程分享给大家。

gradle 配置

apply plugin: 'com.android.application'

android {
    compileSdkVersion 22
    buildToolsVersion "23.0.1"

    //sourceSets.main.jni.srcDirs = ['jniLibs']

    defaultConfig {
        applicationId "jonesx.videoplayer"
        minSdkVersion 9
        targetSdkVersion 22
        versionCode 1
        versionName "1.0"
        ndk {
            abiFilters 'armeabi'
        }
        externalNativeBuild {
            cmake {
                arguments '-DANDROID_TOOLCHAIN=clang','-DANDROID_STL=gnustl_static'
            }
        }


    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }

    externalNativeBuild {
        cmake {
            path "src/main/cpp/CMakeLists.txt"
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:22.2.1'
    compile 'com.android.support:design:22.2.1'
}

cmake 配置

  1. 整个工程的层级。

框架.png
  1. cmake配置文件
    CMakeLists.txt
cmake_minimum_required(VERSION 3.4.1)
set(lib_src_DIR ${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})
include_directories(
        ${CMAKE_SOURCE_DIR}/include
)

add_library(avcodec-57_lib SHARED IMPORTED)
set_target_properties(avcodec-57_lib PROPERTIES IMPORTED_LOCATION
                             ${lib_src_DIR}/libavcodec-57.so)


add_library(avformat-57_lib SHARED IMPORTED)
set_target_properties(avformat-57_lib PROPERTIES IMPORTED_LOCATION
                        ${lib_src_DIR}/libavformat-57.so)

add_library(avutil-55_lib SHARED IMPORTED)
set_target_properties(avutil-55_lib PROPERTIES IMPORTED_LOCATION
                        ${lib_src_DIR}/libavutil-55.so)

add_library(swresample-2_lib SHARED IMPORTED)
set_target_properties(swresample-2_lib PROPERTIES IMPORTED_LOCATION
                        ${lib_src_DIR}/libswresample-2.so)

add_library(swscale-4_lib SHARED IMPORTED)
set_target_properties(swscale-4_lib PROPERTIES IMPORTED_LOCATION
                        ${lib_src_DIR}/libswscale-4.so)


# build application's shared lib
add_library(VideoPlayer SHARED
            VideoPlayer.cpp)

# Include libraries needed for VideoPlayer lib
target_link_libraries(VideoPlayer
log
android
avcodec-57_lib
avformat-57_lib
avutil-55_lib
swresample-2_lib
swscale-4_lib)

解释下,这里添加了依赖的so库,注意路径呀,我在这里坑了半天,还有同事拯救了我。
add_library(swscale-4_lib SHARED IMPORTED)
set_target_properties(swscale-4_lib PROPERTIES IMPORTED_LOCATION
${lib_src_DIR}/libswscale-4.so)
最后,不要忘了,target_link_libraries中添加对应的库。

VideoPlayer.cpp

//
// Created by Jonesx on 2016/3/20.
//

#include <jni.h>
#include "VideoPlayer.h"
#include <android/log.h>
#include <android/native_window.h>
#include <android/native_window_jni.h>

extern "C"{
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
#include "libavutil/imgutils.h"
};


#define  LOG_TAG    "videoplayer"
#define  LOGD(...)  __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)

 JNIEXPORT jint JNICALL Java_jonesx_videoplayer_VideoPlayer_play
        (JNIEnv *env, jclass clazz, jobject surface) {
    LOGD("play");

    // sd卡中的视频文件地址,可自行修改或者通过jni传入
    char *file_name = "/sdcard/test.mp4";

    av_register_all();

    AVFormatContext *pFormatCtx = avformat_alloc_context();

    // Open video file
    if (avformat_open_input(&pFormatCtx, file_name, NULL, NULL) != 0) {

        LOGD("Couldn't open file:%s\n", file_name);
        return -1; // Couldn't open file
    }

    // Retrieve stream information
    if (avformat_find_stream_info(pFormatCtx, NULL) < 0) {
        LOGD("Couldn't find stream information.");
        return -1;
    }

    // Find the first video stream
    int videoStream = -1, i;
    for (i = 0; i < pFormatCtx->nb_streams; i++) {
        if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO
            && videoStream < 0) {
            videoStream = i;
        }
    }
    if (videoStream == -1) {
        LOGD("Didn't find a video stream.");
        return -1; // Didn't find a video stream
    }

    // Get a pointer to the codec context for the video stream
    AVCodecContext *pCodecCtx = pFormatCtx->streams[videoStream]->codec;

    // Find the decoder for the video stream
    AVCodec *pCodec = avcodec_find_decoder(pCodecCtx->codec_id);
    if (pCodec == NULL) {
        LOGD("Codec not found.");
        return -1; // Codec not found
    }

    if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) {
        LOGD("Could not open codec.");
        return -1; // Could not open codec
    }

    // 获取native window
    ANativeWindow *nativeWindow = ANativeWindow_fromSurface(env, surface);

    // 获取视频宽高
    int videoWidth = pCodecCtx->width;
    int videoHeight = pCodecCtx->height;

    // 设置native window的buffer大小,可自动拉伸
    ANativeWindow_setBuffersGeometry(nativeWindow, videoWidth, videoHeight,
                                     WINDOW_FORMAT_RGBA_8888);
    ANativeWindow_Buffer windowBuffer;

    if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) {
        LOGD("Could not open codec.");
        return -1; // Could not open codec
    }

    // Allocate video frame
    AVFrame *pFrame = av_frame_alloc();

    // 用于渲染
    AVFrame *pFrameRGBA = av_frame_alloc();
    if (pFrameRGBA == NULL || pFrame == NULL) {
        LOGD("Could not allocate video frame.");
        return -1;
    }

    // Determine required buffer size and allocate buffer
    // buffer中数据就是用于渲染的,且格式为RGBA
    int numBytes = av_image_get_buffer_size(AV_PIX_FMT_RGBA, pCodecCtx->width, pCodecCtx->height,
                                            1);
    uint8_t *buffer = (uint8_t *) av_malloc(numBytes * sizeof(uint8_t));
    av_image_fill_arrays(pFrameRGBA->data, pFrameRGBA->linesize, buffer, AV_PIX_FMT_RGBA,
                         pCodecCtx->width, pCodecCtx->height, 1);

    // 由于解码出来的帧格式不是RGBA的,在渲染之前需要进行格式转换
    struct SwsContext *sws_ctx = sws_getContext(pCodecCtx->width,
                                                pCodecCtx->height,
                                                pCodecCtx->pix_fmt,
                                                pCodecCtx->width,
                                                pCodecCtx->height,
                                                AV_PIX_FMT_RGBA,
                                                SWS_BILINEAR,
                                                NULL,
                                                NULL,
                                                NULL);

    int frameFinished;
    AVPacket packet;
    while (av_read_frame(pFormatCtx, &packet) >= 0) {
        // Is this a packet from the video stream?
        if (packet.stream_index == videoStream) {

            // Decode video frame
            avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished, &packet);

            // 并不是decode一次就可解码出一帧
            if (frameFinished) {

                // lock native window buffer
                ANativeWindow_lock(nativeWindow, &windowBuffer, 0);

                // 格式转换
                sws_scale(sws_ctx, (uint8_t const *const *) pFrame->data,
                          pFrame->linesize, 0, pCodecCtx->height,
                          pFrameRGBA->data, pFrameRGBA->linesize);

                // 获取stride
                uint8_t *dst = (uint8_t *) windowBuffer.bits;
                int dstStride = windowBuffer.stride * 4;
                uint8_t *src = (pFrameRGBA->data[0]);
                int srcStride = pFrameRGBA->linesize[0];

                // 由于window的stride和帧的stride不同,因此需要逐行复制
                int h;
                for (h = 0; h < videoHeight; h++) {
                    memcpy(dst + h * dstStride, src + h * srcStride, srcStride);
                }

                ANativeWindow_unlockAndPost(nativeWindow);
            }

        }
        av_packet_unref(&packet);
    }

    av_free(buffer);
    av_free(pFrameRGBA);

    // Free the YUV frame
    av_free(pFrame);

    // Close the codecs
    avcodec_close(pCodecCtx);

    // Close the video file
    avformat_close_input(&pFormatCtx);
    return 0;
}

这里是正常的ffmpeg编解码流程。参见雷神


Paste_Image.png

这里又有一个坑点,由于我使用的是c++ 编译器clang,而ffmpeg的so库均为c语言的,故需添加
extern "C"{

include "libavcodec/avcodec.h"

include "libavformat/avformat.h"

include "libswscale/swscale.h"

include "libavutil/imgutils.h"

};
如果这里没添加extern "C"{},即不是按C语言编译,就会存在undefined reference错误。下面是使用

VideoPlayer.java

package jonesx.videoplayer;

/**
 * Created by Jonesx on 2016/3/12.
 */
public class VideoPlayer {

    static {
        System.loadLibrary("VideoPlayer");
    }

    public static native int play(Object surface);
}

MainActivity.java

这里使用的SufaceView来进行播放。

public class MainActivity extends AppCompatActivity implements SurfaceHolder.Callback {

    SurfaceHolder surfaceHolder;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        SurfaceView surfaceView = (SurfaceView) findViewById(R.id.surface_view);
        surfaceHolder = surfaceView.getHolder();
        surfaceHolder.addCallback(this);
    }


    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                VideoPlayer.play(surfaceHolder.getSurface());
            }
        }).start();
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {

    }
}

灰常简单的xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:context=".MainActivity"
    tools:showIn="@layout/activity_main">

    <SurfaceView
        android:id="@+id/surface_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</RelativeLayout>

收工,看效果,非常简陋,没暂停什么滴。


test.png
作者:nothingl3 发表于2016/10/10 9:24:32 原文链接
阅读:25 评论:0 查看评论

Android中引用资源文件

$
0
0

值文件是Android应用必需的资源文件,其承担着应用的UI配置和文字显示工作。值文件包括字符串文件、字符串数组文件、配置文件、整数数组文件、维度文件、属性文件、颜色文件、风格文件、主题文件、标识符文件等,其中英文目录为res\values,简体中文目录为res\values-zh-rCN,繁体中文目录为res\values-zh-rTW,其命名规范遵循ISO639-2和ISO3166MA标准,其命名格式为:
values-<语言>-<方言>

1. 字符串

1.字符串资源的定义方法

res/values/strings.xml

字符串是最常用的值资源,其常用的设置方法如下:

<resources>
    <string name="edit_event_from_label">From</string>
    <string name="edit_event_all_day_label">All day</string>
</resources>

如果该字符串资源不必本地化,则需要设置其translatable属性为 false,具体如下:

<!-不需翻译-->
<string name="voice_mode_off" translatable="false">2</string>

除了name属性外,另一个需要注意的属性是msgid。msgid属性通常用于框架层,在应用层不需显式声明。

在Android中,特定的符号,如Nont中的“′”,无法直接在字符串资源中设置,开发者必须通过转义字符的方式进行显示。

在加载资源的过程中,会以资源的ID为线索进行加载。Android不允许在同一名字空间中出现相同的ID。

2.对字符串资源的引用

在Android中,资源分系统资源和应用资源两种,这两种资源在应用中引用的方式略有差异。系统资源并不全部对应用层开放,对应用层开放的系统资源在frameworks\base\core\res\res\values\目录下的public.xml中定义。不对应用层开放的系统资源的加载方式如下:
getString(com.android.internal.R.string. using);
//采用”@ com.android.internal.R./…”方式

除字符串资源以外的其他系统资源的加载方式和加载字符串资源类似,后面就不再详述其他类型的系统资源的加载了。

1.引用应用本身的字符串资源

1. 在XML配置文件中

<EditText android:id="@+id/text"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:textSize="18sp"
  //引用字符串,采用“@string/...”方式
  android:text="@string/hello_activity_text_text" />

2. 在Java文件中

//采用“R.string.***”方式
test.setText(R.string.hello_activity_text_text);

2.引用框架层的字符串资源

1.在XML配置文件中

<EditText android:id="@+id/text"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  //中等字体
  android:textAppearance="?android:attr/textAppearanceMedium"   
  //引用字符串,采用"@android:string/..."方式
  android:text="@android:string/cancel" />

2.在Java文件中

//采用“android .R.string.***”方式
test.setText(android.R.string.cancel);

3.数据交换

对于复杂的字符串数据,出于方便设计的考虑,Android支持XLIFF (XML Localization Interchange File Format)和通配符。

在一个字符串标签中Android仅支持一个通配符,其应用方法如下:
<string name="test">test "%1"</string>

在Java中加载通配符的方法如下:
test.setText(getString(R.string.hello).replace("%1",
String.valueOf(curPage)));

Android对XLIFF的支持的实现也非常简单。以ANR为例,其字符串资源如下:
<string name="anr_process">Process <xliff:g
id="process">%1$s</xliff:g> is not responding.</string>

XLIFF的名字空间为xmlns:xliff=”urn:oasis:names:tc:xliff:document:1.2”。

在Java中加载字符串的方法如下:
//采用的加载方法为getString (resId, formatArgs)
res.getString(com.android.internal.R.string.anr_process,
name1.toString(), name2.toString());

(3)下画线的实现

在Android中,文字特效目前仅支持加粗和斜体两种,如果希望实现加下画线的功能,则需要借助HTML语法,实现方法如下:
textView.setText(Html.fromHtml(““+helloStr+”“));

(4)语言切换的实现

语言的切换是通过资源管理器进行的,方法如下:
Resources resources = getResources();
Configuration config = resources.getConfiguration();
DisplayMetrics dm = resources .getDisplayMetrics();
config.locale = Locale.SIMPLIFIED_CHINESE;
resources.updateConfiguration(config, dm);

通过类似的设置,开发者也可以切换屏幕密度、MCC、MNC、屏幕方向等,当然部分设置可能需要高级别的权限。

2. 字符串数组

1.字符串数组的定义

字符串数组通常用于下拉框等场景。如果不希望该字符串数组随着语言环境的变化而变化,可以将字符串数组的translatable属性设为false,方法如下:

<string-array name="preferences_alert_type_values" translatable="false">
    <item>"0"</item>
    <item>"1"</item>
    <item>"2"</item>
</string-array>

2.字符串数组的加载

1.在XML配置文件中

    <ListPreference
       android:key="preferences_alerts_type"
       android:title="@string/preferences_alerts_type_title"
       //字符串数组,采用"@array/..."方式
       android:entries="@array/preferences_alert_type_labels"
       android:entryValues="@array/preferences_alert_type_values"/>

2.在Java中

ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(
   this, R.array.colors, android.R.layout.simple_spinner_item);
   //采用“R.array.***”方式

3. 整数数组

1.整数数组的定义

整数数组和字符串数组的使用方法类似,下面是整数数组的实现:

<integer-array name="config_virtualKeyVibePattern">
    <item>0</item>
    <item>10</item>
    <item>20</item>
    <item>30</item>
</integer-array>

2.整数数组的加载

1.在XML配置文件中

2.在Java中

int[]  tmpArray =
getResources().getIntArray(com.android.internal.R.array.config_virtualKeyVibePattern);

4. 配置文件

1.配置文件的定义

配置文件在应用开发中并不常用,通常用于应用的设置等场景,配置文件的实现如下:

<resources>
    <bool name="config_sf_limitedAlpha">false</bool>
    <integer name="config_shortAnimTime">150</integer>
</resources>

2.配置文件的加载

1.在XML配置文件中

2.在Java中

mLimitedAlphaCompositing = context.getResources().getBoolean(
       com.android.internal.R.bool.config_sf_limitedAlpha); //采用“R. bool.***”方式

5. 维度文件

维度文件的实现也非常简单,示例如下:

84dp
63dp

在Java中维度文件的加载方式如下:
Int w =
getResources().getDimensionPixelSize(com.android.internal.R.dimen.thumbnail_width);

6. 属性文件

1.属性文件的定义

res/values/attrs.xml

在Android中,属性文件通常由多个标签(如declare-styleable、eat-comment、attr)组成。

declare-styleable标签用于声明一个属性组,一个属性组可以由多个属性构成。在如下代码中,declare-styleable声明标签名为Animation的属性组。

<declare-styleable name="Animation">
  ...
</declare-styleable>

eat-comment标签用于声明标签上的内容为注释。

attr标签用于声明属性。一个属性包括属性名和属性格式两部分。示例:

<resources>      
     <attr name="buttonStyle" format="reference" />
     <attr name="buttonStyleSmall" format="reference" />
</resources>

目前,Android支持的常用属性格式包括:

属性格式 涵义
reference 对资源的引用
color 颜色
float
integer
fraction
boolean 布尔
dimension
string

需要注意的是,Android允许属性支持多种属性格式。下面是background属性的实现示例:

<attr name="background" format="reference|color" />

由上述示例可以看出,background属性既可加载配置文件,又可直接设置ARGB值。

为属性配置偏好值的方法如下:

<attr name="bufferType">
    <enum name="normal" value="0" />
    <enum name="spannable" value="1" />
    <enum name="editable" value="2" />
</attr>

偏好值的实现分为enum、flag两种类型,其中flag表示以二进制方式表示偏好值。

2.属性文件的加载

2.在JAVA中

TypedArray a = context.obtainStyledAttributes(attrs,
com.android.internal.R.styleable.Animation);
...
a.recycle(); //用于随后的复用

7. 颜色文件

颜色的设置文件通常会放在框架层的资源目录中。对颜色属性进行引用而不是在应用层显式声明,有利于在系统层面保持UI的一致性。在颜色的声明中,采用的是ARGB的格式。

1.颜色的定义

<resources>
    <color name="magic_flame">#00ffffff</color>
    <color name="button_text">#ffffffff</color>
</resources>

2.颜色的加载

1.在XML配置文件中

<TextView android:id="@+id/error"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:textColor="@color/red"
    android:textStyle="bold"
    android:visibility="gone"/>

2.在JAVA中

//采用“ R.color.***”方式
setBackgroundColor(getResources().getColor(R.color.candidate_background));

另外,在Android中,Google提供了多种系统颜色定义,如primary_text_dark、primary_text_light、white、black、transparent等。

8. 风格文件

一个好的应用除了技术上应设计得优雅外(即代码具有高的可读性、可扩展性、复用性、性能),用户体验也非常重要(包括文字的用语和观感、图片的观感和交互设计)。风格的设计决定了用户体验中文字和图片的效果。风格文件的设置方法如下:

 <?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="digit_button_style" parent="button_style">
       <item name="android:background">@drawable/blue_button</item>
    </style>
</resources>

2.风格文件的加载

1.在XML配置文件中

<com.android.calculator2.ColorButton
    android:id="@+id/digit7"
    android:text="@string/digit7"
    style="@style/digit_button_style" />

9. 主题文件

主题文件在框架层比较常用。对于进行差异化工作的厂商而言,保持同一个主题是一个基本的UI需求,定义统一的风格文件供公司所有产品引用,是个不错的选择,示例如下:

<style name="Theme.Light.NoTitleBar">
     <item name="android:windowNoTitle">true</item>
</style>

2.主题文件的加载

1.XML配置文件中

<activity android:name=".fuelgauge.PowerUsageDetail"
     android:theme="@android:style/Theme.NoTitleBar"
     android:label="@string/details_title">
</activity>

风格和主题的设计看似简单,实际上涉及的维度很多,既要考虑到用户的视觉感受,又要照顾不同层次、不同地区用户的文化差异。如何在考虑这些要素的基础上将需要的信息以一种统一、简洁、优雅的方式呈现出来,挑战很大,通常需要UI、UE工程师具备多年的积累和深厚的文化底蕴。

10. 标识符文件

标识符文件对于应用层开发者来说并不常见,但这并不意味着开发者完全接触不到,在使用系统提供的ListView、Tab等就会涉及。标识符文件的实现方式如下。

<resources>
  <item type="id" name="background" />
  <item type="id" name="checkbox" />
  <item type="id" name="content" />
</resources>

在实际的开发中,经常需要自定义Tab的布局,这时候必须声明tabhost、tabcontent、tabs等控件ID为系统ID,方法如下:

<TabHost
   xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@android:id/tabhost"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <TabWidget android:id="@android:id/tabs"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
        />
        <FrameLayout android:id="@android:id/tabcontent"
            android:layout_width="match_parent"
            android:layout_height="0dip"
            android:layout_weight="1"
        />
    </LinearLayout>
</TabHost>

对于自定义列表的布局,必须声明ListView的控件ID为系统ID,方法如下:

<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <ListView
        android:id="@android:id/list"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        />
 ...
</FrameLayout>
作者:donghuiyuan 发表于2016/10/10 9:32:01 原文链接
阅读:37 评论:0 查看评论

多窗口支持

$
0
0

Android N 添加了对同时显示多个应用窗口的支持。 在手持设备上,两个应用可以在“分屏”模式中左右并排或上下并排显示。 在电视设备上,应用可以使用“画中画”模式,在用户与另一个应用交互的同时继续播放视频。

如果您使用 N Preview SDK 构建应用,则可以配置应用处理多窗口显示的方法。 例如,您可以指定 Activity 的最小允许尺寸。 您还可以禁用应用的多窗口显示,确保系统仅以全屏模式显示应用。



概览


Android N 允许多个应用同时共享屏幕。例如,用户可以分屏显示应用,在左边查看网页,同时在右边写邮件。 用户体验取决于设备:

  • 运行 Android N 的手持设备具有分屏模式。 在此模式中,系统以左右并排或上下并排的方式分屏显示两个应用。 用户可以拖动两个应用之间的分界线,放大其中一个应用,同时缩小另一个。
  • 在运行 Android N 的 Nexus Player 上,应用能以画中画模式显示,即在用户浏览网页或与其他应用交互的同时继续显示内容。
  • 较大设备的制造商可选择启用自由形状模式,在该模式中,用户可以自由调整各 Activity 的尺寸。 若制造商启用此功能,设备将同时具有自由形状模式和分屏模式。

图 1. 两个应用在分屏模式中左右并排显示。

用户可以通过以下方式切换到多窗口模式:

  • 若用户打开 Overview 屏幕并长按 Activity 标题,则可以拖动该 Activity 至屏幕突出显示的区域,使 Activity 进入多窗口模式。
  • 若用户长按 Overview 按钮,设备上的当前 Activity 将进入多窗口模式,同时将打开 Overview 屏幕,用户可在该屏幕中选择要共享屏幕的另一个 Activity。

用户可以在两个 Activity 共享屏幕的同时在这两个 Activity 之间拖放数据 (在此之前,用户只能在一个 Activity 内部拖放数据)。



多窗口生命周期


多窗口模式不会更改 Activity 生命周期

在多窗口模式中,在指定时间只有最近与用户交互过的 Activity 为活动状态。 该 Activity 将被视为顶级 Activity。 所有其他 Activity 虽然可见,但均处于暂停状态。 但是,这些已暂停但可见的 Activity 在系统中享有比不可见 Activity 更高的优先级。 如果用户与其中一个暂停的 Activity 交互,该 Activity 将恢复,而之前的顶级 Activity 将暂停。

注:在多窗口模式中,用户仍可以看到处于暂停状态的应用。 应用在暂停状态下可能仍需要继续其操作。 例如,处于暂停模式但可见的视频播放应用应继续显示视频。 因此,我们建议播放视频的 Activity 不要暂停其 onPause() 处理程序中的视频。 应暂停 onStop() 中的视频,并恢复 onStart()中的视频播放。

处理运行时变更中所述,用户使用多窗口模式显示应用时,系统将通知 Activity 发生配置变更。 这也会发生在当用户调整应用大小,或将应用恢复到全屏模式时。 该变更与系统通知应用设备从纵向模式切换到横向模式时的 Activity 生命周期影响基本相同,但设备不仅仅是交换尺寸,而是会变更尺寸。 如处理运行时变更中所述,您的 Activity 可以自行处理配置变更,或允许系统销毁 Activity,并以新的尺寸重新创建该 Activity。

如果用户调整窗口大小,并在任意维度放大窗口尺寸,系统将调整 Activity 以匹配用户操作,同时根据需要发布运行时变更。 如果应用在新公开区域的绘制滞后,系统将使用 windowBackground 属性或默认 windowBackgroundFallback 样式属性指定的颜色暂时填充该区域。



针对多窗口模式配置应用


如果您的应用面向 Android N,您可以对应用的 Activity 是否支持多窗口显示以及显示方式进行配置。 您可以在清单文件中设置属性,以控制大小和布局。 根 Activity 的属性设置适用于其任务栈中的所有 Activity。 例如,如果根 Activity 已 android:resizeableActivity 设定为 true,则任务栈中的所有 Activity 都将可以调整大小。

注:如果您使用低于 Android N 版本的 SDK 构建多向应用,则用户在多窗口模式中使用应用时,系统将强制调整应用大小。 系统将显示对话框,提醒用户应用可能会发生异常。 系统不会调整定向应用的大小;如果用户尝试在多窗口模式下打开定向应用,应用将全屏显示。

android:resizeableActivity

在清单的 <activity> 或 <application> 节点中设置该属性,启用或禁用多窗口显示:

android:resizeableActivity=["true" | "false"]

如果该属性设置为 true,Activity 将能以分屏和自由形状模式启动。 如果此属性设置为 false,Activity 将不支持多窗口模式。 如果该值为 false,且用户尝试在多窗口模式下启动 Activity,该 Activity 将全屏显示。

如果您的应用面向 Android N,但未对该属性指定值,则该属性的值默认设为 true。

android:supportsPictureInPicture

在清单文件的 <activity> 节点中设置该属性,指明 Activity 是否支持画中画显示。 如果 android:resizeableActivity 为 false,将忽略该属性。

android:supportsPictureInPicture=["true" | "false"]

布局属性

对于 Android N,<layout> 清单元素支持以下几种属性,这些属性影响 Activity 在多窗口模式中的行为:

android:defaultWidth
以自由形状模式启动时 Activity 的默认宽度。
android:defaultHeight
以自由形状模式启动时 Activity 的默认高度。
android:gravity
以自由形状模式启动时 Activity 的初始位置。
android:minimalHeightandroid:minimalWidth
分屏和自由形状模式中 Activity 的最小高度和最小宽度。 如果用户在分屏模式中移动分界线,使 Activity 尺寸低于指定的最小值,系统会将 Activity 裁剪为用户请求的尺寸。

例如,以下节点显示了如何指定 Activity 在自由形状模式中显示时 Activity 的默认大小、位置和最小尺寸:

<activity android:name=".MyActivity">
    <layout android:defaultHeight="500dp"
          android:defaultWidth="600dp"
          android:gravity="top|end"
          android:minimalHeight="450dp"
          android:minimalWidth="300dp" />
</activity>



在多窗口模式中运行应用


Android N 添加了新功能,以支持可在多窗口模式中运行的应用。

多窗口模式中被禁用的功能

在设备处于多窗口模式中时,某些功能会被禁用或忽略,因为这些功能对与其他 Activity 或应用共享设备屏幕的 Activity 而言没有意义。 此类功能包括:

  • 某些系统 UI 自定义选项将被禁用;例如,在非全屏模式中,应用无法隐藏状态栏。
  • 系统将忽略对 android:screenOrientation 属性所作的更改。

多窗口变更通知和查询

Activity 类中添加了以下新方法,以支持多窗口显示。 有关各方法的详细信息,请参阅 N Preview SDK 参考

Activity.isInMultiWindowMode()
调用该方法以确认 Activity 是否处于多窗口模式。
Activity.isInPictureInPictureMode()
调用该方法以确认 Activity 是否处于画中画模式。

注:画中画模式是多窗口模式的特例。 如果 myActivity.isInPictureInPictureMode() 返回 true,则myActivity.isInMultiWindowMode() 也返回 true。

Activity.onMultiWindowModeChanged()
Activity 进入或退出多窗口模式时系统将调用此方法。 在 Activity 进入多窗口模式时,系统向该方法传递 true 值,在退出多窗口模式时,则传递 false 值。
Activity.onPictureInPictureModeChanged()
Activity 进入或退出画中画模式时系统将调用此方法。 在 Activity 进入画中画模式时,系统向该方法传递 true 值,在退出画中画模式时,则传递 false 值。

每个方法还有 Fragment 版本,例如 Fragment.isInMultiWindowMode()

进入画中画模式

如需在画中画模式中启动 Activity,请调用新方法 Activity.enterPictureInPictureMode()。 如果设备不支持画中画模式,则此方法无效。 

在多窗口模式中启动新 Activity

在启动新 Activity 时,用户可以提示系统如果可能,应将新 Activity 显示在当前 Activity 旁边。 要执行此操作,可使用标志Intent.FLAG_ACTIVITY_LAUNCH_TO_ADJACENT。 传递此标志将请求以下行为:

  • 如果设备处于分屏模式,系统会尝试在启动系统的 Activity 旁创建新 Activity,这样两个 Activity 将共享屏幕。 系统并不一定能实现此操作,但如果可以,系统将使两个 Activity 处于相邻的位置。
  • 如果设备不处于分屏模式,则该标志无效。

如果设备处于自由形状模式,则在启动新 Activity 时,用户可通过调用 ActivityOptions.setLaunchBounds() 指定新 Activity 的尺寸和屏幕位置。 如果设备不处于多窗口模式,则该方法无效。

注:如果您在任务栈中启动 Activity,该 Activity 将替换屏幕上的 Activity,并继承其所有的多窗口属性。 如果要在多窗口模式中以单独的窗口启动新 Activity,则必须在新的任务栈中启动此 Activity。

支持拖放

用户可以在两个 Activity 共享屏幕的同时在这两个 Activity 之间拖放数据 (在此之前,用户只能在一个 Activity 内部拖放数据)。 因此,如果您的应用目前不支持拖放功能,您可以在其中添加此功能。

N Preview SDK 扩展了 android.view 软件包,以支持跨应用拖放。

android.view.DropPermissions
令牌对象,负责指定对接收拖放数据的应用授予的权限。
View.startDragAndDrop()
View.startDrag() 的新别名。要启用跨 Activity 拖放,请传递新标志 View.DRAG_FLAG_GLOBAL。 如需对接收拖放数据的 Activity 授予 URI 权限,可根据情况传递新标志 View.DRAG_FLAG_GLOBAL_URI_READ 或 View.DRAG_FLAG_GLOBAL_URI_WRITE
View.cancelDragAndDrop()
取消当前正在进行的拖动操作。只能由发起拖动操作的应用调用。
View.updateDragShadow()
替换当前正在进行的拖动操作的拖动阴影。只能由发起拖动操作的应用调用。
Activity.requestDropPermissions()
请求使用 DragEvent 中包含的 ClipData 传递的内容 URI 的权限。



测试应用的多窗口支持


无论您是否针对 Android N 更新应用,都应验证应用在多窗口模式下的行为,以防用户尝试在运行 Android N 的设备上以多窗口模式启动应用。

配置测试设备

如果在设备上安装 Android N,则将自动支持分屏模式。

如果应用并非使用 N Preview SDK 构建

如果您的应用不是使用 N Preview SDK 构建的,则用户尝试在多窗口模式中使用应用时,系统将强制调整应用大小,除非应用进行了定向声明。

如果您的应用没有进行定向声明,则应在运行 Android N 的设备上启动应用,并尝试将应用切换到分屏模式。 验证并确保在强制调整应用大小时用户体验可接受。

如果应用进行了定向声明,则应尝试将应用切换到多窗口模式。 验证并确保执行此操作后,应用仍保持全屏模式。

如果支持多窗口模式

如果您的应用是使用 N Preview SDK 构建的,且未禁用多窗口支持,则分别在分屏和自由形状模式下验证以下行为。

  • 在全屏模式下启动应用,然后通过长按 Overview 按钮切换到多窗口模式。 验证并确保应用正常切换。
  • 直接在多窗口模式中启动应用,验证并确保应用正常启动。 您可以按一下 Overview 按钮,再长按应用的标题栏,并将其拖动到屏幕上任一突出显示的区域,从而在多窗口模式中启动应用。
  • 拖动分界线,在分屏模式中调整应用的大小。 验证并确保应用正常调整大小且未崩溃,并且必要的 UI 元素仍可见。
  • 如果您指定了应用的最小尺寸,请尝试将应用尺寸调整到低于最小值。 验证并确保无法将应用尺寸调整到低于指定最小值。
  • 完成所有测试后,验证并确保应用性能可以接受。例如,验证并确保调整应用大小后更新 UI 没有长时间的滞后。

测试检查单

要在多窗口模式中验证应用性能,请执行以下操作。 除非另有说明,否则请分别在分屏和多窗口模式中执行以下操作。

  • 进入和退出多窗口模式。
  • 从您的应用切换到另一个应用,验证并确保应用在非活动但可见的状态下正常运行。 例如,如果您的应用在播放视频,则验证并确保在用户与另一个应用交互时视频仍在继续播放。
  • 在分屏模式中,尝试移动分界线,放大或缩小应用。 分别在左右和上下并排显示模式中尝试这些操作。 验证并确保应用不会崩溃,主要功能可见,且调整操作不需要过长时间。
  • 快速连续执行几次调整操作。验证并确保应用不会崩溃或出现内存泄漏。
  • 在多个不同窗口配置中正常使用应用,验证并确保应用正常运行。 验证并确保文本可读,且 UI 元素大小正常,不影响交互。

如果已禁用多窗口支持

如果您通过设置 android:resizableActivity="false" 禁用了多窗口支持,则应在运行 Android N 的设备上启动应用,并尝试将应用切换到自由形状和分屏模式。 验证并确保执行此操作后,应用仍保持全屏模式。

作者:yangxu4536 发表于2016/10/10 9:40:43 原文链接
阅读:42 评论:0 查看评论

下载安装APK(兼容Android7.0)

$
0
0

我们使用手机的时候经常会看到应用程序提示升级,大部分应用内部都需要实现升级提醒和应用程序文件(APK文件)下载。

一般写法都差不多,比如在启动app的时候,通过api接口获得服务器最新的版本号,然后和本地的版本号比较,来判断是否需要弹出提示框下载,当然也可以通过推送的自定义消息来实现。

我们这里主要讨论的是应用程序下载,并在通知栏提醒下载完成。
实现过程大致分为三步:

  1. 创建一个service
  2. 在service启动的时候创建一个广播接受者,用于接受下载完成的广播
  3. 当BroadcastReceiver接受到下载完成的广播时,开始执行安装。

主要通过系统提供的DownloadManager进行下载,DownloadManager下载完成会发送广播,具体使用看下面完整的代码。如果详细了解可以参考Android系统下载管理DownloadManager功能介绍及使用示例下面创建新的文件DownloadService.java

public class DownLoadService extends Service {
    /**广播接受者*/
    private BroadcastReceiver receiver;
    /**系统下载管理器*/
    private DownloadManager dm;
    /**系统下载器分配的唯一下载任务id,可以通过这个id查询或者处理下载任务*/
    private long enqueue;
    /**TODO下载地址 需要自己修改,这里随便找了一个*/
    private String downloadUrl="http://dakaapp.troila.com/download/daka.apk?v=3.0";

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {

        receiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                install(context);
                //销毁当前的Service
                stopSelf();
            }
        };
        registerReceiver(receiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
        //下载需要写SD卡权限, targetSdkVersion>=23 需要动态申请权限
        RxPermissions.getInstance(this)
                // 申请权限
                .request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
                .subscribe(new Action1<Boolean>() {
                    @Override
                    public void call(Boolean granted) {
                        if(granted){
                            //请求成功
                            startDownload(downloadUrl);
                        }else{
                            // 请求失败回收当前服务
                            stopSelf();

                        }
                    }
                });
        return Service.START_STICKY;
    }

    /**
     * 通过隐式意图调用系统安装程序安装APK
     */
    public static void install(Context context) {
        Intent intent = new Intent(Intent.ACTION_VIEW);
        // 由于没有在Activity环境下启动Activity,设置下面的标签
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        intent.setDataAndType(Uri.fromFile(
                new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "myApp.apk")),
                "application/vnd.android.package-archive");
        context.startActivity(intent);
    }

    @Override
    public void onDestroy() {
        //服务销毁的时候 反注册广播
        unregisterReceiver(receiver);
        super.onDestroy();
    }

    private void startDownload(String downUrl) {
        //获得系统下载器
        dm = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
        //设置下载地址
        DownloadManager.Request request = new DownloadManager.Request(Uri.parse(downUrl));
        //设置下载文件的类型
        request.setMimeType("application/vnd.android.package-archive");
       //设置下载存放的文件夹和文件名字
 request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "myApp.apk");
        //设置下载时或者下载完成时,通知栏是否显示
 request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
        request.setTitle("下载新版本");
        //执行下载,并返回任务唯一id
        enqueue = dm.enqueue(request);
    }
}

上面代码使用了RxPermissions第三方库动态申请权限,需要在app/build.gradle文件中进行配置

dependencies {
    //...
    compile 'com.tbruyelle.rxpermissions:rxpermissions:0.7.0@aar'
    compile 'io.reactivex:rxjava:1.1.6' //需要引入RxJava
}

记得要配置服务

<application
  ...>
    ...
    <service android:name=".DownLoadService"/>
</application>

最后在MainActivity中添加按钮,执行操作。运行结果:

当下载的时候,会有通知栏进度条提示。下载完成会提示安装。不过当前程序如果在Android7.0上就会报错。下面是报错的日志:

Caused by: android.os.FileUriExposedException: 
file:///storage/emulated/0/Download/myApp.apk exposed beyond app through Intent.getData()

这是由于Android7.0执行了“StrictMode API 政策禁”的原因,不过小伙伴们不用担心,可以用FileProvider来解决这一问题,

现在我们就来一步一步的解决这个问题。

Android 7.0错误原因

随着Android版本越来越高,Android对隐私的保护力度也越来越大。

比如:Android6.0引入的动态权限控制(Runtime Permissions),Android7.0又引入“私有目录被限制访问”,“StrictMode API 政策”。

这些更改在为用户带来更加安全的操作系统的同时也为开发者带来了一些新的任务。如何让你的APP能够适应这些改变而不是crash,是摆在每一位Android开发者身上的责任。

“私有目录被限制访问“ 是指在Android7.0中为了提高私有文件的安全性,面向 Android N 或更高版本的应用私有目录将被限制访问。这点类似iOS的沙盒机制。

” StrictMode API 政策” 是指禁止向你的应用外公开 file:// URI。 如果一项包含文件 file:// URI类型 的 Intent 离开你的应用,应用失败,并出现 FileUriExposedException 异常。

上面用到的代码中的Uri.fromFile 其实就是生成一个file://URL。

//...
intent.setDataAndType(Uri.fromFile(
                new File(Environment.getExternalStoragePublicDirectory(
                  Environment.DIRECTORY_DOWNLOADS), 
                         "myApp.apk")),
                "application/vnd.android.package-archive");

//....

一旦我们通过这种办法打开其它程序(这里打开系统包安装器)就认为file:// URI类型的 Intent 离开你的应用。这样程序就会发生异常。

接下来就用FileProvider来解决这一问题。

使用FileProvider

使用FileProvider的大致步骤如下:

第一步:
在AndroidManifest.xml清单文件中注册provider,因为provider也是Android四大组件之一,可以简单把它理解为向外提供数据的组件,这种组件在实际开发中用的频率并不高,四大组件都可以在清单文件中进行配置。

<application
   ...>
    <provider
        android:name="android.support.v4.content.FileProvider"
        android:authorities="com.yll520wcf.test.fileprovider"
        android:grantUriPermissions="true"
        android:exported="false">
        <!--元数据-->
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/file_paths" />
    </provider>
</application>

注意:

  • exported:要求必须为false,为true则会报安全异常。
  • grantUriPermissions:true,表示授予 URI 临时访问权
    限。
  • authorities 组件标识,按照江湖规矩,都以包名开头,避免和其它应用发生冲突。

第二步:指定共享的目录
上面配置文件中 android:resource="@xml/file_paths" 指的是当前组件引用 res/xml/file_paths.xml 这个文件。

我们需要在资源(res)目录下创建一个xml目录,然后创建一个名为“file_paths”(名字可以随便起,只要和在manifest注册的provider所引用的resource保持一致即可)的资源文件,内容如下:

  • 代表的根目录: Context.getFilesDir()
  • 代表的根目录: Environment.getExternalStorageDirectory()
  • 代表的根目录: getCacheDir()

上述代码中path=”“,是有特殊意义的,它代码根目录,也就是说你可以向其它的应用共享根目录及其子目录下任何一个文件了。

如果你将path设为path="pictures",那么它代表着根目录下的pictures目录(eg:/storage/emulated/0/pictures),如果你向其它应用分享pictures目录范围之外的文件是不行的。

第三步:使用FileProvider
上述准备工作做完之后,现在我们就可以使用FileProvider了。
我们需要将上述安装APK代码修改为如下

public static void install(Context context) {
    File file= new File(
            Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
            , "myApp.apk");
    //参数1 上下文, 参数2 Provider主机地址 和配置文件中保持一致   参数3  共享的文件
    Uri apkUri =
            FileProvider.getUriForFile(context, "com.com.yll520wcf.test.fileprovider", file);

    Intent intent = new Intent(Intent.ACTION_VIEW);
    // 由于没有在Activity环境下启动Activity,设置下面的标签
    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    //添加这一句表示对目标应用临时授权该Uri所代表的文件
    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
    intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
    context.startActivity(intent);
}

上述代码中主要有两处改变:
1. 将之前Uri改成了有FileProvider创建一个content类型的Uri。
2. 添加了intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);来对目标应用临时授权该Uri所代表的文件。

上述代码通过FileProviderUri getUriForFile (Context context, String authority, File file)静态方法来获取Uri
该方法中authority参数就是清单文件中注册provider时填写的authority
android:authorities="com.yll520wcf.test.fileprovider"。
按照上面步骤修改就可以兼容Android7.0了。

参考文献

android应用开发app手动更新通知栏下载实践
Android7.0适配教程,心得

更多精彩请关注微信公众账号likeDev
这里写图片描述

作者:yulianlin 发表于2016/10/10 10:17:39 原文链接
阅读:77 评论:0 查看评论

安卓5.0 6.0新特性简单总结

$
0
0

Android 5.0新特性

1. 了解Material Design

2. 动态替换Theme

  • MaterialTheme配色方案:http://www.materialpalette.com
  • 修改状态栏,ActionBar,界面背景,NavigationBar的颜色。让Activity使用自定义的Theme。
    <style name="AppTheme" parent="@android:style/Theme.Material">
        <!--状态栏颜色-->
        <item name="android:colorPrimaryDark">#f00</item>
        <!--ActionBar颜色-->
        <item name="android:colorPrimary">#ff0</item>
        <!--界面背景颜色-->
        <item name="android:windowBackground">@color/colorWindowBackground</item>
        <!--导航栏颜色-->
        <item name="android:navigationBarColor">#00f</item>
    </style>
    
  • 动态替换Theme的步骤:
    • 定义至少2套theme
    • 调用setTheme方法设置当前的theme,但是该方法要在setContentView之前,如:
      setTheme(mTheme);
      setContentView(R.layout.activity_main);
      
    • 设置了Theme,需要finish当前Activity,然后重启当前Activity,让Theme生效
      Intent intent = getActivity().getIntent();
      getActivity().finish();//结束当前的Activity
      getActivity().overridePendingTransition(0,0);//不要动画
      startActivity(intent);

3. View的高度与阴影

  • 官网介绍:https://developer.android.com/intl/zh-tw/training/material/shadows-clipping.html
  • View新增属性z轴,用来体现Material Design中的层次,影响因素2个:elevation 和 translationZ
    View高度 = elevation + translationZ
    • elevation表示view的高度,高度越大,阴影越大,可以在xml中直接使用属性, 也可以在代码中使用view.setEvelvation();
      android:elevation="10dp"
    • transtionZ属性表示view在Z方向移动的距离,一般用于属性动画中
      android:translationZ="10dp"
  • 高度影响View的绘制顺序,过去是按View添加顺序绘制,先添加的先绘制,现在高度小的先绘制,因为高度小的,层级低,在下面, 高度相同的,按添加顺序绘制
  • 注意:
    • 如果View的背景色为透明,则不会显示出阴影效果
    • 只有子View的大小比父View小时,阴影才能显示出来

4. View的轮廓与裁剪(在Android5.1以及以上才有效果)

  • 官网介绍:https://developer.android.com/intl/zh-tw/training/material/shadows-clipping.html
  • View增加了轮廓概念,轮廓用来表示怎么显示阴影,也就是说轮廓什么形状,阴影就显示什么形状。

    • View的轮廓可以通过outlineProvider属性设置,默认是依据于background的,还有其他3个取值:bounds,none,paddingBounds

      android:outlineProvider="bounds"
      none:即使设置了evaluation也不显示阴影
      background:按背景来显示轮廓,如果background是颜色值,则轮廓就是view的大小,如果是shape,则按shape指定的形状作为轮廓
      bounds: View的矩形大小作轮廓
      paddedBounds: View的矩形大小减去padding的值后的大小作轮廓。
      
    • 可以通过setOutlineProvider()方法自定义轮廓:

      tv_blue.setOutlineProvider(new ViewOutlineProvider() {
              @TargetApi(Build.VERSION_CODES.LOLLIPOP)
              @Override
              public void getOutline(View view, Outline outline) {
                  outline.setOval(0,0,
                          view.getWidth(),view.getHeight());
              }
          });
    • 注意:如果background是图片,那只能通过代码setOutlineProvider()来指定轮廓
  • View的裁剪是指将View按照轮廓裁剪,能改变View的形状,如圆形头像:
    • 先设置轮廓:
    • 再设置根据轮廓裁剪View,目前只支持对矩形,圆形,圆角矩形的裁剪:
      //设置对View进行裁剪
      tv_clip.setClipToOutline(true);

5. Palette的使用

  • 使用Palette可以让我们从一张图片中拾取颜色,将拾取到的颜色赋予ActionBar,StatusBar以及背景色可以让界面色调实现统一
  • 使用Palette需要添加以下依赖:
    compile 'com.android.support:palette-v7:23.0.0+'
  • Palette提供的API

    • 传入Bitmap即可获取Palette对象,以下是同步和异步使用方式:
      //同步获取,需要在子线程中使用
      Palette palette = Palette.from(drawable.getBitmap()).generate();
      //异步获取,可以在主线程中使用
      Palette.from(drawable.getBitmap()).generate(new Palette.PaletteAsyncListener() {
          @Override
          public void onGenerated(Palette palette) {
              //...
          }
      });
    • 得到Palette对象后,获取其中的颜色,颜色对应如下:
      vibrant      -  有活力的颜色
      lightVibrant -  有活力的亮色
      darkVibrant  -  有活力的暗色
      muted        -  柔和暗淡的颜色
      lightMuted   -  柔和的亮色
      darkMuted    -  柔和的暗色
    • 获取指定颜色的采样对象,获取采样得到的颜色:

      //我们可以直接使用palette获取指定颜色:
      palette.getLightMutedColor(defaultColor);
      
      //一般也可以先获取采样对象Swatch,从Swatch中获取我们需要的颜色:
      //获取有活力颜色的采样对象
      Palette.Swatch vibrantSwatch = palette.getVibrantSwatch();
    • 采样对象Swatch提供了以下方法来获取颜色:
      //swatch.getPopulation(): the amount of pixels which this swatch represents.
      //swatch.getRgb(): the RGB value of this color.
      //swatch.getHsl(): the HSL value of this color,即色相,饱和度,明度.
      //swatch.getBodyTextColor(): the RGB value of a text color which can be displayed on top of this color.
      //swatch.getTitleTextColor(): the RGB value of a text color which can be displayed on top of this color
      //一般会将getRgb设置给控件背景色,getBodyTextColor()设置给文字颜色
      textView.setBackgroundColor(vibrantSwatch.getRgb());
      textView.setTextColor(vibrantSwatch.getBodyTextColor());

6. 水波纹动画,自定义水波纹动画以及状态选择器动画

  • 首先,在Android5.0以上,点击效果默认自带水波纹效果,并且有2种选择:
    //矩形边框水波纹
    android:background="?android:attr/selectableItemBackground"
    //无边框限制水波纹
    android:background="?android:attr/selectableItemBackgroundBorderless"
  • 自定义水波纹动画

    • 使用ViewAnimationUtils创建圆形水波纹动画,注意该动画不能在Activity的onCreate方法中执行:
      Animator circularReveal = ViewAnimationUtils.createCircularReveal(text, 0, text.getHeight() , 1f, text.getWidth()*2);
      circularReveal.setDuration(1000);
      circularReveal.start();
    • 使用ripple标签或者RippleDrawable可以更改控件水波纹动画颜色:
      <ripple xmlns:android="http://schemas.android.com/apk/res/android" android:color="#00ff00">
      <item android:id="@android:id/mask" ><color android:color="#0000ff" />
      

  • 定义带有属性动画的状态选择器

    • 通过stateListAnimator属性指定状态选择器的动画:
      android:stateListAnimator="@drawable/selector_anim"
    • 状态选择器文件中需要加入objectAnimator标签:
      <selector  xmlns:android = "http://schemas.android.com/apk/res/android" >
      <item  android:statepressed = "true" >
      <objectAnimator  android:propertyName = "scaleX"
              android:duration = "@android:integer/configshortAnimTime"
              android:valueTo = "0.2"
              android:valueFrom = "1"
              android:valueType = "floatType" >
      	//...
      
    • 同样,状态选择器动画可以用代码方式加载
      //加载动画
      AnimatorInflater.loadStateListAnimator();
      //设置动画
      View.setStateListAnimator();
      
  • 定义带有帧动画的状态选择器,需要设置给background属性,不是stateListAnimator,如下所示:
    <animated-selector  xmlns:android = "http://schemas.android.com/apk/res/android" > 
    <item  android:id = "@+id/pressed"  android:drawable = "@drawable/drawableP" 
        android:state_pressed = "true" /> 
    <item  android:id = "@id/default" 
        android:drawable = "@drawable/drawableD" /> 
    <!-- 指定帧动画 - -> 
    <transition  android:fromId = "@+id/default"  android:toId = "@+id/pressed" > 
        <animation-list> 
            <item  android:duration = "15"  android:drawable = "@drawable/dt1 " /> 
            <item  android:duration = "15"  android:drawable = "@drawable/dt2" /> 
            ... 
        </animation-list> 
     </animated-selector>

7. CardView的使用

  • CardLayout拥有高度和阴影,以及轮廓裁剪,圆角等功能
  • 各属性说明:
    1.设置圆角:card_view:cardCornerRadius="10dp"
    2.设置高度:card_view:cardElevation="10dp"
    3.设置内边距:card_view:contentPadding="10dp"
    4.设置背景色:card_view:cardBackgroundColor="?android:attr/colorPrimary"

8. RecyclerView的使用

  • https://developer.android.com/intl/zh-tw/training/material/lists-cards.html
  • 先添加依赖 compile 'com.android.support:recyclerview-v7:23.1.1'
  • 设置LayoutManager:控制RecyclerView如何显示布局,系统提供3个布局管理器:
    • LinearLayoutManager:线性布局,有横向和竖直方向显示
    • GridLayoutManager:网格布局,有横向和竖直方向显示
    • StaggeredGridLayoutManager: 瀑布流布局,有横向和竖直方向显示
  • 然后给RecyclerView设置Adapter<RecyclerView.ViewHolder>
  • 设置点击事件,由于RecyclerView没有setOnItemClickListener,只能在Adapter中给View设置Click事件

9. ToolBar的使用

  • 它用来代替ActionBar,但是比ActionBar更加灵活,相当于可以写在布局文件中的ActionBar;与DrawerLayout的使用的时候,DrawerLayout可以覆盖在ToolBar上,并且ToolBar和ActionBar不能同时使用
  • 使用ToolBar的步骤:
    • 先隐藏ActionBar,可以继承一个不带ActionBar的Theme,如:
      style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar"
    • 然后在Activity中设置ToolBar替代ActionBar:
      setSupportActionBar(toolBar);
    • 最后设置ToolBar的显示内容:
      toolBar.setTitle("ToolBar");//设置标题
      toolBar.setNavigationIcon(iconRes);//设置图标
      toolBar.setOnMenuItemClickListener();//设置Menu Item点击

10. Android 5.0新特性的向下兼容

  • 可以通过Support Library使用的新特性可以向下兼容,如:
    • RecyclerView (recyclerview-v7)
    • CardView (cardview-v7)
    • Palette颜色识别 (palette-v7)
    • ToolBar (appcompat-v7)
    • SwipeRefreshLayout (v4)
  • 定义针对版本的资源目录
    • layout:使用新API的布局放在res/layout-v21中,其他的放res/layout
    • drawable:使用新属性的drawable文件放到 res/drawable-v21,其他的放 res/drawable
    • values: 新的主题放到 res/values-v21, 其他的放 res/values
  • 在代码中对系统Version做判断,使用对应的效果,如:
    if(Build.VERSION.SDK_INT> 21){
            //使用新动画
            ...
        }

Android 6.0新控件

1. TextInputLayout的使用

  • 高级炫酷带有提示的输入框,相当于输入框中的战斗框
  • 使用需要依赖design类库:
    compile 'com.android.support:design:23.0.0+'
  • 使用步骤:

    • 先在TextInputLayout中包裹一个EditText,如:
    • 然后给EditText添加文本变化监听器:

       editText.addTextChangedListener(new TextWatcher() {
      @Override
      public void beforeTextChanged(CharSequence s, int start, int count, int after) {
      
      }
      
      @Override
      public void onTextChanged(CharSequence s, int start, int before, int count) {
      
      }
      
      @Override
      public void afterTextChanged(Editable s) {
          if(s.length() > 5){
              //设置错误提示信息
              inputLayout.setError("不能超过5个");
              //启用错误提示
              inputLayout.setErrorEnabled(true);
          }else{
              //关闭错误提示
              inputLayout.setErrorEnabled(false);
          }
      }
      });

2. FloatingActionButton的使用

  • 总是能悬浮在界面上的Button,可以设置点击事件
  • 使用需要依赖design类库:
    compile 'com.android.support:design:23.0.0+'
  • 可以设置以下属性:
    app:fabSize="normal"//2个取值,normal=56dp,mini=48dp
    app:elevation="10dp"//高度
    app:rippleColor="#0000ff"//按下水波纹颜色
    app:pressedTranslationZ="20dp"//按下Z轴移动距离  
    注意:设置android:clickable="true"才有按下的效果
    

3. Snackbar的使用

  • 一个介于Toast和Dialog之间的和用户交互的控件,显示在界面底部,并且可以设置点击行为,也可以滑动出去
  • 在和FloatingActionButton一起使用时,需要让CoordinatorLayout作为父布局才能使FloatingActionButton给SnackBar腾出空间

4. TabLayout的使用

  • 相当于ViewPagerIndicator的指示器效果,一般用来跟ViewPager结合使用
  • 使用需要依赖design类库:
    compile 'com.android.support:design:23.0.0+'
  • 单独使用TabLayout的步骤:
    • 先添加Tab,使用tabLayout.newTab()方法创建Tab:
      //1.添加Tab
      tabLayout.addTab(tabLayout.newTab().setText("Tab1"));
    • 给tabLayout设置tab改变的监听器:
       //2.给tabLayout添加tab改变的监听器
      tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
          @Override
          public void onTabSelected(TabLayout.Tab tab) {
              Log.e("TAG", "onTabSelected: "+tab.getText());
          }
          @Override
          public void onTabUnselected(TabLayout.Tab tab) {
          }
          @Override
          public void onTabReselected(TabLayout.Tab tab) {
          }
      });
    • 在xml中给TabLayout设置属性:
      app:tabIndicatorColor="#00f"//横线颜色
      app:tabSelectedTextColor="#f00"//选中字体颜色
      app:tabTextColor="#0f0"//默认字体颜色
      app:tabMode="fixed"//fixed:不能滑动,每个Tab平分宽度,scrollable:可以滑动tab,每个tab宽度很小,适用于tab很多的情景
      app:tabGravity="fill"//fill:平分宽度,center:让tab居中,如果tabMode是scrollable,那tabGravity会被忽略
      
  • 和ViewPager关联使用步骤:
    • 先给ViewPager填充数据,然后关联TabLayout和ViewPager:
      //给ViewPager填充数据
      viewpager.setAdapter(new MyAdapter());
      //关联TabLayout和ViewPager
      tabLayout.setupWithViewPager(viewpager);
    • 需要注意的是,ViewPager的adapter的getPageTitle()方法的返回值将会设置给Tab的标题

5. CoordinatorLayout的使用

  • 协调布局,能够协调多个布局的位置关系,可以实现让FAB上下滑动,展开或折叠ToolBar,控制View扩展收缩以及放大缩小
  • 使用需要依赖design类库:
    compile 'com.android.support:design:23.0.0+'
  • 使用它结合AppBarLayout实现向上滑动隐藏ToolBar的效果:
    • AppBarLayout会将包裹的所有子View作为一个整体的AppBar,有着统一的界面着色;
    • app:layout_scrollFlags属性介绍:
      scroll:表示该View可以被滑动出CoordinatorLayout的范围,所有想滚动出屏幕的view都需要设置这个flag, 没有设置这个flag的view将被固定在屏幕顶部。例如,TabLayout 没有设置这个值,将会停留在屏幕顶部
      enterAlways:表示任意向下的滚动都会导致该View可见
      exitUntilCollapsed:滚动退出屏幕,最后折叠在顶端
      enterAlwaysCollapsed:当你的视图已经设置minHeight属性又使用此标志时,你的视图只能以最小高度进入,只有当滚动视图到达顶部时才扩大到完整高度
      
    • 给想滑动出范围的View设置属性,比如ToolBar:
      app:layout_scrollFlags=”scroll|enterAlways
    • 给发出滑动行为的View设置属性,比如ViewPager:
      app:layout_behavior="@string/appbar_scrolling_view_behavior"
    • 实现步骤:
      1.CoordinatorLayout作根控件,包裹AppBarLayout和可滚动的控件,比如ViewPager  
      2.AppBarLayout包裹 ToolBar 及TabLayout,  
        ToolBar要滑动,给其设置app:layout_scrollFlags  
      3.ViewPager是发出滑动行为的控件,设置属性 app:layout_behavior  
      注意:带layout_scrollFlags的view需要放在固定View的前面,这样滑动的view才能够正常退出,而固定的view继续留在顶部
作者:tianzhu2725 发表于2016/10/10 10:27:08 原文链接
阅读:19 评论:0 查看评论

EditText联想通讯录并清空数据

$
0
0

前言:懂得珍惜方可拥有更多。

废话不多说,先上效果演示:
这里写图片描述

下面是整个工程的思路:
一、获取通讯录
首先通过内容接受者来获取通讯录相关信息
其次PhoneUtils进行数据的处理

// 获取手机号码
    private void getPhoneContacts() {
        ContentResolver resolver = getContentResolver();

        // 获取手机联系人
        Cursor phoneCursor = resolver.query(Phone.CONTENT_URI, PHONES_PROJECTION, null, null, null);

        if (phoneCursor != null) {
            while (phoneCursor.moveToNext()) {

                // 得到手机号码
                String phoneNumber = phoneCursor.getString(PHONES_NUMBER_INDEX);
                // 当手机号码为空的或者为空字段 跳过当前循环
                if (TextUtils.isEmpty(phoneNumber))
                    continue;
                // 得到联系人名称
                String contactName = phoneCursor.getString(PHONES_DISPLAY_NAME_INDEX);

                phoneNumber = phoneNumber.replace("-", "");
                phoneNumber = phoneNumber.replace(" ", "");
                phoneNumber = phoneNumber.replace("+86", "");

                // 判断是否是手机号
                boolean mobileNO = PhoneUtils.isMobileNO(phoneNumber);

                if (mobileNO) {

                    String format344 = PhoneUtils.format344(phoneNumber);
                    if (format344!=null) {
                        mContactsName.add(contactName);
                        mContactsNumber.add(format344);
                    }
                }

            }
            phoneCursor.close();
            if (mContactsName.size() <= 0) {
            } else {
                for (int i = 0; i < mContactsName.size(); i++) {
                    Directory directory = new Directory();
                    directory.setContactName(mContactsName.get(i));
                    directory.setContactNumber(mContactsNumber.get(i));
                    mContacts.add(directory);
                }
            }
        }
    }

二、自定义EditText达到随时联想及全部删除的功能

package com.example.historyedittexts;

import java.util.ArrayList;

import android.content.Context;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.PopupWindow;
import android.widget.RelativeLayout;
import android.widget.ScrollView;
import android.widget.TextView;

/**
 * 自定义EditText
 * 
 * @author Jeffery<br>
 *         创建日期:2016年10月9日
 * @version 1.0
 *
 */
public class HistoryEdit extends EditText {
    private Context mContext;
    private PopupWindow popupWindow;
    private LinearLayout linearLayout;
    private ScrollView scrollView;

    private Drawable mDrawable;
    /** 联系人名称 **/
    private ArrayList<String> mContactsName = new ArrayList<String>();

    /** 联系人号码 **/
    private ArrayList<String> mContactsNumber = new ArrayList<String>();

    public HistoryEdit(Context context) {
        super(context);
        mContext = context;
        initView();
    }

    public HistoryEdit(Context context, AttributeSet attrs) {
        super(context, attrs);
        mContext = context;
        initView();
    }

    public HistoryEdit(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mContext = context;
        initView();
    }

    /*
     * 初始化一个LinearLayout布局 用于显示在popupwindow上
     */
    private void initView() {
        scrollView = (ScrollView) View.inflate(mContext, R.layout.history_scrollview, null);
        // linearLayout = new LinearLayout(mContext);
        linearLayout = (LinearLayout) scrollView.findViewById(R.id.his_li);
        // linearLayout.setLayoutParams(new
        // LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,
        // LinearLayout.LayoutParams.WRAP_CONTENT));
        // linearLayout.setOrientation(LinearLayout.VERTICAL);
        mDrawable = mContext.getResources().getDrawable(R.drawable.clear_icon);

        this.setOnFocusChangeListener(new OnFocusChangeListener() {

            @Override
            public void onFocusChange(View v, boolean hasFocus) {
                updateCleanable(length(), hasFocus);
            }
        });
    }

    // 当内容不为空,而且获得焦点,才显示右侧删除按钮
    public void updateCleanable(int length, boolean hasFocus) {
        if (length() > 0 && hasFocus)
            setCompoundDrawablesWithIntrinsicBounds(null, null, mDrawable, null);
        else
            setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
    }

    // 内容发生变化时调用
    @Override
    protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
        super.onTextChanged(text, start, lengthBefore, lengthAfter);
        updateCleanable(length(), true);
        if (text.length() > 0) {

            if (linearLayout != null) {
                linearLayout.removeAllViews();
            }
            if (popupWindow != null) {
                popupWindow.dismiss();
            }
            String str = text + "";
            // Toast.makeText(mContext, str, Toast.LENGTH_SHORT).show();
            for (int i = 0; i < mContactsNumber.size(); i++) { // 拉取集合中的历史数据
                                                                // 有几个历史数据

                if (mContactsNumber.get(i).startsWith(str)) {// 就创建几个textview
                    showUI(i);
                }
            }
            showWindow();
        }

    }

    private void showUI(int i) {

        RelativeLayout relativeLayout = new RelativeLayout(mContext);
        RelativeLayout.LayoutParams rl = new RelativeLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,
                LinearLayout.LayoutParams.WRAP_CONTENT);
        // rl.addRule(RelativeLayout.ALIGN_LEFT);
        // rl.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
        relativeLayout.setLayoutParams(rl);
        relativeLayout.setPadding(20, 20, 20, 20);
        // 号码
        final TextView textView = new TextView(mContext);
        textView.setText(mContactsNumber.get(i));
        RelativeLayout.LayoutParams tvNumber = new RelativeLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT,
                LinearLayout.LayoutParams.WRAP_CONTENT);
        // textView.setWidth(LinearLayout.LayoutParams.MATCH_PARENT);
        tvNumber.addRule(RelativeLayout.ALIGN_PARENT_LEFT);
        textView.setLayoutParams(tvNumber);// 设置宽高
        // textView.setPadding(10, 10, 10, 10);
        // 名字
        TextView textViewName = new TextView(mContext);
        textViewName.setText(mContactsName.get(i));
        RelativeLayout.LayoutParams tvName = new RelativeLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT,
                LinearLayout.LayoutParams.WRAP_CONTENT);
        // textViewName.setWidth(LinearLayout.LayoutParams.MATCH_PARENT);
        tvName.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
        textViewName.setLayoutParams(tvName);// 设置宽高
        // textViewName.setPadding(10, 10, 10, 10);
        relativeLayout.setOnClickListener(new OnClickListener() { // text点击事件
            // 点击后设置edittext的值
            @Override
            public void onClick(View v) {
                setText(textView.getText().toString());// 给EditText赋值
                setSelection(textView.getText().toString().length());// 设置EditText光标位置
                popupWindow.dismiss();
            }
        });
        relativeLayout.addView(textView); /// 添加到linerlayout中
        relativeLayout.addView(textViewName); /// 添加到linerlayout中

        linearLayout.addView(relativeLayout);
    }

    /*
     * 设置历史数据的方法
     */
    public void setData(ArrayList<Directory> mContacts) {
        // this.strings = mContacts.get(index);
        if (mContacts == null || mContacts.size() == 0) { // 如果历史数据集合为null,或者不包含历史数据,则移除linerlayout中的所有布局,并且隐藏popupwindow
            if (linearLayout != null) {
                linearLayout.removeAllViews();
            }
            if (popupWindow != null) {
                popupWindow.dismiss();
            }
            return;
        }
        for (int i = 0; i < mContacts.size(); i++) { // 拉取集合中的历史数据 有几个历史数据
                                                        // 就创建几个textview
            mContactsNumber.add(mContacts.get(i).getContactNumber());
            mContactsName.add(mContacts.get(i).getContactName());
            showUI(i);
        }
        // showHistory();
    }

    private void showHistory() {
        // 添加历史记录
        TextView history = new TextView(mContext); // 添加消除历史记录的textview
        history.setText("消除历史记录");
        history.setWidth(LinearLayout.LayoutParams.MATCH_PARENT);
        history.setPadding(10, 10, 10, 10);
        history.setGravity(Gravity.CENTER_HORIZONTAL);
        history.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) { // 设置点击事件
                setData(null);
            }
        });
        linearLayout.addView(history);
    }

    public void showWindow() {
        if (popupWindow != null && popupWindow.isShowing()) {
            return; // 如果popupwindow 在显示状态 则不作处理
        } else if (popupWindow != null && !popupWindow.isShowing()) {
            popupWindow.showAsDropDown(this); // 如果popupwindow 是隐藏状态 则显示出来
            return;
        }
        // 否则创建popupwindow 并且显示
        popupWindow = new PopupWindow(scrollView, getWidth(), LinearLayout.LayoutParams.WRAP_CONTENT);
        popupWindow.setBackgroundDrawable(getResources().getDrawable(R.drawable.quanaplha));
        popupWindow.update();
        popupWindow.setFocusable(false);
        popupWindow.showAsDropDown(this);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        // 每次手指按下的时候 调用showWindow();
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            showWindow();
        }

        //点“X”时清空数据
        final int DRAWABLE_RIGHT = 2;
        // 可以获得上下左右四个drawable,右侧排第二。图标没有设置则为空。
        Drawable rightIcon = getCompoundDrawables()[DRAWABLE_RIGHT];
        if (rightIcon != null && event.getAction() == MotionEvent.ACTION_UP) {
            // 检查点击的位置是否是右侧的删除图标
            // 注意,使用getRwwX()是获取相对屏幕的位置,getX()可能获取相对父组件的位置
            int leftEdgeOfRightDrawable = getRight() - getPaddingRight() - rightIcon.getBounds().width();
            if (event.getRawX() >= leftEdgeOfRightDrawable) {
                setText("");
            }
        }
        return super.onTouchEvent(event);
    }
}

三、自定义TextWatcher达到数字输入时344的效果(123 4567 8901)

package com.example.historyedittexts;

import android.text.Editable;
import android.text.Selection;
import android.text.TextWatcher;
import android.widget.EditText;

/**
 * 手机号码格式化 3 4 4
 * 
 * @author Jeffery<br>
 *         创建日期:2016年10月9日
 * @version 1.0
 *
 */
public class PhoneFormatTextWatcher implements TextWatcher {
    int beforeTextLength = 0;
    int onTextLength = 0;
    boolean isChanged = false;

    int location = 0;// 记录光标的位置
    private char[] tempChar;
    private StringBuffer buffer = new StringBuffer();
    int konggeNumberB = 0;
    private EditText et_input;

    public PhoneFormatTextWatcher(EditText et_input) {
        super();
        this.et_input = et_input;
    }

    public void afterTextChanged(Editable editable) {
        if (isChanged) {
            location = et_input.getSelectionEnd();
            int index = 0;
            while (index < buffer.length()) {
                if (buffer.charAt(index) == ' ') {
                    buffer.deleteCharAt(index);
                } else {
                    index++;
                }
            }

            index = 0;
            int konggeNumberC = 0;
            while (index < buffer.length()) {
                if (index == 3 || index == 8) {
                    buffer.insert(index, ' ');
                    konggeNumberC++;
                }
                index++;
            }

            if (konggeNumberC > konggeNumberB) {
                location += (konggeNumberC - konggeNumberB);
            }

            tempChar = new char[buffer.length()];
            buffer.getChars(0, buffer.length(), tempChar, 0);
            String str = buffer.toString();
            if (location > str.length()) {
                location = str.length();
            } else if (location < 0) {
                location = 0;
            }

            et_input.setText(str);
            Editable etable = et_input.getText();
            Selection.setSelection(etable, et_input.getText().length());
//          try {
//              Selection.setSelection(etable, location);
//          } catch (Exception e) {
//              Selection.setSelection(etable, et_input.getText().length());
//          }

            isChanged = false;
        }
    }

    public void onTextChanged(CharSequence s, int start, int before, int count) {
        onTextLength = s.length();
        buffer.append(s.toString());
        if (onTextLength == beforeTextLength || onTextLength <= 3 || isChanged) {
            isChanged = false;
            return;
        }
        isChanged = true;
    }

    public void beforeTextChanged(CharSequence s, int start, int count,
            int after) {
        beforeTextLength = s.length();
        if (buffer.length() > 0) {
            buffer.delete(0, buffer.length());
        }
        konggeNumberB = 0;
        for (int i = 0; i < s.length(); i++) {
            if (s.charAt(i) == ' ') {
                konggeNumberB++;
            }
        }
    }
}

四、自定义ScrollView达到数据太多事显示屏幕的1/3

package com.example.historyedittexts;

import android.app.Activity;
import android.content.Context;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.Display;
import android.widget.ScrollView;

/**
 * 自定义HistoryScrollView
 * 
 * @author Jeffery<br>
 *         创建日期:2016年10月9日
 * @version 1.0
 *
 */
public class HistoryScrollView extends ScrollView {  
        private Context mContext;  

        public HistoryScrollView(Context context) {  
            super(context);  
            init(context);  
        }  

        public HistoryScrollView(Context context, AttributeSet attrs) {  
            super(context, attrs);  
            init(context);  

        }  

        public HistoryScrollView(Context context, AttributeSet attrs, int defStyleAttr) {  
            super(context, attrs, defStyleAttr);  
            init(context);  
        }  

        private void init(Context context) {  
            mContext = context;  
        }  

        @Override  
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
            try {  
                //最大高度显示为屏幕内容高度的一半  
                Display display = ((Activity) mContext).getWindowManager().getDefaultDisplay();  
                DisplayMetrics d = new DisplayMetrics();  
                display.getMetrics(d);  
            //此处是关键,设置控件高度不能超过屏幕高度一半(d.heightPixels / 2)(在此替换成自己需要的高度)  
                heightMeasureSpec = MeasureSpec.makeMeasureSpec(d.heightPixels / 3, MeasureSpec.AT_MOST);  

            } catch (Exception e) {  
                e.printStackTrace();  
            }  
            //重新计算控件高、宽  
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);  
        }  
    }  

下面附上整个工程的代码下载地址:
https://github.com/JefferyShang/historyEditTexts
请大家尊重原创者版权,转载请标明出处:http://blog.csdn.net/u011005173/article/details/523662238谢谢
如果本文有帮到你,记得加关注哦。
初出茅庐,还望指教。

作者:u011005173 发表于2016/10/10 10:46:07 原文链接
阅读:22 评论:0 查看评论

android图片轮播+点击跳转广告页面

$
0
0

Android轮播网络图片+点击跳转广告页面————————–一些新手总是很头疼怎么获取网络图片的url之后让它像一些广告那样轮播起来,点击图片之后跳转到指定网页。效果如下
轮播页广告页

在布局引用自定义控件

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
    xmlns:tools="http://schemas.android.com/tools"  
    android:layout_width="match_parent"  
    android:layout_height="match_parent"  
    android:background="#FFFFFF"  
    tools:context="com.example.slideshowdemo.MainActivity"  >  


    <!-- 主题图片 -->  
    <com.example.sideshowview.SlideShowView
        android:id="@+id/slideshowView"  
        android:layout_width="fill_parent"  
        android:layout_height="200dp"  
        android:layout_centerHorizontal="true" />  

</LinearLayout>

MainActivity.java

public class MainActivity extends Activity {

    private String[] imageUrls = new String[]{  
            "http://d.hiphotos.baidu.com/image/pic/item/9f2f070828381f30b2bd028fac014c086e06f074.jpg",  
            "http://h.hiphotos.baidu.com/image/pic/item/2934349b033b5bb55e73afd833d3d539b600bc74.jpg",  
            "http://b.hiphotos.baidu.com/image/pic/item/ac345982b2b7d0a2bdfa8bbbceef76094b369ae1.jpg",  
            "http://g.hiphotos.baidu.com/image/pic/item/2e2eb9389b504fc213023f23e0dde71190ef6db3.jpg" } ;
    private String[] imageUris = new String[]{
            "http://www.baidu.com",
            "http://www.sina.com.cn",
            "http://www.taobao.com",
             "http://www.tudou.com" };

    private List<Map<String,String>> imageList = new ArrayList<Map<String,String>>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        for (int i = 0; i < 4; i++) {
            Map<String,String> image_uri = new HashMap<String,String>();
            image_uri.put("imageUrls", imageUrls[i]);
            image_uri.put("imageUris", imageUris[i]);
            imageList.add(image_uri);
        }

        SlideShowView view = (SlideShowView) findViewById(R.id.slideshowView); 
        view.setImageUrls(imageList);
    }

}

自定义轮播的详细实现

public class SlideShowView extends FrameLayout implements OnClickListener{  

    // 使用universal-image-loader插件读取网络图片,需要工程导入universal-image-loader-1.8.6-with-sources.jar  
    private ImageLoader imageLoader = ImageLoader.getInstance();  

    //轮播图图片数量  
    private final static int IMAGE_COUNT = 5;  
    //自动轮播的时间间隔  
    private final static int TIME_INTERVAL = 5;  
    //自动轮播启用开关  
    private final static boolean isAutoPlay = true;   

    //自定义轮播图的资源  
    private List<Map<String,String>> imageUrls;  
    //放轮播图片的ImageView 的list  
    private List<ImageView> imageViewsList;  
    //放圆点的View的list  
    private List<View> dotViewsList;  

    private ViewPager viewPager;  
    //当前轮播页  
    private int currentItem  = 0;  
    //定时任务  
    private ScheduledExecutorService scheduledExecutorService;  

    private Context context;  

    //Handler  
    private Handler handler = new Handler(){  

        @Override  
        public void handleMessage(Message msg) {  
            // TODO Auto-generated method stub  
            super.handleMessage(msg);  
            viewPager.setCurrentItem(currentItem);  
        }  

    };  

    public SlideShowView(Context context) {  
        this(context,null);  
        // TODO Auto-generated constructor stub  
    }  
    public SlideShowView(Context context, AttributeSet attrs) {  
        this(context, attrs, 0);  
        // TODO Auto-generated constructor stub  
    }  
    public SlideShowView(Context context, AttributeSet attrs, int defStyle) {  
        super(context, attrs, defStyle);  
        this.context = context;  

        initImageLoader(context);  

        if(isAutoPlay){  
            startPlay();  
        }  

    }  
    /** 
     * 开始轮播图切换 
     */  
    private void startPlay(){  
        scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();  
        scheduledExecutorService.scheduleAtFixedRate(new SlideShowTask(), 1, 4, TimeUnit.SECONDS);  
    }  
    /** 
     * 停止轮播图切换 
     */  
    private void stopPlay(){  
        scheduledExecutorService.shutdown();  
    }  
    /** 
     * 初始化相关Data 
     */  
    private void initData(){  
        imageViewsList = new ArrayList<ImageView>();  
        dotViewsList = new ArrayList<View>();  
        initUI(context);  
    }  

    public void setImageUrls(List<Map<String,String>> imageList){
        this.imageUrls = imageList;
         initData();
    }

    @Override
    public void onClick(View v) {
        // TODO Auto-generated method stub
        Intent intent = null;
        Bundle bundle = null;
        switch (v.getId()) {
        case 0:
            intent = new Intent(context,WebViewActivity.class);
            bundle = new Bundle();
            bundle.putString("url", imageUrls.get(0).get("imageUris"));
            intent.putExtras(bundle);
            context.startActivity(intent);
            break;
        case 1:
            intent = new Intent(context,WebViewActivity.class);
            bundle = new Bundle();
            bundle.putString("url", imageUrls.get(1).get("imageUris"));
            intent.putExtras(bundle);
            context.startActivity(intent);
            break;
        case 2:
            intent = new Intent(context,WebViewActivity.class);
            bundle = new Bundle();
            bundle.putString("url", imageUrls.get(2).get("imageUris"));
            intent.putExtras(bundle);
            context.startActivity(intent);
            break;
        case 3:
            intent = new Intent(context,WebViewActivity.class);
            bundle = new Bundle();
            bundle.putString("url", imageUrls.get(3).get("imageUris"));
            intent.putExtras(bundle);
            context.startActivity(intent);
            break;
        }
    }

    /** 
     * 初始化Views等UI 
     */  
    private void initUI(Context context){  
        if(imageUrls == null || imageUrls.size() == 0)  
            return;  

        LayoutInflater.from(context).inflate(R.layout.layout_slideshow, this, true);  

        LinearLayout dotLayout = (LinearLayout)findViewById(R.id.dotLayout);  
        dotLayout.removeAllViews();  

        // 热点个数与图片特殊相等  
        for (int i = 0; i < imageUrls.size(); i++) {  
            ImageView view =  new ImageView(context);  
            view.setId(i);
            view.setTag(imageUrls.get(i).get("imageUrls"));  
            if(i==0)//给一个默认图  
                view.setBackgroundResource(R.drawable.detail_p1);  
            view.setScaleType(ScaleType.FIT_XY);  
            view.setOnClickListener(this);
            imageViewsList.add(view);  

            ImageView dotView =  new ImageView(context);  
            LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT);  
            params.leftMargin = 4;  
            params.rightMargin = 4;  
            dotLayout.addView(dotView, params);  
            dotViewsList.add(dotView);  
        }  

        viewPager = (ViewPager) findViewById(R.id.viewPager);  
        viewPager.setFocusable(true);  

        viewPager.setAdapter(new MyPagerAdapter());  
        viewPager.setOnPageChangeListener(new MyPageChangeListener());  
    }  

    /** 
     * 填充ViewPager的页面适配器 
     *  
     */  
    private class MyPagerAdapter  extends PagerAdapter{  

        @Override  
        public void destroyItem(View container, int position, Object object) {  
            // TODO Auto-generated method stub  
            //((ViewPag.er)container).removeView((View)object);  
            ((ViewPager)container).removeView(imageViewsList.get(position));  
        }  

        @Override  
        public Object instantiateItem(View container, int position) {  
            ImageView imageView = imageViewsList.get(position);  

            imageLoader.displayImage(imageView.getTag() + "", imageView);  

            ((ViewPager)container).addView(imageViewsList.get(position));  
            return imageViewsList.get(position);  
        }  

        @Override  
        public int getCount() {  
            // TODO Auto-generated method stub  
            return imageViewsList.size();  
        }  

        @Override  
        public boolean isViewFromObject(View arg0, Object arg1) {  
            // TODO Auto-generated method stub  
            return arg0 == arg1;  
        }  
        @Override  
        public void restoreState(Parcelable arg0, ClassLoader arg1) {  
            // TODO Auto-generated method stub  

        }  

        @Override  
        public Parcelable saveState() {  
            // TODO Auto-generated method stub  
            return null;  
        }  

        @Override  
        public void startUpdate(View arg0) {  
            // TODO Auto-generated method stub  

        }  

        @Override  
        public void finishUpdate(View arg0) {  
            // TODO Auto-generated method stub  

        }  

    }  
    /** 
     * ViewPager的监听器 
     * 当ViewPager中页面的状态发生改变时调用 
     *  
     */  
    private class MyPageChangeListener implements OnPageChangeListener{  

        boolean isAutoPlay = false;  

        @Override  
        public void onPageScrollStateChanged(int arg0) {  
            // TODO Auto-generated method stub  
            switch (arg0) {  
            case 1:// 手势滑动,空闲中  
                isAutoPlay = false;  
                break;  
            case 2:// 界面切换中  
                isAutoPlay = true;  
                break;  
            case 0:// 滑动结束,即切换完毕或者加载完毕  
                // 当前为最后一张,此时从右向左滑,则切换到第一张  
                if (viewPager.getCurrentItem() == viewPager.getAdapter().getCount() - 1 && !isAutoPlay) {  
                    viewPager.setCurrentItem(0);  
                }  
                // 当前为第一张,此时从左向右滑,则切换到最后一张  
                else if (viewPager.getCurrentItem() == 0 && !isAutoPlay) {  
                    viewPager.setCurrentItem(viewPager.getAdapter().getCount() - 1);  
                }  
                break;  
        }  
        }  

        @Override  
        public void onPageScrolled(int arg0, float arg1, int arg2) {  
            // TODO Auto-generated method stub  

        }  

        @Override  
        public void onPageSelected(int pos) {  
            // TODO Auto-generated method stub  

            currentItem = pos;  
            for(int i=0;i < dotViewsList.size();i++){  
                if(i == pos){  
                    ((View)dotViewsList.get(pos)).setBackgroundResource(R.drawable.compose_guide_check_box_number);  
                }else {  
                    ((View)dotViewsList.get(i)).setBackgroundResource(R.drawable.compose_guide_check_box_default);  
                }  
            }  
        }  

    }  

    /** 
     *执行轮播图切换任务 
     */  
    private class SlideShowTask implements Runnable{  

        @Override  
        public void run() {  
            // TODO Auto-generated method stub  
            synchronized (viewPager) {  
                currentItem = (currentItem+1)%imageViewsList.size();  
                handler.obtainMessage().sendToTarget();  
            }  
        }  

    }  

    /** 
     * 销毁ImageView资源,回收内存 
     */  
    private void destoryBitmaps() {  

        for (int i = 0; i < IMAGE_COUNT; i++) {  
            ImageView imageView = imageViewsList.get(i);  
            Drawable drawable = imageView.getDrawable();  
            if (drawable != null) {  
                //解除drawable对view的引用  
                drawable.setCallback(null);  
            }  
        }  
    }  


    public static void initImageLoader(Context context) {  
        ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(context)
                                            .threadPriority(Thread.NORM_PRIORITY - 2)
                                            .denyCacheImageMultipleSizesInMemory()
                                            .discCacheFileNameGenerator(new Md5FileNameGenerator())
                                            .tasksProcessingOrder(QueueProcessingType.LIFO)
                                            .writeDebugLogs() 
                                            .build();  
        ImageLoader.getInstance().init(config);  
    }

}  

带指示器的布局

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"  
    android:layout_width="match_parent"  
    android:layout_height="match_parent">  

    <android.support.v4.view.ViewPager  
        android:id="@+id/viewPager"  
        android:layout_width="match_parent"  
        android:layout_height="match_parent" />  

    <LinearLayout android:id="@+id/dotLayout"  
        android:layout_width="match_parent"  
        android:layout_height="wrap_content"  
        android:layout_alignParentBottom="true"  
        android:padding="8dp"  
        android:gravity="right"  
        android:orientation="horizontal">  

        <View  
            android:id="@+id/v_dot1"  
            android:layout_width="8dp"  
            android:layout_height="8dp"  
            android:background="@drawable/compose_guide_check_box_default" />  

        <View  
            android:id="@+id/v_dot2"  
            android:layout_width="8dp"  
            android:layout_height="8dp"  
            android:layout_marginLeft="5dp"  
            android:background="@drawable/compose_guide_check_box_number" />  

    </LinearLayout>  
</RelativeLayout> 

点击广告跳转的web

public class WebViewActivity extends Activity{

    private WebView mWebView;
    private TextView mTitle;
    private ImageButton mBackBtn;
    private Bundle mBundle;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // TODO Auto-generated method stub
        setContentView(R.layout.activity_web);
        super.onCreate(savedInstanceState);
        mBundle = getIntent().getExtras();
        initView();
    }

    public void initView() {
        // TODO Auto-generated method stub
        mWebView = (WebView) findViewById(R.id.web_webview);
        mWebView.loadUrl(mBundle.getString("url"));
        mWebView.requestFocusFromTouch();
        mWebView.getSettings().setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
        /**覆盖调用系统或自带浏览器行为打开网页*/
        mWebView.setWebViewClient(new WebViewClient(){
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                // TODO Auto-generated method stub
                view.loadUrl(url);
                return true;
            }
        });
        /**判断加载过程*/
        mWebView.setWebChromeClient(new WebChromeClient() {
            @Override
            public void onProgressChanged(WebView view, int newProgress) {
                // TODO Auto-generated method stub
                if (newProgress == 100) {
                    // 网页加载完成

                } else {
                    // 加载中

                }
            }
        });
        initListener();
    }

    public void initListener() {
        // TODO Auto-generated method stub
        /**加载javascript*/
//      WebSettings mWebSetting = mWebView.getSettings();
//      mWebSetting.setJavaScriptEnabled(true);
//      mWebView.addJavascriptInterface(new Object() {       
//            public void clickOnAndroid() {       
//                new Handler().post(new Runnable() {       
//                    public void run() {       
//                      mWebView.loadUrl("javascript:wave()");       
//                    }       
//                });       
//            }       
//        }, "demo");

         /**打开页面时, 自适应屏幕*/
         WebSettings webSettings =   mWebView .getSettings();       
         webSettings.setUseWideViewPort(true);//设置此属性,可任意比例缩放
         webSettings.setLoadWithOverviewMode(true);

         /**便页面支持缩放*/
         webSettings.setJavaScriptEnabled(true);  
         webSettings.setBuiltInZoomControls(true);
         webSettings.setSupportZoom(true);

    }

}
作者:snailbaby_soko 发表于2016/10/10 10:46:18 原文链接
阅读:32 评论:0 查看评论

打造android偷懒神器———ListView的万能适配器

$
0
0

我们一向写的自定义适配器,无非就是继承ArrayAdapter,或者继承自BaseAdapter,然后重写4个方法,前三个方法基本相同,不同在于getView方法,getView里面为了减少绑定和View的重建,又会引入一个静态类ViewHolder,我相信下面这段代码你一点见过不少。


 1 package com.example.nanchen.commonadapterforlistviewdemo;
 2 
 3 import android.content.Context;
 4 import android.view.LayoutInflater;
 5 import android.view.View;
 6 import android.view.ViewGroup;
 7 import android.widget.BaseAdapter;
 8 import android.widget.ImageView;
 9 import android.widget.TextView;
10 
11 import java.util.List;
12 
13 /**
14  * 常见的ListView的Adapter适配器
15  * Created by 南尘 on 16-7-28.
16  */
17 public class MyListAdapter extends BaseAdapter {
18     private Context context;
19     private List<Data> list;
20 
21     public MyListAdapter(Context context, List<Data> list) {
22         this.context = context;
23         this.list = list;
24     }
25 
26     @Override
27     public int getCount() {
28         return list == null ? 0 : list.size();
29     }
30 
31     @Override
32     public Object getItem(int position) {
33         return list.get(position);
34     }
35 
36     @Override
37     public long getItemId(int position) {
38         return position;
39     }
40 
41     @Override
42     public View getView(int position, View convertView, ViewGroup viewGroup) {
43         MyViewHolder holder = null;
44         if (convertView == null) {
45             convertView = LayoutInflater.from(context).inflate(R.layout.list_item,viewGroup,false);
46             holder = new MyViewHolder();
47             holder.iv = (ImageView) convertView.findViewById(R.id.item_image);
48             holder.tv = (TextView) convertView.findViewById(R.id.item_text);
49             convertView.setTag(holder);
50         } else {
51             holder = (MyViewHolder) convertView.getTag();
52         }
53         Data data = (Data) getItem(position);
54         holder.iv.setImageResource(data.getImageId());
55         holder.tv.setText(data.getText());
56         return convertView;
57     }
58 
59     public static class MyViewHolder {
60         ImageView iv;
61         TextView tv;
62     }
63 }

上面是大家最常用,见到的适配器的写法,如果咱们项目中需要很多的适配器的话,咱们一个个写,想想就很浪费时间,因此咱们可以自己封装一个
仔细观察上面的Adapter,的确是前三个方法一样。我们要是可以全部抽出来就好了。所以可以抽出来,写一个泛型使其变成一个抽象的基类,继承自BaseAdapter.其子类只需要去关心其getView方法

1 public abstract class MyListAdapter<T> extends BaseAdapter {
 2     private Context context;
 3     private List<T> list;
 4 
 5     public MyListAdapter(Context context, List<T> list) {
 6         this.context = context;
 7         this.list = list;
 8     }
 9 
10     @Override
11     public int getCount() {
12         return list == null ? 0 : list.size();
13     }
14 
15     @Override
16     public Object getItem(int position) {
17         return list.get(position);
18     }
19 
20     @Override
21     public long getItemId(int position) {
22         return position;
23     }
24 }

好像没什么不对,但是这也没解决多少问题呀,要是我们在写大项目的时候还可以抽点时间出来打LOL拿个首胜什么的就更好了。
再来看看getView方法,基本都是先判断ViewHolder是否为空,为空则去Inflate一个xml文件进来,再绑定下视图,设置一个标记,不为空的时候直接引用标记。
或许这里我们可以试一下在ViewHolder上做点什么。
我们要是想把ViewHolder提取出来,只能把每一个Item都固定在ViewHolder里面,而Item又不是固定的,怎么办?
要是我们可以把这个Item直接作为参数传进来就好了,可是传控件好像不能区分,仔细一想,我们能看到一个控件对应着一个id,这个好像可以用HashMap的键值对处理。
而键值由于是Int型的,在新的java API中明确表示在键值为Integer的HashMap中我们要用SparseArray作代替,这样不仅简单,而且性能更优。
我们尝试着封装一下ViewHolder:

 public class ViewHolder {
 2     //现在对于int作为键的官方推荐用SparseArray替代HashMap
 3     private final SparseArray<View> views;
 4     private int position;
 5     private View convertView;
 6     private Context context;
 7 
 8     private ViewHolder(Context context,ViewGroup parent, int layoutId, int position) {
 9         this.context = context;
10         this.views = new SparseArray<>();
11         this.convertView = LayoutInflater.from(parent.getContext()).inflate(layoutId, parent, false);
12         convertView.setTag(this);
13     }
14 
15     /**
16      * 拿到一个ViewHolder对象
17      */
18     public static ViewHolder get(View convertView, ViewGroup parent, int layoutId, int position) {
19         if (convertView == null) {
20             return new ViewHolder(parent.getContext(),parent, layoutId, position);
21         }
22         return (ViewHolder) convertView.getTag();
23     }
24 
25     /**
26      * 通过控件的Id获取对于的控件,如果没有则加入views
27      */
28     public <T extends View> T getView(int viewId) {
29         View view = views.get(viewId);
30         if (view == null) {
31             view = convertView.findViewById(viewId);
32             views.put(viewId, view);
33         }
34         return (T) view;
35     }
36 }

这样的话我们的getView可能会变成这样。

1 @Override  
2     public View getView(int position, View convertView, ViewGroup parent){  
3         ViewHolder viewHolder = ViewHolder.get(convertView, parent,  
4                 R.layout.list_item, position);  
5         TextView mTitle = viewHolder.getView(R.id.id_tv_title);  
6         mTitle.setText(((Data) list.get(position)).getText());  
7         //这里就不设置ImageView了
8         return viewHolder.getConvertView();  
9     } 

好吧。与其这样。我们不如直接写在Activity中。
并且如果我们想设置东西也许可以在Holder里面设置,我们可以试一试。
封装后的ViewHolder是这样。

1 package com.example.nanchen.commonadapterforlistviewdemo;
 2 
 3 import android.content.Context;
 4 import android.graphics.Bitmap;
 5 import android.util.SparseArray;
 6 import android.view.LayoutInflater;
 7 import android.view.View;
 8 import android.view.ViewGroup;
 9 import android.widget.ImageView;
10 import android.widget.TextView;
11 
12 import com.squareup.picasso.Picasso;
13 
14 /**
15  * 万能适配器的ViewHolder
16  * Created by 南尘 on 16-7-28.
17  */
18 public class ViewHolder {
19     //现在对于int作为键的官方推荐用SparseArray替代HashMap
20     private final SparseArray<View> views;
21     private int position;
22     private View convertView;
23     private Context context;
24 
25     private ViewHolder(Context context,ViewGroup parent, int layoutId, int position) {
26         this.context = context;
27         this.views = new SparseArray<>();
28         this.convertView = LayoutInflater.from(parent.getContext()).inflate(layoutId, parent, false);
29         convertView.setTag(this);
30     }
31 
32     /**
33      * 拿到一个ViewHolder对象
34      */
35     public static ViewHolder get(View convertView, ViewGroup parent, int layoutId, int position) {
36         if (convertView == null) {
37             return new ViewHolder(parent.getContext(),parent, layoutId, position);
38         }
39         return (ViewHolder) convertView.getTag();
40     }
41 
42     /**
43      * 通过控件的Id获取对于的控件,如果没有则加入views
44      */
45     public <T extends View> T getView(int viewId) {
46         View view = views.get(viewId);
47         if (view == null) {
48             view = convertView.findViewById(viewId);
49             views.put(viewId, view);
50         }
51         return (T) view;
52     }
53 
54     public View getConvertView() {
55         return convertView;
56     }
57 
58     /**
59      * 设置字符串
60      */
61     public ViewHolder setText(int viewId,String text){
62         TextView tv = getView(viewId);
63         tv.setText(text);
64         return this;
65     }
66 
67     /**
68      * 设置图片
69      */
70     public ViewHolder setImageResource(int viewId,int drawableId){
71         ImageView iv = getView(viewId);
72         iv.setImageResource(drawableId);
73         return this;
74     }
75 
76     /**
77      * 设置图片
78      */
79     public ViewHolder setImageBitmap(int viewId, Bitmap bitmap){
80         ImageView iv = getView(viewId);
81         iv.setImageBitmap(bitmap);
82         return this;
83     }
84 
85     /**
86      * 设置图片
87      */
88     public ViewHolder setImageByUrl(int viewId,String url){
89         Picasso.with(context).load(url).into((ImageView) getView(viewId));
90 //        ImageLoader.getInstance().init(ImageLoaderConfiguration.createDefault(context));
91 //        ImageLoader.getInstance().displayImage(url, (ImageView) getView(viewId));
92         return this;
93     }
94 
95     public int getPosition(){
96         return position;
97     }
98 }

上面的图片网络加载我用Picasso加载框架,这个网上很多图片加载框架
再看看我们的万能适配器,这里我们把它写做一个抽象类,传入一个泛型作为参数。

package com.example.nanchen.commonadapterforlistviewdemo;
 2 
 3 import android.content.Context;
 4 import android.view.View;
 5 import android.view.ViewGroup;
 6 import android.widget.BaseAdapter;
 7 
 8 import java.util.List;
 9 
10 /**
11  * 打造ListView的万能适配器
12  * Created by 南尘 on 16-7-28.
13  */
14 public abstract class CommonAdaper<T> extends BaseAdapter {
15     private Context context;
16     private List<T> list;
17 
18 
19     public CommonAdaper(Context context, List<T> list) {
20         this.context = context;
21         this.list = list;
22     }
23 
24     @Override
25     public int getCount() {
26         return list == null ? 0 : list.size();
27     }
28 
29     @Override
30     public T getItem(int position) {
31         return list.get(position);
32     }
33 
34     @Override
35     public long getItemId(int position) {
36         return position;
37     }
38 
39     @Override
40     public View getView(int i, View view, ViewGroup viewGroup) {
41         ViewHolder holder = ViewHolder.get(view,viewGroup,R.layout.list_item,i);
42         convert(holder,getItem(i));
43         return holder.getConvertView();
44     }
45 
46     public abstract void convert(ViewHolder holder,T item);
47 
48 }

用法的话就更简单了

package com.example.nanchen.commonadapterforlistviewdemo;
 2 
 3 import android.support.v7.app.AppCompatActivity;
 4 import android.os.Bundle;
 5 import android.widget.ListView;
 6 
 7 import java.util.ArrayList;
 8 import java.util.List;
 9 
10 public class MainActivity extends AppCompatActivity {
11 
12     private ListView listView;
13     private List<Data> list;
14 
15     @Override
16     protected void onCreate(Bundle savedInstanceState) {
17         super.onCreate(savedInstanceState);
18         setContentView(R.layout.activity_main);
19 
20         listView = (ListView) findViewById(R.id.main_lv);
21         initList();
22 
23         listView.setAdapter(new CommonAdaper<Data>(this,list) {
24             @Override
25             public void convert(ViewHolder holder, Data item) {
26                 holder.setText(R.id.item_text,item.getText());
27                 if (item.getImageUrl() != null){
28                     holder.setImageByUrl(R.id.item_image,item.getImageUrl());
29                 }else {
30                     holder.setImageResource(R.id.item_image,item.getImageId());
31                 }
32             }
33         });
34     }
35 
36     private void initList() {
37         list = new ArrayList<>();
38         for (int i = 0; i < 5; i++) {
39             list.add(new Data("本地 "+i,R.mipmap.ic_launcher));
40         }
41 
42         for (int i = 0; i < 5; i++) {
43             list.add(new Data("网络 "+i,"http://pic.cnblogs.com/face/845964/20160301162812.png"));
44         }
45     }
46 }

itme的xml布局文件
一个是Activity_main.xml

 <?xml version="1.0" encoding="utf-8"?>
 2 <RelativeLayout
 3     xmlns:android="http://schemas.android.com/apk/res/android"
 4     xmlns:tools="http://schemas.android.com/tools"
 5     android:layout_width="match_parent"
 6     android:layout_height="match_parent"
 7     tools:context="com.example.nanchen.commonadapterforlistviewdemo.MainActivity">
 8 
 9 
10     <ListView
11         android:layout_width="match_parent"
12         android:layout_height="match_parent"
13         android:id="@+id/main_lv"/>
14 </RelativeLayout>

list_item.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:orientation="horizontal">

    <ImageView
        android:id="@+id/item_image"
        android:layout_width="60dp"
        android:layout_height="60dp"
        android:src="@mipmap/ic_launcher"/>


    <TextView
        android:id="@+id/item_text"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1"
        android:gravity="center"
        android:text="内容"/>

</LinearLayout>

完事,有问题留言交流~

作者:liwei123liwei123 发表于2016/10/10 10:48:19 原文链接
阅读:31 评论:0 查看评论

Android6.0 显示系统(五) SurfaceFlinger服务

$
0
0

SurfaceFlinger是一个独立的进程,我们来看下init.rc关于SurfaceFlinger的代码,我们可以看到SurfaceFlinger是属于core服务的。

service surfaceflinger /system/bin/surfaceflinger
    class core
    user system
    group graphics drmrpc
    onrestart restart zygote
    writepid /dev/cpuset/system-background/tasks


一、SurfaceFlinger的启动过程

SurfaceFlinger的main函数在framework/native/services/surfaceflinger/main_surfaceflinger.cpp中

int main(int, char**) {
    // When SF is launched in its own process, limit the number of
    // binder threads to 4.
    ProcessState::self()->setThreadPoolMaxThreadCount(4);//设置了Binder线程池最大线程为4

    // start the thread pool
    sp<ProcessState> ps(ProcessState::self());
    ps->startThreadPool();

    // instantiate surfaceflinger
    sp<SurfaceFlinger> flinger = new SurfaceFlinger();//创建SurfaceFlinger对象

    setpriority(PRIO_PROCESS, 0, PRIORITY_URGENT_DISPLAY);

    set_sched_policy(0, SP_FOREGROUND);

    // initialize before clients can connect
    flinger->init();//调用SurfaceFlinger的init函数

    // publish surface flinger
    sp<IServiceManager> sm(defaultServiceManager());
    sm->addService(String16(SurfaceFlinger::getServiceName()), flinger, false);//像serviceManager注册SurfaceFlinger服务

    // run in this thread
    flinger->run();//运行

    return 0;
}

在主函数中先设置了该进程的binder线程池最大数为4,然后创建了SurfaceFlinger对象,并且调用了其init函数,接着把SurfaceFlinger服务注册到ServiceManager中,然后调用了run方法。

我们先来看下init函数

void SurfaceFlinger::init() {
    ALOGI(  "SurfaceFlinger's main thread ready to run. "
            "Initializing graphics H/W...");

    Mutex::Autolock _l(mStateLock);

    // initialize EGL for the default display 将EGL初始化成缺省的显示
    mEGLDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY);
    eglInitialize(mEGLDisplay, NULL, NULL);

    // start the EventThread
    sp<VSyncSource> vsyncSrc = new DispSyncSource(&mPrimaryDispSync,
            vsyncPhaseOffsetNs, true, "app");
    mEventThread = new EventThread(vsyncSrc);
    sp<VSyncSource> sfVsyncSrc = new DispSyncSource(&mPrimaryDispSync,
            sfVsyncPhaseOffsetNs, true, "sf");
    mSFEventThread = new EventThread(sfVsyncSrc);
    mEventQueue.setEventThread(mSFEventThread);

    // Initialize the H/W composer object.  There may or may not be an
    // actual hardware composer underneath.
    mHwc = new HWComposer(this,//显示硬件抽象类
            *static_cast<HWComposer::EventHandler *>(this));

    // get a RenderEngine for the given display / config (can't fail)
    mRenderEngine = RenderEngine::create(mEGLDisplay, mHwc->getVisualID());

    // retrieve the EGL context that was selected/created
    mEGLContext = mRenderEngine->getEGLContext();

    LOG_ALWAYS_FATAL_IF(mEGLContext == EGL_NO_CONTEXT,
            "couldn't create EGLContext");

    // initialize our non-virtual displays 初始化显示设备
    for (size_t i=0 ; i<DisplayDevice::NUM_BUILTIN_DISPLAY_TYPES ; i++) {
        DisplayDevice::DisplayType type((DisplayDevice::DisplayType)i);
        // set-up the displays that are already connected
        if (mHwc->isConnected(i) || type==DisplayDevice::DISPLAY_PRIMARY) {
            // All non-virtual displays are currently considered secure.
            bool isSecure = true;
            createBuiltinDisplayLocked(type);
            wp<IBinder> token = mBuiltinDisplays[i];

            sp<IGraphicBufferProducer> producer;
            sp<IGraphicBufferConsumer> consumer;
            BufferQueue::createBufferQueue(&producer, &consumer,
                    new GraphicBufferAlloc());

            sp<FramebufferSurface> fbs = new FramebufferSurface(*mHwc, i,
                    consumer);
            int32_t hwcId = allocateHwcDisplayId(type);
            sp<DisplayDevice> hw = new DisplayDevice(this,
                    type, hwcId, mHwc->getFormat(hwcId), isSecure, token,
                    fbs, producer,
                    mRenderEngine->getEGLConfig());
            if (i > DisplayDevice::DISPLAY_PRIMARY) {
                // FIXME: currently we don't get blank/unblank requests
                // for displays other than the main display, so we always
                // assume a connected display is unblanked.
                ALOGD("marking display %zu as acquired/unblanked", i);
                hw->setPowerMode(HWC_POWER_MODE_NORMAL);
            }
            mDisplays.add(token, hw);
        }
    }

    // make the GLContext current so that we can create textures when creating Layers
    // (which may happens before we render something)
    getDefaultDisplayDevice()->makeCurrent(mEGLDisplay, mEGLContext);

    mEventControlThread = new EventControlThread(this);
    mEventControlThread->run("EventControl", PRIORITY_URGENT_DISPLAY);

    // set a fake vsync period if there is no HWComposer
    if (mHwc->initCheck() != NO_ERROR) {
        mPrimaryDispSync.setPeriod(16666667);
    }

    // initialize our drawing state
    mDrawingState = mCurrentState;

    // set initial conditions (e.g. unblank default device)
    initializeDisplays();//初始化显示设备

    // start boot animation
    startBootAnim();//启动开机动画
}

init函数主要工作:

1.初始化OpenGL ES图形库。

2. 创建显示设备的抽象代表,负责和显示设备打交道。

3. 创建显示设备对象。

4. 启动EventThread。监听和处理SurfaceFlinger中的事件。

5.设置软件VSync信号周期。

6.初始化显示设备,调用initializeDisplays完成。

7.启动开机动画,调用了startBootAnim函数,只是设置了两个属性,其中一个ctl.start是启动了bootanim进程。

void SurfaceFlinger::startBootAnim() {
    // start boot animation
    property_set("service.bootanim.exit", "0");
    property_set("ctl.start", "bootanim");
}


二、消息和事件分发 MessageQueue和EventThread

MessageQueue和用于消息和事件的分发,这个和之前博客分析过得消息机制原理差不多。

我们先来看看MessageQueue的主要成员变量

class MessageQueue {
    ......
    sp<SurfaceFlinger> mFlinger;//指向SurfaceFlinger
    sp<Looper> mLooper;//消息机制Looper对象
    sp<EventThread> mEventThread;//关联的EventThread
    sp<IDisplayEventConnection> mEvents;
    sp<BitTube> mEventTube;
    sp<Handler> mHandler;//消息处理器
其中mEventThread主要是用来分析VSync信号的。

在SurfaceFlinger中有一个类型为MessageQueue的成员变量mEventQueue,在SurfaceFlinger的onFirstRef函数中会调用其init函数

void SurfaceFlinger::onFirstRef()
{
    mEventQueue.init(this);
}
在这个函数中创建了Looper和Handler对象,并且把flinger保存在mFlinger中。

void MessageQueue::init(const sp<SurfaceFlinger>& flinger)
{
    mFlinger = flinger;
    mLooper = new Looper(true);
    mHandler = new Handler(*this);
}
我们来看下Handler这个类,它是MessageQueue类的一个内部类,这个类主要处理3个消息。

    class Handler : public MessageHandler {
        enum {
            eventMaskInvalidate     = 0x1,//invalidate消息
            eventMaskRefresh        = 0x2,//刷新消息
            eventMaskTransaction    = 0x4
        };
        MessageQueue& mQueue;
        int32_t mEventMask;
    public:
        Handler(MessageQueue& queue) : mQueue(queue), mEventMask(0) { }
        virtual void handleMessage(const Message& message);
        void dispatchRefresh();
        void dispatchInvalidate();
        void dispatchTransaction();
    };


我们再来看在SurfaceFlinger主函数最后调用了下面方法。

    flinger->run();

SurfaceFlinger::run代码如下

void SurfaceFlinger::run() {
    do {
        waitForEvent();
    } while (true);
}

waitForEvent函数如下:

void SurfaceFlinger::waitForEvent() {
    mEventQueue.waitMessage();
}

然后又调用了EventQueue的waitMessage方法,记住这里是在主线程中循环调用的。

下面我们来看下waitMessage方法,flushCommands之前在分析Binder的博客中有提到,主要是清理工作的,和Binder驱动的交互关了。而pollOnce是消息机制,主要调用了epoll_wait函数,会阻塞,阻塞完了会分发消息队列中的消息。这里的消息只有自己在Handler中发的消息,还有在setEventThread中自己添加的fd。

void MessageQueue::waitMessage() {
    do {
        IPCThreadState::self()->flushCommands();
        int32_t ret = mLooper->pollOnce(-1);
        switch (ret) {
            case Looper::POLL_WAKE:
            case Looper::POLL_CALLBACK:
                continue;
            case Looper::POLL_ERROR:
                ALOGE("Looper::POLL_ERROR");
            case Looper::POLL_TIMEOUT:
                // timeout (should not happen)
                continue;
            default:
                // should not happen
                ALOGE("Looper::pollOnce() returned unknown status %d", ret);
                continue;
        }
    } while (true);
}

下面是Handler中发送消息,这个会在pollOnce中处理。

void MessageQueue::Handler::dispatchRefresh() {
    if ((android_atomic_or(eventMaskRefresh, &mEventMask) & eventMaskRefresh) == 0) {
        mQueue.mLooper->sendMessage(this, Message(MessageQueue::REFRESH));
    }
}

void MessageQueue::Handler::dispatchInvalidate() {
    if ((android_atomic_or(eventMaskInvalidate, &mEventMask) & eventMaskInvalidate) == 0) {
        mQueue.mLooper->sendMessage(this, Message(MessageQueue::INVALIDATE));
    }
}

void MessageQueue::Handler::dispatchTransaction() {
    if ((android_atomic_or(eventMaskTransaction, &mEventMask) & eventMaskTransaction) == 0) {
        mQueue.mLooper->sendMessage(this, Message(MessageQueue::TRANSACTION));
    }
}

在SurfaceFlinger的init函数还调用了如下函数

    mSFEventThread = new EventThread(sfVsyncSrc);
    mEventQueue.setEventThread(mSFEventThread);

我们来看setEventThread函数会调用Looper的addFd,这个最终也会在pollOnce中执行。mEventThread是一个EventThread对象,调用createEventConnection来创建一个连接。EventThread是一个线程类用来分发VSync消息。这个我们后面讲解VSync信号的时候还会详细分析。

void MessageQueue::setEventThread(const sp<EventThread>& eventThread)
{
    mEventThread = eventThread;
    mEvents = eventThread->createEventConnection();
    mEventTube = mEvents->getDataChannel();
    mLooper->addFd(mEventTube->getFd(), 0, Looper::EVENT_INPUT,
            MessageQueue::cb_eventReceiver, this);
}


三、显示设备  DisplayDevice类

DisplayDevice是显示设备的抽象,定义了3中类型的显示设备。

1.DISPLAY_PRIMARY:主显示设备,通常是LCD屏

2.DISPLAY_EXTERNAL:扩展显示设备。通过HDMI输出的显示信号

3.DISPLAY_VIRTUAL:虚拟显示设备,通过WIFI输出信号
这3钟设备,第一种是基本配置,另外两种需要硬件支持。这里我们主要讲解第一种。


SurfaceFlinger中需要显示的图层(layer)将通过DisplayDevice对象传递到OpenGLES中进行合成,合成之后的图像再通过HWComposer对象传递到Framebuffer中显示。DisplayDevice对象中的成员变量mVisibleLayersSortedByZ保存了所有需要显示在本显示设备中显示的Layer对象,同时DisplayDevice对象也保存了和显示设备相关的显示方向、显示区域坐标等信息。

上节在SurfaceFlinger的init函数中有一段代码来创建DisplayDevice对象

    for (size_t i=0 ; i<DisplayDevice::NUM_BUILTIN_DISPLAY_TYPES ; i++) {
        DisplayDevice::DisplayType type((DisplayDevice::DisplayType)i);
        // set-up the displays that are already connected
        if (mHwc->isConnected(i) || type==DisplayDevice::DISPLAY_PRIMARY) {
            // All non-virtual displays are currently considered secure.
            bool isSecure = true;
            createBuiltinDisplayLocked(type);//给显示设备分配一个token
            wp<IBinder> token = mBuiltinDisplays[i];

            sp<IGraphicBufferProducer> producer;
            sp<IGraphicBufferConsumer> consumer;
            BufferQueue::createBufferQueue(&producer, &consumer,
                    new GraphicBufferAlloc());

            sp<FramebufferSurface> fbs = new FramebufferSurface(*mHwc, i,
                    consumer);
            int32_t hwcId = allocateHwcDisplayId(type);
            sp<DisplayDevice> hw = new DisplayDevice(this,
                    type, hwcId, mHwc->getFormat(hwcId), isSecure, token,
                    fbs, producer,
                    mRenderEngine->getEGLConfig());
            if (i > DisplayDevice::DISPLAY_PRIMARY) {
                // FIXME: currently we don't get blank/unblank requests
                // for displays other than the main display, so we always
                // assume a connected display is unblanked.
                ALOGD("marking display %zu as acquired/unblanked", i);
                hw->setPowerMode(HWC_POWER_MODE_NORMAL);
            }
            mDisplays.add(token, hw);//把显示设备对象保存在mDisplays列表中
        }
    }

所有显示设备的输出都要通过HWComposer对象完成,因此上面这段代码先调用了HWComposer的isConnected来检查显示设备是否已连接,只有和显示设备连接的DisplayDevice对象才会被创建出来。即使没有任何物理显示设备被检测到,SurfaceFlinger都需要一个DisplayDevice对象才能正常工作,因此,DISPLAY_PRIMARY类型的DisplayDevice对象总是会被创建出来。

createBuiltinDisplayLocked函数就是为显示设备对象创建一个BBinder类型的Token而已。

void SurfaceFlinger::createBuiltinDisplayLocked(DisplayDevice::DisplayType type) {
    ALOGW_IF(mBuiltinDisplays[type],
            "Overwriting display token for display type %d", type);
    mBuiltinDisplays[type] = new BBinder();
    DisplayDeviceState info(type);
    // All non-virtual displays are currently considered secure.
    info.isSecure = true;
    mCurrentState.displays.add(mBuiltinDisplays[type], info);
}

然后会调用createBufferQueue函数创建一个producer和consumer,这个之前分析过。然后又创建了一个FramebufferSurface对象。这里我们看到在新建FramebufferSurface对象时把consumer参数传入了代表是一个消费者。而在DisplayDevice的构造函数中,会创建一个Surface对象传递给底层的OpenGL ES使用,而这个Surface是一个生产者。在OpenGl ES中合成好了图像之后会将图像数据写到Surface对象中,这将触发consumer对象的onFrameAvailable函数被调用:

这就是Surface数据好了就通知消费者来拿数据做显示用,在onFrameAvailable函数汇总,通过nextBuffer获得图像数据,然后调用HWComposer对象mHwc的fbPost函数输出。

void FramebufferSurface::onFrameAvailable(const BufferItem& /* item */) {
    sp<GraphicBuffer> buf;
    sp<Fence> acquireFence;
    status_t err = nextBuffer(buf, acquireFence);
    if (err != NO_ERROR) {
        ALOGE("error latching nnext FramebufferSurface buffer: %s (%d)",
                strerror(-err), err);
        return;
    }
    err = mHwc.fbPost(mDisplayType, acquireFence, buf);
    if (err != NO_ERROR) {
        ALOGE("error posting framebuffer: %d", err);
    }
}

fbPost函数最后通过调用Gralloc模块的post函数来输出图像。

我们再来看看DisplayDevice的构造函数

DisplayDevice::DisplayDevice(
        const sp<SurfaceFlinger>& flinger,
        DisplayType type,
        int32_t hwcId,
        int format,
        bool isSecure,
        const wp<IBinder>& displayToken,
        const sp<DisplaySurface>& displaySurface,
        const sp<IGraphicBufferProducer>& producer,
        EGLConfig config)
    : lastCompositionHadVisibleLayers(false),
      mFlinger(flinger),
      mType(type), mHwcDisplayId(hwcId),
      mDisplayToken(displayToken),
      mDisplaySurface(displaySurface),
      mDisplay(EGL_NO_DISPLAY),
      mSurface(EGL_NO_SURFACE),
      mDisplayWidth(), mDisplayHeight(), mFormat(),
      mFlags(),
      mPageFlipCount(),
      mIsSecure(isSecure),
      mSecureLayerVisible(false),
      mLayerStack(NO_LAYER_STACK),
      mOrientation(),
      mPowerMode(HWC_POWER_MODE_OFF),
      mActiveConfig(0)
{
    mNativeWindow = new Surface(producer, false);//创建Surface对象
    ANativeWindow* const window = mNativeWindow.get();

    /*
     * Create our display's surface
     */

    EGLSurface surface;
    EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
    if (config == EGL_NO_CONFIG) {
        config = RenderEngine::chooseEglConfig(display, format);
    }
    surface = eglCreateWindowSurface(display, config, window, NULL);
    eglQuerySurface(display, surface, EGL_WIDTH,  &mDisplayWidth);
    eglQuerySurface(display, surface, EGL_HEIGHT, &mDisplayHeight);

    // Make sure that composition can never be stalled by a virtual display
    // consumer that isn't processing buffers fast enough. We have to do this
    // in two places:
    // * Here, in case the display is composed entirely by HWC.
    // * In makeCurrent(), using eglSwapInterval. Some EGL drivers set the
    //   window's swap interval in eglMakeCurrent, so they'll override the
    //   interval we set here.
    if (mType >= DisplayDevice::DISPLAY_VIRTUAL)//虚拟设备不支持图像合成
        window->setSwapInterval(window, 0);

    mConfig = config;
    mDisplay = display;
    mSurface = surface;
    mFormat  = format;
    mPageFlipCount = 0;
    mViewport.makeInvalid();
    mFrame.makeInvalid();

    // virtual displays are always considered enabled 虚拟设备屏幕认为是不关闭的
    mPowerMode = (mType >= DisplayDevice::DISPLAY_VIRTUAL) ?
                  HWC_POWER_MODE_NORMAL : HWC_POWER_MODE_OFF;

    // Name the display.  The name will be replaced shortly if the display
    // was created with createDisplay().
    switch (mType) {//显示设备名称
        case DISPLAY_PRIMARY:
            mDisplayName = "Built-in Screen";
            break;
        case DISPLAY_EXTERNAL:
            mDisplayName = "HDMI Screen";
            break;
        default:
            mDisplayName = "Virtual Screen";    // e.g. Overlay #n
            break;
    }

    // initialize the display orientation transform.
    setProjection(DisplayState::eOrientationDefault, mViewport, mFrame);
}

上面构造函数主要功能是创建了一个Surface对象mNativeWindow,同时用它作为参数创建EGLSurface对象,这个EGLSurface对象是OpenGL ES中绘图需要的。

这样,在DisplayDevice中就建立了一个通向Framebuffer的通道,只要向DisplayDevice的mSurface写入数据。就会到消费者FrameBufferSurface的onFrameAvailable函数,然后到HWComposer在到Gralloc模块,最后输出到显示设备。

swapBuffers函数将内部缓冲区的图像数据刷新到显示设备的Framebuffer中,它通过调用eglSwapBuffers函数来完成缓冲区刷新工作。但是注意调用swapBuffers输出图像是在显示设备不支持硬件composer的情况下。

void DisplayDevice::swapBuffers(HWComposer& hwc) const {
    // We need to call eglSwapBuffers() if:
    //  (1) we don't have a hardware composer, or
    //  (2) we did GLES composition this frame, and either
    //    (a) we have framebuffer target support (not present on legacy
    //        devices, where HWComposer::commit() handles things); or
    //    (b) this is a virtual display
    if (hwc.initCheck() != NO_ERROR ||
            (hwc.hasGlesComposition(mHwcDisplayId) &&
             (hwc.supportsFramebufferTarget() || mType >= DISPLAY_VIRTUAL))) {
        EGLBoolean success = eglSwapBuffers(mDisplay, mSurface);
        if (!success) {
            EGLint error = eglGetError();
            if (error == EGL_CONTEXT_LOST ||
                    mType == DisplayDevice::DISPLAY_PRIMARY) {
                LOG_ALWAYS_FATAL("eglSwapBuffers(%p, %p) failed with 0x%08x",
                        mDisplay, mSurface, error);
            } else {
                ALOGE("eglSwapBuffers(%p, %p) failed with 0x%08x",
                        mDisplay, mSurface, error);
            }
        }
    }
    else if(hwc.supportsFramebufferTarget() || mType >= DISPLAY_VIRTUAL)
    {
        EGLBoolean success = eglSwapBuffersVIV(mDisplay, mSurface);
        if (!success) {
            EGLint error = eglGetError();
            ALOGE("eglSwapBuffersVIV(%p, %p) failed with 0x%08x",
                        mDisplay, mSurface, error);
        }
    }

    status_t result = mDisplaySurface->advanceFrame();
    if (result != NO_ERROR) {
        ALOGE("[%s] failed pushing new frame to HWC: %d",
                mDisplayName.string(), result);
    }
}


四、VSync信号的分发过程

在之前的博客分析过,当VSync信号到来时会调用HWComposer类中的vsync函数

void HWComposer::vsync(int disp, int64_t timestamp) {
    if (uint32_t(disp) < HWC_NUM_PHYSICAL_DISPLAY_TYPES) {
        {
            Mutex::Autolock _l(mLock);

            // There have been reports of HWCs that signal several vsync events
            // with the same timestamp when turning the display off and on. This
            // is a bug in the HWC implementation, but filter the extra events
            // out here so they don't cause havoc downstream.
            if (timestamp == mLastHwVSync[disp]) {
                ALOGW("Ignoring duplicate VSYNC event from HWC (t=%" PRId64 ")",
                        timestamp);
                return;
            }

            mLastHwVSync[disp] = timestamp;
        }

        char tag[16];
        snprintf(tag, sizeof(tag), "HW_VSYNC_%1u", disp);
        ATRACE_INT(tag, ++mVSyncCounts[disp] & 1);

        mEventHandler.onVSyncReceived(disp, timestamp);
    }
}

这个函数主要是调用了mEventHandler.onVSyncReceived函数,让我们先来看下mEventHandler的构造,看HWComposer的构造函数,mEventHandler是传入的参数handler。

HWComposer::HWComposer(
        const sp<SurfaceFlinger>& flinger,
        EventHandler& handler)
    : mFlinger(flinger),
      mFbDev(0), mHwc(0), mNumDisplays(1),
      mCBContext(new cb_context),
      mEventHandler(handler),
      mDebugForceFakeVSync(false)

那么我们就要看新建HWComposer的地方了,是在SurfaceFlinger的init函数中新建的,这handler就是SurfaceFlinger对象。

    mHwc = new HWComposer(this,
            *static_cast<HWComposer::EventHandler *>(this));

因此上面的mEventHandler.onVSyncReceived函数,就是调用了SurfaceFlinger的onVSyncReceived函数

void SurfaceFlinger::onVSyncReceived(int type, nsecs_t timestamp) {
    bool needsHwVsync = false;

    { // Scope for the lock
        Mutex::Autolock _l(mHWVsyncLock);
        if (type == 0 && mPrimaryHWVsyncEnabled) {
            needsHwVsync = mPrimaryDispSync.addResyncSample(timestamp);
        }
    }

    if (needsHwVsync) {
        enableHardwareVsync();
    } else {
        disableHardwareVsync(false);
    }
}


4.1 DispSync类

上面函数我们主要看下DispSync的addResyncSample函数,看这个函数之前先看下DispSync的构造函数,在构造函数中启动了DispSyncThread线程

DispSync::DispSync() :
        mRefreshSkipCount(0),
        mThread(new DispSyncThread()) {

    mThread->run("DispSync", PRIORITY_URGENT_DISPLAY + PRIORITY_MORE_FAVORABLE);

我们再来看addResyncSample函数,将VSync信号的时间戳保存大搜了数组mResyncSamples中。然后调用了updateModelLocked函数继续分发VSync信号。

bool DispSync::addResyncSample(nsecs_t timestamp) {
    Mutex::Autolock lock(mMutex);

    size_t idx = (mFirstResyncSample + mNumResyncSamples) % MAX_RESYNC_SAMPLES;
    mResyncSamples[idx] = timestamp;

    if (mNumResyncSamples < MAX_RESYNC_SAMPLES) {
        mNumResyncSamples++;
    } else {
        mFirstResyncSample = (mFirstResyncSample + 1) % MAX_RESYNC_SAMPLES;
    }

    updateModelLocked();

    if (mNumResyncSamplesSincePresent++ > MAX_RESYNC_SAMPLES_WITHOUT_PRESENT) {
        resetErrorLocked();
    }

    if (kIgnorePresentFences) {
        // If we don't have the sync framework we will never have
        // addPresentFence called.  This means we have no way to know whether
        // or not we're synchronized with the HW vsyncs, so we just request
        // that the HW vsync events be turned on whenever we need to generate
        // SW vsync events.
        return mThread->hasAnyEventListeners();
    }

    return mPeriod == 0 || mError > kErrorThreshold;
}

updateModelLocked主要显示利用数组mResyncSamples中的值计算mPeriod和mPhase这两个时间值。然后最后调用了mThread的updateModel函数。mThread是DispSyncThread类。

void DispSync::updateModelLocked() {
    if (mNumResyncSamples >= MIN_RESYNC_SAMPLES_FOR_UPDATE) {
        ......
        //计算mPeriod和mPhase

        mThread->updateModel(mPeriod, mPhase);
    }
}

我们来看下DispSyncThread的updateModel函数,这个函数只是保存了参数,然后调用了Condition的signal唤醒线程。

    void updateModel(nsecs_t period, nsecs_t phase) {
        Mutex::Autolock lock(mMutex);
        mPeriod = period;
        mPhase = phase;
        mCond.signal();
    }

我们再来看DispSyncThread的threadLoop函数,主要这个函数比较计算时间来决定是否要发送信号。如果没有信号发送就会在mCond等待,有信号发送前面会在updateModel中调用mCond的singal函数,这里线程就唤醒了。gatherCallbackInvocationsLocked函数获取本次VSync信号的回调函数列表,这些回调函数是通过DispSync类的addEventListener函数加入的。接着就调用fireCallbackInvocations来依次调用列表中所有对象的onDispSyncEvent函数。

    virtual bool threadLoop() {
        ......
        while (true) {
            Vector<CallbackInvocation> callbackInvocations;

            nsecs_t targetTime = 0;

            { // Scope for lock
                ......

                if (now < targetTime) {
                    err = mCond.waitRelative(mMutex, targetTime - now);
                    ......
                }

                now = systemTime(SYSTEM_TIME_MONOTONIC);
                ......

                callbackInvocations = gatherCallbackInvocationsLocked(now);
            }

            if (callbackInvocations.size() > 0) {
                fireCallbackInvocations(callbackInvocations);
            }
        }

        return false;
    }

fireCallbackInvocations函数就是遍历回调列表调用其onDispSyncEvent函数。

    void fireCallbackInvocations(const Vector<CallbackInvocation>& callbacks) {
        for (size_t i = 0; i < callbacks.size(); i++) {
            callbacks[i].mCallback->onDispSyncEvent(callbacks[i].mEventTime);
        }
    }


4.2 EventThread和DispSync的关系

这里我们先不往下分析DispSync遍历调用回调,我们先来看看EventThread的threadLoop函数,这个函数逻辑很简单。调用waitForEvent来获取事件,然后调用每个连接的postEvent来发送Event。

bool EventThread::threadLoop() {
    DisplayEventReceiver::Event event;
    Vector< sp<EventThread::Connection> > signalConnections;
    signalConnections = waitForEvent(&event);//获取Event

    // dispatch events to listeners...
    const size_t count = signalConnections.size();
    for (size_t i=0 ; i<count ; i++) {
        const sp<Connection>& conn(signalConnections[i]);
        // now see if we still need to report this event
        status_t err = conn->postEvent(event);//发送Event
        if (err == -EAGAIN || err == -EWOULDBLOCK) {
            // The destination doesn't accept events anymore, it's probably
            // full. For now, we just drop the events on the floor.
            // FIXME: Note that some events cannot be dropped and would have
            // to be re-sent later.
            // Right-now we don't have the ability to do this.
            ALOGW("EventThread: dropping event (%08x) for connection %p",
                    event.header.type, conn.get());
        } else if (err < 0) {
            // handle any other error on the pipe as fatal. the only
            // reasonable thing to do is to clean-up this connection.
            // The most common error we'll get here is -EPIPE.
            removeDisplayEventConnection(signalConnections[i]);
        }
    }
    return true;
}

我们再来看下waitForEvent函数中下面代码段,timestamp为0表示没有时间,waitForSync为true表示至少有一个客户和EventThread建立了连接。这段代码一旦有客户连接,就调用enableVSyncLocked接收DispSyncSource的VSync信号。如果在接受信号中,所有客户都断开了连接,则调用disableVSyncLocked函数停止接受DispSyncSource对象的信号。

        // Here we figure out if we need to enable or disable vsyncs
        if (timestamp && !waitForVSync) {
            // we received a VSYNC but we have no clients
            // don't report it, and disable VSYNC events
            disableVSyncLocked();
        } else if (!timestamp && waitForVSync) {
            // we have at least one client, so we want vsync enabled
            // (TODO: this function is called right after we finish
            // notifying clients of a vsync, so this call will be made
            // at the vsync rate, e.g. 60fps.  If we can accurately
            // track the current state we could avoid making this call
            // so often.)
            enableVSyncLocked();
        }

我们先来看下enableVSyncLocked函数

void EventThread::enableVSyncLocked() {
    if (!mUseSoftwareVSync) {
        // never enable h/w VSYNC when screen is off
        if (!mVsyncEnabled) {
            mVsyncEnabled = true;
            mVSyncSource->setCallback(static_cast<VSyncSource::Callback*>(this));
            mVSyncSource->setVSyncEnabled(true);
        }
    }
    mDebugVsyncEnabled = true;
    sendVsyncHintOnLocked();
}

我们先来看看mVSyncSource->setCallback函数。先要知道这个mVSyncSource是在SurfaceFlinger的init函数中。

    sp<VSyncSource> vsyncSrc = new DispSyncSource(&mPrimaryDispSync,
            vsyncPhaseOffsetNs, true, "app");
    mEventThread = new EventThread(vsyncSrc);
    sp<VSyncSource> sfVsyncSrc = new DispSyncSource(&mPrimaryDispSync,
            sfVsyncPhaseOffsetNs, true, "sf");
    mSFEventThread = new EventThread(sfVsyncSrc);
    mEventQueue.setEventThread(mSFEventThread);

看到上面这段代码,我们知道这个mVsyncSource是DispSyncSource类,我们先来看起setCallback函数,就是把callback保存起来

    virtual void setCallback(const sp<VSyncSource::Callback>& callback) {
        Mutex::Autolock lock(mCallbackMutex);
        mCallback = callback;
    }

再来看setVSyncEnabled函数,这里参数enable是true,就是调用了DispSync的addEventListenter。这里就回到了上一小节了,这里我们就在DispSync中注册了回调了

    virtual void setVSyncEnabled(bool enable) {
        Mutex::Autolock lock(mVsyncMutex);
        if (enable) {
            status_t err = mDispSync->addEventListener(mPhaseOffset,
                    static_cast<DispSync::Callback*>(this));
            if (err != NO_ERROR) {
                ALOGE("error registering vsync callback: %s (%d)",
                        strerror(-err), err);
            }
            //ATRACE_INT(mVsyncOnLabel.string(), 1);
        } else {
            status_t err = mDispSync->removeEventListener(
                    static_cast<DispSync::Callback*>(this));
            if (err != NO_ERROR) {
                ALOGE("error unregistering vsync callback: %s (%d)",
                        strerror(-err), err);
            }
            //ATRACE_INT(mVsyncOnLabel.string(), 0);
        }
        mEnabled = enable;
    }

这样回想上一节在fireCallbackInvocations中遍历所有的回调时,就调用了DispSyncSource类的onDispSyncEvent函数,而这个函数主要是调用了其成员变量mCallback的onVSyncEvent,这个mCallback就是之前在EventThread中的waitForEvent注册的,就是EventThread自己。

    virtual void onDispSyncEvent(nsecs_t when) {
        sp<VSyncSource::Callback> callback;
        {
            Mutex::Autolock lock(mCallbackMutex);
            callback = mCallback;

            if (mTraceVsync) {
                mValue = (mValue + 1) % 2;
                ATRACE_INT(mVsyncEventLabel.string(), mValue);
            }
        }

        if (callback != NULL) {
            callback->onVSyncEvent(when);
        }
    }

因此最后到了EventThread的onVsyncEvent函数,这个函数把数据放在mVSyncEvent数组第一个,然后调用了Condition的broadcast函数。

void EventThread::onVSyncEvent(nsecs_t timestamp) {
    Mutex::Autolock _l(mLock);
    mVSyncEvent[0].header.type = DisplayEventReceiver::DISPLAY_EVENT_VSYNC;
    mVSyncEvent[0].header.id = 0;
    mVSyncEvent[0].header.timestamp = timestamp;
    mVSyncEvent[0].vsync.count++;
    mCondition.broadcast();
}

最后之前在EventThread的threadLoop函数中调用waitForEvent会阻塞,当这里调用Condition的broadcast之后,waitForEvent就唤醒,并且得到了mVsynEvent中的数据。紧接着就是在EventThread中的threadLoop调用conn->postEvent来发送Event。这里是通过BitTube对象中的socket发送到MessageQueue中。这个我们在第二节中分析过了。


4.3 MessageQueue分发VSync信号

我们来回顾下,在MessageQueue中有下面这个函数。这样当EventThread通过BitTube传送数据的话,pollOnce会唤醒epoll_wait然后就到这个cb_eventReceiver这个回调函数

void MessageQueue::setEventThread(const sp<EventThread>& eventThread)
{
    mEventThread = eventThread;
    mEvents = eventThread->createEventConnection();
    mEventTube = mEvents->getDataChannel();
    mLooper->addFd(mEventTube->getFd(), 0, Looper::EVENT_INPUT,
            MessageQueue::cb_eventReceiver, this);
}

cb_eventReceiver就是调用了eventReceiver函数。

int MessageQueue::cb_eventReceiver(int fd, int events, void* data) {
    MessageQueue* queue = reinterpret_cast<MessageQueue *>(data);
    return queue->eventReceiver(fd, events);
}

int MessageQueue::eventReceiver(int /*fd*/, int /*events*/) {
    ssize_t n;
    DisplayEventReceiver::Event buffer[8];
    while ((n = DisplayEventReceiver::getEvents(mEventTube, buffer, 8)) > 0) {
        for (int i=0 ; i<n ; i++) {
            if (buffer[i].header.type == DisplayEventReceiver::DISPLAY_EVENT_VSYNC) {
#if INVALIDATE_ON_VSYNC
                mHandler->dispatchInvalidate();
#else
                mHandler->dispatchRefresh();
#endif
                break;
            }
        }
    }
    return 1;
}

这里我们支持VSync信号就会调用mHandler的dispatchRefresh函数。

void MessageQueue::Handler::dispatchRefresh() {
    if ((android_atomic_or(eventMaskRefresh, &mEventMask) & eventMaskRefresh) == 0) {
        mQueue.mLooper->sendMessage(this, Message(MessageQueue::REFRESH));
    }
}

而在Hander的处理中,最终是调用了SurfaceFlinger的onMessageReceived函数

        case REFRESH:
            android_atomic_and(~eventMaskRefresh, &mEventMask);
            mQueue.mFlinger->onMessageReceived(message.what);
            break;

而在SurfaceFlinger的onMessageReceived函数最终会调用了handleMessageRefresh函数。

void SurfaceFlinger::onMessageReceived(int32_t what) {
    ATRACE_CALL();
    switch (what) {
        ......
        case MessageQueue::REFRESH: {
            handleMessageRefresh();
            break;
        }
    }
}

handleMessageRefresh函数负责刷新系统的显示。这样我们就分析了从底层发送VSync信号最终到达SurfaceFlinger的handleMessageRefresh函数。


4.4 VSync信号分发总结

我们回顾下整个流程,VSync信号从底层产生后,经过几个函数,保存到了SurfaceFlinger的mPrimaryDispSync变量(DisySync类)的数组中,这样设计的目的让底层的调用尽快结束,否则会耽搁下次VSync信号的发送。然后在mPrimaryDispSync变量关联的线程开始分发数组中的VSync信号,分发的过程也调用了几个回调函数,最终结果是放在EventThread对象的数组中。EventThread是转发VSync信号的中心。不但会把VSync信号发给SurfaceFlinger,还会把信号发送到用户进程中去。EventThread的工作比较重,因此SurfaceFlinger中使用了两个EventThread对象来转发VSync信号。确保能及时转发。SurfaceFlinger中的MessageQueue收到Event后,会将Event转化成消息发送,这样最终就能在主线程调用SurfaceFlinger的函数处理VSync信号了。


作者:kc58236582 发表于2016/10/10 10:57:06 原文链接
阅读:29 评论:0 查看评论

Java Collection、List、Map、Set

$
0
0

歌唱与自由


Collection

  Collection是最基本的集合接口,一个Collection代表一组Object,即Collection的元素(Elements)。使程序员操作成批的数据或对象元素极为方便。
  Collection 层次结构 中的根接口。Collection 表示一组对象,这些对象也称为 collection 的元素。一些 collection 允许有重复的元素,而另一些则不允许。一些 collection 是有序的,而另一些则是无序的。JDK 不提供此接口的任何直接 实现:它提供更具体的子接口(如 Set 和 List)实现。此接口通常用来传递 collection,并在需要最大普遍性的地方操作这些 collection。
  这些接口和类有很多对抽象数据类型操作的API,而这是我们常用的且在数据结构中熟知的。例如Map,Set,List等。并且Java用面向对象的设计对这些数据结构和算法进行了封装,这就极大的减化了程序员编程时的负担。
  程序员也可以以这个集合框架为基础,定义更高级别的数据抽象,比如栈、队列和线程安全的集合等,从而满足自己的需要。

Java集合主要分为三种类型:

  • Set(集)
  • List(列表)
  • Map(映射)

注意,这里的 Collection、List、Set和Map都是接口,不是具体的类实现。

常用集合类的继承结构如下:

Collection<--List<--Vector
Collection<--List<--ArrayList
Collection<--List<--LinkedList
Collection<--Set<--HashSet
Collection<--Set<--HashSet<--LinkedHashSet
Collection<--Set<--SortedSet<--TreeSet
Map<--SortedMap<--TreeMap
Map<--HashMap 
public interface List<E> extends Collection<E>

public interface Collection<E> extends Iterable<E>

public interface Iterable<T>
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

1List接口

  有序的 collection(也称为序列)。此接口的用户可以对列表中每个元素的插入位置进行精确地控制。用户可以根据元素的整数索引(在列表中的位置)访问元素,并搜索列表中的元素。
  
  与 set 不同,列表通常允许重复的元素。更确切地讲,列表通常允许满足 e1.equals(e2) 的元素对 e1 和 e2,并且如果列表本身允许 null 元素的话,通常它们允许多个 null 元素。难免有人希望通过在用户尝试插入重复元素时抛出运行时异常的方法来禁止重复的列表,但我们希望这种用法越少越好。
  
  List 接口提供了特殊的迭代器,称为 ListIterator,除了允许 Iterator 接口提供的正常操作外,该迭代器还允许元素插入和替换,以及双向访问。

List list = new ArrayList(); 

这是我们平常经常使用的创建一个新的List的语句, List是接口,ArrayList才是具体的类。

2Set

3Map

List * ArrayList

public class ArrayList<E> 
    extends AbstractList<E> 
    implements List<E>, RandomAccess, Cloneable, Serializable

底层用数组实现的List ,查询效率高,增删效率低、 轻量级、 线程不安全。

   List 接口的大小可变数组的实现。实现了所有可选列表操作,并允许包括 null 在内的所有元素。除了实现 List 接口外,此类还提供一些方法来操作内部用来存储列表的数组的大小。(此类大致上等同于 Vector 类,除了此类是不同步的。)

  size、isEmpty、get、set、iterator 和 listIterator 操作都以固定时间运行。add 操作以分摊的固定时间 运行,也就是说,添加 n 个元素需要 O(n) 时间。其他所有操作都以线性时间运行(大体上讲)。与用于 LinkedList 实现的常数因子相比,此实现的常数因子较低。

  每个 ArrayList 实例都有一个容量。该容量是指用来存储列表元素的数组的大小。它总是至少等于列表的大小。随着向 ArrayList 中不断添加元素,其容量也自动增长。并未指定增长策略的细节,因为这不只是添加元素会带来分摊固定时间开销那样简单。
  
  在添加大量元素前,应用程序可以使用 ensureCapacity 操作来增加 ArrayList 实例的容量。这可以减少递增式再分配的数量。
  
  注意,此实现不是同步的。如果多个线程同时访问一个 ArrayList 实例,而其中至少一个线程从结构上修改了列表,那么它必须 保持外部同步。(结构上的修改是指任何添加或删除一个或多个元素的操作,或者显式调整底层数组的大小;仅仅设置元素的值不是结构上的修改。)这一般通过对自然封装该列表的对象进行同步操作来完成。如果不存在这样的对象,则应该使用 Collections.synchronizedList 方法将该列表“包装”起来。这最好在创建时完成,以防止意外对列表进行不同步的访问:

 List list = Collections.synchronizedList(new ArrayList(...)); 

  此类的 iterator 和 listIterator 方法返回的迭代器是快速失败(faill-fast)的:在创建迭代器之后,除非通过迭代器自身的 remove 或 add 方法从结构上对列表进行修改,否则在任何时间以任何方式对列表进行修改,迭代器都会抛出 ConcurrentModificationException。

ArrayList 常用方法

类型 方法 描述
boolean add(E e) 将指定的元素添加到此列表的尾部。
void add(int index,E element) 将指定的元素插入此列表中的指定位置。
boolean addAll(Collection c) 按照指定 collection 的迭代器所返回的元素顺序,将该 collection 中的所有元素添加到此列表的尾部。
boolean addAll(int index, Collection< ? extends E> c) 从指定的位置开始,将指定 collection 中的所有元素插入到此列表中。
void clear() 移除此列表中的所有元素。
Object clone() 返回此 ArrayList 实例的浅表副本。
boolean contains(Object o) 如果此列表中包含指定的元素,则返回 true。
void ensureCapacity(int minCapacity) 如有必要,增加此 ArrayList 实例的容量,以确保它至少能够容纳最小容量参数所指定的元素数。
E get(int index) 返回此列表中指定位置上的元素。
int indexOf(Object o) 返回此列表中首次出现的指定元素的索引,或如果此列表不包含元素,则返回 -1。
boolean isEmpty() 如果此列表中没有元素,则返回 true
int lastIndexOf(Object o) 返回此列表中最后一次出现的指定元素的索引,或如果此列表不包含索引,则返回 -1。
E remove(int index) 移除此列表中指定位置上的元素。
boolean remove(Object o) 移除此列表中首次出现的指定元素(如果存在)。
protected void removeRange(int fromIndex, int toIndex) 移除列表中索引在 fromIndex(包括)和 toIndex(不包括)之间的所有元素。
E set(int index, E element) 用指定的元素替代此列表中指定位置上的元素。
int size() 返回此列表中的元素数。
Object[] toArray() 按适当顺序(从第一个到最后一个元素)返回包含此列表中所有元素的数组。
< T> T[] toArray(T[] a) 按适当顺序(从第一个到最后一个元素)返回包含此列表中所有元素的数组;返回数组的运行时类型是指定数组的运行时类型。
void trimToSize() 将此 ArrayList 实例的容量调整为列表的当前大小。

import java.util.ArrayList;

public class Arraylist2 {
    public static void main(String[] args) {

        ArrayList<String> list = new ArrayList<>();
        /**
         *  依次添加数据
         */
        list.add("a");
        list.add("b");
        list.add("c");
        list.add("d");      
        System.out.println("顺序添加list:" + list);
        // [a, b, c, d]

        /**
         * 在第X个元素后面添加数据
         * list必须要有足够多的数据,才能在某个元素下添加
         * IndexOutOfBoundsException 数组越界
         */
        list.add(1,"list1");
        System.out.println("在第X个元素后面添加数据list:" + list);
        // [a, list1, b, c, d]

        /**
         * 把一个链表复制到另一个
         */
        ArrayList<String> list2 = new ArrayList<>();
        list2.addAll(list);
        System.out.println("复制整个链表:"+list2);
        // [a, list1, b, c, d]

        /**
         * 把一个链表中的数据添加到另一个链表的第某个元素后
         * IndexOutOfBoundsException 数组越界
         */
        ArrayList<String> list3 = new ArrayList<>();
        list3.add("list3");
        list3.addAll(1,list);
        System.out.println("复制整个链表给特定区域:"+list3);
        // [list3, a, list1, b, c, d]

    }
}

import java.util.ArrayList;

public class Arraylist {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();

        list.add("a");
        list.add("b");
        list.add("c");
        list.add("d");
        list.add("e");
        list.add("f");

        /**
         * 删除某个数据
         */
        list.remove(2);// 把c删除
        System.out.println(list);
        // [a, b, d, e, f]

        /**
         * 按内容删除
         */
        list.remove("d");
        System.out.println(list);
        // [a, b, e, f]

        /**
         * 按照某个链表删除
         * 
         */
        ArrayList<String> list2 = new ArrayList<>();

        list2.add("a");
        list2.add("b");
        list.removeAll(list2);
        System.out.println(list);
        // [e, f]

        /**
         * 清空链表
         */
        list.clear();
        System.out.println(list);
        // []
    }
}

改,查

import java.util.ArrayList;
/**
 * 改,查
 * Title: Arraylist3
 * Description: 
 * Company: 
 * @author Peng
 * @date 下午4:55:55
 */
public class Arraylist3 {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();

        list.add("a");
        list.add("b");
        list.add("c");
        list.add("d");
        list.add("e");
        list.add("f");

        /**
         * 修改指定位置的元素
         * 位置从0开始计算(0、1、2、3...)
         */
        list.set(1, "修改2");
        System.out.println(list);
        // [a, 修改2, c, d, e, f]

        /**
         * 获取指定位置的元素
         */     
        String num = list.get(2);
        System.out.println("第二个元素:"+num);
        // c

    }
}

遍历

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
/**
 * 遍历
 * Title: Arraylist4
 * Description: 
 * Company: 
 * @author Peng
 * @date 上午10:30:17
 */
public class Arraylist4 {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();

        list.add("a");
        list.add("b");
        list.add("c");
        list.add("d");
        list.add("e");
        list.add("f");

        /**
         * ArrayList与数组转换
         */
        Object [] obj = (Object[]) list.toArray();

        for(Object c:obj){
            System.out.printf("%2s",c);
        }

        /**
         * 迭代器遍历ArrayList
         */
        Iterator<String> it = list.iterator();
        while(it.hasNext()){
            String str = (String) it.next();
            System.out.println(str);
        }
        /**
         *  Arrays遍历ArrayList
         */
        System.out.println(Arrays.toString(obj));
        /**
         * foreach遍历ArrayList
         *  
         */
        for(String s:list){
            System.out.println(s);
        }
    }
}

List -LinkedList

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable

底层用双向循环链表链式结构实现的List ,查询效率低,增删效率高
每个节点含有一个数据和下一个节点的引用

  List 接口的链接列表实现。实现所有可选的列表操作,并且允许所有元素(包括 null)。除了实现 List 接口外,LinkedList 类还为在列表的开头及结尾 get、remove 和 insert 元素提供了统一的命名方法。这些操作允许将链接列表用作堆栈、队列或双端队列
  此类实现 Deque 接口,为 add、poll 提供先进先出队列操作,以及其他堆栈和双端队列操作
  注意,此实现不是同步的
  
LinkedList笔记:http://blog.csdn.net/peng_hong_fu/article/details/52637143#t10

作者:Peng_Hong_fu 发表于2016/10/10 11:02:14 原文链接
阅读:26 评论:0 查看评论

Android网络开源库-Retrofit(六)源码浅析-上

$
0
0

Retrofit配置RxJava真的很好用。
希望亲们看的时候,自己也看下代码,这样效果最好了。

1. 初始化过程

一般来说,我们通过Builder去初始化Retrofit。我们看看支持我们配置那些呢。如下图:
这里写图片描述

  • platform 平台信息(java,android,ios)
  • callFactory 进行实际请求的(一般为okhttpclient)
  • baseUrl baseUrl
  • convertFactories 包装器集合,对请求数据和响应数据进行包装的东西
  • adapterFactories 适配器集合,用来配置其他框架使用的,如RxJava
  • callbackExecuter 执行回调的小玩意
  • validateEagerly 是否先进行ServiceMethod的初始化(稍后就介绍ServiceMethod)

我们针对platform和callbackExecuter来说下。

2. Platform 平台信息

Android为例,如图
这里写图片描述

callbackExecuter,就是defaultCallbackExecutor返回值,对应Android平台也就是这个了。这里就不多介绍了哈。

3. Retrofit#create

这个方法中,用动态代理的方式,构建出我们的ApiService。瞟一眼代码。
这里写图片描述

上面提到的validateEagerly,在这里起到作用了,eagerlyValidateMethods方法体中是什么呢?看一下。
这里写图片描述
没错,也是loadServiceMethod,和我们在InvocationHandler中load并没有很么区别,唯一的区别就是先手顺序了。

create方法非常重要的三步骤,就是最下面的三行代码。

  • 构建ServiceMethod
  • 构建OKHttpCall
  • 构建ApiService

ok,我们继续。

4. ServiceMethod

先来看看loadServiceMethod方法。
这里写图片描述
优质代码无处不在!!!ServiceMethod被缓存起来,没有缓存,在初始化。平常没有缓存这种思想,一定要注意了。

我们来看ServiceMethod.Builder的初始化方法干了什么。
这里写图片描述

这里就是获取我们的注解信息

  • getAnnotations 获取方法的注解
  • getGenericParameterTypes 获取参数的类型
  • getParameterAnnotations 获取参数的注解

接下来,就是build方法了。这个方法很重要,也是核心。代码稍长,我们分段来看,啦啦啦。
这里写图片描述

  • 初始化callAdapter
  • 出实处responseType
  • 初始化responseConverter

createCallAdapter,根据method的返回值和注解返回callAdapter
这里写图片描述
会调用Retrofit类的这里。
这里写图片描述
我们添加RxJava的Adapter,用Observable<>的时候,就会返回RxJavaAdapter,当然,有一个默认的Adapter,就是这个DefaultCallAdapterFactory。

responseType,DefaultCallAdapterFactory就是Call<?> 中?的部分,为什么呢?这是由于会使用utils来获取泛型参数类型的关系

同理createResponseConverter,也调用了Retrofit里的方法,返回对应的Converter,当然,这里也有个默认的BuiltInConverters,如果我们添加了GsonConverter的话,就会返回Gson相关的。

然而,这并没有完,接下来会解析methodAnnotations。将都安排哟普那个parseMethodAnnotation方法,我们以解析POST为例。 parseHttpMethodAndPath(“POST”, ((POST) annotation).value(), true);如图:
这里写图片描述

  • 确保url中参数不含{}块,要用@Query来替代,就是说url不能是/xxx?name={xx},这样的。
  • 解析url中查询参数

解析其他标签,也类似,当然,有几个不同,如图:
这里写图片描述

到这里。你以为ServiceMethod.Builder#build完了么?年轻,继续
这里写图片描述
这部分代码是干啥子。当然是解析参数中的注解了。
请留意一下ParameterHandler类,我们稍后会用到

parseParameter,解析了我们的参数的注解,返回ParameterHandler。
这里写图片描述
我们重点看下parseParameterAnnotation方法,这个方法巨长。我们看下Field相关的一段。
这里写图片描述

  • 参数类型是Iterable
  • 参数是Array
  • 其他

三种参数类型对应的代码,唯一的区别就在与Converter。一般来说,我们传的Field为String(以简单的来看。。。),那么最终就会调用下面这行代码(Retrofit.class里面)

return (Converter<T, String>) BuiltInConverters.ToStringConverter.INSTANCE;

,没错,默认的Convert。现在我们回过头来,来看ParameterHandler类。这次我们看他的子类,ParameterHandler.FIeld类。
这里写图片描述

,哈,到这里,这个类只是浮现在我们的眼前,他的关键用处,让我们回到Retrofit#create方法。

ps:到这里,ServiceMethod暂时完结了。

5. Retrofit#create方法剩下的两行代码

OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args);
return serviceMethod.callAdapter.adapt(okHttpCall);

adapt 干了什么呢?默认的是DefaultCallAdapterFactory,(我们平常使用RxJavaCallAdapter),而DefaultCallAdapterFactory.adapt直接返回okhttpCall。我们接下来就会调用OkHttpCall的enqueue方法。到这里,就基本将后续交给OkHttp了。不过,在enqueue方法中会调用createRawCall。

6. OkHttpCall#createRawCall

Request request = serviceMethod.toRequest(args);

亚哈,又转区serviceMethod的。

7. serviceMethod#toRequest

这里写图片描述

看到没。这行代码中,出现了parameterHandlers。并且出现了apply。

我们看看去。还是以Field为
这里写图片描述

apply方法,调用RequestBuilder的addFormField方法,添加一个表单字段,其他同理。
最后,调用build方法,组装完成的Request,然后交给Okhttp来执行,后面的事,就不说了。


如果觉得对您有帮助,点一波关注吧。

作者:qq_21430549 发表于2016/10/10 23:53:02 原文链接
阅读:236 评论:0 查看评论

Android群英传知识点回顾——第六章:Android绘图机制与处理技巧

$
0
0

Android群英传知识点回顾——第六章:Android绘图机制与处理技巧


知识点目录

  • 6.1 屏幕的尺寸信息
    • 6.1.1 屏幕参数
    • 6.1.2 系统屏幕密度
    • 6.1.3 独立像素密度dp
    • 6.1.4 单位转换
  • 6.2 2D绘图基础
  • 6.3 Android XML绘图
    • 6.3.1 Bitmap
    • 6.3.2 Shape
    • 6.3.3 Layer
    • 6.3.4 Selector
  • 6.4 Android绘图技巧
    • 6.4.1 Canvas
    • 6.4.2 Layer图层
  • 6.5 Android图像处理之色彩特效处理
    • 6.5.1 色彩矩阵分析
    • 6.5.2 Android颜色矩阵——ColorMatrix
    • 6.5.3 常用图像颜色矩阵处理效果
    • 6.5.4 像素点分析
    • 6.5.5 常用图像像素点处理效果
  • 6.6 Android图像处理之图形特效处理
    • 6.6.1 Android变形矩阵——Matrix
    • 6.6.2 像素块分析
  • 6.7 Android图像处理之画笔特效处理
    • 6.7.1 PorterDuffXfermode
    • 6.7.2 Shader
    • 6.7.3 PathEffect
  • 6.8 View之孪生兄弟——SurfaceView
    • 6.8.1 SurfaceView与View的区别
    • 6.8.2 SurfaceView的使用
    • 6.8.3 SurfaceView实例

知识点回顾

由于这一章比较难理解,所以大部分知识点都是摘抄原文的,如果是没学习过线性代数的同学,那就难上加难了,不过克服它,是你进阶高级工程师的必经之路,文章比较长,请耐心观看

6.1 屏幕的尺寸信息

无知识点

6.1.1 屏幕参数

  • 屏幕大小:屏幕对角线的长度,通常用寸来度量
  • 分辨率:手机屏幕的像素点个数
  • PPI:每英寸像素,又称DPI,由对角线像素点除以屏幕大小获得

6.1.2 系统屏幕密度

6.1.3 独立像素密度dp

Android系统使用mdpi即密度值为160的屏幕作为标准,在这个屏幕上1px = 1dp

ldpi:mdpi:hdpi:xhdpi:xxhdpi=3:4:6:8:12

6.1.4 单位转换

这里加0.5f的巧用目的是:四舍五入

dp == dip

/**
 * dp、sp转换成px的工具类
 * 
 */
public class DisplayUtils {
    /**
     * 将px值转换为dpi或者dp值,保持尺寸大小不变
     *
     * @param content
     * @param pxValus
     * 
     * @return
     */
    public static int px2dip(Context content, float pxValus) {
        final float scale = content.getResources().getDisplayMetrics().density;
        return (int) (pxValus / scale + 0.5f);
    }

    /**
     * 将dip和dp值转换为px值,保证尺寸大小不变
     *
     * @param content
     * @param dipValus
     * 
     * @return
     */
    public static int dip2px(Context content, float dipValus) {
        final float scale = content.getResources().getDisplayMetrics().density;
        return (int) (dipValus / scale + 0.5f);
    }

    /**
     * 将px值转换为sp值,保证文字大小不变
     *
     * @param content
     * @param pxValus
     * @return
     */
    public static int px2sp(Context content, float pxValus) {
        final float fontScale = content.getResources().getDisplayMetrics().scaledDensity;
        return (int) (pxValus / fontScale + 0.5f);
    }

    /**
     * 将sp值转换为px值,保证文字大小不变
     *
     * @param content
     * @param spValus
     * @return
     */
    public static int sp2px(Context content, float spValus) {
        final float fontScale = content.getResources().getDisplayMetrics().scaledDensity;
        return (int) (spValus / fontScale + 0.5f);
    }
}

系统提供了TypedValue类帮助我们转换

/**
 * dp2px
 */
protected int dp2px(int dp){
    return (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,dp,getResources().getDisplayMetrics());
}

/**
 * sp2px
 */
protected int sp2px(int sp){
    return (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,sp,getResources().getDisplayMetrics());
}

6.2 2D绘图基础

Paint作为一个重要的元素有以下方法:

  • setAntiAlias():设置画笔的锯齿效果
  • setColor():设置画笔的颜色
  • setARGB():设置画笔的A、R、G、B值
  • setAlpha():设置画笔的Alpha值
  • setTextSize():设置字体的尺寸
  • setStyle():设置画笔的风格(空心或实心)
  • setStrokeWidth():设置空心边框的宽度

设置Paint的Style可以画出空心或者实心的矩形:

  • paint.setStyle(Paint.Style.STROKE):空心效果
  • paint.setStyle(Paint.Style.FILL):实心效果

系统通过提供的Canvas对象来提供绘图方法:

  • canvas.drawPoint(x, y, paint):绘制点
  • canvas.drawLine(startX, startY ,endX, endY, paint):绘制直线
  • canvas.drawLines(new float[]{startX1, startY1, endX1, endY1,……,startXn, startYn, endXn, endYn}, paint):绘制多条直线
  • canvas.drawRect(left, top, right, bottom, paint):绘制矩形
  • canvas.drawRoundRect(left, top, right, bottom, radiusX, radiusY, paint):绘制圆角矩形
  • canvas.drawCircle(circleX, circleY, radius, paint):绘制圆
  • canvas.drawOval(left, top, right, bottom, paint):绘制椭圆
  • canvas.drawText(text, startX, startY, paint):绘制文本
  • canvas.drawPosText(text, new float[]{X1,Y1,X2,Y2,……Xn,Yn}, paint):在指定位置绘制文本
  • Path path = new Path();
    path.moveTo(50, 50);
    path.lineTo(100, 100);
    path.lineTo(100, 300);
    path.lineTo(300, 50);
    canvas.drawPath(path, paint):绘制路径

  • paint.setStyle(Paint.Style.STROKE);
    drawArc(left, top, right,bottom, startAngle, sweepAngle, true, paint):绘制扇形
  • paint.setStyle(Paint.Style.STROKE);
    drawArc(left, top, right,bottom, startAngle, sweepAngle, false, paint):绘制弧形
  • paint.setStyle(Paint.Style.FILL);
    drawArc(left, top, right,bottom, startAngle, sweepAngle, true, paint):绘制实心扇形
  • paint.setStyle(Paint.Style.FILL);
    drawArc(left, top, right,bottom, startAngle, sweepAngle, false, paint):绘制实心弧形

6.3 Android XML绘图

无知识点

6.3.1 bitmap

通过这样在XML中使用Bitmap就可以将图片直接转换成了Bitmap在程序中使用

<?xml version="1.0" encoding="utf-8"?>
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
    android:src="@mipmap/ic_launcher">
</bitmap>

6.3.2 Shape

通过Shape可以在XML中绘制各种形状

<?xml version="1.0" encoding="utf-8"?>
<shape
    xmlns:android="http://schemas.android.com/apk/res/android"
    //默认为rectangle
    android:shape=["rectangle" | "oval" | "line" | "ring"] >
    <corners   //当shape="rectangle"时使用
        //半径,会被后面的单个半径属性覆盖,默认为1dp
        android:radius="integer"
        android:topLeftRadius="integer"
        android:topRightRadius="integer"
        android:bottomLeftRadius="integer"
        android:bottomRightRadius="integer" />
    <gradient   //渐变
        android:angle="integer"
        android:centerX="integer"
        android:centerY="integer"
        android:centerColor="integer"
        android:endColor="color"
        android:gradientRadius="integer"
        android:startColor="color"
        android:type=["linear" | "radial" | "sweep"]
        android:useLevel=["true" | "false"] />
    <padding
        android:left="integer"
        android:top="integer"
        android:right="integer"
        android:bottom="integer" />
    <size   //指定大小,一般在imageview配合scaleType属性使用
        android:width="integer"
        android:height="integer" />
    <solid   //填充颜色
        android:color="color" />
    <stroke   //指定边框
        android:width="integer"
        android:color="color"
        //虚线宽度
        android:dashWidth="integer"
        //虚线间隔宽度
        android:dashGap="integer" />
</shape>

下面通过渐变来实现的阴影效果

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">

    <gradient
        android:angle="45"
        android:endColor="#805FBBFF"
        android:startColor="#FF5DA2FF" />

    <padding
        android:bottom="7dp"
        android:left="7dp"
        android:right="7dp"
        android:top="7dp" />

    <corners android:radius="8dp" />

</shape>

6.3.3 Layer

通过Layer会产生图片依次叠加的效果

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <!--图片1-->
    <item android:drawable="@mipmap/ic_launcher"/>

    <!--图片2-->
    <item
        android:bottom="10dp"
        android:top="10dp"
        android:right="10dp"
        android:left="10dp"
        android:drawable="@mipmap/ic_launcher"
        />

</layer-list>

6.3.4 Selector

Selector的作用在于帮助开发者实现静态绘图中的事件反馈,通过给不同的事件设置不同的图像,从而在程序中根据用户输入,返回不同的效果

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- 默认时的背景图片-->
    <item android:drawable="@mipmap/ic_launcher" />
    <!-- 没有焦点时的背景图片-->
    <item android:drawable="@mipmap/ic_launcher" android:state_window_focused="false" />
    <!-- 非触摸模式下获得焦点并单击时的背景图片-->
    <item android:drawable="@mipmap/ic_launcher" android:state_pressed="true" android:state_window_focused="true" />
    <!-- 触摸模式下单击时的背景图片-->
    <item android:drawable="@mipmap/ic_launcher" android:state_focused="false" android:state_pressed="true" />
    <!--选中时的图片背景-->
    <item android:drawable="@mipmap/ic_launcher" android:state_selected="true" />
    <!--获得焦点时的图片背景-->
    <item android:drawable="@mipmap/ic_launcher" android:state_focused="true" />
</selector>

下面这个例子实现了一个具有点击反馈效果的、圆角举证Selector

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_pressed="true">
        <shape android:shape="rectangle">
            <!--填充的颜色-->
            <solid android:color="#33444444" />
            <!--设置按钮的四个角为弧形-->
            <!--android:radius 弧形的半径-->
            <corners android:radius="5dp" />
            <!--padding:Button里面的文字与Button边界的间隔-->
            <padding android:bottom="10dp" android:left="10dp" android:right="10dp" android:top="10dp" />
        </shape>
    </item>

    <item>
        <shape android:shape="rectangle">
            <!--填充的颜色-->
            <solid android:color="#FFFFFF" />
            <!--设置按钮的四个角为弧形-->
            <!--android:radius 弧形的半径-->
            <corners android:radius="5dp" />
            <!--padding:Button里面的文字与Button边界的间隔-->
            <padding android:bottom="10dp" android:left="10dp" android:right="10dp" android:top="10dp" />
        </shape>
    </item>
</selector>

6.4 Android绘图技巧

无知识点

6.4.1 Canvas

Canvas提供了以下几种非常有用的方法:

  • Canvas.save():保存画布,将之前所有已绘制图像保存起来,让后续的操作就好像在一个新的图层上操作一样
  • Canvas.restore():在save()之后绘制的所有图像与save()之前的图像进行合并,可以理解为Photoshop中的合并图层操作
  • Canvas.translate():画布平移
  • Canvas.roate():画布翻转

通过一个实例——仪表盘,来理解这几个方法,将仪表盘分为以下几个元素:

  • 仪表盘:外面的大圆盘
  • 刻度线:包含四个长的刻度线和其他短的刻度线
  • 刻度值:包含长刻度线对应的大的刻度值和其他小的刻度值
  • 指针:中间的指针,一粗一细两根指针

仪表盘……见经典代码案例一

6.4.2 Layer图层

一张复杂的画可以由很多个图层叠加起来,形成一个复杂的图像,使用saveLayer()方法来创建一个图层,图层同样是基于栈的结构进行管理的

Android通过调用saveLayer()方法,saveLayerAlpha()方法将一个图层入栈,使用restore()方法、restoreToCount()方法将一个图层出栈,仿照API Demos里面的一个实例来使用Layer

@Override
protected void onDraw(Canvas canvas) {
    canvas.drawColor(Color.WHITE);
    mPaint.setColor(Color.BLUE);
    canvas.drawCircle(150, 150, 100, mPaint);

    canvas.saveLayerAlpha(0, 0,400,400,127,LAYER_TYPE_NONE);
    mPaint.setColor(Color.RED);
    canvas.drawCircle(200, 200, 100, mPaint);
    canvas.restore();
}
  • 当透明度为127时,即半透明
  • 当透明度为255时,即完全不透明
  • 当透明度为0时,即完全透明

6.5 Android图像处理之色彩特效处理

Bitmap图片都是由点阵和颜色值组成的,所谓点阵就是一个包含像素的矩阵,每一个元素对应着图片的一个像素。而颜色值——ARGB,分别对应透明度、红、绿、蓝这四个通道分量,它们共同决定了每个像素点显示的颜色

6.5.1 色彩矩阵分析

在色彩处理中,我们通常用三个角度描述一张图片:

  • 色调:物体传播的颜色
  • 饱和度:颜色的纯度,从0(灰)-100%(饱和)来进行描述
  • 亮度:颜色的相对明暗程度

而在Android中,系统会使用一个颜色矩阵——ColorMatrix,来处理这些色彩的效果,Android中的颜色矩阵是4X5的数字矩阵,他用来对颜色色彩进行处理,而对于每一个像素点,都有一个颜色分量矩阵来保存ARGB值

根据前面对矩阵A、C的定义,通过矩阵乘法运算法则,可以得到:

矩阵运算的乘法计算过程如下:

我们观察颜色矩阵A

从这个公式可以发现

  • 第一行的abcde用来决定新的颜色值R——红色
  • 第二行的fghij用来决定新的颜色值G——绿色
  • 第三行的kimno用来决定新的颜色值B——蓝色
  • 第四行的pqrst用来决定新的颜色值A——透明度
  • 矩阵A中的第五列——ejot值分别用来决定每个分量中的offset,即偏移量

通过一个小例子来讲解:
首先重新看一下矩阵变换计算公式,以R分量为例,计算过程如下:

R1 = a * R + b* G + c*B+d *A + e

如果让a = 1,b,c,d,e都等于0,那么计算的结果为R1 = R,因此我们可以构建一个矩阵

如果把这个矩阵公式带入R1 = AC,那么根据矩阵的乘法运算法则,可以得到R1 = R。因此,这个矩阵通常是用来作为初始的颜色矩阵来使用,他不会对原有颜色进行任何变化

那么当我们要变换颜色值的时候,通常有两种方法。一个是直接改变颜色的offset,即偏移量的值来修改颜色的分量。另一种方法直接改变对应RGBA值的系数来调整颜色分量的值

从前面的分析中,可以知道要修改R1的值,只要将第五列的值进行修改即可。即改变颜色的偏移量,其它值保存初始矩阵的值,如图:

在上面这个矩阵中,我们修改了R、G所对应的颜色偏移量,那么最后的处理结果就是图像的红色、绿色分别增加了100。而我们知道,红色混合绿色会得到黄色,所以最终的颜色处理结果就是让整个图片的色调偏黄色

如果修改颜色分量中的某个系数值,而其他只依然保存初始矩阵的值,如图:

在上面这个矩阵中,改变了G分量所对应的系数g,这样在矩形运算后G分量会变成以前的两倍,最终效果就是图像的色调更加偏绿

下面通过实例看看如何通过矩阵改变图像的色调、饱和度和亮度:

  • 色调:setRotate(int axis, float degree),第一个参数分别使用0、1、2代表Red、Green、Blue三种颜色,第二参数需要处理的值
ColorMatrix hueMatrix = new ColorMatrix();
hueMatrix.setRotate(0, hue);
hueMatrix.setRotate(1, hue);
hueMatrix.setRotate(2, hue);
  • 饱和度:setSaturation(float sat),参数代表设置饱和度的值
ColorMatrix saturationMatrix = new ColorMatrix();
saturationMatrix.setSaturation(saturation);
  • 亮度:setScale(float rscale,float gscale,float bscale,float ascale),参数分别是红、绿、蓝、透明度的亮度大小值
ColorMatrix lumMatrix = new ColorMatrix();
lumMatrix.setScale(lum, lum, lum, 1);

除了单独使用上面三种方式来进行颜色效果处理之外,还提供了postConcat()方法来将矩阵的作用效果混合,从而叠加处理效果

ColorMatrix imageMatrix = new ColorMatrix();
imageMatrix.postConcat(hueMatrix);
imageMatrix.postConcat(saturationMatrix);
imageMatrix.postConcat(lumMatrix);

通过SeekBar调节色调、饱和度、亮度……见经典代码回顾案例二

6.5.2 Android颜色矩阵——ColorMatrix

模拟4x5的颜色矩阵……见经典代码回顾案例三

6.5.3 常用图像颜色矩阵处理效果

  • 灰色效果

  • 图像反转

  • 怀旧效果

  • 去色效果

  • 高饱和度

6.5.4 像素点分析

在Android中,系统系统提供了Bitmap.getPixels()方法来帮我们提取整个Bitmap中的像素点,并保存在一个数组中:

bitmap.getPixels(pixels, offset, stride, x, y, width, height);

这几个参数的具体含义如下:

  • pixels:接收位图颜色值的数组
  • offset:写入到pixels[]中的第一个像素索引值
  • stride:pixels[]中的行间距
  • x:从位图中读取的第一个像素的x坐标值
  • y:从位图中读取的第一个像素的y坐标值
  • width:从每一行中读取的像素宽度
  • height:读取的行数

通常使用如下代码:

bitmap.getPixels(oldPx, 0, bitmap.getWidth(), 0, 0, width, height);

接下来获取每个像素具体的ARGB值:

color = oldPx[i];
r = Color.red(color);
g = Color.green(color);
b = Color.blue(color);
a = Color.alpha(color);

接下来就是修改像素值,产生新的像素值

r1 = (int) (0.393 * r + 0.769 * g + 0.189 * b);
g1 = (int) (0.349 * r + 0.686 * g + 0.168 * b);
b1 = (int) (0.272 * r + 0.534 * g + 0.131 * b);

newPx[i] = Color.argb(a, r1, b1, g1);

最后使用我们的新像素值

bmp.setPixels(newPx, 0, width, 0, 0, width, height);

6.5.5 常用图像像素点处理效果

底片效果、老照片效果、浮雕效果……见经典代码回顾案例四

6.6 Android图像处理之图形特效处理

无知识点

6.6.1 Android变形矩阵——Matrix

对于图形变换,系统提供了3x3的举证来处理:

与颜色矩阵一样,计算方法通过矩阵乘法:

X1 = a x X +b x Y +c;
Y1 = d x X +e x Y +f;
1 = g x X +h x Y + i;

与颜色矩阵一样,也有一个初始矩阵:

图像的变形处理包含以下四类基本变换:

  • Translate:平移变换
  • Rotate:旋转变换
  • Scale:缩放变换
  • Skew:错切变换

平移变换:即对每个像素点都进行平移变换,通过计算可以发现如下平移公式:

X = X0 + △X;
Y = Y0 + △Y;

旋转变换:通过以下三步骤完成以任意点为旋转中心的旋转变换

  • 将坐标原点平移到O点
  • 使用前面讲的以坐标原点为中心的旋转方法进行旋转变换
  • 将坐标原点还原

缩放变换:缩放变换的效果计算公式如下

x = K1 X x0;
y = K2 X y0;

错切变换:错切变换的效果计算公式如下

x = x0 + k1 + y0
y = k2 x x0 + y0

了解四种图形变换矩阵,可以通过setValues()方法将一个一维数组转换为图形变换矩阵:

private float [] mImageMatrix = new float[9];
Matrix matrix = new Matrix();
matrix.setValues(mImageMatrix);
canvas.drawBitmap(mBitmmap,matrix,null);

Android中Matrix类也帮我们封装好了几个操作方法:

  • matrix.setRotate():旋转变换
  • matrix.setTranslate():平移变换
  • matrix.setScale():缩放变换
  • matrix.setSkew():错切变换
  • pre()和post():提供矩阵的前乘和后乘运算

举个例子说明前乘和后乘的不同运算方式:

  • 先平移到(300, 100)
  • 再旋转45度
  • 最后平移到(200, 200)

如果使用后乘运算,代码如下:

matrix.setRotate(45);
matrix.postTranslate(200, 200);

如果使用前乘运算,代码如下:

matrix.setTranslate(200, 200);
matrix.perRotate(45);

6.6.2 像素块分析

drawBitmapMesh()与操纵像素点来改变色彩的原理类似,只不过是把图像分成了一个个的小块,然后通过改变每一个图像块来修改整个图像:

canvas.drawBitmapMesh(Bitmap bitmap,int meshWidth,int meshHeight,float [] verts,
    int vertOffset,int [] colors,int colorOffset,Paint paint);

参数分析:

  • bitmap:将要扭曲的图像
  • meshWidth:需要的横向网格数目
  • meshHeight:需要的纵向网格数目
  • verts:网格交叉点的坐标数组
  • vertOffset:verts数组中开始跳过的(X,Y)坐标对的数目

飘动的旗子……见经典代码回顾案例五

6.7 Android图像处理之画笔特效处理

之前绘图的时候也提到过画笔的一些方法,这里就不再介绍

6.7.1 PorterDuffXfermode

PorterDuffXfermod设置的是两个图层交集区域的显示方式,dst是先画的图形,而src是后画的图形

以一个圆角图片为例子:

mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test1);
mOut = Bitmap.createBitmap(mBitmap.getWidth(), mBitmap.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(mOut);
mPaint = new Paint();
mPaint.setAntiAlias(true);
canvas.drawRoundRect(0, 0, mBitmap.getWidth(), mBitmap.getHeight(), 80, 80, mPaint);
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
canvas.drawBitmap(mBitmap,0,0,mPaint);

效果图,由于图片过大,只能看出一边角

刮刮卡效果……见经典代码回顾案例六

6.7.2 Shader

Shader又被称为着色器。渲染器,它可以实现渲染,渐变等效果,Android中的Shader包括以下几种:

  • BitmapShader:位图Shader
  • LinearGradient:线性Shader
  • RadialGradient:光束Shader
  • SweepGradient:梯形Shader
  • ComposeShader:混合Shader

其中BitmapShader有三种模式可以选择:

  • CLAMP拉伸:拉伸的是图片最后的那一个像素,不断重复
  • REPEAT重复:横向,纵向不断重复
  • MIRROR镜像:横向不断翻转重复,纵向不断翻转重复

下面看下例子说明,圆形图片:

mBitmap = BitmapFactory.decodeResource(getResources(),R.drawable.nice);
mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP,Shader.TileMode.CLAMP);
mPaint = new Paint();
mPaint.setShader(mBitmapShader);
canvas.drawCircle(500,250,200,mPaint);

效果图

下面把TileMode改为REPEAT:

mBitmap = BitmapFactory.decodeResource(getResources(),R.mipmap.ic_launcher);
mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.REPEAT,Shader.TileMode.REPEAT);
mPaint = new Paint();
mPaint.setShader(mBitmapShader);
canvas.drawCircle(500,250,200,mPaint);

使用LinearGradient:

mPaint = new Paint();
mPaint.setShader(new LinearGradient(0,0,400,400, Color.BLUE,Color.YELLOW, Shader.TileMode.REPEAT));
canvas.drawRect(0,0,400,400,mPaint);

效果图

结合前面的PorterDuffXfermode和刚学的LinearGradient,制作出倒影的效果图片

倒影图片效果……见经典代码回顾案例七

6.7.3 PathEffect

先上一张直观的图:

Android提供的几种绘制PathEffect方式:

  • 没效果
  • CornerPathEffect:拐弯角变得圆滑
  • DiscretePathEffect:线段上会产生许多杂点
  • DashPathEffect:绘制虚线,用一个数据来设置各个点之间的间隔
  • PathDashPathEffect:绘制虚线,可以使用方形点虚线和圆形点虚线
  • ComposePathEffect:可任意组合两种路径(PathEffect)的特性

我们通过一个实例来认识这些效果:

public class PathEffectView extends View{

    private Path mPath;
    private PathEffect [] mEffect = new PathEffect[6];
    private Paint mPaint;

    /**
     * 构造方法
     * @param context
     * @param attrs
     */
    public PathEffectView(Context context, AttributeSet attrs) {
        super(context, attrs);

        init();
    }

    /**
     * 初始化
     */
    private void init() {
        mPaint = new Paint();
        mPath = new Path();
        mPath.moveTo(0,0);
        for (int i = 0; i<= 30;i++){
            mPath.lineTo(i*35,(float)(Math.random()*100));
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        mEffect[0] = null;
        mEffect[1] = new CornerPathEffect(30);
        mEffect[2] = new DiscretePathEffect(3.0F,5.0F);
        mEffect[3] = new DashPathEffect(new float[]{20,10,5,10},0);
        Path path = new Path();
        path.addRect(0,0,8,8,Path.Direction.CCW);
        mEffect[4]= new PathDashPathEffect(path,12,0,PathDashPathEffect.Style.ROTATE);
        mEffect[5] = new ComposePathEffect(mEffect[3],mEffect[1]);
        for (int i = 0; i<mEffect.length;i++){
            mPaint.setPathEffect(mEffect[i]);
            canvas.drawPath(mPath,mPaint);
            canvas.translate(0,200);
        }
    }
}

每绘制一个Path,就将画布平移,从而让各种PathEffect依次绘制出来

效果图

6.8 View之孪生兄弟——SurfaceView

无知识点

6.8.1 SurfaceView与View的区别

View的绘制刷新间隔时间为16ms,如果在16ms内完成你所需要执行的所有操作,那么在用户视觉上,就不会产生卡顿的感觉,否则,就会出现卡顿,所以可以考虑使用SurfaceView来替代View的绘制

通常在Log会看到这样的提示:

Skipped 47 frames! The application may be doing too much work on its main thread

SurfaceView与View的主要区别:

  • View主要适用于主动更新的情况下,而surfaceVicw主要适用于被动更新,例如频繁刷新
  • View在主线程中对画面进行刷新,而surfaceView通常会通过一 个子线程来进行页面的刷新
  • View在绘制时没有使用双缓冲机制,而surfaceVicw在底层实现机制中就已经实现了双缓冲机制

总结一句话就是,如果你的自定义View需要频繁刷新,或者刷新数据处理量比较大,就可以考虑使用SurfaceView替代View

6.8.2 SurfaceView的使用

SurfaceView使用步骤:

  • 创建SurfaceView继承自SurfaceView,并实现两个接口——SurfaceHolder.Callback和Runnable
  • 初始化SurfacHolder对象,并注册SurfaceHolder的回调方法
  • 通过SurfacHolder对象lockCanvas()方法获得Canvas对象进行绘制,并通过unlockCanvasAndPost(mCanvas)方法对画布内容进行提交

整个使用SurfaceView的模板代码:

public class SurfaView extends SurfaceView implements SurfaceHolder.Callback, Runnable {

    //SurfaceHolder
    private SurfaceHolder mHolder;
    //用于绘制的Canvas
    private Canvas mCanvas;
    //子线程标志位
    private boolean mIsDrawing;

    /**
     * 构造方法
     *
     * @param context
     * @param attrs
     */
    public SurfaView(Context context, AttributeSet attrs) {
        super(context, attrs);

        mHolder = getHolder();
        mHolder.addCallback(this);
        setFocusable(true);
        setFocusableInTouchMode(true);
        setKeepScreenOn(true);
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        mIsDrawing = true;
        new Thread(this).start();
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        mIsDrawing = false;
    }

    @Override
    public void run() {
        while (mIsDrawing) {
            draw();
        }
    }

    private void draw() {
        try {
            mCanvas = mHolder.lockCanvas();
        } catch (Exception e) {

        } finally {
            if (mCanvas != null) {
                //提交
                mHolder.unlockCanvasAndPost(mCanvas);
            }
        }
    }
}

唯一注意的是,在绘制中将mHolder.unlockCanvasAndPost(mCanvas)方法放到finally代码块中,保证每次都能将内容提交

6.8.3 SurfaceView实例

正弦曲线……见经典代码回顾案例八
绘图板……见经典代码回顾案例九


经典代码回顾

案例一:仪表盘

public class Clock extends View {

    private int mHeight, mWidth;

    public Clock(Context context) {
        super(context);
    }

    public Clock(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public Clock(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        // 获取宽高参数
        mWidth = getMeasuredWidth();
        mHeight = getMeasuredHeight();
        // 画外圆
        Paint paintCircle = new Paint();
        paintCircle.setStyle(Paint.Style.STROKE);
        paintCircle.setAntiAlias(true);
        paintCircle.setStrokeWidth(5);
        canvas.drawCircle(mWidth / 2,
                mHeight / 2, mWidth / 2, paintCircle);
        // 画刻度
        Paint painDegree = new Paint();
        paintCircle.setStrokeWidth(3);
        for (int i = 0; i < 24; i++) {
            // 区分整点与非整点
            if (i == 0 || i == 6 || i == 12 || i == 18) {
                painDegree.setStrokeWidth(5);
                painDegree.setTextSize(30);
                canvas.drawLine(mWidth / 2, mHeight / 2 - mWidth / 2,
                        mWidth / 2, mHeight / 2 - mWidth / 2 + 60,
                        painDegree);
                String degree = String.valueOf(i);
                canvas.drawText(degree,
                        mWidth / 2 - painDegree.measureText(degree) / 2,
                        mHeight / 2 - mWidth / 2 + 90,
                        painDegree);
            } else {
                painDegree.setStrokeWidth(3);
                painDegree.setTextSize(15);
                canvas.drawLine(mWidth / 2, mHeight / 2 - mWidth / 2,
                        mWidth / 2, mHeight / 2 - mWidth / 2 + 30,
                        painDegree);
                String degree = String.valueOf(i);
                canvas.drawText(degree,
                        mWidth / 2 - painDegree.measureText(degree) / 2,
                        mHeight / 2 - mWidth / 2 + 60,
                        painDegree);
            }
            // 通过旋转画布简化坐标运算
            canvas.rotate(15, mWidth / 2, mHeight / 2);
        }
        // 画圆心
        Paint paintPointer = new Paint();
        paintPointer.setStrokeWidth(30);
        canvas.drawPoint(mWidth / 2, mHeight / 2, paintPointer);
        // 画指针
        Paint paintHour = new Paint();
        paintHour.setStrokeWidth(20);
        Paint paintMinute = new Paint();
        paintMinute.setStrokeWidth(10);
        canvas.save();
        canvas.translate(mWidth / 2, mHeight / 2);
        canvas.drawLine(0, 0, 100, 100, paintHour);
        canvas.drawLine(0, 0, 100, 200, paintMinute);
        canvas.restore();
    }
}

效果图


案例二:通过SeekBar调节色调、饱和度、亮度

布局文件

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <ImageView
        android:id="@+id/imageview"
        android:layout_width="300dp"
        android:layout_height="300dp"
        android:layout_centerHorizontal="true"
        android:layout_marginBottom="24dp"
        android:layout_marginTop="24dp" />

    <SeekBar
        android:id="@+id/seekbarHue"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/imageview" />

    <SeekBar
        android:id="@+id/seekbarSaturation"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/seekbarHue" />

    <SeekBar
        android:id="@+id/seekbatLum"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/seekbarSaturation" />

</RelativeLayout>

Activity文件

public class PrimaryColor extends Activity implements SeekBar.OnSeekBarChangeListener {

    private static int MAX_VALUE = 255;
    private static int MID_VALUE = 127;
    private ImageView mImageView;
    private SeekBar mSeekbarhue, mSeekbarSaturation, mSeekbarLum;
    private float mHue, mStauration, mLum;
    private Bitmap bitmap;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.primary_color);
        bitmap = BitmapFactory.decodeResource(getResources(),
                R.drawable.test3);
        mImageView = (ImageView) findViewById(R.id.imageview);
        mSeekbarhue = (SeekBar) findViewById(R.id.seekbarHue);
        mSeekbarSaturation = (SeekBar) findViewById(R.id.seekbarSaturation);
        mSeekbarLum = (SeekBar) findViewById(R.id.seekbatLum);
        mSeekbarhue.setOnSeekBarChangeListener(this);
        mSeekbarSaturation.setOnSeekBarChangeListener(this);
        mSeekbarLum.setOnSeekBarChangeListener(this);
        mSeekbarhue.setMax(MAX_VALUE);
        mSeekbarSaturation.setMax(MAX_VALUE);
        mSeekbarLum.setMax(MAX_VALUE);
        mSeekbarhue.setProgress(MID_VALUE);
        mSeekbarSaturation.setProgress(MID_VALUE);
        mSeekbarLum.setProgress(MID_VALUE);
        mImageView.setImageBitmap(bitmap);
    }

    @Override
    public void onProgressChanged(SeekBar seekBar,
                                  int progress, boolean fromUser) {
        switch (seekBar.getId()) {
            case R.id.seekbarHue:
                mHue = (progress - MID_VALUE) * 1.0F / MID_VALUE * 180;
                break;
            case R.id.seekbarSaturation:
                mStauration = progress * 1.0F / MID_VALUE;
                break;
            case R.id.seekbatLum:
                mLum = progress * 1.0F / MID_VALUE;
                break;
        }
        mImageView.setImageBitmap(ImageHelper.handleImageEffect(
                bitmap, mHue, mStauration, mLum));
    }

    @Override
    public void onStartTrackingTouch(SeekBar seekBar) {
    }

    @Override
    public void onStopTrackingTouch(SeekBar seekBar) {
    }
}
public class ImageHelper {

    public static Bitmap handleImageEffect(Bitmap bm,
                                           float hue,
                                           float saturation,
                                           float lum) {
        Bitmap bmp = Bitmap.createBitmap(
                bm.getWidth(), bm.getHeight(), Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bmp);
        Paint paint = new Paint();

        ColorMatrix hueMatrix = new ColorMatrix();
        hueMatrix.setRotate(0, hue);
        hueMatrix.setRotate(1, hue);
        hueMatrix.setRotate(2, hue);

        ColorMatrix saturationMatrix = new ColorMatrix();
        saturationMatrix.setSaturation(saturation);

        ColorMatrix lumMatrix = new ColorMatrix();
        lumMatrix.setScale(lum, lum, lum, 1);

        ColorMatrix imageMatrix = new ColorMatrix();
        imageMatrix.postConcat(hueMatrix);
        imageMatrix.postConcat(saturationMatrix);
        imageMatrix.postConcat(lumMatrix);

        paint.setColorFilter(new ColorMatrixColorFilter(imageMatrix));
        canvas.drawBitmap(bm, 0, 0, paint);
        return bmp;
    }

}

效果图


案例三:模拟4x5的颜色矩阵

布局文件

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <ImageView
        android:id="@+id/imageview"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="2" />

    <GridLayout
        android:id="@+id/group"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="3"
        android:columnCount="5"
        android:rowCount="4">

    </GridLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <Button
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:onClick="btnChange"
            android:text="Change" />

        <Button
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:onClick="btnReset"
            android:text="Reset" />
    </LinearLayout>

</LinearLayout>

Activity文件

public class ColorMatrix extends Activity {

    private ImageView mImageView;
    private GridLayout mGroup;
    private Bitmap bitmap;
    private int mEtWidth, mEtHeight;
    private EditText[] mEts = new EditText[20];
    private float[] mColorMatrix = new float[20];

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.color_matrix);
        bitmap = BitmapFactory.decodeResource(getResources(),
                R.drawable.test1);
        mImageView = (ImageView) findViewById(R.id.imageview);
        mGroup = (GridLayout) findViewById(R.id.group);
        mImageView.setImageBitmap(bitmap);

        mGroup.post(new Runnable() {
            @Override
            public void run() {
                // 获取宽高信息
                mEtWidth = mGroup.getWidth() / 5;
                mEtHeight = mGroup.getHeight() / 4;
                addEts();
                initMatrix();
            }
        });
    }

    // 获取矩阵值
    private void getMatrix() {
        for (int i = 0; i < 20; i++) {
            mColorMatrix[i] = Float.valueOf(
                    mEts[i].getText().toString());
        }
    }

    // 将矩阵值设置到图像
    private void setImageMatrix() {
        Bitmap bmp = Bitmap.createBitmap(
                bitmap.getWidth(),
                bitmap.getHeight(),
                Bitmap.Config.ARGB_8888);
        android.graphics.ColorMatrix colorMatrix =
                new android.graphics.ColorMatrix();
        colorMatrix.set(mColorMatrix);

        Canvas canvas = new Canvas(bmp);
        Paint paint = new Paint();
        paint.setColorFilter(new ColorMatrixColorFilter(colorMatrix));
        canvas.drawBitmap(bitmap, 0, 0, paint);
        mImageView.setImageBitmap(bmp);
    }

    // 作用矩阵效果
    public void btnChange(View view) {
        getMatrix();
        setImageMatrix();
    }

    // 重置矩阵效果
    public void btnReset(View view) {
        initMatrix();
        getMatrix();
        setImageMatrix();
    }

    // 添加EditText
    private void addEts() {
        for (int i = 0; i < 20; i++) {
            EditText editText = new EditText(ColorMatrix.this);
            mEts[i] = editText;
            mGroup.addView(editText, mEtWidth, mEtHeight);
        }
    }

    // 初始化颜色矩阵为初始状态
    private void initMatrix() {
        for (int i = 0; i < 20; i++) {
            if (i % 6 == 0) {
                mEts[i].setText(String.valueOf(1));
            } else {
                mEts[i].setText(String.valueOf(0));
            }
        }
    }
}

关键点:将一个颜色矩阵传入画笔,然后画出原始的图在新建的图上面

paint.setColorFilter(new ColorMatrixColorFilter(colorMatrix));

效果图


案例四:底片效果、老照片效果、浮雕效果

布局文件

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1">

        <ImageView
            android:id="@+id/imageview1"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1" />

        <ImageView
            android:id="@+id/imageview2"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1" />
    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1">

        <ImageView
            android:id="@+id/imageview3"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1" />

        <ImageView
            android:id="@+id/imageview4"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1" />
    </LinearLayout>

</LinearLayout>

Activity文件

public class PixelsEffect extends Activity {

    private ImageView imageView1, imageView2, imageView3, imageView4;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.pixels_effect);
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test2);
        imageView1 = (ImageView) findViewById(R.id.imageview1);
        imageView2 = (ImageView) findViewById(R.id.imageview2);
        imageView3 = (ImageView) findViewById(R.id.imageview3);
        imageView4 = (ImageView) findViewById(R.id.imageview4);

        imageView1.setImageBitmap(bitmap);
        imageView2.setImageBitmap(ImageHelper.handleImageNegative(bitmap));
        imageView3.setImageBitmap(ImageHelper.handleImagePixelsOldPhoto(bitmap));
        imageView4.setImageBitmap(ImageHelper.handleImagePixelsRelief(bitmap));
    }
}

工具类

public class ImageHelper {

    public static Bitmap handleImageNegative(Bitmap bm) {
        int width = bm.getWidth();
        int height = bm.getHeight();
        int color;
        int r, g, b, a;

        Bitmap bmp = Bitmap.createBitmap(width, height
                , Bitmap.Config.ARGB_8888);

        int[] oldPx = new int[width * height];
        int[] newPx = new int[width * height];
        bm.getPixels(oldPx, 0, width, 0, 0, width, height);

        for (int i = 0; i < width * height; i++) {
            color = oldPx[i];
            r = Color.red(color);
            g = Color.green(color);
            b = Color.blue(color);
            a = Color.alpha(color);

            r = 255 - r;
            g = 255 - g;
            b = 255 - b;

            if (r > 255) {
                r = 255;
            } else if (r < 0) {
                r = 0;
            }
            if (g > 255) {
                g = 255;
            } else if (g < 0) {
                g = 0;
            }
            if (b > 255) {
                b = 255;
            } else if (b < 0) {
                b = 0;
            }
            newPx[i] = Color.argb(a, r, g, b);
        }
        bmp.setPixels(newPx, 0, width, 0, 0, width, height);
        return bmp;
    }

    public static Bitmap handleImagePixelsOldPhoto(Bitmap bm) {
        Bitmap bmp = Bitmap.createBitmap(bm.getWidth(), bm.getHeight(),
                Bitmap.Config.ARGB_8888);
        int width = bm.getWidth();
        int height = bm.getHeight();
        int color = 0;
        int r, g, b, a, r1, g1, b1;

        int[] oldPx = new int[width * height];
        int[] newPx = new int[width * height];

        bm.getPixels(oldPx, 0, bm.getWidth(), 0, 0, width, height);
        for (int i = 0; i < width * height; i++) {
            color = oldPx[i];
            a = Color.alpha(color);
            r = Color.red(color);
            g = Color.green(color);
            b = Color.blue(color);

            r1 = (int) (0.393 * r + 0.769 * g + 0.189 * b);
            g1 = (int) (0.349 * r + 0.686 * g + 0.168 * b);
            b1 = (int) (0.272 * r + 0.534 * g + 0.131 * b);

            if (r1 > 255) {
                r1 = 255;
            }
            if (g1 > 255) {
                g1 = 255;
            }
            if (b1 > 255) {
                b1 = 255;
            }

            newPx[i] = Color.argb(a, r1, g1, b1);
        }
        bmp.setPixels(newPx, 0, width, 0, 0, width, height);
        return bmp;
    }

    public static Bitmap handleImagePixelsRelief(Bitmap bm) {
        Bitmap bmp = Bitmap.createBitmap(bm.getWidth(), bm.getHeight(),
                Bitmap.Config.ARGB_8888);
        int width = bm.getWidth();
        int height = bm.getHeight();
        int color = 0, colorBefore = 0;
        int a, r, g, b;
        int r1, g1, b1;

        int[] oldPx = new int[width * height];
        int[] newPx = new int[width * height];

        bm.getPixels(oldPx, 0, bm.getWidth(), 0, 0, width, height);
        for (int i = 1; i < width * height; i++) {
            colorBefore = oldPx[i - 1];
            a = Color.alpha(colorBefore);
            r = Color.red(colorBefore);
            g = Color.green(colorBefore);
            b = Color.blue(colorBefore);

            color = oldPx[i];
            r1 = Color.red(color);
            g1 = Color.green(color);
            b1 = Color.blue(color);

            r = (r - r1 + 127);
            g = (g - g1 + 127);
            b = (b - b1 + 127);
            if (r > 255) {
                r = 255;
            }
            if (g > 255) {
                g = 255;
            }
            if (b > 255) {
                b = 255;
            }
            newPx[i] = Color.argb(a, r, g, b);
        }
        bmp.setPixels(newPx, 0, width, 0, 0, width, height);
        return bmp;
    }
}

效果图


案例五:飘动的旗子

本人也是懵逼,没有搞懂这个例子

public class FlagBitmapMeshView extends View {

    private final int WIDTH = 200;
    private final int HEIGHT = 200;
    private int COUNT = (WIDTH + 1) * (HEIGHT + 1);
    private float[] verts = new float[COUNT * 2];
    private float[] orig = new float[COUNT * 2];
    private Bitmap bitmap;
    private float A;
    private float k = 1;

    public FlagBitmapMeshView(Context context) {
        super(context);
        initView(context);
    }

    public FlagBitmapMeshView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView(context);
    }

    public FlagBitmapMeshView(Context context, AttributeSet attrs,
                              int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView(context);
    }

    private void initView(Context context) {
        setFocusable(true);
        bitmap = BitmapFactory.decodeResource(context.getResources(),
                R.drawable.test);
        float bitmapWidth = bitmap.getWidth();
        float bitmapHeight = bitmap.getHeight();
        int index = 0;
        for (int y = 0; y <= HEIGHT; y++) {
            float fy = bitmapHeight * y / HEIGHT;
            for (int x = 0; x <= WIDTH; x++) {
                float fx = bitmapWidth * x / WIDTH;
                orig[index * 2 + 0] = verts[index * 2 + 0] = fx;
                orig[index * 2 + 1] = verts[index * 2 + 1] = fy + 100;
                index += 1;
            }
        }
        A = 50;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        flagWave();
        k += 0.1F;
        canvas.drawBitmapMesh(bitmap, WIDTH, HEIGHT,
                verts, 0, null, 0, null);
        invalidate();
    }

    private void flagWave() {
        for (int j = 0; j <= HEIGHT; j++) {
            for (int i = 0; i <= WIDTH; i++) {
                verts[(j * (WIDTH + 1) + i) * 2 + 0] += 0;
                float offsetY =
                        (float) Math.sin((float) i / WIDTH * 2 * Math.PI +
                                Math.PI * k);
                verts[(j * (WIDTH + 1) + i) * 2 + 1] =
                        orig[(j * WIDTH + i) * 2 + 1] + offsetY * A;
            }
        }
    }
}

效果图


案例六:刮刮卡效果

public class XfermodeView extends View {

    private Bitmap mBgBitmap, mFgBitmap;
    private Paint mPaint;
    private Canvas mCanvas;
    private Path mPath;

    public XfermodeView(Context context) {
        super(context);
        init();
    }

    public XfermodeView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public XfermodeView(Context context, AttributeSet attrs,
                        int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        mPaint = new Paint();
        mPaint.setAlpha(0);
        mPaint.setXfermode(
                new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeJoin(Paint.Join.ROUND);
        mPaint.setStrokeWidth(50);
        mPaint.setStrokeCap(Paint.Cap.ROUND);
        mPath = new Path();
        mBgBitmap = BitmapFactory.decodeResource(getResources(),
                R.drawable.test);
        mFgBitmap = Bitmap.createBitmap(mBgBitmap.getWidth(),
                mBgBitmap.getHeight(), Bitmap.Config.ARGB_8888);
        mCanvas = new Canvas(mFgBitmap);
        mCanvas.drawColor(Color.GRAY);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mPath.reset();
                mPath.moveTo(event.getX(), event.getY());
                break;
            case MotionEvent.ACTION_MOVE:
                mPath.lineTo(event.getX(), event.getY());
                break;
        }
        mCanvas.drawPath(mPath, mPaint);
        invalidate();
        return true;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawBitmap(mBgBitmap, 0, 0, null);
        canvas.drawBitmap(mFgBitmap, 0, 0, null);
    }
}

效果图


案例七:倒影图片效果

public class ReflectView extends View {
    private Bitmap mSrcBitmap, mRefBitmap;
    private Paint mPaint;
    private PorterDuffXfermode mXfermode;

    public ReflectView(Context context) {
        super(context);
        initRes(context);
    }

    public ReflectView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initRes(context);
    }

    public ReflectView(Context context, AttributeSet attrs,
                       int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initRes(context);
    }

    private void initRes(Context context) {
        mSrcBitmap = BitmapFactory.decodeResource(getResources(),
                R.drawable.test);
        Matrix matrix = new Matrix();
        matrix.setScale(1F, -1F);
        mRefBitmap = Bitmap.createBitmap(mSrcBitmap, 0, 0,
                mSrcBitmap.getWidth(), mSrcBitmap.getHeight(), matrix, true);

        mPaint = new Paint();
        mPaint.setShader(new LinearGradient(0, mSrcBitmap.getHeight(), 0,
                mSrcBitmap.getHeight() + mSrcBitmap.getHeight() / 4,
                0XDD000000, 0X10000000, Shader.TileMode.CLAMP));
        mXfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_IN);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawColor(Color.BLACK);
        canvas.drawBitmap(mSrcBitmap, 0, 0, null);
        canvas.drawBitmap(mRefBitmap, 0, mSrcBitmap.getHeight(), null);
        mPaint.setXfermode(mXfermode);
        // 绘制渐变效果矩形
        canvas.drawRect(0, mSrcBitmap.getHeight(),
                mRefBitmap.getWidth(), mSrcBitmap.getHeight() * 2, mPaint);
        mPaint.setXfermode(null);
    }
}

效果图

案例八:正弦曲线

public class SinView extends SurfaceView
        implements SurfaceHolder.Callback, Runnable {

    private SurfaceHolder mHolder;
    private Canvas mCanvas;
    private boolean mIsDrawing;
    private int x = 0;
    private int y = 0;
    private Path mPath;
    private Paint mPaint;

    public SinView(Context context) {
        super(context);
        initView();
    }

    public SinView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    public SinView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        initView();
    }

    private void initView() {
        mHolder = getHolder();
        mHolder.addCallback(this);
        setFocusable(true);
        setFocusableInTouchMode(true);
        this.setKeepScreenOn(true);
        mPath = new Path();
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(Color.RED);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(10);
        mPaint.setStrokeCap(Paint.Cap.ROUND);
        mPaint.setStrokeJoin(Paint.Join.ROUND);
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        mIsDrawing = true;
        mPath.moveTo(0, 400);
        new Thread(this).start();
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder,
                               int format, int width, int height) {
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        mIsDrawing = false;
    }

    @Override
    public void run() {
        while (mIsDrawing) {
            draw();
            x += 1;
            y = (int) (100*Math.sin(x * 2 * Math.PI / 180) + 400);
            mPath.lineTo(x, y);
        }
    }

    private void draw() {
        try {
            mCanvas = mHolder.lockCanvas();
            // SurfaceView背景
            mCanvas.drawColor(Color.WHITE);
            mCanvas.drawPath(mPath, mPaint);
        } catch (Exception e) {
        } finally {
            if (mCanvas != null)
                mHolder.unlockCanvasAndPost(mCanvas);
        }
    }
}

效果图

案例九:绘图板

public class SimpleDraw extends SurfaceView
        implements SurfaceHolder.Callback, Runnable {

    private SurfaceHolder mHolder;
    private Canvas mCanvas;
    private boolean mIsDrawing;
    private Path mPath;
    private Paint mPaint;

    public SimpleDraw(Context context) {
        super(context);
        initView();
    }

    public SimpleDraw(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    public SimpleDraw(Context context, AttributeSet attrs,
                      int defStyle) {
        super(context, attrs, defStyle);
        initView();
    }

    private void initView() {
        mHolder = getHolder();
        mHolder.addCallback(this);
        setFocusable(true);
        setFocusableInTouchMode(true);
        this.setKeepScreenOn(true);
        mPath = new Path();
        mPaint = new Paint();
        mPaint.setColor(Color.RED);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(20);
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        mIsDrawing = true;
        new Thread(this).start();
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder,
                               int format, int width, int height) {
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        mIsDrawing = false;
    }

    @Override
    public void run() {
        long start = System.currentTimeMillis();
        while (mIsDrawing) {
            draw();
        }
        long end = System.currentTimeMillis();
        // 50 - 100
        if (end - start < 100) {
            try {
                Thread.sleep(100 - (end - start));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private void draw() {
        try {
            mCanvas = mHolder.lockCanvas();
            mCanvas.drawColor(Color.WHITE);
            mCanvas.drawPath(mPath, mPaint);
        } catch (Exception e) {
        } finally {
            if (mCanvas != null)
                mHolder.unlockCanvasAndPost(mCanvas);
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mPath.moveTo(x, y);
                break;
            case MotionEvent.ACTION_MOVE:
                mPath.lineTo(x, y);
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return true;
    }
}

效果图

经典回顾源码下载

github:https://github.com/CSDNHensen/QunYingZhuang

作者:qq_30379689 发表于2016/10/11 0:31:23 原文链接
阅读:134 评论:0 查看评论

Android 增量更新完全解析 是增量不是热修复

$
0
0

本文在我的微信公众号:鸿洋(hongyangAndroid)首发。

转载请标明出处:
http://blog.csdn.net/lmj623565791/article/details/52761658
本文出自:【张鸿洋的博客】

一、概述

最近一直关注热修复的东西,偶尔聊天谈到了增量更新,当然了两个完全不是一个东西。借此找了一些资料,收集整理了一下,本来是不想写博客的,因为主要都是工具的实现,但是昨晚在整理资料的时候,忽然发现,我快要忘了这玩意,又要从头找一圈工具。

So,权当一个记录,也方便以后自己查找。

首先要明确的是,什么是增量更新:

相信大家都见过在应用市场省流量更新软件,一个几百M的软件可能只需要下载一个20M的增量包就能完成更新。那么它是如何做的呢?

就是本篇博客的主题了。

增量更新的流程是:用户手机上安装着某个应用,下载了增量包,手机上的apk和增量包合并形成新的包,然后再次安装(注意这个过程是要重新安装的,当然部分应用市场有root权限你可能感知不到)。

ok,那么把整个流程细化为几个关键点:

  1. 用户手机上提取当前安装应用的apk
  2. 如何利用old.apk和new.apk生成增量文件
  3. 增加文件与1.中的old.apk合并,然后安装

解决了上述3个问题,就ok了。

下面开始解决,首先我们看下增量文件的生成与合并,这个环节可以说是整个流程的核心,也是技术难点,值得开心的是,这个技术难点已经有工具替我们实现了。

二、增量文件的生成与合并

这个其实就是利用工具做二进制的一个diff和patch了。

网址:

下载地址:

对了,本文环境为mac,其他系统如果阻碍,慢慢搜索解决即可。

下载好了,解压,切到对应的目录,然后执行make:

aaa:bsdiff-4.3 zhy$ make
Makefile:13: *** missing separator.  Stop.

恩,你没看错,报错了,这个错误还比较好解决。

解压文件里面有个文件:Makefile,以文本的形式打开,将install:下面的if,endif添加一个缩进。

修改完成是这个样子的:

CFLAGS      +=  -O3 -lbz2

PREFIX      ?=  /usr/local
INSTALL_PROGRAM ?=  ${INSTALL} -c -s -m 555
INSTALL_MAN ?=  ${INSTALL} -c -m 444

all:        bsdiff bspatch
bsdiff:     bsdiff.c
bspatch:    bspatch.c

install:
    ${INSTALL_PROGRAM} bsdiff bspatch ${PREFIX}/bin
    .ifndef WITHOUT_MAN
    ${INSTALL_MAN} bsdiff.1 bspatch.1 ${PREFIX}/man/man1
    .endif

然后,重新执行make:

aaa:bsdiff-4.3 zhy$ make
cc -O3 -lbz2    bsdiff.c   -o bsdiff
cc -O3 -lbz2    bspatch.c   -o bspatch
bspatch.c:39:21: error: unknown type name 'u_char'; did you mean 'char'?
static off_t offtin(u_char *buf)
                    ^~~~~~
                    char

这次比上次好点,这次生成了一个bsdiff,不过在生成bspatch的时候报错了,好在其实我们只需要使用bsdiff,为什么这么说呢?

因为生成增量文件肯定是在服务端,或者是我们本地pc上做的,使用的就是bsdiff这个工具;

另外一个bspatch,合并old.apk和增量文件肯定是在我们应用内部做的。

当然这个问题也是可以解决的,搜索下,很多解决方案,我们这里就不继续在这个上面浪费篇幅了。

我这里提供个下载地址:

https://github.com/hymanAndroid/tools/tree/master/bsdiff-4.3

下载完成,直接make,bsdiff和bspatch都会生成(mac环境下)。

=============神奇的分割线==============

ok,假设到这里,不管你使用何种手段,咱们已经有了bsdiff和bspacth,下面演示下这个工具的使用:

首先我们准备两个apk,old.apk和new.apk,你可以自己随便写个项目,先运行一次拿到生成的apk作为old.apk;然后修改些代码,或者加一些功能,再运行一次生成new.apk;

  • 生成增量文件
./bsdiff old.apk new.apk old-to-new.patch

这样就生成了一个增量文件old-to-new.patch

  • 增量文件和old.apk合并成新的apk
./bspatch old.apk new2.apk old-to-new.patch

这样就生成一个new2.apk

那么怎么证明这个生成的new2.apk和我们的new.apk一模一样呢?

我们可以查看下md5的值,如果两个文件md5值一致,那么几乎可以肯定两个文件时一模一样的(不要跟我较真说什么碰撞可以产生一样的md5的值~~)。

aaa:bsdiff-4.3 zhy$ md5 new.apk 
MD5 (new.apk) = 0900d0d65f49a0cc3b472e14da11bde7
aaa:bsdiff-4.3 zhy$ md5 new2.apk 
MD5 (new2.apk) = 0900d0d65f49a0cc3b472e14da11bde7

可以看到两个文件的md5果然一样~~

恩,假设你不是mac,怎么获取一个文件的md5呢?(自己写代码,下载工具,不要遇到这样的问题,还弹窗我,我会被扣工资的…)

那么到这里我们就已经知道了如何生成增量文件和将patch与旧的文件合并为新的文件。那么我们再次梳理下整个流程:

  1. 服务端已经做好了增量文件(本节完成)
  2. 客户端下载增量文件+提取该应用的apk,使用bspatch合并
  3. 产生的新的apk,调用安装程序

还是蛮清晰的,那么主要是第二点,第二点有两件事,一个是提取应用的apk;一个是使用bspatch合并,那么这个合并肯定是需要native方法和so文件去做的,也就是说我们要自己打个so出来;

三、客户端的行为

(1)提取应用的apk文件

其实提取当前应用的apk非常简单,如下代码:

public class ApkExtract {
    public static String extract(Context context) {
        context = context.getApplicationContext();
        ApplicationInfo applicationInfo = context.getApplicationInfo();
        String apkPath = applicationInfo.sourceDir;
        Log.d("hongyang", apkPath);
        return apkPath;
    }
}

(2)制作bspatch so

首先声明一个类,写个native方法,如下:

public class BsPatch {

    static {
        System.loadLibrary("bsdiff");
    }

    public static native int bspatch(String oldApk, String newApk, String patch);

}

三个参数已经很明确了;

同时别忘了在module的build.gradle下面:

defaultConfig {
    ndk {
        moduleName = 'bsdiff'
    }
}

注意该步骤需要你配置过ndk的环境(下载ndk,设置ndk.dir)~

ok,接下来就是去完成c的代码的编写了;

首先在app/main目录下新建一个文件夹jni,把之前下载的bsdiff中的bspatch.c拷贝进去;

然后按照jni的规则,在里面新建一个方法:

JNIEXPORT jint JNICALL Java_com_zhy_utils_BsPatch_bspatch
        (JNIEnv *env, jclass cls,
         jstring old, jstring new, jstring patch){
    int argc = 4;
    char * argv[argc];
    argv[0] = "bspatch";
    argv[1] = (char*) ((*env)->GetStringUTFChars(env, old, 0));
    argv[2] = (char*) ((*env)->GetStringUTFChars(env, new, 0));
    argv[3] = (char*) ((*env)->GetStringUTFChars(env, patch, 0));


    int ret = patchMethod(argc, argv);

    (*env)->ReleaseStringUTFChars(env, old, argv[1]);
    (*env)->ReleaseStringUTFChars(env, new, argv[2]);
    (*env)->ReleaseStringUTFChars(env, patch, argv[3]);
    return ret;
}

方法名是有规律的,这个规律不用提了吧~~

注意bsdiff.c中并没有patchMethod方法,这个方法实际上是main方法,直接修改为patchMethod即可,觉得复杂没关系,文末有源码。

ok,此时你可以尝试运行,会提示依赖bzlib,其实从文件顶部的include中也能看出来。

既然依赖,那我们就导入吧:

首先下载:

下载完成后,解压:

将其中的.h和.c文件提取出来,然后可以选择连文件夹copy到我们module的app/main/jni下,结果如下:

记得修改bsdiff中的include:

#include "bzip2/bzlib.h"

再次运行;

然后会发现报一堆类似下面的错误:

Error:(70) multiple definition of `main'

提示main方法重复定义了,在出错信息中会给出哪些类中包含main方法,可以选择直接将这些类中的main方法直接删除。

删除以后,就ok了~~

那么到这里,我们就完成了JNI的编写,当然文件是bsdiff提供的c源码。

四、增量更新后安装

上面的操作完成后,最后一步就简单了,首先准备两个apk:

old.apk new.apk

然后制作一个patch,下面代码中的PATCH.patch;

将old.apk安装,然后将new.apk以及PATCH.patch放置到存储卡;

最后在Activity中触发调用:

private void doBspatch() {
    final File destApk = new File(Environment.getExternalStorageDirectory(), "dest.apk");
    final File patch = new File(Environment.getExternalStorageDirectory(), "PATCH.patch");

    //一定要检查文件都存在

    BsPatch.bspatch(ApkExtract.extract(this),
            destApk.getAbsolutePath(),
            patch.getAbsolutePath());

    if (destApk.exists())
        ApkExtract.install(this, destApk.getAbsolutePath());
    }

记得开启读写SDCard权限,记得在代码中校验需要的文件都存在。

install实际就是通过Intent去安装了:

 public static void install(Context context, String apkPath) {
        Intent i = new Intent(Intent.ACTION_VIEW);
        i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        i.setDataAndType(Uri.fromFile(new File(apkPath)),
                "application/vnd.android.package-archive");
        context.startActivity(i);
    }

这里7.0可能会有问题,把路径暴露给别的app了,应该需要FileProvider去实现(未实验,猜测可能有可能)。

大致的效果图如下:

五、总结

如果你只是单纯的要使用该功能,大可以直接将生成的so文件拷入,直接loadLibrary使用即可。

其次,在做增量更新的时候,patch肯定是根据你当前的版本号与最新(或者目标)版本apk,比对下发diff文件,于此同时应该也把目标apk的md5下发,再做完合并后,不要忘记校验下md5;

博客结束,虽然很简单,主要利用工具实现,但是还是建议自己去实现一次,想一次性跑通还是需要一些时间的,可能过程中也会发现一些坑,也能提升自己对JNI的熟练度。

源码:

也可以选择直接使用so


欢迎关注我的微博:
http://weibo.com/u/3165018720


群号: 497438697 ,欢迎入群

微信公众号:hongyangAndroid
(欢迎关注,不要错过每一篇干货,支持投稿)

参考以及相关链接

作者:lmj623565791 发表于2016/10/11 8:45:09 原文链接
阅读:1424 评论:12 查看评论
Viewing all 5930 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>