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

AndroidStudio 和 Gradle 总结

$
0
0


前言

主要从AndroidStudio的环境安装升级,Gradle,Eclipse转AS,多渠道配置,Maven私服,Action,Option,快捷键等几个方面出发,讲一些操作技巧以及我对AndroidStudio使用的一些理解与经验。本文较全面的讲述了我们在开发中必须要了解的,比较多而全,可能不能马上记住,目的在于大家看我之后能有一个认识,在需要使用的时候知道有这么个东西。希望对你的开发工作有所帮助,不足之处,请批评指正。

知识大纲

  • Install&Settings&Update
    • Gradle
    • AS
  • Gradle
    • Groovy
    • Gradle编程框架
    • task
    • Android文件结构
    • 几个gradle配置文件
  • Grade构建过程简析
    • 构建流程图
    • 分析
  • 构建速度优化
    • 注意AS配置
    • 慎重sub-module
    • 守护进程deamon
    • 并行编译parallel
    • 依赖库使用固定版本
    • 去除无用的构建任务task
    • 巧用include
    • Instant Run
  • Maven仓库
    • maven()和jcenter()
    • maven私服
  • 多渠道
    • 简介
    • 配置
  • Eclipse项目彻底转为AS
    • 策略
    • 技巧
  • 只有一个快捷键
    • 提示类
    • 编辑类
    • 查找替换类
  • 代码检查以及质量改善
    • inspect code
    • clean up
  • 调试
  • Monitors
    • Memory
    • CPU\NetWork\GPU
  • 单元测试
    • 分分钟上手单元测试
    • 单元测试的意义简单理解
  • 终极技巧
    • 插件:plugin
      • SimpleUML
      • MarkDown
    • 书签、喜欢分组(bookMarks)
    • 抓屏布局分析(LayoutInspector)
    • 宏(Edit|Macors)
    • LiveTemplates (Settings|Live Teamplates)
    • Intentions
    • 版本管理Annotate
    • layout布局遍历
    • 巧用Alt+Enter
    • 巧用注解
    • 一些有趣的Task
  • AS 2.2新特性

一、Install&Settings&Update

1、Gradle

Gradle官方会不断更新,我们可以使用本地安装的方式,并配置path,我们就可以使用Terminal直接输入gradle命令执行构建任务。当然如果我们想快速更新,可以修改配置文件。 首先,修改project\gradle\warpper\gradle-wapper.properties 文件,其中distributionUrl的值:

    distributionUrl=https\://services.gradle.org/distributions/gradle-2.4-all.zip

这里实际是从网络下载配置的版本,会自动检测,如果不是的就会下载。

然后修改 project的build.gradle

  dependencies {
    classpath 'com.android.tools.build:gradle:1.3.0'

    // NOTE: Do not place your application dependencies here; they belong
    // in the individual module build.gradle files
}

注意:这两个配置是一一对应的,比如gradle-2.4-all对应的就是1.3.0。后者的意思是这个project配置的gradle构建版本为1.3.0,前者的意思是这个project使用的gradle的版本为2.4。我们会发现,如果我们修改前者,如果本地没有安装这个版本的gradle,会自动从gradle官网下载。但是,如果我们修改后者,它会自动从jcenter()仓库下载一些plugin之类的。

2、AS具体的安装和更新网上有许多的详细教程,我只想说以下三点。

  • Android Studio是Google官方基于IntelliJ IDEA开发的一款Android应用开发工具,绝逼比Eclipse强大,还没有转的尽快吧:

  • 关闭AndroidStudio的自检升级,如果准备好升级还是自己选择想升级的版本升级靠谱;

  • 升级前导出AndroidStudio的配置文件settings.jar(C:\Users\Administrator.AndroidStudio1.4\config目录下,或者操作File|Export Setings导出),升级后导入Settings.jar,这样就不需要重新配置,有必要的话给自己备份一个,说不定老天无缘无故挂了重装很方便。

  • 具体细节的配置可以阅读,强烈建议直接打开AS的设置窗口,多转几次就熟悉了里边的各种配置啦。也可以参考这边文章,(1.4版本,有点旧了,差不多够用)打造你的开发工具,settings必备

二、Gradle

1 简述Groovy语言

Groovy是一种开发语言,是在Java平台上的,具有向Python,Ruby语言特性的灵活动态语言,Groovy保重了这些特性像Java语法一样被Java开发者使用。编译最终都会转成java的.class文件。他们的关系如下图。我想这大概也是Gradle构建系统为什么要选择Groovy的原因,它具有java语言的特性,开发者容易理解使用。一定要明白我们在build.gradle里边不是简单的配置,而是直接的逻辑开发。如果你熟练掌握Groovy,那么你在build.grale里边可以做任何你想做的事。

2 Gradle编程框架

Gradle是一个工具,同时它也是一个编程框架。使用这个工具可以完成app的编译打包等工作,也可以干别的工作!Gradle里边有许多不同的插件,对应不同的工程结构、打包方式和输出文件类型。我们经常使用到的便是maven\java\com.android.application\android-library等。当按照要求配置好gradle的环境后,执行gradle的task,便会自动编译打包输出你想要的.apk.aar.jar文件,如果你足够牛逼,你有gradle就够了,直接拿记事本开发;

如下图,是Gradle的工作流程。

  • Initializtion 初始化,执行settings.gradle(我们看到都是include",实际里边可深了)
  • Hook 通过API来添加,这中间我们可以自己编程干些自己想做的事情
  • Configuration 解析每个project的build.gradle,确定project以及内部Task关系,形成一个有向图
  • Execution 执行任务,输入命令 gradle xxx ,按照顺序执行该task的所有依赖以自己本身

3 关于gradle的task

每个构建由一个或者多个project构成,一个project代表一个jar,一个moudle等等。一个project包含若干个task,包含多少由插件决定,而每一个task就是更细的构建任务,比如创建一个jar、生成Javadoc、上传aar到maven仓库。我们可以通过执行如下命令查看所有的task:

gradle tasks --all

当然,我们也可以在AS中可以看到所有的task,双击就可以执行单个的task.

当然,我们也可以在build.gradle中写自己的task。关于详细的task介绍可以查看网络资料进行学习,推荐Gradle入门系列,基本花上半天到一天的时候简单的过一遍就有一个大概的了解。

4 Gradle环境下Android的文件结构

  • project-name
    • gradle
    • module-name
      • build //构建生成文件
        • intermediates//构建打包的资源文件
          • assets//资源文件
          • exploded-aar//如果我们依赖了许多的aar或者依赖工程,最终都“copy"到了这个目录下
          • mainfests//合并的mainfest
        • outputs
          • apk//输出我们需要的.apk文件
          • lint-results.html//lint检查报告
        • reports
          • tests//单元测试报告
        • ivy.xml//moudle的配置(task任务)、依赖关系等
      • libs //本地的依赖jar包等
      • src //本moudule所有的代码和资源文件
        • androidTest //需要android环境的单元测试,比如UI的单元测试
        • Test //普通的java单元测试
        • main //主渠道
          • java //java code
          • jni //navtive jni so
          • gen
          • res
          • assets
          • AndroidManifest.xml +build.gradle //module
    • build.gradle // for all module
    • gradle.propeties //全局配置文件
    • local.properties //SDK、NDK配置
    • config.gradle//自定义的配置文件
    • settings.gradle//module管理

6 关于几个buid.gradle、gradle.propeties文件

  • build.gradle文件(主工程的Top-level)

    apply from:"config.gradle"//可以给所有的moudle引入一个配置文件
    
    buildscript {
         repositories {
         jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:1.3.0'
        // NOTE: Do not place your application dependencies here; they belong
         // in the individual module build.gradle files
        }
    }
    
    allprojects {
        repositories {
            jcenter()//引入远程仓库
            maven { url MAVEN_URL }//引入自己的私有maven仓库
        }
    }
    
  • gradle.properties(全局配置文件)

    # This can really make a significant difference if you are building a very complex project with many sub-module dependencies:
    #sub-moudle并行构建
    org.gradle.parallel=true
    #后台进程构建
    org.gradle.daemon=true
    #私有maven仓库地址
    MAVEN_URL= http://xxx.xx.1.147:8081/nexus/content/repositories/thirdparty/
    
  • build.gradle(module)

    apply plugin: 'com.android.application'//插件 决定是apk\aar\jar等
    
    android {
    compileSdkVersion 23
    buildToolsVersion "24.0.0"
    
    // 此处注释保持默认打开,关闭后可使不严格的图片可以通过编译,但会导致apk包变大
    //aaptOptions.cruncherEnabled = false
    //aaptOptions.useNewCruncher = false
    
     packagingOptions {
         exclude 'META-INF/NOTICE.txt'// 这里是具体的冲突文件全路径
         exclude 'META-INF/LICENSE.txt'
    }
    //默认配置
    defaultConfig {
        applicationId "com.locove.meet"
        minSdkVersion 16
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"
        multiDexEnabled=true//65536问题
    }
    sourceSets {
        main {
            jniLibs.srcDirs = ['libs']//重新配置路径
        }
    }
    buildTypes {
        release {
        // zipAlign优化
        zipAlignEnabled true
        // 移除无用的resource文件
        shrinkResources false
        // 混淆
        minifyEnabled false
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        signingConfig signingConfigs.releaseConfig
        }
    }
    }
    
    dependencies {
        compile fileTree(dir: 'libs', include: ['*.jar'])
        compile 'com.google.code.gson:gson:2.2.+'
        testCompile 'junit:junit:4.12'
    }
    

7 gradle编译文件和缓存文件

  • gradle缓存文件:C:\Users\Administrator.gradle\caches\modules-2\files-2.1
  • idea缓存文件: C:\Users\Administrator.AndroidStudio1.4

三、构建过程简析

这里参考了QQ音乐技术团队Android构建过程分析 下图是文章末尾的一张构建流程图:

  • 解压合并资源

    主要是assets目录,res目录,Androidmainfest.xml目录。其中合并的时候会涉及到优先级的问题,详情请查看该篇文章。

  • AAPT(Android Asset Packaging Tool)打包
    • R.java文件 资源ID
    • app.ap 压缩包
    • 对png图进行优化等
  • 源码编译:

生成.class字节码,在这里可以进行删除无用类,字节码优化,重命名(包名),还有一点就是代码混淆。

  • 生成dex、打包签名、zipalign

三、构建速度优化

AS编译太慢是我们经常吐槽的,我们该做些什么来加快编译的速度呢?前面我们简单的了解了gradle构建项目的流程。我们可以从以下几个方面来做:

注意AS配置:

如及时更新Gradle和JDK版本、扩大AS内存等(修改-Xms256m)、取消AS的自更新设置,去除不经常使用的插件等。

慎重sub-module:

减少sub-module或者将sub-module导成aar,并上传到私有的maven仓库就更加方便啦,每增加一个sub-module的构建的时间会增加很多。从根上解决这个问题,我们应该增加sub-module时要慎重,同时要考虑他的独立性,与主module要完全解耦。这样我们不会再开发的时候产品要换个ui图也跑到sub-module里边修改。当我们修改了sub-module的时候,编译器会检测到修改重新编译,然后copy到主工程的buid/intermediates/exploded-aar目录下。

守护进程deamon:

当我们在gradle.peopeties中配置org.gradle.daemon=true的时候,相当于开了个进程,这样我们构建项目的许多工作比如加载虚拟机之类的就在该进程中完成。

并行编译parallel:

这个适用于有多个依赖sub-module的情况下,如果单单只有一个module实测会耗时更多。看看官方的说法:When configured, Gradle will run in incubating parallel mode.This option should only be used with decoupled projects. org.gradle.parallel=true。这里通过增大gradle运行的java虚拟机大小,达到多个module并行构建的目的。

依赖库使用固定版本:

我们配置依赖的时候 如依赖V4包,com.android.support:support-v4:23.0.0+,再后边有个+后表示依赖最新的,这样可以保证依赖的库有更新时能够得到更新。但是,小编并不建议这么做。因为每次构建都需要访问网络去判断是否有最新版本,这样也是需要耗时的。我们可能需要频繁的构建调试,但是我们一般很少更新库。当然,这些可以配置在你的release分支上,总之,调试的请配置固定版本吧。

去除无用的构建任务task:

Gradle每次构建都执行了默认的许多task,其中部分task是我们不需要执行的,至少在调试的时候不需要,我们可以把这些屏蔽掉,方法如下:

    tasks.whenTaskAdded { task ->
        if (task.name.contains('AndroidTest') || task.name.contains('Test')) {
             task.enabled = false
        }
    }

巧用include:

对于我们没有依赖的module,我们可以在settings.gradle里边去掉改module的include,建议写成一行只include一个module,如下:

    include:'lib1'
    //include:'lib2'

这样我们实际就只include了lib1,当我们sync或者build\clean的时候就没有lib2的事啦,这样解决时间。

减少构建过程中的I/O操作:I/O操作,如copy文件,访问git等,Debug版本配置的minSdkVersion 21+(builder faster)

Instant Run:

注入依赖技术,不需要安装就可以达到更新apk的目的。 详细参考Instant Run: How Does it Work?!

最后还想说几点:

1、不仅仅只是快1mms:

这里的每一项我都尝试配置使用过,实际测试基本可以解决个10多mms,当然也得看你的项目当然的状态,以及你编辑修改的东西。虽然解决下的时间不是很显眼,但是如果用百分数来看可以提高到20%的速度你就能感受到其中的价值啦,然后再乘以你编译的次数,绝对的节约时间。

2、组件化的优势:

另外一点我曾在知乎上也回答过,尽量的模块化项目内容,对于一些功能特点我们都可以抽象成小组件在demo中调试完成,然后挪到工程里边。这样不仅丰富了自己的公共库,也提高了工作效率。

3、java调试或者说单元测试:

对于纯java的业务(相信在model层里有很多都是)我们可以通过只运行java来调试,比如我最近在做单词的短语匹配算法,那么我在调试的时候再class里边写个main方法(很low的方案),仅调试算法部分就好,然后实际还是要使用单元测试更忧。

四、Maven仓库

1、Jcenter和mavenCentral

  • 区别:
    • jcenter比mavenCentral仓库要全,性能优,响应要快但有一些构建还是仅存在maven里边。
    • jcenter上传lib流程更加简单。
  • 参数:
    • group 分组
    • artifact 名称
    • version 版本

2、maven私服

五、依赖以及统一依赖管理

  • 依赖种类
    • 库(library)依赖
      • aar
        • /AndroidManifest.xml (mandatory)
        • /classes.jar (mandatory)
        • /res/ (mandatory)
        • /R.txt (mandatory)
        • /assets/ (optional)
        • /libs/*.jar (optional)
        • /jni//*.so (optional)
        • /proguard.txt (optional)
        • /lint.jar (optional)
      • jar
    • 工程(moudle)依赖
  • Dependency configurations
    • compile
    • runtime
    • testCompile
    • testRuntime
    • xxxCompile
  • 依赖方式
    • 本地依赖(离线:AS可以配置离线,那么远程依赖会使用本地缓存的,而不再去请求加载)
    • 远程仓库依赖
      • @aar
        • 指定了文件类型为aar
        • You shouldn't need to put @aar if the pom declares the packaging as aar (which is true in your case).
      • +1 自动更新最新版本(建议Release版本中使用)
  • 依赖配置
    • build.gradle
      • testCompile group: 'junit', name: 'junit', version: '4.+'
      • compile group: 'org.hibernate', name: 'hibernate-core', version: '3.6.7.Final'
      • compile com.tencent.bugly:crashreport:2.1.5+
      • compile 'com.stubworld.core:CorelibAndroid:1.0@aar'
    • project Structure
  • 统一依赖管理实际就是将每个moudle的依赖配置写到一个全局的配置文件中,方便我们管理。我们可以添加一个全局的config.gradle,然后在全局的build.gradle中apply一下。详解请参考文章搭建自己的maven私服,并使用Gradle统一依赖管理
  • 官方资料
  • How to distribute your own Android library through jCenter and Maven Central from Android Studio

五、多渠道BuildType+ProductFlavor=Build Variant

buildType称为构建类型,ProductFlavor称为定制产品。怎么理解呢?buldType默认有debug和release两个版本,而productFlavor默认是匿名的。我们可以这么理解,buildType是我们开发需要的通用类型,比如默认的调试、发行版本、也可以定制方便测试人员测试的版本,对于ProductFlavor就是我们需要对产品进行定制啦,不同的Product对应了独有的部分代码,比如我们要开发个免费版、企业版,这里是举个例子,主要还是要看你的产品构成。两者还有一个区别在于,buildType默认共享一个defaultConfig,但是不同的Product可以对应不同的xxxConfig,由此就可以拥有不同包名等。

下面教你如果给有多重依赖关系module配置多渠道,直接贴码说明: 我们假设A依赖了B、B依赖了C

//A为主工程   
productFlavors{
    free{ 
        //免费版
        minSdkVersion 21
    }
    pay{
        //付费版
        minSdkVersion 14
    }
}
buildTypes{
    release{

        resValue "string", "app_name", "CoolApp"
        signingConfig signingConfigs.release

        buildConfigField "String", "API_URL", '"http://api.dev.com/"'
        buildConfigField "boolean", "REPORT_CRASHES", "true"
    }
    debug{
        applicationIdSuffix ".debug"
        versionNameSuffix "-debug"
        resValue "string", "app_name", "CoolApp (debug)"
        signingConfig signingConfigs.debug

        buildConfigField "String", "API_URL", '"http://api.prod.com/"'
        buildConfigField "boolean", "REPORT_CRASHES", "false"
    }
}
dependencies{
    freeCompile project(path:'B',configuration:'freeRelease')
    freeCompile project(path:'B',configuration:'freeDebug')
    payCompile project(path:'B',configuration:'payRelease')
    payCompile project(path:'B',configuration:'payDebug')
}
//B为依赖工程 
publishNonDefault true //依赖工程这一句很重要,不然sync不过

productFlavors{
    free{
        }
    pay{            
    }
}
buildTypes{
    debug{

    }
    release{

    }
}
dependencies{
    freeCompile project(path:'C',configuration:'freeRelease')
    freeCompile project(path:'C',configuration:'freeDebug')
    payCompile project(path:'C',configuration:'payRelease')
    payCompile project(path:'C',configuration:'payDebug')
}

//在对应的module里面新建和main平级,和productFlavors名字相同的问题夹,并且将src等放在和main相同的路径下就ok啦,编译不同的渠道会找不同的文件。
  • 多渠道的意义
    • 可以配置不同的URL地址
    • 不同的名字、版本名、ApplicationId
    • 不同的签名配置
    • 不同的源码
    • 不同的manifest
    • 不同的sdk
  • 小结

    不同的产品要求、不同的生产环境,我们都可以通过配置多个渠道来达到我们的工作要求,省时省力。

六、Eclipse项目彻底转为AS

  • 1、策略

    • 步骤一:Eclipse导出,获得xxx.gradle
    • 步骤二:画出依赖关系图
    • 步骤三:调整文件结构,把文件结果弄成标准的,用资源管理器就可
    • 步骤四:修改build.gradle,修复依赖关系
    • 步骤五:建立gradle统一管理(对于大项目依赖负责,常更新的还是有必要的)
  • 2、坑

    • R文件不存在

    平时使用的时候也会出现这种问题,好好的sync的时候,一直报R文件找不到,clean后还是不行,我的解决方案是在settings.gradle文件里边注释掉报错的module所有上层的include,包括自身,sync一下没有问题。然后打开自己,sync没有问题,然后逐层向上打开工程,一切ok。

  • 3、技巧

    • 依赖关系理清,由下至上
    • 灵活使用settings.gradle,控制include,可以sync,build任何一个工程以及所依赖的工程,减少时间,方法查找问题,提高效率
    • 构建交给服务器(如果公司有服务器构建的话,对于大项目可以交给服务器来编译,解放自己的电脑来干别的工作)
    • clean、Invalidate Caches、Restart、删除.idea、.gradle

七、 只有一个快捷键:Acton or option

网络上有许多的快捷键大全之类的,看的密密麻麻的东西我就头晕,最后在知乎上有位大神提到AS只有一个快捷键,那就是Control+Shift+A,瞬间顿悟。所以后面我很少去既什么操作快捷键是什么,而只是研究快捷操作英文名是什么?比如说定位跳转到某一行,我只要control+shift+A,然后输入Line,就会列出所有有关line的操作,与此同时,每次操作我都会留意下跟在后边括号里边的快捷键是啥,如此往复循环一次,使用频率高的就给记住直接的快捷键,没有记住的模糊搜索下操作指令名称也能快速找到,如果没有可以去keymap里边自定义设置。关于操作,大概总结了以下几类:

  • 1、提示

    • 源码提示:Documentationctrl Ctrl+shilf+space 连续两次放大查看 再次点击缩小
    • 查看某个方法的调用路径:Call Hierachy Ctrl+alt+h
    • 查看某个方法的具体内容:Definition Ctrl+Shift+I
    • extends或者implement 的所有子类 chooseImplementation Ctrl+Alt+B \Ctrl+T
  • 2、编辑

    • 多行编辑

      • 同起点:按住Alt,同时鼠标上下走

      • 不同起点:按住Shift+Alt ,同时鼠标多处点击

    • 自动补全:Surround ,if.else,try/catch可以使用快捷键Ctrl+Alt+T (Code | Surround With)
  • 3、查找和替换 file find goto search replace

    • 打开文件:enter file name ctrl+shift+r 输入文件名
    • 打开文件-资源文件: ctrl+alt+F12 , 按住ctrl,鼠标点击
    • 打开一个类: enter class name ctrl+shift+t 输入类名
    • 跳转到一个方法:File Structure Ctrl+O 输入方法名
    • 打开某一个对象: symbol 输入对象名
    • 跳转到某一行:line 输入行数
    • 跳转到当期接口的实现方法:implementation(s) ctrl+t
    • 全部文件替换 Replace in path 比如你要替换整个工程真某个字符串
    • 书签 bookmarks 我的总结是有限的,实际在官方文档中有分类说明:

八、代码检查

  • 操作 Analyze | Inspect Code | 窗口显示
  • Android
    • lint
      • performance
        • Unused resources //没有使用到的资源(res)
  • class structure
    • File can be local //可以是局部变量的写成了全局变量
  • Imports
    • Unused import 没有使用的import
  • General
    • Default File Template Usage //有些没有使用模板的规范
  • Performance issues
    • 'StringBuffer'can be replaced with 'String'
  • Probable bugs
    • String comparison using '==', instead of 'equals()'
  • 简单举例,也可以在面板上直接输入文字搜索

九、调试

这里不再过多叙述啦,本人实际开发中使用的也是比较基础,不过有个技巧,当我们按住Alt键然后点击某一行代码,可以查看到这一行代码的执行结果。前提是这行代码已经执行过啦,觉得还是挺好用的。更多可以参考Android Studio代码调试大全学习。

十、 终极技巧

  • 合理使用插件:plugin
    • SimpleUML
    • MarkDown
  • 书签、喜欢分组(bookMarks、favorites)
  • 抓屏布局分析(LayoutInspector)

  • 宏(Edit|Macors)

这个和office办公软件有点类似,我们可以录制一段操作,然后播放就可以自动执行录制好的操作。

  • Templates(模板)

    • LiveTemplates (Settings|Live Teamplates)
      • .notnull 自动填充为空的判断
      • .for
      • 具体的有很多,在设置面板中查看
    • FileTemplates
      • SaveAsTemplates,可以将某一个class或者文件保存为模板,新建的时候直接生成引用。
    • File and Code Templates
      • File 的头部可能有声明作者、日期的注释,这里可以自动生成。
    • Project Templates
    • 前后缀(Editor|Code Style|Java)

      这个可以自动给变量添加一个前后缀,比如我们的成员变量常在前面加一个m。

  • Intentions

    意图,这里可以根据预设的代码结构来填充修改代码,比如我们有 a.equals("A"),如果我们按 Alt+Enter,就会有一个split的提示选项,点击之后代码就会变成 “A".equals(a)。具体有很多,打开设置面板自己看吧。

  • 版本管理

    • 分支代码对比 AS右下角的边框有分支可以看,点开就可以有许多的功能。前提是已经激活配置好git.
    • Annotate
  • layout布局遍历
  • 巧用Alt+Enter

当我们的代码报错的时候,当我们希望有更多功能的时候,按这个快捷键就会有很多惊喜。比如,我们在一个Class上使用该快捷键,就会有提示添加单元测试 Create Test ,然后一秒钟添加好了单元测试类。

  • 巧用Settings.gradle 我们可以通过注释include,达到自由管理项目中各个moudle,在我们编译出错的时候方便查问题。
  • 快捷键,有这个命令图就够啦。
  • 模板 Tools/Save File as a Template
  • 巧用AS无处不在的筛选排列显示
    • project里边的 project/android/FlattenPackages
    • Find Usage 结果列表中的 write
    • 以及每个工具面板中的筛选按钮
  • 巧用注解@NonNull (官方文档 Improve Code Inspection with Annotations)
  • 面板区域内直接输入搜索

我们在很多的面板,比如左侧的项目导航栏,可以直接输入字母模糊搜索你需要的文件。

  • LocalHistory

代码编辑的历史记录,方便找回意外丢失修改的代码。

  • 一些有趣的task

    • 根据git提交自动配置版本号

      defaultConfig {
      # Add 10000 to move past old SVN revisions.
       versionCode gitCommitCount() + 10000 ?: 0
      }
      
      def gitCommitCount() {
      try {
          def stdout = new ByteArrayOutputStream()
       exec {
           commandLine 'git', 'rev-list', '--count', 'HEAD'
      standardOutput = stdout
       }
      def commitCount = stdout.toString().trim().toInteger()
      return commitCount
       }
      catch (ignored) {
          return 0;
      }
      }
      
      def gitRevision() {
      try {
       def stdout = new ByteArrayOutputStream()
          exec {
          commandLine 'git', 'rev-list', 'HEAD', '-n', '1'
       standardOutput = stdout
           }
      def commitRevision = stdout.toString().trim()
       return commitRevision
       }
      catch (ignored) {
          return "(unknown revision)";
      }
      }
      
    • 其他参考

十一、 AndroidStudio 2.2版本特性

  • 设计
    • 布局编辑器
    • 约束布局
  • 开发
    • 改进C++支持
    • 示例浏览器
  • 构建
    • Instant Run 改进
    • Java 8 支持
    • Open JDK 支持
  • 测试
    • 模拟器增加了虚拟传感器
    • apk分析器
  • 官方原版介绍+视频

十二、总结

  • Gradle是一个强大的包装机;
  • maven远程依赖很给力;
  • 快捷键只有一个,那就是Ctrl+Shift+A;
  • 绝招有很多.套路太深,别说你不懂;
  • 妹纸都是浮云,我们天天双眼就盯着AndroidStudio,所谓日久生情。

十三、参考资料

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

android4.0以上实现Mqtt客户端

$
0
0
由于wmqtt.jar库在android4.0以上实现有问题会报MqttException Null异常,原因是该库只支持4.0以下版本。无奈只有寻找其他解决方案,最后选择的是Paho库中的client版本,org.eclipse.paho.client.mqttv3.jar。利用该库可以在android4.0以上正常连接Mqtt的服务器,博主用的android5.1进行实验的。
博主利用mqtt实现的主要功能是android端要与特定的智能硬件进行通信,而且是双向通信,android要给智能硬件发控制信息,智能硬件要给android返回状态信息;关于订阅与发布的id问题是其中的关键,android端要获取智能硬件的id,作为android发布信息的主题,在绑定完智能硬件之后,智能硬件发布以智能硬件的id+后缀作为智能硬件的发布信息id,而android端订阅智能硬件的id+后缀的主题。(启动的时候默认订阅服务器主题,服务器发送目前已经绑定了的智能硬件的主题,拉取主题列表。主题列表中有智能硬件在线、离线状态,更新客户端状态。外部设置进行其他智能硬件的主题订阅,在接口中添加主题订阅操作,在接口中添加自定义主题发布操作。)
android端为了每个活动都可以更方便得对确定主题的发布,与订阅主题的接收。在android建立一个基础活动类MqttBaseActivity,而其他需要进行mqtt操作的活动都继承该活动,减少活动中非主要业务代码冗余,提高代码可维护性。
要实现上述设想,需要进行3方面的工作:
    1.MqttService类实现;
    2.MqttBaseActivity类实现;
    3.主活动调用示例;
MqttService提供给外界的接口主要包括内容订阅,自定义主题消息发送,全局广播消息接收。
package com.splxtech.powermanagor.engine;

import java.util.Locale;

import org.eclipse.paho.client.mqttv3.MqttCallback;
import org.eclipse.paho.client.mqttv3.MqttClient;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttDefaultFilePersistence;
import org.eclipse.paho.client.mqttv3.MqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.eclipse.paho.client.mqttv3.MqttPersistenceException;
import org.eclipse.paho.client.mqttv3.MqttTopic;
import org.eclipse.paho.client.mqttv3.internal.MemoryPersistence;

import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.provider.Settings.Secure;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;

import com.splxtech.powermanagor.IMqttService;


public class MqttService extends Service implements MqttCallback
{
    public static final String DEBUG_TAG = "MqttService"; // Debug TAG

    private static final String MQTT_THREAD_NAME = "MqttService[" + DEBUG_TAG + "]"; // Handler Thread ID

    private static final String MQTT_BROKER = ""; // Broker URL or IP Address
    private static final int MQTT_PORT = 1883; // Broker Port

    public static final int MQTT_QOS_0 = 0; // QOS Level 0 ( Delivery Once no confirmation )
    public static final int MQTT_QOS_1 = 1; // QOS Level 1 ( Delevery at least Once with confirmation )
    public static final int MQTT_QOS_2 = 2; // QOS Level 2 ( Delivery only once with confirmation with handshake )

    private static final int MQTT_KEEP_ALIVE = 240000; // KeepAlive Interval in MS
    private static final String MQTT_KEEP_ALIVE_TOPIC_FORAMT = "/users/%s/keepalive"; // Topic format for KeepAlives
    private static final byte[]     MQTT_KEEP_ALIVE_MESSAGE = { 0 }; // Keep Alive message to send
    private static final int MQTT_KEEP_ALIVE_QOS = MQTT_QOS_0; // Default Keepalive QOS

    private static final boolean MQTT_CLEAN_SESSION = true; // Start a clean session?

    private static final String MQTT_URL_FORMAT = "tcp://%s:%d"; // URL Format normally don't change

    private static final String ACTION_START  = DEBUG_TAG + ".START"; // Action to start
    private static final String ACTION_STOP   = DEBUG_TAG + ".STOP"; // Action to stop
    private static final String ACTION_KEEPALIVE= DEBUG_TAG + ".KEEPALIVE"; // Action to keep alive used by alarm manager
    private static final String ACTION_RECONNECT= DEBUG_TAG + ".RECONNECT"; // Action to reconnect


    private static final String DEVICE_ID_FORMAT = "andr_%s"; // Device ID Format, add any prefix you'd like
    // Note: There is a 23 character limit you will get
    // An NPE if you go over that limit
    private boolean mStarted = false;   // Is the Client started?
    private String mDeviceId;       // Device ID, Secure.ANDROID_ID
    private Handler mConnHandler;     // Seperate Handler thread for networking

    private MqttDefaultFilePersistence mDataStore; // Defaults to FileStore
    private MemoryPersistence mMemStore; // On Fail reverts to MemoryStore
    private MqttConnectOptions mOpts; // Connection Options

    private MqttTopic mKeepAliveTopic; // Instance Variable for Keepalive topic

    private MqttClient mClient; // Mqtt Client

    private AlarmManager mAlarmManager; // Alarm manager to perform repeating tasks
    private ConnectivityManager mConnectivityManager; // To check for connectivity changes

    private LocalBroadcastManager localBroadcastManager;
    public static final String MQTT_RECE_MESSAGE_ACTION = "com.splxtech.powermanagor.engine.mqttservice.recemessage";
    /**
     * Start MQTT Client
     * @param
     * @return void
     */
    public static void actionStart(Context ctx) {
        Intent i = new Intent(ctx,MqttService.class);
        i.setAction(ACTION_START);
        ctx.startService(i);
    }
    /**
     * Stop MQTT Client
     * @param
     * @return void
     */
    public static void actionStop(Context ctx) {
        Intent i = new Intent(ctx,MqttService.class);
        i.setAction(ACTION_STOP);
        ctx.startService(i);
    }
    /**
     * Send a KeepAlive Message
     * @param
     * @return void
     */
    public static void actionKeepalive(Context ctx) {
        Intent i = new Intent(ctx,MqttService.class);
        i.setAction(ACTION_KEEPALIVE);
        ctx.startService(i);
    }

    /**
     * Initalizes the DeviceId and most instance variables
     * Including the Connection Handler, Datastore, Alarm Manager
     * and ConnectivityManager.
     */
    @Override
    public void onCreate() {
        super.onCreate();

        mDeviceId = String.format(DEVICE_ID_FORMAT,
                Secure.getString(getContentResolver(), Secure.ANDROID_ID));

        HandlerThread thread = new HandlerThread(MQTT_THREAD_NAME);
        thread.start();

        mConnHandler = new Handler(thread.getLooper());

        try {
            mDataStore = new MqttDefaultFilePersistence(getCacheDir().getAbsolutePath());
        } catch(MqttPersistenceException e) {
            e.printStackTrace();
            mDataStore = null;
            mMemStore = new MemoryPersistence();
        }

        mOpts = new MqttConnectOptions();
        mOpts.setCleanSession(MQTT_CLEAN_SESSION);
        // Do not set keep alive interval on mOpts we keep track of it with alarm's

        mAlarmManager = (AlarmManager) getSystemService(ALARM_SERVICE);
        mConnectivityManager = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE);
        localBroadcastManager = LocalBroadcastManager.getInstance(this);
    }

    /**
     * Service onStartCommand
     * Handles the action passed via the Intent
     *
     * @return START_REDELIVER_INTENT
     */
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        super.onStartCommand(intent, flags, startId);

        String action = intent.getAction();

        Log.i(DEBUG_TAG,"Received action of " + action);

        if(action == null) {
            Log.i(DEBUG_TAG,"Starting service with no action\n Probably from a crash");
        } else {
            if(action.equals(ACTION_START)) {
                Log.i(DEBUG_TAG,"Received ACTION_START");
                start();
            } else if(action.equals(ACTION_STOP)) {
                stop();
            } else if(action.equals(ACTION_KEEPALIVE)) {
                keepAlive();
            } else if(action.equals(ACTION_RECONNECT)) {
                if(isNetworkAvailable()) {
                    reconnectIfNecessary();
                }
            }
        }

        return START_REDELIVER_INTENT;
    }

    /**
     * Attempts connect to the Mqtt Broker
     * and listen for Connectivity changes
     * via ConnectivityManager.CONNECTVITIY_ACTION BroadcastReceiver
     */
    private synchronized void start() {
        if(mStarted) {
            Log.i(DEBUG_TAG,"Attempt to start while already started");
            return;
        }

        if(hasScheduledKeepAlives()) {
            stopKeepAlives();
        }

        connect();

        registerReceiver(mConnectivityReceiver,new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
    }
    /**
     * Attempts to stop the Mqtt client
     * as well as halting all keep alive messages queued
     * in the alarm manager
     */
    private synchronized void stop() {
        if(!mStarted) {
            Log.i(DEBUG_TAG,"Attemtpign to stop connection that isn't running");
            return;
        }

        if(mClient != null) {
            mConnHandler.post(new Runnable() {
                @Override
                public void run() {
                    try {
                        mClient.disconnect();
                    } catch(MqttException ex) {
                        ex.printStackTrace();
                    }
                    mClient = null;
                    mStarted = false;

                    stopKeepAlives();
                }
            });
        }

        unregisterReceiver(mConnectivityReceiver);
    }
    /**
     * Connects to the broker with the appropriate datastore
     */
    private synchronized void connect() {
        String url = String.format(Locale.US, MQTT_URL_FORMAT, MQTT_BROKER, MQTT_PORT);
        Log.i(DEBUG_TAG,"Connecting with URL: " + url);
        try {
            if(mDataStore != null) {
                Log.i(DEBUG_TAG,"Connecting with DataStore");
                mClient = new MqttClient(url,mDeviceId,mDataStore);
            } else {
                Log.i(DEBUG_TAG,"Connecting with MemStore");
                mClient = new MqttClient(url,mDeviceId,mMemStore);
            }
        } catch(MqttException e) {
            e.printStackTrace();
        }

        mConnHandler.post(new Runnable() {
            @Override
            public void run() {
                try {
                    mClient.connect(mOpts);

                    mClient.subscribe("hello", 0);

                    mClient.setCallback(MqttService.this);

                    mStarted = true; // Service is now connected

                    Log.i(DEBUG_TAG,"Successfully connected and subscribed starting keep alives");

                    startKeepAlives();
                } catch(MqttException e) {
                    e.printStackTrace();
                }
            }
        });
    }
    /**
     * Schedules keep alives via a PendingIntent
     * in the Alarm Manager
     */
    private void startKeepAlives() {
        Intent i = new Intent();
        i.setClass(this, MqttService.class);
        i.setAction(ACTION_KEEPALIVE);
        PendingIntent pi = PendingIntent.getService(this, 0, i, 0);
        mAlarmManager.setRepeating(AlarmManager.RTC_WAKEUP,
                System.currentTimeMillis() + MQTT_KEEP_ALIVE,
                MQTT_KEEP_ALIVE, pi);
    }
    /**
     * Cancels the Pending Intent
     * in the alarm manager
     */
    private void stopKeepAlives() {
        Intent i = new Intent();
        i.setClass(this, MqttService.class);
        i.setAction(ACTION_KEEPALIVE);
        PendingIntent pi = PendingIntent.getService(this, 0, i, 0);
        mAlarmManager.cancel(pi);
    }
    /**
     * Publishes a KeepALive to the topic
     * in the broker
     */
    private synchronized void keepAlive() {
        if(isConnected()) {
            try {
                sendKeepAlive();
                return;
            } catch(MqttConnectivityException ex) {
                ex.printStackTrace();
                reconnectIfNecessary();
            } catch(MqttPersistenceException ex) {
                ex.printStackTrace();
                stop();
            } catch(MqttException ex) {
                ex.printStackTrace();
                stop();
            }
        }
    }
    /**
     * Checkes the current connectivity
     * and reconnects if it is required.
     */
    private synchronized void reconnectIfNecessary() {
        if(mStarted && mClient == null) {
            connect();
        }
    }
    /**
     * Query's the NetworkInfo via ConnectivityManager
     * to return the current connected state
     * @return boolean true if we are connected false otherwise
     */
    private boolean isNetworkAvailable() {
        NetworkInfo info = mConnectivityManager.getActiveNetworkInfo();

        return (info == null) ? false : info.isConnected();
    }
    /**
     * Verifies the client State with our local connected state
     * @return true if its a match we are connected false if we aren't connected
     */
    private boolean isConnected() {
        if(mStarted && mClient != null && !mClient.isConnected()) {
            Log.i(DEBUG_TAG,"Mismatch between what we think is connected and what is connected");
        }

        if(mClient != null) {
            return (mStarted && mClient.isConnected()) ? true : false;
        }

        return false;
    }
    /**
     * Receiver that listens for connectivity chanes
     * via ConnectivityManager
     */
    private final BroadcastReceiver mConnectivityReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            Log.i(DEBUG_TAG,"Connectivity Changed...");
        }
    };
    /**
     * Sends a Keep Alive message to the specified topic
     * @return MqttDeliveryToken specified token you can choose to wait for completion
     */
    private synchronized MqttDeliveryToken sendKeepAlive()
            throws MqttConnectivityException, MqttPersistenceException, MqttException {
        if(!isConnected())
            throw new MqttConnectivityException();

        if(mKeepAliveTopic == null) {
            mKeepAliveTopic = mClient.getTopic(
                    String.format(Locale.US, MQTT_KEEP_ALIVE_TOPIC_FORAMT,mDeviceId));
        }

        Log.i(DEBUG_TAG,"Sending Keepalive to " + MQTT_BROKER);

        MqttMessage message = new MqttMessage(MQTT_KEEP_ALIVE_MESSAGE);
        message.setQos(MQTT_KEEP_ALIVE_QOS);

        return mKeepAliveTopic.publish(message);
    }
    /**
     * Query's the AlarmManager to check if there is
     * a keep alive currently scheduled
     * @return true if there is currently one scheduled false otherwise
     */
    private synchronized boolean hasScheduledKeepAlives() {
        Intent i = new Intent();
        i.setClass(this, MqttService.class);
        i.setAction(ACTION_KEEPALIVE);
        PendingIntent pi = PendingIntent.getBroadcast(this, 0, i, PendingIntent.FLAG_NO_CREATE);

        return (pi != null) ? true : false;
    }


    @Override
    public IBinder onBind(Intent arg0) {
        return iMqttService;
    }
    /**
     * Connectivity Lost from broker
     */
    @Override
    public void connectionLost(Throwable arg0) {
        stopKeepAlives();

        mClient = null;

        if(isNetworkAvailable()) {
            reconnectIfNecessary();
        }
    }
    /**
     * Publish Message Completion
     */
    @Override
    public void deliveryComplete(MqttDeliveryToken arg0) {

    }
    /**
     * Received Message from broker
     */
    @Override
    public void messageArrived(MqttTopic topic, MqttMessage message)
            throws Exception {
        Intent intent = new Intent(MQTT_RECE_MESSAGE_ACTION);
        intent.putExtra("Topic",topic.getName());
        intent.putExtra("Message",message.getPayload());
        localBroadcastManager.sendBroadcast(intent);
        Log.i(DEBUG_TAG,"  Topic:\t" + topic.getName() +
                "  Message:\t" + new String(message.getPayload()) +
                "  QoS:\t" + message.getQos());
    }
    /**
     * MqttConnectivityException Exception class
     */
    private class MqttConnectivityException extends Exception {
        private static final long serialVersionUID = -7385866796799469420L;
    }

    private IMqttService.Stub iMqttService = new IMqttService.Stub(){
        @Override
        public boolean mqttSubscribe(String topic,int mqttQOS)
        {
            if(isConnected()) {
                try {
                    mClient.subscribe(topic, mqttQOS);
                    return true;
                } catch (MqttException e) {
                    e.printStackTrace();
                    return false;
                }
            }
            else
            {
                return false;
            }
        }
        @Override
        public boolean mqttPubMessage(String topic,String Message,int mqttQOS)
        {
            if(isConnected())
            {
                MqttTopic mqttTopic = mClient.getTopic(topic);
                MqttMessage message = new MqttMessage(Message.getBytes());
                message.setQos(mqttQOS);
                try{
                    mqttTopic.publish(message);
                    return true;
                }
                catch (MqttException e)
                {
                    e.printStackTrace();
                    return false;
                }
            }
            else {
                return false;
            }
        }

    };
}
作者:li3007liuu 发表于2016/10/19 12:23:28 原文链接
阅读:18 评论:0 查看评论

