Android 源码设计模式解析与实战

978-7-115-40671-2
作者: 何红辉 关爱民
译者:
编辑: 张涛

图书目录:

详情

本书从Android源码的角度由浅入深地剖析设计模式的运用,让工程师们把设计与模式重视起来,提升自己的设计能力与代码质量。因此本书适合的读者为初、中、高级Android工程师。另外,设计思想都是相通的,其他领域的工程师也能从中受益。

图书摘要

版权信息

书名:Android 源码设计模式解析与实战

ISBN:978-7-115-40671-2

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

何红辉 : 友盟Android工程师,CSDN博客专家,活跃于国内各大技术社区,热爱开源,热爱技术,热爱分享。Android事件总线开源库(AndroidEventBus)作者。

作者现在是阿里巴巴旗下友盟的高级程序员,CSDN博客专家,在业界很有影响力

本书稿在网站(https://github.com/simple-android-framework-exchange/android_design_patterns_analysis)分享时,得到许多网友的赞许,许多出版社联系出版。

作者会请阿里巴巴集团有影响力的人和业界有影响的如郭林写推荐序

本书专门介绍Android源代码的设计模式,共26章,主要讲解面向对象的六大原则、主流的设计模式以及MVC和MVP模式。主要内容为:优化代码的第一步、开闭原则、里氏替换原则、依赖倒置原则、接口隔离原则、迪米特原则、单例模式、Builder模式、原型模式、工厂方法模式、抽象工厂模式、策略模式、状态模式、责任链模式、解释器模式、命令模式、观察者模式、备忘录模式、迭代器模式、模板方法模式、访问者模式、中介者模式、代理模式、组合模式、适配器模式、装饰模式、享元模式、外观模式、桥接模式,以及MVC的介绍与实战和MVP应用架构模式。每个章节都对某个模式做了深入的分析,并且会对模式相关的技术点进行深入拓展,让读者在掌握模式的同时学习到Android中的一些重要知识,通过实战帮助读者达到学以致用的目的,且能够将模式运用于项目中,开发出高质量的程序。

本书适合的读者为初、中、高级Android工程师,也可以作为大专院校相关师生的学习用书和培训学校的教材。

设计模式本身并不复杂,但是设计模式的出现,却是GOF大师们耗费无数心血,研究成百上千的例子,历经千锤百炼取其精华而得之,所以,它的重要性毋庸置疑。几年前,我曾见过高焕堂老师一本类似书籍的原稿,可惜此书未能出版,心中一直对此遗憾。但今天有幸一窥CSDN社区专家何红辉、关爱民老师精心撰写的这本以Android源码为案例的设计模式解析与实战一书时,激动之情勃然而发。是的,本书的确是国内第一本以Android为平台介绍设计模式的书,并且书中实例还不是简单的Sample,而是作者在自己开发实践中的经历和一些实际使用的精彩代码段,实用性很强。

另外,我觉得《Android源码设计模式解析与实战》中的主人公小民就是那些不断追求技术进步,从而得以不断成长的IT技术人的代表,小民的成长过程基本上反映了我们现在程序员的成长经历,他的成功很值得我们学习和借鉴。

学习设计模式,是程序员自我修炼、提升实力过程中必不可少的一关。读完此书的您或许已是设计模式的熟手,但我个人觉得,程序员的自我修炼远未结束,因为在设计模式之后,更有像Pattern Oriented Software Architecture 5卷本这样的、着眼于更高层次的书籍需要我们认真、刻苦地学习。希望何红辉和关爱民继续努力,将来为广大工程师提供层次更深、味道更丰富的精神食粮。

邓凡平

畅销书《深入理解Android》系列的总策划和主笔,著有《深入理解Android:卷I》《深入理解Android:卷II》和《深入理解Android:Wi-Fi,NFC和GPS卷》。

想写一本Android设计模式的书的念头由来已久,也许是从我开始接触Android开发后就有了,于是很早就在自己的记事本上记录了一些相关学习心得。2014年4月我就在博客上连载了《Android源码分析之设计模式》系列,简单分析Android源码中的一些设计模式。到了2014年年底开始写一些开发框架相关的博客,并且在此期间发布了AndroidEventBus开源库,此后就一直活跃于Github、博客圈。2015年3月,我开始在Github上创建Android源码设计模式分析的开源项目,借助开源力量在一个月之内发布了十多篇Android源码中设计模式分析的文章,一经发布便得到了业界的普遍好评。

这些文章得到了业界的认可,让我又想起了最初出书的念头。原因很简单,Android是一个开源的系统,很多优秀的思想、架构、设计模式必然在它的源码中得以体现,而在开源社区发布的文章还不够深入。从学习“Hello World”开始,我们都是先从学习他人如何做,然后再到学着做,最后经过自己的理解与思考再到自己做,因此,学习这些优秀的实现正是我们每个开发人员成长过程中的重要一步。在学习Android源码的优秀设计之后,我们如何将设计模式运用在Android开发上成了至关重要的问题,正所谓学以致用。因此,设计模式在Android开发中的实战又成了第二个关键。恰好,这两个领域目前都没有相关的书籍,我和关爱民老师就考虑出版这样的一本书籍。一来是通过写书实现自我提升以及对知识的梳理,二来也希望本书能够让更多的Android开发人员了解设计模式,从而提高自己的代码质量。如此一来,也算是尽了我们的绵薄之力。

何红辉

于北京

很多Android源码的实现都有设计模式的影子,对于很多从事Android开发的读者来说,学习Android源码的最大障碍往往是对其设计思想的理解而非源码本身,很多时候我们能看懂一段源码但却不明白其思想,看懂的是这一段源码的实现逻辑而不懂的则是为什么逻辑会是这样,对于开发者来说,知其然却又不知其所以然往往是进阶中最大的阻力。在今年早些时候,何红辉老师找到我说想构思一本关于Android源码设计模式的书,而此时的我刚好在深究Android源码的一些架构设计。市面上有很多关于Android基础方面的书籍,这些书籍帮助很多开发者入门并走向Android的开发殿堂,而对Android内核解析方面的书籍和资料也有不少,我曾经也阅读过很多关于Android内核的书籍,这些书籍对我从了解到深入内核层面有很大的帮助,但是,像一些基于设计和架构方面的Android书籍却很少看到,基于这样一个契机,我俩一拍即合决定写一本这样的书来帮助一些开发者进阶提升。我是一个喜欢分享、喜欢传授且不拘一束的人,借此机会,我想将我的一些经验或方法通过此书分享给大家,希望大家在今后的开发道路上少走一些弯路。

写书是需要付出极大的努力,且远比想象中困难,特别是在开发技术飞速发展的今天想要跟上技术迭代的脚步极不容易,在这里我想先感谢阅读本书的读者们,因为你们的认可才给予我们最大的写作动力;其次是帮我审稿、阅稿,以及在我学习过程中不断指点我的李志豪、常正宇和刘峰前辈,还有人民邮电出版社的张涛编辑,在出版的过程中给予我们很多建议和教导;再次要感谢的是一起写书的搭档何红辉老师,因为他的想法才有本书的诞生,并且在百忙之中还帮助我阅稿并指正错误;最后要感谢的是我的家人和朋友,因为你们的理解才能让我在几个月的时间中集中精力写好本书。

关爱民

于重庆

本书的第一部分是面向对象六大原则,通过具体的示例讲解六大原则的定义与作用,以及遵循这些原则会存在什么问题,遵循这些原则又会得到什么好处。通过第一部分使得读者对于面向接口编程以及OOP的基本原则有一个深入的认识。

第二部分就是本书的核心部分,每个章节分析一个设计模式,每个模式分为如下几个部分:

(1)模式基本介绍,介绍模式的定义、使用场景、UML类图,使读者对相关模式有一个大致的认识;

(2)模式的简单实现,该模式的经典实现,使读者从代码的角度进一步理解相关模式;

(3)Android源码中的模式实现,模式在Android源码中的运用与分析;

(4)深入拓展,进一步了解运用该模式模块的核心机制,使得读者彻底了解Android的一些基本原理;

(5)模式实战,通过一个示例让读者学习到如何将该模式运用于Android开发中;

(6)总结,总结该模式的优、缺点。

每个章节都对某个模式做了深入的分析,并且会对与模式相关的技术点进行深入拓展,让读者在掌握模式的同时学习到Android中的一些重要知识,通过实战则使读者达到学以致用的效果,能够将模式运用于开发当中。

本书的第三部分则是简单介绍了MVC及MVP模式,使读者从更高的应用架构角度看待应用开发,从整体上把握应用的基本结构。

本书涵盖了基本的OOP原则、设计模式的详细介绍、分析、实战,使得读者能够通过设计模式解决一些局部的设计问题,从而达到可扩展、灵活的局部架构。最后的MVC与MVP则从架构的高度帮助读者避免出现臃肿的类型、紧耦合等问题,使得应用真正实现高内聚、低耦合的应用架构。

在行业内很多初、中级工程师甚至高级工程师由于某些原因都还停留在功能实现层面,甚至对设计模式、面向对象知之甚少,因此,很少考虑代码的设计问题。本书从源码的角度由浅入深地剖析设计模式的运用,让工程师们把设计与模式重视起来,提升自己的设计能力与代码质量。因此,本书适合的读者为初、中、高级Android工程师。另外,本书强调的是编程思想,而思想都是相通的,因此,其他开发领域的工程师也能从中受益。

本书并不是一本Android开发或者设计模式的入门书籍,因此,阅读本书时你最好掌握了Android以及设计模式的相关知识。如果还没有,那么《第一行代码》和《Android开发艺术探索》都是Android领域的优秀书籍;对于设计模式而言,《设计模式之禅》和《设计模式:可复用面向对象软件的基础》都是很好的选择。

本书分为3部分,分别为面向对象六大原则、23种设计模式解析、MVC与MVP。其中第二部分的各个章节之间没有相关的联系,因此读者可以从中选择自己感兴趣的章节进行阅读。初、中级工程师建议至少阅读第一部分、第二部分的常用模式以及第三部分,高级工程师可以选择自己感兴趣的部分进行阅读。判定你是否需要阅读某个章节的标准是,当你看到标题时是否对这个知识点了然于心,如果答案是否定的,那么阅读该章节还是很有必要的。当然,通读全书自然是最好的选择。

最后需要说明的一点是,编写任何一本书籍都难免会有一些错误或不准确甚至不正确的地方,我们很乐意听到读者对我们的意见或建议,您可以通过发邮件(邮箱地址:simplecoder.h@gmail.com)的方式向我们反馈,在这里致以我们诚挚的谢意。编辑联系邮箱为:zhangtao@ptpress.com.cn。

编者

任何一本书的诞生都不是某个个人努力的产物,本书也一样。经过数月的笔耕不辍,本书最终得以出版,在这期间有很多人为此付出了自己的辛勤劳动。

首先要感谢我的搭档关爱民老师,他不仅分担了本书一半的写作任务,而且在写作的过程中任劳任怨,挤出一切闲暇时间以保证按时甚至提前完成写作,只为能让本书尽快出版。其次是要感谢张涛编辑,在本书的出版过程中给了我们很多鼓励,并且为本书的出版付出了很多的努力。还要感谢秦汉、潘超群(Joker)两位前辈在百忙之中抽出时间帮助审稿,保证了本书的质量。最后要感谢我的家人,在我写作的时候给我建议并帮我校稿,在整个过程中给予我很大的支持。

何红辉

2015年8月4日于北京

写书是需要付出极大的努力与勇气的,且远比想象中困难,特别是在开发技术飞速发展的今天,想要跟上技术迭代的脚步极不容易。在这里我想先感谢阅读本书的读者们,因为你们的认可才能给予我们最大的写作动力;其次是帮我审稿以及在我学习过程中不断指点我的李志豪、常正宇和刘峰前辈;人民邮电出版社的张涛编辑,在出版的过程中给予我们很多建议和帮助;再次要感谢的是一起写书的搭档何红辉老师,因为他的想法才有了本书的诞生,并且在百忙之中还帮助我阅稿、指正错误;最后要感谢我的家人和朋友,因为你们的理解才能让我在几个月的时间中集中精力写好本书。

关爱民

2015年8月4日于重庆

单一职责原则的英文名称是Single Responsibility Principle,缩写是SRP。SRP的定义是:就一个类而言,应该仅有一个引起它变化的原因。简单来说,一个类中应该是一组相关性很高的函数、数据的封装。就像老师在《设计模式之禅》中说的:“这是一个备受争议却又及其重要的原则。只要你想和别人争执、怄气或者是吵架,这个原则是屡试不爽的”。因为单一职责的划分界限并不是总是那么清晰,很多时候都是需要靠个人经验来界定。当然,最大的问题就是对职责的定义,什么是类的职责,以及怎么划分类的职责。

对于计算机技术,通常只单纯地学习理论知识并不能很好地领会其深意,只有自己动手实践,并在实际运用中发现问题、解决问题、思考问题,才能够将知识吸收到自己的脑海中。下面以我的朋友小民的事情说起。

自从Android系统发布以来,小民就是Android的铁杆粉丝,于是在大学期间一直保持着对Android的关注,并且利用课余时间做些小项目,锻炼自己的实战能力。毕业后,小民如愿地加入了心仪的公司,并且投入到了他热爱的Android应用开发行业中。将爱好、生活、事业融为一体,小民的第一份工作也算是顺风顺水,一切尽在掌握中。

在经历过一周的适应期以及熟悉公司的产品、开发规范之后,小民的开发工作就正式开始了。小民的主管是个工作经验丰富的技术专家,对于小民的工作并不是很满意,尤其小民最薄弱的面向对象设计,而Android开发又是使用Java语言,程序中的抽象、接口、六大原则、23种设计模式等名词把小民弄得晕头转向。小民自己也察觉到了自己的问题所在,于是,小民的主管决定先让小民做一个小项目来锻炼这方面的能力。正所谓养兵千日用兵一时,磨刀不误砍柴工,小民的开发之路才刚刚开始。

在经过一番思考之后,主管挑选了使用范围广、难度也适中的图片加载器(ImageLoader)作为小民的训练项目。既然要训练小民的面向对象设计,那么就必须考虑到可扩展性、灵活性,而检测这一切是否符合需求的最好途径就是开源。用户不断地提出需求、反馈问题,小民的项目需要不断升级以满足用户需求,并且要保证系统的稳定性、灵活性。在主管跟小民说了这一特殊任务之后,小民第一次感到了压力,“生活不容易!”年仅22岁的小民发出了如此深刻的感叹!

挑战总是要面对的,何况是从来不服输的小民。主管的要求很简单,要小民实现图片加载,并且要将图片缓存起来。在分析了需求之后,小民一下就放心下来了,“这么简单,原来我还以为很难呢……”小民胸有成足地喃喃自语。在经历了10分钟的编码之后,小民写下了如下代码:

/**
 * 图片加载类
 */
public class ImageLoader {
  // 图片缓存
  LruCache<String, Bitmap> mImageCache;
  // 线程池,线程数量为CPU的数量
ExecutorService mExecutorService = Executors.newFixedThreadPool (Runtime.   getRuntime().availableProcessors());

  public ImageLoader() {
    initImageCache();
  }

  private void initImageCache() {
    // 计算可使用的最大内存
    final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
    // 取四分之一的可用内存作为缓存
    final int cacheSize = maxMemory / 4;
    mImageCache = new LruCache(cacheSize) {

      @Override
      protected int sizeOf(String key, Bitmap bitmap) {
        return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
      }
    };
  }

  public void displayImage(final String url, final ImageView imageView) {
     imageView.setTag(url);
     mExecutorService.submit(new Runnable() {

    @Override
    public void run() {
        Bitmap bitmap = downloadImage(url);
      if (bitmap == null) {
        return;
        }
      if (imageView.getTag().equals(url)) {
        imageView.setImageBitmap(bitmap);
        }
      mImageCache.put(url, bitmap);
      }
    });
  }

  public  Bitmap downloadImage(String imageUrl) {
     Bitmap bitmap = null;
    try {
      URL url = newURL(imageUrl);
      final HttpURLConnection conn = (HttpURLConnection) url.openConnection();
      bitmap = BitmapFactory.decodeStream(conn.getInputStream());
        conn.disconnect();
    } catch (Exception e) {
        e.printStackTrace();
    }
    return bitmap;
  }
}

并且使用Git软件进行版本控制,将工程托管到Github上,伴随着git push命令的完成,小民的ImageLoader 0.1版本就正式发布了!如此短的时间内就完成了这个任务,而且还是一个开源项目,小民暗暗自喜,并幻想着待会儿被主管称赞。

在小民给主管报告了ImageLoader的发布消息的几分钟之后,主管就把小民叫到了会议室。这下小民纳闷了,怎么夸人还需要到会议室。“小民,你的ImageLoader耦合太严重啦!简直就没有设计可言,更不要说扩展性、灵活性了。所有的功能都写在一个类里怎么行呢,这样随着功能的增多,ImageLoader类会越来越大,代码也越来越复杂,图片加载系统就越来越脆弱……”这简直就是当头棒喝,小民的脑海里已经听不清主管下面说的内容了,只是觉得自己之前没有考虑清楚就匆匆忙忙完成任务,而且把任务想得太简单了。

“你还是把ImageLoader拆分一下,把各个功能独立出来,让它们满足单一职责原则。”主管最后说道。小民是个聪明人,敏锐地捕捉到了单一职责原则这个关键词,他用Google搜索了一些资料之后,总算是对单一职责原则有了一些认识,于是打算对ImageLoader进行一次重构。这次小民不敢过于草率,也是先画了一幅UML图,如图1-1所示。

▲图1-1

ImageLoader代码修改如下所示:

/**
 * 图片加载类
 */
public class ImageLoader {
  // 图片缓存
  ImageCache mImageCache = new ImageCache() ;
  // 线程池,线程数量为CPU的数量
  ExecutorService mExecutorService = Executors.newFixedThreadPool (Runtime.   
  getRuntime().availableProcessors());

        // lk图片缓存
  public void displayImage(final String url, final ImageView imageView) {
    Bitmap bitmap = mImageCache.get(url);
    if (bitmap != null) {
      imageView.setImageBitmap(bitmap);
      return;
    }
    imageView.setTag(url);
    mExecutorService.submit(new Runnable() {

      @Override
      public void run() {
        Bitmap bitmap = downloadImage(url);
        if (bitmap == null) {
          return;
        }
        if (imageView.getTag().equals(url)) {
          imageView.setImageBitmap(bitmap);
        }
        mImageCache.put(url, bitmap);
      }
    });
   }
  public Bitmap downloadImage(String imageUrl) {
     Bitmap bitmap = null;
     try {
      URL url = new URL(imageUrl);
      final HttpURLConnection conn = (HttpURLConnection) url.openConnection();
      bitmap = BitmapFactory.decodeStream(conn.getInputStream());
      conn.disconnect();
     } catch (Exception e) {
      e.printStackTrace();
     }

     return bitmap;
  }
}  

并且添加了一个ImageCache类用于处理图片缓存,具体代码如下:

public class ImageCache {
  // 图片LRU缓存
  LruCache<String, Bitmap>mImageCache;

  public ImageCache() {
    initImageCache();
  }

  private void initImageCache() {
    // 计算可使用的最大内存
    final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
    // 取四分之一的可用内存作为缓存
    final int cacheSize = maxMemory / 4;
    mImageCache = new LruCache<String, Bitmap>(cacheSize) {

      @Override
      protected int sizeOf(String key, Bitmap bitmap) {
         return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
      }
    };
   }

  public void put(String url, Bitmap bitmap) {
    mImageCache.put(url, bitmap) ;
  }

  public Bitmap get(String url) {
    return mImageCache.get(url) ;
  }
}

如图1-1和上述代码所示,小民将ImageLoader一拆为二,ImageLoader只负责图片加载的逻辑,而ImageCache只负责处理图片缓存的逻辑,这样ImageLoader的代码量变少了,职责也清晰了;当与缓存相关的逻辑需要改变时,不需要修改ImageLoader类,而图片加载的逻辑需要修改时也不会影响到缓存处理逻辑。主管在审核了小民的第一次重构之后,对小民的工作给予了表扬,大致意思是结构变得清晰了许多,但是可扩展性还是比较欠缺。虽然没有得到主管的完全肯定,但也是颇有进步,再考虑到自己确实有所收获,小民原本沮丧的心里也略微地好转起来。

从上述的例子中我们能够体会到,单一职责所表达出的用意就是“单一”二字。正如上文所说,如何划分一个类、一个函数的职责,每个人都有自己的看法,这需要根据个人经验、具体的业务逻辑而定。但是,它也有一些基本的指导原则,例如,两个完全不一样的功能就不应该放在一个类中。一个类中应该是一组相关性很高的函数、数据的封装。工程师可以不断地审视自己的代码,根据具体的业务、功能对类进行相应的拆分,这是程序员优化代码迈出的第一步。

开闭原则的英文全称是Open Close Principle,缩写是OCP,它是Java世界里最基础的设计原则,它指导我们如何建立一个稳定的、灵活的系统。开闭原则的定义是:软件中的对象(类、模块、函数等)应该对于扩展是开放的,但是,对于修改是封闭的。在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会将错误引入原本已经经过测试的旧代码中,破坏原有系统。因此,当软件需要变化时,我们应该尽量通过扩展的方式来实现变化,而不是通过修改已有的代码来实现。当然,在现实开发中,只通过继承的方式来升级、维护原有系统只是一个理想化的愿景,因此,在实际的开发过程中,修改原有代码、扩展代码往往是同时存在的。

软件开发过程中,最不会变化的就是变化本身。产品需要不断地升级、维护,没有一个产品从第一版本开发完就再没有变化了,除非在下个版本诞生之前它已经被终止。而产品需要升级,修改原来的代码就可能会引发其他的问题。那么,如何确保原有软件模块的正确性,以及尽量少地影响原有模块,答案就是,尽量遵守本章讲述的开闭原则。

勃兰特·梅耶在1988年出版的《面向对象软件构造》一书中提出这一原则——开闭原则。这一想法认为,程序一旦开发完成,程序中一个类的实现只应该因错误而被修改,新的或者改变的特性应该通过新建不同的类实现,新建的类可以通过继承的方式来重用原类的代码。显然,梅耶的定义提倡实现继承,已存在的实现类对于修改是封闭的,但是新的实现类可以通过覆写父类的接口应对变化。

说了这么多,想必大家还是半懂不懂,还是让我们以一个简单示例说明一下。

在对ImageLoader进行了一次重构之后,小民的这个开源库获得了一些用户,小民第一次感受成功的快乐,对开源的热情也越发高涨起来!通过动手实现一些开源库来深入学习相关技术,不仅能够提升自我,也能更好地将这些技术运用到工作中,从而开发出更稳定、更优秀的应用,这就是小民的真实想法。

小民第一轮重构之后的ImageLoader职责单一、结构清晰,不仅获得了主管的一点肯定,还得到了用户的夸奖,算是个不错的开始。随着用户的增多,有些问题也暴露出来了,小民的缓存系统就是大家“吐槽”最多的地方。通过内存缓存解决了每次从网络加载图片的问题,但是,Android应用的内存很有限,且具有易失性,即当应用重新启动之后,原来已经加载过的图片将会丢失,这样重启之后就需要重新下载!这又会导致加载缓慢、耗费用户流量的问题。小民考虑引入SD卡缓存,这样下载过的图片就会缓存到本地,即使重启应用也不需要重新下载了。小民在和主管讨论了该问题之后就投入了编程中,下面就是小民的代码。

DiskCache.java类,将图片缓存到SD卡中:

public class DiskCache {
  static String cacheDir = "sdcard/cache/";
  // 从缓存中获取图片
  public  Bitmap get(String url) {
    return BitmapFactory.decodeFile(cacheDir + url);
  }

  // 将图片缓存到内存中
  public void put(String url, Bitmap bmp) {
    FileOutputStream fileOutputStream = null;
    try {
      fileOutputStream = new FileOutputStream(cacheDir + url);
      bmp.compress(CompressFormat.PNG, 100, fileOutputStream);
    } catch (FileNotFoundException e) {
         e.printStackTrace();
    } final ly {
      if (fileOutputStream != null) {
        try {
            fileOutputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
      }
    }
  }
}

因为需要将图片缓存到SD卡中,所以,ImageLoader代码有所更新,具体代码如下:

public class ImageLoader {
  // 内存缓存
  ImageCache mImageCache = new ImageCache();
  // SD卡缓存
  DiskCache mDiskCache = new DiskCache();
  // 是否使用SD卡缓存
  boolean isUseDiskCache = false;
  // 线程池,线程数量为CPU的数量
  ExecutorService mExecutorService = Executors.newFixedThreadPool (Runtime.  
  getRuntime().availableProcessors());

  public void displayImage(final String url, final ImageView imageView) {
    // 判断使用哪种缓存
    Bitmap bitmap = isUseDiskCache ? mDiskCache.get(url) : mImageCache.get (url);
    if (bitmap != null) {
      imageView.setImageBitmap(bitmap);
      return;
      }
     // 没有缓存,则提交给线程池进行下载
  }

  public void useDiskCache(boolean useDiskCache) {
    isUseDiskCache = useDiskCache ;
  }
}

从上述的代码中可以看到,仅仅新增了一个DiskCache类和往ImageLoader类中加入了少量代码就添加了SD卡缓存的功能,用户可以通过useDiskCache方法来对使用哪种缓存进行设置,例如:

ImageLoader imageLoader = new ImageLoader() ;
 // 使用SD卡缓存
imageLoader.useDiskCache(true);
// 使用内存缓存
imageLoader.useDiskCache(false);

通过useDiskCache方法可以让用户设置不同的缓存,非常方便啊!小民对此很满意,于是提交给主管做代码审核。“小民,你思路是对的,但是有些明显的问题,就是使用内存缓存时用户就不能使用SD卡缓存。类似地,使用SD卡缓存时用户就不能使用内存缓存。用户需要这两种策略的综合,首先缓存优先使用内存缓存,如果内存缓存没有图片再使用SD卡缓存,如果SD卡中也没有图片最后才从网络上获取,这才是最好的缓存策略。”主管的解释真是一针见血,小民这时才如梦初醒,刚才还得意洋洋的脸上突然有些泛红……

于是小民按照主管的指点新建了一个双缓存类DoubleCache,具体代码如下:

/**
 * 双缓存。获取图片时先从内存缓存中获取,如果内存中没有缓存该图片,再从SD卡中获取。
 * 缓存图片也是在内存和SD卡中都缓存一份
 */
public class DoubleCache {
   ImageCache mMemoryCache = new ImageCache();
   DiskCache mDiskCache = new DiskCache();

  // 先从内存缓存中获取图片,如果没有,再从SD卡中获取
  public  Bitmap get(String url) {
    Bitmap bitmap = mMemoryCache.get(url);
    if (bitmap == null) {
      bitmap = mDiskCache.get(url);
    }
    return bitmap;
  }

  // 将图片缓存到内存和SD卡中
  public void put(String url, Bitmap bmp) {
    mMemoryCache.put(url, bmp);
    mDiskCache.put(url, bmp);
  }
}

我们再看看最新的ImageLoader类,代码更新也不多:

public class ImageLoader {
  // 内存缓存
  ImageCache mImageCache = new ImageCache();
  // SD卡缓存
  DiskCache mDiskCache = new DiskCache();
  // 双缓存
  DoubleCache mDoubleCache = new DoubleCache() ;
  // 使用SD卡缓存
  boolean isUseDiskCache = false;
  // 使用双缓存
  boolean isUseDoubleCache = false;
  // 线程池,线程数量为CPU的数量
  ExecutorService mExecutorService = Executors.newFixedThreadPool 
  (Runtime.getRuntime().availableProcessors());

  public void displayImage(final String url, final ImageView imageView) {
    Bitmap bmp = null;
    if (isUseDoubleCache) {
        bmp = mDoubleCache.get(url);
    } else if (isUseDiskCache) {
        bmp = mDiskCache.get(url);
     } else {
      bmp = mImageCache.get(url);
   }
  if ( bmp != null ) {
    imageView.setImageBitmap(bmp);
  }
  // 没有缓存,则提交给线程池进行异步下载图片
  }

  public void useDiskCache(boolean useDiskCache) {
    isUseDiskCache = useDiskCache ;
  }

  public void useDoubleCache(boolean useDoubleCache) {
    isUseDoubleCache = useDoubleCache ;
  }
}

通过增加短短几行代码和几处修改就完成了如此重要的功能。小民已越发觉得自己Android开发已经到了得心应手的境地,顿时感觉一阵春风袭来,小民感觉今天天空比往常敞亮许多。

“小民,你每次加新的缓存方法时都要修改原来的代码,这样很可能会引入Bug,而且会使原来的代码逻辑变得越来越复杂。按照你这样的方法实现,用户也不能自定义缓存实现呀!”到底是主管水平高,一语道出了小民这缓存设计上的问题。

我们还是来分析一下小民的程序。小民每次在程序中加入新的缓存实现时都需要修改ImageLoader类,然后通过一个布尔变量来让用户选择使用哪种缓存,因此,就使得在ImageLoader中存在各种if-else判断语句,通过这些判断来确定使用哪种缓存。随着这些逻辑的引入,代码变得越来越复杂、脆弱,如果小民一不小心写错了某个if条件(条件太多,这是很容易出现的),那就需要更多的时间来排除,整个ImageLoader类也会变得越来越臃肿。最重要的是,用户不能自己实现缓存注入到ImageLoader中,可扩展性差,可扩展性可是框架的最重要特性之一。

“软件中的对象(类、模块、函数等)应该对于扩展是开放的,但是对于修改是封闭的,这就是开放——关闭原则。也就是说,当软件需要变化时,我们应该尽量通过扩展的方式来实现变化,而不是通过修改已有的代码来实现。”小民的主管补充到,小民听得云里雾里的。主管看小民这等反应,于是亲自“操刀”,为他画下了图1-2所示的UML图。

▲图1-2

小民看到图1-2似乎明白些什么,但又不是很明确如何修改程序。主管看到小民这般模样,只好亲自上阵,带着小民把ImageLoader程序按照图1-2进行了一次重构,具体代码如下:

public class ImageLoader {
  // 图片缓存
  ImageCache mImageCache = new MemoryCache();
  // 线程池,线程数量为CPU的数量
  ExecutorService mExecutorService = Executors.newFixedThreadPool 
  (Runtime.getRuntime().availableProcessors());

  // 注入缓存实现
  public void setImageCache(ImageCache cache) {
    mImageCache = cache;
   }

  public void displayImage(String imageUrl, ImageView imageView) {
  Bitmap bitmap = mImageCache.get(imageUrl);
    if (bitmap != null) {
      imageView.setImageBitmap(bitmap);
      return;
      }
      // 图片没缓存,提交到线程池中下载图片
    submitLoadRequest(imageUrl, imageView);
   }

  private void submitLoadRequest(final String imageUrl,
  final ImageView imageView) {
    imageView.setTag(imageUrl);
    mExecutorService.submit(new Runnable() {

      @Override
      public void run() {
        Bitmap bitmap = downloadImage(imageUrl);
        if (bitmap == null) {
          return;
        }
        if (imageView.getTag().equals(imageUrl)) {
          imageView.setImageBitmap(bitmap);
      }
      mImageCache.put(imageUrl, bitmap);
      }
    });
  }

  public Bitmap downloadImage(String imageUrl) {
    Bitmap bitmap = null;
    try {
      URL url = new URL(imageUrl);
      final HttpURLConnection conn = (HttpURLConnection) url.openConnection();
      bitmap = BitmapFactory.decodeStream(conn.getInputStream());
      conn.disconnect();
    } catch (Exception e) {
    e.printStackTrace();
    }

  return bitmap;
  }
}

经过这次重构,没有了那么多的if-else语句,没有了各种各样的缓存实现对象、布尔变量,代码确实清晰、简单了很多,小民对主管的崇敬之情又“泛滥”了起来。需要注意的是,这里的ImageCache类并不是小民原来的那个ImageCache,这次重构程序,主管把它提取成一个图片缓存的接口,用来抽象图片缓存的功能,我们看看该接口的声明:

public interface ImageCache {
  public  Bitmap get(String url);
  public void put(String url, Bitmap bmp);
}

ImageCache接口简单定义了获取、缓存图片两个函数,缓存的key是图片的url,值是图片本身。内存缓存、SD卡缓存、双缓存都实现了该接口,我们看看这几个缓存实现:

// 内存缓存MemoryCache类
public class MemoryCache implements ImageCache {
  private LruCache<String, Bitmap> mMemeryCache;

  public MemoryCache() {
    // 初始化LRU缓存
  }

   @Override
  public  Bitmap get(String url) {
    return mMemeryCache.get(url);
  }

  @Override
  public void put(String url, Bitmap bmp) {
    mMemeryCache.put(url, bmp);
   }
}

// SD卡缓存DiskCache类
public class DiskCache implements ImageCache {
   @Override
  public  Bitmap get(String url) {
    return null/* 从本地文件中获取该图片 */;
  }

  @Override
  public void put(String url, Bitmap bmp) {
    // 将Bitmap写入文件中
  }
}

// 双缓存DoubleCache类
public class DoubleCache implements ImageCache{
  ImageCache mMemoryCache = new MemoryCache();
  ImageCache mDiskCache = new DiskCache();

  // 先从内存缓存中获取图片,如果没有,再从SD卡中获取
  public Bitmap get(String url) {
    Bitmap bitmap = mMemoryCache.get(url);
    if (bitmap == null) {
      bitmap = mDiskCache.get(url);
    }
    return bitmap;
   }

  // 将图片缓存到内存和SD卡中
  public void put(String url, Bitmap bmp) {
    mMemoryCache.put(url, bmp);
    mDiskCache.put(url, bmp);
  }
}

细心的读者可能注意到了,ImageLoader类中增加了一个etImageCache(ImageCache cache)函数,用户可以通过该函数设置缓存实现,也就是通常说的依赖注入。下面就看看用户是如何设置缓存实现的:

ImageLoader imageLoader = new ImageLoader() ;
    // 使用内存缓存
imageLoader.setImageCache(new MemoryCache());
    // 使用SD卡缓存
imageLoader.setImageCache(new DiskCache());
    // 使用双缓存
imageLoader.setImageCache(new DoubleCache());
    // 使用自定义的图片缓存实现
imageLoader.setImageCache(new ImageCache() {

   @Override
   public void put(String url, Bitmap bmp) {
        // 缓存图片
      }

      @Override
      public Bitmap get(String url) {
        return null/*从缓存中获取图片*/;
      }
    });

在上述代码中,通过setImageCache(ImageCache cache)方法注入不同的缓存实现,这样不仅能够使ImageLoader更简单、健壮,也使得ImageLoader的可扩展性、灵活性更高。MemoryCache、DiskCache、DoubleCache缓存图片的具体实现完全不一样,但是,它们的一个特点是,都实现了ImageCache接口。当用户需要自定义实现缓存策略时,只需要新建一个实现ImageCache接口的类,然后构造该类的对象,并且通过setImageCache(ImageCache cache)注入到ImageLoader中,这样ImageLoader就实现了千变万化的缓存策略,且扩展这些缓存策略并不会导致ImageLoader类的修改。经过这次重构,小民的ImageLoader已经基本算合格了。咦!这不就是主管说的开闭原则么!“软件中的对象(类、模块、函数等)应该对于扩展是开放的,但是,对于修改是封闭的。而遵循开闭原则的重要手段应该是通过抽象……”小民细声细语地念叨着,陷入了思索中……

开闭原则指导我们,当软件需要变化时,应该尽量通过扩展的方式来实现变化,而不是通过修改已有的代码来实现。这里的“应该尽量”4个字说明OCP原则并不是说绝对不可以修改原始类的。当我们嗅到原来的代码“腐化气味”时,应该尽早地重构,以便使代码恢复到正常的“进化”过程,而不是通过继承等方式添加新的实现,这会导致类型的膨胀以及历史遗留代码的冗余。我们的开发过程中也没有那么理想化的状况,完全地不用修改原来的代码,因此,在开发过程中需要自己结合具体情况进行考量,是通过修改旧代码还是通过继承使得软件系统更稳定、更灵活,在保证去除“代码腐化”的同时,也保证原有模块的正确性。

里氏替换原则英文全称是Liskov Substitution Principle,缩写是LSP。LSP的第一种定义是:如果对每一个类型为S的对象O1,都有类型为T的对象O2,使得以T定义的所有程序P在所有的对象O1都代换成O2时,程序P的行为没有发生变化,那么类型S是类型T的子类型。上面这种描述确实不太好理解,我们再看看另一个直截了当的定义。里氏替换原则第二种定义:所有引用基类的地方必须能透明地使用其子类的对象。

我们知道,面向对象的语言的三大特点是继承、封装、多态,里氏替换原则就是依赖于继承、多态这两大特性。里氏替换原则简单来说就是,所有引用基类的地方必须能透明地使用其子类的对象。通俗点讲,只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,使用者可能根本就不需要知道是父类还是子类。但是,反过来就不行了,有子类出现的地方,父类未必就能适应。说了那么多,其实最终总结就两个字:抽象。

小民为了深入地了解Android中的Window与View的关系,特意写了一个简单示例,为了便于理解,我们先看如图1-3所示。

▲图1-3

我们看看具体的代码实现:

// 窗口类
public class Window {
    public void show(View child){
        child.draw();
    }
}

// 建立视图抽象,测量视图的宽高为公用代码,绘制实现交给具体的子类
public abstract class  View {
    public abstract void  draw() ;
    public void measure(int width, int height){
        // 测量视图大小
    }
}

// 按钮类的具体实现
public class Button extends View {
    public void draw(){
        // 绘制按钮
    }
}
// TextView的具体实现
public class TextView extends View {
    public void draw(){
        // 绘制文本
    }
}

上述示例中,Window依赖于View,而View定义了一个视图抽象,measure是各个子类共享的方法,子类通过覆写View的draw方法实现具有各自特色的功能,在这里,这个功能就是绘制自身的内容。任何继承自View类的子类都可以设置给show方法,就是所说的里氏替换。通过里氏替换,就可以自定义各式各样、千变万化的View,然后传递给Window,Window负责组织View,并且将View显示到屏幕上。

里氏替换原则的核心原理是抽象,抽象又依赖于继承这个特性,在OOP当中,继承的优缺点都相当明显。优点有以下几点:

(1)代码重用,减少创建类的成本,每个子类都拥有父类的方法和属性;

(2)子类与父类基本相似,但又与父类有所区别;

(3)提高代码的可扩展性。

继承的缺点:

(1)继承是侵入性的,只要继承就必须拥有父类的所有属性和方法;

(2)可能造成子类代码冗余、灵活性降低,因为子类必须拥有父类的属性和方法。

事物总是具有两面性,如何权衡利与弊都是需要根据具体情况来做出选择并加以处理。里氏替换原则指导我们构建扩展性更好的软件系统,我们还是接着上面的ImageLoader来做说明。

图1-2也很好地反应了里氏替换原则,即MemoryCache、DiskCache、DoubleCache都可以替换ImageCache的工作,并且能够保证行为的正确性。ImageCache建立了获取缓存图片、保存缓存图片的接口规范,MemoryCache等根据接口规范实现了相应的功能,用户只需要在使用时指定具体的缓存对象就可以动态地替换ImageLoader中的缓存策略。这就使得ImageLoader的缓存系统具有了无限的可能性,也就是保证了可扩展性。

想象一种情况,当ImageLoader中的setImageCache(ImageCache cache)中的cache对象不能够被子类所替换,那么用户如何设置不同的缓存对象,以及用户如何自定义自己的缓存实现,通过1.3节中的useDiskCache方法吗?显然不是的,里氏替换原则就为这类问题提供了指导原则,也就是建立抽象,通过抽象建立规范,具体的实现在运行时替换掉抽象,保证系统的扩展性、灵活性。开闭原则和里氏替换原则往往是生死相依、不弃不离的,通过里氏替换来达到对扩展开放,对修改关闭的效果。然而,这两个原则都同时强调了一个OOP的重要特性——抽象,因此,在开发过程中运用抽象是走向代码优化的重要一步。

依赖倒置原则英文全称是Dependence Inversion Principle,缩写是DIP。依赖倒置原则指代了一种特定的解耦形式,使得高层次的模块不依赖于低层次的模块的实现细节的目的,依赖模块被颠倒了。这个概念有点不好理解,这到底是什么意思呢?

依赖倒置原则有以下几个关键点:

(1)高层模块不应该依赖低层模块,两者都应该依赖其抽象;

(2)抽象不应该依赖细节;

(3)细节应该依赖抽象。

在Java语言中,抽象就是指接口或抽象类,两者都是不能直接被实例化的;细节就是实现类,实现接口或继承抽象类而产生的类就是细节,其特点就是,可以直接被实例化,也就是可以加上一个关键字new产生一个对象。高层模块就是调用端,低层模块就是具体实现类。依赖倒置原则在 Java语言中的表现就是:模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的。这又是一个将理论抽象化的实例,其实一句话就可以概括:面向接口编程,或者说是面向抽象编程,这里的抽象指的是接口或者抽象类。面向接口编程是面向对象精髓之一,也就是上面两节强调的抽象。

如果类与类直接依赖于细节,那么它们之间就有直接的耦合,当具体实现需要变化时,意味着要同时修改依赖者的代码,这限制了系统的可扩展性。在1.3节的图1-3中,ImageLoader直接依赖于MemoryCache,这个MemoryCache是一个具体实现,而不是一个抽象类或者接口。这导致了ImageLoader直接依赖了具体细节,当MemoryCache不能满足ImageLoader而需要被其他缓存实现替换时,此时就必须修改ImageLoader的代码,例如:

public  class  ImageLoader {
    // 内存缓存 ( 直接依赖于细节 )
    MemoryCache mMemoryCache = new MemoryCache();
     // 加载图片到ImageView中
    public  void  displayImage(String url, ImageView imageView) {
        Bitmap bmp = mMemoryCache.get(url);
         if (bmp == null) {
            downloadImage(url, imageView);
        } else {
            imageView.setImageBitmap(bmp);
        }
    }

    public  void   setImageCache(MemoryCache cache) {
         mCache = cache ;
    }
    // 代码省略
}

随着产品的升级,用户发现MemoryCache已经不能满足需求,用户需要小民的ImageLoader可以将图片同时缓存到内存和SD卡中,或者可以让用户自定义实现缓存。此时,我们的MemoryCache这个类名不仅不能够表达内存缓存和SD卡缓存的意义,也不能够满足功能。另外,用户需要自定义缓存实现时还必须继承自MemoryCache,而用户的缓存实现可不一定与内存缓存有关,这在命名上的限制也让用户体验不好。重构的时候到了!小民的第一种方案是将MemoryCache修改为DoubleCache,然后在DoubleCache中实现具体的缓存功能。我们需要将ImageLoader修改如下:

public  class  ImageLoader {
    // 双缓存 ( 直接依赖于细节 )
    DoubleCache mCache = newDoubleCache();
    // 加载图片到ImageView中
    public  void  displayImage(String url, ImageView imageView) {
        Bitmap bmp = mCache.get(url);
         if (bmp == null) {
            // 异步下载图片
            downloadImageAsync(url, imageView);
        } else {
            imageView.setImageBitmap(bmp);
        }
    }

    public  void   setImageCache(DoubleCache cache) {
         mCache = cache ;
    }
    // 代码省略
}

在程序中我们将MemoryCache修改成DoubleCache,然后修改了ImageLoader中缓存类的具体实现,轻轻松松就满足了用户需求。等等!这不还是依赖于具体的实现类(DoubleCache)吗?当用户的需求再次变化时,我们又要通过修改缓存实现类和ImageLoader代码来实现?修改原有代码不是违反了1.3节中的开闭原则吗?小民突然醒悟了过来,低下头思索着如何才能让缓存系统更灵活,拥抱变化……

当然,这些都是在主管给出图1-2以及相应的代码之前,小民体验的煎熬过程。既然是这样,那显然主管给出的解决方案就能够让缓存系统更加灵活。一句话概括起来就是:依赖抽象,而不依赖具体实现。针对于图片缓存,主管建立的ImageCache抽象,该抽象中增加了get和put方法用以实现图片的存取。每种缓存实现都必须实现这个接口,并且实现自己的存取方法。当用户需要使用不同的缓存实现时,直接通过依赖注入即可,保证了系统的灵活性。我们再来简单回顾一下相关代码。

ImageCache缓存抽象:

public  interface ImageCache {
    public   Bitmap get(String url);
    public  void  put(String url, Bitmap bmp);
}

ImageLoader类:

public  class  ImageLoader {
    // 图片缓存类,依赖于抽象,并且有一个默认的实现
    ImageCache mCache = new MemoryCache();
    // 加载图片
    public  void  displayImage(String url, ImageView imageView) {
        Bitmap bmp = mCache.get(url);
         if (bmp == null) {
            // 异步加载图片
            downloadImageAsync(url, imageView);
        } else {
            imageView.setImageBitmap(bmp);
        }
    }

    /**
     * 设置缓存策略,依赖于抽象
     */
    public  void  setImageCache(ImageCache cache) {
        mCache = cache;
    }
    // 代码省略
}

在这里,我们建立了ImageCache抽象,并且让ImageLoader依赖于抽象而不是具体细节。当需求发生变化时,小民只需要实现ImageCahce类或者继承其他已有的ImageCache子类完成相应的缓存功能,然后将具体的实现注入到ImageLoader即可实现缓存功能的替换,这就保证了缓存系统的高可扩展性,有了拥抱变化的能力,这就是依赖倒置原则。从上述几节中我们发现,要想让系统更为灵活,抽象似乎成了我们唯一的手段。

接口隔离原则英文全称是InterfaceSegregation Principles,缩写是ISP。ISP的定义是:客户端不应该依赖它不需要的接口。另一种定义是:类间的依赖关系应该建立在最小的接口上。接口隔离原则将非常庞大、臃肿的接口拆分成更小的和更具体的接口,这样客户将会只需要知道他们感兴趣的方法。接口隔离原则的目的是系统解开耦合,从而容易重构、更改和重新部署。

接口隔离原则说白了就是,让客户端依赖的接口尽可能地小,这样说可能还是有点抽象,我们还是以一个示例来说明一下。在此之前我们来说一个场景,在Java 6以及之前的JDK版本,有一个非常讨厌的问题,那就是在使用了OutputStream或者其他可关闭的对象之后,我们必须保证它们最终被关闭了,我们的SD卡缓存类中就有这样的代码:

// 将图片缓存到内存中
public  void  put(String url, Bitmap bmp) {
    FileOutputStream fileOutputStream = null;
    try {
        fileOutputStream = new FileOutputStream(cacheDir + url);
        bmp.compress(CompressFormat.PNG, 100, fileOutputStream);
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } finally {
        if (fileOutputStream != null) {
            try {
                fileOutputStream.close();
          } catch (IOException e) {
                e.printStackTrace();
          }
        } // end if
    } // end if finally
}

我们看到的这段代码可读性非常差,各种try…catch嵌套都是些简单的代码,但是会严重影响代码的可读性,并且多层级的大括号很容易将代码写到错误的层级中。大家应该对这类代码也非常反感,那我们看看如何解决这类问题。

我们可能知道Java中有一个Closeable接口,该接口标识了一个可关闭的对象,它只有一个close方法,如图1-4所示。

我们要讲的FileOutputStream类就实现了这个接口。我们从图1-4中可以看到,还有100多个类实现了Closeable这个接口,这意味着,在关闭这100多个类型的对象时,都需要写出像put方法中finally代码段那样的代码。这还了得!你能忍,反正小民是忍不了的!于是小民打算要发挥他的聪明才智解决这个问题,既然都是实现了Closeable接口,那只要我建一个方法统一来关闭这些对象不就可以了么?说干就干,于是小民写下来如下的工具类:

▲图1-4

public final class CloseUtils {
    Private CloseUtils() { }
    /**
     * 关闭Closeable对象
     * @param closeable
     */
    public  static void closeQuietly(Closeable closeable) {
        if (null != closeable) {
            try {
                closeable.close();
            }catch (IOException e) {
                    e.printStackTrace();
            }
        }
    }
}

我们再看看把这段代码运用到上述的put方法中的效果如何:

public  void put(String url, Bitmap bmp) {
    FileOutputStream fileOutputStream = null;
    try {
        fileOutputStream = new FileOutputStream(cacheDir + url);
        bmp.compress(CompressFormat.PNG, 100, fileOutputStream);
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } finally {
        CloseUtils.closeQuietly(fileOutputStream);
    }
}

代码简洁了很多!而且这个closeQuietly方法可以运用到各类可关闭的对象中,保证了代码的重用性。CloseUtils的closeQuietly方法的基本原理就是依赖于Closeable抽象而不是具体实现(这不是1.4节中的依赖倒置原则么),并且建立在最小化依赖原则的基础上,它只需要知道这个对象是可关闭,其他的一概不关心,也就是这里的接口隔离原则。

试想一下,如果在只是需要关闭一个对象时,它却暴露出了其他的接口函数,如OutputStream的write方法,这就使得更多的细节暴露在客户端代码面前,不仅没有很好地隐藏实现,还增加了接口的使用难度。而通过Closeable接口将可关闭的对象抽象起来,这样只需要客户端依赖于Closeable就可以对客户端隐藏其他的接口信息,客户端代码只需要知道这个对象可关闭(只可调用close方法)即可。小民设计的ImageLoader中的ImageCache就是接口隔离原则的运用,ImageLoader只需要知道该缓存对象有存、取缓存图片的接口即可,其他的一概不管,这就使得缓存功能的具体实现对ImageLoader隐藏。这就是用最小化接口隔离了实现类的细节,也促使我们将庞大的接口拆分到更细粒度的接口当中,这使得我们的系统具有更低的耦合性,更高的灵活性。

Bob大叔(Robert C Martin)在21世纪早期将单一职责、开闭原则、里氏替换、接口隔离以及依赖倒置(也称为依赖反转)5个原则定义为SOLID原则,作为面向对象编程的5个基本原则。当这些原则被一起应用时,它们使得一个软件系统更清晰、简单,最大程度地拥抱变化。SOLID被典型地应用在测试驱动开发上,并且是敏捷开发以及自适应软件开发基本原则的重要组成部分。在经过第1.1~1.5节的学习之后,我们发现这几大原则最终就可以化为这几个关键词:抽象、单一职责、最小化。那么,在实际开发过程中如何权衡、实践这些原则,大家需要在实践中多思考与领悟,正所谓”学而不思则罔,思而不学则殆”,只有不断地学习、实践、思考,才能够在积累的过程中有一个质的飞越。

迪米特原则英文全称为Law of Demeter,缩写是LOD,也称为最少知识原则(Least Knowledge Principle)。虽然名字不同,但描述的是同一个原则:一个对象应该对其他对象有最少的了解。通俗地讲,一个类应该对自己需要耦合或调用的类知道得最少,类的内部如何实现与调用者或者依赖者没关系,调用者或者依赖者只需要知道它需要的方法即可,其他的可一概不用管。类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。

迪米特法则还有一个英文解释是Only talk to your immedate friends,翻译过来就是:只与直接的朋友通信。什么叫做直接的朋友呢?每个对象都必然会与其他对象有耦合关系,两个对象之间的耦合就成为朋友关系,这种关系的类型有很多,如组合、聚合、依赖等。

下面我们就以租房为例来讲讲迪米特原则的应用。

“北漂”的朋友比较了解,在北京租房绝大多数都是通过中介找房。我们设定的情况为:我只要求房间的面积和租金,其他的一概不管,中介将符合我要求的房子提供给我就可以。下面我们看看这个示例:

/**
 * 房间
 */
public  class  Room {
    public  float  area;
    public  float  price;
    public Room(float  area, float  price) {
        this.area = area;
        this.price = price;
    }
    @Override
    public  String toString() {
        return "Room [area=" + area + ", price=" + price + "]";
    }

}

/**
 * 中介
 */
public  class  Mediator {
    List<Room> mRooms = new ArrayList<Room>();
    public  Mediator() {
        for (inti = 0; i < 5; i++) {
            mRooms.add(new Room(14 + i, (14 + i) * 150));
        }
    }
    public   List<Room>getAllRooms() {
        return  mRooms;
    }

}

/**
 * 租户
 */
public  class  Tenant {
    public float  roomArea;
    public float  roomPrice;
    public  static final float diffPrice = 100.0001f;
    public  static final float diffArea = 0.00001f;
    public  void rentRoom(Mediator mediator) {
        List<Room>rooms = mediator.getAllRooms();
        for (Room room : rooms) {
            if (isSuitable(room)) {
              System.out.println("租到房间啦! " + room);
                break;
            }
        }
    }

    private boolean isSuitable(Room room) {
        return  Math.abs(room.price - roomPrice) < diffPrice
                &&Math.abs(room.area - roomArea) < diffArea;
    }
}

从上面的代码中可以看到,Tenant不仅依赖了Mediator类,还需要频繁地与Room类打交道。租户类的要求只是通过中介找到一间适合自己的房间罢了,如果把这些检测条件都放在Tenant类中,那么中介类的功能就被弱化,而且导致Tenant与Room的耦合较高,因为Tenant必须知道许多关于Room的细节。当Room变化时Tenant也必须跟着变化。Tenant又与Mediator耦合,这就出现了纠缠不清的关系。这个时候就需要我们分清谁才是我们真正的“朋友”,在我们所设定的情况下,显然是Mediator(虽然现实生活中不是这样的)。上述代码的结构如图1-5所示。

▲图1-5

既然是耦合太严重,那我们就只能解耦了。首先要明确的是,我们只和我们的朋友通信,这里就是指Mediator对象。必须将Room相关的操作从Tenant中移除,而这些操作案例应该属于Mediator。我们进行如下重构:

/**
 * 中介
 */
public  class  Mediator {
    List<Room> mRooms = new ArrayList<Room>();
    public  Mediator() {
         for (inti = 0; i < 5; i++) {
            mRooms.add(new Room(14 + i, (14 + i) * 150));
        }
    }

    public   Room rentOut(float  area, float  price) {
         for (Room room : mRooms) {
            if (isSuitable(area, price, room)) {
                return  room;
           }
       }

        return null;
    }

    private boolean isSuitable(float  area, float  price, Room room) {
        return  Math.abs(room.price - price) < Tenant.diffPrice
            && Math.abs(room.area - area) < Tenant.diffPrice;
    }
}

/**
 * 租户
 */
public  class  Tenant {
    public  float  roomArea;
    public  float  roomPrice;
    public  static final float diffPrice = 100.0001f;
    public  static final float diffArea = 0.00001f;

    public  void rentRoom(Mediator mediator) {
        System.out.println("租到房啦 " + mediator.rentOut(roomArea, roomPrice));
     }
}

重构后的结构图如图1-6所示。

▲图1-6

只是将对于Room的判定操作移到了Mediator类中,这本应该是Mediator的职责,根据租户设定的条件查找符合要求的房子,并且将结果交给租户就可以了。租户并不需要知道太多关于Room的细节,比如与房东签合同,房东的房产证是不是真的,房内的设施坏了之后要找谁维修等。当我们通过我们的“朋友”——中介租了房之后,所有的事情我们都通过与中介沟通就好了,房东、维修师傅等这些角色并不是我们直接的“朋友”。“只与直接的朋友通信”这简单的几个字就能够将我们从复杂的关系网中抽离出来,使程序耦合度更低、稳定性更好。

通过上述示例以及小民的后续思考,迪米特原则这把利剑在小民的手中已经舞得风生水起。就拿SD卡缓存来说吧,ImageCache就是用户的直接朋友,而SD卡缓存内部却是使用了jake wharton的DiskLruCache实现,这个DiskLruCache就不属于用户的直接朋友了,因此,用户完全不需要知道它的存在,用户只需要与ImageCache对象打交道即可,如将图片存到SD卡中的代码如下:

public  void put(String url, Bitmap value) {
    DiskLruCache.Editor editor = null;
    try {
       // 如果没有找到对应的缓存,则准备从网络上请求数据,并写入缓存
        editor = mDiskLruCache.edit(url);
        if (editor != null) {
            OutputStream outputStream = editor.newOutputStream(0);
            if (writeBitmapToDisk(value, outputStream)) {
                // 写入Disk缓存
                editor.commit();
            } else {
                editor.abort();
            }
            CloseUtils.closeQuietly(outputStream);
        }
    } catch (IOException e) {
          e.printStackTrace();
    }
}

用户在使用SD卡缓存时,根本不知道DiskLruCache的实现,这就很好地对用户隐藏了具体实现。当小民已经“牛”到可以自己完成SD卡的LRU实现时,他就可以随心所欲地替换掉jake wharton的DiskLruCache。小民的代码大体如下:

@Override
public  void put(String url, Bitmap bmp) {
    // 将Bitmap写入文件中
    FileOutputStream fos = null;
    try {
       // 构建图片的存储路径 ( 省略了对url取md5)
        fos = new FileOutputStream("sdcard/cache/" + imageUrl2MD5(url));
        bmp.compress(CompressFormat.JPEG, 100, fos);
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } finally {
        if ( fos != null ) {
            try {
                fos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    } // end if finally
}

SD卡缓存的具体实现虽然被替换了,但用户根本不会感知到。因为用户根本不知道DiskLruCache的存在,他们没有与DiskLruCache进行通信,他们只认识直接“朋友”——ImageCache,ImageCache将一切细节隐藏在了直接“朋友”的外衣之下,使得系统具有更低的耦合性和更好的可扩展性。

在应用开发过程中,最难的不是完成应用的开发工作,而是在后续的升级、维护过程中让应用系统能够拥抱变化。拥抱变化也就意味着在满足需求且不破坏系统稳定性的前提下保持高可扩展性、高内聚、低耦合,在经历了各版本的变更之后依然保持清晰、灵活、稳定的系统架构。当然,这是一个比较理想的情况,但我们必须要朝着这个方向去努力,那么遵循面向对象六大原则就是我们走向灵活软件之路所迈出的第一步。

单例模式是应用最广的模式之一,也可能是很多初级工程师唯一会使用的设计模式。在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个全局对象,这样有利于我们协调系统整体的行为。如在一个应用中,应该只有一个ImageLoader实例,这个ImageLoader中又含有线程池、缓存系统、网络请求等,很消耗资源,因此,没有理由让它构造多个实例。这种不能自由构造对象的情况,就是单例模式的使用场景。

确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。

确保某个类有且只有一个对象的场景,避免产生多个对象消耗过多的资源,或者某种类型的对象只应该有且只有一个。例如,创建一个对象需要消耗的资源过多,如要访问IO和数据库等资源,这时就要考虑使用单例模式。

UML类图如图2-1所示。

▲图2-1

角色介绍:

(1)Client——高层客户端;

(2)Singleton——单例类。

实现单例模式主要有如下几个关键点:

(1)构造函数不对外开放,一般为Private;

(2)通过一个静态方法或者枚举返回单例类对象;

(3)确保单例类的对象有且只有一个,尤其是在多线程环境下;

(4)确保单例类对象在反序列化时不会重新构建对象。

通过将单例类的构造函数私有化,使得客户端代码不能通过new的形式手动构造单例类的对象。单例类会暴露一个公有静态方法,客户端需要调用这个静态方法获取到单例类的唯一对象,在获取这个单例对象的过程中需要确保线程安全,即在多线程环境下构造单例类的对象也是有且只有一个,这也是单例模式实现中比较困难的地方。

单例模式是设计模式中比较简单的,只有一个单例类,没有其他的层次结构与抽象。该模式需要确保该类只能生成一个对象,通常是该类需要消耗较多的资源或者没有多个实例的情况。例如,一个公司只有一个CEO、一个应用只有一个Application对象等。下面以公司里的CEO为例来简单演示一下,一个公司可以有几个VP、无数个员工,但是CEO只有一个,请看下面示例。

示例实现代码

package com.dp.example.singleton;
// 普通员工
public class Staff { 
  public void work() {
    // 干活
  }
}

// 副总裁
public class VP extends Staff {
  @Override
  public void work() {
    // 管理下面的经理
  }
}

// CEO,饿汉单例模式
public class CEO extends Staff {
  private static final CEO mCeo = new CEO();
  // 构造函数私有
  private CEO() {
  }

  // 公有的静态函数,对外暴露获取单例对象的接口
  public static CEO getCeo() {
    return mCeo;
  }

  @Override
  public void work() {
    // 管理VP
  }
}

// 公司类
public class Company {
  private List<Staff> allStaffs = new ArrayList<Staff>();
  public void addStaff(Staff per) {
    allStaffs.add(per);
  }

  public void showAllStaffs() {
    for (Staff per : allStaffs) {
      System.out.println("Obj : " + per.toString());
    }
  }
}

public class Test {
  public static void main(String[] args) {
    Company cp = new Company() ;
    // CEO对象只能通过getCeo函数获取
    Staff ceo1 = CEO.getCeo() ;
    Staff ceo2 = CEO.getCeo() ;
    cp.addStaff(ceo1);
    cp.addStaff(ceo2);
    // 通过new创建VP对象
    Staff vp1 = new VP() ;
    Staff vp2 = new VP() ;
    // 通过new创建Staff对象
    Staff staff1 = new Staff() ;
    Staff staff2 = new Staff() ;
    Staff staff3 = new Staff() ;

    cp.addStaff(vp1);
    cp.addStaff(vp2);
    cp.addStaff(staff1);
    cp.addStaff(staff2);
    cp.addStaff(staff3);

    cp.showAllStaffs();
  }
}  

输出结果如下:

Obj : com.android.dp.book.chapter2.company.CEO@5e8fce95
Obj : com.android.dp.book.chapter2.company.CEO@5e8fce95
Obj : com.android.dp.book.chapter2.company.VP@8b3
Obj : com.android.dp.book.chapter2.company.VP@222d10
Obj : com.android.dp.book.chapter2.company.Staff@1aa488
Obj : com.android.dp.book.chapter2.company.Staff@3dfeca64
Obj : com.android.dp.book.chapter2.company.Staff@22998b08

从上述的代码中可以看到,CEO类不能通过new的形式构造对象,只能通过CEO.getCEO()函数来获取,而这个CEO对象是静态对象,并且在声明的时候就已经初始化,这就保证了CEO对象的唯一性。从输出结果中发现,CEO两次输出的CEO对象都是一样的,而VP、Staff等类型的对象都是不同的。这个实现的核心在于将CEO类的构造方法私有化,使得外部程序不能通过构造函数来构造CEO对象,而CEO类通过一个静态方法返回一个静态对象。

懒汉模式是声明一个静态对象,并且在用户第一次调用getInstance时进行初始化,而上述的饿汉模式(CEO类)是在声明静态对象时就已经初始化。懒汉单例模式实现如下。

public class Singleton {
  private static Singleton instance;  
  private Singleton () {}

  public static synchronized Singleton getInstance() {       
    if (instance == null) {            
      instance = new Singleton ();      
    }
    return instance;                   
  }
}

读者可能已经发现了,getInstance()方法中添加了synchronized关键字,也就是getInstance是一个同步方法,这就是上面所说的在多线程情况下保证单例对象唯一性的手段。细想一下,大家可能会发现一个问题,即使instance已经被初始化(第一次调用时就会被初始化instance),每次调用getInstance方法都会进行同步,这样会消耗不必要的资源,这也是懒汉单例模式存在的最大问题。

最后总结一下,懒汉单例模式的优点是单例只有在使用时才会被实例化,在一定程度上节约了资源;缺点是第一次加载时需要及时进行实例化,反应稍慢,最大的问题是每次调用getInstance都进行同步,造成不必要的同步开销。这种模式一般不建议使用。

DCL方式实现单例模式的优点是既能够在需要时才初始化单例,又能够保证线程安全,且单例对象初始化后调用getInstance不进行同步锁。代码如下所示:

public class Singleton {
  private static Singleton sInstance = null;
  private Singleton() {
  }
  public void doSomething() {
    System.out.println("do sth.");
  }

  public static Singleton getInstance() {
    if (mInstance == null) {
      synchronized (Singleton.class) {
        if (mInstance == null) {
          sInstance = new Singleton();
        }
      }
    }
    return sInstance;
  }

本程序的亮点自然都在getInstance方法上,可以看到getInstance方法中对instance进行了两次判空:第一层判断主要是为了避免不必要的同步,第二层的判断则是为了在null的情况下创建实例。这是什么意思呢?是不是有点摸不着头脑,下面就一起来分析一下。

假设线程A执行到sInstance = new Singleton()语句,这里看起来是一句代码,但实际上它并不是一个原子操作,这句代码最终会被编译成多条汇编指令,它大致做了3件事情:

(1)给Singleton的实例分配内存;

(2)调用Singleton()的构造函数,初始化成员字段;

(3)将sInstance对象指向分配的内存空间(此时sInstance就不是null了)。

但是,由于Java编译器允许处理器乱序执行,以及JDK1.5之前JMM(Java Memory Model,即Java内存模型)中Cache、寄存器到主内存回写顺序的规定,上面的第二和第三的顺序是无法保证的。也就是说,执行顺序可能是也可能是1-3-2。如果是后者,并且在3执行完毕、2未执行之前,被切换到线程B上,这时候sInstance因为已经在线程A内执行过了第三点,sInstance已经是非空了,所以,线程B直接取走sInstance,再使用时就会出错,这就是DCL失效问题,而且这种难以跟踪难以重现的错误很可能会隐藏很久。

在JDK1.5之后,SUN官方已经注意到这种问题,调整了JMM、具体化了volatile关键字,因此,如果JDK是1.5或之后的版本,只需要将sInstance的定义改成private volatile static Singleton sInstance = null就可以保证sInstance对象每次都是从主内存中读取,就可以使用DCL的写法来完成单例模式。当然,volatile或多或少也会影响到性能,但考虑到程序的正确性,牺牲这点性能还是值得的。

DCL的优点:资源利用率高,第一次执行getInstance时单例对象才会被实例化,效率高。缺点:第一次加载时反应稍慢,也由于Java内存模型的原因偶尔会失败。在高并发环境下也有一定的缺陷,虽然发生概率很小。DCL模式是使用最多的单例实现方式,它能够在需要时才实例化单例对象,并且能够在绝大多数场景下保证单例对象的唯一性,除非你的代码在并发场景比较复杂或者低于JDK 6版本下使用,否则,这种方式一般能够满足需求。

DCL虽然在一定程度上解决了资源消耗、多余的同步、线程安全等问题,但是,它还是在某些情况下出现失效的问题。这个问题被称为双重检查锁定(DCL)失效,在《Java并发编程实践》一书的最后谈到了这个问题,并指出这种“优化”是丑陋的,不赞成使用。而建议使用如下的代码替代:

public class Singleton {
  private Singleton() { }
  public static Singleton getInstance () {
    return SingletonHolder.sInstance;
  }

  /**
   * 静态内部类
   */
  private static class SingletonHolder {
    private static final Singleton sInstance = new Singleton();
  }
}

当第一次加载Singleton类时并不会初始化sInstance,只有在第一次调用Singleton的getInstance方法时才会导致sInstance被初始化。因此,第一次调用getInstance方法会导致虚拟机加载SingletonHolder类,这种方式不仅能够确保线程安全,也能够保证单例对象的唯一性,同时也延迟了单例的实例化,所以这是推荐使用的单例模式实现方式。

前面讲解了一些单例模式实现方式,但是,这些实现方式不是稍显麻烦就是会在某些情况下出现问题。还有没有更简单的实现方式呢?我们看看下面的实现:

public enum SingletonEnum {
  INSTANCE;
  public void doSomething() {
    System.out.println("do sth.");
  }
}

什么?枚举!没错,就是枚举!

写法简单是枚举单例最大的优点,枚举在Java中与普通的类是一样的,不仅能够有字段,还能够有自己的方法。最重要的是默认枚举实例的创建是线程安全的,并且在任何情况下它都是一个单例。

为什么这么说呢?在上述的几种单例模式实现中,在一个情况下它们会出现重新创建对象的情况,那就是反序列化。

通过序列化可以将一个单例的实例对象写到磁盘,然后再读回来,从而有效地获得一个实例。即使构造函数是私有的,反序列化时依然可以通过特殊的途径去创建类的一个新的实例,相当于调用该类的构造函数。反序列化操作提供了一个很特别的钩子函数,类中具有一个私有的、被实例化的方法readResolve(),这个方法可以让开发人员控制对象的反序列化。例如,上述几个示例中如果要杜绝单例对象在被反序列化时重新生成对象,那么必须加入如下方法:

private Object readResolve() throws ObjectStreamException {
  return sInstance;
}

也就是在readResolve方法中将sInstance对象返回,而不是默认的重新生成一个新的对象。而对于枚举,并不存在这个问题,因为即使反序列化它也不会重新生成新的实例。

在学习了上述各类单例模式的实现之后,再来看看一种另类的实现,具体代码如下:

  public class SingletonManager { 
  private static Map<String, Object> objMap = new HashMap<String,Object>();

  private Singleton() { }
  public static void registerService(String key, Objectinstance) {
    if (!objMap.containsKey(key) ) {
      objMap.put(key, instance) ;
    }
  }

  public static ObjectgetService(String key) {
    return objMap.get(key) ;
  }
}

在程序的初始,将多种单例类型注入到一个统一的管理类中,在使用时根据key获取对象对应类型的对象。这种方式使得我们可以管理多种类型的单例,并且在使用时可以通过统一的接口进行获取操作,降低了用户的使用成本,也对用户隐藏了具体实现,降低了耦合度。

不管以哪种形式实现单例模式,它们的核心原理都是将构造函数私有化,并且通过静态方法获取一个唯一的实例,在这个获取的过程中必须保证线程安全、防止反序列化导致重新生成实例对象等问题。选择哪种实现方式取决于项目本身,如是否是复杂的并发环境、JDK版本是否过低、单例对象的资源消耗等。

在Android系统中,我们经常会通过Context获取系统级别的服务,如WindowsManagerService、ActivityManagerService等,更常用的是一个LayoutInflater的类,这些服务会在合适的时候以单例的形式注册在系统中,在我们需要的时候就通过Context的getSystemService(String name)获取。我们以LayoutInflater为例来说明,平时我们使用LayoutInflater较为常见的地方是在ListView的getView方法中:

@Override
public View getView(int position, View convertView, ViewGroup parent)
  View itemView = null;
  if (convertView == null) {
    itemView = LayoutInflater.from(mContext).inflate(mLayoutId, null);
    // 代码省略
  } else {
    // 代码省略
  }
  // 代码省略
  return itemView;
}

通常我们使用LayoutInflater.from(Context)来获取LayoutInflater服务,下面看看LayoutInflater.from (Context)的实现:

public static LayoutInflater from(Context context) {
  LayoutInflater LayoutInflater =(LayoutInflater) context.getSystemService(Context. LAYOUT_INFLATER_SERVICE);

  if (LayoutInflater == null) {
    throw new AssertionError("LayoutInflater not found.");
  }
  return LayoutInflater;
}

可以看到from(Context)函数内部调用的是Context类的getSystemService(String key)方法,我们跟踪到Context类看到,该类是抽象类:

public abstract class Context {
  // 省略
}

getView中使用的Context对象的具体实现类是什么呢?其实在Application、Activity、Service中都会存在一个Context对象,即Context的总个数为Activity个数 + Service个数 + 1。而ListView通常都是显示在Activity中,那么我们就以Activity中的Context来分析。

我们知道,一个Activity的入口是ActivityThread的main函数,在main函数中创建一个新的ActivityThread对象,并且启动消息循环(UI线程),创建新的Activity、新的Context对象,然后将该Context对象传递给Activity。下面看看ActivityThread源代码:

public static void main(String[] args) {
// 代码省略
    Process.setArgV0("<pre-initialized>");
    // 主线程消息循环
    Looper.prepareMainLooper();
    // 创建ActivityThread对象
    ActivityThread thread = new ActivityThread();
    thread.attach(false);

    if (sMainThreadHandler == null) {
      sMainThreadHandler = thread.getHandler();
    }

    AsyncTask.init();
    // 代码省略
    Looper.loop();
  }

  private void attach(boolean system) {
    sThreadLocal.set(this);
    mSystemThread = system;
    // 不是系统应用的情况
    if (!system) {
      ViewRootImpl.addFirstDrawHandler(new Runnable() {
        public void run() {
          ensureJitEnabled();
        }
      });

      android.ddm.DdmHandleAppName.setAppName("<pre-initialized>",UserHandle.myUserId());
      RuntimeInit.setApplicationObject(mAppThread.asBinder());
      IActivityManager mgr = ActivityManagerNative.getDefault();
      try {
        // 关联mAppThread
        mgr.attachApplication(mAppThread);
      } catch (RemoteException ex) {
        // 省略
      }
    } else {
        // 省略
    }
}  

在main方法中,我们创建一个ActivityThread对象后,调用了其attach函数,并且参数为false。在attach函数中,参数为false的情况下(即非系统应用),会通过Binder机制与ActivityManager Service通信,并且最终调用handleLaunchActivity函数,我们看看该函数的实现:

private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    // 代码省略
    Activity a = performLaunchActivity(r, customIntent);
    // 代码省略
  }

   private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    // 代码省略
    Activity activity = null;
    try {
      java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
      activity = mInstrumentation.newActivity(     // 1. 创建Activity
      cl, component.getClassName(), r.intent);
     // 代码省略
    } catch (Exception e) {
     // 省略
    }

    try {
    // 创建Application对象
      Application app = r.packageInfo.makeApplication(false, mInstrumentation);
      if (activity != null) {
        Context appContext = createBaseContextForActivity(r, activity);  
        // 2. 获取Context对象
        CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
        Configuration config = new Configuration(mCompatConfiguration);
        // 3. 将appContext等对象attach到activity中
        activity.attach(appContext, this, getInstrumentation(), r.token,
        r.ident, app, r.intent, r.activityInfo, title, r.parent,
        r.embeddedID, r.lastNonConfigurationInstances, config);

        // 代码省略
        // 4. 调用Activity的onCreate方法
        mInstrumentation.callActivityOnCreate(activity, r.state);
        // 代码省略
    } catch (SuperNotCalledException e) {
      throw e;
    } catch (Exception e) {
      // 代码省略
    }

    return activity;
  }
  private Context createBaseContextForActivity(ActivityClientRecord r,
      final Activity activity) {
    // 5. 创建Context对象, 可以看到实现类是ContextImpl
 ContextImpl appContext = ContextImpl.createActivityContext(this, r.packageInfo, r.token);
    appContext.setOuterContext(activity);
    Context baseContext = appContext;
    // 代码省略
    return baseContext;
  }

通过上面1~5注释处的代码分析可以知道,Context的实现类为ComtextImpl。我们继续跟踪ContextImpl类:

class ContextImpl extends Context {

// 代码省略
// ServiceFetcher通过getService获取服务对象
   static class ServiceFetcher {
    int mContextCacheIndex = -1;

    // 获取系统服务
    public Object getService(ContextImpl ctx) {
      ArrayList<Object> cache = ctx.mServiceCache;
      Object service;
      synchronized (cache) {
        if (cache.size() == 0) {
          for (int i = 0; i < sNextPerContextServiceCacheIndex; i++) {
            cache.add(null);
          }
        } else {
          service = cache.get(mContextCacheIndex); // 从缓存中获取Service对象
          if (service != null) {
            return service;
          }
        }
        service = createService(ctx);
        cache.set(mContextCacheIndex, service);
        return service;
      }
    }

    /**
     * 子类覆写该方法用以创建服务对象
     */
    public Object createService(ContextImpl ctx) {
      throw new RuntimeException("Not implemented");
    }
  }

  // 1. Service容器
  private static final HashMap<String, ServiceFetcher> SYSTEM_SERVICE_MAP =
      new HashMap<String, ServiceFetcher>();

  private static int sNextPerContextServiceCacheIndex = 0;
  // 2. 注册服务器
  private static void registerService(String serviceName, ServiceFetcher fetcher) {
    if (!(fetcher instanceof StaticServiceFetcher)) {
      fetcher.mContextCacheIndex = sNextPerContextServiceCacheIndex++;
    }
    SYSTEM_SERVICE_MAP.put(serviceName, fetcher);
  }

  // 3. 静态语句块, 第一次加载该类时执行 ( 只执行一次, 保证实例的唯一性 )
  static {
    // 代码省略
    // 注册LayoutInflater service
    registerService(LAYOUT_INFLATER_SERVICE, new ServiceFetcher() {
        public Object createService(ContextImpl ctx) {
          return PolicyManager.makeNewLayoutInflater(ctx.getOuterContext());
        }});
    // 代码省略
  }

  // 4. 根据key获取对应的服务 
  @Override
  public Object getSystemService(String name) {
    // 根据name来获取服务
    ServiceFetcher fetcher = SYSTEM_SERVICE_MAP.get(name);
    return fetcher == null ? null : fetcher.getService(this);
  }
  // 代码省略
}

从ContextImpl类的部分代码中可以看到,在虚拟机第一次加载该类时会注册各种ServiceFatcher,其中就包含了LayoutInflater Service。将这些服务以键值对的形式存储在一个HashMap中,用户使用时只需要根据key来获取到对应的ServiceFetcher,然后通过ServiceFetcher对象的getService函数来获取具体的服务对象。当第一次获取时,会调用ServiceFetcher的createService函数创建服务对象,然后将该对象缓存到一个列表中,下次再取时直接从缓存中获取,避免重复创建对象,从而达到单例的效果。这种模式就是小节中通过容器的单例模式实现方式,系统核心服务以单例形式存在,减少了资源消耗。

LayoutInflater在我们的开发中扮演着重要的角色,但很多时候我们都不知道它的重要性,因为它的重要性被隐藏在了Activity、Fragment等组件的光环之下。

LayoutInflater是一个抽象类,具体代码如下:

public abstract class LayoutInflater {
    // 代码省略
}

既然是抽象不是具体的,那我们必须把这个深藏功与名的“家伙”找出来!需要先从layoutInflater的起源开始。在上文中知道,在加载ContenxtImpl时会通过如下代码将LayoutInflater的ServiceFetcher注入到容器中,具体代码如下:

registerService(LAYOUT_INFLATER_SERVICE, new ServiceFetcher() {
     public Object createService(ContextImpl ctx) {
        return PolicyManager.makeNewLayoutInflater(ctx.getOuterContext());
        }
});

这里调用了PolicyManager. makeNewLayoutInflater方法,继续往下看看:

public final class PolicyManager {
    // Policy实现类
    private static final String POLICY_IMPL_CLASS_NAME =
        "com.android.internal.policy.impl.Policy";
    private static final IPolicy sPolicy;
    static {
        // 通过反射构造Policy对象
        try {
            Class policyClass = Class.forName(POLICY_IMPL_CLASS_NAME);
            sPolicy = (IPolicy)policyClass.newInstance();
        } 
        // catch 代码段
}
    private PolicyManager() {}
    // 这里就是创建PhoneWindow对象的地方
    public static Window makeNewWindow(Context context) {
        return sPolicy.makeNewWindow(context);
    }
    // 通过sPolicy创建LayoutInflater
    public static LayoutInflater makeNewLayoutInflater(Context context) {
        return sPolicy.makeNewLayoutInflater(context);
    }
}

PolicyManager中通过反射构造了Policy实现类,这个类实现了IPolicy接口,通过这种形式将具体的Policy类对外进行隐藏实现。PolicyManager实际上是一个代理类,具体的功能通过sPolicy对象进行实现,我们看看sPolicy对应的Policy类,也就是com.android.internal.policy.impl.Policy:

public class Policy implements IPolicy {
    // 代码省略
    // 创建PhoneWindow,这就是Activity中Window的具体实现类
    public Window makeNewWindow(Context context) {
        return new PhoneWindow(context);
    }
    // 创建LayoutInflater,具体类为PhoneLayoutInflater,这才是我们要关注的地方
    public LayoutInflater makeNewLayoutInflater(Context context) {
        return new PhoneLayoutInflater(context);
    }
}

此时,已经很清楚了,真正LayoutInflater的实现类就是PhoneLayoutInflater。我们继续深入看看PhoneLayoutInflater的源代码:

public class PhoneLayoutInflater extends LayoutInflater {
    // 内置View类型的前缀,如TextView的完整路径是android.widget.TextView
    private static final String[] sClassPrefixList = {
        "android.widget.",
        "android.webkit."
    };

    // 代码省略
    @Override protected View onCreateView(String name, AttributeSet attrs) throws ClassNot FoundException {
        // 在View名字的前面添加前缀来构造View的完整路径,例如,类名为TextView,那么TextView完整  
        //路径是android.widget.TextView
        for (String prefix : sClassPrefixList) {
            try {
                View view = createView(name, prefix, attrs);
                if (view != null) {
                    return view;
                }
            } catch (ClassNotFoundException e) {
                // 省略
            }
        }
        return super.onCreateView(name, attrs);
    }
}

代码不多,核心的程序语句就是覆写了LayoutInflater的onCreateView方法,该方法就是在传递进来的View名字前面加上“android.widget.”或者“android.webkit.”前缀用以得到该内置View类(如TextView、Button等都在android.widget包下)的完整路径。最后,根据类的完整路径来构造对应的View对象。

具体是一个怎样的流程呢?以Activity的setContentView为例,先来看看这个函数的实现:

public void setContentView(View view) {
    getWindow().setContentView(view);
    initActionBar();
}

Activity的setContentView方法实际上调用的是Window的setContentView,而Window是一个抽象类,上文提到Window的具体实现类是PhoneWindow,我们看看PhoneWindow中对应的方法:

 @Override
public void setContentView(int layoutResID) {
        // 1. 当mContentParent为空时先构建DecorView
        // 并且将DecorView包裹到mContentParent中
        if (mContentParent == null) {
            installDecor();
        } else {
            mContentParent.removeAllViews();
        }
        // 2. 解析layoutResID
        mLayoutInflater.inflate(layoutResID, mContentParent);
    // 代码省略
    }

在分析之前,我们来看看一个Window的View层级图,如图2-2所示。

▲图2-2

从图2-2中,我们看到mDecor中会加载一个系统定义好的布局,这个布局中又包裹了mContentParent,而这个mContentParent就是我们设置的布局,并将添加到parent区域。在PhoneWindow的setContentView方法中也验证了这点,首先会构建mContentParent这个对象,然后通过LayoutInflater的inflate函数将指定布局的视图添加到mContentParent中。那么就先来看看inflate方法:

public View inflate(int resource, ViewGroup root) {
        // root不为空,则会从resource布局解析到View,并添加到root中
        return inflate(resource, root, root != null);
}

public View inflate(int resource, ViewGroup root, boolean attachToRoot) {
        // 获取xml资源解析器
    XmlResourceParser parser = getContext().getResources().getLayout(resource);
    try {
            return inflate(parser, root, attachToRoot);
     } finally {
            parser.close();
        }
    }

// 参数1为xml解析器,参数2为要解析布局的父视图,参数3为是否将要解析的视图添加到父视图中
public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            final AttributeSet attrs = Xml.asAttributeSet(parser);
            Context lastContext = (Context)mConstructorArgs[0];
            // Context对象
            mConstructorArgs[0] = mContext;
            // 存储父视图
            View result = root;
            try {
                // Look for the root node.
                int type;
                // 找到root元素
                while ((type = parser.next()) != XmlPullParser.START_TAG &&
                        type != XmlPullParser.END_DOCUMENT) {
                    // Empty
                }
                // 代码省略
                final String name = parser.getName();
                // 1. 解析merge标签
                if (TAG_MERGE.equals(name)) {
                    rInflate(parser, root, attrs, false);
                } else {
                    // 2. 不是merge标签那么直接解析布局中的视图
                    View temp;
                    if (TAG_1995.equals(name)) {
                        temp = new BlinkLayout(mContext, attrs);
                    } else {
                        // 3. 这里就是通过xml的tag来解析layout根视图
                        // name就是要解析的视图的类名,如RelativeLayout
                        temp = createViewFromTag(root, name, attrs);
                    }

                    ViewGroup.LayoutParams params = null;
                    if (root != null) {
                        // 生成布局参数
                        params = root.generateLayoutParams(attrs);
                        // 如果attachToRoot为false,那么将给temp设置布局参数
                        if (!attachToRoot) {
                            temp.setLayoutParams(params);
                        }
                    }
                    // 解析temp视图下的所有子View
                    rInflate(parser, temp, attrs, true);
                    // 如果Root不为空,且attachToRoot为true,那么将temp添加到父视图中
                    if (root != null && attachToRoot) {
                        root.addView(temp, params);
                    }
                    // 如果root为空或者attachToRoot为false,那么返回的结果就是temp
                    if (root == null || !attachToRoot) {
                        result = temp;
                    }
                }
            } 
            // 省略catch, finaly代码
            return result;
        }

上述的inflate方法中,主要有下面几步:

(1)解析xml中的根标签(第一个元素);

(2)如果根标签是merge,那么调用rInflate进行解析,rInflate会将merge标签下的所有子View直接添加到根标签中;

(3)如果标签是普通元素,那么运行到代码3,调用createViewFromTag对该元素进行解析;

(4)调用rInflate解析temp根元素下的所有子View,并且将这些子View都添加到temp下;

(5)返回解析到的根视图。

我们先从简单的地方理解,即解析单个元素的createViewFromTag,看看如下代码:

 View createViewFromTag(View parent, String name, AttributeSet attrs) {
        if (name.equals("view")) {
            name = attrs.getAttributeValue(null, "class");
        }
        try {
            View view;
            // 1. 用户可以通过设置LayoutInflater的factory来自行解析View,默认这些Factory都为  
            //空, 可以忽略这段
            if (mFactory2 != null) view = mFactory2.onCreateView(parent, name, mContext, attrs);
            else if (mFactory != null) view = mFactory.onCreateView(name, mContext, attrs);
            else view = null;
            // 代码省略
            // 2. 没有Factory的情况下通过onCreateView或者createView创建View
            if (view == null) {
                // 3. 内置View控件的解析
                if (-1 == name.indexOf('.')) {
                    view = onCreateView(parent, name, attrs);
                } else {
                    // 4. 自定义控件的解析
                    view = createView(name, null, attrs);
                }
            }
            return view;

        } 
        // 省略catch块
    }

本程序重点就在代码2,以及以后的代码,createViewFromTag会将该元素的parent及名字传递过来。当这个tag的名字中没有包含“.”(在名字中查找“.”返回-1)时,LayoutInflater会认为这是一个内置的View,例如,我们在xml中声明一个内置View时大概是这样的:

<TextView
        android:id="@+id/my_textview "
        android:layout_width="60dp"
    android:layout_height="60dp" />

这里的TextView就是xml元素的名字,因此,在执行infate时就会调用代码3处的onCreateView来解析这个TextView标签。当我们自定义View时,在xml中必须写View的完整路径,例如:

<com.dp.custom.MyView
            android:id="@+id/my_custom_view "
            android:layout_width="fill_parent"
       android:layout_height="fill_parent" />

此时,就会调用代码注释4的createView来解析该View。为什么要这么处理,它们之间又有什么不同呢?

在上文的PhoneLayoutInflater中我们知道,PhoneLayoutInflater覆写了onCreateView方法,也就是代码3处的onCreateView,该方法就是在View标签名的前面设置一个“android.widget.”前缀,然后再传递给createView进行解析。也就是说内置View和自定义View最终都调用了createView进行解析,只是Google为了让开发者在xml中更方便定义View,只写View名称而不需要写完整的路径。在LayoutInflater解析时如果遇到只写类名的View,那么认为是内置的View控件,在 onCreateView方法中会将“android.widget.”前缀传递给createView方法,最后,在createView中构造View的完整路径来进行解析。如果是自定义控件,那么必须写完整的路径,此时调用createView且前缀为null进行解析。

关于createView的解释已经有很多,我们还是看下面的代码吧:

// 根据完整路径的类名通过反射机制构造View对象
    public final View createView(String name, String prefix, AttributeSet attrs)
            throws ClassNotFoundException, InflateException {
        // 1. 从缓存中获取构造函数
        Constructor<? extends View> constructor = sConstructorMap.get(name);
        Class<? extends View> clazz = null;
        try {
            // 2. 没有缓存构造函数
            if (constructor == null) {
                // 如果prefix不为空,那么构造完整的View路径,并且加载该类
                clazz = mContext.getClassLoader().loadClass(
                        prefix != null ? (prefix + name) : name).asSubclass(View.class);
                // 代码省略
                // 3. 从Class对象中获取构造函数
                constructor = clazz.getConstructor(mConstructorSignature);
                // 4. 将构造函数存入缓存中
                sConstructorMap.put(name, constructor);
            } else {
                // 代码省略
            }
            Object[] args = mConstructorArgs;
            args[1] = attrs;
            // 5. 通过反射构造View
            final View view = constructor.newInstance(args);
            if (view instanceof ViewStub) {
                // always use ourselves when inflating ViewStub later
                final ViewStub viewStub = (ViewStub) view;
                viewStub.setLayoutInflater(this);
            }
            return view;

        } 
        // 省略各种catch、finaly代码
    }

createView相对比较简单,如果有前缀,那么构造View的完整路径,并且将该类加载到虚拟机中,然后获取该类的构造函数并且缓存起来,再通过构造函数来创建该View的对象,最后将View对象返回,这就是解析单个View的过程。而我们的窗口中是一棵视图树,LayoutInflater需要解析完这棵树,这个功能就交给了rInflate方法,具体代码如下:

void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs,
            boolean finishInflate) throws XmlPullParserException, IOException {
        // 1. 获取树的深度,深度优先遍历
        final int depth = parser.getDepth();
        int type;
        // 2. 挨个元素解析
        while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) 
         && type != XmlPullParser.END_DOCUMENT) {
            if (type != XmlPullParser.START_TAG) {
                continue;
            }
            final String name = parser.getName();
            if (TAG_REQUEST_FOCUS.equals(name)) {
                parseRequestFocus(parser, parent);
            } else if (TAG_INCLUDE.equals(name)) {  // 解析include标签
                parseInclude(parser, parent, attrs);
            } else if (TAG_MERGE.equals(name)) { // 解析merge标签,抛出异常,因为merge标签  
                                                        //必须为根视图
                throw new InflateException("<merge /> must be the root element");
            } else if (TAG_1995.equals(name)) {// 闪烁视图,这里可以不用管
// 代码省略
            } else {
                // 3. 根据元素名进行解析
                final View view = createViewFromTag(parent, name, attrs);
                final ViewGroup viewGroup = (ViewGroup) parent;
                final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams (attrs);
                // 递归调用进行解析,也就是深度优先遍历
                rInflate(parser, view, attrs, true);
                // 将解析到的View添加到viewGroup中,也就是它的parent
                viewGroup.addView(view, params);
            }
        }
        if (finishInflate) parent.onFinishInflate();
    }