swift 常用动画

$
0
0

一、位置变化动画

  UIView.animateWithDuration(1) {

            self.viKuai.center.x = self.view.bounds.width - self.viKuai.center.x

        }

        UIView.animateKeyframesWithDuration(1, delay: 0.5, options: [], animations: {

            self.viKuai2.center.y = self.view.bounds.height - self.viKuai2.center.y
           //self.viKuai3.center.x = self.view.bounds.width - self.viKuai3.center.x
          //self.viKuai3.center.y = self.view.bounds.height - self.viKuai3.center.y

            }, completion: nil)


        UIView.animateKeyframesWithDuration(1, delay: 1, options: [], animations: {

            //self.viKuai2.center.y = self.view.bounds.height - self.viKuai2.center.y
            self.viKuai3.center.x = self.view.bounds.width - self.viKuai3.center.x
            self.viKuai3.center.y = self.view.bounds.height - self.viKuai3.center.y

            }, completion: nil)

二、透明度动画

   UIView.animateWithDuration(1) {

            self.viKuang.alpha = 0.1

        }

三、大小变化动画

   UIView.animateWithDuration(1) {

            self.viKuang.transform = CGAffineTransformMakeScale(2, 2)

        }

四,颜色变化动画

     UIView.animateWithDuration(1) {

            self.viKuang.backgroundColor = UIColor.redColor()

            self.lb.textColor = UIColor.whiteColor()

        }

五、旋转动画

 func spin(){

        print("1=====%@",NSDate.init(timeIntervalSinceNow: 0))





        UIView.animateKeyframesWithDuration(1, delay: 0, options:.CalculationModeLinear, animations: {

            self.viKuang.transform = CGAffineTransformRotate(self.viKuang.transform, CGFloat(M_PI))



            }, completion: {(finished) -> Void in

                print("2=====%@",NSDate.init(timeIntervalSinceNow: 0))

                self.spin()    

                })
    }

========点击下载代码demo========

作者:u012903898 发表于2016/10/19 12:55:28 原文链接
阅读:17 评论:0 查看评论

IOS推送总结

$
0
0

此文主要以证书生成配置为主,实现简单推送,部分截图与内容来自于互联网,若对大家有所帮助,还请给个赞O(∩_∩)O~~。如有误,请指出,一起探讨。

一、 推送原理


Provider是指某个iPhone软件的Push服务器。APNS 是Apple Push Notification Service(Apple Push服务器)的缩写,是苹果的服务器。上图可以分为三个阶段。
* 第一阶段:Push服务器应用程序把要发送的消息、目的iPhone的标识打包,发给APNS。
* 第二阶段:APNS在自身的已注册Push服务的iPhone列表中,查找有相应标识的iPhone,并把消息发到iPhone。
* 第三阶段:iPhone把发来的消息传递给相应的应用程序, 并且按照设定弹出Push通知。

1. 应用程序注册消息推送。
2. IOS跟APNS Server要deviceToken。应用程序接受deviceToken。
3. 应用程序将deviceToken发送给PUSH服务端程序。
4. 服务端程序向APNS服务发送消息。
5. APNS服务将消息发送给iPhone应用程序。无论是iPhone客户端跟APNS,还是Provider和APNS都需要通过证书进行连接的。

二、 证书的创建

1. 创建本地请求证书文件–CertificateSigningRequest

  • 钥匙串访问 –> 证书助理 –> 从证书颁发机构请求证书;
  • 填写邮箱和名称(可以随意填写) –> 存储到磁盘
  • 选择继续-保存至桌面即可(此文件可长期重复使用,只作为本机器的一个识别作用)

2. 进入苹果开发者网站,生成证书以及profiles

依次选择Member Center - Certificates, Identifiers &Profiles - Certificates
C95BD68F-E6FA-49F4-8ECB-C72902F41D4D.png
选择当前要设置通知的APP IDs
这里我以新建一个APP IDs为例,若公司已有项目APP IDs,则略过此步
选择右上角”+”号创建一个APPID
填写name以及Bundle ID
A93F9CAF-20CA-47B8-8DA2-B46AF441C089.png
Bundle ID需要与Xcode里项目Bundle ID一致
6F810F29-E7DF-4190-A069-457036873A10.png
勾选push notification 并完成提交
找到刚创建好的APP IDs,可以看到下面的通知选项处提示Configurable,表示证书还没配置,点击Edit进行编辑
这里看到推送证书分为2个版本,一个开发模式,一个生产模式,即我们在开发测试时使用开发模式证书,发布上线后采用生产模式证书,两个都要创建(本次只作开发模式演示,当然创建生产版证书的步骤也是一样的)
Choose File选择最开始创建在桌面的Request文件
1F5ABE01-3DD1-487D-B7D4-8470FE2FD686.png
创建证书OK后 下载到本地
86A5F026-7B97-41C4-A6BB-C3D7341D8A23.png
开发版和生产版证书都创建好后,此时这里已经都是启用状态了。
生成XCODE使用的provisioning文件,该文件用于真机调试:
0D1B1B3A-FAB6-47BD-A721-C391CC63F0F8.png
生成过程:
进入developer.apple.com,选择member center - Certificates, Identifiers & Profiles - Provisioning Profiles,然后选择创建Provisioning file,接着选择iOS App Development ,下一步选择AppId,选中之前建立的支持push的appid,接着下一步选择支持push的certificate,下一步勾选需要支持的device id,最后一步设置provisioning文件的文件名,这样provisioning文件就生成了。

3. 生成服务端使用的证书文件

  • 首先双击前面保存的cer文件,此时会打开“钥匙串访问”软件,里面会出现一个Apple Development IOS push services证书,一个公用密钥和一个专用秘钥,秘钥的名称与证书助理中填写的名称一致。
  • 选中证书,导出为 apns-dev-cert-development.p12 文件
  • 选中专有秘钥,导出为apns-dev-key-development.p12文件
  • 通过终端命令将这些文件转换为PEM格式:
  • 设置密码,建议所有都为一个密码

    openssl pkcs12 -clcerts -nokeys -out apns-dev-cert-development.pem -in apns-dev-cert-development.p12
    openssl pkcs12 -nocerts -out apns-dev-key-development.pem -in apns-dev-key-development.p12
  • 最后, 需要将两个pem文件合并成一个apns-dev.pem文件,此文件在连接到APNS时需要使用:
    “`
    cat apns-dev-cert-development.pem apns-dev-key-noenc-development.pem > apns-dev-development.pem

####4. 测试证书
执行下面命令:

telnet gateway.sandbox.push.apple.com 2195(apns的测试环境)
telnet gateway.push.apple.com 2195(apns的正式环境)

它将尝试发送一个规则的,不加密的连接到APNS。如果你看到下面的反馈,那说明你的MAC能够到达APNS。按下Ctrl+C关闭连接。如果得到一个错误信息,那么你需要确保你的防火墙允许2195端口,一般这里都不会出现什么问题。
![终端显示](http://upload-images.jianshu.io/upload_images/1085368-a72adc48d5be13db.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
下面我们使用生成的SSL证书和私钥来设置一个安全的链接去链接苹果服务器,执行命令如下:

openssl s_client -connect gateway.sandbox.push.apple.com:2195 -cert apns-dev-cert-development.pem -key apns-dev-key-development.pem
“`
执行之后需要输入密码,就是上面我们设置的密码即可。
如果链接是成功的,你可以随便输入一个字符,按下回车,服务器就会断开链接,如果建立连接时有问题,OpenSSL会给你返回一个错误信息。
输出结果如下,则说明是正确链接了,把上述合并的apns-dev-development.pem给服务端使用。
ECCD5164-A95C-4777-A7CF-EA60D6D73398.png

如果有错误,请检查是否导出正确的推送证书。

作者:demon_zero 发表于2016/10/19 12:56:15 原文链接
阅读:20 评论:0 查看评论

关于使用AsyncHttpClient做断点上传功能时无法回调进度的问题

$
0
0

在使用AsyncHttpClient做简单的非断点上传功能时,我们要想实时检测任务的开始、结束以及进度,需要实现AsyncHttpResponseHandler,并复写其各种onXXX()方法。代码如下:


  1. @Override
  2. public void onSuccess(int statusCode, Header[] headers, byte[] responseBody) {
  3. }
  4. @Override
  5. public void onFailure(int statusCode, Header[] headers, byte[] responseBody, Throwable error) {
  6. }
  7. @Override
  8. public void onProgress(long bytesWritten, long totalSize) {
  9. }
  10. @Override
  11. public void onCancel() {
  12. }
  13. @Override
  14. public void onFinish() {
  15. }
  16. @Override
  17. public void onRetry(int retryNo) {
  18. }
  19. @Override
  20. public void onStart() {
  21. }

其中,在onProgress里,我们可以得到上传的进度。以上说的是简单上传,运行一点问题都没有。
可是当我按照服务端提供的接口做断点上传时,onProgress就不能正常的返回进度了。为了找出原因,我对两种情况的http请求都进行了抓包,发现原因出在http请求头里。在简单上传时,请求头里的"Content-Type"字段默认为 "multipart/form-data"的形式,而服务端给我的接口中,请求头里的"Content-Type"必须是"application/octet-stream"。
为什么只是改了个请求头就不能回调进度了呢?我想只有在框架源码中才能找到答案。只有找到onProgress这个回调最原始的地方才能知道原因。我先在AsyncHttpResponseHandler里找到了这个onProgress方法:

  1. /**
  2. * Fired when the request progress, override to handle in your own code
  3. *
  4. * @param bytesWritten offset from start of file
  5. * @param totalSize total size of file
  6. */
  7. public void onProgress(long bytesWritten, long totalSize) {
  8. AsyncHttpClient.log.v(LOG_TAG, String.format("Progress %d from %d (%2.0f%%)", bytesWritten, totalSize, (totalSize > 0) ? (bytesWritten * 1.0 / totalSize) * 100 : -1));
  9. }

发现它是在一个handleMessage方法的一个等于PROGRESS_MESSAGE 的case分支中被调用的:

  1. case PROGRESS_MESSAGE:
  2. response = (Object[]) message.obj;
  3. if (response != null && response.length >= 2) {
  4. try {
  5. onProgress((Long) response[0], (Long) response[1]);
  6. } catch (Throwable t) {
  7. AsyncHttpClient.log.e(LOG_TAG, "custom onProgress contains an error", t);
  8. }
  9. } else {
  10. AsyncHttpClient.log.e(LOG_TAG, "PROGRESS_MESSAGE didn't got enough params");
  11. }
  12. break;

接着找到这个PROGRESS_MESSAGE 是在sendProgressMessage方法中传递过来的。而这个sendProgressMessage方法是复写的方法。它是在AsyncHttpResponseHandler的父类ResponseHandlerInterface中被定义的。

  1. @Override
  2. final public void sendProgressMessage(long bytesWritten, long bytesTotal) {
  3. sendMessage(obtainMessage(PROGRESS_MESSAGE, new Object[]{bytesWritten, bytesTotal}));
  4. }
接下来只要能找到用ResponseHandlerInterface来调用sendProgressMessage的地方差不多就能找到问题所在了。果然在SimpleMultipartEntity类中,我找到了这个方法被调用的地方,它在updateProgress方法中被调用,从名字也可以看出这是用来更新进度的。

  1. private void updateProgress(long count) {
  2. bytesWritten += count;
  3. progressHandler.sendProgressMessage(bytesWritten, totalSize);
  4. }

再看这个updateProgress在哪几个地方被调用了:首先在这个writeTo方法中被调用了2次

  1. @Override
  2. public void writeTo(final OutputStream outstream) throws IOException {
  3. bytesWritten = 0;
  4. totalSize = (int) getContentLength();
  5. out.writeTo(outstream);
  6. updateProgress(out.size());
  7. for (FilePart filePart : fileParts) {
  8. filePart.writeTo(outstream);
  9. }
  10. outstream.write(boundaryEnd);
  11. updateProgress(boundaryEnd.length);
  12. }

另外在SimpleMultipartEntity的内部类FilePart类的writeTo方法中被调用了三次。同样从名字及代码中的file也可以看出,这一段其实是真正更新文件传输进度的地方!

  1. private class FilePart {
  2. .
  3. .
  4. .
  5. public void writeTo(OutputStream out) throws IOException {
  6. out.write(header);
  7. updateProgress(header.length);
  8. FileInputStream inputStream = new FileInputStream(file);
  9. final byte[] tmp = new byte[4096];
  10. int bytesRead;
  11. while ((bytesRead = inputStream.read(tmp)) != -1) {
  12. out.write(tmp, 0, bytesRead);
  13. updateProgress(bytesRead);
  14. }
  15. out.write(CR_LF);
  16. updateProgress(CR_LF.length);
  17. out.flush();
  18. AsyncHttpClient.silentCloseInputStream(inputStream);
  19. }
  20. }

但是看这里的代码写的没什么问题,为什么会没有被调用呢?
再来仔细研究一下SimpleMultipartEntity这个类发现:类的开头有段注释:意思是这是个主要用来发送一个或多个文件的简单的分段实体(请求体)。

  1. /**
  2. * Simplified multipart entity mainly used for sending one or more files.
  3. */

也就是说,在用AsyncHttpClient进行上传文件的时候,是把文件封装成这个http请求体来操作的,那我们再来仔细看看他到底做了些什么。
该类只有一个构造方法,目的是把ResponseHandlerInterface的实例传过来,好进行回调操作。然后他搞了一些boundary相关的东西。后来通过抓包发现,在发出的请求中出现的分割线原来就是在这里写入的。由于服务端给出的断点续传接口要求不要分割线,因此我就把这里的分割线相关的全部注释掉了。

  1. public BreakpointFileEntity(ResponseHandlerInterface progressHandler) {
  2. // final StringBuilder buf = new StringBuilder();
  3. // final Random rand = new Random();
  4. // for (int i = 0; i < 30; i++) {
  5. // buf.append(MULTIPART_CHARS[rand.nextInt(MULTIPART_CHARS.length)]);
  6. // }
  7. //
  8. // boundary = buf.toString();
  9. // boundaryLine = ("--" + boundary + STR_CR_LF).getBytes();
  10. // boundaryEnd = ("--" + boundary + "--" + STR_CR_LF).getBytes();
  11. this.progressHandler = progressHandler;
  12. }

再来看除了构造方法外,重载最多的就是addPart方法。可以看到,除了流文件外,其他的都是最终将一个file文件封装成FilePart实例,然后添加到一个叫fileParts的集合中。然后在计算总大小(getContentLength)和写入(writeTo)的时候,再遍历集合来操作。
再来看另外两个方法createContentType和createContentDisposition,从名字可以看出是用来创建请求头里的ContentType和ContentDisposition的。

  1. private byte[] createContentType(String type) {
  2. String result = AsyncHttpClient.HEADER_CONTENT_TYPE + ": " + normalizeContentType(type) + STR_CR_LF;
  3. return result.getBytes();
  4. }
  5. private byte[] createContentDisposition(String key) {
  6. return (
  7. AsyncHttpClient.HEADER_CONTENT_DISPOSITION +
  8. // ": form-data; name=\"" + key + "\"" + STR_CR_LF).getBytes();
  9. ": attachment; name=\"" + key + "\"" + STR_CR_LF).getBytes();
  10. //attachment; filename="Folder.jpg"
  11. }
  12. private byte[] createContentDisposition(String key, String fileName) {
  13. return (
  14. AsyncHttpClient.HEADER_CONTENT_DISPOSITION +
  15. // ": form-data; name=\"" + key + "\"" +
  16. // "; filename=\"" + fileName + "\"" + STR_CR_LF).getBytes();
  17. ": attachment; name=\"" + key + "\"" +
  18. "; filename=\"" + fileName + "\"" + STR_CR_LF).getBytes();
  19. }

这两个方法主要是在FilePart的createHeader方法里,通过此方法返回一个header对象,供FilePart持有。由于我们的断点上传要求的http头的Content_Type和Content_Disposition分别是:"application/octet-stream"和“attachment; filename="xxxx”的,所以这两个地方我需要改过来。
另外,由于断点之后再续传,应该是从同一个文件的不同字节位置开始上传的,这个位置startPos是服务器那边返回过来的,所以原来的从开头位置读取文件的做法就要做相应的修改了。

  1. // 修改为从指定位置开始读取
  2. public void writeTo(OutputStream out) throws IOException {
  3. out.write(header);
  4. updateProgress(header.length);
  5. FileInputStream inputStream = new FileInputStream(file);
  6. inputStream.skip(startPos); // 从指定位置开始读
  7. final byte[] tmp = new byte[4096];
  8. int bytesRead;
  9. while ((bytesRead = inputStream.read(tmp)) != -1) {
  10. out.write(tmp, 0, bytesRead);
  11. updateProgress(bytesRead);
  12. }
  13. out.write(CR_LF);
  14. updateProgress(CR_LF.length);
  15. out.flush();
  16. AsyncHttpClient.silentCloseInputStream(inputStream);
  17. }

但是这个startPos怎么传到这里来是个问题。我们可以在FilePart中新增一个startPos的成员变量,然后重载一个带startPos参数的构造方法。

  1. // 新增构造方法
  2. public FilePart(String key, File file, int startPos, String type, String customFileName) {
  3. header = createHeader(key, TextUtils.isEmpty(customFileName) ? file.getName() : customFileName, type);
  4. this.file = file;
  5. this.startPos = startPos;
  6. }
然后在新增一个addPart方法,里面的add的FilePart对象的构造方法用我们刚才新增的带startPos的。

  1. // 新增
  2. public void addPart(String key, File file, int startPos, String type, String customFileName) {
  3. fileParts.add(new FilePart(key, file, startPos, normalizeContentType(type), customFileName));
  4. }

由于这个addPart方法是在外部的RequestParams类中Add file params地方调用的。因为RequestParams类是在HttpClint包中不好修改,所以我们可以复制一个RequestParams类命名为BreakpointRequestParams,然后把其中的Add file params一段,修改为以下部分,因为file文件是通过fileWrapper这个对象带过去的,所以我们同样可以用它把startPos参数带过去。

  1. // Add file params
  2. // for (ConcurrentHashMap.Entry<String, FileWrapper> entry : fileParams.entrySet()) {
  3. // FileWrapper fileWrapper = entry.getValue();
  4. // entity.addPart(entry.getKey(), fileWrapper.file, fileWrapper.contentType, fileWrapper.customFileName);
  5. // }
  6. // 新增
  7. for (ConcurrentHashMap.Entry<String, FileWrapper> entry : fileParams.entrySet()) {
  8. FileWrapper fileWrapper = entry.getValue();
  9. entity.addPart(entry.getKey(), fileWrapper.file, fileWrapper.startPos, fileWrapper.contentType, fileWrapper.customFileName);
  10. }
因此,就需要把BreakpointRequestParams中的FileWrapper这个内部类新增一个构造方法:

  1. public static class FileWrapper implements Serializable {
  2. public final File file;
  3. public final String contentType;
  4. public final String customFileName;
  5. public final int startPos;
  6. public FileWrapper(File file, String contentType, String customFileName) {
  7. this.file = file;
  8. this.contentType = contentType;
  9. this.customFileName = customFileName;
  10. this.startPos = 0;
  11. }
  12. // 新增
  13. public FileWrapper(File file,int startPos, String contentType, String customFileName) {
  14. this.file = file;
  15. this.contentType = contentType;
  16. this.customFileName = customFileName;
  17. this.startPos = startPos;
  18. }
  19. }
由于上面的for (ConcurrentHashMap.Entry<String, FileWrapper> entry : fileParams.entrySet())遍历的是fileParams这个map集合。所以我们再找到这个map装进数据的地方,并将此处的put方法的参数加上 startPos。

  1. /**
  2. * Adds a file to the request with both custom provided file content-type and file name
  3. *
  4. * @param key the key name for the new param.
  5. * @param file the file to add.
  6. * @param contentType the content type of the file, eg. application/json
  7. * @param customFileName file name to use instead of real file name
  8. * @throws FileNotFoundException throws if wrong File argument was passed
  9. */
  10. // public void put(String key, File file, String contentType, String customFileName) throws FileNotFoundException {
  11. // if (file == null || !file.exists()) {
  12. // throw new FileNotFoundException();
  13. // }
  14. // if (key != null) {
  15. // fileParams.put(key, new FileWrapper(file, contentType, customFileName));
  16. // }
  17. // }
  18. // 转为断点上传准备的
  19. // int startPos 传输的起始位置, int fragmentLength 传输碎片的长度
  20. public void put(String key, File file, int startPos, String contentType, String customFileName) throws FileNotFoundException {
  21. if (file == null || !file.exists()) {
  22. throw new FileNotFoundException();
  23. }
  24. if (key != null) {
  25. fileParams.put(key, new FileWrapper(file, startPos, contentType, customFileName));
  26. }
  27. }

再重载一个简单参数的put方法共客户端调用,这样客户端只用调用param.put(“file”,fileToUpload,startPos),就可以实现断点上传了。

  1. // 新增
  2. public void put(String key, File file, int startPos) throws FileNotFoundException {
  3. put(key, file, startPos, null, null);
  4. }
客户端调用代码:

  1. private void uploadSingleFile(final TranslistFileBean fileBean, final String startPos) {
  2. String token = ExitApplication.getInstance().getToken();
  3. AsyncHttpClient client = new AsyncHttpClient();
  4. String url = ControlEasyHttpUtils.getBaseUrlNoV1() + "upload?_token=" + token + "&folder_id=" + fileBean.getFolderId();
  5. BreakpointRequestParams params = new BreakpointRequestParams();
  6. File file = new File(fileBean.getPath());
  7. try {
  8. params.put("file", file, Integer.parseInt(startPos));
  9. } catch (FileNotFoundException e) {
  10. e.printStackTrace();
  11. }
  12. // 不加这一段文件名字会变乱码
  13. String encode = null;
  14. try {
  15. encode = URLEncoder.encode(fileBean.getName(), "UTF-8");
  16. encode = encode.replace("+", "%20"); // 把转码后的+好还原为空格
  17. } catch (UnsupportedEncodingException e) {
  18. e.printStackTrace();
  19. }
  20. // 添加请求头
  21. client.addHeader("Content-Length", (file.length() - Integer.parseInt(startPos)) + "");
  22. client.addHeader("Content-Disposition", "attachment;filename=\"" + encode + "\"");
  23. client.addHeader("Session-ID", fileBean.getSession_id());
  24. client.addHeader("X-Content-Range", "bytes " + startPos + "-" + (file.length() - 1) + "/" + (file.length()));
  25. client.addHeader("Content-Type", "application/octet-stream");
  26. BreakpointAsyncHttpResponseHandler responseHandler = new BreakpointAsyncHttpResponseHandler(mContext, fileBean);
  27. // 设置 到了该获取加密进度的时候的监听
  28. responseHandler.setTime2GetPackPersentListener(this);
  29. RequestHandle requestHandle = client.post(mContext, url, params, responseHandler);
  30. //把 上传线程 添加到全局map变量中,或者替换原来的
  31. ExitApplication.getInstance().mCancelableMap.put(fileBean, requestHandle);
  32. }

这样如果是第一次上传,startPos 设置为"0",如果是续传,就传服务器返回的字节位置即可。这种情况下是可以上传成功的,但还是有个问题,上传上去的文件发现已坏,后来发现原来是文件头部被写入了http请求头中的信息,也就是FilePart类中的header信息也被写入到文件中去了,于是,我就header的写入注释掉了,发现问题解决了。

  1. // 修改为从指定位置开始读取
  2. public void writeTo(OutputStream out) throws IOException {
  3. // 这里如果不把header去掉的话,就会将头部一些信息写入到文件开头里,造成文件损坏!
  4. // out.write(header);
  5. // updateProgress(header.length);
  6. FileInputStream inputStream = new FileInputStream(file);
  7. inputStream.skip(startPos); // 从指定位置开始读
  8. final byte[] tmp = new byte[4096];
  9. int bytesRead;
  10. while ((bytesRead = inputStream.read(tmp)) != -1) {
  11. out.write(tmp, 0, bytesRead);
  12. updateProgress(bytesRead);
  13. }
  14. out.write(CR_LF);
  15. updateProgress(CR_LF.length);
  16. out.flush();
  17. AsyncHttpClient.silentCloseInputStream(inputStream);
  18. }

需要完整代码的请在留言里注明邮箱,谢谢!


作者:woshiwangbiao 发表于2016/10/19 12:57:50 原文链接
阅读:23 评论:0 查看评论

原型模式(Prototype pattern)

$
0
0
定义

原型模式是一种非常简单的是模式,属于创建型设计模式的一种。原型模式(Prototype)即应用于“复制”操作的模式,此模式最初定义在《设计模式》(Addison-Wesley,1994),定义为:“使用原型实例指定创建对象的种类,并通过复制这个原型创建新的对象”。简单来理解就是根据这个原型创建新的对象,这种创建是指深复制,得到一份新的内存资源,而不是一个新的指针引用。我们都知道,创建型模式一般是用来创建一个新的对象,然后我们使用这个对象完成一些对象的操作,我们通过原型模式可以快速的创建一个对象而不需要提供专门的new()操作就可以快速完成对象的创建,这无疑对于快速的创建一个新的对象是一种非常有效的方式。

结构

原型模式结构如下页上图所示:

       

客户(Client):使用原型对象的客户程序,客服端知道抽象的Prototype类
抽象原型(Prototype):规定了具体原型对象必须实现的接口(如果要提供深拷贝,则必须具有实现clone的规定)
具体原型(ConcretePrototype):从抽象原型派生而来,是客户程序使用的对象,即被复制的对象。此角色需要实现抽象原型角色所要求的接口。

Prototype类中包括一个Clone方法,Client知道Prototype,调用其复制方法clone即可得到多个实例,不需要手工去创建这些实例。
Prototype声明了赋值自身的接口,作为Prototype的子类,ConcretePrototype实现了Concrete复制自身的clone操作,这里的客户端通过请求原型复制其自身,创建一个新的对象。

解决什么问题?

1:解决了每次创建新的对象,都需要alloc init,这样就造成了代码要直接访问具体的类,也就增加了代码的耦合度,编码了使用标准方式创建新对象的固有代价。
2:避免创建工厂类的子类(例如抽象工厂模式)
3:通过copy能够保存对象当时的状态

适用性

原型模式的主要思想是基于现有的对象克隆一个新的对象出来,一般是由对象的内部提供克隆的方法,通过该方法返回一个对象的副本,特别是以下的几个场景的时候,可能使用原型模式更简单也效率更高。

1)需要创建的对象应独立于其类型与创建方式。
2)当要实例化的类是在运行时刻指定,例如,通过动态装载
3)为了避免创建一个与产品类层次平行的工厂类层次时,不通过工厂方法或者抽象工厂来控制产品的创建过程,想要直接复制对象
4)当一个类的实例只能有几个不同状态组合中的一种时。建立相应数目的原型并克隆它们可能比每次用合适的状态手工实例化该类更方便一些。(也就是当我们在处理一些对象比较简单,并且对象之间的区别很小,可能只是很固定的几个属性不同的时候,可能我们使用原型模式更合适)
5)类不容易创建,比如每个组件可把其他组件作为子节点的组合对象,复制已有的组合对象并对副本进行修改会更容易

实现原理

1:Swift/Objective C并不支持抽象基类或者抽象方法。但是可以使用协议类似定义一个抽象的“基类”,定义通用的属性,方法,以及复制方法。
2:具体的类负责实现复制方法,以及公用的方法。
3:通过抽象基类的接口创建对象

深复制与浅复制

非指针型实例变量没有浅复制与深复制之分,像布尔型、整型、浮点型。浅复制与深复制是针对指针型实例变量说的,浅复制就只是复制了指针到副本中,原始对象与副本共享内存数据;深复制就是把内存的资源也复制了,原始对象和副本分别对应自己的内存数据。

如果对象有个指针型成员指向内存的某个资源,那么如何复制这个对象呢?你会只是复制指针的值传给副本的新对象吗?指针只是存储内存中资源地址的占位符。在复制操作中,如果只是将指针复制给新对象(副本),那么底层的资源实际上仍然由两个实例在共享,如下图:

           
复制ConcretePrototype的场景,只复制了资源指针,实际的资源并没有复制

在ConcretePrototype的clone操作中,把资源指针的值复制到新的副本,尽管ConcretePrototype的实例生成了一个同类型的实例作为其副本,但两个实例的指针仍然指向内存中的同一资源。因此只复制了指针值而不是实际资源,这称为浅复制。

那么什么是深复制呢?深复制是指不仅是赋值指针值,还赋值指针所指向的资源,同一个clone操作的深复制如下图:clone操作不只是简单的复制资源指针,还要生成内存中实际的资源的真正副本。因此副本对象的指针指向了内存中不同位置的同一资源(内容)的副本。

      
 与上图类似,但是复制过程中对内存中的实际资源进行了赋值

使用Cocoa Touch框架的对象复制

Cocoa Touch框架为NSObject的派生类提供了实现深复制的协议,NSObject的子类需要实现NSCoping协议- (id)copyWithZone:(NSZone *)zone.NSObject有一个实例方法(id)copy,这个方法默认调用了[self copyWithZone:nil].对于采纳了NSCoping协议的子类,需要实现这个方法,否则将引发异常。在iOS中,这个方法保持新的副本对象,然后将其返回。此方法的调用者要负责释放返回的对象。

多数情况下,深复制的实现看起来并不分复杂,其思路是复制必需的成员变量与资源,传给此类的新实例,然后返回这个新实例。在Objective-C中使用原型模式, 首先要遵循NSCoping协议(OC中一些内置类遵循该协议, 例如NSArray, NSMutableArray等)。刚才我们提到了深复制, 如下图所示:


具体Demo,直接看代码

struct Part{
    var name: String
    var price: Double
    var brand: String
    
    init(name: String,price: Double, brand: String) {
        self.name = name
        self.price = price
        self.brand = brand
    }
}

struct Service {
    var name: String
    var laborDurationInMinutes: Int
    
    init(name: String, laborDurationInMinutes: Int) {
        self.name = name
        self.laborDurationInMinutes = laborDurationInMinutes
    }
}

struct Mechanic {
    static var id: Int = 0
    var id: Int
    var name: String
    
    init(name: String) {
        self.name = name
        Mechanic.id += 1
        self.id = Mechanic.id 
    }
}
由上可知,Part是一个简单的结构体,拥有namepricebrand等属性,并在初始化期间设置值,Service也是相同的模式,在初始化期间设置namelaborDurationInMinutes两个基本属性,Mechanic也是一样,拥有基本属性并初始化。

class CorporateQuote{
    var services: [Service]
    var price: Double
    var parts: [Part]
    var numberOfCars: Int
    var startTime: NSDate?
    var mechanics: [Mechanic]
    var client: String?
    var address: String?
    
    init(services: [Service],
         price: Double,
         parts: [Part],
         numberOfCars: Int,
         mechanics: [Mechanic]) {
        
        self.services = services
        self.price = price
        self.parts = parts
        self.numberOfCars = numberOfCars
        self.mechanics = mechanics
        
    }
    
    func clone() -> CorporateQuote {
        return CorporateQuote(services: self.services,
                              price: self.price,
                              parts: self.parts,
                              numberOfCars: self.numberOfCars,
                              mechanics: self.mechanics)
    }
}

由上面代码可知,我们的CorporateQuote类中有servicespricepartsnumberOfstartTimeCarsmechanicsclientaddress等属性,并且在初始化方法中,我们设置了servicespricepartsnumberOfCarsmechanics等属性。并且拥有一个clone方法来获取CorporateQuote类的实例对象,并且clone方法所使用的参数跟之前实例使用参数一样,这里就是实现了复制当前的CorporateQuote。但是这对我们有什么用处呢?下面一起看看测试代码:

func testCorporateQuote(){
        var steve = Mechanic (name: "Steve Brimington")
        var mike = Mechanic(name: "Mike Fulton")
        var ali = Mechanic (name: "Ali Belevue")
        
        var corporateMechanics = [steve, mike, ali]
        
        var brakePadReplacement = Service(name: "Brake Pad Replacement", laborDurationInMinutes: 100)
        var oilChange = Service(name: "Oil Change", laborDurationInMinutes: 65)
        var rotateTires = Service(name: "Roate Tires", laborDurationInMinutes: 45)
        
        var corporateServices = [brakePadReplacement, oilChange, rotateTires]
        
        var corporateParts = [Part(name: "Brake Pads Front", price: 25.65, brand: "ACME Pads"),
                     Part(name: "Filter", price: 8.99, brand: "ACME Pads"),
                     Part(name: "Synthetic Oil", price: 15.19, brand: "ACME Pads"),
                     Part(name: "Brake Pads Rear", price: 32.65, brand: "ACME Pads"),
                     Part(name: "Air Freshners", price: 3.65, brand: "ACME Pads")]
}
首先在testCorporateQuote函数中添加了如上的代码,代码简单,初始化了相关结构体并使用数组存储同类变量。如果我们需要更多这样的数据,我们可以模仿当前代码例子很轻松的编写更多的代码部分。但是假设在上面代码中,我们共有了mechanicsservicesparts等属性,而且创建corporateMechanicscorporateServices以及corporateParts的代价非常昂贵并且耗时,你是愿意只做一次创建操作还是操作多次呢?肯定是一次创建咯,下面我们来生成一个prototype corporateQuote,在测试函数corporateParts下添加如下代码:

 var prototypedCorporeateQuote = CorporateQuote(services: corporateServices,
                                                       price: 1488.99,
                                                       parts: corporateParts,
                                                       numberOfCars: 20,
                                                       mechanics: corporateMechanics)
  
 var googleQuote = prototypedCorporeateQuote.clone()
     googleQuote.client = "Google"
     googleQuote.startTime = NSDate(timeIntervalSinceNow: 0)
     googleQuote.address = "1600 Amphitheatre Pkwy, Mountain View"
           
 var facebookQuote = prototypedCorporeateQuote.clone()
     facebookQuote.client = "Facebook"
     facebookQuote.startTime =  NSDate(timeIntervalSinceNow: 1)
     facebookQuote.address = "1 Hacker Way, Menlo Park"
        
 var microsoftQuote = prototypedCorporeateQuote.clone()
     microsoftQuote.client = "Microsoft"
     microsoftQuote.startTime = NSDate(timeIntervalSinceNow: 2)
     microsoftQuote.address = "1085 La Avenida St, Mountain View"
现在我们有了prototypedCorporeateQuote变量,我们可以使用它的clone方法创建更多的其他corporate quotes并且拥有着相同的配置。他们将拥有相同的services、price、parts、numberOf、Carsmechanics。所有的操作都不涉及再次创建任意的这些值。而且我们仅仅是调用了clone()方法,就获得了复杂和昂贵的预配置CorporateQuote类的实例。这看起来非常的棒,这时我们能够修改每一个corporate quote,指定具体的客户(client)、日期、地址等信息了。

参考链接


作者:longshihua 发表于2016/10/20 9:53:18 原文链接
阅读:61 评论:0 查看评论

Swift 面向协议编程实践--数据结构之链表

$
0
0

标题有没有很标题党的样子?
实际上这篇文章改编自我对数据结构链表的笔记,只是我没有想到,当我想要用 Swift 来实现链表的时候,会发生这些有趣的事情。同时还让我对面向协议编程做了一次实践。
于是就有了这么一个唬人的标题,因为实际上我想复习的是链表,只是不小心发现了新大陆。我想这就跟 Bug 差不多,当你解决一个 Bug, 就会产生更多的 Bug.
程序员的生活就是有趣……

C 数据结构 – 线性表 之 链表

先复习一下链表吧,不然我总感觉不务正业。

  • 定义: 由同类型数据元素构成的有序序列线性结构。
    • 长度: 表中元素的个数
    • 空表: 没有元素的时候
    • 表头: 起始位置
    • 表尾: 结束位置
  • 常用操作
    • 初始化一个空表: List MakeEmpty()
    • 计算长度: int Length(List L)
    • 返回某个元素: ElementType FindK(int K, List L)
    • 查找元素位置: int Find(ElementType X, list L)
    • 插入元素: void Insert(ElementType X, int i, List L)
    • 删除某个元素: void Delete(int i, List L)
  • 实现方式
    • 数组
    • 链表

代码实现

据说一言不合就丢代码是一个程序员的好习惯。总之对这部分没有兴趣的同学可以跳过她,这只是很普通的链表实现,如果你学过数据结构,你就肯定不会陌生。

#include <stdio.h>
#include <stdlib.h>

#define Type int

// MARK: - 线性表 (链表 ChainList)

/// 链表结构
typedef struct ChainListNode {
    Type data;
    struct ChainListNode *next;
} ChainList;

/// 创建空链表初始值为 -1
ChainList *chainListInit() {
    ChainList *list = (ChainList *)malloc(sizeof(ChainList));
    list->data = -1;
    list->next = NULL;
    return list;
}

/// 计算链表长度
int chainListLength(ChainList *list) {
    ChainList *p = list;
    int i = 0;
    while (p) {
        p = p->next;
        i++;
    }
    return i;
}

/// 根据序号查找链表节点,序号从 0 开始
ChainList *chainListFindWithIndex(int index, ChainList *list) {
    ChainList *p = list;
    int i = 0;
    while (p != NULL && i < index) {
        p = p->next;
        i++;
    }
    return p;
}

/// 根据值查找链表节点
ChainList *chainListFindWithData(Type data, ChainList *list) {
    ChainList *p = list;
    while (p != NULL && p->data != data) {
        p = p->next;
    }
    return p;
}

/// 插入: 新建节点; 查找到插入节点的上一个节点; 新节点指向下一个节点; 上一个节点指向新节点。
ChainList *chainListInsert(Type data, int index, ChainList *list) {
    ChainList *p, *n;

    // 在头结点处插入
    if (index == 0) {
        n = (ChainList *)malloc(sizeof(ChainList));
        n->data = data;
        n->next = list;
        return n;
    }

    // 获取插入位置
    p = chainListFindWithIndex(index, list);
    if (p == NULL) {
        return NULL;
    }

    // 插入
    n = (ChainList *)malloc(sizeof(ChainList));
    n->data = data;
    n->next = p->next;
    p->next = n;
    return list;
}

/// 删除节点: 找到前一个节点; 获取删除节点; 前一个节点指向后一个节点; 释放删除节点
ChainList *chainListDelete(int index, ChainList *list) {
    ChainList *p, *d;

    // 如果列表为空
    if (list == NULL) {
        return NULL;
    }

    // 删除头元素
    if (index == 0) {
        p = list->next;
        free(list);
        return p;
    }

    // 查找删除元素的上一个位置
    p = chainListFindWithIndex(index - 1, list);
    if (p == NULL) {
        return NULL;
    }

    // 删除
    d = p->next;
    p->next = d->next;
    free(d);
    return list;
}

Swift 面向协议编程

开了那么大篇幅才讲到重点,如果我的职业是编辑的话,估计连睡大街都会被别的小编嫌碍眼。幸运的是,我是个程序员……一步步来是很重要的。

我使用泛型协议来定义了链表。并且使用协议扩展来实现关于链表的常用操作。这样只要任意一个类遵从该协议就可以自动获得这些实现而不需要进行额外的操作。(在我第一次感受到这个特性的强大时,内心被满满的 awesome 刷屏。如果你有我的博客的话,可以在上面看到我利用这个特性实现了 Notify 协议,只要在类声明上添加一下这个协议就可以自动获得一些非常建议的消息发送和接收功能。)

由于语言特性的不同,这个链表跟传统的链表有所不同。你应该通过一个指向头结点的 optional 变量来操作链表,当变量为 nil 时链表为空。

我在代码中除了实现该协议,还写了一个遵从该协议的类作为示例,并有它的调用方法。欢迎吐槽。

// MARK: - 线性表

/* 
在这个泛型协议中,我定义了一个准守 Equatable 协议的泛型 Element, 这是为了后面按值查找的时候可以直接使用等号进行判断。
但实际上这并不是一种聪明的做法,在进行判断的时候完全可以使用闭包来进行处理,这样就能获取更多的类型支持。这里只是为了能表现泛型类型约束的用法,才就这样做。
协议后面的 class 表示这个协议只能被 class 遵从,这种约束是必要的,如果你想使用 struct 类型来实现链表,不是说不可以,但这明显不是一个适用值拷贝场景的地方。
*/
protocol ChainList: class {
    associatedtype Element: Equatable
    var data: Element { get set }
    var next: Self? { get set }
    init()
}

extension ChainList {

    /// 返回当前节点到链表结尾的长度
    var length: Int {
        var i = 1
        var p: Self? = self
        while p?.next != nil {
            p = p?.next
            i += 1
        }
        return i
    }

    /// 查找元素
    subscript(index: Int) -> Self? {
        var i = 0
        var p: Self? = self
        while p != nil && i < index {
            p = p?.next
            i += 1
        }
        return p
    }

    /// 通过值来查找元素
    func find(value: Element) -> Self? {
        var p: Self? = self
        while p != nil && value != p?.data {
            p = p?.next
        }
        return p
    }

    /// 插入元素
    @discardableResult func insert(value: Element, to: Int) -> Self? {
        if to == 0 {
            let node  = Self.init()
            node.data = value
            node.next = self
            return node
        }

        if let pre = self[to - 1] {
            let node  = Self.init()
            node.data = value
            node.next = pre.next
            pre.next  = node
            return self
        }

        return nil
    }

    /// 删除元素
    @discardableResult func delete(index: Int) -> Self? {
        if index == 0 {
            return self.next
        }

        if let pre = self[index - 1] {
            pre.next = pre.next?.next
            return self
        }

        return nil
    }

}

// MARK: - 使用示例

/*
遗憾的是,由于协议当中使用了 Self 类型,所以遵从这个协议的类不得不设置为 final。也就是无法继承了。
*/
final class List: ChainList {
    typealias Element = String
    var data: List.Element = ""
    var next: List?
    required init() { }
}

var top: List? = List()
top?.data = "0"

for i in 1 ..< 5 {
    let _ = top?.insert(value: "\(i)", to: i)
}

if let length = top?.length {
    for i in 0 ..< length {
        print(top?[i]?.data)
    }

    for _ in 0 ..< length-1 {
        let _ = top?.delete(index: 1)
    }
}

print("Tag")

if let length = top?.length {
    for i in 0 ..< length {
        print(top?[i]?.data)
    }
}

print("Done")

/* 打印输出

Optional("0")
Optional("1")
Optional("2")
Optional("3")
Optional("4")
Tag
Optional("0")
Done
Program ended with exit code: 0

 */

如果你看到这里了,那说明你居然坚持看完了…… awesome ….
我相信你此刻的内心会有一个疑惑,为什么不直接用一个数组呢?Swift 的数组完美的解决了链表所需要解决的问题。是的,之所以这么做,原因只是 because we can …

作者:MuBinHuang 发表于2016/10/20 10:04:11 原文链接
阅读:63 评论:0 查看评论

观察者模式(从放弃到入门)

$
0
0

观察者模式(从放弃到入门)

今天分享第二个模式,观察者模式。相信作Android开发或者Java开发的童鞋都听说过这个模式,而且有很多流行的框架都是使用了观察者模式,例如著名的RxJava。而且Java中直接就自带了观察者模式,可见它有多常用。

然后文中的例子也是来自 Head First 设计模式,很棒的一本书,推荐大家看看。

废话不多说了,直接上需求:

需求1:错误示范

系统中有3个东东:气象站(温度,湿地,气压等),WeatherData对象(获取各种消息的变化),广告板(显示变化)。直接上一张书上的图好了:

这里写图片描述

注意一定,公告板只是一种展示,可能还有其他功能的公告板,获取相同的数据,但是做不同的处理。图中是做目前的状态展示,还可能有气候统计天气预测等。
意思就是说:我们可以将 WeatherData对象看作一个消息的来源,它的各项参数会不停的变化,他的每一次变化,都需要通知广告板做出相应的处理。

那么最简单的设计就来了,既然后很多公告板,那么我的WeatherData对象中,存放他们的引用,然后当我的值变化的时候,依次调用公告板的change() 函数通知变化不久搞定了。代码如下:

WeatherData .java

public class WeatherData {

    CurrentConditionDisplay cDisplay;
    StaticsDisplay sDisplay;
    ForecastDisplay fDisplay;

    public WeatherData(){
        cDisplay = new CurrentConditionDisplay();
        sDisplay = new StaticsDisplay();
        fDisplay = new ForecastDisplay();
    }

    // 冲气象站获取到数据时执行更新
    public void measurementsChanged(){
        double temp = getTemperature();
        double humidity = getHumidity();
        double pressure = getPressure();

        cDisplay.update(temp, humidity, pressure);
        sDisplay.update(temp, humidity, pressure);
        fDisplay.update(temp, humidity, pressure);
    }

    public float getTemperature(){
        //TODO: 冲气象站获取温度
        return 1.0f;
    }

    public float getHumidity(){
        //TODO: 冲气象站获取湿度
        return 2.0f;
    }

    public float getPressure(){
        //TODO: 冲气象站获取气压
        return 3.0f;
    }
}

其他代码都不用看,我们关注measurementsChanged() 方法,通过 getXXX() 方法获取到变化之后,通知本类中的3个广告板对象调用 update(…) 方法,做出相应的更新。

IUpdate.java

public interface IUpdate {
    void update(double temp, double humidity, double presure);
}

ForecastDisplay.java

public class ForecastDisplay implements IUpdate{
    public void update(double temp, double humidity, double presure) {
        System.out.println("I doing forecast work:"+temp+","+humidity+","+presure);
    }

}

StaticsDisplay.java

public class StaticsDisplay implements IUpdate{

    public void update(double temp, double humidity, double presure) {
        System.out.println("I doing statics work:"+temp+","+humidity+","+presure);
    }
}

CurrentConditionDisplay .java

public class CurrentConditionDisplay implements IUpdate{

    public void update(double temp, double humidity, double presure) {
        System.out.println("I doing current work:"+temp+","+humidity+","+presure);
    }
}

客户端调用:
Main.java

public class Main {

    public static void main(String[] args) {
        WeatherData wd = new WeatherData();
        wd.measurementsChanged();
    }
}

输出

I doing current work:1.0,2.0,3.0
I doing statics work:1.0,2.0,3.0
I doing forecast work:1.0,2.0,3.0

首先必须说,这样写问题真是太大了:

  1. 没有面相接口编程的思想(WeatherData类中直接使用CurrentConditionDisplay ,其实可以使用 IUpdate接口
  2. 如果还有新的广告板,需要修改WeatherData
  3. 如果通知的信号(温度,湿度,气压)再增加一个风力,那么IUpdate需要修改,所有的广告版都需要修改。

需求2:观察者模式

先上一张图:

这里写图片描述

现在我们要解决上面的3个问题:

认识观察者模式

在上面的示范中,我们其实可以看作,广告板(观察者Observer),时刻关注着 WeatherData (主题Subject) 的变化,如果有变化,则通知广告板更新。看看代码:

首先提供主题和观察者两个接口:

ISubject.java

public interface ISubject {
    void registObserver(IObserver o);
    void removeObserver(IObserver o);
    void notifyObservers();
}

IObserver .java

public interface IObserver {
    public void update(double temp, double humidity, double pressure);
}

WeatherData.java

public class WeatherData implements ISubject {
    private Set<IObserver> observers = new HashSet<IObserver>();

    double temp;
    double humidity;
    double pressure;

    public void registObserver(IObserver o) {
        observers.add(o);
    }

    public void removeObserver(IObserver o) {
        observers.remove(o);
    }

    public void notifyObservers() {
        for (IObserver o : observers) {
            o.update(temp, humidity, pressure);
        }
    }

    public void mesurementsChanged(double temp, double humidity, double pressure) {
        this.temp = temp;
        this.humidity = humidity;
        this.pressure = pressure;

        notifyObservers();
    }
}

用一个 Set observers 来装所有关注这个Subject的观察者,在 mesurementsChanged() 中先更新自己的数据,然后通知所有观察者并做出反应。

CurrentConditionDisplay.java

public class CurrentConditionDisplay implements IObserver{

    private ISubject weatherData;

    public CurrentConditionDisplay(ISubject weatherData){
        this.weatherData = weatherData;
        weatherData.registObserver(this);
    }

    public void update(double temp, double humidity, double presure) {
        System.out.println("I doing current work:"+temp+","+humidity+","+presure);
    }
}

ForecastDisplay.java

public class ForecastDisplay implements IObserver{

    private ISubject weatherData;

    public ForecastDisplay(ISubject weatherData){
        this.weatherData = weatherData;
        weatherData.registObserver(this);
    }

    public void update(double temp, double humidity, double presure) {
        System.out.println("I doing forecast work:"+temp+","+humidity+","+presure);
    }
}

StaticsDisplay.java

public class StaticsDisplay implements IObserver{

    private ISubject weatherData;

    public StaticsDisplay(ISubject weatherData){
        this.weatherData = weatherData;
        weatherData.registObserver(this);
    }

    public void update(double temp, double humidity, double presure) {
        System.out.println("I doing statics work:"+temp+","+humidity+","+presure);
    }
}

3个广告板类,每个包含一个要观察的主题,在初始化时添加要观察的主题,并将自己添加到主题的观察者列表中。

客户端调用:

Main.java

public class Main {

    public static void main(String[] args) {

        WeatherData weatherData = new WeatherData();

        IObserver cDisplay = new CurrentConditionDisplay(weatherData);
        IObserver sDisplay = new StaticsDisplay(weatherData);
        IObserver fDisplay = new ForecastDisplay(weatherData);

        weatherData.mesurementsChanged(1.0, 2.0, 3.0);
    }
}

改进的地方:

  1. 首先现在 WeatherData 可以添加任意多个观察者
  2. 主题和观察者解耦,类中保持的是接口对象。
  3. 观察主题的具体内容解耦了,WeatherData获取的信息,和Observer关心的数据分开。(简单的说就是,如果另外一种Observer之关心温度,也可是使用WeatherData,只是update方法不一样就可以了),但是也有不足指出,因为数据是由IObserver中的update方法决定的,这里感觉还可以改进。

需求3:Java中自带的观察者模式

Java 的 java.util包 中自带了 Observer接口和Observable类和我们前面的 Subject接口和Observer接口很像。

我们来看看使用Java的观察者模式,重写上面的例子:

WeatherData.java

public class WeatherData extends Observable {

    private double temperature;
    private double humidity;
    private double pressure;

    public WeatherData() {
    }

    public void measurementsChanged() {
        setChanged();
        notifyObservers();
    }

    public void setMeasurements(double temperature, double humidity,
            double pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;

        measurementsChanged();
    }

    public double getTemperature() {
        return temperature;
    }

    public double getHumidity() {
        return humidity;
    }

    public double getPressure() {
        return pressure;
    }
}

主要修改了几个地方:
1. 继承自Observable,等下讲完,我们来看看Observable里面写了什么。
2. setMeasurements()还是更新数据,然后调用notifyObservers()通知所有注册的Observer修改数据。所以我们应该关注 Observerable类中的两个方法:update()notifyObservers()

CurrentConditionDisplay.java

public class CurrentConditionDisplay implements Observer {

    private Observable observable;

    public CurrentConditionDisplay(Observable observable) {
        this.observable = observable;
        observable.addObserver(this);
    }

    public void update(Observable o, Object arg) {
        if (o instanceof WeatherData) {
            WeatherData wd = (WeatherData) o;
            display(wd.getTemperature(), wd.getHumidity(), wd.getPressure());
        }
    }

    public void display(double temp, double humidity, double presure) {
        System.out.println("I doing current work:" + temp + "," + humidity
                + "," + presure);
    }
}

修改的地方:
1. 与之前一样,首先还是在构造方法中添加 主题对象,并注册自己。
2. 然后看我们重写的方法 void update(Observable o, Object arg),先判断发送update消息的主题对象是不是WeatherData,然后从主题中获取并更新数据。

思考

至于调用和之前一样,看到这里有些童鞋可能看出一掉猫腻,我们在需求2中最后提到,我们关心的消息是由 IObserver中的update 方法中的具体参数决定的,不够完善。但是这里呢?完全跟参数无关,到底从 WeatherData 中获取什么数据,可以任意获取。

我们还可以做一定的解耦,就是说 WeatherData 这个主题现在提供3种数据,但是我们完全可以将3中数据分开为3个主题,然后所有的观察者都关心自己的数据,而不需要通过WeatherData 同时获取3中数据。至于 update 中我们就需要判断 到底是 TemperatureData, HumidityData,还是 PressureData,将关心的消息也解耦了,不知道你理解没有。

源码Observable , Observer 解析

public class Observable {
    private boolean changed = false;
    private Vector<Observer> obs;

    public Observable() {
        obs = new Vector<>();
    }

    public synchronized void addObserver(Observer o) {
        if (o == null)
            throw new NullPointerException();
        if (!obs.contains(o)) {
            obs.addElement(o);
        }
    }

    public synchronized void deleteObserver(Observer o) {
        obs.removeElement(o);
    }


    public void notifyObservers() {
        notifyObservers(null);
    }


    public void notifyObservers(Object arg) {

        Object[] arrLocal;

        synchronized (this) {
            if (!changed)
                return;
            arrLocal = obs.toArray();
            clearChanged();
        }

        for (int i = arrLocal.length-1; i>=0; i--)
            ((Observer)arrLocal[i]).update(this, arg);
    }


    public synchronized void deleteObservers() {
        obs.removeAllElements();
    }


    protected synchronized void setChanged() {
        changed = true;
    }


    protected synchronized void clearChanged() {
        changed = false;
    }


    public synchronized boolean hasChanged() {
        return changed;
    }


    public synchronized int countObservers() {
        return obs.size();
    }
}

为了节省篇幅,我狠心的把注释全删掉了,其实有很多注释的。

如果前面的内容都理解的童鞋,看这个代码也应该很好理解。套路一样,但是有一些点我们应该注意:

  1. addObserver(), deleteObserver()添加和删除观察者对象
  2. 代码中用一个 changed 关键字来表示是否有改变,在发送改变前我们需要先使用setChanged()将其设置为true。 在 notifyObservers()之后自动调用 clearChanged() 恢复 changed 为false。所以不需要我们自己来重置。
  3. 我们应该注意所有的方法都是 synchronized 的,而且存储观察者对象的集合类也是 Vector obs,所以整个 Observable 都是线程安全的。

还需要看一下 Observer 接口吗?

public interface Observer {
    void update(Observable o, Object arg);
}

确实精简,只有一个 update 方法。看看 Obsererable 对象中的 notifyObservers 方法 调用的 update:

 ((Observer)arrLocal[i]).update(this, arg);

对没错,就是这样调用的。

一些体会

感觉设计模式确实很有意思,但是上大学的时候,我们居然没有看这门课,我的天!!这个例子完整的结合了书本,自己的思考,以及Java的源码,感觉写下来我自己也受益匪浅。

作者:u013647382 发表于2016/10/20 10:30:10 原文链接
阅读:27 评论:0 查看评论

iOS 多线程(四)GCD

$
0
0

GCD:Grand Central Dispatch(GCD) 是异步执行任务的技术之一。一般将应用程序中记述的线程管理用的代码在系统级中实现。开发者只需要定义想执行的任务并追加到适当的Dispatch Queue中,GCD就能生成必要的线程并计划执行任务。由于线程管理是作为系统的一部分来实现的,因此可以统一管理,也可执行任务,这样就比以前的线程更有效率。

1 Dispatch Queue

Dispatch Queue”是什么,如其名称所示,是执行处理的等待队列。Dispatch Queue 按照追加的顺序执行处理。先进先出FIFO,first-in-first-out。

队列类型:

1 Serial Dispatch Queue    等待现在执行处理结束

2 Concurrent Dispatch Queue  不等待现在执行处理结束

比较两种Dispatch Queue。

dispatch_sync(queue,block1);
dispatch_sync(queue,block2);
dispatch_sync(queue,block3);
dispatch_sync(queue,block4);
dispatch_sync(queue,block5);
dispatch_sync(queue,block6);
当queue为serial Dispatch Queue,会先执行block1,当block1执行完毕后,接着执行block2,当block2执行完毕后,接着执行block3,如此重复。同时执行的处理数只能有一个。

block1 -> block2-> block3-> block4-> block5-> block6

当queue为Concurrent Dispatch Queue时,首先执行block1,不管block1是否结束,都开始执行后面的block2,不管block2是否结束,都开始执行block3,如此重复循环。(同时执行的处理数量取决于当前系统的状态。)

线程1 线程2 线程3
block1 block2 block3
block4 block6 block5

创建方式

第一种方式:dispatch_queue_create

// 创建串行队列    <span style="font-family: Arial, Helvetica, sans-serif;">DISPATCH_QUEUE_SERIAL = NULL</span>
    dispatch_queue_t queue = dispatch_queue_create("com.baidu.queue", DISPATCH_QUEUE_SERIAL);


// 1.创建一个并发队列
    dispatch_queue_t queue = dispatch_queue_create("com.baidu.queue", DISPATCH_QUEUE_CONCURRENT);

虽然Serial Dispatch Queue 和Current Dispatch Queue将受到限制,但是使用dispatch_queue_create函数可生成人意多个Dispatch Queue。

当生成多个Serial Dispatch Queue时,虽然在一个Serial Dispatch Queue中同时只能执行一个追加处理,但多个Serial Dispatch Queue之间可以并行执行。

通过dispatch_queue_create 函数生成的Dispatch Queue,在使用结束后需要释放。

//释放
dispatch_release(queue);

第二种方式:获取系统标准提供的Dispatch Queue。

Main Dispatch Queue,是在主线程中执行的Dispatch Queue,属于Serial Dispatch Queue。

// 1.获得主队列
dispatch_queue_t mainQueue = dispatch_get_main_queue();
追加到Main Dispatch Queue的处理在主线程的RunLoop中执行。因此要将用户界面的更新等一些必须在主线程中执行的处理追加到Main Dispatch Queue使用。与NSObject类的persormSelectorOnMainThread实例方法相同。

Global Dispatch Queue是ConCurrent Dispatch Queue。所有应用程序都可以使用,没有必要通过dispatch_queue_create函数逐个生成Concurrent Dispatch Queue。

// 1.获得全局的并发队列
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
第一个参数表示优先级。Global Dispatch Queue有4个执行优先级,分别是:

#define DISPATCH_QUEUE_PRIORITY_HIGH 2
#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0
#define DISPATCH_QUEUE_PRIORITY_LOW (-2)
#define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN

2 执行任务方式

GCD有两种方法执行任务。

1 同步的方式执行任务

dispatch_sync(dispatch_queue_t queue,dispatch_block_t block);

2 异步的方式执行任务

dispatch_async(dispatch_queue_t queue,dispatch_block_t block);

同步和异步的区别

同步:只能在当前线程中执行任务,不具备开启新线程的能力

异步:可以在新的线程中执行任务,具备开启新线程的能力


串行队列  同步函数

/**
 * 串行队列 + 同步函数:不会开启新的线程,在当前线程执行任务。任务是串行的,执行完一个任务,再执行下一个任务
 */
-(void)syncSerial{
    NSLog(@"syncSerial ----- begin");
    //创建串行队列 serial
    dispatch_queue_t serialQueue = dispatch_queue_create("com.vn.serial", NULL);
    dispatch_sync(serialQueue, ^{
        NSLog(@"1...sync....%@",[NSThread currentThread]);
    });
    dispatch_sync(serialQueue, ^{
        NSLog(@"2...sync....%@",[NSThread currentThread]);
    });
    dispatch_sync(serialQueue, ^{
        NSLog(@"3...sync....%@",[NSThread currentThread]);
    });
    NSLog(@"syncSerial ----- end");
}
打印:

2016-10-19 15:36:43.810 GCD基本使用[9712:1396171] syncSerial ----- begin
2016-10-19 15:36:43.810 GCD基本使用[9712:1396171] 1...sync....<NSThread: 0x60000006bd40>{number = 1, name = main}
2016-10-19 15:36:43.811 GCD基本使用[9712:1396171] 2...sync....<NSThread: 0x60000006bd40>{number = 1, name = main}
2016-10-19 15:36:43.811 GCD基本使用[9712:1396171] 3...sync....<NSThread: 0x60000006bd40>{number = 1, name = main}
2016-10-19 15:36:43.811 GCD基本使用[9712:1396171] syncSerial ----- end

串行队列 异步函数

/**
 * 串行队列 + 异步函数 :会开启新的线程,但是任务是串行的,执行完一个任务,再执行下一个任务
 */
-(void)asyncSerial{
    NSLog(@"asyncSerial ----- begin");
    //创建串行队列 serial
    dispatch_queue_t serialQueue = dispatch_queue_create("com.vn.serial", NULL);
    dispatch_async(serialQueue, ^{
        NSLog(@"1...async....%@",[NSThread currentThread]);
    });
    dispatch_async(serialQueue, ^{
        NSLog(@"2...async....%@",[NSThread currentThread]);
    });
    dispatch_async(serialQueue, ^{
        NSLog(@"3...async....%@",[NSThread currentThread]);
    });
    NSLog(@"asyncSerial ----- end");
}

打印:

2016-10-19 15:38:55.354 GCD基本使用[9730:1397289] asyncSerial ----- begin
2016-10-19 15:38:55.354 GCD基本使用[9730:1397289] asyncSerial ----- end
2016-10-19 15:38:55.354 GCD基本使用[9730:1397758] 1...async....<NSThread: 0x608000070d40>{number = 3, name = (null)}
2016-10-19 15:38:55.355 GCD基本使用[9730:1397758] 2...async....<NSThread: 0x608000070d40>{number = 3, name = (null)}
2016-10-19 15:38:55.355 GCD基本使用[9730:1397758] 3...async....<NSThread: 0x608000070d40>{number = 3, name = (null)}

主队列  同步函数

/**
 * 主队列 + 同步函数: 死锁,不应该这样使用
 */
- (void)syncMain{
    NSLog(@"syncMain ----- begin");
    // 1.获得主队列
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    dispatch_sync(mainQueue, ^{
        NSLog(@"1--syncMain---%@", [NSThread currentThread]);
    });
    NSLog(@"syncMain ----- end");
}
只输出下面数据后就会卡死。
syncMain ----- begin

主队列 异步函数

/**
 * 主队列 + 异步函数: 在主线程中执行任务,不会阻塞主线程。任务是串行的,执行完一个任务,再执行下一个任务
 */
- (void)asyncMain{
    NSLog(@"asyncMain ----- begin");
    // 1.获得主队列
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    dispatch_async(mainQueue, ^{
        NSLog(@"1--asyncMain---%@", [NSThread currentThread]);
    });
    dispatch_async(mainQueue, ^{
        NSLog(@"2--asyncMain---%@", [NSThread currentThread]);
    });
    dispatch_async(mainQueue, ^{
        NSLog(@"3--asyncMain---%@", [NSThread currentThread]);
    });
    NSLog(@"asyncMain ----- end");
}
打印:

2016-10-19 15:39:56.747 GCD基本使用[9750:1398368] asyncMain ----- begin
2016-10-19 15:39:56.748 GCD基本使用[9750:1398368] asyncMain ----- end
2016-10-19 15:39:56.748 GCD基本使用[9750:1398368] 1--asyncMain---<NSThread: 0x60000006ab80>{number = 1, name = main}
2016-10-19 15:39:56.753 GCD基本使用[9750:1398368] 2--asyncMain---<NSThread: 0x60000006ab80>{number = 1, name = main}
2016-10-19 15:39:56.753 GCD基本使用[9750:1398368] 3--asyncMain---<NSThread: 0x60000006ab80>{number = 1, name = main}

并行队列 同步函数

/**
 * 并发队列 + <span style="font-family: Arial, Helvetica, sans-serif;">同步函数</span><span style="font-family: Arial, Helvetica, sans-serif;">:不会开启新的线程,由于不开启新线程,需要等待执行完,所以任务执行完一个,再执行下一个。</span>
 */
- (void)syncConcurrent{
    // 1.获得全局的并发队列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    // 2.将任务加入队列
    dispatch_sync(queue, ^{
        NSLog(@"1--syncConcurrent---%@", [NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
        NSLog(@"2--syncConcurrent---%@", [NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
        NSLog(@"3--syncConcurrent---%@", [NSThread currentThread]);
    });
    NSLog(@"syncConcurrent--------end");
}
打印:

2016-10-19 15:52:30.536 GCD基本使用[9790:1404534] syncConcurrent--------begin
2016-10-19 15:52:30.536 GCD基本使用[9790:1404534] 1--syncConcurrent---<NSThread: 0x60000006b140>{number = 1, name = main}
2016-10-19 15:52:30.536 GCD基本使用[9790:1404534] 2--syncConcurrent---<NSThread: 0x60000006b140>{number = 1, name = main}
2016-10-19 15:52:30.537 GCD基本使用[9790:1404534] 3--syncConcurrent---<NSThread: 0x60000006b140>{number = 1, name = main}
2016-10-19 15:52:30.537 GCD基本使用[9790:1404534] syncConcurrent--------end

并行队列 异步函数

/**
 * 并发队列 + 异步函数:可以同时开启多条线程
 */
- (void)asyncConcurrent{
    NSLog(@"asyncConcurrent--------begin");
    // 1.获得全局的并发队列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    // 2.将任务加入队列
    dispatch_async(queue, ^{
        NSLog(@"1--asyncConcurrent---%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"2--asyncConcurrent---%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"3--asyncConcurrent---%@", [NSThread currentThread]);
    });
    NSLog(@"asyncConcurrent--------end");
}
打印:
2016-10-19 15:55:34.637 GCD基本使用[9820:1406397] asyncConcurrent--------begin
2016-10-19 15:55:34.637 GCD基本使用[9820:1406397] asyncConcurrent--------end
2016-10-19 15:55:34.637 GCD基本使用[9820:1406608] 1--asyncConcurrent---<NSThread: 0x600000069e00>{number = 3, name = (null)}
2016-10-19 15:55:34.637 GCD基本使用[9820:1406613] 2--asyncConcurrent---<NSThread: 0x608000077a00>{number = 4, name = (null)}
2016-10-19 15:55:34.637 GCD基本使用[9820:1406614] 3--asyncConcurrent---<NSThread: 0x600000072680>{number = 5, name = (null)}

3 线程间通信

dispatch_async(dispatch_get_main_queue(), ^{//});

- (void)threadCommunication{
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // 图片的网络路径
        NSURL *url = [NSURL URLWithString:@"http://img.pconline.com.cn/images/photoblog/9/9/8/1/9981681/200910/11/1255259355826.jpg"];
        // 加载图片
        NSData *data = [NSData dataWithContentsOfURL:url];
        // 生成图片
        UIImage *image = [UIImage imageWithData:data];
        // 回到主线程
        dispatch_async(dispatch_get_main_queue(), ^{
            self.imageView.image = image;
        });
    });
}

4 延时

// 延时追加
- (void)delay{
    NSLog(@"delay-----begin");
    //延时1s
    dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 1ull*NSEC_PER_SEC);
    dispatch_after(time, dispatch_get_main_queue(), ^{
        NSLog(@"after------%@",[NSThread currentThread]);
    });
    NSLog(@"delay-----end");
}

打印:
2016-10-19 16:35:45.680 GCD基本使用[9932:1427078] delay-----begin
2016-10-19 16:35:45.680 GCD基本使用[9932:1427078] delay-----end
2016-10-19 16:35:46.752 GCD基本使用[9932:1427078] after------<NSThread: 0x6000000635c0>{number = 1, name = main}
dispatch_after 并不是延时指定时间后执行处理,而是延时指定时间后追加任务到Dispatch Queue。

其中dispatch_time(DISPATCH_TIME_NOW,1ull*NSEC_PER_SEC);表示从现在开始1s后时间的值。

DISPATCH_TIME_NOW   表示当前的时间。
第二个参数单位为纳秒。

#define NSEC_PER_SEC 1000000000ull  //每秒有多少纳秒
#define NSEC_PER_MSEC 1000000ull    //每毫秒有多少纳秒
#define USEC_PER_SEC 1000000ull     //每秒有多少微秒
#define NSEC_PER_USEC 1000ull       //每微秒有多少纳秒
延时1s写法:
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 1ull*NSEC_PER_SEC);
dispatch_time_t time1 = dispatch_time(DISPATCH_TIME_NOW, 1000ull*NSEC_PER_MSEC);
dispatch_time_t time2 = dispatch_time(DISPATCH_TIME_NOW, 1000ull*USEC_PER_SEC);

Dispatch Group

开发中会有这种需求,执行多个任务,只有这几个任务都执行完成后才执行最终任务。这种情况可以使用Dispatch Queue。

- (void)group{
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    // 创建一个队列组
    dispatch_group_t group = dispatch_group_create();
    dispatch_group_async(group, queue, ^{
        for (int i=0; i<5; i++) {
            NSLog(@"任务1-------%d",i);
        }
    });
    dispatch_group_async(group, queue, ^{
        for (int i=0; i<5; i++) {
            NSLog(@"任务2-------%d",i);
        }
    });
    dispatch_group_notify(group, queue, ^{
        NSLog(@"end.....");
    });
}
打印:
2016-10-19 18:05:56.138 GCD基本使用[10052:1471672] 任务1-------0
2016-10-19 18:05:56.138 GCD基本使用[10052:1471674] 任务2-------0
2016-10-19 18:05:56.139 GCD基本使用[10052:1471674] 任务2-------1
2016-10-19 18:05:56.139 GCD基本使用[10052:1471672] 任务1-------1
2016-10-19 18:05:56.139 GCD基本使用[10052:1471674] 任务2-------2
2016-10-19 18:05:56.139 GCD基本使用[10052:1471672] 任务1-------2
2016-10-19 18:05:56.139 GCD基本使用[10052:1471674] 任务2-------3
2016-10-19 18:05:56.139 GCD基本使用[10052:1471672] 任务1-------3
2016-10-19 18:05:56.139 GCD基本使用[10052:1471674] 任务2-------4
2016-10-19 18:05:56.139 GCD基本使用[10052:1471672] 任务1-------4
2016-10-19 18:05:56.139 GCD基本使用[10052:1471672] end....

6 dispatch_barrier_async

在访问数据库或文件时,写入处理确实不可与其他的写入处理以及其他包含读写的处理并行执行。但是读取处理只是和读取处理并行执行就不会发生问题。

为了高效的进行访问,读取处理追加到Concurrent Dispatch Queue中,写入处理在一个读取都没有的情况下,追加到Serial Dispatch Queue中可。利用Dispatch Group和dispatch_set_target_queue也可以实现,但是会很复杂。

GCD提供了简便方法——dispatch_barrier_async函数。

- (void)barrier{
    dispatch_queue_t queue = dispatch_queue_create("abcd", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_async(queue, ^{
        NSLog(@"----1-----%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"----2-----%@", [NSThread currentThread]);
    });
    dispatch_barrier_async(queue, ^{
        NSLog(@"----barrier-----%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"----3-----%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"----4-----%@", [NSThread currentThread]);
    });
}
打印:
2016-10-19 19:18:30.739 GCD基本使用[10096:1490892] ----2-----<NSThread: 0x600000073d40>{number = 4, name = (null)}
2016-10-19 19:18:30.739 GCD基本使用[10096:1490890] ----1-----<NSThread: 0x608000075300>{number = 3, name = (null)}
2016-10-19 19:18:30.740 GCD基本使用[10096:1490890] ----barrier-----<NSThread: 0x608000075300>{number = 3, name = (null)}
2016-10-19 19:18:30.740 GCD基本使用[10096:1490890] ----3-----<NSThread: 0x608000075300>{number = 3, name = (null)}
2016-10-19 19:18:30.740 GCD基本使用[10096:1490892] ----4-----<NSThread: 0x600000073d40>{number = 4, name = (null)}
在前两个任务执行完后,才会追加处理到该queue中。然后当该处理执行完毕后,开始追加其他处理。

7 dispatch_apply

- (void)apply{
    NSLog(@"apply------begin");
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    //该函数按指定的次数 将block追加到Dispatch Queue中。并等待全部处理执行结束。
    dispatch_apply(10, queue, ^(size_t index) {
        NSLog(@"apply------%zu",index);
    });
    NSLog(@"apply------end");
}
打印:
2016-10-19 19:45:19.186 GCD基本使用[10114:1500525] apply------begin
2016-10-19 19:45:19.187 GCD基本使用[10114:1500525] apply------1
2016-10-19 19:45:19.187 GCD基本使用[10114:1500833] apply------0
2016-10-19 19:45:19.187 GCD基本使用[10114:1500837] apply------2
2016-10-19 19:45:19.187 GCD基本使用[10114:1500838] apply------3
2016-10-19 19:45:19.187 GCD基本使用[10114:1500525] apply------4
2016-10-19 19:45:19.187 GCD基本使用[10114:1500833] apply------5
2016-10-19 19:45:19.187 GCD基本使用[10114:1500837] apply------6
2016-10-19 19:45:19.187 GCD基本使用[10114:1500838] apply------7
2016-10-19 19:45:19.188 GCD基本使用[10114:1500525] apply------8
2016-10-19 19:45:19.188 GCD基本使用[10114:1500833] apply------9
2016-10-19 19:45:19.188 GCD基本使用[10114:1500525] apply------end
可以使用该函数遍历NSArray对象,

dispatch_apply([array count],queue,^(size_t index){});

由于dispatch_apply函数与dispatch_sync相同,会等待处理执行结束,因此推荐在dispatch_async函数中异步执行dispatch_apply函数。

- (void)apply1{
    NSLog(@"apply1------begin");
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(queue, ^{
        dispatch_apply(5, queue, ^(size_t index) {
            NSLog(@"apply------%zu",index);
        });
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"done");
        });
    });
    NSLog(@"apply1------end");
}
打印:
2016-10-19 20:26:16.245 GCD基本使用[10143:1518627] apply1------begin
2016-10-19 20:26:16.246 GCD基本使用[10143:1518627] apply1------end
2016-10-19 20:26:16.246 GCD基本使用[10143:1518924] apply------0
2016-10-19 20:26:16.246 GCD基本使用[10143:1518928] apply------1
2016-10-19 20:26:16.246 GCD基本使用[10143:1518929] apply------2
2016-10-19 20:26:16.246 GCD基本使用[10143:1518930] apply------3
2016-10-19 20:26:16.246 GCD基本使用[10143:1518924] apply------4
2016-10-19 20:26:16.251 GCD基本使用[10143:1518627] done

8 dispatch_once

dispatch_once 函数保证应用程序中只执行一次。

- (void)once{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSLog(@"------run");
    });
}