rInflate通过深度优先遍历来构造视图树,每解析到一个View元素就会递归调用rInflate,直到这条路径下的最后一个元素,然后再回溯过来将每个View元素添加到它们的parent中。通过rInflate的解析之后,整棵视图树就构建完毕。当调用了Activity的onResume之后,我们通过setContentView设置的内容就会出现在我们的视野中。

在Android应用开发过程中,ImageLoader是我们最为常用的开发工具库之一。Android中最著名的ImageLoader就是Universal-Image-Loader(https://github.com/nostra13/Android-Universal-Image- Loader),它的使用过程大概是这样的:

public  void initImageLoader(Context context) {
    // 1. 使用Builder构建ImageLoader的配置对象
    ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(context)
        //加载图片的线程数
        .threadPriority(Thread.NORM_PRIORITY - 2)
        //解码图像的大尺寸,将在内存中缓存先前解码图像的小尺寸
        .denyCacheImageMultipleSizesInMemory() 
        //设置磁盘缓存文件名称
        .discCacheFileNameGenerator(new Md5FileNameGenerator())
        //设置加载显示图片队列进程
        .tasksProcessingOrder(QueueProcessingType.LIFO)
        .writeDebugLogs() 
        .build();
    // 2. 使用配置对象初始化ImageLoader
    ImageLoader.getInstance().init(config);
    // 3. 加载图片
    ImageLoader.getInstance().displayImage("图片url", myImageView);
}

代码中出现了熟悉的getInstance()方法,因此,我们猜测这个ImageLoader使用的是单例模式。正好,小民的ImageLoader也是类似的实现,通过一个getInstance函数返回单例对象,具体代码如下:

public final class ImageLoader{
// ImageLoader实例
    private static ImageLoadersInstance;
//网络请求队列
    private RequestQueue mImageQueue;
    // 缓存
    private volatile BitmapCache mCache = new MemoryCache();
    // 图片加载配置对象
    private ImageLoaderConfig mConfig;
    // 私有构造函数
    private ImageLoader() {
    }

    /**
     * 获取ImageLoader单例,DCL形式
     * @return单例对象
     */
    public static ImageLoadergetInstance() {
        if (sInstance == null) {
            synchronized (ImageLoader.class) {
                if (sInstance == null) {
                    sInstance = new ImageLoader();
                }
            }
        }
        return sInstance;
    }

    /**通过配置类初始化ImageLoader,设置线程数量、缓存策略、加载策略等
     * @param config配置对象
     */
    public void init(ImageLoaderConfig config) {
        mConfig = config;
        mCache = mConfig.bitmapCache;
        checkConfig();
        mImageQueue = new RequestQueue(mConfig.threadCount);
        mImageQueue.start();
    }
    // 代码省略
    // 加载图片的接口
    public void displayImage(ImageView imageView, String uri) {
        displayImage(imageView, uri, null, null);
    }
    public void displayImage(ImageView imageView, String uri, ImageListener listener) {
        displayImage(imageView, uri, null, listener);
    }

    public void displayImage(final ImageView imageView, final String uri,
    final DisplayConfig config, final ImageListener listener) {
        BitmapRequest request = new BitmapRequest(imageView, uri, config, listener);
        // 加载的配置对象,如果没有设置,则使用ImageLoader的配置
        request.displayConfig = request.displayConfig != null ? request.displayConfig
                : mConfig.displayConfig;
        // 添加到队列中
        mImageQueue.addRequest(request);
    }
    public void stop() {
        mImageQueue.stop();
    }

    // 图片加载Listener,加载完成后回调给客户端代码
    public static interface ImageListener {
        public void onComplete(ImageView imageView, Bitmap bitmap, String uri);
    }
}

我们的ImageLoader类中将构造函数私有化,并且使用Double Check Lock的形式实现单例,用户通过getInstance方法获取ImageLoader单例对象。在用户使用之前需要使用ImageLoaderConfig来配置ImageLoader,配置合理的情况下才会启动用户指定数量的线程来执行图片加载请求。当用户调用displayImage方法时,ImageLoader会将请求构造成一个BitmapRequest,然后将该请求添加到请求队列中,图片加载线程(RequestDispatcher)会从请求队列(RequestQueue)中获取图片加载请求,然后加载该图片,并且将图片显示到对应的ImageView上,最后将图片缓存到缓存系统中。

在后续章节中将会阐述关于该ImageLoader的更多细节,大家可以到github(https://github.com/ bboyfeiyu/simple_imageloader)下载该库的源代码,并且结合《教你写Android ImageLoader框架》系列博文(地址为http://blog.csdn.net/column/details/android-imageloader.html)进行学习。

单例模式是运用频率很高的模式,但是,由于在客户端通常没有高并发的情况,因此,选择哪种实现方式并不会有太大的影响。即便如此,出于效率考虑,我们推荐用2.6.2小节、2.6.3小节使用的形式。

优点

(1)由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁地创建、销毁时,而且创建或销毁时性能又无法优化,单例模式的优势就非常明显。

(2)由于单例模式只生成一个实例,所以,减少了系统的性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后用永久驻留内存的方式来解决。

(3)单例模式可以避免对资源的多重占用,例如一个写文件操作,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作。

(4)单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如,可以设计一个单例类,负责所有数据表的映射处理。

缺点

(1)单例模式一般没有接口,扩展很困难,若要扩展,除了修改代码基本上没有第二种途径可以实现。

(2)单例对象如果持有Context,那么很容易引发内存泄漏,此时需要注意传递给单例对象的Context最好是Application Context。

相关图书

Android App开发入门与实战
Android App开发入门与实战
Kotlin入门与实战
Kotlin入门与实战
Android 并发开发
Android 并发开发
Android APP开发实战——从规划到上线全程详解
Android APP开发实战——从规划到上线全程详解
Android应用案例开发大全( 第4版)
Android应用案例开发大全( 第4版)
深入理解Android内核设计思想(第2版)(上下册)
深入理解Android内核设计思想(第2版)(上下册)

相关文章

相关课程