作者:VNanyesheshou 发表于2016/10/20 10:42:51 原文链接
阅读:58 评论:0 查看评论

Fragment

$
0
0

为了让界面可以在平板上更好地展示,Android3.0版本引入了Fragment(碎片)功能。

首先需要注意,Fragment是在3.0版本引入的,如果你使用的是3.0之前的系统,需要先导入android-support-v4的jar包才能使用Fragment功能。

①静态创建Fragment

这是使用Fragment最简单的一种方式,把Fragment当成普通的控件,直接写在Activity的布局文件中。步骤:

1、继承Fragment,重写onCreateView决定Fragemnt的布局

2、在Activity中声明此Fragment,就当和普通的View一样

就是把Fragment当成普通的View一样声明在Activity的布局文件中,然后所有控件的事件处理等代码都由各自的Fragment去处理,瞬间觉得Activity好干净有木有~~代码的可读性、复用性以及可维护性是不是瞬间提升了~~~

新建一个项目叫做Fragments,然后在layout文件夹下新建一个名为fragment1.xml的布局文件:

<span style="font-family:SimSun;font-size:12px;"><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
    android:layout_width="match_parent"  
    android:layout_height="match_parent"  
    android:background="#00ff00" >  
  
    <TextView  
        android:layout_width="wrap_content"  
        android:layout_height="wrap_content"  
        android:text="This is fragment 1"  
        android:textColor="#000000"  
        android:textSize="25sp" />  
  
</LinearLayout> </span>

可以看到,这个布局文件非常简单,只有一个LinearLayout,里面加入了一个TextView。我们如法炮制再新建一个fragment2.xml :

<span style="font-family:SimSun;font-size:12px;"><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
    android:layout_width="match_parent"  
    android:layout_height="match_parent"  
    android:background="#ffff00" >  
  
    <TextView  
        android:layout_width="wrap_content"  
        android:layout_height="wrap_content"  
        android:text="This is fragment 2"  
        android:textColor="#000000"  
        android:textSize="25sp" />  
  
</LinearLayout>  </span>

然后新建一个类Fragment1,这个类是继承自Fragment的:

<span style="font-family:SimSun;font-size:12px;">public class Fragment1 extends Fragment {  
  
    @Override  
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {  
        return inflater.inflate(R.layout.fragment1, container, false);  
    }  
  
}  </span>

我们可以看到,这个类也非常简单,主要就是加载了我们刚刚写好的fragment1.xml布局文件并返回。同样的方法,我们再写好Fragment2 :

<span style="font-family:SimSun;font-size:12px;">public class Fragment2 extends Fragment {  
  
    @Override  
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {  
        return inflater.inflate(R.layout.fragment2, container, false);  
    }  
 
} </span>

然后打开或新建activity_main.xml作为主Activity的布局文件,在里面加入两个Fragment的引用,使用android:name前缀来引用具体的Fragment:

<span style="font-size:12px;"><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
    android:layout_width="match_parent"  
    android:layout_height="match_parent"  
    android:baselineAligned="false" >  
  
    <fragment  
        android:id="@+id/fragment1"  
        android:name="com.example.fragmentdemo.Fragment1"  
        android:layout_width="0dip"  
        android:layout_height="match_parent"  
        android:layout_weight="1" />  
  
    <fragment  
        android:id="@+id/fragment2"  
        android:name="com.example.fragmentdemo.Fragment2"  
        android:layout_width="0dip"  
        android:layout_height="match_parent"  
        android:layout_weight="1" />  
  
</LinearLayout> </span>

最后打开或新建MainActivity作为程序的主Activity,里面的代码非常简单,都是自动生成的:

public class MainActivity extends Activity {  
  
    @Override  
    protected void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        setContentView(R.layout.activity_main);  
    }  
  
}  

②动态创建Fragment

动态添加Fragment主要分为4步:

1.获取到FragmentManager,在Activity中可以直接通过getFragmentManager得到。

2.开启一个事务,通过调用beginTransaction方法开启。

3.向容器内加入Fragment,一般使用replace方法实现,需要传入容器的id和Fragment的实例。

4.提交事务,调用commit方法提交

public class MainActivity extends Activity {  
  
    @Override  
    protected void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        setContentView(R.layout.activity_main);  
        Display display = getWindowManager().getDefaultDisplay();  
        if (display.getWidth() > display.getHeight()) {  
            Fragment1 fragment1 = new Fragment1();  
            getFragmentManager().beginTransaction().replace(R.id.main_layout, fragment1).commit();  
        } else {  
            Fragment2 fragment2 = new Fragment2();  
            getFragmentManager().beginTransaction().replace(R.id.main_layout, fragment2).commit();  
        }  
    }  
  
} 

Fragment家族常用的API

Fragment常用的三个类:

android.app.Fragment 主要用于定义Fragment

android.app.FragmentManager 主要用于在Activity中操作Fragment

android.app.FragmentTransaction 保证一些列Fragment操作的原子性,熟悉事务这个词,一定能明白~

a、获取FragmentManage的方式:

getFragmentManager() // v4中,getSupportFragmentManager

b、主要的操作都是FragmentTransaction的方法

FragmentTransaction transaction = fm.benginTransatcion();//开启一个事务

transaction.add() 

往Activity中添加一个Fragment

transaction.remove()

从Activity中移除一个Fragment,如果被移除的Fragment没有添加到回退栈(回退栈后面会详细说),这个Fragment实例将会被销毁。

transaction.replace()

使用另一个Fragment替换当前的,实际上就是remove()然后add()的合体~

transaction.hide()

隐藏当前的Fragment,仅仅是设为不可见,并不会销毁

transaction.show()

显示之前隐藏的Fragment

detach()

会将view从UI中移除,和remove()不同,此时fragment的状态依然由FragmentManager维护。

attach()

重建view视图,附加到UI上并显示。

transatcion.commit()//提交一个事务

注意:常用Fragment的哥们,可能会经常遇到这样Activity状态不一致:State loss这样的错误。主要是因为:commit方法一定要在Activity.onSaveInstance()之前调用。

上述,基本是操作Fragment的所有的方式了,在一个事务开启到提交可以进行多个的添加、移除、替换等操作。

值得注意的是:如果你喜欢使用Fragment,一定要清楚这些方法,哪个会销毁视图,哪个会销毁实例,哪个仅仅只是隐藏,这样才能更好的使用它们。

a、比如:我在FragmentA中的EditText填了一些数据,当切换到FragmentB时,如果希望回到A还能看到数据,则适合你的就是hide和show;也就是说,希望保留用户操作的面板,你可以使用hide和show,当然了不要使劲在那new实例,进行下非null判断。

b、再比如:我不希望保留用户操作,你可以使用remove(),然后add();或者使用replace()这个和remove,add是相同的效果。

c、remove和detach有一点细微的区别,在不考虑回退栈的情况下,remove会销毁整个Fragment实例,而detach则只是销毁其视图结构,实例并不会被销毁。那么二者怎么取舍使用呢?如果你的当前Activity一直存在,那么在不希望保留用户操作的时候,你可以优先使用detach。


生命周期



· onAttach方法:Fragment和Activity建立关联的时候调用。

· onCreateView方法:为Fragment加载布局时调用。

· onActivityCreated方法:当Activity中的onCreate方法执行完后调用。

· onDestroyView方法:Fragment中的布局被移除时调用。

· onDetach方法:Fragment和Activity解除关联的时候调用。

启动Activity


销毁Activity


可以看出针对Activity状态的改变Fragment状态的改变就如果入栈出栈的操作,Activity启动的时候相应的Fragment状态总是后执行,当我们要销毁Activity时,Fragment的状态总是优先销毁。就如同进栈的时候Activity先进入,出栈的时候Activity后出,先进后出,恰好符合栈的操作。

Activity 和 Fragment之间传值

可以使用bundle进行参数传递、这样在两个Fragment跳转的时候就可以带上参数了、同样也可以传递一个复杂的对象

ft.hide(getActivity().getSupportFragmentManager().findFragmentByTag(""));
	DemoFragment demoFragment = new DemoFragment();  
	Bundle bundle = new Bundle();  
	bundle.putString("key", "这是方法二");  
	demoFragment.setArguments(bundle);  
	ft.add(R.id.fragmentRoot, demoFragment, SEARCHPROJECT);  
	ft.commit(); 

在另外一个Fragment获取参数的方式只需要一个语句、key是自己定义的一个标识、参数的形式只要bundle能传递都可以实现

String string = getArguments().getString("key"); 

Activity主动传值到Fragment


Fragment主动传值到Activity

这种方式更简单了就是通过intent传值


Activity或Fragment获取值

例如获取Fragment中EditText中的值,或者Activity获取Fragment中EditText值:


在这里所讲的获取值指的是一个Fragment在某个Activity中的情况,也就是上面所讲的生命周期部分,Fragment的生命周期受Activity控制的情况,这也是最常见的传值方式。无论是Activity获取Fragment中的值还是Fragment获取Activity中的值,应该都不是太难,因为一个Fragment一定属于这个Activity了,所以在Fragment中可以通过getActivity()就获取到了Activity,再通过Activity中的UI控件或方法得到所要的值都是一件很简单的事。

同样的道理,Activity获取某一个Fragment中值也同上面说的一样,既然Activity已经有了这个Fragment的对象,想拿到你控件或方法值都轻而易举了。

回调函数传值

简单的举个例子,在Activity中获取Fragment中某个控件的值:


回调函数解释

回调函数透彻理解Java

Android学习笔记之java中的回调函数

程序员A写了一段程序(程序a),其中预留有回调函数接口,并封装好了该程序。程序员B要让a调用自己的程序b中的一个方法,于是,他通过a中的接口回调自己b中的方法。目的达到。在C/C++中,要用回调函数,被调函数需要告诉调用者自己的指针地址,但在JAVA中没有指针,怎么办?我们可以通过接口(interface)来实现定义回调函数。

管理Fragment回退栈

类似与Android系统为Activity维护一个任务栈,我们也可以通过Activity维护一个回退栈来保存每次Fragment事务发生的变化。如果你将Fragment任务添加到回退栈,当用户点击后退按钮时,将看到上一次的保存的Fragment。一旦Fragment完全从后退栈中弹出,用户再次点击后退键,则退出当前Activity。

如何添加一个Fragment事务到回退栈:

FragmentTransaction.addToBackStack(String)

如何处理运行时配置发生变化

当屏幕发生旋转,Activity发生重新启动,默认的Activity中的Fragment也会跟着Activity重新创建;这样造成当旋转的时候,本身存在的Fragment会重新启动,然后当执行Activity的onCreate时,又会再次实例化一个新的Fragment,这就是出现的原因。

那么如何解决呢:

其实通过检查onCreate的参数Bundle savedInstanceState就可以判断,当前是否发生Activity的重新创建:

默认的savedInstanceState会存储一些数据,包括Fragment的实例:通过打印可以看出:

1.07-20 08:23:12.952: E/FragmentOne(1782): Bundle[{android:fragments=android.app.FragmentManagerState@40d0b7b8, 
android:viewHierarchyState=Bundle[{android:focusedViewId=2131230721, android:views=android.util.SparseArray@40d0af68}]}]  

所以,我们简单改一下代码,只有在savedInstanceState==null时,才进行创建Fragment实例:

 public class MainActivity extends Activity  {  
     private static final String TAG = "FragmentOne";  
     private FragmentOne mFOne;  
   
     @Override  
     protected void onCreate(Bundle savedInstanceState) {  
         super.onCreate(savedInstanceState);  
         requestWindowFeature(Window.FEATURE_NO_TITLE);  
         setContentView(R.layout.activity_main);  
   
         Log.e(TAG, savedInstanceState+"");  
           
         if(savedInstanceState == null)   {  
             mFOne = new FragmentOne();  
             FragmentManager fm = getFragmentManager();  
             FragmentTransaction tx = fm.beginTransaction();  
             tx.add(R.id.id_content, mFOne, "ONE");  
             tx.commit();  
         }  
     }  
 } 

现在无论进行多次旋转都只会有一个Fragment实例在Activity中。

现在还存在一个问题,就是重新绘制时,Fragment发生重建,原本的数据如何保持?

其实和Activity类似,Fragment也有onSaveInstanceState的方法,在此方法中进行保存数据,然后在onCreate或者onCreateView或者onActivityCreated进行恢复都可以。

没有布局的Fragment的作用

没有布局文件Fragment实际上是为了保存,当Activity重启时,保存大量数据准备的

请参考博客:Android 屏幕旋转 处理 AsyncTask 和 ProgressDialog 的最佳方案

如果重新启动你的Activity需要恢复大量的数据,重新建立网络连接,或者执行其他的密集型操作,这样因为配置发生变化而完全重新启动可能会是一个慢的用户体验。

并且,使用系统提供的onSaveIntanceState()的回调中,使用Bundle来完全恢复你Activity的状态是可能是不现实的(Bundle不是设计用来携带大量数据的(例如bitmap),并且Bundle中的数据必须能够被序列化和反序列化),这样会消耗大量的内存和导致配置变化缓慢。

在这样的情况下,当你的Activity因为配置发生改变而重启,你可以通过保持一个Fragment来缓解重新启动带来的负担。这个Fragment可以包含你想要保持的有状态的对象的引用。

当Android系统因为配置变化关闭你的Activity的时候,你的Activity中被标识保持的fragments不会被销毁。你可以在你的Activity中添加这样的fragements来保存有状态的对象。

在运行时配置发生变化时,在Fragment中保存有状态的对象
a) 继承Fragment,声明引用指向你的有状态的对象
b) 当Fragment创建时调用setRetainInstance(boolean)
c) 把Fragment实例添加到Activity中
d) 当Activity重新启动后,使用FragmentManager对Fragment进行恢复

 

解释:一旦我们设置 setRetainInstance(true),意味着在 Activity 重绘时,我们的 Fragment 不会被重复绘制,也就是它会被“保留”。为了验证其作用,我们发现在设置为 true 状态时,旋转屏幕,Fragment 依然是之前的 Fragment。而如果将它设置为默认的 false,那么旋转屏幕时 Fragment 会被销毁,然后重新创建出另外一个 fragment 实例。

作者:Evan123mg 发表于2016/10/20 11:07:16 原文链接
阅读:62 评论:0 查看评论

Android6.0 图像合成过程详解(二) doComposition函数

$
0
0

上篇博客分析到setUpHWComposer函数,这里我们继续分析图像合成的过程从doComposition函数开始,以及在这过程中解答一些上篇博客提出的疑问。


一、doComposition合成图层

doComposition这个函数就是合成所有层的图像

void SurfaceFlinger::doComposition() {
    ATRACE_CALL();
    const bool repaintEverything = android_atomic_and(0, &mRepaintEverything);
    for (size_t dpy=0 ; dpy<mDisplays.size() ; dpy++) {
        const sp<DisplayDevice>& hw(mDisplays[dpy]);
        if (hw->isDisplayOn()) {
            // transform the dirty region into this screen's coordinate space
            const Region dirtyRegion(hw->getDirtyRegion(repaintEverything));

            // repaint the framebuffer (if needed)
            doDisplayComposition(hw, dirtyRegion);

            hw->dirtyRegion.clear();
            hw->flip(hw->swapRegion);
            hw->swapRegion.clear();
        }
        // inform the h/w that we're done compositing
        hw->compositionComplete();
    }
    postFramebuffer();
}

上面函数遍历所有的DisplayDevice然后调用doDisplayComposition函数。然后我们再看看doDisplayComposition函数

void SurfaceFlinger::doDisplayComposition(const sp<const DisplayDevice>& hw,
        const Region& inDirtyRegion)
{
    bool isHwcDisplay = hw->getHwcDisplayId() >= 0;
    if (!isHwcDisplay && inDirtyRegion.isEmpty()) {
        return;
    }

    Region dirtyRegion(inDirtyRegion);

    //swapRegion设置为需要更新的区域
    hw->swapRegion.orSelf(dirtyRegion);

    uint32_t flags = hw->getFlags();//获得显示设备支持的更新方式标志
    if (flags & DisplayDevice::SWAP_RECTANGLE) {//支持矩阵更新        
        dirtyRegion.set(hw->swapRegion.bounds());
    } else {
        if (flags & DisplayDevice::PARTIAL_UPDATES) {//支持部分更新
            dirtyRegion.set(hw->swapRegion.bounds());
        } else {
            //将更新区域调整为整个窗口大小
            dirtyRegion.set(hw->bounds());
            hw->swapRegion = dirtyRegion;
        }
    }

    if (CC_LIKELY(!mDaltonize && !mHasColorMatrix)) {
        if (!doComposeSurfaces(hw, dirtyRegion)) return;//合成
    } else {
        RenderEngine& engine(getRenderEngine());
        mat4 colorMatrix = mColorMatrix;
        if (mDaltonize) {
            colorMatrix = colorMatrix * mDaltonizer();
        }
        mat4 oldMatrix = engine.setupColorTransform(colorMatrix);
        doComposeSurfaces(hw, dirtyRegion);//合成
        engine.setupColorTransform(oldMatrix);
    }

    // update the swap region and clear the dirty region
    hw->swapRegion.orSelf(dirtyRegion);

    // swap buffers (presentation)
    hw->swapBuffers(getHwComposer());//使用egl将egl中的合成好的图像,输出到DisplayDevice的mSurface中
}

这个函数设置下需要更新的区域,后面调用doComposeSurfaces函数来合成图层,调用完doComposeSurfaces函数后,如果需要egl合成图像话,在这个函数中合成好。而最后调用swapBuffers只是将egl合成好的图像输出到DisplayDevice的mSurface中。

我们再来看看doComposeSurfaces函数,我们先来看一开始的代码,先判断是否有egl合成,然后再看是否有hwc合成(硬件合成)

bool SurfaceFlinger::doComposeSurfaces(const sp<const DisplayDevice>& hw, const Region& dirty)
{
    RenderEngine& engine(getRenderEngine());
    const int32_t id = hw->getHwcDisplayId();
    HWComposer& hwc(getHwComposer());
    HWComposer::LayerListIterator cur = hwc.begin(id);
    const HWComposer::LayerListIterator end = hwc.end(id);

    bool hasGlesComposition = hwc.hasGlesComposition(id);
    if (hasGlesComposition) {//是否有egl合成
        if (!hw->makeCurrent(mEGLDisplay, mEGLContext)) {
            ALOGW("DisplayDevice::makeCurrent failed. Aborting surface composition for display %s",
                  hw->getDisplayName().string());
            eglMakeCurrent(mEGLDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
            if(!getDefaultDisplayDevice()->makeCurrent(mEGLDisplay, mEGLContext)) {
              ALOGE("DisplayDevice::makeCurrent on default display failed. Aborting.");
            }
            return false;
        }

        // Never touch the framebuffer if we don't have any framebuffer layers
        const bool hasHwcComposition = hwc.hasHwcComposition(id);
        if (hasHwcComposition) {//是否有hwc合成
            // when using overlays, we assume a fully transparent framebuffer
            // NOTE: we could reduce how much we need to clear, for instance
            // remove where there are opaque FB layers. however, on some
            // GPUs doing a "clean slate" clear might be more efficient.
            // We'll revisit later if needed.
            engine.clearWithColor(0, 0, 0, 0);
        } else {
            // we start with the whole screen area
            const Region bounds(hw->getBounds());

            // we remove the scissor part
            // we're left with the letterbox region
            // (common case is that letterbox ends-up being empty)
            const Region letterbox(bounds.subtract(hw->getScissor()));

            // compute the area to clear
            Region region(hw->undefinedRegion.merge(letterbox));

            // but limit it to the dirty region
            region.andSelf(dirty);

            // screen is already cleared here
            if (!region.isEmpty()) {
                // can happen with SurfaceView
                drawWormhole(hw, region);
            }
        }

        if (hw->getDisplayType() != DisplayDevice::DISPLAY_PRIMARY) {
            // just to be on the safe side, we don't set the
            // scissor on the main display. It should never be needed
            // anyways (though in theory it could since the API allows it).
            const Rect& bounds(hw->getBounds());
            const Rect& scissor(hw->getScissor());
            if (scissor != bounds) {
                // scissor doesn't match the screen's dimensions, so we
                // need to clear everything outside of it and enable
                // the GL scissor so we don't draw anything where we shouldn't

                // enable scissor for this frame
                const uint32_t height = hw->getHeight();
                engine.setScissor(scissor.left, height - scissor.bottom,
                        scissor.getWidth(), scissor.getHeight());
            }
        }
    }
......

我们来看hasGlesComposition函数和hasHwcComposition函数,就是看其对应的DisplayData中是否有hasFbComp和hasOvComp。

bool HWComposer::hasGlesComposition(int32_t id) const {
    if (!mHwc || uint32_t(id)>31 || !mAllocatedDisplayIDs.hasBit(id))
        return true;
    return mDisplayData[id].hasFbComp;
}
bool HWComposer::hasHwcComposition(int32_t id) const {
    if (!mHwc || uint32_t(id)>31 || !mAllocatedDisplayIDs.hasBit(id))
        return false;
    return mDisplayData[id].hasOvComp;
}
而这两个值是在prepare中调用Hwc的prepare函数之后赋值的
status_t HWComposer::prepare() {
   ......
   int err = mHwc->prepare(mHwc, mNumDisplays, mLists);
    ALOGE_IF(err, "HWComposer: prepare failed (%s)", strerror(-err));

    if (err == NO_ERROR) {
        // here we're just making sure that "skip" layers are set
        // to HWC_FRAMEBUFFER and we're also counting how many layers
        // we have of each type.
        //
        // If there are no window layers, we treat the display has having FB
        // composition, because SurfaceFlinger will use GLES to draw the
        // wormhole region.
        for (size_t i=0 ; i<mNumDisplays ; i++) {
            DisplayData& disp(mDisplayData[i]);
            disp.hasFbComp = false;
            disp.hasOvComp = false;
            if (disp.list) {
                for (size_t i=0 ; i<disp.list->numHwLayers ; i++) {
                    hwc_layer_1_t& l = disp.list->hwLayers[i];

                    //ALOGD("prepare: %d, type=%d, handle=%p",
                    //        i, l.compositionType, l.handle);

                    if (l.flags & HWC_SKIP_LAYER) {
                        l.compositionType = HWC_FRAMEBUFFER;
                    }
                    if (l.compositionType == HWC_FRAMEBUFFER) {
                        disp.hasFbComp = true;//只要有一个layer是HWC_FRAMEBUFFER
                    }
                    if (l.compositionType == HWC_OVERLAY) {
                        disp.hasOvComp = true;//有一个layer是HWC_OVERLAY
                    }
                    if (l.compositionType == HWC_CURSOR_OVERLAY) {
                        disp.hasOvComp = true;//有一个layer是HWC_CURSOR_OVERLAY
                    }
                }
                if (disp.list->numHwLayers == (disp.framebufferTarget ? 1 : 0)) {//layer的数量 有framebufferTarget为1 没有为0
                    disp.hasFbComp = true;
                }
            } else {
                disp.hasFbComp = true;//没有list
            }
        }
    }
    return (status_t)err;
}

我们继续看doComposeSurfaces函数,下面这个函数当cur!=end代表起码有两个以上图层,然后遍历图层,当layer是HWC_FRAMEBUFFER代表是需要egl合成的,而HWC_FRAMEBUFFER_TARGET是egl合成后使用的直接就跳了,HWC_CURSOR_OVERLAY和HWC_OVERLAY是用HWC模块(硬件合成)的,也就不用调用Layer的draw方法。而如果图层只要1个或者没有,那么直接使用egl合成。

    HWComposer::LayerListIterator cur = hwc.begin(id);
    const HWComposer::LayerListIterator end = hwc.end(id);
    ......

    const Vector< sp<Layer> >& layers(hw->getVisibleLayersSortedByZ());
    const size_t count = layers.size();
    const Transform& tr = hw->getTransform();
    if (cur != end) { //代表起码有两个以上图层
        // we're using h/w composer
        for (size_t i=0 ; i<count && cur!=end ; ++i, ++cur) {//遍历图层
            const sp<Layer>& layer(layers[i]);
            const Region clip(dirty.intersect(tr.transform(layer->visibleRegion)));
            if (!clip.isEmpty()) {
                switch (cur->getCompositionType()) {
                    case HWC_CURSOR_OVERLAY:
                    case HWC_OVERLAY: {
                        const Layer::State& state(layer->getDrawingState());
                        if ((cur->getHints() & HWC_HINT_CLEAR_FB)
                                && i
                                && layer->isOpaque(state) && (state.alpha == 0xFF)
                                && hasGlesComposition) {
                            // never clear the very first layer since we're
                            // guaranteed the FB is already cleared
                            layer->clearWithOpenGL(hw, clip);
                        }
                        break;
                    }
                    case HWC_FRAMEBUFFER: {
                        layer->draw(hw, clip);//只有是HWC_FRAMEBUFFER才会调用Layer的draw合成
                        break;
                    }
                    case HWC_FRAMEBUFFER_TARGET: {
                        // this should not happen as the iterator shouldn't
                        // let us get there.
                        ALOGW("HWC_FRAMEBUFFER_TARGET found in hwc list (index=%zu)", i);
                        break;
                    }
                }
            }
            layer->setAcquireFence(hw, *cur);
        }
    } else {
        // we're not using h/w composer
        for (size_t i=0 ; i<count ; ++i) {//只有一个或者没有图层  就直接使用Layer的draw合成
            const sp<Layer>& layer(layers[i]);
            const Region clip(dirty.intersect(
                    tr.transform(layer->visibleRegion)));
            if (!clip.isEmpty()) {
                layer->draw(hw, clip);
            }
        }
    }

    // disable scissor at the end of the frame
    engine.disableScissor();
    return true;
}

Layer的draw我们就不看了主要是使用egl合成纹理,但是有一点疑问,我们从来没有把layer中的mActiveBuffer放到egl中去,那么egl又是怎么合成各个layer的呢,我想肯定客户进程在绘制各个layer的时候,也是用egl绘制的,所有后面合成的时候egl有各个layer的buffer。


后面我们再来看下DisplayDevice::swapBuffers函数,是使用eglSwapBuffers来把egl合成的数据放到mSurface中去。

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);
    }
}


二、FramebufferSurface收到egl合成数据

之前分析DisplayDevice时候,还分析了FramebufferSurface,我们这里再来看下。

在SurfaceFlinger.cpp中的init函数,在创建DisplayDevice之前,我们先调用createBufferQueue来创建了一个buffer的生产者和消费者,然后把消费者放入了FramebufferSurface,生产者放入了DisplayDevice中。

          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());

我们先来看生产者,下面是DisplayDevice的构造函数,生产者作为参数直接新建了一个Surface,然后把这个Surface作为参数调用eglCreateWindowSurface返回的就是mSurface,之前我们分析最后egl合成的数据时调用eglSwapBuffers并且把数据放到mSurface,这样最后肯定就到消费者(FramebufferSurface)去了。

    mNativeWindow = new Surface(producer, false);
    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);

最后到消费者那端的onFrameAvailable,也就是FramebufferSurface的onFrameAvailable中,我们现在来分析下这个过程,也就解答了一个onFrameAvailable的疑惑。


FramebufferSurface的父类是ConsumerBase类,我们来看其构造函数。先是构造了mConsumer,这里其实就是BufferQueueConsumer类,后面调用了其consumerConnect方法。

ConsumerBase::ConsumerBase(const sp<IGraphicBufferConsumer>& bufferQueue, bool controlledByApp) :
        mAbandoned(false),
        mConsumer(bufferQueue) {
    mName = String8::format("unnamed-%d-%d", getpid(), createProcessUniqueId());

    wp<ConsumerListener> listener = static_cast<ConsumerListener*>(this);
    sp<IConsumerListener> proxy = new BufferQueue::ProxyConsumerListener(listener);

    status_t err = mConsumer->consumerConnect(proxy, controlledByApp);
    if (err != NO_ERROR) {
        CB_LOGE("ConsumerBase: error connecting to BufferQueue: %s (%d)",
                strerror(-err), err);
    } else {
        mConsumer->setConsumerName(mName);
    }
}

我们来看下BufferQueueConsumer类的consumerConnect方法,就是调用了connect方法。

    virtual status_t consumerConnect(const sp<IConsumerListener>& consumer,
            bool controlledByApp) {
        return connect(consumer, controlledByApp);
    }

这个方法中将mCore->mConsumerListener = consumerListener,这个mCore就是BufferQueueCore类。我们再从ConsumerBase的构造函数看这个consumerListener参数其实就是FrameBufferSurface对象本身。

status_t BufferQueueConsumer::connect(
        const sp<IConsumerListener>& consumerListener, bool controlledByApp) {
    ATRACE_CALL();

    if (consumerListener == NULL) {
        BQ_LOGE("connect(C): consumerListener may not be NULL");
        return BAD_VALUE;
    }

    BQ_LOGV("connect(C): controlledByApp=%s",
            controlledByApp ? "true" : "false");

    Mutex::Autolock lock(mCore->mMutex);

    if (mCore->mIsAbandoned) {
        BQ_LOGE("connect(C): BufferQueue has been abandoned");
        return NO_INIT;
    }

    mCore->mConsumerListener = consumerListener;//设置回调
    mCore->mConsumerControlledByApp = controlledByApp;

    return NO_ERROR;
}


我们再看BufferQueueProducer::queueBuffer函数,这个函数应该是生产者已经使用好buffer了,这个使用会调用如下代码这个listener就是BufferQueueCore的mConsumerListener,传输的数据时BufferItem。再传之前把BufferItem的mGraphicBuffer清了,因为消费者可以自己获取buffer,不用通过BufferItem传。

    item.mGraphicBuffer.clear();
    item.mSlot = BufferItem::INVALID_BUFFER_SLOT;

    // Call back without the main BufferQueue lock held, but with the callback
    // lock held so we can ensure that callbacks occur in order
    {
        Mutex::Autolock lock(mCallbackMutex);
        while (callbackTicket != mCurrentCallbackTicket) {
            mCallbackCondition.wait(mCallbackMutex);
        }

        if (frameAvailableListener != NULL) {
            frameAvailableListener->onFrameAvailable(item);
        } else if (frameReplacedListener != NULL) {
            frameReplacedListener->onFrameReplaced(item);
        }

        ++mCurrentCallbackTicket;
        mCallbackCondition.broadcast();
    }

这样就要FramebufferSurface的onFrameAvailable函数中去了,我们来看下这个函数。

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);
    }
}
这个函数先用nextBuffer获取数据,然后调用了HWComposer的fbPost函数。我们先来看下nextBuffer函数,这个函数主要通过acquireBufferLocked获取BufferItem,其中的mBuf就是buffer了。
status_t FramebufferSurface::nextBuffer(sp<GraphicBuffer>& outBuffer, sp<Fence>& outFence) {
    Mutex::Autolock lock(mMutex);

    BufferItem item;
    status_t err = acquireBufferLocked(&item, 0);
    if (err == BufferQueue::NO_BUFFER_AVAILABLE) {
        outBuffer = mCurrentBuffer;
        return NO_ERROR;
    } else if (err != NO_ERROR) {
        ALOGE("error acquiring buffer: %s (%d)", strerror(-err), err);
        return err;
    }

    if (mCurrentBufferSlot != BufferQueue::INVALID_BUFFER_SLOT &&
        item.mBuf != mCurrentBufferSlot) {
        // Release the previous buffer.
        err = releaseBufferLocked(mCurrentBufferSlot, mCurrentBuffer,
                EGL_NO_DISPLAY, EGL_NO_SYNC_KHR);
        if (err < NO_ERROR) {
            ALOGE("error releasing buffer: %s (%d)", strerror(-err), err);
            return err;
        }
    }
    mCurrentBufferSlot = item.mBuf;
    mCurrentBuffer = mSlots[mCurrentBufferSlot].mGraphicBuffer;
    outFence = item.mFence;
    outBuffer = mCurrentBuffer;
    return NO_ERROR;
}
而这个acquireBufferLocked还是用mConsumer的acquireBuffer来获取BufferItem。mConsumer就是BufferQueueConsumer类。
status_t ConsumerBase::acquireBufferLocked(BufferItem *item,
        nsecs_t presentWhen, uint64_t maxFrameNumber) {
    status_t err = mConsumer->acquireBuffer(item, presentWhen, maxFrameNumber);
    if (err != NO_ERROR) {
        return err;
    }

    if (item->mGraphicBuffer != NULL) {
        mSlots[item->mBuf].mGraphicBuffer = item->mGraphicBuffer;
    }

    mSlots[item->mBuf].mFrameNumber = item->mFrameNumber;
    mSlots[item->mBuf].mFence = item->mFence;

    CB_LOGV("acquireBufferLocked: -> slot=%d/%" PRIu64,
            item->mBuf, item->mFrameNumber);

    return OK;
}

回到FramebufferSurface的onFrameAvailable中这样获取了buffer之后,调用了HWComposer的fbPost方法。


三、egl合成数据在HWComposer的处理

继上面调用fbPost方法,我们来看下,这里是调用了setFramebufferTarget方法。

int HWComposer::fbPost(int32_t id,
        const sp<Fence>& acquireFence, const sp<GraphicBuffer>& buffer) {
    if (mHwc && hwcHasApiVersion(mHwc, HWC_DEVICE_API_VERSION_1_1)) {
        return setFramebufferTarget(id, acquireFence, buffer);
    } else {
        acquireFence->waitForever("HWComposer::fbPost");
        return mFbDev->post(mFbDev, buffer->handle);
    }
}

我们来看下setFramebufferTarget方法,这里就是把该设备的DisplayData数据中的framebufferTarget填充,主要是其handle数据,这里就是egl合成好的数据buffer。

也就是最终egl合成好的数据放在DisplayData的framebufferTarget变量的handle中。

status_t HWComposer::setFramebufferTarget(int32_t id,
        const sp<Fence>& acquireFence, const sp<GraphicBuffer>& buf) {
    if (uint32_t(id)>31 || !mAllocatedDisplayIDs.hasBit(id)) {
        return BAD_INDEX;
    }
    DisplayData& disp(mDisplayData[id]);
    if (!disp.framebufferTarget) {
        // this should never happen, but apparently eglCreateWindowSurface()
        // triggers a Surface::queueBuffer()  on some
        // devices (!?) -- log and ignore.
        ALOGE("HWComposer: framebufferTarget is null");
        return NO_ERROR;
    }

    int acquireFenceFd = -1;
    if (acquireFence->isValid()) {
        acquireFenceFd = acquireFence->dup();
    }

    // ALOGD("fbPost: handle=%p, fence=%d", buf->handle, acquireFenceFd);
    disp.fbTargetHandle = buf->handle;//egl合成好的数据
    disp.framebufferTarget->handle = disp.fbTargetHandle;//egl合成好的数据,最终是放在这里
    disp.framebufferTarget->acquireFenceFd = acquireFenceFd;
    return NO_ERROR;
}



四、硬件模块合成

这样就剩最后一步了,把不管是普通layer的数据,还是egl合成好的数据发送到硬件模块合成了,最后就到显示设备了。

继第一节分析的doComposition函数最后会调用postFramebuffer函数,我们再来分析下这个函数,这个函数主要是调用了HWComposer的commit函数。

void SurfaceFlinger::postFramebuffer()
{
    ATRACE_CALL();

    const nsecs_t now = systemTime();
    mDebugInSwapBuffers = now;

    HWComposer& hwc(getHwComposer());
    if (hwc.initCheck() == NO_ERROR) {
        if (!hwc.supportsFramebufferTarget()) {
            // EGL spec says:
            //   "surface must be bound to the calling thread's current context,
            //    for the current rendering API."
            getDefaultDisplayDevice()->makeCurrent(mEGLDisplay, mEGLContext);
        }
        hwc.commit();
    }
......
我们来看下HWComposer的commit函数,这个函数就是先设置了egl的那个设备的surface和display,然后处理虚拟设备的outbuf等,最后调用了硬件模块合成到显示设备上。
status_t HWComposer::commit() {
    int err = NO_ERROR;
    if (mHwc) {
        if (!hwcHasApiVersion(mHwc, HWC_DEVICE_API_VERSION_1_1)) {
            // On version 1.0, the OpenGL ES target surface is communicated
            // by the (dpy, sur) fields and we are guaranteed to have only
            // a single display.
            mLists[0]->dpy = eglGetCurrentDisplay();//设置下egl相关变量
            mLists[0]->sur = eglGetCurrentSurface(EGL_DRAW);
        }

        for (size_t i=VIRTUAL_DISPLAY_ID_BASE; i<mNumDisplays; i++) {
            DisplayData& disp(mDisplayData[i]);
            if (disp.outbufHandle) {//只有虚拟设备需要设置outbuf
                mLists[i]->outbuf = disp.outbufHandle;
                mLists[i]->outbufAcquireFenceFd =
                        disp.outbufAcquireFence->dup();
            }
        }

        err = mHwc->set(mHwc, mNumDisplays, mLists);//调用硬件模块合成
......



作者:kc58236582 发表于2016/10/20 11:17:07 原文链接
阅读:69 评论:0 查看评论

2016 View 使用TextPaint来绘制文字

$
0
0

TextPaint是paint的子类,用它可以很方便的进行文字的绘制,一般情况下遇到绘制文字的需求时,我们一般用TextPaint所提供的方法。开始学习如何绘制文字之前,我们必须要先了解下android中文字是怎么绘制到屏幕上的,文字的格式又是怎么样的。

一、FontMetrics(字体度量)
1.1理论知识
它是一个Paint的内部类,作用是“字体测量”。它里面呢就定义了top,ascent,descent,bottom,leading五个成员变量其他什么也没有,和rect很相似。如果你不信,我们可以去看看源码:

 /**
     * Class that describes the various metrics for a font at a given text size.
     * Remember, Y values increase going down, so those values will be positive,
     * and values that measure distances going up will be negative. This class
     * is returned by getFontMetrics().
     */
    public static class FontMetrics {
        /**
         * The maximum distance above the baseline for the tallest glyph in
         * the font at a given text size.
         */
        public float   top;
        /**
         * The recommended distance above the baseline for singled spaced text.
         */
        public float   ascent;
        /**
         * The recommended distance below the baseline for singled spaced text.
         */
        public float   descent;
        /**
         * The maximum distance below the baseline for the lowest glyph in
         * the font at a given text size.
         */
        public float   bottom;
        /**
         * The recommended additional space to add between lines of text.
         */
        public float   leading;
    }

为了很好的理解这5个变量的意义,我们用下面的图示来进行说明。
这里写图片描述

Baseline是基线
在Android中,文字的绘制都是从Baseline处开始的Baseline往上至字符“最高处”的距离我们称之为ascent(上坡度),Baseline往下至字符“最低处”的距离我们称之为descent(下坡度)

leading(行间距)则表示上一行字符的descent到该行字符的ascent之间的距离

  top和bottom文档描述地很模糊,其实这里我们可以借鉴一下TextView对文本的绘制,TextView在绘制文本的时候总会在文本的最外层留出一些内边距,为什么要这样做?因为TextView在绘制文本的时候考虑到了类似读音符号,下图中的A上面的符号就是一个拉丁文的类似读音符号的东西
  这里写图片描述
  top的意思其实就是除了Baseline到字符顶端的距离外还应该包含这些符号的高度,bottom的意思也是一样。一般情况下我们极少使用到类似的符号所以往往会忽略掉这些符号的存在,但是Android依然会在绘制文本的时候在文本外层留出一定的边距,这就是为什么top和bottom总会比ascent和descent大一点的原因。而在TextView中我们可以通过xml设置其属性android:includeFontPadding=”false”去掉一定的边距值但是不能完全去掉。

1.2 代码验证

private static final String TEXT = "ap卡了ξτβбпшㄎㄊěǔぬも┰┠№@↓"; 
 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        mTextPaint.setTextSize(50);  
        mTextPaint.setColor(Color.BLACK);  

        FontMetrics fontMetrics = mTextPaint.getFontMetrics();  
        Log.d("Aige", "ascent:" + fontMetrics.ascent);  
        Log.d("Aige", "top:" + fontMetrics.top);  
        Log.d("Aige", "leading:" + fontMetrics.leading);  
        Log.d("Aige", "descent:" + fontMetrics.descent);  
        Log.d("Aige", "bottom:" + fontMetrics.bottom);  

        mTextPaint.clearShadowLayer();
        canvas.drawText(TEXT, 0, Math.abs(fontMetrics.top), mTextPaint);
    }

结果:
这里写图片描述

打印的Log:

ascent:-46.38672
top:-52.807617
leading:0.0
descent:12.207031
bottom:13.549805

注:Baseline上方的值为负,下方的值为正
我们来分析一下这个结果:

因为基线上方为负,所以ascent和top的值都是负数,而且top要大于ascent,原因是要为符号留出位置。
因为只有一行文本所以leading恒为0。
基线下方为正,所以descent和bottom都是正的,bottom要略大于descent
在得到的结果中,我们发现文字是紧紧贴着屏幕顶端的,再看下我们的程序代码:
canvas.drawText(TEXT, 0, Math.abs(fontMetrics.top), mTextPaint);

x坐标是0,y坐标是Math.abs(fontMetrics.top),因为android是从基线开始绘制的,所以我们为了让字体顶端紧贴屏幕就必须让它移下来一点,移动的距离是top的距离,也就是基线到文字对顶部的距离。有人可能会问,如果不设置呢?x,y坐标都是0,是什么效果呢?因为android会从基线开始绘制,所以如果不做处理,基线就是屏幕的顶部,因此会出现如下的效果:
这里写图片描述

1.3 fontMetrics中的变量和文字的size、typeface有关
从代码中我们可以看到一个很特别的现象,在我们绘制文本之前我们便可以获取文本的FontMetrics属性值,也就是说我们FontMetrics的这些值跟我们要绘制什么文本是无关的,而仅与绘制文本Paint的size和typeface有关。当你改变了paint绘制文字的size或typeface时,FontMetrics中的top、bottom等值就会发生改变。如果我们仅仅更改了文字,这些值是不会发生任何改变的。

1.4 绘制居中屏幕的文字
我们知道了这些理论知识,也知道android是怎么绘制文字的,一会我们要做一个实际的例子来巩固巩固。首先,我们要先来扩展认识两个方法:

float android.graphics.Paint.descent()
解释:the distance below (positive) the baseline (descent) based on the current typeface and text size. 
一句话解释:得到下坡度的值

float android.graphics.Paint.ascent()
解释:the distance above (negative) the baseline (ascent) based on the current typeface and text size. 
一句话解释:就是得到上坡度的值

实际代码:

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        mTextPaint.setTextSize(50);
        mTextPaint.setColor(Color.BLACK);

        // 计算Baseline绘制的起点X轴坐标 ,计算方式:画布宽度的一半 - 文字宽度的一半
        int baseX = (int) (canvas.getWidth() / 2 - mTextPaint.measureText(TEXT) / 2);

        // 计算Baseline绘制的Y坐标 ,计算方式:画布高度的一半 - 文字总高度的一半
        int baseY = (int) ((canvas.getHeight() / 2) - ((mTextPaint.descent() + mTextPaint.ascent()) / 2));

        // 居中画一个文字
        canvas.drawText(TEXT, baseX, baseY, mTextPaint);

        mPaint.setColor(Color.RED);
        mPaint.setStrokeWidth(2);
        // 为了便于理解我们在画布中心处绘制一条中线
        canvas.drawLine(0, canvas.getHeight() / 2, canvas.getWidth(), canvas.getHeight() / 2, mPaint);
    }

x坐标的计算方法是(屏幕宽度-文字宽度)/2,如果文字宽度比屏幕宽度长得到的就是负数,如果文字宽度比屏幕宽度短,得到的就是正数,这个很容易理解;

y坐标的的计算方式是(屏幕高度-文字高度)/2,这里的文字高度用的是:descent+ascent(忽略了音标)。

结果:
这里写图片描述

这里的TextPaint.FontMetrics(); 就是帮助我们得到我们字体的各种距离属性的,然后方便我们对文字进行绘制。
这里我们需要牢记的就是TOP ASCENT DESCENT BOTTOM LEADING等等属性值。这些都可以让我们在自定义View的时候画Text更得心应手,更精确。

二、TextPaint中的各种方法

float ascent():顾名思义就是返回上坡度的值
float descent():得到下坡度的值

public int breakText (String text, boolean measureForwards, float maxWidth, float[] measuredWidth)
public int breakText (char[] text, int index, int count, float maxWidth, float[] measuredWidth)
public int breakText (CharSequence text, int start, int end, boolean measureForwards, float maxWidth, float[] measuredWidth)

这个方法让我们设置一个最大宽度,在不超过这个宽度的范围内返回实际测量值否则停止测量。
**text表示我们的字符串;
start表示从第几个字符串开始测量;
end表示从测量到第几个字符串为止;
measureForwards表示向前还是向后测量;
maxWidth表示一个给定的最大宽度在这个宽度内能测量出几个字符;
measuredWidth为一个可选项,可以为空,不为空时返回真实的测量值**

这些方法在一些结合文本处理的应用里比较常用,比如文本阅读器的翻页效果,我们需要在翻页的时候动态折断或生成一行字符串,这就派上用场了~

getFontMetrics();//得到一个FontMetrics对象。 然后可以得到这个对象下面对于文字测量所需要的一些数值。

**getFontMetrics (Paint.FontMetrics metrics)
这个和我们之前用到的getFontMetrics()相比多了个参数,getFontMetrics()返回的是FontMetrics对象,而getFontMetrics(Paint.FontMetrics metrics)返回的是文本的行间距,如果metrics的值不为空则返回FontMetrics对象的值。**

**getFontMetricsInt()
该方法返回了一个FontMetricsInt对象,FontMetricsInt和FontMetrics是一样的,只不过getFontMetricsInt()得到的对象中的参数都是int类型,而getFontMetrics()返回对象中的参数都是float。**

**getFontMetricsInt(Paint.FontMetricsInt fmi)
得到文字的间距,距离是int类型**

getFontSpacing();//返回字符行间距

setUnderlineText(boolean underlineText);//设置文字的下划线

setTypeface(Typeface typeface);//设置字体类型,上面我们也使用过。
Android中字体有四种样式:BOLD(加粗),BOLD_ITALIC(加粗并倾斜),ITALIC(倾斜),NORMAL(正常); 这四个是静态常量,直接传入即可 和WORD是很像的。

  **Paint p = new Paint();  
     String familyName = "宋体";  
     **Typeface** font = Typeface.create(familyName, Typeface.BOLD);  
     p.setColor(Color.RED);  
     p.setTypeface(font);** 

setTextSkewX(float skewX)

设置文本在水平方向上的倾斜。这个倾斜值没有具体的范围,但是官方推崇的值为-0.25可以得到比较好的倾斜文本效果,值为负右倾值为正左倾,默认值为0。

setTextSize (float textSize)
设置文字的大小,但是要注意该值必需大于零。

setTextScaleX (float scaleX)
将文本沿X轴水平缩放,默认值为1,当值大于1会沿X轴水平放大文本,当值小于1会沿X轴水平缩放文本

setTextLocale (Locale locale)
设置地理位置,这里如果你要使用,直接传入Locale.getDefault()即可。

setTextAlign (Paint.Align align)
设置文本的对齐方式,可供选的方式有三种:CENTER,LEFT和RIGHT。

我们的文本大小是通过size和typeface确定的(其实还有其他的因素但这里影响不大忽略),一旦baseline确定,对不对齐好像不相干吧。但是,你要知道一点,文本的绘制是从baseline开始没错,但是是从哪边开始绘制的呢?左端还是右端呢?而这个Align就是为我们定义在baseline绘制文本究竟该从何处开始,上面我们在进行对文本的水平居中时是用Canvas宽度的一半减去文本宽度的一半:

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        mTextPaint.setTextSize(50);
        mTextPaint.setColor(Color.BLACK);
        // 计算Baseline绘制的起点X轴坐标 ,计算方式:画布宽度的一半 - 文字宽度的一半
        int baseX = (int) (canvas.getWidth() / 2 - mTextPaint.measureText(TEXT) / 2);

        // 计算Baseline绘制的Y坐标 ,计算方式:画布高度的一半 - 文字总高度的一半
        int baseY = (int) ((canvas.getHeight() / 2) - ((mTextPaint.descent() + mTextPaint.ascent()) / 2));

        // 居中画一个文字
        canvas.drawText(TEXT, baseX, baseY, mTextPaint);

        mPaint.setColor(Color.RED);
        mPaint.setStrokeWidth(2);
        // 为了便于理解我们在画布中心处绘制一条中线
        canvas.drawLine(0, canvas.getHeight() / 2, canvas.getWidth(), canvas.getHeight() / 2, mPaint);
    }

实际上我们大可不必这样计算,我们只需设置Paint的文本对齐方式为CENTER,drawText的时候起点x = canvas.getWidth() / 2即可。产生的效果是,文字先算好一个基准线,从这个基准线的中点开始向左右开始绘制文字,最终自然就变成了居中显示了。如果你设定了RIGHT,那么从baseline的右边的顶点开始,文字开始慢慢绘制。

**textPaint.setTextAlign(Align.CENTER);
canvas.drawText(TEXT, canvas.getWidth() / 2, baseY, textPaint);**
注:这里的剧中还是 左右是根据你传入的点进行计算的,即传入点把这个点看成中心点然后画出文字。

当我们将文本对齐方式设置为CENTER后就相当于告诉Android我们这个文本绘制的时候从文本的中点开始向两端绘制;如果设置为LEFT则从文本的左端开始往右绘制;如果为RIGHT则从文本的右端开始往左绘制:
这里写图片描述

setSubpixelText (boolean subpixelText)
设置是否打开文本的亚像素显示,什么叫亚像素显示呢?你可以理解为对文本显示的一种优化技术,如果大家用的是Win7+系统可以在控制面板中找到一个叫ClearType的设置,该设置可以让你的文本更好地显示在屏幕上就是基于亚像素显示技术。

setStrikeThruText (boolean strikeThruText);//文本删除线

**setLinearText (boolean linearText)
设置是否打开线性文本标识,这玩意对大多数人来说都很奇怪不知道这玩意什么意思。想要明白这东西你要先知道文本在Android中是如何进行存储和计算的。在Android中文本的绘制需要使用一个bitmap作为单个字符的缓存,既然是缓存必定要使用一定的空间,我们可以通过setLinearText (true)告诉Android我们不需要这样的文本缓存。**

setFakeBoldText (boolean fakeBoldText);//设置文本仿粗体

measureText (String text)
measureText (CharSequence text, int start, int end)
measureText (String text, int start, int end)
measureText (char[] text, int index, int count)
测量文本宽度,上面我们已经使用过了,这四个方法都是一样的只是参数稍有不同罢了。

三、Typeface中的方法

defaultFromStyle(int style)
最简单的,简而言之就是把上面所说的四种Style封装成Typeface。传入的参数是:BOLD(加粗),BOLD_ITALIC(加粗并倾斜),ITALIC(倾斜),NORMAL(正常)

mTextPaint.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD));

create(String familyName, int style)
create(Typeface family, int style)
**textPaint.setTypeface(Typeface.create(“SERIF”, Typeface.NORMAL));
textPaint.setTypeface(Typeface.create(Typeface.SERIF, Typeface.NORMAL));**
这两个方法执行的效果完全一样。

**createFromAsset(AssetManager mgr, String path)
createFromFile(String path)
createFromFile(File path)
这三者也是一样的,它们都允许我们使用自己的字体比如我们从asset目录读取一个字体文件。下面是一个简单的例子:**

// 获取字体并设置画笔字体
Typeface typeface = Typeface.createFromAsset(context.getAssets(), “kt.ttf”);
textPaint.setTypeface(typeface);

3.2 扩展到TextView

说到文本大家第一时间想到的应该是TextView,其实在TextView里我们依然可以找到上面很多方法的影子,比如我们可以从TextView中获取到TextPaint:

TextPaint paint = mTextView.getPaint();
当然也可以设置TextView的字体等等:

Typeface typeface = Typeface.createFromAsset(getAssets(), “kt.ttf”);
mTextView.setTypeface(typeface);

其实写到这里,我希望大家能清楚这样一个概念,在Android的View上进行绘制文字,和我们在Word上绘制是一样的,我们有的时候需要对文字的各种属性进行设置,那么我们的TextPaint同样提供这些方法, 这要和WORD相对应就可以了,不要把这个类想的很复杂,他就是一个代码化的WORD他甚至比WORD更精确灵活,让你随心所欲的对文字进行处理。

那么常用的就是:我们选择我们喜欢的字体 这里的格式是 ttf把文字放到asset当中就可以了
我们设置文字的位置,那么这个属性就是Align
我们设置文字的大小,TextSize
我们设置文字的颜色,TextColor
等等只要你需要的这个类都会提供相应的方法给你!

作者:u010451990 发表于2016/10/20 11:24:56 原文链接
阅读:23 评论:0 查看评论

Android OkHttp(二)实战

$
0
0

     Android OkHttp(一)初识,这篇文章最后提供了一个封装Okhttp请求的类,今天就来看看在项目中具体的使用情况。

一、简单接口请求。

     接口请求,需要有一个服务端,这里就使用之前用SpringMVC做的一个接口服务,接口有关的详细开发步骤,请参考这篇文章,SpringMVC 开发接口

1.启动接口服务后,运行后的效果截图如下,


可以看到 ,接口返回的是json格式的数据,json数据定义了返回状态、返回码以及返回数据等,有关接口定义,可以看这篇文章,java web开发(二) 接口开发

2.客户端程序开发。

    客户端使用Android小程序去调用接口。这个更加详细的描述,请参考, java web开发(三) 接口使用。记得加入网络访问请求权限

<uses-permission android:name="android.permission.INTERNET"/>

下面主要展示具体的调用,

public class MainActivity extends AppCompatActivity {

    private Button btn;
    private TextView tv;
    private ImageView iv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        btn = (Button) findViewById(R.id.btn);
        tv = (TextView) findViewById(R.id.tv);
        iv = (ImageView) findViewById(R.id.iv);
        Log.e("Thread.currentThread()--->", Thread.currentThread().getId() + "");//打印当前线程的id
        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
            getAllStudent();
            }
        });
    }
}

MainActivity界面有Button和TextView,点击Button去调用接口,TextView则用来显示接口数据。下面是具体调用方法,

  /**
     * 获取接口数据`
     */
    private void getAllStudent() {
        //接口返回的json
        TypeToken<ListResponse<Students>> typeToken = new TypeToken<ListResponse<Students>>() {
        };
        OkHttpUtils.getInstance().getAsyn(Constant.GET_ALL_STUDENT, null, typeToken, new BaseResponseCallback<ListResponse<Students>>() {
            @Override
            public void onCompleted(final Throwable e, ListResponse<Students> result) {
                if (e != null) {
                    tv.post(new Runnable() {
                        @Override
                        public void run() {
                            Toast.makeText(MainActivity.this, e.getMessage(), Toast.LENGTH_LONG).show();
                        }
                    });
                } else {
                    Log.e("Thread.currentThread()--->", Thread.currentThread().getId() + "");//打印当前线程的id
                    //获取接口返回的列表数据
                    List<Students> list = result.getItems();
                    final StringBuffer sb = new StringBuffer();
                    for (Students students : list) {
                        sb.append("姓名:" + students.getName() + ", 年龄" + students.getAge() + ", 电话" + students.getMobile()).append("\n");
                    }
                    //更新UI,在子线程中,不能直接更新UI
                    tv.post(new Runnable() {
                        @Override
                        public void run() {
                            tv.setText(sb.toString());
                        }
                    });
                }
            }
        });
    }
在回调方法中要加入判断请求是否成功,最后记得显示接口数据时,不能直接在回调方法中更新UI,否则会报异常,如下图所示,


再看下面这种截图,


可以看到打印了两个线程的id是不一样的,一个是主线程,一个是子线程。程序运行后的效果截图,


  PS:  我们还可以在点击Button时,显示一个进度条(模态对话框),当请求结束后,关闭该进度条。这样在请求时,有一个比较好的用户体验,明确告诉用户,程序现在在做什么!微笑

      更新UI,有多种方式,还可以使用handler发消息到主线程中,让主线程更新UI!

二、文件下载。

   1.服务端。

下载一张图片。该图片位于Tomact服务器路径底下。


启动在Tomact服务器,然后在浏览器中输入图片地址,截图如下所示,


2.客户端程序开发。

MainActivity界面有Button和ImageView,点击Button去调用接口,ImageView则用来显示下载的图片。

首先记得加入读写sd卡权限,

 <!-- 在SD卡中创建与删除文件权限 -->
    <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
    <!-- 向SD卡写入数据权限 -->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.WRITE_SETTINGS"/>
下面看具体调用,

  /**
     * 下载图片
     */
    private void downLoadPic() {
        OkHttpUtils.getInstance().downloadFileAsyn(Constant.PIC_URL, null, FileUtils.createFile(MainActivity.this, "pic", "1.png"), new BaseResponseCallback() {
            @Override
            public void onCompleted(final Throwable e, final Object result) {
                if (e != null) {
                    tv.post(new Runnable() {
                        @Override
                        public void run() {
                            Toast.makeText(MainActivity.this, e.getMessage(), Toast.LENGTH_LONG).show();
                        }
                    });
                } else {
                    if (result != null) {
                        iv.post(new Runnable() {
                            @Override
                            public void run() {
                                String path = (String) result;
                                Log.e("path--->", path);
                                Bitmap bitmap = BitmapFactory.decodeFile(path);
                                iv.setImageBitmap(bitmap);
                            }
                        });
                    }
                }
            }
        });
    }

FileUtils.createFile(MainActivity.this,"pic","1.png") //是创建一个文件

点击Button去调用下载图片,下载完成后在ImageView中加载图片,点击运行后的效果如下,

打印出下载文件的路径,我们在手机上去该目录底下看看文件是否存在,


在目录下,已经存在下载的文件了,下面显示手机运行后的效果截图,


三、文件上传。

      在调用接口上传文件前,先做了一个JSP网页上传一个文件。

1. 使用form 上传一个文件。

(1). 首先需要创建一个上传的JSP网页,具体代码

<%@ page language="java" contentType="text/html; charset=utf-8"
    pageEncoding="utf-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Insert title here</title>
</head>
<body>
<form action="./upLoadFile.dbo" method="post" enctype="multipart/form-data"> 
   选择文件:<input type="file" name="file"/> 
   <input type="submit" value="提交"/> 
 </form> 

</body>
</html>
该JSP网页功能很简单,选择文件,然后点击提交。
(2).创建两个JSP网页,一个是成功,另外一个是失败,用于上传后跳转,内容自己可以定制,我这边只显示文字。具体代码就不列举了!具体代码详解Demo工程。

(3).创建一个上传接口,具体代码如下,

public interface UpLoadFileService {
	 public boolean uploadFile(String destinationDir, MultipartFile file);
}
该接口只有一个上传方法,下面在看该接口的实现类,

public class UploadFileServiceImpl implements UpLoadFileService {

	public boolean upload(String destinationDir, MultipartFile file) {
		return uploadFile(destinationDir, file);
	}

	/**
	 * 保存文件
	 * 
	 * @param stream
	 * @param path
	 * @param filename
	 * @throws IOException
	 */
	private void SaveFileFromInputStream(InputStream stream, String path, String filename) throws IOException {
		FileOutputStream outputStream = new FileOutputStream(path + "/" + filename);
		int byteCount = 0;
		byte[] bytes = new byte[1024];
		while ((byteCount = stream.read(bytes)) != -1) {
			outputStream.write(bytes, 0, byteCount);
		}
		outputStream.close();
		stream.close();
	}

	@Override
	public boolean uploadFile(String destinationDir, MultipartFile file) {
		StringBuffer sb = new StringBuffer();
		sb.append("文件长度: " + file.getSize());
		sb.append("文件类型: " + file.getContentType());
		sb.append("文件名称: " + file.getName());
		sb.append("文件原名: " + file.getOriginalFilename());
		System.out.println(sb.toString());
		try {
			SaveFileFromInputStream(file.getInputStream(), destinationDir, file.getOriginalFilename());
		} catch (IOException e) {
			e.printStackTrace();
			return false;
		}
		return true;
	}

}
该实现类,将文件流写入到具体目录中,和我们第二部的下载文件很类似。
(4).接着定义个Action,提供外部调用接口,

@Controller
public class UpLoadFileServlet {

	@RequestMapping(value = "/upLoadFile.dbo", method = RequestMethod.POST)
	public ModelAndView getAllStudent(HttpServletRequest request, HttpServletResponse response,
			 ModelMap modelMap) {
		 // 转型为MultipartHttpRequest:     
        MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;     
        // 获得文件:     
        MultipartFile file = multipartRequest.getFile("file"); 
		String realPath = "D:/java_web/upload";//文件存放路径,需要首先创建
		boolean result = false;
		result = UploadFileBusiness.upload(realPath, file);
		String resultJsp = "";
		SingleObject singleObject = new SingleObject();
		if (result) {
			resultJsp = "uploadFileSuccess";
			singleObject.setCode(StatusCode.CODE_SUCCESS);
			singleObject.setMsg("上传成功");
		} else {
			resultJsp = "uploadFileFail";
			singleObject.setCode(StatusCode.CODE_ERROR);
			singleObject.setMsg("上传失败");
		}
		singleObject.setObject("");
		modelMap.addAttribute("result", singleObject);
		return new ModelAndView(resultJsp, modelMap);
	}
}

上传文件调用了UploadFileBusiness.upload()方法,

public class UploadFileBusiness {
	
	/**
	 * 上传文件
	 * @param destinationDir
	 * @param file
	 * @return
	 */
	public static boolean upload(String destinationDir, MultipartFile file) {
		return new UploadFileServiceImpl().upload(destinationDir, file);
	}

}

(5).最后记得要在springmvc.xml中配置过滤器,

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
	xmlns:mvc="http://www.springframework.org/schema/mvc"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
		http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd">

	<!--配置自动解析的包 -->
	<context:component-scan base-package="cn.springmvc"></context:component-scan>
	<!--配置视图解析器:如何把handler 方法返回值解析为实际的物理视图  -->
	<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
	<property name="prefix" value="/WEB-INF/views/"></property>
	<property name="suffix" value=".jsp"></property>
	</bean>
	
	<!-- SpringMVC上传文件时,需要配置MultipartResolver处理器 -->
	<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
		<property name="defaultEncoding" value="UTF-8"/>
		<!-- 指定所上传文件的总大小不能超过200KB。注意maxUploadSize属性的限制不是针对单个文件,而是所有文件的容量之和 -->
		<property name="maxUploadSize" value="200000"/>
	</bean>
	...
	<!-- SpringMVC在超出上传文件限制时,会抛出org.springframework.web.multipart.MaxUploadSizeExceededException -->
	<!-- 该异常是SpringMVC在检查上传的文件信息时抛出来的,而且此时还没有进入到Controller方法中 -->
	<bean id="exceptionResolver" class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
		<property name="exceptionMappings">
			<props>
				<!-- 遇到MaxUploadSizeExceededException异常时,自动跳转到/WEB-INF/jsp/error_fileupload.jsp页面 -->
				<prop key="org.springframework.web.multipart.MaxUploadSizeExceededException">error_fileupload</prop>
			</props>
		</property>
	</bean>
	...
</beans>

因为需要上传文件,所以还要导入几个jar包才可以,commons-fileupload-*.jar和commons-io-*.jar等包。

此致,上传功能已经完成了!下面展示一张,项目截图,


最后运行,效果截图如下,选择文件,点击提交,


点击提交后,如果上传成功,会跳转至上传成功(uploadFileSuccess.jsp),并且在管理端打印了该文件的信息,

我们去上传文件的保存目录(D:/java_web/upload)看看是否有上传文件,


点击提交后,如果上传失败,会跳转至上传失败(uploadFileFail.jsp),在控制台会输出上传失败原因,便于大家分析出错原因。

2.客户端调用接口上传文件。

  在Android设备上,选择一张图片,上传至后台。

(1). 选择图片,

  /**
     * 选择图片
     */
    private void selectPic() {
        Intent intent = new Intent();
        if (Build.VERSION.SDK_INT < 19) {//因为Android SDK在4.4版本后图片action变化了 所以在这里先判断一下
            intent.setAction(Intent.ACTION_GET_CONTENT);
        } else {
            intent.setAction(Intent.ACTION_OPEN_DOCUMENT);
        }
        intent.setType("image/*");
        intent.addCategory(Intent.CATEGORY_OPENABLE);
        startActivityForResult(intent, PICTURE);
    }
回调处理,

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (data == null) {
            return;
        }
        Uri uri = data.getData();
        switch (requestCode) {
            case PICTURE:
                String imagePath = FileUtils.getUriPath(this, uri); //(因为4.4以后图片uri发生了变化)通过文件工具类 对uri进行解析得到图片路径
                if (!TextUtils.isEmpty(imagePath)) {
                    uploadFile(imagePath);
                }
                break;
            default:
                break;
        }
    }
上传文件,

  /**
     * 文件上传
     *
     * @param filePath
     */
    private void uploadFile(String filePath) {
        File file = new File(filePath);
        TypeToken<EntityResponse<Image>> typeToken = new TypeToken<EntityResponse<Image>>() {
        };
        OkHttpUtils.getInstance().postAsyn(Constant.UPLOAD_PIC_URL, null, file,"file",typeToken, new BaseResponseCallback<EntityResponse<Image>>() {

            @Override
            public void onCompleted(final Throwable e, EntityResponse<Image> result) {
                if (e != null) {
                    tv.post(new Runnable() {
                        @Override
                        public void run() {
                            Toast.makeText(MainActivity.this, e.getMessage(), Toast.LENGTH_LONG).show();
                        }
                    });
                } else {
                    //获取接口返回的列表数据
                   Image image = result.getObject();
                    final StringBuffer sb = new StringBuffer();
                    sb.append("文件名称:" + image.getName() + ", 下载地址:" + image.getUrl() );
                    //更新UI,在子线程中,不能直接更新UI
                    tv.post(new Runnable() {
                        @Override
                        public void run() {
                            tv.setText(sb.toString());
                        }
                    });
                }
            }
        });
    }
客户端运行截图,首先选择图片,


选择完成后,上传成功返回json数据截图,

上传成功截图,


再看看服务端截图,控制台输入日志截图,


然后打开上传文件目录,


上传文件也OK了!奋斗

四、总结

     本文主要是基于封装Okhttp请求的类调用后台服务接口,实现不同的功能!经测试,该类基本满足功能!本文对上篇文章封装的Okhttp请求类有所优化,更加详细的代码,请看本篇的例子工程源代码!本文需要有点Okhttp和SpringMVC方面的知识!微笑

PS: Demo下载链接(包含服务端和客户端程序)


作者:zxw136511485 发表于2016/10/20 11:34:56 原文链接
阅读:48 评论:0 查看评论

android java设计模式之单例模式

$
0
0
一、java设计模式的分类


总体来说设计模式分为三大类:


创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。


结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。


行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。


其实还有两类:并发型模式和线程池模式。




二、设计模式的六大原则


1、开闭原则(Open Close Principle)


开闭原则就是说对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。所以一句话概括就是:为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类,后面的具体设计中我们会提到这点。


2、里氏代换原则(Liskov Substitution Principle)


里氏代换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。 LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。—— From Baidu 百科


3、依赖倒转原则(Dependence Inversion Principle)


这个是开闭原则的基础,具体内容:真对接口编程,依赖于抽象而不依赖于具体。


4、接口隔离原则(Interface Segregation Principle)


这个原则的意思是:使用多个隔离的接口,比使用单个接口要好。还是一个降低类之间的耦合度的意思,从这儿我们看出,其实设计模式就是一个软件的设计思想,从大型软件架构出发,为了升级和维护方便。所以上文中多次出现:降低依赖,降低耦合。


5、迪米特法则(最少知道原则)(Demeter Principle)


为什么叫最少知道原则,就是说:一个实体应当尽量少的与其他实体之间发生相互作用,使得系统功能模块相对独立。


6、合成复用原则(Composite Reuse Principle)


原则是尽量使用合成/聚合的方式,而不是使用继承。






-------------------单例模式 


概念:
  Java中单例模式是一种常见的设计模式,单例模式的写法有好几种,这里主要介绍三种:懒汉式单例、饿汉式单例、登记式单例。
  单例模式有以下特点:
  1、单例类只能有一个实例。
  2、单例类必须自己创建自己的唯一实例。
  3、单例类必须给所有其他对象提供这一实例。
  单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态,避免政出多头。




一、懒汉式单例


[java] view plain copy print?在CODE上查看代码片派生到我的代码片
//懒汉式单例类.在第一次调用的时候实例化自己   
public class Singleton {  
    private Singleton() {}  
    private static Singleton single=null;  
    //静态工厂方法   
    public static Singleton getInstance() {  
         if (single == null) {    
             single = new Singleton();  
         }    
        return single;  
    }  
}  


Singleton通过将构造方法限定为private避免了类在外部被实例化,在同一个虚拟机范围内,Singleton的唯一实例只能通过getInstance()方法访问。
(事实上,通过Java反射机制是能够实例化构造方法为private的类的,那基本上会使所有的Java单例实现失效。此问题在此处不做讨论,姑且掩耳盗铃地认为反射机制不存在。)


但是以上懒汉式单例的实现没有考虑线程安全问题,它是线程不安全的,并发环境下很可能出现多个Singleton实例,要实现线程安全,有以下三种方式,都是对getInstance这个方法改造,保证了懒汉式单例的线程安全,如果你第一次接触单例模式,对线程安全不是很了解,可以先跳过下面这三小条,去看饿汉式单例,等看完后面再回头考虑线程安全的问题:






1、在getInstance方法上加同步




[java] view plain copy print?在CODE上查看代码片派生到我的代码片
public static synchronized Singleton getInstance() {  
         if (single == null) {    
             single = new Singleton();  
         }    
        return single;  
}  


2、双重检查锁定




[java] view plain copy print?在CODE上查看代码片派生到我的代码片
public static Singleton getInstance() {  
        if (singleton == null) {    
            synchronized (Singleton.class) {    
               if (singleton == null) {    
                  singleton = new Singleton();   
               }    
            }    
        }    
        return singleton;   
    }  
3、静态内部类


[java] view plain copy print?在CODE上查看代码片派生到我的代码片
public class Singleton {    
    private static class LazyHolder {    
       private static final Singleton INSTANCE = new Singleton();    
    }    
    private Singleton (){}    
    public static final Singleton getInstance() {    
       return LazyHolder.INSTANCE;    
    }    
}    
这种比上面1、2都好一些,既实现了线程安全,又避免了同步带来的性能影响。






二、饿汉式单例


[java] view plain copy print?在CODE上查看代码片派生到我的代码片
//饿汉式单例类.在类初始化时,已经自行实例化   
public class Singleton1 {  
    private Singleton1() {}  
    private static final Singleton1 single = new Singleton1();  
    //静态工厂方法   
    public static Singleton1 getInstance() {  
        return single;  
    }  
}  
饿汉式在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以天生是线程安全的。




三、登记式单例(可忽略)


[java] view plain copy print?在CODE上查看代码片派生到我的代码片
//类似Spring里面的方法,将类名注册,下次从里面直接获取。  
public class Singleton3 {  
    private static Map<String,Singleton3> map = new HashMap<String,Singleton3>();  
    static{  
        Singleton3 single = new Singleton3();  
        map.put(single.getClass().getName(), single);  
    }  
    //保护的默认构造子  
    protected Singleton3(){}  
    //静态工厂方法,返还此类惟一的实例  
    public static Singleton3 getInstance(String name) {  
        if(name == null) {  
            name = Singleton3.class.getName();  
            System.out.println("name == null"+"--->name="+name);  
        }  
        if(map.get(name) == null) {  
            try {  
                map.put(name, (Singleton3) Class.forName(name).newInstance());  
            } catch (InstantiationException e) {  
                e.printStackTrace();  
            } catch (IllegalAccessException e) {  
                e.printStackTrace();  
            } catch (ClassNotFoundException e) {  
                e.printStackTrace();  
            }  
        }  
        return map.get(name);  
    }  
    //一个示意性的商业方法  
    public String about() {      
        return "Hello, I am RegSingleton.";      
    }      
    public static void main(String[] args) {  
        Singleton3 single3 = Singleton3.getInstance(null);  
        System.out.println(single3.about());  
    }  
}  
 登记式单例实际上维护了一组单例类的实例,将这些实例存放在一个Map(登记薄)中,对于已经登记过的实例,则从Map直接返回,对于没有登记的,则先登记,然后返回。 


这里我对登记式单例标记了可忽略,我的理解来说,首先它用的比较少,另外其实内部实现还是用的饿汉式单例,因为其中的static方法块,它的单例在类被装载的时候就被实例化了。






饿汉式和懒汉式区别


从名字上来说,饿汉和懒汉,


饿汉就是类一旦加载,就把单例初始化完成,保证getInstance的时候,单例是已经存在的了,


而懒汉比较懒,只有当调用getInstance的时候,才回去初始化这个单例。


另外从以下两点再区分以下这两种方式:






1、线程安全:


饿汉式天生就是线程安全的,可以直接用于多线程而不会出现问题,


懒汉式本身是非线程安全的,为了实现线程安全有几种写法,分别是上面的1、2、3,这三种实现在资源加载和性能方面有些区别。








2、资源加载和性能:


饿汉式在类创建的同时就实例化一个静态对象出来,不管之后会不会使用这个单例,都会占据一定的内存,但是相应的,在第一次调用时速度也会更快,因为其资源已经初始化完成,


而懒汉式顾名思义,会延迟加载,在第一次使用该单例的时候才会实例化对象出来,第一次调用时要做初始化,如果要做的工作比较多,性能上会有些延迟,之后就和饿汉式一样了。


至于1、2、3这三种实现又有些区别,


第1种,在方法调用上加了同步,虽然线程安全了,但是每次都要同步,会影响性能,毕竟99%的情况下是不需要同步的,


第2种,在getInstance中做了两次null检查,确保了只有第一次调用单例的时候才会做同步,这样也是线程安全的,同时避免了每次都同步的性能损耗


第3种,利用了classloader的机制来保证初始化instance时只有一个线程,所以也是线程安全的,同时没有性能损耗,所以一般我倾向于使用这一种。






什么是线程安全?


如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。


或者说:一个类或者程序所提供的接口对于线程来说是原子操作,或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题,那就是线程安全的。






应用


以下是一个单例类使用的例子,以懒汉式为例,这里为了保证线程安全,使用了双重检查锁定的方式:


[java] view plain copy print?在CODE上查看代码片派生到我的代码片
public class TestSingleton {  
    String name = null;  
  
        private TestSingleton() {  
    }  
  
    private static volatile TestSingleton instance = null;  
  
    public static TestSingleton getInstance() {  
           if (instance == null) {    
             synchronized (TestSingleton.class) {    
                if (instance == null) {    
                   instance = new TestSingleton();   
                }    
             }    
           }   
           return instance;  
    }  
  
    public String getName() {  
        return name;  
    }  
  
    public void setName(String name) {  
        this.name = name;  
    }  
  
    public void printInfo() {  
        System.out.println("the name is " + name);  
    }  
  
}  
可以看到里面加了volatile关键字来声明单例对象,既然synchronized已经起到了多线程下原子性、有序性、可见性的作用,为什么还要加volatile呢,原因已经在下面评论中提到,


还有疑问可参考http://www.iteye.com/topic/652440
和http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html




[java] view plain copy print?在CODE上查看代码片派生到我的代码片
public class TMain {  
    public static void main(String[] args){  
        TestStream ts1 = TestSingleton.getInstance();  
        ts1.setName("jason");  
        TestStream ts2 = TestSingleton.getInstance();  
        ts2.setName("0539");  
          
        ts1.printInfo();  
        ts2.printInfo();  
          
        if(ts1 == ts2){  
            System.out.println("创建的是同一个实例");  
        }else{  
            System.out.println("创建的不是同一个实例");  
        }  
    }  
}  
 运行结果:






结论:由结果可以得知单例模式为一个面向对象的应用程序提供了对象惟一的访问点,不管它实现何种功能,整个应用程序都会同享一个实例对象。


对于单例模式的几种实现方式,知道饿汉式和懒汉式的区别,线程安全,资源加载的时机,还有懒汉式为了实现线程安全的3种方式的细微差别。

作者:sinat_29060967 发表于2016/10/20 11:36:07 原文链接
阅读:23 评论:0 查看评论

自定义View——模拟水银柱

$
0
0

由于项目需要,所以用SurfaceView写了一个自定义View,根据晓风飞雨的温度计源码做了一部分修改而来,效果是双汞柱 不废话了 先上源码

package view;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

import com.example.sc.bloodpressuremeter.R;

import java.text.DecimalFormat;

/**
 * TODO: document your custom view class.
 */
public class Thermometer extends SurfaceView implements SurfaceHolder.Callback, Runnable {
    private SurfaceHolder mHolder;
    private Canvas mCanvas;

    //定义左侧范围
    int left_temperatureRange = 15;
    //定义右侧范围
    int right_temperatureRange = 20;
    //定义一个盘快的范围
    private RectF mRange = new RectF();
    //定义温度计的宽度和中心宽度
    int mWith;
    int mHeight;
    int centerWith;
    int centerHeight;

    //定义温度计刻度总长度
    int temperatureAllLong;

    //定义一下水银的宽度
    int MercuryWith;
    //十的倍数的线长度
    int MaxLineLong;
    //五的倍数的线的长度
    int MidLineLong;
    //其他刻度线的长度
    int MinLineLong;
    //左侧刻度间隔
    float left_scaleLong;
    //右侧刻度间隔
    float right_scaleLong;

    //定义温度计距离画布的上宽度
    float abHeight;

    //绘制线条的画笔
    private Paint LinePaint;
    //绘制文本的画笔
    private Paint TextPaint1;
    //绘制单位的画笔
    private Paint TextPaint;


    //设置温度上升的速度
    private volatile float mSpeed = 0;
    //kpa上升的速度
    private volatile float mSpeed_kpa = 0;

    //设置背景图
    private Bitmap mBitmap;

    /**
     * 定义初始温度,当前显示正在变化也就是显示的温度,还有目标温度
     * 其中,初始温度不变,
     * 当前温度是有程序根据不同的速度和目标温度计算出来的,
     * 目标温度则是由仪器传送过来的数据
     */
    private float BeginTenperature = (float) 0;
    private int left_EndTenperature = 300;
    private int right_EndTenperature = 40;
    private volatile float CurrentTemperature = (float) 0;
    private volatile float CurrentLow_hight = (float) 0;

    float TargetTemperature = 0;
    float Targetlow_hight = 0;

    /**
     * 定义每一秒绘制的次数
     */
    int everySecondTime = 100;

    //设置文字的大小
    private float mTextSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SHIFT, 25, getResources().getDisplayMetrics());
    private float mTextSize_ten = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SHIFT, 10, getResources().getDisplayMetrics());
    private float mSymbolTextSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SHIFT, 35, getResources().getDisplayMetrics());
    private float mShowSymbolTextSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SHIFT, 45, getResources().getDisplayMetrics());
    /**
     * 用户绘制的线程
     */
    private Thread mThread;
    /**
     * 根据目标温度改变要显示的当前温度的线程
     */
    private Thread mChangeTemperatureThread;

    /**
     * 设置一个标志位,用于线程的开启还是关闭的标志
     *
     * @param context
     */
    private Boolean isRunning, isRunning_kpa;

    private DecimalFormat fomat;//格式化float

    public Thermometer(Context context) {
        this(context, null);
    }

    public Thermometer(Context context, AttributeSet attrs) {
        super(context, attrs);
        mHolder = getHolder();
        mHolder.addCallback(this);

    }

    @Override
    protected void onMeasure(int with, int height) {
        super.onMeasure(with, height);
        this.mWith = getMeasuredWidth() / 2;
        this.mHeight = getMeasuredHeight();
        //这里先把中心设置在屏幕的中心
        this.centerWith = mWith / 2 + 100;
        this.centerHeight = mHeight / 2;
        //设置水银的宽度
        MercuryWith = mWith / 6;
        MinLineLong = MercuryWith;
        MidLineLong = MinLineLong * 8 / 6;
        MaxLineLong = MidLineLong * 3 / 2;
        //temperatureAllLong表示温度刻度总长度
        temperatureAllLong = mHeight * 9 / 10;
        //设置左侧刻度间隔,包含了刻度线的长度
        left_scaleLong = temperatureAllLong / left_temperatureRange / 10.0f;//表示一个温度十个刻度

        //设置右侧刻度间隔,包含了刻度线的长度
        right_scaleLong = temperatureAllLong / right_temperatureRange / 5.0f;//表示一个温度5个刻度

        abHeight = mHeight / 30.0f;
    }

    @Override
    public void surfaceCreated(SurfaceHolder surfaceHolder) {
        //初始化画笔
        LinePaint = new Paint();
        //去锯齿
        LinePaint.setAntiAlias(true);
        LinePaint.setColor(Color.WHITE);
        LinePaint.setStyle(Paint.Style.STROKE);
        LinePaint.setStrokeWidth(1);
        //初始化画笔
        TextPaint1 = new Paint();
        TextPaint1.setColor(Color.WHITE);
        TextPaint1.setTextSize(mTextSize);
        TextPaint1.setShader(null);
        //初始化画笔
        TextPaint = new Paint();
        TextPaint.setColor(Color.WHITE);
        TextPaint.setTextSize(mTextSize_ten);
        TextPaint.setShader(null);
        //初始化温度计的范围
        mRange = new RectF(0, 0, mWith, mHeight);
        isRunning = true;
        isRunning_kpa = true;
        mThread = new Thread(this);
        mThread.start();

    }

    @Override
    public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {

    }

    @Override
    public void surfaceDestroyed(SurfaceHolder surfaceHolder) {

        isRunning = false;
        isRunning_kpa = false;

    }

    @Override
    public void run() {
        //不断进行绘制
        while (isRunning) {
            long start = System.currentTimeMillis();
            draw();
            long end = System.currentTimeMillis();
            if (end - start < everySecondTime) {
                //这里控制一下,一秒绘制二十次。也就是五十秒绘制一次
                try {
                    Thread.sleep(everySecondTime - (end - start));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private void draw() {

        try {
            mCanvas = mHolder.lockCanvas();
            //这里要判断是不是为空,之因为在用户点击了home以后,可能已经执行到这里
            if (mCanvas != null) {
                //这里是开始绘制自己要的东西
                //先绘制背景,
                drawBg();
                //绘制水银的高度还有,显示体温
                drawShowHeightAndShow();
            }
        } catch (Exception e) {
            // e.printStackTrace();这里的异常不处理,
        } finally {
            if (mCanvas != null) {
                mHolder.unlockCanvasAndPost(mCanvas);
            }
        }

    }


    private void drawShowHeightAndShow() {
        float kpa = Targetlow_hight;
        float CurrentKpa = CurrentLow_hight;
        //这里控制水银的上升速度
        float difference = Math.abs(TargetTemperature - CurrentTemperature);
        float difference_kpa = Math.abs(kpa - CurrentKpa);
        /**
         * //这里定义一个boolean来控制是使用加法还是减法,其中true表示当前温度小于
         * 目标温度,要使用加法,false表示当前温度大于目标温度,要使用减法。
         */
        boolean addORsub = CurrentTemperature >= TargetTemperature ? false : true;
        boolean addOrsub_kpa = CurrentKpa >= kpa ? false : true;
        if (difference == 0 || difference <= 0.005) {
            mSpeed = 0;
            CurrentTemperature = TargetTemperature;
        } else {
            if (difference > 20) {
                mSpeed = (float) 0.5;
            } else {
                if (difference > 10) {
                    mSpeed = (float) 0.15;
                } else {
                    mSpeed = (float) 0.05;
                }
            }
        }
        if (difference_kpa == 0 || difference_kpa <= 0.005) {
            mSpeed_kpa = 0;
            CurrentKpa = kpa;
        } else {
            if (difference_kpa > 2) {
                mSpeed_kpa = (float) 0.6;
            } else {
                if (difference_kpa > 1) {
                    mSpeed_kpa = (float) 0.5;
                } else {
                    mSpeed_kpa = (float) 0.1;
                }
            }
        }
        if (addORsub) {
            CurrentTemperature += 20 * mSpeed;
        } else {
            CurrentTemperature -= 20 * mSpeed;
        }
        if (addOrsub_kpa) {
            CurrentKpa += 2 * mSpeed_kpa;
        } else {
            CurrentKpa -= 2 * mSpeed_kpa;
        }

        //

        Paint RightRectPaint = new Paint();
        RightRectPaint.setColor(Color.WHITE);
        RightRectPaint.setStyle(Paint.Style.FILL);
        Paint LeftRectPaint = new Paint();
        LeftRectPaint.setColor(Color.WHITE);
        LeftRectPaint.setStyle(Paint.Style.FILL);
        //这里主要是对温度的显示,画矩形的过程中,唯一改变的就是Top这一个值了
        //左侧水银柱
        if (Math.abs(CurrentTemperature - TargetTemperature) > 10) {
            float left = (temperatureAllLong / (left_temperatureRange * 1.0f)) * (int) (CurrentTemperature / 20) + (CurrentTemperature % 20) / 2 * (left_scaleLong);
            mCanvas.drawRect(centerWith - MercuryWith / 2, temperatureAllLong + abHeight * 2 - left, centerWith, temperatureAllLong + abHeight * 2, LeftRectPaint);
//            float right = (temperatureAllLong / (right_temperatureRange * 1.0f)) * (int) (CurrentKpa / 2) + (CurrentKpa % 2) / 2 * 4 * (right_scaleLong);
            float right = (temperatureAllLong / (left_temperatureRange * 1.0f)) * (int) (CurrentKpa / 20) + (CurrentKpa % 20) / 2 * (left_scaleLong);
            mCanvas.drawRect(centerWith + MercuryWith / 8, temperatureAllLong + abHeight * 2 - right, centerWith + MercuryWith / 2, temperatureAllLong + abHeight * 2, RightRectPaint);
            isRunning = true;
        } else {
            float left = (temperatureAllLong / (left_temperatureRange * 1.0f)) * (int) (TargetTemperature / 20) + (TargetTemperature % 20) / 2 * (left_scaleLong);
            mCanvas.drawRect(centerWith - MercuryWith / 2, temperatureAllLong + abHeight * 2 - left, centerWith, temperatureAllLong + abHeight * 2, LeftRectPaint);
//            float right = (temperatureAllLong / (right_temperatureRange * 1.0f)) * (int) (kpa / 2) + (kpa % 2) / 2 * 4 * (right_scaleLong);
            float right = (temperatureAllLong / (left_temperatureRange * 1.0f)) * (int) (kpa / 20) + (kpa % 20) / 2 * (left_scaleLong);
            mCanvas.drawRect(centerWith + MercuryWith / 8, temperatureAllLong + abHeight * 2 - right, centerWith + MercuryWith / 2, temperatureAllLong + abHeight * 2, RightRectPaint);
            isRunning = false;
        }
    }

    private void drawBg() {
        mCanvas.drawColor(getResources().getColor(R.color.class_blue));
        //画右边的刻度
        //定义每一个长刻度的高度
        float left_everyTemparaturHeight = temperatureAllLong / (left_temperatureRange * 1.0f);
        float right_everyTemparaturHeight = temperatureAllLong / (right_temperatureRange * 1.0f);
        mCanvas.drawLine(centerWith + MercuryWith / 2, abHeight * 2, centerWith + MercuryWith / 2, right_everyTemparaturHeight * 20 + abHeight * 2, LinePaint);
        for (int i = 0; i < right_temperatureRange; i++) {
            mCanvas.drawLine(centerWith + MercuryWith / 2, right_everyTemparaturHeight * i + abHeight * 2, centerWith + MercuryWith / 2 + MaxLineLong, right_everyTemparaturHeight * i + abHeight * 2, LinePaint);
            if (i == 0) {
                mCanvas.drawText(right_EndTenperature - i * 2 + "", centerWith + MercuryWith / 2 + MaxLineLong + MinLineLong / 3, right_everyTemparaturHeight * i + TextPaint1.getTextSize() / 2 + abHeight * 2, TextPaint1);
                mCanvas.drawText("Kpa", centerWith + MercuryWith / 2 + MaxLineLong + MinLineLong / 3 + 5, right_everyTemparaturHeight * i + TextPaint1.getTextSize() / 2 + abHeight * 2 + 15, TextPaint);
            } else {
                mCanvas.drawText(right_EndTenperature - i * 2 + "", centerWith + MercuryWith / 2 + MaxLineLong + MinLineLong / 3, right_everyTemparaturHeight * i + TextPaint1.getTextSize() / 2 + abHeight * 2, TextPaint1);
            }
            for (int j = 1; j < 5; j++) {
                mCanvas.drawLine(centerWith + MercuryWith / 2, right_everyTemparaturHeight * i + j * (right_scaleLong) + abHeight * 2, centerWith + MercuryWith / 2 + MinLineLong, right_everyTemparaturHeight * i + j * (right_scaleLong) + abHeight * 2, LinePaint);
            }
            //画最后一个刻度
            if (i == right_temperatureRange - 1) {

                mCanvas.drawLine(centerWith + MercuryWith / 2,
                        right_everyTemparaturHeight * (i + 1) + abHeight * 2,//这里加上两倍的上距离
                        centerWith + MercuryWith / 2 + MaxLineLong,
                        right_everyTemparaturHeight * (i + 1) + abHeight * 2, LinePaint);
                mCanvas.drawText(right_EndTenperature - (i + 1) * 2 + "", centerWith + MercuryWith / 2 + MaxLineLong + MinLineLong / 3,
                        right_everyTemparaturHeight * (i + 1) + TextPaint1.getTextSize() / 2 + abHeight * 2, TextPaint1);
            }
        }
        //画左边的刻度
        mCanvas.drawLine(centerWith - MercuryWith / 2, abHeight * 2, centerWith - MercuryWith / 2, left_everyTemparaturHeight * left_temperatureRange + abHeight * 2, LinePaint);
        for (int i = 0; i < left_temperatureRange; i++) {
            mCanvas.drawLine(centerWith - MercuryWith / 2, left_everyTemparaturHeight * i + abHeight * 2, centerWith - MercuryWith / 2 - MaxLineLong, left_everyTemparaturHeight * i + abHeight * 2, LinePaint);
            if (i == 0) {
                mCanvas.drawText(left_EndTenperature - i * 20 + "", centerWith - (MercuryWith / 2 + MaxLineLong + MinLineLong) - TextPaint1.getTextSize(), left_everyTemparaturHeight * i + TextPaint1.getTextSize() / 2 + abHeight * 2, TextPaint1);
                mCanvas.drawText("mmHg", centerWith - (MercuryWith / 2 + MaxLineLong + MinLineLong) - TextPaint1.getTextSize() + 5, left_everyTemparaturHeight * i + TextPaint1.getTextSize() / 2 + abHeight * 2 + 20, TextPaint);
            } else {
                mCanvas.drawText(left_EndTenperature - i * 20 + "", centerWith - (MercuryWith / 2 + MaxLineLong + MinLineLong) - TextPaint1.getTextSize(), left_everyTemparaturHeight * i + TextPaint1.getTextSize() / 2 + abHeight * 2, TextPaint1);
            }
            for (int j = 1; j <= 9; j++) {
                if (j == 5) {
                    mCanvas.drawLine(centerWith - MercuryWith / 2, left_everyTemparaturHeight * i + j * (left_scaleLong) + abHeight * 2, centerWith - MercuryWith / 2 - MidLineLong, left_everyTemparaturHeight * i + j * (left_scaleLong) + abHeight * 2, LinePaint);
                } else {
                    mCanvas.drawLine(centerWith - MercuryWith / 2, left_everyTemparaturHeight * i + j * (left_scaleLong) + abHeight * 2, centerWith - MercuryWith / 2 - MinLineLong, left_everyTemparaturHeight * i + j * (left_scaleLong) + abHeight * 2, LinePaint);
                }

            }
            //画最后一个刻度
            if (i == left_temperatureRange - 1) {
                mCanvas.drawLine(centerWith - MercuryWith / 2, left_everyTemparaturHeight * (i + 1) + abHeight * 2, centerWith - MercuryWith / 2 - MaxLineLong, left_everyTemparaturHeight * (i + 1) + abHeight * 2, LinePaint);
                mCanvas.drawText(left_EndTenperature - (i + 1) * 20 + "", centerWith - (MercuryWith / 2 + MaxLineLong + MinLineLong / 3) - TextPaint1.getTextSize(), left_everyTemparaturHeight * (i + 1) + TextPaint1.getTextSize() / 2 + abHeight * 2, TextPaint1);
            }
        }     
    }

    private float trueTemperature = 0;

    public void setTargetTemperature(float targetTemperature, float low_hight) {
        trueTemperature = targetTemperature;
        if (targetTemperature < 0) {
            targetTemperature = 0;
        }
        if (targetTemperature > left_EndTenperature) {
            targetTemperature = left_EndTenperature;
        }
        if (low_hight > left_EndTenperature) {
            low_hight = left_EndTenperature;
        }
        if (low_hight - targetTemperature > 10 && false) {
            TargetTemperature = targetTemperature;
            Targetlow_hight = targetTemperature + 10;
        } else {
            TargetTemperature = targetTemperature;
            Targetlow_hight = low_hight;
        }

        isRunning = true;
        draw();
    }

    public void setTargetTemperatureToZero() {
        TargetTemperature = 0;
        Targetlow_hight = 0;
        isRunning = true;
        run();
    }
}

用法:

            <view.Thermometer
            android:id = "@+id/thermometer"
            android:layout_width = "wrap_content"
            android:layout_height = "wrap_content" />
        thermometer = (Thermometer) view.findViewById(R.id.thermometer);
        thermometer.setZOrderOnTop(true);      // 这句不能少
        thermometer.setZOrderMediaOverlay(true);
        thermometer.getHolder().setFormat(PixelFormat.TRANSPARENT);

将控件放置顶部 如果不写这个,Fragment上面放置控件的时候 切换屏幕可能会导致控件不隐藏

常用方法:

    //将汞柱归零
    thermometer.setTargetTemperatureToZero();

这是单独开启的一个线程,会从当前温度降至最低点

    //输入数值 汞柱升高至对应数值 temp为左侧汞柱 temp_low_hight为右侧汞柱
    thermometer.setTargetTemperature(temp, temp_low_hight);

效果这个样子:
这里写图片描述

作者:jyww03 发表于2016/10/20 11:39:37 原文链接
阅读:15 评论:0 查看评论

安卓高级3 RecyclerView 和cardView使用案例

$
0
0

cardView:

添加依赖:在Studio搜索cardview即可 在V7包中
或者直接在gradle中添加
compile 'com.android.support:cardview-v7:24.0.0'

cardView 完成一些绚丽特效使用:

属性:
app:cardElevation=”10dp”添加景深 (阴影效果让其更立体)
app:cardCornerRadius=”10dp”四边圆角值
app:cardBackgroundColor=”@android:color/holo_blue_ligh”背景色
android:foreground=”?android:attr/selectableItemBackground” 用户点击时会生成涟漪效果

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="10dp"
    android:foreground="?android:attr/selectableItemBackground"
    android:orientation="vertical"
    app:cardBackgroundColor="@android:color/holo_blue_light"
    app:cardCornerRadius="10dp"
    app:cardElevation="10dp">


    <TextView
        android:id="@+id/item_tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_marginBottom="30dp"
        android:layout_marginTop="30dp"
        android:textColor="@android:color/white"
        android:textSize="20sp" />

</android.support.v7.widget.CardView>

效果图:
这里写图片描述


RecyclerView :

同样需要添加依赖 和上诉一样.

compile ‘com.android.support:recyclerview-v7:24.0.0’

可以实现GridView ListView 和流布局并且可以指定方向,和数据颠倒等

使用方法和适配器介绍:

  1. 在布局文件写入RecyclerView

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout 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"
        tools:context="qianfeng.com.day38_recyclerview.MainActivity">
    
    
    
        <android.support.v7.widget.RecyclerView
            android:id="@+id/recyclerView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"></android.support.v7.widget.RecyclerView>
    </RelativeLayout>
    
  2. 写布局管理器(多种) 和适配器:

    这里大家可以先不用管适配后面详解

    package qianfeng.com.day38_recyclerview;
    
    import android.os.Bundle;
    import android.support.v7.app.AppCompatActivity;
    import android.support.v7.widget.LinearLayoutManager;
    import android.support.v7.widget.RecyclerView;
    import android.widget.Toast;
    
    import java.util.ArrayList;
    import java.util.List;
    
    
    /**java
     * RecyclerView:
     * 升级版的ListView   融合了  ListView  GridView  瀑布流    横纵向滑动
     * RecyclerView  使用  LayoutManager  进行布局管理
     * 1.LinearLayoutManager   线性布局管理器   -- > 横向或纵向的ListView展示效果
     * 2.GridLayoutManager    网格布局管理器 ---> 横向或纵向的 GridView 展示效果
     * 3.StraggXXXXXXXXXX    瀑布流
     *
     * LayoutManager
     * 实例化的时候 参数 上下文对象,横竖展示标记, 反转
     *
     * recyclerView 通过setLayoutManager()  方式 设置 布局管理器
     *
     * RecyclerAdapter
     * 是抽象类
     * 需要继承与  RecyclerView.Adapter<VH></>
     *
     * 传入的 ViewHodler 对象 必须继承与 RecyclerView.ViewHolder
     * 并重写 含有ItemView 的 构造方法
     *
     * 继承 RecyclerView.Adapter<VH></>类之后  需要重写三个方法
     * 1.getItemCount
     *      返回数据源的长度
     * 2.onCreateViewHolder
     *      实例化 itmeView
     *      并创建ViewHolder 对象 将其返回
     * 3.onBindViewHolder
     *      数据源 与ViewHolder 的持有View 进行绑定
     *
     * 设置监听器
     * RecyclerView 本身并不含有监听器  需要自己添加
     *
     * 在适配器中定义接口及方法
     * 给ItemView设置监听 并 对外暴露出接口
     * 由RecyclerView 进行监听 点击事件
     *
     * 获取ItemView 的position
     * RecyclerView .getChildAdapterPosition(ItemView)
     *
     *
     * RecyclerView 没有分割线属性
     * 不推荐使用
     * LinearLayoutmanager  纵向模式下
     * 可以使用 DividerItemDecoration  进行 分割线修改
     *
     * recyclerView.addItemDecoration();  添加分割线属性
     *
     *  value  -- style
     *  添加一个    android:listDivider 属性
     *  值  添加  shape 或者 图片  作为分割线展示
     *  如果只添加颜色 则 没有高度  无法展示
     *
     * 推荐使用的:(不需要分割线)
     *
     * CardView
     *
     *
     */
    public class MainActivity extends AppCompatActivity implements MyRecyclerAdapter.OnItemClickListener {
    
        private RecyclerView recyclerView;
        private List<String> list = new ArrayList<>();
        private MyRecyclerAdapter myRecyclerAdapter;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            // 实例化RecyclerView 对象
            recyclerView = (RecyclerView) findViewById(R.id.recyclerView);
    
            /**
             *  上下文对象
             *  方向
             *  是否反转
             *  如果翻转 数据颠倒并且进入界面时跳到最后显示第一个item(相当于正向的第一个数据item)
             *  如果方向是横向LinearLayoutManager.HORIZONTAL 那么整个activity只显示一个item,向右滑动即可显示下一个item和Viewpager相识
             */
            LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false);
    //        GridLayoutManager gridLayoutManager = new GridLayoutManager(this, 4, LinearLayoutManager.HORIZONTAL, false);
            recyclerView.setLayoutManager(linearLayoutManager);
            // 添加自定义的   分割线 (不推荐)
            recyclerView.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL_LIST));
            initData();
            // 设置适配器
            myRecyclerAdapter = new MyRecyclerAdapter(list, this, recyclerView, this);
            recyclerView.setAdapter(myRecyclerAdapter);
        }
    
        private void initData() {
            for (int i = 0; i < 50; i++) {
                list.add("哈哈哈哈" + i);
            }
        }
        //以下监听为我们在适配器中添加的
      @Override
    public void onItemClickListener(int position) {
        Toast.makeText(MainActivity.this, "点击————" + position, Toast.LENGTH_LONG).show();
    }
    
    @Override
    public void onItemLongClickListener(int position) {
        Toast.makeText(MainActivity.this, "长按————" + position, Toast.LENGTH_LONG).show();
    }
    
    }
    

    效果图:
    这里写图片描述

    如果上诉案例写成竖直方向:

    LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
    效果图
    

    这里写图片描述

    • 反向:大家可以自己试试:这里直接说结果,上诉竖直方向会直接滑到哈哈0 (此时哈哈0是在列表底部的注意)

    上诉案例:用改用gridlayout

    //没行4个
    GridLayoutManager(this, 4, LinearLayoutManager.HORIZONTAL, false);
            recyclerView.setLayoutManager(gridLayoutManager);

    效果图:
    这里写图片描述
    另一个方向参考上一个Linear案例

    适配器

package qianfeng.com.day38_recyclerview;

import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import java.util.List;

/**
 * Created by ${Mr.Zhao} on 2016/10/19.
 */
public class MyRecyclerAdapter extends RecyclerView.Adapter<MyRecyclerAdapter.MyViewHolder> {


    List<String> list;
    Context context;
    LayoutInflater inflater;
    RecyclerView recyclerView;

    private OnItemClickListener listener;

    public interface OnItemClickListener {
        public void onItemClickListener(int position);

        public void onItemLongClickListener(int position);

    }


    public MyRecyclerAdapter(List<String> list, Context context, RecyclerView recyclerView, OnItemClickListener listener) {
        this.list = list;
        this.context = context;
        this.recyclerView = recyclerView;
        inflater = LayoutInflater.from(context);
        this.listener = listener;
    }

    // 该方法为 创建ViewHolder 使用
    @Override
    public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        //  实例化  itemView
        View view = inflater.inflate(R.layout.item_layout, parent, false);
        return new MyViewHolder(view);
    }

    //  该方法为  绑定数据使用
    @Override
    public void onBindViewHolder(MyViewHolder holder, int position) {
        holder.item_tv.setText(list.get(position));
    }

    // 返回数据长度
    @Override
    public int getItemCount() {
        return list.size();
    }

    // 定义  ViewHolder
    public class MyViewHolder extends RecyclerView.ViewHolder {
        TextView item_tv;

        // 必须实现的有参构造
        public MyViewHolder(final View itemView) {
            super(itemView);
            //   实例化 textView
            item_tv = ((TextView) itemView.findViewById(R.id.item_tv));

            itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    //   获取itemView 的 位置
                    int childPosition = recyclerView.getChildAdapterPosition(itemView);
                    // 监听方法
                    listener.onItemClickListener(childPosition);
                }
            });

            itemView.setOnLongClickListener(new View.OnLongClickListener() {
                @Override
                public boolean onLongClick(View v) {
                    int childAdapterPosition = recyclerView.getChildAdapterPosition(itemView);
                    listener.onItemLongClickListener(childAdapterPosition);
                    return true;
                }
            });
        }
    }
}
  • 一个分割线类
package qianfeng.com.day38_recyclerview;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.View;

/**
 *   ListView 模式 下的  下划线
 * Created by ${Mr.Zhao} on 2016/5/26.
 *
 *  可以自由更改分割线   android.R.attr.listDivider
 *  去  values  的 styles   下 添加 android:listDivider  属性  在面的值 可以任意修改
 *  图片或者颜色 或者 Drawable 资源
 */
public class DividerItemDecoration extends RecyclerView.ItemDecoration {

    private static final int[] ATTRS = new int[]{
            android.R.attr.listDivider
    };

    public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;

    public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;

    private Drawable mDivider;

    private int mOrientation;

    public DividerItemDecoration(Context context, int orientation) {
        final TypedArray a = context.obtainStyledAttributes(ATTRS);
        mDivider = a.getDrawable(0);
        a.recycle();
        setOrientation(orientation);
    }

    public void setOrientation(int orientation) {
        if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) {
            throw new IllegalArgumentException("invalid orientation");
        }
        mOrientation = orientation;
    }

    @Override
    public void onDraw(Canvas c, RecyclerView parent) {

        if (mOrientation == VERTICAL_LIST) {
            drawVertical(c, parent);
        } else {
            drawHorizontal(c, parent);
        }

    }


    public void drawVertical(Canvas c, RecyclerView parent) {
        final int left = parent.getPaddingLeft();
        final int right = parent.getWidth() - parent.getPaddingRight();

        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = parent.getChildAt(i);
            RecyclerView v = new RecyclerView(parent.getContext());
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
                    .getLayoutParams();
            final int top = child.getBottom() + params.bottomMargin;
            final int bottom = top + mDivider.getIntrinsicHeight();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);
        }
    }

    public void drawHorizontal(Canvas c, RecyclerView parent) {
        final int top = parent.getPaddingTop();
        final int bottom = parent.getHeight() - parent.getPaddingBottom();

        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = parent.getChildAt(i);
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
                    .getLayoutParams();
            final int left = child.getRight() + params.rightMargin;
            final int right = left + mDivider.getIntrinsicHeight();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);
        }
    }

    @Override
    public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
        if (mOrientation == VERTICAL_LIST) {
            outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
        } else {
            outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
        }
    }

}
作者:qfanmingyiq 发表于2016/10/20 11:51:03 原文链接
阅读:27 评论:0 查看评论

Android四大组件之Service

$
0
0

Service基本用法

新建一个MyService继承自Service,并重写父类的onCreate()、onStartCommand()和onDestroy()方法,如下所示:

public class MyService extends Service {  
   
    public static final String TAG = "MyService";  
   
    @Override  
    public void onCreate() {  
         super.onCreate();  
        Log.d(TAG, "onCreate() executed");  
     }  
   
    @Override  
    public int onStartCommand(Intent intent, int flags, int startId) {  
         Log.d(TAG, "onStartCommand() executed");  
         return super.onStartCommand(intent, flags, startId);  
    }  
       
     @Override  
     public void onDestroy() {  
        super.onDestroy();  
        Log.d(TAG, "onDestroy() executed");  
     }  
   
     @Override  
     public IBinder onBind(Intent intent) {  
         return null;  
    }  
} 


然后打开或新建MainActivity作为程序的主Activity,在里面加入启动Service和停止Service的逻辑,代码如下所示:

<span style="font-family:SimSun;font-size:12px;">public class MainActivity extends Activity implements OnClickListener {  
  
    private Button startService;  
  
    private Button stopService;  
  
    @Override  
    protected void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        setContentView(R.layout.activity_main);  
        startService = (Button) findViewById(R.id.start_service);  
        stopService = (Button) findViewById(R.id.stop_service);  
        startService.setOnClickListener(this);  
        stopService.setOnClickListener(this);  
    }  
  
    @Override  
    public void onClick(View v) {  
        switch (v.getId()) {  
        case R.id.start_service:  
            Intent startIntent = new Intent(this, MyService.class);  
            startService(startIntent);  
            break;  
        case R.id.stop_service:  
            Intent stopIntent = new Intent(this, MyService.class);  
            stopService(stopIntent);  
            break;  
        default:  
            break;  
        }  
    }  
  
}  </span>

可以看到,在Start Service按钮的点击事件里,我们构建出了一个Intent对象,并调用startService()方法来启动MyService。然后在Stop Serivce按钮的点击事件里,我们同样构建出了一个Intent对象,并调用stopService()方法来停止MyService。

onCreate()方法只会在Service第一次被创建的时候调用,如果当前Service已经被创建过了,不管怎样调用startService()方法,onCreate()方法都不会再执行。因此你可以再多点击几次Start Service按钮试一次,每次都只会有onStartCommand()方法执行。

另外需要注意,项目中的每一个Service都必须在AndroidManifest.xml中注册才行,所以还需要编辑AndroidManifest.xml文件,代码如下所示:

<span style="font-family:SimSun;font-size:12px;"><application  
        android:allowBackup="true"  
        android:icon="@drawable/ic_launcher"  
        android:label="@string/app_name"  
        android:theme="@style/AppTheme" >  
          
    ……  
  
        <service android:name="com.example.servicetest.MyService" >  
        </service></span>

Service和Activity通信

上面的MyService中的代码,你会发现一直有一个onBind()方法我们都没有使用到,这个方法其实就是用于和Activity建立关联的。

<span style="font-family:SimSun;font-size:12px;">public class MyService extends Service {  
  
    public static final String TAG = "MyService";  
  
    private MyBinder mBinder = new MyBinder();  
  
    @Override  
    public void onCreate() {  
        super.onCreate();  
        Log.d(TAG, "onCreate() executed");  
    }  
  
    @Override  
    public int onStartCommand(Intent intent, int flags, int startId) {  
        Log.d(TAG, "onStartCommand() executed");  
        return super.onStartCommand(intent, flags, startId);  
    }  
  
    @Override  
    public void onDestroy() {  
        super.onDestroy();  
        Log.d(TAG, "onDestroy() executed");  
    }  
  
    @Override  
    public IBinder onBind(Intent intent) {  
        return mBinder;  
    }  
  
    class MyBinder extends Binder {  
  
        public void startDownload() {  
            Log.d("TAG", "startDownload() executed");  
            // 执行具体的下载任务  
        }  
  
    }  
  
}  
public class MainActivity extends Activity implements OnClickListener {  
  
    private Button startService;  
  
    private Button stopService;  
  
    private Button bindService;  
  
    private Button unbindService;  
  
    private MyService.MyBinder myBinder;  
  
    private ServiceConnection connection = new ServiceConnection() {  
  
        @Override  
        public void onServiceDisconnected(ComponentName name) {  
        }  
  
        @Override  
        public void onServiceConnected(ComponentName name, IBinder service) {  
            myBinder = (MyService.MyBinder) service;  
            myBinder.startDownload();  
        }  
    };  
  
    @Override  
    protected void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        setContentView(R.layout.activity_main);  
        startService = (Button) findViewById(R.id.start_service);  
        stopService = (Button) findViewById(R.id.stop_service);  
        bindService = (Button) findViewById(R.id.bind_service);  
        unbindService = (Button) findViewById(R.id.unbind_service);  
        startService.setOnClickListener(this);  
        stopService.setOnClickListener(this);  
        bindService.setOnClickListener(this);  
        unbindService.setOnClickListener(this);  
    }  
  
    @Override  
    public void onClick(View v) {  
        switch (v.getId()) {  
        case R.id.start_service:  
            Intent startIntent = new Intent(this, MyService.class);  
            startService(startIntent);  
            break;  
        case R.id.stop_service:  
            Intent stopIntent = new Intent(this, MyService.class);  
            stopService(stopIntent);  
            break;  
        case R.id.bind_service:  
            Intent bindIntent = new Intent(this, MyService.class);  
            bindService(bindIntent, connection, BIND_AUTO_CREATE);  
            break;  
        case R.id.unbind_service:  
            unbindService(connection);  
            break;  
        default:  
            break;  
        }  
    }  
  
}  </span>

  可以看到,这里我们首先创建了一个ServiceConnection的匿名类,在里面重写了onServiceConnected()方法和onServiceDisconnected()方法,这两个方法分别会在Activity与Service建立关联和解除关联的时候调用。在onServiceConnected()方法中,我们又通过向下转型得到了MyBinder的实例,有了这个实例,Activity和Service之间的关系就变得非常紧密了。现在我们可以在Activity中根据具体的场景来调用MyBinder中的任何public方法,即实现了Activity指挥Service干什么Service就去干什么的功能。

  当然,现在Activity和Service其实还没关联起来了呢,这个功能是在Bind Service按钮的点击事件里完成的。可以看到,这里我们仍然是构建出了一个Intent对象,然后调用bindService()方法将Activity和Service进行绑定。bindService()方法接收三个参数,第一个参数就是刚刚构建出的Intent对象第二个参数是前面创建出的ServiceConnection的实例第三个参数是一个标志位,这里传入BIND_AUTO_CREATE表示在Activity和Service建立关联后自动创建Service,这会使得MyService中的onCreate()方法得到执行,但onStartCommand()方法不会执行。

  然后如何我们想解除Activity和Service之间的关联怎么办呢?调用一下unbindService()方法就可以了,这也是Unbind Service按钮的点击事件里实现的逻辑。

如何销毁Service

  点击Start Service按钮启动Service,再点击Stop Service按钮停止Service,这样MyService就被销毁了.

  点击的Bind Service按钮呢?由于在绑定Service的时候指定的标志位是BIND_AUTO_CREATE,说明点击Bind Service按钮的时候Service也会被创建,这时应该怎么销毁Service呢?其实也很简单,点击一下Unbind Service按钮,将Activity和Service的关联解除就可以了。

  如果我们既点击了Start Service按钮,又点击了Bind Service按钮会怎么样呢?这个时候你会发现,不管你是单独点击Stop Service按钮还是Unbind Service按钮,Service都不会被销毁,必要将两个按钮都点击一下,Service才会被销毁。也就是说,点击Stop Service按钮只会让Service停止,点击Unbind Service按钮只会让Service和Activity解除关联,一个Service必须要在既没有和任何Activity关联又处理停止状态的时候才会被销毁

服务的分类:


解释:

①AIDL(Android Interface Definition Language)是Android接口定义语言的意思,它可以用于让某个Service与多个应用程序组件之间进行跨进程通信,从而可以实现多个应用程序共享同一个Service的功能。

②为什么将MyServiSce转换成远程Service后就不会导致程序ANR了呢?这是由于,使用了远程Service后,MyService已经在另外一个进程当中运行了,所以只会阻塞该进程中的主线程,并不会影响到当前的应用程序。

③那么如何才能让Activity与一个远程Service建立关联呢?这就要使用AIDL来进行跨进程通信了(IPC)。

具体AIDL跨进程通信见《Android 进程间通信》


Service 和 IntentService的区别:

参考:http://blog.csdn.net/p106786860/article/details/17885115

IntentService的使用   

IntentService是Service的子类,比普通的Service增加了额外的功能。IntentService是继承Service的,那么它包含了Service的全部特性,当然也包含service的生命周期和启动方式,IntentService集开启线程和自动停止于一身,IntentService还是博得了不少程序员的喜爱。

IntentService用法:

这里首先是要提供一个无参的构造函数,并且必须在其内部调用父类的有参构造函数。然后要在子类中去实现 onHandleIntent()这个抽象方法,在这个方法中可以去处理一些具体的 逻辑,而且不用担心 ANR(Application Not Responding)的问题,因为这个方法已经是在子线程中运行的了。这里为了证 实一下,我们在 onHandleIntent()方法中打印了当前线程的 id。另外根据 IntentService的特性, 这个服务在运行结束后应该是会自动停止的,所以我们又重写了 onDestroy()方法,在这里也打印了一行日志,以证实服务是不是停止掉了。


Service和线程的关系:

Service 服务 和 Thread进程的区别  http://www.cnblogs.com/perfy/p/3820502.html

Service是运行在主线程里的,也就是说如果你在Service里编写了非常耗时的代码,程序必定会出现ANR的。

你可能会惊呼,这不是坑爹么!?那我要Service又有何用呢?其实大家不要把后台和子线程联系在一起就行了,这是两个完全不同的概念。Android的后台就是指,它的运行是完全不依赖UI的即使Activity被销毁,或者程序被关闭,只要进程还在,Service就可以继续运行。比如说一些应用程序,始终需要与服务器之间始终保持着心跳连接,就可以使用Service来实现。你可能又会问,前面不是刚刚验证过Service是运行在主线程里的么?在这里一直执行着心跳连接,难道就不会阻塞主线程的运行吗?当然会,但是我们可以在Service中再创建一个子线程,然后在这里去处理耗时逻辑就没问题了。

既然在Service里也要创建一个子线程,那为什么不直接在Activity里创建呢?这是因为Activity很难对Thread进行控制,当Activity被销毁之后,就没有任何其它的办法可以再重新获取到之前创建的子线程的实例。而且在一个Activity中创建的子线程,另一个Activity无法对其进行操作。但是Service就不同了,所有的Activity都可以与Service进行关联,然后可以很方便地操作其中的方法,即使Activity被销毁了,之后只要重新与Service建立关联,就又能够获取到原有的Service中Binder的实例。因此,使用Service来处理后台任务,Activity就可以放心地finish,完全不需要担心无法对后台任务进行控制的情况。

生命周期


  一旦在项目的任何位置调用了 Context的 startService()方法,相应的服务就会启动起来,并回调 onStartCommand()方法。如果这个服务之前还没有创建过,onCreate()方法会先于onStartCommand()方法执行。服务启动了之后会一直保持运行状态,直到 stopService()或stopSelf()方法被调用。注意虽然每调用一次startService()方法,onStartCommand()就会执行一次,但实际上每个服务都只会存在一个实例。所以不管你调用了多少次 startService()方法, 只需调用一次 stopService()或 stopSelf()方法,服务就会停止下来了。

  另外,还可以调用 Context的 bindService()来获取一个服务的持久连接,这时就会回调服务中的 onBind()方法。类似地,如果这个服务之前还没有创建过,onCreate()方法会先于 onBind()方法执行。之后,调用方可以获取到 onBind()方法里返回的 IBinde对象的实例,这样就能自由地和服务进行通信了。只要调用方和服务之间的连接没有断开,服务就会一直保持运行状态。 当调用了 startService()方法后,又去调用 stopService()方法,这时服务中的 onDestroy() 方法就会执行,表示服务已经销毁了。类似地,当调用了 bindService()方法后,又去调用 unbindService()方法,onDestroy()方法也会执行,这两种情况都很好理解。

  但是需要注意,我们是完全有可能对一个服务既调用了 startService()方法,又调用了 bindService()方法的, 这种情况下该如何才能让服务销毁掉呢?根据 Android系统的机制,一个服务只要被启动或者被绑定了之后,就会一直处于运行状态,必须要让以上两种条件同时不满足,服务才能被销毁。所以,这种情况下要同时调用 stopService()和 unbindService()方法,onDestroy()方法才会执行。


系统Service

参考:

Android的系统服务一览

Android中的服务(service)详解(四)--系统服务

1. 概要:

Android系统服务提供系统最基本的,最核心的功能,如设备控制,位置信息,通知等。这些服务有的在Framework层,有的在Libraries层。

2. 分类:

正如前面的应用服务一样,根据实现方式 ,我们可以把系统服务分为java系统服务本地系统服务。存在于Framework层的,我们称之为java系统服务,这些服务都处于框架层,是用java语言编写的;存在于Libraries层的,我们称之为本地系统服务,这些服务都处于更低的Libraries层,是用C++语言编写的,运行在各自独立的进程中。

例如:

(1) 在Android Framework框架层服务有:

Activity Manager, Content Providers, NotificationManager, PackageManager, TelephonyManager, Location Manager..., 这些组件都是以单例模式在SystemServer进程中进行初始化的。

1EntropyService

熵服务,周期性的加载和保存随机信息。主要是linux开机后,/dev/random的状态可能是可预知的,这样一些需要随机信息的应用程序就可能会有问题。这个无需提供应用程序接口。

2PowerManagerService –> PowerManager

Android 的电源管理也是很重要的一部分。比如在待机的时候关掉不用的设备,待机时屏幕和键盘背光的关闭,用户操作的时候该打开多少设备等等。

3.ActivityManagerService->ActivityManager

这个是整个Android framework框架中最为核心的一个服务,管理整个框架中任务、进程管理, Intent解析等的核心实现。虽然名为Activity的Manager Service,但它管辖的范围,不只是Activity,还有其他三大组件,和它们所在的进程。也就是说用户应用程序的生命管理,都是由他负责的。

4.TelephonyRegistry->TelephonyManager

电话注册、管理服务模块,可以获取电话的链接状态、信号强度等等。<可以删掉,但要看的大概明白>

5.PackageManagerService -> PackageManager

包括对软件包的解包,验证,安装以及升级等等,对于我们现在不能安装.so文件的问题,应该先从这块着手分析原因。

6.AccountManagerService -> AccountManager

A system service that provides  account, password, and authtoken management for all

 accounts on the device

7ContentService -> ContentResolver

内容服务,主要是数据库等提供解决方法的服务。

8BatteryService

监控电池充电及状态的服务,当状态改变时,会广播Intent

9HardwareService

一般是ringvibrate的服务程序

10SensorService -> SensorManager

管理Sensor设备的服务,负责注册client设备及当client需要使用sensor时激活Sensor

11WindowManagerService -> WindowManager -> PhoneWindowManager

ActivityManagerService高度粘合

窗口管理,这里最核心的就是输入事件的分发和管理。

12AlarmManagerService -> AlarmManager

闹钟服务程序

13BluetoothService -> BluetoothDevice

蓝牙的后台管理和服务程序

14StatusBarService -> StatusBarManager

负责statusBar上图标的更新、动画等等的服务,服务不大。

15ClipboardService -> ClipboardManager

和其他系统的clipBoard服务类似,提供复制黏贴功过。

16InputMethodManagerService -> InputMethodManager

输入法的管理服务程序,包括何时使能输入法,切换输入法等等。

17NetStatService

手机网络服务

18ConnectivityService -> ConnectivityManager

网络连接状态服务,可供其他应用查询,当网络状态变化时,也可广播改变。

19AccessibilityManagerService-> AccessibilityManager

这块可能要仔细看一下,主要是一些View获得点击、焦点、文字改变等事件的分发管理,对整个系统的调试、问题定位等,也需要最这个服务仔细过目一下。

20NotificationManagerService -> NotificationManager

负责管理和通知后台事件的发生等,这个和statusbar胶黏在一起,一般会在statusbar上添加响应图标。用户可以通过这知道系统后台发生了什么事情。

21MountService

磁盘加载服务程序,一般要和一个linux daemon程序如vold/mountd等合作起作用,主要负责监听并广播devicemount/unmount/bad removal等等事件。

22DeviceStorageMonitorService

       监控磁盘空间的服务,当磁盘空间不足10%的时候会给用户警告

23LocationManagerService -> LocationManager

       要加入GPS服务等,这部分要细看,现在应用中的navigation没响应,可以从此处着手看一下

24SearchManagerService -> SearchManager

The search manager service handles the search UI, and maintains a registry of searchable activities.

25Checkin Service(FallbackCheckinService)

貌似checkin servicegoogle提供的包,没有源代码,源码只有fallbackCheckinService

26WallpaperManagerService -> WallpaperManager
管理桌面背景的服务,深度定制化桌面系统,需要看懂并扩展<同时要兼容>这部分

27AudioService -> AudioManager

AudioFlinger的上层管理封装,主要是音量、音效、声道及铃声等的管理

28HeadsetObserver

耳机插拔事件的监控小循环

29DockObserver

如果系统有个座子,当手机装上或拔出这个座子的话,就得靠他来管理了

30BackupManagerService -> BackupManager

备份服务

31AppWidgetService -> AppWidgetManager

Android可以让用户写的程序以widget的方式放在桌面上,这就是这套管理和服务的接口

32StatusBarPolicy

管理哪个图标该在status bar上显示的策略。

(2)在Libraries层的系统服务有:
SurfaceFlinger

这是framebuffer合成的服务,将各个应用程序及应用程序中的逻辑窗口图像数据(surface)合成到一个物理窗口中显示(framebuffer)的服务程序

mediaServer服务进程:

MediaServer服务基本上都是native的services,mediaServer进程也是在init.rc中启动的,它不是一个daemon进程,这点容易搞混。他也是和systemserver进程类似的系统服务进程,提供应用进程的RPC调用的真正服务代码所运行的位置。其服务都是和媒体录播放有关,主要有三个服务:

AudioFlinger

声音的录播放服务,包括混音等

MediaPlayerService

提供媒体播放服务,opencore是这块的核心模块,对java端的接口在mediaplayer.java

CameraService

提供camera的录制、preview等功能的服务

AudioPolicyService

主要功能有检查输入输出设备的连接状态及系统的音频策略的切换等。

3.系统服务的使用:

相信大家对getSystemService()并不陌生,无论是java系统服务,还是本地系统服务,直接调用getSystemService()就能获取指定的服务,这一点与应用服务(前面几节已经讲过)不同(应用服务是通过startService()来启动的。)。之所以能直接使用getSystemService(),是因为在Android初始化过程中,已经在init进程中启动了这些服务。

SensorManager SM = (SensorManager)getSystemService(context.SENSOR_SERVICE);

4.系统服务的实现:

无论是java系统服务,还是本地系统服务,要实现它,就要按照Android平台的要求,实现相应的函数和接口,这需要在源码的基本上进行修改。 

如何保证Service不被杀死

1.onDestroy方法里重启service

service +broadcast  方式,就是当service走ondestory的时候,发送一个自定义的广播,当收到广播的时候,重新启动service;

<receiver android:name="com.dbjtech.acbxt.waiqin.BootReceiver" >  

   <intent-filter>  

       <action android:name="android.intent.action.BOOT_COMPLETED" />  

        <action android:name="android.intent.action.USER_PRESENT" />  

        <action android:name="com.dbjtech.waiqin.destroy" />//这个就是自定义的action  

   </intent-filter>  

</receiver>  

在onDestroy时:

@Override  

public void onDestroy() {  

   stopForeground(true);  

     Intent intent = new Intent("com.dbjtech.waiqin.destroy");  

     sendBroadcast(intent);  

    super.onDestroy();  

}  

在BootReceiver里

<span style="font-family:SimSun;font-size:12px;">public class BootReceiver extends BroadcastReceiver {  
   
     @Override  
     public void onReceive(Context context, Intent intent) {  
         if (intent.getAction().equals("com.dbjtech.waiqin.destroy")) {  
            //TODO  
            //在这里写重新启动service的相关操作  
            startUploadService(context);  
        }  
   }  
 }</span>

也可以直接在onDestroy()里startService

<span style="font-family:SimSun;font-size:12px;">@Override  
public void onDestroy() {  
     Intent sevice = new Intent(this, MainService.class);  
     this.startService(sevice);  
     super.onDestroy();  
}  </span>

【结论】当使用类似口口管家等第三方应用或是在setting里-应用-强制停止时,APP进程可能就直接被干掉了,onDestroy方法都进不来,所以还是无法保证~.~

2.监听系统广播判断Service状态

通过系统的一些广播,比如:手机重启、界面唤醒、应用状态改变等等监听并捕获到,然后判断我们的Service是否还存活,别忘记加权限啊。

<span style="font-family:SimSun;font-size:12px;"><receiver android:name="com.dbjtech.acbxt.waiqin.BootReceiver" >  
    <intent-filter>  
        <action android:name="android.intent.action.BOOT_COMPLETED" />  
        <action android:name="android.intent.action.USER_PRESENT" />  
        <action android:name="android.intent.action.PACKAGE_RESTARTED" />  
        <action android:name="com.dbjtech.waiqin.destroy" />  
    </intent-filter>  
</receiver>  </span>

BroadcastReceiver中:

<span style="font-family:SimSun;font-size:12px;">@Override  
public void onReceive(Context context, Intent intent) {  
    if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {  
        System.out.println("手机开机了....");  
        startUploadService(context);  
    }  
    if (Intent.ACTION_USER_PRESENT.equals(intent.getAction())) {  
        startUploadService(context);  
    }  
} </span>

【结论】这也能算是一种措施,不过感觉监听多了会导致Service很混乱,带来诸多不便

3.AlarmManager 定时广播重新开启服务

这点不具体展开,具体见第一行代码,天气应用实例

作者:Evan123mg 发表于2016/10/20 12:09:40 原文链接
阅读:35 评论:0 查看评论

实现折叠式Toolbar:CollapsingToolbarLayout 使用完全解析

$
0
0

简介

在各种不同的应用中,大家可能会经常见到这样一个效果:Toolbar是透明的,有着一个背景图片以及大标题,随着页面向上滑动,其标题逐渐缩放到Toolbar上,而背景图片则在滑动到一定程度后变成了Toolbar的颜色,这种效果也即是折叠式效果。其实这种效果在GitHub上面已经有很多开源库实现了,但是Google在其推出的Design Library库中也给出了一个这种控件,让我们很方便地实现了这种效果。这个控件是CollapsingToolbarLayout,它是一个增强型的FrameLayout。那么,本篇文章就给大家详细地介绍该控件的使用方法以及注意事项

效果

本文结合一个Demo来进行演示,下面是最终的显示效果,也即折叠式Toolbar的效果:
效果1

引入

使用该控件,需要引入Android Design Library这个库,同时地,我们需要把app的主题也要做相应的修改以便适应这个控件,所以我们也需要appcompat这个库,那么我们在build.gradle文件中引入如下:

dependencies {
    compile 'com.android.support:cardview-v7:24.1.0'  //cardview
    compile 'com.android.support:design:24.1.0'
    compile 'com.android.support:appcompat-v7:24.1.0'
}

本文内容均基于官方文档,有兴趣的读者可以前往官方文档进一步查看(自备梯子)。

知识储备

接下来,笔者一步步地介绍该控件的用法。首先,我们先来了解这个控件的常用xml属性。

一、常用xml属性介绍

1)contentScrim:当Toolbar收缩到一定程度时的所展现的主体颜色。即Toolbar的颜色。
2)title:当titleEnable设置为true的时候,在toolbar展开的时候,显示大标题,toolbar收缩时,显示为toolbar上面的小标题。
3)scrimAnimationDuration:该属性控制toolbar收缩时,颜色变化的动画持续时间。即颜色变为contentScrim所指定的颜色进行的动画所需要的时间。
4)expandedTitleGravity:指定toolbar展开时,title所在的位置。类似的还有expandedTitleMargin、collapsedTitleGravity这些属性。
5)collapsedTitleTextAppearance:指定toolbar收缩时,标题字体的样式,类似的还有expandedTitleTextAppearance。

二、常见的标志位

一般开发中,CollapsingToolbarLayout不会单独出现在布局文件中,而是作为另一个控件CoordinatorLayout的子元素出现,那么CoordinatorLayout又是什么呢?其实CoordinatorLayout这个控件很强大,能对其子元素实现多种不同的功能,一个常见的用法就是:给它的一个子元素A设置一个layout_scrollFlags的属性,然后给另外一个子元素B设置一个layout_behavior=”@string/appbar_scrolling_view_behavior”的属性,这个子元素B一般是一个可以滑动的控件,比如RecyclerView、NestedScrollView等,那么当子元素B滑动的时候,子元素A就会根据其layout_scrollFlags的属性值而做出不同的改变,所以我们要为CollapsingToolbarLayout设置layout_scrollFlags属性。

layout_scrollFlags

我们来看看layout_scrollFlags有哪几个属性可以选择:
* scroll:所有想要滑动的控件都要设置这个标志位。如果不设置这个标志位,那么View会固定不动。
* enterAlways:设置了该标志位后,若View已经滑出屏幕,此时手指向下滑,View会立刻出现,这是另一种使用场景。
* enterAlwaysCollapsed:设置了minHeight,同时设置了该标志位的话,view会以最小高度进度屏幕,当滑动控件滑动到顶部的时候才会拓展为完整的高度。
* exitUntilCollapsed:向上滑动时收缩当前View。但view可以被固定在顶部。
可能直接用语言来描述还是有点太抽象,下面会以实际的效果给大家展示这几个标志位的具体作用。

layout_collapseMode

上面提到CollapsingToolbarLayout是一个FrameLayout,它内部能有多个子元素,而子元素也会有不同的表现。比如说,在上面的GIF图中,toolbar在缩放后是固定在顶部的,而imageview则是随着布局的滚动而滚动,也即存在一个相对滚动的过程。所以这些子元素可以添加layout_collapseMode标志位进而产生不同的行为。其实这里也只有两种标志位,分别是:
* pin:有该标志位的View在页面滚动的过程中会一直停留在顶部,比如Toolbar可以被固定在顶部
* parallax:有该标志位的View表示能和页面同时滚动。与该标志位相关联的一个属性是:layout_collapseParallaxMultiplier,该属性是视差因子,表示该View与页面的滚动速度存在差值,造成一种相对滚动的效果。

三、常用的层级关系

上面说到CollapsingToolbarLayout一般作为CoordinatorLayout的子元素出现,其实如果要实现上面的效果,还需要另外一个控件:AppBarLayout。该控件也是Design库的控件,作用是把其所有子元素当做一个AppBar来使用。一般来说,实现折叠式Toolbar可以使用以下的层级关系:

<android.support.design.widget.CoordinatorLayout...>
    <android.support.design.widget.AppBarLayout...>
        <android.support.design.widget.CollapsingToolbarLayout...>
            <!-- your collapsed view -->
            <View.../>
            <android.support.v7.widget.Toolbar.../>
        </android.support.design.widget.CollapsingToolbarLayout>
    </android.support.design.widget.AppBarLayout>

    <!-- Scroll view -->
    <android.support.v7.widget.RecyclerView.../>
</android.support.design.widget.CoordinatorLayout>

从上面的层级关系来看,最外面的一层是CoordinatorLayout,它有两个子元素,分别是AppBarLayout和RecyclerView(可滑动控件),而AppBarLayout则包裹着CollapsingToolbarLayout,CollapsingToolbarLayout的子元素分别是被折叠的View(可以是一张图片,也可以是一个布局)以及我们的Toolbar。

例子①

有了以上的知识储备,我们就可以开始动手写代码了,我们的目标是实现上面的gif图的效果。
1、在activity_main.xml文件中(注:以下注释只是为了方便说明):

<android.support.design.widget.CoordinatorLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"  <!-- 自定义命名空间 -->
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:fitsSystemWindows="true">

        <android.support.design.widget.CollapsingToolbarLayout
            android:id="@+id/collapsing_toolbar_layout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:contentScrim="?attr/colorPrimary"       <!--toolbar折叠后的主体颜色  -->
            app:expandedTitleMarginEnd="10dp"           <!--文字展开时的Margin  -->
            app:expandedTitleMarginStart="10dp"
            app:collapsedTitleTextAppearance="@style/TextAppearance.AppCompat.Title"    <!--字体的表现  -->
            app:layout_scrollFlags="scroll|exitUntilCollapsed">             


            <ImageView
                android:id="@+id/iv"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:scaleType="centerCrop"
                app:layout_collapseMode="parallax"               <!--设置imageView可随着滑动控件的滑动而滑动 -->
                app:layout_collapseParallaxMultiplier="0.5"/>    <!--视差因子 -->

            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                app:layout_collapseMode="pin" />        <!--toolbar折叠后固定于顶部 -->
        </android.support.design.widget.CollapsingToolbarLayout>

    </android.support.design.widget.AppBarLayout>

    <android.support.v4.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">   <!--为滑动控件设置Behavior,这样上面的控件才能做出相应改变 -->

        <LinearLayout
            android:orientation="vertical"
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <include layout="@layout/item_card"/>
            <include layout="@layout/item_card"/>
            <include layout="@layout/item_card"/>
            <include layout="@layout/item_card"/>
            <include layout="@layout/item_card"/>
            <include layout="@layout/item_card"/>
            <include layout="@layout/item_card"/>
            <include layout="@layout/item_card"/>

        </LinearLayout>
    </android.support.v4.widget.NestedScrollView>
</android.support.design.widget.CoordinatorLayout>

与其相关联的item_card.xml布局文件:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="100dp"
    android:layout_margin="5dp"
    app:cardElevation="5dp"
    app:contentPaddingTop="2dp"
    app:contentPaddingBottom="2dp">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Learn and Share Android"
        android:textSize="20sp"
        android:layout_gravity="center"/>

</android.support.v7.widget.CardView>

2、在MainActivity.java文件中再做出一些处理:

    private ImageView iv;
    private CollapsingToolbarLayout collapsingToolbarLayout;
    private Toolbar toolbar;

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

        collapsingToolbarLayout = (CollapsingToolbarLayout) findViewById(R.id.collapsing_toolbar_layout);
        iv = (ImageView) findViewById(R.id.iv);
        toolbar = (Toolbar) findViewById(R.id.toolbar);

        setSupportActionBar(toolbar);
        toolbar.setNavigationIcon(R.mipmap.ic_drawer_home);
        collapsingToolbarLayout.setTitle("DesignLibrarySample");
        collapsingToolbarLayout.setCollapsedTitleTextColor(Color.WHITE);
        collapsingToolbarLayout.setExpandedTitleColor(Color.WHITE);
        iv.setImageResource(R.mipmap.ic_bg);
    }

上面,我们为collapsingToolbarLayout设置了标题,以及收缩时标题的颜色和展开时标题的颜色等。经过上面的一个简单例子,就能实现上面gif图所显示的折叠式Toolbar的效果了。

这里先小结一下:在CoordinatorLayout作为父布局的情况下,给滑动控件设置一个layout_behavior=”@string/appbar_scrolling_view_behavior”标志位(该Behavior系统以及帮我们实现),那么当带有这个标志位的控件滑动的时候会触发带有scroll_flags标志位的另一个控件进行滑动,此时imageview的layout_collapseMode是parallax,所以它会以有视差的方式来相对滑动,而toolbar设置了pin的标记位,所以在收缩后会固定在屏幕顶部。

例子②

在例子①内,我们为CollapsingToolbarLayout设置的scroll_flags是”scroll | exitUntilCollapsed”,那么我们把标志位换成别的会有什么不同的效果呢?
在activity_main.xml内,作如下修改:


<android.support.design.widget.CollapsingToolbarLayout
    ...
   app:layout_scrollFlags="scroll|enterAlwaysCollapsed|enterAlways">

然后别的不作改动,效果如下:
效果2
显然,所造成的效果发生了变化,这里toolbar并不一致固定在顶部了,而是随着滑动而滑出了屏幕之外,同时如果手指向下滑动,toolbar会逐渐出现并保持着最小的高度,等到回到了最顶部后,toolbar会展开成原来的样子。
那么,基于以上的例子,如果上面少了一个“enterAlwaysCollapsed”这个标志位又会怎样呢?该标志位的作用上面也已经解释过了,是控制toolbar以最小的高度进入屏幕,并且在滑动控件滑动到最顶端的时候再展开成完整的高度。如果少了这个标志位,在我们手指向下滑的时候,toolbar也会逐渐出现,但是与上面gif图不同的是,toolbar会继续展开变成原来的样子,即出现imageview。图这里就不放出来了,读者可以自行验证~

通过以上的两个小例子,我们对CollapsingToolbarLayout有了一定的认识,也学会了它的使用方法了,使用它能让我们的应用变得更加美观。那么最后,我们再来谈谈注意事项,也即笔者开发过程中遇到的坑。

注意事项

1、Android Design Support Library的使用需要配合特定的主题,一般用AppCompat下的主题即可,也可以自定义主题,继承自AppCompat的主题,否则会报错。另外如果使用Android Studio的话,主题的相关代码需要在styles.xml(v21)文件内做出相应的修改,否则使用Android 5.0以上的机子做测试的话也会报错。
2、由于使用了AppCompat的主题,那么我们的Activity应该继承自AppCompatActivity。
3、笔者之前使用design support library的版本号是23.1.0,在此版本上,CollapsingToolbarLayout没有设置collapsedTitleTextAppearance属性,标题可以正常显示,然而到了24.1.0版本,即上面所用的版本,如果没有设置collapsedTitleTextAppearance属性,则当toolbar收缩后,其标题文字变得非常小。所以我们要设置collapsedTitleTextAppearance=”@style/TextAppearance.AppCompat.Title”这个属性,才能变得正常。
4、如果没有为CollapsingToolbarLayout设置一个title,那么会使用ActionBar自带的标题来显示应用的名称,这是因为调用了setSupportActionBar(toolbar)函数。

最后附上代码的地址:DesignSupportLibrarySample

作者:a553181867 发表于2016/10/20 12:29:51 原文链接
阅读:66 评论:0 查看评论

安卓友盟分享集成微信QQ微博分享demo精简版

$
0
0

最近做项目用到了分享功能,介于友盟的全家桶,于是放弃shareSDK,选择友盟,虽然是完全按照友盟集成文档来做的,但是总是有些不顺畅,不能一下就搞定,这里记录一下过程,与遇到的坑

1.项目准备工作

a.到http://dev.umeng.com/social/android/sdk-download下载安卓版sdk,这里我选择了微信,QQ,与微博的精简版

下载sdk

b.新建一个项目,这里我选择应用的包名以及签名都与友盟分享的demo一样,目的就是为了使用他的签名和它在各大平台注册的APPid与appkey,

当然自己的项目要有分享功能的时候一定要自己到各大开放平台去注册申请,qq分享集成很简单,可以直接拿别人的appid来做,但是微信跟微博就不行,必须要用自己的项目appid以及签名,所以这就是为何我建项目跟友盟demo一样的原因,建好项目后,将下载下来的lib与res文件都加入到项目对应的文件夹下,并且确保引用了这些jar文件,项目结构如下


c.将debug.keystore文件加入到build.gradle同级,目的是为了使用友盟的签名,完成后

build.gradle应该是这样

apply plugin: 'com.android.application'

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.1"

    defaultConfig {
        applicationId "com.umeng.soexample"
        minSdkVersion 19
        targetSdkVersion 22
    }
    buildTypes {
        release {
            // 是否进行混淆
            minifyEnabled true
            // 混淆文件的位置
           // signingConfig signingConfigs.debug
            proguardFiles 'proguard-rules.pro'
        }

        debug {
            minifyEnabled true
            //signingConfig signingConfigs.debug
            proguardFiles 'proguard-rules.pro'
        }
    }
    /*加上这段代码是为了直接运行可以用正式的签名,所以在自己的项目中如果是直接用正式签名可以删除
    不用真是签名微博与微信是无法正常运行的,这个demo是友盟分享demo的简化版,只有微信,qq,微博
    三个平台,在项目中一定要确定自己的项目包名签名与各个平台申请的app是相对应的*/
    signingConfigs {
        debug {
            storeFile file('debug.keystore')
            storePassword "android"
            keyAlias "androiddebugkey"
            keyPassword "android"
        }
    }
    lintOptions {
        abortOnError false
    }
    packagingOptions {
        exclude 'META-INF/LICENSE.txt'
    }
}

dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:23.2.1'
    testCompile 'junit:junit:4.12'
    compile files('libs/SocialSDK_QQZone_3.jar')
    compile files('libs/SocialSDK_umengqq.jar')
    compile files('libs/SocialSDK_umengsina.jar')
    compile files('libs/SocialSDK_umengwx.jar')
    compile files('libs/SocialSDK_WeiXin_2.jar')
    compile files('libs/umeng_social_api.jar')
    compile files('libs/umeng_social_net.jar')
    compile files('libs/umeng_social_view.jar')
}
d.在项目包名中添加wxapi文件夹,将WXEntryActivity,添加进去,注意WXEntryActivity路径一定要是mainfest中的包名,如果项目java文件夹与mainfest的包名不一致,请建立一个mainfest的包名.wxapi为准的文件夹,并且将WXEntryActivity放进去
<span style="font-size:10px;">package com.umeng.soexample.wxapi;


import com.umeng.socialize.weixin.view.WXCallbackActivity;


public class WXEntryActivity extends WXCallbackActivity {

}</span>



e.在mainfest中添加activity

<span style="font-size:12px;"><?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.umeng.soexample" >
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.READ_LOGS" />
    <uses-permission android:name="android.permission.CALL_PHONE" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />


    <!-- renren sso Permission for Unit Test -->
    <!-- QQ、QQ空间所需权限 -->
    <uses-permission android:name="android.permission.GET_TASKS" />
    <uses-permission android:name="android.permission.SET_DEBUG_APP" />
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
    <uses-permission android:name="android.permission.GET_ACCOUNTS" />
    <uses-permission android:name="android.permission.USE_CREDENTIALS" />
    <uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />



    <application
        android:name="com.umeng.soexample.App"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme" >
        <activity android:name=".MainActivity" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <!--微信-->
        <activity
            android:name=".wxapi.WXEntryActivity"
            android:configChanges="keyboardHidden|orientation|screenSize"
            android:exported="true"
            android:screenOrientation="portrait"
            android:theme="@android:style/Theme.Translucent.NoTitleBar" />
        <!--qq-->
        <activity
            android:name="com.tencent.tauth.AuthActivity"
            android:launchMode="singleTask"
            android:noHistory="true" >
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />

                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
<!--这里的scheme是qq分享要用的,根据自己申请的appid=100424468,可以写成如下格式,
appid与自己应用不一致的话,会导致分享成功成功,但是返回后提示分享取消了,不会走分享成功的回调方法-->
                <data android:scheme="tencent100424468" />
            </intent-filter>
        </activity>
        <activity
            android:name="com.tencent.connect.common.AssistActivity"
            android:screenOrientation="portrait"
            android:theme="@android:style/Theme.Translucent.NoTitleBar"
            android:configChanges="orientation|keyboardHidden|screenSize"/>
        <!--分享编辑页-->
        <activity
            android:name="com.umeng.socialize.editorpage.ShareActivity"
            android:theme="@style/Theme.UMDefault"
            android:excludeFromRecents="true"
            />

        <meta-data
            android:name="UMENG_APPKEY"
            android:value="561cae6ae0f55abd990035bf" >
        </meta-data>
    </application>

</manifest></span>

2.项目代码

a.mainactivity.java

package com.umeng.soexample;

import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Toast;

import com.umeng.socialize.ShareAction;
import com.umeng.socialize.UMShareAPI;
import com.umeng.socialize.UMShareListener;
import com.umeng.socialize.bean.SHARE_MEDIA;
import com.umeng.socialize.media.UMImage;
import com.umeng.socialize.shareboard.SnsPlatform;
import com.umeng.socialize.utils.Log;

import java.util.ArrayList;

public class MainActivity extends AppCompatActivity {
    public ArrayList<SnsPlatform> platforms = new ArrayList<SnsPlatform>();

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

    private UMShareListener umShareListener = new UMShareListener() {
        @Override
        public void onResult(SHARE_MEDIA platform) {
            Log.d("plat","platform"+platform);
            if(platform.name().equals("WEIXIN_FAVORITE")){
                Toast.makeText(MainActivity.this,platform + " 收藏成功啦",Toast.LENGTH_SHORT).show();
            }else{
                Toast.makeText(MainActivity.this, platform + " 分享成功啦", Toast.LENGTH_SHORT).show();
            }
        }

        @Override
        public void onError(SHARE_MEDIA platform, Throwable t) {
            Toast.makeText(MainActivity.this,platform + " 分享失败啦", Toast.LENGTH_SHORT).show();
            if(t!=null){
                Log.d("throw","throw:"+t.getMessage());
            }
        }

        @Override
        public void onCancel(SHARE_MEDIA platform) {
            Toast.makeText(MainActivity.this,platform + " 分享取消了", Toast.LENGTH_SHORT).show();
        }
    };

    public void click(View view) {
        new ShareAction(MainActivity.this).setDisplayList(SHARE_MEDIA.SINA,SHARE_MEDIA.QQ,SHARE_MEDIA.WEIXIN,SHARE_MEDIA.WEIXIN_CIRCLE)
                .withTitle(Defaultcontent.title)
                .withText(Defaultcontent.text+"——来自友盟分享面板")
                .withMedia(new UMImage(MainActivity.this, Defaultcontent.imageurl))
                .withTargetUrl("https://wsq.umeng.com/")
                .setCallback(umShareListener)
                .open();
    }


    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        /** attention to this below ,must add this**/
        UMShareAPI.get(this).onActivityResult(requestCode, resultCode, data);
        Log.d("result","onActivityResult");
    }
}
activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin"
    >

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:onClick="click"
        android:text="自定义面面板分享"/>
</RelativeLayout>
b,运行效果

3.注意事项

a.如果项目中出现微信闪退,原因可能是,自己的appid与签名不正确问题(微信app默认是开启分享功能的,而支付需要再次申请)

b.微博出现文件不存在c8998错误,可以去微博开放平台检查是否申请该项目的分享功能,并且确认

Config.REDIRECT_URL="http://sns.whalecloud.com/sina2/callback";//需要替换成自己的回调地址
这个url与自己的app的回调地址相同,获取这个地址的方式可以自己百度下

c.qq提示应用不存在的错误,可以去腾讯开放平台查看下,是否申请了分享功能

Demo地址:
点击打开链接 




作者:u012402940 发表于2016/10/20 12:41:43 原文链接
阅读:41 评论:0 查看评论

缩减代码和资源

$
0
0

为了使APK文件尽可能小,在发布版本中应该启用压缩来删除未使用的代码和资源。 本页描述如何指定在构建过程中要保留或丢弃的代码和资源。

代码缩减可使用ProGuard,它从您的打包应用程序中检测和删除未使用的类,字段,方法和属性,包括来自包含的代码库(使其成为处理64k引用限制的有价值的工具)。 ProGuard还优化字节码,删除未使用的代码指令,并使用短名称混淆剩余的类,字段和方法。 代码混淆使您的APK难以反向工程,这在您的应用使用安全敏感功能(例如许可验证)时尤其有用。

资源缩减可使用Gradle的Android插件,它从您的打包应用程序中删除未使用的资源,包括代码库中未使用的资源。 它与代码缩减结合使用,以便一旦未使用的代码被删除,任何不再被引用的资源也可以被安全地删除。

本文档中的功能依赖于:

  • SDK Tools 25.0.10 or higher
  • Android Plugin for Gradle 2.0.0 or higher

代码缩减


要使用ProGuard启用代码缩减,请将minifyEnabled true添加到build.gradle文件中的相应构建类型。

请注意,代码缩减会减慢构建时间,因此,如果可能,应避免在调试版本上使用它。 不过,重要的是,您必须在用于测试的最终APK上启用代码缩减,因为如果您不能充分地自定义要保留的代码。,它可能会导致错误。

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

注意:Android Studio在使用即时运行时会禁用ProGuard。

除了minifyEnabled属性,proguardFiles属性定义了ProGuard规则:

  • getDefaultProguardFile('proguard-android.txt')方法从tools/proguard/文件夹获取默认的ProGuard设置。
    提示:对于更多的代码缩减,请尝试位于同一位置的proguard-android-optimize.txt文件。 它包括相同的ProGuard规则,但与其他优化,在字节码内部级别和跨方法执行分析上帮助您的APK大小进一步降低,而且它运行更快。
  • proguard-rules.pro文件是您可以添加自定义ProGuard规则的位置。 默认情况下,此文件位于模块的根目录(在build.gradle文件旁边)。

要添加特定于每个构建变量的更多ProGuard规则,请在相应的productFlavor块中添加另一个proguardFiles属性。 例如,Gradle文件内将flavor2-rules.pro添加到flavor2 产品中。 现在flavor2使用所有三个ProGuard规则,因为来自发布块的那些规则也被应用。

android {
    ...
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'),
                   'proguard-rules.pro'
        }
    }
    productFlavors {
        flavor1 {
        }
        flavor2 {
            proguardFile 'flavor2-rules.pro'
        }
    }
}

对于每个构建,ProGuard输出以下文件:

dump.txt

描述APK中所有类文件的内部结构。

mapping.txt

提供原始和混淆的类,方法和字段名称之间的转换。

seeds.txt

列出未被混淆的类和成员。

usage.txt

列出从APK中移除的代码。

这些文件保存在<module-name>/build/outputs/mapping/release/

自定义要保留的代码

对于某些情况,默认的ProGuard配置文件(proguard-android.txt)就足够了,ProGuard删除所有(而且只有)未使用的代码。 然而,许多情况下,ProGuard很难正确分析,它可能会删除您的应用程序实际需要的代码。 下列情况可能会错误地删除代码:

  • 当您的应用程序仅引用来自AndroidManifest.xml文件的类
  • 当应用程序从Java本机接口(JNI)调用方法时,
  • 当您的应用在运行时操作代码(如使用反射或内省)

测试的应用程序应该暴露由不适当删除的代码导致的任何错误,但您也可以通过查看保存在<module-name>/build/outputs/mapping/release/中的usage.txt输出文件来检查删除的代码。

要修复错误并强制ProGuard保留某些代码,请在ProGuard配置文件中添加一个-keep行。 例如:

-keep public class MyClass

另外,您可以使用@Keep注释添加到要保留的代码。 在类上添加@Keep会保持整个类不变。 将它添加到方法或字段将保持方法/字段(和它的名称)以及类名称不变。 请注意,此注释仅在使用注释支持库时可用。

使用-keep选项时,您应该考虑许多因素; 有关自定义配置文件的更多信息,请阅读ProGuard手册疑难解答部分概述了在您的代码被删除时可能遇到的其他常见问题。

混淆解码的堆栈跟踪

ProGuard收缩代码后,读取堆栈跟踪很困难(即使并非不可能),因为方法名称被模糊处理。 幸运的是,ProGuard每次运行时都会创建一个mapping.txt文件,它显示映射到模糊名称的原始类,方法和字段名称。 ProGuard将文件保存在应用程序<module-name>/build/outputs/mapping/release/目录中。

请注意,每次使用ProGuard创建发布版本时,mapping.txt文件都会被覆盖,因此每次发布新版本时都必须小心保存副本。 通过为每个版本构建保留mapping.txt文件的副本,如果用户从旧版本的应用程序提交混淆的堆栈跟踪,您将能够调试问题。

在Google Play上发布应用程式时,您可以为每个版本的APK上传mapping.txt档案。 然后,Google Play会从用户报告的问题中解析进入的堆栈跟踪,以便您可以在Google Play开发者控制台中查看这些跟踪。 有关详细信息,请参阅帮助中心文章,了解如何解析混淆崩溃堆栈跟踪

要将混淆的堆栈跟踪转换为可读的堆栈跟踪,请使用回溯脚本(Windows上为retrace.bat; Mac上为retrace.sh)。 它位于<sdk-root>/tools/proguard/目录中。 该脚本采用mapping.txt文件和您的堆栈跟踪,产生一个新的,可读的堆栈跟踪。 使用回溯工具的语法是:

retrace.bat|retrace.sh [-verbose] mapping.txt [<stacktrace_file>]

例如:

retrace.bat -verbose mapping.txt obfuscated_trace.txt

如果不指定堆栈跟踪文件,则回溯工具从标准输入读取。

资源缩减


资源缩减只能与代码缩减相结合。 代码缩减删除所有未使用的代码后,资源缩减器可以识别应用程序仍在使用哪些资源。 当您添加包含资源的代码库时,尤其如此,您必须删除未使用的库代码,以便库资源变为未引用,从而可由资源缩减器移除。

要启用资源缩减,请在build.gradle文件(与代码缩减minifyEnabled 属性并列)中将shrinkResources属性设置为true。 例如:

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

如果你还没有使用minifyEnabled为代码收缩构建你的应用程序,那么在启用shrinkResources之前尝试一下,因为你可能需要编辑你的proguard-rules.pro文件,以保持在开始删除之前动态创建或调用的类或方法资源。

注意:资源缩减器当前不会删除在values/文件夹中定义的资源(例如字符串,维度,样式和颜色)。 这是因为Android资源打包工具(AAPT)不允许Gradle插件为资源指定预定义版本。 有关详细信息,请参阅问题70869

自定义要保留的资源

如果有要保留或丢弃的特定资源,请使用<resources>标记在项目中创建一个XML文件,并在指定每个资源保留或舍弃,用tools:keep属性指定要保留的资源,用tools:discard属性指定要舍弃的资源。 两个属性都接受以逗号分隔的资源名称列表。 您可以使用星号字符作为通配符。
例如:

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
    tools:keep="@layout/l_used*_c,@layout/l_used_a,@layout/l_used_b*"
    tools:discard="@layout/unused2" />

将此文件保存在项目资源中,例如,在res/raw/keep.xml。 该文件不会打包到您的APK中。

指定要丢弃的资源可能看起来很愚蠢,当然可以改为删除它们,但这在使用构建变体时可能很有用。 例如,您可以将所有资源放入公共项目目录,然后为每个构建变量创建一个不同的keep.xml文件,您知道给定的资源似乎在代码中有使用(因此不会被缩减器删除),但是 你知道它实际上不会用于给定的构建变体。

启用严格引用检查

通常,资源缩减器可以精确地确定是否使用资源。 但是,如果你的代码调用了Resources.getIdentifier()(或者你的库中的任何一个 - AppCompat库),这意味着你的代码是基于动态生成的字符串查找资源名称。 执行此操作时,资源缩小器默认情况下会防御性地运行,并将匹配名称格式的所有资源标记为可能已使用并且不可用于删除。

例如,以下代码会将所有带有img_前缀的资源标记为已使用。

String name = String.format("img_%1d", angle + 1);
res = getResources().getIdentifier(name, "drawable", getPackageName());

资源缩减器还查看代码中的所有字符串常量以及各种res/raw/资源,以类似于file:///android_res/drawable/ic_plus_anim_016.png的格式查找资源URL。 如果它发现这样的字符串或其他,看起来像他们可以用于构造这样的URL,它不会删除它们。

这些是默认情况下启用的安全缩小模式的示例。 但是,您可以关闭此“更安全比对不起”处理,并指定资源缩小器仅保留其确定使用的资源。 为此,请在keep.xml文件中将shrinkMode设置为strict,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
    tools:shrinkMode="strict" />

如果您确实启用了严格缩减模式,并且您的代码还引用了具有动态生成的字符串的资源,如上所示,那么您必须使用tools:keep属性手动保留这些资源。

删除未使用的备用资源

Gradle资源缩减器只会移除应用程序代码未引用的资源,这意味着它不会移除不同装置设定的替代资源。 如果需要,您可以使用Android Gradle插件的resConfigs属性来删除应用程序不需要的替代资源文件。

例如,如果您使用的库包含语言资源(例如AppCompat或Google Play服务),尽管应用程序是不需要翻译这些语言资源,APK也会包含这些库中所有翻译的语言字符串。 如果您只想保留应用程序支持的语言,可以使用resConfig属性指定这些语言。 将删除未指定语言的任何资源。

以下代码段显示了如何将您的语言资源限制为仅限英语和法语:

android {
    defaultConfig {
        ...
        resConfigs "en", "fr"
    }
}

同样,您可以在APK中自定义要包含哪些屏幕密度或ABI资源,并使用APK拆分为不同设备构建不同的APK。

合并重复资源

默认情况下,Gradle还合并相同名称的资源,例如可能在不同资源文件夹中具有相同名称的图片。 此行为不受shrinkResources属性控制,不能禁用,因为必须避免在多个资源与代码查找的名称匹配时出现错误。

仅当两个或多个文件共享相同的资源名称,类型和限定符时,才会进行资源合并。 Gradle会选择那个被认为是重复项中的最佳选择(基于下面描述的优先级顺序),并且仅将那个资源传递给AAPT以在APK文件中分发。

Gradle会在以下位置查找重复的资源:

  • 主资源,与主源集相关,一般位于src/main/res/
  • 变体覆盖,来源于构建类型和构建风格。
  • 依赖的库项目

Gradle在以下级联优先级顺序中合并重复资源:依赖→主→构建风格→构建类型。

例如,如果重复资源同时出现在主资源和构建风格中,Gradle将选择构建风格中的一个。

如果相同的资源出现在同一源集中,则Gradle不能合并它们并发出资源合并错误。 如果在build.gradle文件的sourceSet属性中定义多个源集,例如,如果src/main/res/src/main/res2/包含相同的资源,则可能会发生这种情况。

排查资源缩减问题

当缩减资源时,Gradle 控制台会显示从应用程序包中删除的资源的摘要。 例如:

:android:shrinkDebugResources
Removed unused resources: Binary resource data reduced from 2570KB to 1711KB: Removed 33%
:android:validateDebugSigning

Gradle还在<module-name>/build/outputs/mapping/release/(与ProGuard的输出文件相同的文件夹)中创建一个名为resources.txt的诊断文件。 此文件包括例如哪些资源引用其他资源以及使用或删除哪些资源的详细信息。

例如,要找出为什么@drawable/ic_plus_anim_016仍在您的APK中,请打开resources.txt文件并搜索该文件名。 您可能会发现它是从另一个资源引用的,如下所示:

16:25:48.005 [QUIET] [system.out] @drawable/add_schedule_fab_icon_anim : reachable=true
16:25:48.009 [QUIET] [system.out]     @drawable/ic_plus_anim_016

你现在需要知道为什么@drawable/add_schedule_fab_icon_anim 是可访问的,如果你向上搜索,你会发现该资源列在“根可访问的资源是:”。 这意味着有一个代码引用add_schedule_fab_icon_anim(也就是说,它的R.drawable ID在可访问代码中找到)。

如果不使用严格检查,如果有字符串常量看起来像可能被用来为动态加载的资源构造资源名称,则资源ID可以标记为可达。 在这种情况下,如果您在构建输出中搜索资源名称,您可能会发现这样的消息:

10:32:50.590 [QUIET] [system.out] Marking drawable:ic_plus_anim_016:2130837506
    used because it format-string matches string pool constant ic_plus_anim_%1$d.

如果您看到其中一个字符串,并且您确定该字符串未用于动态加载给定资源,则可以使用以下tools:discard 属性通知构建系统将其删除,相关资料查阅定制要保留的资源

作者:Sayangnala 发表于2016/10/20 13:16:24 原文链接
阅读:37 评论:0 查看评论
Viewing all 5930 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>