精华内容
参与话题
问答
  • 7个代表性的Android应用程序完整源代码

    万次下载 热门讨论 2011-06-14 15:04:30
    7个比较具有代表性的Android应用程序源代码!
  • Android应用程序UI架构 高清PTT

    千次下载 热门讨论 2013-10-23 01:23:45
    Android应用程序中,每一个Activity组件都关联有一个或者若干个窗口,每一个窗口都对应有一个Surface。有了这个Surface之后,应用程序就可以在上面渲染窗口的UI。最终这些已经绘制好了的Surface都会被统一提交给...
  • Android应用开发实战.pdf

    千次下载 热门讨论 2012-12-18 15:25:17
    Android应用开发实战》是一本实践与理论紧密结合的Android应用开发参考书。实践部分以一个完整的大型案例(功能完善的微博客户端)贯穿始终,以迭代的方式详细演示和讲解了该案例的开发全过程,旨在帮助读者迅速...
  • Android应用程序消息处理机制

    千次下载 热门讨论 2013-10-23 01:22:30
    Android应用程序与传统的PC应用程序一样,都是消息驱动的。也就是说,在Android应用程序主线程中,所有函数都是在一个消息循环中执行的。Android应用程序其它线程,也可以像主线程一样,拥有消息循环。Android应用...
  • Android应用程序进程管理

    千次下载 热门讨论 2013-10-23 01:21:26
    这个PPT讲Android应用程序进程的启动和回收,主要涉及到Zygote进程、System Server进程,以及组件管理服务ActivityManagerService、窗口服务WindowManagerService,还有专用驱动Low Memory Killer。通过了解Android...
  • 反编译Android应用

    万人学习 2015-01-26 12:18:38
    学习技术的渠道多种多样,而通过反编译一些经典应用来学习是一种比较好的途径,在Android领域,有比较好的反编译工具,本课程将会教大家如何反编译Android应用
  • Android应用案例开发大全 吴亚峰等 PDF扫描版

    千次下载 热门讨论 2014-09-26 12:28:12
    Android应用案例开发大全》是以Android手机综合应用程序开发为主题 通过11个典型范例全面且深度地讲解了单机应用 网络应用 商业案例 2D和3D游戏等多个开发领域 全书共分12章 主要以范例集的方式来讲述Android的...
  • Android应用图标微技巧,8.0系统中应用图标的适配

    万次阅读 多人点赞 2018-03-13 07:56:38
    大家好,2018年的第一篇文章到的稍微有点迟,也是因为在上一个Glide系列...三星今年推出的最新旗舰机Galaxy S9已经搭载了Android 8.0系统,紧接着小米、华为、OV等国产手机厂商即将推出的新年旗舰机也会搭载Android 8.0

    转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/79417483

    本文同步发表于我的微信公众号,扫一扫文章底部的二维码或在微信搜索 郭霖 即可关注,每个工作日都有文章更新。

    大家好,2018年的第一篇文章到的稍微有点迟,也是因为在上一个Glide系列结束之后一直还没想到什么好的新题材。

    现在已经进入了2018年,Android 8.0系统也逐渐开始普及起来了。三星今年推出的最新旗舰机Galaxy S9已经搭载了Android 8.0系统,紧接着小米、华为、OV等国产手机厂商即将推出的新年旗舰机也会搭载Android 8.0系统。因此,现在已经是时候需要让我们的应用程序对Android 8.0系统进行适配了。

    其实在去年Android 8.0系统刚推出的时候,我就仔细翻阅过Google官方的功能变更文档。变更项着实不少,但是真正需要我们去进行功能适配的地方却并不多。总结了一下,最主要需要进行适配的地方有两处:应用图标和通知栏。那么我们就分为上下两篇来分别对这两处功能适配进行讲解,先从应用图标开始。

    为什么要进行应用图标适配?

    可能有些朋友觉得困惑,应用图标这种东西从Android远古时代就已经有了,而且功能格外的简单,就是放张图片而已,这有什么好适配的呢?但实际上,在当前Android环境下,应用图标功能是极其混乱的。

    如果说要讲一讲手机应用图标的历史,其实要从苹果开始讲起。在上世纪80年代,苹果还在设计Lisa和Macintosh电脑的时候,乔布斯就是个圆角矩形的狂热支持者。当时苹果的工程师写出了一套绝妙的算法,可以在电脑上绘制出圆和椭圆,所有观看者都被震惊了,除了乔布斯,因为乔布斯觉得圆和椭圆虽然也不错,但是如果能绘制出带圆角的矩形就更好了。当时那位工程师觉得这是不可能实现的,而且也完全用不着圆角矩形,能满足基本的绘图需求就可以了。乔布斯愤怒地拉着他走了3条街,指出大街上各种应用圆角矩形的例子,最后那位工程师第二天就做出了绘制圆角矩形的功能。

    因此,在2007年一代iPhone诞生的时候,所有应用程序的图标都毫不出乎意料地使用了圆角矩形图标,即使是第三方应用也被强制要求使用圆角矩形图标,并且这一规则一直延续到了今天的iOS 11当中,如下图所示:

    相反,Android系统在设计的时候就不喜欢苹果这样的封闭与强制,而是选择了自由与开放,对应用图标的形状不做任何强制要求,开发者们可以自由进行选择:

    可以看到,在Android上,应用图标可以是方形、圆形、圆角矩形、或者是其他任意不规则图形。

    本来就是两家公司不同的设计理念,也说不上孰高孰低。但由于Android操作系统是开源的,国内一些手机厂商在定制操作系统的时候就把这一特性给改了。比如小米手机,就选择了向苹果靠拢,强制要求应用图标圆角化。如果某些应用的图标不是圆角矩形的呢?小米系统会自动给它加上一个圆角的效果,如下图所示:

    小米的这种做法看上去是向苹果学习,但实际上是相当恶心的。因为谁都可以看出来,这种自动添加的圆角矩形非常丑,因此很多公司就索性直接将应用的图标都设计成圆角矩形的,正好Android和iOS都用同一套图标还省事了。

    但是这就让Google不开心了,这不是变向强制要求开发者必须将图标设计成圆角矩形吗?于是在去年的Google I/O大会上,Google点名批评了小米的这种做法,说其违反了Android自由和开放的理念。

    除了变向强制要求应用图标圆角化,小米的这种处理方式还有一个弊端,就是如果应用图标的圆角弧度和小米系统要求的不同,那么会出现异常丑陋的效果:

    看到这样的应用图标,真的是一脸尴尬症都要犯了。就因为这两款应用图标的圆角弧度设计得大于了小米系统要求的圆角弧度,就被自动添加上了这样丑陋的白边。

    问题是已经存在了,那么应该怎么解决呢?说实话,这确实是一个长期以来都让人头疼的问题,Google多年来对此也是睁一只眼闭一只眼。终于在Android 8.0系统中,Google下定决心要好好整治一下Android应用图标的规范性了,今天我们就来学习一下。

    8.0系统的应用图标适配

    这个问题对于Google来说还是挺难解决的。因为Google一直在强调自由与开放,那么小米强制要求所有应用图标都必须圆角化也是人家的自由呀,你不准人家这么干是不是本身就违背了自由和开放的理念呢?当然我们在这里讨论这个,有点像讨论先有鸡还是先有蛋的感觉,不过Google还是想出了一套完美的解决方案。

    从Android 8.0系统开始,应用程序的图标被分为了两层:前景层和背景层。也就是说,我们在设计应用图标的时候,需要将前景和背景部分分离,前景用来展示应用图标的Logo,背景用来衬托应用图标的Logo。需要注意的是,背景层在设计的时候只允许定义颜色和纹理,但是不能定义形状。

    那么应用图标的形状由谁来定义呢?Google将这个权利就交给手机厂商了。不是有些手机厂商喜欢学习苹果的圆角图标吗?没问题,由于应用图标的设计分为了两层,手机厂商只需要在这两层之上再盖上一层mask,这个mask可以是圆角矩形、圆形或者是方形等等,视具体手机厂商而定,就可以瞬间让手机上的所有应用图标都变成相同的规范。原理示意图如下:

    可以看到,这里背景层是一张蓝色的网格图,前景层是一张Android机器人Logo图,然后盖上一层圆形的mask,最终就裁剪出了一张圆形的应用图标。

    我一定要适配吗?

    有些朋友可能会觉得这种分成两层的应用图标设计太过于麻烦,不适配可以吗?也有些朋友可能会说,自己的APP并没有做过应用图标适配,在Android 8.0手机上也照样跑得好好的。

    事实上,这个新功能Google是准备让它慢慢过渡的,而不是一次性就强推给所有的开发者。如果你的APP中的targetSdkVersion是低于26的,那么就可以不用进行应用图标适配,Android 8.0系统仍然是向下兼容的。但是如果你将targetSdkVersion指定到了26或者更高,那么Android系统就会认为你的APP已经做好了8.0系统的适配工作,当然包括了应用图标的适配。

    如果你将targetSdkVersion指定到了26,但是却没有进行Android 8.0系统的应用图标适配,那么会出现什么样的效果呢?这里我举几个反面示例:

    这是Google Pixel手机上的截图,操作系统是Android 8.0。可以看到,这两个应用的图标都非常奇怪,本来设计的都是一个圆角矩形的图标,但是却又在外面套上了一个白色的圆圈。为什么会出现这种情况呢?就是因为这两个应用都将targetSdkVersion指定到了26以上,但是却又没有做8.0系统的应用图标适配,而Pixel手机设定的mask是圆形的,所以就自动在应用图标的外层套了一个白色的圆圈。

    由此可以看出,爱奇艺和饿了么这两款应用都是没有在Pixel上进行兼容性测试的。不过考虑到它们都是只在国内市场提供服务,因此也情有可原。

    当然了,国内的Android 8.0手机很快也要开始普及了,我相信没有任何人会希望自己的APP也出现上述的效果,因此下面我们就来开始具体学习,如何进行8.0系统的应用图标适配。

    新建一个项目

    如果有人问我8.0系统应用图标适配到底难不难?这里我会回答,一点都不难。相信所有看完这篇文章的人立马就能学会,但前提是你需要有一个好的工具——Android Studio 3.0或更高版本。

    很高兴告诉大家,Android Studio 3.0中已经内置了8.0系统应用图标适配的功能,如果你已经安装了Android Studio 3.0的话,那么恭喜你,你已经成功了百分之九十了。如果你还在用老版的Android Studio,那么赶快去升级一下,然后再接着看这篇文章。

    好的,那么现在我们就用Android Studio 3.0来新建一个项目,就叫它IconTest吧。

    创建好项目之后,打开app/build.gradle文件检查一下,确保targetSdkVersion已经指定到了26或者更高,如下所示:

    apply plugin: 'com.android.application'
    
    android {
        compileSdkVersion 26
        defaultConfig {
            applicationId "com.example.icontest"
            minSdkVersion 15
            targetSdkVersion 26
            versionCode 1
            versionName "1.0"
            testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        }
    }

    可以看到,这里我在创建新项目的时候默认targetSdkVersion就是26,如果你是低于26的话,说明你的Android SDK有些老了,最好还是更新一下。当然如果你懒得更新也没关系,手动把它改成26就可以了。

    接下来打开AndroidManifest.xml文件,代码如下所示:

    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.example.icontest">
    
        <application
            android:allowBackup="true"
            android:icon="@mipmap/ic_launcher"
            android:label="@string/app_name"
            android:roundIcon="@mipmap/ic_launcher_round"
            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>
        </application>
    
    </manifest>

    这里我们需要关注的点是android:icon这个属性,通过这个属性,我们将应用的图标指定为了mipmap目录下的ic_launcher文件。另外大家可能注意到还有一个android:roundIcon属性,这是一个只适用在Android 7.1系统上的过渡版本,很快就被8.0系统的应用图标适配所替代了,我们不用去管它。

    刚才说了,应用图标被指定为了mipmap目录下的ic_launcher文件,那么我们快去看下这个文件吧:

    这里虽然目录很多,但是相信任何只要是入了门的Android开发者都能看得懂。唯一需要我们留意的就是mipmap-anydpi-v26这个目录,这个目录表示什么意思呢?就是Android 8.0或以上系统的手机,都会使用这个目录下的ic_launcher来作为图标。

    你会发现,mipmap-anydpi-v26目录下的ic_launcher并不是一张图片,而是一个XML文件,我们打开这个文件看一下,代码如下所示:

    <?xml version="1.0" encoding="utf-8"?>
    <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
        <background android:drawable="@drawable/ic_launcher_background" />
        <foreground android:drawable="@drawable/ic_launcher_foreground" />
    </adaptive-icon>

    这是一个8.0系统应用图标适配的标准写法,在<adaptive-icon>标签中定义一个<background>标签用于指定图标的背景层,定义一个<foreground>标签用于指定图标的前景层。

    那么我们分别来看一下背景层和前景层分别都是些什么内容吧,首先打开ic_launcher_background文件,内容如下图所示:

    这是一个使用SVG格式绘制出来的带纹理的底图。当然如果你看不懂这里面的代码也没有关系,因为我也看不懂。SVG格式的图片都是使用AI、PS等图像编辑软件制作之后导出的,基本没有人可以手工编写SVG图片。

    当然,背景层并不是一定要用SVG格式的图片,你也可以使用普通的PNG、JPG等格式的图片,甚至是直接指定一个背景色都可以。

    看完了背景层接着我们来看前景层,打开ic_launcher_foreground文件,内容如下所示:

    类似地,这里也是使用SVG格式绘制出了一个Android机器人的Logo,并且这个机器人还是带投影效果的。当然了,前景层我们也是可以使用PNG、JPG等格式的图片的,待会儿会进行演示。

    好的,现在已经把应用图标相关部分的代码都解释完了,那么这样一个刚刚创建完成的空项目运行起来到底会是什么样的效果呢?我们跑一下看看就知道了,如下图所示:

    可以看到,这就是一个前景层盖在背景层上,然后再被圆形mask进行裁剪之后的效果。

    好的,那么现在剩下的问题就是,我们如何才能对自己的应用图标在Android 8.0系统上进行适配?

    开始适配

    看到爱奇艺的8.0系统应用图标适配工作做得这么差,我就准备拿爱奇艺来做为例子了,我们一起来帮爱奇艺的Android版做个漂亮的应用图标适配吧。

    那么很显然,根据8.0系统的应用图标设计,我们需要准备一个前景层和一个背景层才行。

    前景层也就是爱奇艺的Logo了,这里我通过Photoshop把爱奇艺的Logo图取了出来。

    由于这是一张背景透明的图片,如果直接贴到文章里面就一片白色,啥也看不见了,于是我只好在文章里贴了一张带灰色背景的图片。如果大家需要获取爱奇艺这张前景图的原图,可以点击 这里 获取。

    解决了前景层,接下来我们来看背景层。其实背景层比前景层就简单多了,一般如果没有什么特殊需求的话,背景层直接使用某种纯色就可以了。

    这里我用Photoshop吸取了一下爱奇艺原始应用图标的背景色,值是#04ca00。当然,爱奇艺的背景色并不是完全的纯色,而是有细微的颜色渐变的。不过这里我们只是举例讲解而已,就不追究这些细节了。

    那么现在前景层和背景层都准备好了,接下来我们正式开始进行8.0系统的应用图标适配。重新回到IconTest项目当中,然后按下Windows:Ctrl+Shift+A / Mac:command+shft+A 快捷键,并输入Image Asset,如下所示:

    点击回车键打开Asset Studio编辑器,在这里就可以进行应用图标适配了。

    这个Asset Studio编辑器非常简单好用,一学就会。左边是操作区域,右边是预览区域。

    先来看操作区域,第一行的Icon Type保持默认就可以了,表示同时创建兼容8.0系统以及老版本系统的应用图标。第二行的Name用于指定应用图标的名称,这里也保持默认即可。接下来的三个页签,Foreground Layer用于编辑前景层,Background Layer用于编辑背景层,Legacy用于编辑老版本系统的图标。

    再来看预览区域,这个就十分简单了,用于预览应用图标的最终效果。在预览区域中给出了可能生成的图标形状,包括圆形、圆角矩形、方形等等。注意每个预览图标中都有一个圆圈,这个圆圈叫作安全区域,必须要保证图标的前景层完全处于安全区域当中才行,否则可能会出现图标被手机厂商的mask裁剪掉的情况。

    为了让大家能够更加直观地看到操作,这里我使用一张GIF图来演示操作的过程:

    最终,Android Studio会自动帮我们生成适配8.0系统的应用图标,以及适配老版本系统的应用图标,我们甚至一行代码都不用写,一切工作就已经完成了。感兴趣的朋友可以自己到mipmap目录下面去观察一下Android Studio帮我们生成了哪些东西,这里就不带着大家一一去看了。

    最后,让我们来运行一下程序,并且和正版爱奇艺的应用图标放在一起对比一下吧:

    可以看到,做过8.0系统应用图标适配之后,效果明显要好看太多了,也希望爱奇艺的官方APP也能早日完成适配吧。

    好了,今天这篇文章就到这里,相信大家都已经轻松掌握了Android 8.0系统的应用图标适配,下篇文章会讲解Android 8.0系统的通知栏适配,感兴趣的朋友请继续阅读 Android通知栏微技巧,8.0系统中通知栏的适配

    关注我的技术公众号,每天都有优质技术文章推送。关注我的娱乐公众号,工作、学习累了的时候放松一下自己。

    微信扫一扫下方二维码即可关注:

    20160507110203928         20161011100137978

    展开全文
  • Android应用的调试

    万人学习 2015-02-09 16:33:39
    Android应用的调试
  • Android应用底部导航栏(选项卡)实例Demo

    千次下载 热门讨论 2012-04-18 14:17:55
    Android应用底部导航栏(选项卡)实例代码http://blog.csdn.net/cjjky/article/details/7209056
  • C#开发Android应用实战 使用Mono for Android和.NET C# PDF扫描版,希望对你有用处。 一般下载的都7.07MB的,那个只是一个样章,这个是书的全部。
  • Android应用开发性能优化完全分析

    万次阅读 多人点赞 2015-10-11 02:20:58
    1 背景其实有点不想写这篇文章的,但是又想写,有些矛盾。不想写的原因是随便上网一搜一堆关于性能的建议,感觉大家你一总结、我一总结的都说到了很多优化注意事项,但是看过这些文章后大多数存在一个问题就是只给出...

    1 背景

    其实有点不想写这篇文章的,但是又想写,有些矛盾。不想写的原因是随便上网一搜一堆关于性能的建议,感觉大家你一总结、我一总结的都说到了很多优化注意事项,但是看过这些文章后大多数存在一个问题就是只给出啥啥啥不能用,啥啥啥该咋用等,却很少有较为系统的进行真正性能案例分析的,大多数都是嘴上喊喊或者死记住规则而已(当然了,这话我自己听着都有些刺耳,实在不好意思,其实关于性能优化的优质博文网上也还是有很多的,譬如Google官方都已经推出了优化专题,我这里只是总结下自的感悟而已,若有得罪欢迎拍砖,我愿挨打,因为我之前工作的一半时间都是负责性能优化)。

    当然了,本文不会就此编辑这么一次,因为技术在发展,工具在强大(写着写着Android Studio 1.4版本都推送了),自己的经验也在增加,所以本文自然不会覆盖所有性能优化及分析;解决的办法就是该文章会长期维护更新,同时在评论区欢迎你关于性能优化点子的探讨。

    Android应用的性能问题其实可以划分为几个大的模块的,而且都具有相对不错的优化调试技巧,下面我们就会依据一个项目常规开发的大类型来进行一些分析讲解。

    PS:之前呆过一家初创医疗互联网公司,别提性能优化了,老板立完新项目后一个月就要求见到上线成品,这种压迫下谈何性能优化,纯属扯蛋,所以不到三个月时间我主动选择撤了,这种现象后来我一打听发现在很多初创公司都很严重,都想速成却忽略了体验。

    PPPS:本文只是达到抛砖引玉的作用,很多东西细究下去都是值得深入研究的,再加上性能优化本来就是一个需要综合考量的任务,不是说会了本文哪一点就能做性能分析了,需要面面俱到才可高效定位问题原因。

    这里写图片描述

    【工匠若水 http://blog.csdn.net/yanbober 转载请注明出处。点我开始Android技术交流

    2 应用UI性能问题分析

    UI可谓是一个应用的脸,所以每一款应用在开发阶段我们的交互、视觉、动画工程师都拼命的想让它变得自然大方美丽,可是现实总是不尽人意,动画和交互总会觉得开发做出来的应用用上去感觉不自然,没有达到他们心目中的自然流畅细节;这种情况之下就更别提发布给终端用户使用了,用户要是能够感觉出来,少则影响心情,多则卸载应用;所以一个应用的UI显示性能问题就不得不被开发人员重视。

    2-1 应用UI卡顿原理

    人类大脑与眼睛对一个画面的连贯性感知其实是有一个界限的,譬如我们看电影会觉得画面很自然连贯(帧率为24fps),用手机当然也需要感知屏幕操作的连贯性(尤其是动画过度),所以Android索性就把达到这种流畅的帧率规定为60fps。

    有了上面的背景,我们开发App的帧率性能目标就是保持在60fps,也就是说我们在进行App性能优化时心中要有如下准则:

    换算关系:60帧/秒-----------16ms/帧;
    
    准则:尽量保证每次在16ms内处理完所有的CPU与GPU计算、绘制、渲染等操作,否则会造成丢帧卡顿问题。

    从上面可以看出来,所谓的卡顿其实是可以量化的,每次是否能够成功渲染是非常重要的问题,16ms能否完整的做完一次操作直接决定了卡顿性能问题。

    当然了,针对Android系统的设计我们还需要知道另一个常识;虚拟机在执行GC垃圾回收操作时所有线程(包括UI线程)都需要暂停,当GC垃圾回收完成之后所有线程才能够继续执行(这个细节下面小节会有详细介绍)。也就是说当在16ms内进行渲染等操作时如果刚好遇上大量GC操作则会导致渲染时间明显不足,也就从而导致了丢帧卡顿问题。

    有了上面这两个简单的理论基础之后我们下面就会探讨一些UI卡顿的原因分析及解决方案。

    2-2 应用UI卡顿常见原因

    我们在使用App时会发现有些界面启动卡顿、动画不流畅、列表等滑动时也会卡顿,究其原因,很多都是丢帧导致的;通过上面卡顿原理的简单说明我们从应用开发的角度往回推理可以得出常见卡顿原因,如下:

    1. 人为在UI线程中做轻微耗时操作,导致UI线程卡顿;

    2. 布局Layout过于复杂,无法在16ms内完成渲染;

    3. 同一时间动画执行的次数过多,导致CPU或GPU负载过重;

    4. View过度绘制,导致某些像素在同一帧时间内被绘制多次,从而使CPU或GPU负载过重;

    5. View频繁的触发measure、layout,导致measure、layout累计耗时过多及整个View频繁的重新渲染;

    6. 内存频繁触发GC过多(同一帧中频繁创建内存),导致暂时阻塞渲染操作;

    7. 冗余资源及逻辑等导致加载和执行缓慢;

    8. 臭名昭著的ANR;

    可以看见,上面这些导致卡顿的原因都是我们平时开发中非常常见的。有些人可能会觉得自己的应用用着还蛮OK的,其实那是因为你没进行一些瞬时测试和压力测试,一旦在这种环境下运行你的App你就会发现很多性能问题。

    2-3 应用UI卡顿分析解决方法

    分析UI卡顿我们一般都借助工具,通过工具一般都可以直观的分析出问题原因,从而反推寻求优化方案,具体如下细说各种强大的工具。

    2-3-1 使用HierarchyViewer分析UI性能

    我们可以通过SDK提供的工具HierarchyViewer来进行UI布局复杂程度及冗余等分析,如下:

    xxx@ThinkPad:~$ hierarchyviewer   //通过命令启动HierarchyViewer

    选中一个Window界面item,然后点击右上方Hierarchy window或者Pixel Perfect window(这里不介绍,主要用来检查像素属性的)即可操作。

    先看下Hierarchy window,如下:

    这里写图片描述

    一个Activity的View树,通过这个树可以分析出View嵌套的冗余层级,左下角可以输入View的id直接自动跳转到中间显示;Save as PNG用来把左侧树保存为一张图片;Capture Layers用来保存psd的PhotoShop分层素材;右侧剧中显示选中View的当前属性状态;右下角显示当前View在Activity中的位置等;左下角三个进行切换;Load View Hierarchy用来手动刷新变化(不会自动刷新的)。当我们选择一个View后会如下图所示:

    这里写图片描述

    类似上图可以很方便的查看到当前View的许多信息;上图最底那三个彩色原点代表了当前View的性能指标,从左到右依次代表测量、布局、绘制的渲染时间,红色和黄色的点代表速度渲染较慢的View(当然了,有些时候较慢不代表有问题,譬如ViewGroup子节点越多、结构越复杂,性能就越差)。

    当然了,在自定义View的性能调试时,HierarchyViewer上面的invalidate Layout和requestLayout按钮的功能更加强大,它可以帮助我们debug自定义View执行invalidate()和requestLayout()过程,我们只需要在代码的相关地方打上断点就行了,接下来通过它观察绘制即可。

    可以发现,有了HierarchyViewer调试工具,我们的UI性能分析变得十分容易,这个工具也是我们开发中调试UI的利器,在平时写代码时会时常伴随我们左右。

    2-3-2 使用GPU过度绘制分析UI性能

    我们对于UI性能的优化还可以通过开发者选项中的GPU过度绘制工具来进行分析。在设置->开发者选项->调试GPU过度绘制(不同设备可能位置或者叫法不同)中打开调试后可以看见如下图(对settings当前界面过度绘制进行分析):

    这里写图片描述

    可以发现,开启后在我们想要调试的应用界面中可以看到各种颜色的区域,具体含义如下:

    颜色 含义
    无色 WebView等的渲染区域
    蓝色 1x过度绘制
    绿色 2x过度绘制
    淡红色 3x过度绘制
    红色 4x(+)过度绘制


    由于过度绘制指在屏幕的一个像素上绘制多次(譬如一个设置了背景色的TextView就会被绘制两次,一次背景一次文本;这里需要强调的是Activity设置的Theme主题的背景不被算在过度绘制层级中),所以最理想的就是绘制一次,也就是蓝色(当然这在很多绚丽的界面是不现实的,所以大家有个度即可,我们的开发性能优化标准要求最极端界面下红色区域不能长期持续超过屏幕三分之一,可见还是比较宽松的规定),因此我们需要依据此颜色分布进行代码优化,譬如优化布局层级、减少没必要的背景、暂时不显示的View设置为GONE而不是INVISIBLE、自定义View的onDraw方法设置canvas.clipRect()指定绘制区域或通过canvas.quickreject()减少绘制区域等。

    2-3-3 使用GPU呈现模式图及FPS考核UI性能

    Android界面流畅度除过视觉感知以外是可以考核的(测试妹子专用),常见的方法就是通过GPU呈现模式图或者实时FPS显示进行考核,这里我们主要针对GPU呈现模式图进行下说明,因为FPS考核测试方法有很多(譬如自己写代码实现、第三方App测试、固件支持等),所以不做统一说明。

    通过开发者选项中GPU呈现模式图工具来进行流畅度考量的流程是(注意:如果是在开启应用后才开启此功能,记得先把应用结束后重新启动)在设置->开发者选项->GPU呈现模式(不同设备可能位置或者叫法不同)中打开调试后可以看见如下图(对settings当前界面上下滑动列表后的图表):

    这里写图片描述

    当然,也可以在执行完UI滑动操作后在命令行输入如下命令查看命令行打印的GPU渲染数据(分析依据:Draw + Process + Execute = 完整的显示一帧时间 < 16ms):

    adb shell dumpsys gfxinfo [应用包名]

    打开上图可视化工具后,我们可以在手机画面上看到丰富的GPU绘制图形信息,分别展示了StatusBar、NavgationBar、Activity区域等的GPU渲染时间信息,随着界面的刷新,界面上会以实时柱状图来显示每帧的渲染时间,柱状图越高表示渲染时间越长,每个柱状图偏上都有一根代表16ms基准的绿色横线,每一条竖着的柱状线都包含三部分(蓝色代表测量绘制Display List的时间,红色代表OpenGL渲染Display List所需要的时间,黄色代表CPU等待GPU处理的时间),只要我们每一帧的总时间低于基准线就不会发生UI卡顿问题(个别超出基准线其实也不算啥问题的)。

    可以发现,这个工具是有局限性的,他虽然能够看出来有帧耗时超过基准线导致了丢帧卡顿,但却分析不到造成丢帧的具体原因。所以说为了配合解决分析UI丢帧卡顿问题我们还需要借助traceview和systrace来进行原因追踪,下面我们会介绍这两种工具的。

    2-3-4 使用Lint进行资源及冗余UI布局等优化

    上面说了,冗余资源及逻辑等也可能会导致加载和执行缓慢,所以我们就来看看Lint这个工具是如何发现优化这些问题的(当然了,Lint实际的功能是非常强大的,我们开发中也是经常使用它来发现一些问题的,这里主要有点针对UI性能的说明了,其他的雷同)。

    在Android Studio 1.4版本中使用Lint最简单的办法就是将鼠标放在代码区点击右键->Analyze->Inspect Code–>界面选择你要检测的模块->点击确认开始检测,等待一下后会发现如下结果:

    这里写图片描述

    可以看见,Lint检测完后给了我们很多建议的,我们重点看一个关于UI性能的检测结果;上图中高亮的那一行明确说明了存在冗余的UI层级嵌套,所以我们是可以点击跳进去进行优化处理掉的。

    当然了,Lint还有很多功能,大家可以自行探索发挥,这里只是达到抛砖引玉的作用。

    2-3-5 使用Memory监测及GC打印与Allocation Tracker进行UI卡顿分析

    关于Android的内存管理机制下面的一节会详细介绍,这里我们主要针对GC导致的UI卡顿问题进行详细说明。

    Android系统会依据内存中不同的内存数据类型分别执行不同的GC操作,常见应用开发中导致GC频繁执行的原因主要可能是因为短时间内有大量频繁的对象创建与释放操作,也就是俗称的内存抖动现象,或者短时间内已经存在大量内存暂用介于阈值边缘,接着每当有新对象创建时都会导致超越阈值触发GC操作。

    如下是我工作中一个项目的一次经历(我将代码回退特意抓取的),出现这个问题的场景是一次压力测试导致整个系统卡顿,瞬间杀掉应用就OK了,究其原因最终查到是一个API的调运位置写错了方式,导致一直被狂调,当普通使用时不会有问题,压力测试必现卡顿。具体内存参考图如下:
    这里写图片描述
    与此抖动图对应的LogCat抓取如下:

    //截取其中比较密集一段LogCat,与上图Memory检测到的抖动图对应,其中xxx为应用包名
    ......
    10-06 00:59:45.619 xxx I/art: Explicit concurrent mark sweep GC freed 72515(3MB) AllocSpace objects, 65(2028KB) LOS objects, 80% free, 17MB/89MB, paused 3.505ms total 60.958ms
    10-06 00:59:45.749 xxx I/art: Explicit concurrent mark sweep GC freed 5396(193KB) AllocSpace objects, 0(0B) LOS objects, 75% free, 23MB/95MB, paused 2.079ms total 100.522ms
    ......
    10-06 00:59:48.059 xxx I/art: Explicit concurrent mark sweep GC freed 4693(172KB) AllocSpace objects, 0(0B) LOS objects, 75% free, 23MB/95MB, paused 2.227ms total 101.692ms
    ......

    我们知道,类似上面logcat打印一样,触发垃圾回收的主要原因有以下几种:

    • GC_MALLOC——内存分配失败时触发;

    • GC_CONCURRENT——当分配的对象大小超过一个限定值(不同系统)时触发;

    • GC_EXPLICIT——对垃圾收集的显式调用(System.gc()) ;

    • GC_EXTERNAL_ALLOC——外部内存分配失败时触发;

    可以看见,这种不停的大面积打印GC导致所有线程暂停的操作必定会导致UI视觉的卡顿,所以我们要避免此类问题的出现,具体的常见优化方式如下:

    • 检查代码,尽量避免有些频繁触发的逻辑方法中存在大量对象分配;
    • 尽量避免在多次for循环中频繁分配对象;
    • 避免在自定义View的onDraw()方法中执行复杂的操作及创建对象(譬如Paint的实例化操作不要写在onDraw()方法中等);
    • 对于并发下载等类似逻辑的实现尽量避免多次创建线程对象,而是交给线程池处理。

    当然了,有了上面说明GC导致的性能后我们就该定位分析问题了,可以通过运行DDMS->Allocation Tracker标签打开一个新窗口,然后点击Start Tracing按钮,接着运行你想分析的代码,运行完毕后点击Get Allocations按钮就能够看见一个已分配对象的列表,如下:

    这里写图片描述

    点击上面第一个表格中的任何一项就能够在第二个表格中看见导致该内存分配的栈信息,通过这个工具我们可以很方便的知道代码分配了哪类对象、在哪个线程、哪个类、哪个文件的哪一行。譬如我们可以通过Allocation Tracker分别做一次Paint对象实例化在onDraw与构造方法的一个自定义View的内存跟踪,然后你就明白这个工具的强大了。

    PS一句,Android Studio新版本除过DDMS以外在Memory视图的左侧已经集成了Allocation Tracker功能,只是用起来还是没有DDMS的方便实用,如下图:

    这里写图片描述

    2-3-6 使用Traceview和dmtracedump进行分析优化

    关于UI卡顿问题我们还可以通过运行Traceview工具进行分析,他是一个分析器,记录了应用程序中每个函数的执行时间;我们可以打开DDMS然后选择一个进程,接着点击上面的“Start Method Profiling”按钮(红色小点变为黑色即开始运行),然后操作我们的卡顿UI(小范围测试,所以操作最好不要超过5s),完事再点一下刚才按的那个按钮,稍等片刻即可出现下图,如下:

    这里写图片描述

    花花绿绿的一幅图我们怎么分析呢?下面我们解释下如何通过该工具定位问题:

    整个界面包括上下两部分,上面是你测试的进程中每个线程运行的时间线,下面是每个方法(包含parent及child)执行的各个指标的值。通过上图的时间面板可以直观发现,整个trace时间段main线程做的事情特别多,其他的做的相对较少。当我们选择上面的一个线程后可以发现下面的性能面板很复杂,其实这才是TraceView的核心图表,它主要展示了线程中各个方法的调用信息(CPU使用时间、调用次数等),这些信息就是我们分析UI性能卡顿的核心关注点,所以我们先看几个重要的属性说明,如下:

    属性名 含义
    name 线程中调运的方法名;
    Incl CPU Time 当前方法(包含内部调运的子方法)执行占用的CPU时间;
    Excl CPU Time 当前方法(不包含内部调运的子方法)执行占用的CPU时间;
    Incl Real Time 当前方法(包含内部调运的子方法)执行的真实时间,ms单位;
    Excl Real Time 当前方法(不包含内部调运的子方法)执行的真实时间,ms单位;
    Calls+Recur Calls/Total 当前方法被调运的次数及递归调运占总调运次数百分比;
    CPU Time/Call 当前方法调运CPU时间与调运次数比,即当前方法平均执行CPU耗时时间;
    Real Time/Call 当前方法调运真实时间与调运次数比,即当前方法平均执行真实耗时时间;(重点关注)


    有了对上面Traceview图表的一个认识之后我们就来看看具体导致UI性能后该如何切入分析,一般Traceview可以定位两类性能问题:

    • 方法调运一次需要耗费很长时间导致卡顿;

    • 方法调运一次耗时不长,但被频繁调运导致累计时长卡顿。

    譬如我们来举个实例,有时候我们写完App在使用时不觉得有啥大的影响,但是当我们启动完App后静止在那却十分费电或者导致设备发热,这种情况我们就可以打开Traceview然后按照Cpu Time/Call或者Real Time/Call进行降序排列,然后打开可疑的方法及其child进行分析查看,然后再回到代码定位检查逻辑优化即可;当然了,我们也可以通过该工具来trace我们自定义View的一些方法来权衡性能问题,这里不再一一列举喽。

    可以看见,Traceview能够帮助我们分析程序性能,已经很方便了,然而Traceview家族还有一个更加直观强大的小工具,那就是可以通过dmtracedump生成方法调用图。具体做法如下:

    dmtracedump -g result.png target.trace  //结果png文件  目标trace文件

    通过这个生成的方法调运图我们可以更加直观的发现一些方法的调运异常现象。不过本人优化到现在还没怎么用到它,每次用到Traceview分析就已经搞定问题了,所以说dmtracedump自己酌情使用吧。

    PS一句,Android Studio新版本除过DDMS以外在CPU视图的左侧已经集成了Traceview(start Method Tracing)功能,只是用起来还是没有DDMS的方便实用(这里有一篇AS MT个人觉得不错的分析文章(引用自网络,链接属于原作者功劳)),如下图:

    这里写图片描述

    2-3-7 使用Systrace进行分析优化

    Systrace其实有些类似Traceview,它是对整个系统进行分析(同一时间轴包含应用及SurfaceFlinger、WindowManagerService等模块、服务运行信息),不过这个工具需要你的设备内核支持trace(命令行检查/sys/kernel/debug/tracing)且设备是eng或userdebug版本才可以,所以使用前麻烦自己确认一下。

    我们在分析UI性能时一般只关注图形性能(所以必须选择Graphics和View,其他随意),同时一般对于卡顿的抓取都是5s,最多10s。启动Systrace进行数据抓取可以通过两种方式,命令行方式如下:

    python systrace.py --time=10 -o mynewtrace.html sched gfx view wm

    图形模式:
    打开DDMS->Capture system wide trace using Android systrace->设置时间与选项点击OK就开始了抓取,接着操作APP,完事生成一个trace.html文件,用Chrome打开即可如下图:

    这里写图片描述
    在Chrome中浏览分析该文件我们可以通过键盘的W-A-S-D键来搞定,由于上面我们在进行trace时选择了一些选项,所以上图生成了左上方相关的CPU频率、负载、状态等信息,其中的CPU N代表了CPU核数,每个CPU行的柱状图表代表了当前时间段当前核上的运行信息;下面我们再来看看SurfaceFlinger的解释,如下:

    这里写图片描述

    可以看见上面左边栏的SurfaceFlinger其实就是负责绘制Android程序UI的服务,所以SurfaceFlinger能反应出整体绘制情况,可以关注上图VSYNC-app一行可以发现前5s多基本都能够达到16ms刷新间隔,5s多开始到7s多大于了15ms,说明此时存在绘制丢帧卡顿;同时可以发现surfaceflinger一行明显存在类似不规律间隔,这是因为有的地方是不需要重新渲染UI,所以有大范围不规律,有的是因为阻塞导致不规律,明显可以发现0到4s间大多是不需要渲染,而5s以后大多是阻塞导致;对应这个时间点我们放大可以看到每个部分所使用的时间和正在执行的任务,具体如下:
    这里写图片描述

    可以发现具体的执行明显存在超时性能卡顿(原点不是绿色的基本都代表存在一定问题,下面和右侧都会提示你选择的帧相关详细信息或者alert信息),但是遗憾的是通过Systrace只能大体上发现是否存在性能问题,具体问题还需要通过Traceview或者代码中嵌入Trace工具类等去继续详细分析,总之很蛋疼。

    PS:如果你想使用Systrace很轻松的分析定位所有问题,看明白所有的行含义,你还需要具备非常扎实的Android系统框架的原理才可以将该工具使用的得心应手。

    2-3-8 使用traces.txt文件进行ANR分析优化

    ANR(Application Not Responding)是Android中AMS与WMS监测应用响应超时的表现;之所以把臭名昭著的ANR单独作为UI性能卡顿的分析来说明是因为ANR是直接卡死UI不动且必须要解掉的Bug,我们必须尽量在开发时避免他的出现,当然了,万一出现了那就用下面介绍的方法来分析吧。

    我们应用开发中常见的ANR主要有如下几类:

    • 按键触摸事件派发超时ANR,一般阈值为5s(设置中开启ANR弹窗,默认有事件派发才会触发弹框ANR);

    • 广播阻塞ANR,一般阈值为10s(设置中开启ANR弹窗,默认不弹框,只有log提示);

    • 服务超时ANR,一般阈值为20s(设置中开启ANR弹窗,默认不弹框,只有log提示);

    当ANR发生时除过logcat可以看见的log以外我们还可以在系统指定目录下找到traces文件或dropbox文件进行分析,发生ANR后我们可以通过如下命令得到ANR trace文件:

    adb pull /data/anr/traces.txt ./

    然后我们用txt编辑器打开可以发现如下结构分析:

    //显示进程id、ANR发生时间点、ANR发生进程包名
    ----- pid 19073 at 2015-10-08 17:24:38 -----
    Cmd line: com.example.yanbo.myapplication
    //一些GC等object信息,通常可以忽略
    ......
    //ANR方法堆栈打印信息!重点!
    DALVIK THREADS (18):
    "main" prio=5 tid=1 Sleeping
      | group="main" sCount=1 dsCount=0 obj=0x7497dfb8 self=0x7f9d09a000
      | sysTid=19073 nice=0 cgrp=default sched=0/0 handle=0x7fa106c0a8
      | state=S schedstat=( 125271779 68162762 280 ) utm=11 stm=1 core=0 HZ=100
      | stack=0x7fe90d3000-0x7fe90d5000 stackSize=8MB
      | held mutexes=
      at java.lang.Thread.sleep!(Native method)
      - sleeping on <0x0a2ae345> (a java.lang.Object)
      at java.lang.Thread.sleep(Thread.java:1031)
      - locked <0x0a2ae345> (a java.lang.Object)
    //真正导致ANR的问题点,可以发现是onClick中有sleep导致。我们平时可以类比分析即可,这里不详细说明。
      at java.lang.Thread.sleep(Thread.java:985)
      at com.example.yanbo.myapplication.MainActivity$1.onClick(MainActivity.java:21)
      at android.view.View.performClick(View.java:4908)
      at android.view.View$PerformClick.run(View.java:20389)
      at android.os.Handler.handleCallback(Handler.java:815)
      at android.os.Handler.dispatchMessage(Handler.java:104)
      at android.os.Looper.loop(Looper.java:194)
      at android.app.ActivityThread.main(ActivityThread.java:5743)
      at java.lang.reflect.Method.invoke!(Native method)
      at java.lang.reflect.Method.invoke(Method.java:372)
      at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:988)
      at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:783)
    ......
    //省略一些不常关注堆栈打印
    ......

    至此常见的应用开发中ANR分析定位就可以解决了。

    2-4 应用UI性能分析解决总结

    可以看见,关于Android UI卡顿的性能分析还是有很多工具的,上面只是介绍了应用开发中我们经常使用的一些而已,还有一些其他的,譬如Oprofile等工具不怎么常用,这里就不再详细介绍。

    通过上面UI性能的原理、原因、工具分析总结可以发现,我们在开发应用时一定要时刻重视性能问题,如若真的没留意出现了性能问题,不妨使用上面的一些案例方式进行分析。但是那终归是补救措施,在我们知道上面UI卡顿原理之后我们应该尽量从项目代码架构搭建及编写时就避免一些UI性能问题,具体项目中常见的注意事项如下:

    • 布局优化;尽量使用include、merge、ViewStub标签,尽量不存在冗余嵌套及过于复杂布局(譬如10层就会直接异常),尽量使用GONE替换INVISIBLE,使用weight后尽量将width和heigh设置为0dp减少运算,Item存在非常复杂的嵌套时考虑使用自定义Item View来取代,减少measure与layout次数等。

    • 列表及Adapter优化;尽量复用getView方法中的相关View,不重复获取实例导致卡顿,列表尽量在滑动过程中不进行UI元素刷新等。

    • 背景和图片等内存分配优化;尽量减少不必要的背景设置,图片尽量压缩处理显示,尽量避免频繁内存抖动等问题出现。

    • 自定义View等绘图与布局优化;尽量避免在draw、measure、layout中做过于耗时及耗内存操作,尤其是draw方法中,尽量减少draw、measure、layout等执行次数。

    • 避免ANR,不要在UI线程中做耗时操作,遵守ANR规避守则,譬如多次数据库操作等。

    当然了,上面只是列出了我们项目中常见的一些UI性能注意事项而已,相信还有很多其他的情况这里没有说到,欢迎补充。还有一点就是我们上面所谓的UI性能优化分析总结等都是建议性的,因为性能这个问题是一个涉及面很广很泛的问题,有些优化不是必需的,有些优化是必需的,有些优化掉以后又是得不偿失的,所以我们一般着手解决那些必须的就可以了。

    【工匠若水 http://blog.csdn.net/yanbober 转载请注明出处。点我开始Android技术交流

    3 应用开发Memory内存性能分析优化

    说完了应用开发中的UI性能问题后我们就该来关注应用开发中的另一个重要、严重、非常重要的性能问题了,那就是内存性能优化分析。Android其实就是嵌入式设备,嵌入式设备核心关注点之一就是内存资源;有人说现在的设备都在堆硬件配置(譬如国产某米的某兔跑分手机、盒子等),所以内存不会再像以前那么紧张了,其实这句话听着没错,但为啥再牛逼配置的Android设备上有些应用还是越用系统越卡呢?这里面的原因有很多,不过相信有了这一章下面的内容分析,作为一个移动开发者的你就有能力打理好自己应用的那一亩三分地内存了,能做到这样就足以了。关于Android内存优化,这里有一篇Google的官方指导文档,但是本文为自己项目摸索,会有很多不一样的地方。

    3-1 Android内存管理原理

    系统级内存管理:

    Android系统内核是基于Linux,所以说Android的内存管理其实也是Linux的升级版而已。Linux在进程停止后就结束该进程,而Android把这些停止的进程都保留在内存中,直到系统需要更多内存时才选择性的释放一些,保留在内存中的进程默认(不包含后台service与Thread等单独UI线程的进程)不会影响整体系统的性能(速度与电量等)且当再次启动这些保留在内存的进程时可以明显提高启动速度,不需要再去加载。

    再直白点就是说Android系统级内存管理机制其实类似于Java的垃圾回收机制,这下明白了吧;在Android系统中框架会定义如下几类进程、在系统内存达到规定的不同level阈值时触发清空不同level的进程类型。

    这里写图片描述

    可以看见,所谓的我们的Service在后台跑着跑着挂了,或者盒子上有些大型游戏启动起来就挂(之前我在上家公司做盒子时遇见过),有一个直接的原因就是这个阈值定义的太大,导致系统一直认为已经达到阈值,所以进行优先清除了符合类型的进程。所以说,该阈值的设定是有一些讲究的,额,扯多了,我们主要是针对应用层内存分析的,系统级内存回收了解这些就基本够解释我们应用在设备上的一些表现特征了。

    应用级内存管理:

    在说应用级别内存管理原理时大家先想一个问题,假设有一个内存为1G的Android设备,上面运行了一个非常非常吃内存的应用,如果没有任何机制的情况下是不是用着用着整个设备会因为我们这个应用把1G内存吃光然后整个系统运行瘫痪呢?

    哈哈,其实Google的工程师才不会这么傻的把系统设计这么差劲。为了使系统不存在我们上面假想情况且能安全快速的运行,Android的框架使得每个应用程序都运行在单独的进程中(这些应用进程都是由Zygote进程孵化出来的,每个应用进程都对应自己唯一的虚拟机实例);如果应用在运行时再存在上面假想的情况,那么瘫痪的只会是自己的进程,不会直接影响系统运行及其他进程运行。

    既然每个Android应用程序都执行在自己的虚拟机中,那了解Java的一定明白,每个虚拟机必定会有堆内存阈值限制(值得一提的是这个阈值一般都由厂商依据硬件配置及设备特性自己设定,没有统一标准,可以为64M,也可以为128M等;它的配置是在Android的属性系统的/system/build.prop中配置dalvik.vm.heapsize=128m即可,若存在dalvik.vm.heapstartsize则表示初始申请大小),也即一个应用进程同时存在的对象必须小于阈值规定的内存大小才可以正常运行。

    接着我们运行的App在自己的虚拟机中内存管理基本就是遵循Java的内存管理机制了,系统在特定的情况下主动进行垃圾回收。但是要注意的一点就是在Android系统中执行垃圾回收(GC)操作时所有线程(包含UI线程)都必须暂停,等垃圾回收操作完成之后其他线程才能继续运行。这些GC垃圾回收一般都会有明显的log打印出回收类型,常见的如下:

    • GC_MALLOC——内存分配失败时触发;

    • GC_CONCURRENT——当分配的对象大小超过一个限定值(不同系统)时触发;

    • GC_EXPLICIT——对垃圾收集的显式调用(System.gc()) ;

    • GC_EXTERNAL_ALLOC——外部内存分配失败时触发;

    通过上面这几点的分析可以发现,应用的内存管理其实就是一个萝卜一个坑,坑都一般大,你在开发应用时要保证的是内存使用同一时刻不能超过坑的大小,否则就装不下了。

    3-2 Android内存泄露性能分析

    有了关于Android的一些内存认识,接着我们来看看关于Android应用开发中常出现的一种内存问题—-内存泄露。

    3-2-1 Android应用内存泄露概念

    众所周知,在Java中有些对象的生命周期是有限的,当它们完成了特定的逻辑后将会被垃圾回收;但是,如果在对象的生命周期本来该被垃圾回收时这个对象还被别的对象所持有引用,那就会导致内存泄漏;这样的后果就是随着我们的应用被长时间使用,他所占用的内存越来越大。如下就是一个最常见简单的泄露例子(其它的泄露不再一一列举了):

    public final class MainActivity extends Activity {
        private DbManager mDbManager;
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
    
            setContentView(R.layout.activity_main);
    
            //DbManager是一个单例模式类,这样就持有了MainActivity引用,导致泄露
            mDbManager = DbManager.getInstance(this);
        }
    }

    可以看见,上面例子中我们让一个单例模式的对象持有了当前Activity的强引用,那在当前Acvitivy执行完onDestroy()后,这个Activity就无法得到垃圾回收,也就造成了内存泄露。

    内存泄露可以引发很多的问题,常见的内存泄露导致问题如下:

    • 应用卡顿,响应速度慢(内存占用高时JVM虚拟机会频繁触发GC);

    • 应用被从后台进程干为空进程(上面系统内存原理有介绍,也就是超过了阈值);

    • 应用莫名的崩溃(上面应用内存原理有介绍,也就是超过了阈值OOM);

    造成内存泄露泄露的最核心原理就是一个对象持有了超过自己生命周期以外的对象强引用导致该对象无法被正常垃圾回收;可以发现,应用内存泄露是个相当棘手重要的问题,我们必须重视。

    3-2-2 Android应用内存泄露察觉手段

    知道了内存泄露的概念之后肯定就是想办法来确认自己的项目是否存在内存泄露了,那该如何察觉自己项目是否存在内存泄露呢?如下提供了几种常用的方式:

    察觉方式 场景
    AS的Memory窗口 平时用来直观了解自己应用的全局内存情况,大的泄露才能有感知。
    DDMS-Heap内存监测工具 同上,大的泄露才能有感知。
    dumpsys meminfo命令 常用方式,可以很直观的察觉一些泄露,但不全面且常规足够用。
    leakcanary神器 比较强大,可以感知泄露且定位泄露;实质是MAT原理,只是更加自动化了,当现有代码量已经庞大成型,且无法很快察觉掌控全局代码时极力推荐;或者是偶现泄露的情况下极力推荐。

    AS的Memory窗口如下,详细的说明这里就不解释了,很简单很直观(使用频率高):

    这里写图片描述

    DDMS-Heap内存监测工具窗口如下,详细的说明这里就不解释了,很简单(使用频率不高):

    这里写图片描述

    dumpsys meminfo命令如下(使用频率非常高,非常高效,我的最爱之一,平时一般关注几个重要的Object个数即可判断一般的泄露;当然了,adb shell dumpsys meminfo不跟参数直接展示系统所有内存状态):

    这里写图片描述

    leakcanary神器使用这里先不说,下文会专题介绍,你会震撼的一B。有了这些工具的定位我们就能很方便的察觉我们App的内存泄露问题,察觉到以后该怎么定位分析呢,继续往下看。

    3-2-3 Android应用内存泄露leakcanary工具定位分析

    leakcanary是一个开源项目,一个内存泄露自动检测工具,是著名的GitHub开源组织Square贡献的,它的主要优势就在于自动化过早的发觉内存泄露、配置简单、抓取贴心,缺点在于还存在一些bug,不过正常使用百分之九十情况是OK的,其核心原理与MAT工具类似。

    关于leakcanary工具的配置使用方式这里不再详细介绍,因为真的很简单,详情点我参考官方教程学习使用即可

    PS:之前在优化性能时发现我们有一个应用有两个界面退出后Activity没有被回收(dumpsys meminfo发现一直在加),所以就怀疑可能存在内存泄露。但是问题来了,这两个Activity的逻辑十分复杂,代码也不是我写的,相关联的代码量也十分庞大,更加郁闷的是很难判断是哪个版本修改导致的,这时候只知道有泄露,却无法定位具体原因,使用MAT分析解决掉了一个可疑泄露后发现泄露又变成了概率性的。可以发现,对于这种概率性的泄露用MAT去主动抓取肯定是很耗时耗力的,所以决定直接引入leakcanary神器来检测项目,后来很快就彻底解决了项目中所有必现的、偶现的内存泄露。

    总之一点,工具再强大也只是帮我们定位可能的泄露点,而最核心的GC ROOT泄露信息推导出泄露问题及如何解决还是需要你把住代码逻辑及泄露核心概念去推理解决。

    3-2-4 Android应用内存泄露MAT工具定位分析

    Eclipse Memory Analysis Tools(点我下载)是一个专门分析Java堆数据内存引用的工具,我们可以使用它方便的定位内存泄露原因,核心任务就是找到GC ROOT位置即可,哎呀,关于这个工具的使用我是真的不想说了,自己搜索吧,实在简单、传统的不行了。

    PS:这是开发中使用频率非常高的一个工具之一,麻烦务必掌握其核心使用技巧,虽然Android Studio已经实现了部分功能,但是真的很难用,遇到问题目前还是使用Eclipse Memory Analysis Tools吧。

    原谅我该小节的放荡不羁!!!!(其实我是困了,呜呜!)

    3-2-5 Android应用开发规避内存泄露建议

    有了上面的原理及案例处理其实还不够,因为上面这些处理办法是补救的措施,我们正确的做法应该是在开发过程中就养成良好的习惯和敏锐的嗅觉才对,所以下面给出一些应用开发中常见的规避内存泄露建议:

    • Context使用不当造成内存泄露;不要对一个Activity Context保持长生命周期的引用(譬如上面概念部分给出的示例)。尽量在一切可以使用应用ApplicationContext代替Context的地方进行替换(原理我前面有一篇关于Context的文章有解释)。

    • 非静态内部类的静态实例容易造成内存泄漏;即一个类中如果你不能够控制它其中内部类的生命周期(譬如Activity中的一些特殊Handler等),则尽量使用静态类和弱引用来处理(譬如ViewRoot的实现)。

    • 警惕线程未终止造成的内存泄露;譬如在Activity中关联了一个生命周期超过Activity的Thread,在退出Activity时切记结束线程。一个典型的例子就是HandlerThread的run方法是一个死循环,它不会自己结束,线程的生命周期超过了Activity生命周期,我们必须手动在Activity的销毁方法中中调运thread.getLooper().quit();才不会泄露。

    • 对象的注册与反注册没有成对出现造成的内存泄露;譬如注册广播接收器、注册观察者(典型的譬如数据库的监听)等。

    • 创建与关闭没有成对出现造成的泄露;譬如Cursor资源必须手动关闭,WebView必须手动销毁,流等对象必须手动关闭等。

    • 不要在执行频率很高的方法或者循环中创建对象,可以使用HashTable等创建一组对象容器从容器中取那些对象,而不用每次new与释放。

    • 避免代码设计模式的错误造成内存泄露。

    关于规避内存泄露上面我只是列出了我在项目中经常遇见的一些情况而已,肯定不全面,欢迎拍砖!当然了,只有我们做到好的规避加上强有力的判断嗅觉泄露才能让我们的应用驾驭好自己的一亩三分地。

    3-3 Android内存溢出OOM性能分析

    上面谈论了Android应用开发的内存泄露,下面谈谈内存溢出(OOM);其实可以认为内存溢出与内存泄露是交集关系,具体如下图:

    这里写图片描述

    下面我们就来看看内存溢出(OOM)相关的东东吧。

    3-3-1 Android应用内存溢出OOM概念

    上面我们探讨了Android内存管理和应用开发中的内存泄露问题,可以知道内存泄露一般影响就是导致应用卡顿,但是极端的影响是使应用挂掉。前面也提到过应用的内存分配是有一个阈值的,超过阈值就会出问题,这里我们就来看看这个问题—–内存溢出(OOM–OutOfMemoryError)。

    内存溢出的主要导致原因有如下几类:

    • 应用代码存在内存泄露,长时间积累无法释放导致OOM;

    • 应用的某些逻辑操作疯狂的消耗掉大量内存(譬如加载一张不经过处理的超大超高清图片等)导致超过阈值OOM;

    可以发现,无论哪种类型,导致内存溢出(OutOfMemoryError)的核心原因就是应用的内存超过阈值了。

    3-3-2 Android应用内存溢出OOM性能分析

    通过上面的OOM概念和那幅交集图可以发现,要想分析OOM原因和避免OOM需要分两种情况考虑,泄露导致的OOM,申请过大导致的OOM。

    内存泄露导致的OOM分析:

    这种OOM一旦发生后会在logcat中打印相关OutOfMemoryError的异常栈信息,不过你别高兴太早,这种情况下导致的OOM打印异常信息是没有太大作用,因为这种OOM的导致一般都如下图情况(图示为了说明问题数据和场景有夸张,请忽略):

    这里写图片描述

    从图片可以看见,这种OOM我们有时也遇到,第一反应是去分析OOM异常打印栈,可是后来发现打印栈打印的地方没有啥问题,没有可优化的余地了,于是就郁闷了。其实这时候你留心观察几个现象即可,如下:

    • 留意你执行触发OOM操作前的界面是否有卡顿或者比较密集的GC打印;
    • 使用命令查看下当前应用占用内存情况;

    确认了以上这些现象你基本可以断定该OOM的log真的没用,真正导致问题的原因是内存泄露,所以我们应该按照上节介绍的方式去着手排查内存泄露问题,解决掉内存泄露后红色空间都能得到释放,再去显示一张0.8M的优化图片就不会再报OOM异常了。

    不珍惜内存导致的OOM分析:

    上面说了内存泄露导致的OOM异常,下面我们再来看一幅图(数据和场景描述有夸张,请忽略),如下:

    这里写图片描述

    可见,这种类型的OOM就很好定位原因了,一般都可以从OOM后的log中得出分析定位。

    如下例子,我们在Activity中的ImageView放置一张未优化的特大的(30多M)高清图片,运行直接崩溃如下:

    //抛出OOM异常
    10-10 09:01:04.873 11703-11703/? E/art: Throwing OutOfMemoryError "Failed to allocate a 743620620 byte allocation with 4194208 free bytes and 239MB until OOM"
    10-10 09:01:04.940 11703-11703/? E/art: Throwing OutOfMemoryError "Failed to allocate a 743620620 byte allocation with 4194208 free bytes and 239MB until OOM"
    //堆栈打印
    10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: FATAL EXCEPTION: main
    10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: Process: com.example.application, PID: 11703
    10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.application/com.example.myapplication.MainActivity}: android.view.InflateException: Binary XML file line #21: Error inflating class <unknown>
    10-10 09:01:04.958 11703-11703/? E/AndroidRuntime:     at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2610)
    10-10 09:01:04.958 11703-11703/? E/AndroidRuntime:     at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2684)
    10-10 09:01:04.958 11703-11703/? E/AndroidRuntime:     at android.app.ActivityThread.access$800(ActivityThread.java:177)
    10-10 09:01:04.958 11703-11703/? E/AndroidRuntime:     at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1542)
    10-10 09:01:04.958 11703-11703/? E/AndroidRuntime:     at android.os.Handler.dispatchMessage(Handler.java:111)
    10-10 09:01:04.958 11703-11703/? E/AndroidRuntime:     at android.os.Looper.loop(Looper.java:194)
    10-10 09:01:04.958 11703-11703/? E/AndroidRuntime:     at android.app.ActivityThread.main(ActivityThread.java:5743)
    10-10 09:01:04.958 11703-11703/? E/AndroidRuntime:     at java.lang.reflect.Method.invoke(Native Method)
    10-10 09:01:04.958 11703-11703/? E/AndroidRuntime:     at java.lang.reflect.Method.invoke(Method.java:372)
    10-10 09:01:04.958 11703-11703/? E/AndroidRuntime:     at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:988)
    10-10 09:01:04.958 11703-11703/? E/AndroidRuntime:     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:783)
    //出错地点,原因是21行的ImageView设置的src是一张未优化的31M的高清图片
    10-10 09:01:04.958 11703-11703/? E/AndroidRuntime:  Caused by: android.view.InflateException: Binary XML file line #21: Error inflating class <unknown>
    10-10 09:01:04.958 11703-11703/? E/AndroidRuntime:     at android.view.LayoutInflater.createView(LayoutInflater.java:633)

    通过上面的log可以很方便的看出来问题原因所在地,那接下来的做法就是优化呗,降低图片的相关规格即可(譬如使用BitmapFactory的Option类操作等)。

    PS:提醒一句的是记得应用所属的内存是区分Java堆和native堆的!

    3-3-3 Android应用规避内存溢出OOM建议

    还是那句话,等待OOM发生是为时已晚的事,我们应该将其扼杀于萌芽之中,至于如何在开发中规避OOM,如下给出一些我们应用开发中的常用的策略建议:

    • 时刻记得不要加载过大的Bitmap对象;譬如对于类似图片加载我们要通过BitmapFactory.Options设置图片的一些采样比率和复用等,具体做法点我参考官方文档,不过过我们一般都用fresco或Glide开源库进行加载。

    • 优化界面交互过程中频繁的内存使用;譬如在列表等操作中只加载可见区域的Bitmap、滑动时不加载、停止滑动后再开始加载。

    • 有些地方避免使用强引用,替换为弱引用等操作。

    • 避免各种内存泄露的存在导致OOM。

    • 对批量加载等操作进行缓存设计,譬如列表图片显示,Adapter的convertView缓存等。

    • 尽可能的复用资源;譬如系统本身有很多字符串、颜色、图片、动画、样式以及简单布局等资源可供我们直接使用,我们自己也要尽量复用style等资源达到节约内存。

    • 对于有缓存等存在的应用尽量实现onLowMemory()和onTrimMemory()方法。

    • 尽量使用线程池替代多线程操作,这样可以节约内存及CPU占用率。

    • 尽量管理好自己的Service、Thread等后台的生命周期,不要浪费内存占用。

    • 尽可能的不要使用依赖注入,中看不中用。

    • 尽量在做一些大内存分配等可疑内存操作时进行try catch操作,避免不必要的应用闪退。

    • 尽量的优化自己的代码,减少冗余,进行编译打包等优化对齐处理,避免类加载时浪费内存。

    可以发现,上面只是列出了我们开发中常见的导致OOM异常的一些规避原则,还有很多相信还没有列出来,大家可以自行追加参考即可。

    3-4 Android内存性能优化总结

    无论是什么电子设备的开发,内存问题永远都是一个很深奥、无底洞的话题,上面的这些内存分析建议也单单只是Android应用开发中一些常见的场景而已,真正的达到合理的优化还是需要很多知识和功底的。

    合理的应用架构设计、设计风格选择、开源Lib选择、代码逻辑规范等都会决定到应用的内存性能,我们必须时刻头脑清醒的意识到这些问题潜在的风险与优劣,因为内存优化必须要有一个度,不能一味的优化,亦不能置之不理。

    【工匠若水 http://blog.csdn.net/yanbober 转载请注明出处。点我开始Android技术交流

    4 Android应用API使用及代码逻辑性能分析

    在我们开发中除过常规的那些经典UI、内存性能问题外其实还存在很多潜在的性能优化、这种优化不是十分明显,但是在某些场景下却是非常有必要的,所以我们简单列举一些常见的其他潜在性能优化技巧,具体如下探讨。

    4-1 Android应用String/StringBuilder/StringBuffer优化建议

    字符串操作在Android应用开发中是十分常见的操作,也就是这个最简单的字符串操作却也暗藏很多潜在的性能问题,下面我们实例来说说。

    先看下面这个关于String和StringBuffer的对比例子:

    //性能差的实现
    String str1 = "Name:";
    String str2 = "GJRS";
    String Str = str1 + str2;
    //性能好的实现
    String str1 = "Name:";
    String str2 = "GJRS";
    StringBuffer str = new StringBuilder().append(str1).append(str2);

    通过这个例子可以看出来,String对象(记得是对象,不是常量)和StringBuffer对象的主要性能区别在于String对象是不可变的,所以每次对String对象做改变操作(譬如“+”操作)时其实都生成了新的String对象实例,所以会导致内存消耗性能问题;而StringBuffer对象做改变操作每次都会对自己进行操作,所以不需要消耗额外的内存空间。

    我们再看一个关于String和StringBuffer的对比例子:

    //性能差的实现
    StringBuffer str = new StringBuilder().append("Name:").append("GJRS");
    //性能好的实现
    String Str = "Name:" + "GJRS";

    在这种情况下你会发现StringBuffer的性能反而没有String的好,原因是在JVM解释时认为
    String Str = "Name:" + "GJRS";就是String Str = "Name:GJRS";,所以自然比StringBuffer快了。

    可以发现,如果我们拼接的是字符串常量则String效率比StringBuffer高,如果拼接的是字符串对象,则StringBuffer比String效率高,我们在开发中要酌情选择。当然,除过注意StringBuffer和String的效率问题,我们还应该注意另一个问题,那就是StringBuffer和StringBuilder的区别,其实StringBuffer和StringBuilder都继承自同一个父类,只是StringBuffer是线程安全的,也就是说在不考虑多线程情况下StringBuilder的性能又比StringBuffer高。

    PS:如果想追究清楚他们之间具体细节差异,麻烦自己查看实现源码即可。

    4-2 Android应用OnTrimMemory()实现性能建议

    OnTrimMemory是Android 4.0之后加入的一个回调方法,作用是通知应用在不同的情况下进行自身的内存释放,以避免被系统直接杀掉,提高应用程序的用户体验(冷启动速度是热启动的2~3倍)。系统会根据当前不同等级的内存使用情况调用这个方法,并且传入当前内存等级,这个等级有很多种,我们可以依据情况实现不同的等级,这里不详细介绍,但是要说的是我们应用应该至少实现如下等级:

    • TRIM_MEMORY_BACKGROUND
      内存已经很低了,系统准备开始根据LRU缓存来清理进程。这时候如果我们手动释放一些不重要的缓存资源,则当用户返回我们应用时会感觉到很顺畅,而不是重新启动应用。

    可以实现OnTrimMemory方法的系统组件有Application、Activity、Fragement、
    Service、ContentProvider;关于OnTrimMemory释放哪些内存其实在架构阶段就要考虑清楚哪些对象是要常驻内存的,哪些是伴随组件周期存在的,一般需要释放的都是缓存。
    如下给出一个我们项目中常用的例子:

    @Override
    public void onTrimMemory(int level) {
       if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
           clearCache();
       }
    }

    通常在我们代码实现了onTrimMemory后很难复显这种内存消耗场景,但是你又怕引入新Bug,想想办法测试。好在我们有一个快捷的方式来模拟触发该水平内存释放,如下命令:

    adb shell dumpsys gfxinfo packagename -cmd trim value

    packagename为包名或者进程id,value为ComponentCallbacks2.java里面定义的值,可以为80、60、40、20、5等,我们模拟触发其中的等级即可。

    4-3 Android应用HashMap与ArrayMap及SparseArray优化建议

    在Android开发中涉及到数据逻辑部分大部分用的都是Java的API(譬如HashMap),但是对于Android设备来说有些Java的API并不适合,可能会导致系统性能下降,好在Google团队已经意识到这些问题,所以他们针对Android设备对Java的一些API进行了优化,优化最多就是使用了ArrayMap及SparseArray替代HashMap来获得性能提升。

    HashMap:

    HashMap内部使用一个默认容量为16的数组来存储数据,数组中每一个元素存放一个链表的头结点,其实整个HashMap内部结构就是一个哈希表的拉链结构。HashMap默认实现的扩容是以2倍增加,且获取一个节点采用了遍历法,所以相对来说无论从内存消耗还是节点查找上都是十分昂贵的。

    SparseArray:

    SparseArray比HashMap省内存是因为它避免了对Key进行自动装箱(int转Integer),它内部是用两个数组来进行数据存储的(一个存Key,一个存Value),它内部对数据采用了压缩方式来表示稀疏数组数据,从而节约内存空间,而且其查找节点的实现采用了二分法,很明显可以看见性能的提升。

    ArrayMap:

    ArrayMap内部使用两个数组进行数据存储,一个记录Key的Hash值,一个记录Value值,它和SparseArray类似,也会在查找时对Key采用二分法。

    有了上面的基本了解我们可以得出结论供开发时参考,当数据量不大(千位级内)且Key为int类型时使用SparseArray替换HashMap效率高;当数据量不大(千位级内)且数据类型为Map类型时使用ArrayMap替换HashMap效率高;其他情况下HashMap效率相对高于二者。

    4-4 Android应用ContentProviderOperation优化建议

    ContentProvider是Android应用开发的核心组件之一,有时候在开发中需要使用ContentProvider对多行数据进行操作,我们的做法一般是多次调运相关操作方法,殊不知这种实现方式是非常低性能的,取而代之的做法应该是使用批量操作,具体为了使批量更新、插入、删除数据操作更加方便官方提供了ContentProviderOperation工具类。所以在我们开发中遇到类似情景时请务必使用批量操作,具体的优势如下:

    • 所有的操作都在一个事务中执行,可以保证数据的完整性。

    • 批量操作在一个事务中执行,所以只用打开、关闭一个事务。

    • 减轻应用程序与ContentProvider间的多次频繁交互,提升性能。

    可以看见,这对于数据库操作来说是一个非常有用的优化措施,烦请务必重视(我们项目优化过,的确有很大提升)。

    4-5 Android应用其他逻辑优化建议

    关于API及逻辑性能优化其实有多知识点的,这里无法一一列出,只能给出一些重要的知识点,下面再给出一些常见的优化建议:

    • 避免在Android中使用Java的枚举类型,因为编译后不但占空间,加载也费时,完全没有static final的变量好用、高效。

    • Handler发送消息时尽量使用obtain去获取已经存在的Message对象进行复用,而不是新new Message对象,这样可以减轻内存压力。

    • 在使用后台Service时尽量将能够替换为IntentService的地方替换为此,这样可以减轻系统压力、省电、省内存、省CPU占用率。

    • 在当前类内部尽量不要通过自己的getXXX、setXXX对自己内部成员进行操作,而是直接使用,这样可以提高代码执行效率。

    • 不要一味的为了设计模式而过分的抽象代码,因为代码抽象系数与代码加载执行时间成正比。

    • 尽量减少锁个数、减小锁范围,避免造成性能问题。

    • 合理的选择使用for循环与增强型for循环,譬如不要在ArrayList上使用增强型for循环等。

    哎呀,类似的小优化技巧有很多,这里不一一列举了,自行发挥留意即可。

    【工匠若水 http://blog.csdn.net/yanbober 转载请注明出处。点我开始Android技术交流

    5 Android应用移动设备电池耗电性能分析

    有了UI性能优化、内存性能优化、代码编写优化之后我们在来说说应用开发中很重要的一个优化模块—–电量优化。

    5-1 Android应用耗电量概念

    在盒子等开发时可能电量优化不是特别重视(视盒子待机真假待机模式而定),但是在移动设备开发中耗电量是一个非常重要的指标,如果用户一旦发现我们的应用非常耗电,不好意思,他们大多会选择卸载来解决此类问题,所以耗电量是一个十分重要的问题。

    关于我们应用的耗电量情况我们可以进行定长时间测试,至于具体的耗电量统计等请参考此文,同时我们还可以直接通过Battery Historian Tool来查看详细的应用电量消耗情况。最简单常用办法是通过命令直接查看,如下:

    adb shell dumpsys batterystats

    其实我们一款应用耗电量最大的部分不是UI绘制显示等,常见耗电量最大原因基本都是因为网络数据交互、GPS定位、大量内存性能问题、冗余的后台线程和Service等造成。

    5-2 Android应用耗电量优化建议

    优化电量使用情况我们不仅可以使用系统提供的一些API去处理,还可以在平时编写代码时就养成好的习惯。具体的一些建议如下:

    • 在需要网络的应用中,执行某些操作前尽量先进行网络状态判断。

    • 在网络应用传输中使用高效率的数据格式和解析方法,譬如JSON等。

    • 在传输用户反馈或者下载OTA升级包等不是十分紧急的操作时尽量采用压缩数据进行传输且延迟到设备充电和WIFI状态时进行。

    • 在有必要的情况下尽量通过PowerManager.WakeLock和JobScheduler来控制一些逻辑操作达到省电优化。

    • 对定位要求不太高的场景尽量使用网络定位,而不是GPS定位。

    • 对于定时任务尽量使用AlarmManager,而不是sleep或者Timer进行管理。

    • 尽可能的减少网络请求次数和减小网络请求时间间隔。

    • 后台任务要尽可能少的唤醒CPU,譬如IM通信的长连接心跳时间间隔、一些应用的后台定时唤醒时间间隔等要设计合理。

    • 特殊耗电业务情况可以进行弹窗等友好的交互设计提醒用户该操作会耗用过多电量。

    可以看见,上面只是一些常见的电量消耗优化建议。总之,作为应用开发者的我们要意识到电量损耗对于用户来说是非常敏感的,只有我们做到合理的电量优化才能赢得用户的芳心。

    【工匠若水 http://blog.csdn.net/yanbober 转载请注明出处。点我开始Android技术交流

    6 Android应用开发性能优化总结

    性能优化是一个很大的话题,上面我们谈到的只是应用开发中常见的性能问题,也是应用开发中性能问题的冰山一角,更多的性能优化技巧和能力不是靠看出来,而是靠经验和实战结果总结出来的,所以说性能优化是一个涉及面非常广的话题,如果你想对你的应用进行性能你必须对你应用的整个框架有一个非常清晰的认识。

    当然了,如果在我们开发中只是一味的追求各种极致的优化也是不对的。因为优化本来就是存在风险的,甚至有些过度的优化会直接导致项目的臃肿,所以不要因为极致的性能优化而破坏掉了你项目的合理架构。

    总之一句话,性能优化适可而止,请酌情优化。

    PS:附上Google关于Android开发的一些专题建议视频链接,不过在天朝需要自备梯子哦。

    【工匠若水 http://blog.csdn.net/yanbober 转载请注明出处。点我开始Android技术交流

    这里写图片描述

    展开全文
  • Android应用自动更新代码实现

    千次下载 热门讨论 2012-04-27 21:37:34
    Android应用自动更新代码实现,完美实现代码的自动更新。
  • Android应用程序组件Content Provider应用实例

    万次阅读 多人点赞 2011-11-21 00:58:44
    上文简要介绍了Android应用程序组件Content Provider在应用程序间共享数据的原理,但是没有进一步研究它的实现。本文将实现两个应用程序,其中一个以Content Provider的形式来提供数据访问入口,另一个通过这个...

            上文简要介绍了Android应用程序组件Content Provider在应用程序间共享数据的原理,但是没有进一步研究它的实现。本文将实现两个应用程序,其中一个以Content Provider的形式来提供数据访问入口,另一个通过这个Content Provider来访问这些数据。本文的例子不仅可以为下文分析Content Provider的实现原理准备好使用情景,还可以学习到它的一个未公开接口。

    《Android系统源代码情景分析》一书正在进击的程序员网(http://0xcc0xcd.com)中连载,点击进入!

            本文中的应用程序是按照上一篇文章Android应用程序组件Content Provider简要介绍和学习计划中提到的一般应用程序架构方法来设计的。本文包含两个应用程序,其中,第一个应用程序命名为ArticlesProvider,它使用了SQLite数据库来维护一个文章信息列表,同时,它定义了访问这个文章信息列表的URI,这样,我们就可以通过一个Content Provider组件来向第三方应用程序提供访问这个文章信息列表的接口;第二个应用程序命名为Article,它提供了管理保存在ArticlesProvider应用程序中的文章信息的界面入口,在这个应用程序中,用户可以添加、删除和修改这些文章信息。接下来我们就分别介绍这两个应用程序的实现。

            1. ArticlesProvider应用程序的实现

             首先是参照在Ubuntu上为Android系统内置Java应用程序测试Application Frameworks层的硬件服务一文,在packages/experimental目录下建立工程文件目录ArticlesProvider。在继续介绍这个应用程序的实现之前,我们先介绍一下这个应用程序用来保存文章信息的数据库的设计。

             我们知道,在Android系统中,内置了一款轻型的数据库SQLite。SQLite是专门为嵌入式产品而设计的,它具有占用资源低的特点,而且是开源的,非常适合在Android平台中使用,关于SQLite的更多信息可以访问官方网站http://www.sqlite.org

             ArticlesProvider应用程序就是使用SQLite来作为数据库保存文章信息的,数据库文件命名为Articles.db,它里面只有一张表ArticlesTable,表的结构如下所示:

             -------------------------------------------------------------

             | -- _id -- | --  _title -- | -- _abstrat -- | -- _url -- |

             -------------------------------------------------------------

             |               |                    |                        |                  | 

            它由四个字段表示,第一个字段_id表示文章的ID,类型为自动递增的integer,它作为表的key值;第二个字段_title表示文章的题目,类型为text;第三个字段_abstract表示文章的摘要,类型为text;第四个字段_url表示文章的URL,类型为text。注意,当我们打算将数据库表的某一列的数据作为一个数据行的ID时,就约定它的列名为_id。这是因为我们经常需要从数据库中获取一批数据,这些数据以Cursor的形式返回,对这些返回来的数据我们一般用一个ListView来显示,而这个ListView需要一个数据适配器Adapter来作为数据源,这时候就我们就可以以这个Cursor来构造一个Adapter。有些Adapter,例如android.widget.CursorAdapter,它们在实现自己的getItemId成员函数来获取指定数据行的ID时,就必须要从这个Cursor中相应的行里面取出列名为_id的字段的内容出来作为这个数据行的ID返回给调用者。当然,我们不在数据库表中定义这个_id列名也是可以的,不过这样从数据库中查询数据后得到的Cursor适合性就变差了,因此,建议我们在设计数据库表时,尽量设置其中一个列名字_id,并且保证这一列的内容是在数据库表中是唯一的。

            下面我们就开始介绍这个应用程序的实现了。这个应用程序只有两个源文件,分别是Articles.java和ArticlesProvider,都是放在shy.luo.providers.articles这个package下面。在Articles.java文件里面,主要是定义了一些常量,例如用来访问文章信息数据的URI、MIME(Multipurpose Internet Mail Extensions)类型以及格式等,这些常量是第三方应用程序访问这些文章信息数据时要使用到的,因此,我们把它定义在一个单独的文件中,稍后我们会介绍如果把这个Articles.java文件打包成一个jar文件,然后第三方应用程序就可以引用这个常量了,这样也避免了直接把这个源代码文件暴露给第三方应用程序。

            源文件Articles.java位于src/shy/luo/providers/articles目录下,它的内容如下所示:

    package shy.luo.providers.articles;
    
    import android.net.Uri;
    
    public class Articles {
            /*Data Field*/
            public static final String ID = "_id";
            public static final String TITLE = "_title";
            public static final String ABSTRACT = "_abstract";
            public static final String URL = "_url";
    
            /*Default sort order*/
            public static final String DEFAULT_SORT_ORDER = "_id asc";
    
            /*Call Method*/
            public static final String METHOD_GET_ITEM_COUNT = "METHOD_GET_ITEM_COUNT";
            public static final String KEY_ITEM_COUNT = "KEY_ITEM_COUNT";
    
            /*Authority*/
            public static final String AUTHORITY = "shy.luo.providers.articles";
    
            /*Match Code*/
            public static final int ITEM = 1;
            public static final int ITEM_ID = 2;
            public static final int ITEM_POS = 3;
    
            /*MIME*/
            public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.shy.luo.article";
            public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.shy.luo.article";
    
            /*Content URI*/
            public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/item");
            public static final Uri CONTENT_POS_URI = Uri.parse("content://" + AUTHORITY + "/pos");
    }
            ID、TITLE、ABSTRACT和URL四个常量前面已经解释过了,它是我们用来保存文章信息的数据表的四个列名;DEFAULT_SORT_ORDER常量是调用ContentProvider接口的query函数来查询数据时用的,它表示对查询结果按照_id列的值从小到大排列;METHOD_GET_ITEM_COUNT和KEY_ITEM_COUNT两个常量是调用ContentProvider接口的一个未公开函数call来查询数据时用的,它类似于微软COM中的IDispatch接口的Invoke函数,使用这个call函数时,传入参数METHOD_GET_ITEM_COUNT表示我们要调用我们自定义的ContentProvider子类中的getItemCount函数来获取数据库中的文章信息条目的数量,结果放在一个Bundle中以KEY_ITEM_COUNT为关键字的域中。

            剩下的常量都是跟数据URI相关的,这个需要详细解释一下。URI的全称是Universal Resource Identifier,即通用资源标志符,通过它用来唯一标志某个资源在网络中的位置,它的结构和我们常见的HTTP形式URL是一样的,其实我们可以把常见的HTTP形式的URL看成是URI结构的一个实例,URI是在更高一个层次上的抽象。在Android系统中,它也定义了自己的用来定痊某个特定的Content Provider的URI结构,它通常由四个组件来组成,如下所示:

            [content://][shy.luo.providers.articles][/item][/123]

            |------A------|-----------------B-------------------|---C---|---D--|

            A组件称为Scheme,它固定为content://,表示它后面的路径所表示的资源是由Content Provider来提供的。

            B组件称为Authority,它唯一地标识了一个特定的Content Provider,因此,这部分内容一般使用Content Provider所在的package来命名,使得它是唯一的。

            C组件称为资源路径,它表示所请求的资源的类型,这部分内容是可选的。如果我们自己所实现的Content Provider只提供一种类型的资源访问,那么这部分内部就可以忽略;如果我们自己实现的Content Provider同时提供了多种类型的资源访问,那么这部分内容就不可以忽略了。例如,我们有两种电脑资源可以提供给用户访问,一种是笔记本电脑,一种是平板电脑,我们就把分别它们定义为notebook和pad;如果我们想进一步按照系统类型来进一步细分这两种电脑资源,对笔记本电脑来说,一种是安装了windows系统的,一种是安装了linux系统的,我们就分别把它们定义为notebook/windows和notebook/linux;对平板电脑来说,一种是安装了ios系统的,一种是安装了android系统的,我们就分别把它们定义为pad/ios和pad/android。

            D组件称为资源ID,它表示所请求的是一个特定的资源,它通常是一个数字,对应前面我们所介绍的数据库表中的_id字段的内容,它唯一地标志了某一种资源下的一个特定的实例。继续以前面的电脑资源为例,如果我们请求的是编号为123的装了android系统的平板电脑,我们就把它定义为pad/android/123。当忽略这部分内容时,它有可能是表示请求某一种资源下的所有实例,取决于我们的URI匹配规则,后面我们将会进一步解释如何设置URI匹配规则。

            回到上面的Articles.java源文件中,我们定义了两个URI,分别用COTENT_URI和CONTENT_POS_URI两个常量来表示,它们的Authority组件均指定为shy.luo.providers.articles。其中,COTENT_URI常量表示的URI表示是通过ID来访问文章信息的,而CONTENT_POS_URI常量表示的URI表示是通过位置来访问文章信息的。例如,content://shy.luo.providers.articles/item表示访问所有的文章信息条目;content://shy.luo.providers.articles/item/123表示只访问ID值为123的文章信息条目;content://shy.luo.providers.articles/pos/1表示访问数据库表中的第1条文章信息条目,这条文章信息条目的ID值不一定为1。通过常量CONTENT_POS_URI来访问文章信息条目时,必须要指定位置,这也是我们设置的URI匹配规则来指定的,后面我们将会看到。

            此外,我们还需要定义与URI对应的资源的MIME类型。每个MIME类型由两部分组成,前面是数据的大类别,后面定义具体的种类。在Content Provider中,URI所对应的资源的MIME类型的大类别根据同时访问的资源的数量分为两种,对于访问单个资源的URI,它的大类别就为vnd.android.cursor.item,而对于同时访问多个资源的URI,它的大类别就为vnd.android.cursor.dir。Content Provider的URI所对应的资源的MIME类型的具体类别就需要由Content Provider的提供者来设置了,它的格式一般为vnd.[company name].[resource type]的形式。例如,在我们的例子中,CONTENT_TYPE和COTENT_ITEM_TYPE两个常量分别定义了两种MIME类型,它们的大类别分别为vnd.android.cursor.dir和vnd.android.cursor.item,而具体类别均为vdn.shy.luo.article,其中shy.luo就是表示公司名了,而article表示资源的类型为文章。这两个MIME类型常量主要是在实现ContentProvider的getType函数时用到的,后面我们将会看到。

            最后,ITEM、ITEM_ID和POS_ID三个常量分别定了三个URI匹配规则的匹配码。如果URI的形式为content://shy.luo.providers.articles/item,则匹配规则返回的匹配码为ITEM;如果URI的形式为content://shy.luo.providers.articles/item/#,其中#表示任意一个数字,则匹配规则返回的匹配码为ITEM_ID;如果URI的形式为#也是表示任意一个数字,则匹配规则返回的匹配码为ITEM_POS。这三个常量的用法我们在后面也将会看到。

            这样,Articles.java文件的内容就介绍完了。下面我们再接着介绍位于src/shy/luo/providers/articles目录下的ArticlesProvider.java文件,它的内容如下所示:

    import java.util.HashMap;
    
    import android.content.ContentValues;
    import android.content.Context;
    import android.content.UriMatcher;
    import android.content.ContentProvider;
    import android.content.ContentUris;
    import android.content.ContentResolver;
    import android.database.Cursor;
    import android.database.sqlite.SQLiteDatabase;
    import android.database.sqlite.SQLiteDatabase.CursorFactory;
    import android.database.sqlite.SQLiteException;
    import android.database.sqlite.SQLiteOpenHelper;
    import android.database.sqlite.SQLiteQueryBuilder;
    import android.net.Uri;
    import android.os.Bundle;
    import android.text.TextUtils;
    import android.util.Log;
    
    public class ArticlesProvider extends ContentProvider {
            private static final String LOG_TAG = "shy.luo.providers.articles.ArticlesProvider";
    
            private static final String DB_NAME = "Articles.db";
            private static final String DB_TABLE = "ArticlesTable";
            private static final int DB_VERSION = 1;
    
            private static final String DB_CREATE = "create table " + DB_TABLE +
                                    " (" + Articles.ID + " integer primary key autoincrement, " +
                                    Articles.TITLE + " text not null, " +
                                    Articles.ABSTRACT + " text not null, " +
                                    Articles.URL + " text not null);";
    
            private static final UriMatcher uriMatcher;
            static {
                    uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
                    uriMatcher.addURI(Articles.AUTHORITY, "item", Articles.ITEM);
                    uriMatcher.addURI(Articles.AUTHORITY, "item/#", Articles.ITEM_ID);
                    uriMatcher.addURI(Articles.AUTHORITY, "pos/#", Articles.ITEM_POS);
            }
    
            private static final HashMap<String, String> articleProjectionMap;
            static {
                    articleProjectionMap = new HashMap<String, String>();
                    articleProjectionMap.put(Articles.ID, Articles.ID);
                    articleProjectionMap.put(Articles.TITLE, Articles.TITLE);
                    articleProjectionMap.put(Articles.ABSTRACT, Articles.ABSTRACT);
                    articleProjectionMap.put(Articles.URL, Articles.URL);
            }
    
            private DBHelper dbHelper = null;
            private ContentResolver resolver = null;
    
            @Override
            public boolean onCreate() {
                    Context context = getContext();
                    resolver = context.getContentResolver();
                    dbHelper = new DBHelper(context, DB_NAME, null, DB_VERSION);
    
                    Log.i(LOG_TAG, "Articles Provider Create");
    
                    return true;
            }
    
            @Override
            public String getType(Uri uri) {
                    switch (uriMatcher.match(uri)) {
                    case Articles.ITEM:
                            return Articles.CONTENT_TYPE;
                    case Articles.ITEM_ID:
                    case Articles.ITEM_POS:
                            return Articles.CONTENT_ITEM_TYPE;
                    default:
                            throw new IllegalArgumentException("Error Uri: " + uri);
                    }
            }
    
            @Override
            public Uri insert(Uri uri, ContentValues values) {
                    if(uriMatcher.match(uri) != Articles.ITEM) {
                            throw new IllegalArgumentException("Error Uri: " + uri);
                    }
    
                    SQLiteDatabase db = dbHelper.getWritableDatabase();
    
                    long id = db.insert(DB_TABLE, Articles.ID, values);
                    if(id < 0) {
                            throw new SQLiteException("Unable to insert " + values + " for " + uri);
                    }
    
                    Uri newUri = ContentUris.withAppendedId(uri, id);
                    resolver.notifyChange(newUri, null);
    
                    return newUri;
            }
    
            @Override
            public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
                    SQLiteDatabase db = dbHelper.getWritableDatabase();
                    int count = 0;
    
                    switch(uriMatcher.match(uri)) {
                    case Articles.ITEM: {
                            count = db.update(DB_TABLE, values, selection, selectionArgs);
                            break;
                    }
                    case Articles.ITEM_ID: {
                            String id = uri.getPathSegments().get(1);
                            count = db.update(DB_TABLE, values, Articles.ID + "=" + id
                                            + (!TextUtils.isEmpty(selection) ? " and (" + selection + ')' : ""), selectionArgs);
                            break;
                    }
                    default:
                            throw new IllegalArgumentException("Error Uri: " + uri);
                    }
    
                    resolver.notifyChange(uri, null);
    
                    return count;
            }
    
            @Override
            public int delete(Uri uri, String selection, String[] selectionArgs) {
                    SQLiteDatabase db = dbHelper.getWritableDatabase();
                    int count = 0;
    
                    switch(uriMatcher.match(uri)) {
                    case Articles.ITEM: {
                            count = db.delete(DB_TABLE, selection, selectionArgs);
                            break;
                    }
                    case Articles.ITEM_ID: {
                            String id = uri.getPathSegments().get(1);
                            count = db.delete(DB_TABLE, Articles.ID + "=" + id
                                            + (!TextUtils.isEmpty(selection) ? " and (" + selection + ')' : ""), selectionArgs);
                            break;
                    }
                    default:
                            throw new IllegalArgumentException("Error Uri: " + uri);
                    }
    
                    resolver.notifyChange(uri, null);
    
                    return count;
            }
    
            @Override
            public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
                    Log.i(LOG_TAG, "ArticlesProvider.query: " + uri);
    
                    SQLiteDatabase db = dbHelper.getReadableDatabase();
    
                    SQLiteQueryBuilder sqlBuilder = new SQLiteQueryBuilder();
                    String limit = null;
    
                    switch (uriMatcher.match(uri)) {
                    case Articles.ITEM: {
                            sqlBuilder.setTables(DB_TABLE);
                            sqlBuilder.setProjectionMap(articleProjectionMap);
                            break;
                    }
                    case Articles.ITEM_ID: {
                            String id = uri.getPathSegments().get(1);
                            sqlBuilder.setTables(DB_TABLE);
                            sqlBuilder.setProjectionMap(articleProjectionMap);
                            sqlBuilder.appendWhere(Articles.ID + "=" + id);
                            break;
                    }
                    case Articles.ITEM_POS: {
                            String pos = uri.getPathSegments().get(1);
                            sqlBuilder.setTables(DB_TABLE);
                            sqlBuilder.setProjectionMap(articleProjectionMap);
                            limit = pos + ", 1";
                            break;
                    }
                    default:
                            throw new IllegalArgumentException("Error Uri: " + uri);
                    }
    
                    Cursor cursor = sqlBuilder.query(db, projection, selection, selectionArgs, null, null, TextUtils.isEmpty(sortOrder) ? Articles.DEFAULT_SORT_ORDER : sortOrder, limit);
                    cursor.setNotificationUri(resolver, uri);
    
                    return cursor;
            }
      
            @Override
            public Bundle call(String method, String request, Bundle args) {
                    Log.i(LOG_TAG, "ArticlesProvider.call: " + method);
    
                    if(method.equals(Articles.METHOD_GET_ITEM_COUNT)) {
                            return getItemCount();
                    }
    
                    throw new IllegalArgumentException("Error method call: " + method);
            }
    
            private Bundle getItemCount() {
                    Log.i(LOG_TAG, "ArticlesProvider.getItemCount");
    
                    SQLiteDatabase db = dbHelper.getReadableDatabase();
                    Cursor cursor = db.rawQuery("select count(*) from " + DB_TABLE, null);
    
                    int count = 0;
                    if (cursor.moveToFirst()) {
                            count = cursor.getInt(0);
                    }
    
                    Bundle bundle = new Bundle();
                    bundle.putInt(Articles.KEY_ITEM_COUNT, count);
    
                    cursor.close();
                    db.close();
    
                    return bundle;
            }
    
            private static class DBHelper extends SQLiteOpenHelper {
                    public DBHelper(Context context, String name, CursorFactory factory, int version) {
                            super(context, name, factory, version);
                    }
    
                    @Override
                    public void onCreate(SQLiteDatabase db) {
                            db.execSQL(DB_CREATE);
                    }
    
                    @Override
                    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
                            db.execSQL("DROP TABLE IF EXISTS " + DB_TABLE);
                            onCreate(db);
                    }
            }
    }

            我们在实现自己的Content Provider时,必须继承于ContentProvider类,并且实现以下六个函数:

            -- onCreate(),用来执行一些初始化的工作。

            -- query(Uri, String[], String, String[], String),用来返回数据给调用者。

            -- insert(Uri, ContentValues),用来插入新的数据。

            -- update(Uri, ContentValues, String, String[]),用来更新已有的数据。

            -- delete(Uri, String, String[]),用来删除数据。

            -- getType(Uri),用来返回数据的MIME类型。

            这些函数的实现都比较简单,这里我们就不详细介绍了,主要解释五个要点。

            第一点是我们在ArticlesProvider类的内部中定义了一个DBHelper类,它继承于SQLiteOpenHelper类,它用是用辅助我们操作数据库的。使用这个DBHelper类来辅助操作数据库的好处是只有当我们第一次对数据库时行操作时,系统才会执行打开数据库文件的操作。拿我们这个例子来说,只有第三方应用程序第一次调用query、insert、update或者delete函数来操作数据库时,我们才会真正去打开相应的数据库文件。这样在onCreate函数里,就不用执行打开数据库的操作,因为这是一个耗时的操作,而在onCreate函数中,要避免执行这些耗时的操作。

            第二点是设置URI匹配规则。因为我们是根据URI来操作数据库的,不同的URI对应不同的操作,所以我们首先要定义好URI匹配规则,这样,当我们获得一个URI时,就能快速地判断出要如何去操作数据库。设置URI匹配规则的代码如下所示:

    private static final UriMatcher uriMatcher;
    static {
          uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
          uriMatcher.addURI(Articles.AUTHORITY, "item", Articles.ITEM);
          uriMatcher.addURI(Articles.AUTHORITY, "item/#", Articles.ITEM_ID);
          uriMatcher.addURI(Articles.AUTHORITY, "pos/#", Articles.ITEM_POS);
    }

            在创建UriMatcher对象uriMatcher时,我们传给构造函数的参数为UriMatcher.NO_MATCH,它表示当uriMatcher不能匹配指定的URI时,就返回代码UriMatcher.NO_MATCH。接下来增加了三个匹配规则,分别是content://shy.luo.providers.articles/item、content://shy.luo.providers.articles/item/#和content://shy.luo.providers.articles/pos/#,它们的匹配码分别是Articles.ITEM、Articles.ITEM_ID和Articles.ITEM_POS,其中,符号#表示匹配任何数字。

            第三点是SQLiteQueryBuilder的使用。在query函数中,我们使用SQLiteQueryBuilder来辅助数据库查询操作,使用这个类的好处是我们可以不把数据库表的字段暴露出来,而是提供别名给第三方应用程序使用,这样就可以把数据库表内部设计隐藏起来,方便后续扩展和维护。列别名到真实列名的映射是由下面这个HashMap成员变量来实现的:

     private static final HashMap<String, String> articleProjectionMap;
     static {
           articleProjectionMap = new HashMap<String, String>();
           articleProjectionMap.put(Articles.ID, Articles.ID);
           articleProjectionMap.put(Articles.TITLE, Articles.TITLE);
           articleProjectionMap.put(Articles.ABSTRACT, Articles.ABSTRACT);
           articleProjectionMap.put(Articles.URL, Articles.URL);
     }
           在上面的put函数中,第一个参数表示列的别名,第二个参数表示列的真实名称。在这个例子中,我们把列的别名和和真实名称都设置成一样的。

           第四点是数据更新机制的使用。执行insert、update和delete三个函数时,都会导致数据库中的数据发生变化,所以这时候要通过调用ContentResolver接口的notifyChange函数来通知那些注册了监控特定URI的ContentObserver对象,使得它们可以相应地执行一些处理,例如更新数据在界面上的显示。在query函数中,最终返回给调用者的是一个Cursor,调用者获得这个Cursor以后,可以通过它的deleteRow或者commitUpdates来执行一些更新数据库的操作,这时候也要通知那些注册了相应的URI的ContentObserver来作相应的处理,因此,这里在返回Cursor之前,要通过Cursor类的setNotificationUri函数来把当前上下文的ContentResolver对象保存到Curosr里面去,以便当通过这个Cursor来改变数据库中的数据时,可以通知相应的ContentObserver来处理。不过这种用法已经过时了,即不建议通过这个Cursor来改变数据库的数据,要把Cursor中的数据看作是只读数据。这里调用Cursor类的setNotificationUri函数还有另外一个作用,我们注意到它的第二个参数uri,对应的是Cursor中的内容,当把这个uri传给Cursor时,Cursor就会注册自己的ContentObserver来监控这个uri对应的数据的变化。一旦这个uri对应的数据发生变化,这个Cursor对应的数据就不是再新的了,这时候就需要采取一些操作来更新内容了。

             第五点我们实现了ContentProvider的call函数。这个函数是一个未公开的函数,第三方应用程序只有Android源代码环境下开发,才能使用这个函数。设计这个函数的目的是什么呢?我们知道,当我们需要从Content Provider中获得数据时,一般都是要通过调用它的query函数来获得的,而这个函数将数据放在Cursor中来返回给调用者。以前面一篇文章Android应用程序组件Content Provider简要介绍和学习计划中,我们提到,Content Provider传给第三方应用程序的数据,是通过匿名共享内存来传输的。当要传输的数据量大的时候,使用匿名共享内存来传输数据是有好处的,它可以减入数据的拷贝,提高传输效率。但是,当要传输的数据量小时,使用匿名共享内存来作为媒介就有点用牛刀来杀鸡的味道了,因为匿名共享内存并不是免费的午餐,系统创建和匿名共享内存也是有开销的。因此,Content Provider提供了call函数来让第三方应用程序来获取一些自定义数据,这些数据一般都比较小,例如,只是传输一个整数,这样就可以用较小的代价来达到相同的数据传输的目的。

            至此,ArticlesProvider的源代码就分析完了,下面我们还要在AndroidManifest.xml文件中配置这个ArticlesProvider类才能正常使用。AndroidManifest.xml文件的内容如下所示:

    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
            package="shy.luo.providers.articles">
        <application android:process="shy.luo.process.article"
            android:label="@string/app_label"
            android:icon="@drawable/app_icon">
            <provider android:name="ArticlesProvider"
                android:authorities="shy.luo.providers.articles"
                android:label="@string/provider_label"
                android:multiprocess="false">
            </provider>
        </application>
    </manifest>
            在配置Content Provider的时候,最重要的就是要指定它的authorities属性了,只有配置了这个属性,第三方应用程序才能通过它来找到这个Content Provider。这要需要注意的,这里配置的authorities属性的值是和我们前面在Articles.java文件中定义的AUTHORITY常量的值是一致的。另外一个属性multiprocess是一个布尔值,它表示这个Content Provider是否可以在每个客户进程中创建一个实例,这样做的目的是为了减少进程间通信的开销。这里我们为了减少不必要的内存开销,把属性multiprocess的值设置为false,使得系统只能有一个Content Provider实例存在,它运行在自己的进程中。在这个配置文件里面,我们还可以设置这个Content Provider的访问权限,这里我们为了简单起见,就不设置权限了。有关Content Provider的访问权限的设置,可以参考官方文档http://developer.android.com/guide/topics/manifest/provider-element.html

            这个应用程序使用到的字符串资源定义在res/values/strings.xml文件中,它的内容如下所示:

    <?xml version="1.0" encoding="utf-8"?>
    <resources>
        <string name="app_label">Articles Storage</string>
        <string name="provider_label">Articles</string>
    </resources>
            由于Content Provider类型的应用程序是没有用户界面的,因此,我们不需要在res/layout目录下为程序准备界面配置文件。

            程序的编译脚本Android.mk的内容如下所示:

    LOCAL_PATH:= $(call my-dir)
    include $(CLEAR_VARS)
    
    LOCAL_MODULE_TAGS := optional
    
    LOCAL_SRC_FILES := $(call all-subdir-java-files)
    
    LOCAL_PACKAGE_NAME := ArticlesProvider
    
    include $(BUILD_PACKAGE)
            下面我们就可以参照如何单独编译Android源代码中的模块一文来编译和打包这个应用程序了:

    USER-NAME@MACHINE-NAME:~/Android$ mmm packages/experimental/ArticlesProvider    
    USER-NAME@MACHINE-NAME:~/Android$ make snod 

            这样,打包好的Android系统镜像文件system.img就包含我们这里所创建的ArticlesProvider应用程序了。

            前面说过,在Articles.java文件中定义的常量是要给第三方应用程序使用的,那么我们是不是直接把这个源文件交给第三方呢?这样就显得太不专业了,第三方拿到这个文件后,还必须要放在shy/luo/providers/articles目录下或者要把这个Articles类所在的package改掉才能正常使用。正确的做法是把编译好的Articles.java文件打包成一个jar文件交给第三方使用。编译ArticlesProvider这个应用程序成功后,生成的中间文件放在out/target/common/obj/APPS/ArticlesProvider_intermediates目录下,我们进入到这个目录中,然后执后下面的命令把Articles.class文件提取出来:

    USER-NAME@MACHINE-NAME:~/Android/out/target/common/obj/APPS/ArticlesProvider_intermediates$ jar -xvf classes.jar shy/luo/providers/articles/Articles.class

            然后再单独打包这个Articles.class文件:

    USER-NAME@MACHINE-NAME:~/Android/out/target/common/obj/APPS/ArticlesProvider_intermediates$ jar -cvf ArticlesProvider.jar ./shy
            这样,我们得到的ArticlesProvider.jar文件就包含了Articles.java这个文件中定义的常量了,第三方拿到这个文件后,就可以开发自己的应用程序来访问我们在ArticlesProvider这个Content Provider中保存的数据了。接下来我们就介绍调用这个ArticlesProvider来获取数据的第三方应用程序Article。

            2. Article应用程序的实现

            首先是参照前面的ArticlesProvider工程,在packages/experimental目录下建立工程文件目录Article。这个应用程序的作用是用来管理ArticlesProvider应用程序中保存的文章信息的,因此,它需要获得相应的Content Provider接口来访问ArticlesProvider中的数据。我们首先在工程目录Article下面创建一个libs目录,把上面得到的ArticlesProvider.jar放在libs目录下,后面我们在编译脚本的时候,再把它引用到工程上来。下面我们就开始分析这个应用程序的实现。

            这个应用程序的主界面MainActivity包含了一个ListView控件,用来显示从ArticlesProvider中得到的文章信息条目,在这个主界面上,可以浏览、增加、删除和更新文章信息。当需要增加、删除或者更新文章信息时,就会跳到另外一个界面ArticleActivity中去执行具体的操作。为了方便开发,我们把每一个文章信息条目封装成了一个Article类,并且把与ArticlesProvider进交互的操作都通过ArticlesAdapter类来实现。下面介绍每一个类的具本实现。

            下面是Article类的实现,它实现在src/shy/luo/Article.java文件中:

    package shy.luo.article;
    
    public class Article {
            private int id;
            private String title;
            private String abs;
            private String url;
    
            public Article(int id, String title, String abs, String url) {
                    this.id = id;
                    this.title = title;
                    this.abs = abs;
                    this.url = url;
            }
    
            public void setId(int id) {
                    this.id = id;
            }
    
            public int getId() {
                    return this.id;
            }
    
            public void setTitle(String title) {
                    this.title = title;
            }
    
            public String getTitle() {
                    return this.title;
            }
    
            public void setAbstract(String abs) {
                    this.abs = abs;
            }
    
            public String getAbstract() {
                    return this.abs;
            }
    
            public void setUrl(String url) {
                    this.url = url;
            }
    
            public String getUrl() {
                    return this.url;
            }
    }
           下面是ArticlesAdapter类的实现,它实现在src/shy/luo/ArticlesAdapter.java文件中:

    package shy.luo.article;
    
    import java.util.LinkedList;
    
    import shy.luo.providers.articles.Articles;
    import android.content.ContentResolver;
    import android.content.ContentUris;
    import android.content.ContentValues;
    import android.content.Context;
    import android.content.IContentProvider;
    import android.database.Cursor;
    import android.net.Uri;
    import android.os.Bundle;
    import android.os.RemoteException;
    import android.util.Log;
    
    
    public class ArticlesAdapter {
            private static final String LOG_TAG = "shy.luo.article.ArticlesAdapter";
    
            private ContentResolver resolver = null;
    
            public ArticlesAdapter(Context context) {
                    resolver = context.getContentResolver();
            }
    
            public long insertArticle(Article article) {
                    ContentValues values = new ContentValues();
                    values.put(Articles.TITLE, article.getTitle());
                    values.put(Articles.ABSTRACT, article.getAbstract());
                    values.put(Articles.URL, article.getUrl());
    
                    Uri uri = resolver.insert(Articles.CONTENT_URI, values);
                    String itemId = uri.getPathSegments().get(1);
    
                    return Integer.valueOf(itemId).longValue();
            }
    
            public boolean updateArticle(Article article) {
                    Uri uri = ContentUris.withAppendedId(Articles.CONTENT_URI, article.getId());
    
                    ContentValues values = new ContentValues();
                    values.put(Articles.TITLE, article.getTitle());
                    values.put(Articles.ABSTRACT, article.getAbstract());
                    values.put(Articles.URL, article.getUrl());
    
                    int count = resolver.update(uri, values, null, null);
    
                    return count > 0;
            }
    
            public boolean removeArticle(int id) {
                    Uri uri = ContentUris.withAppendedId(Articles.CONTENT_URI, id);
    
                    int count = resolver.delete(uri, null, null);
    
                    return count > 0;
            }
    
            public LinkedList<Article> getAllArticles() {
                    LinkedList<Article> articles = new LinkedList<Article>();
    
                    String[] projection = new String[] {
                            Articles.ID,
                            Articles.TITLE,
                            Articles.ABSTRACT,
                            Articles.URL
                    };
    
                    Cursor cursor = resolver.query(Articles.CONTENT_URI, projection, null, null, Articles.DEFAULT_SORT_ORDER);
                    if (cursor.moveToFirst()) {
                            do {
                                    int id = cursor.getInt(0);
                                    String title = cursor.getString(1);
                                    String abs = cursor.getString(2);
                                    String url = cursor.getString(3);
    
                                    Article article = new Article(id, title, abs, url);
                                    articles.add(article);
                            } while(cursor.moveToNext());
                    }
    
                    return articles;
            }
    
            public int getArticleCount() {
                    int count = 0;
    
                    try {
                            IContentProvider provider = resolver.acquireProvider(Articles.CONTENT_URI);
                            Bundle bundle = provider.call(Articles.METHOD_GET_ITEM_COUNT, null, null);
                            count = bundle.getInt(Articles.KEY_ITEM_COUNT, 0);
                    } catch(RemoteException e) {
                            e.printStackTrace();
                    }
    
                    return count;
            }
    
            public Article getArticleById(int id) {
                    Uri uri = ContentUris.withAppendedId(Articles.CONTENT_URI, id);
    
                    String[] projection = new String[] {
                                    Articles.ID,
                        Articles.TITLE,
                        Articles.ABSTRACT,
                        Articles.URL
                    };
    
                    Cursor cursor = resolver.query(uri, projection, null, null, Articles.DEFAULT_SORT_ORDER);
    
                    Log.i(LOG_TAG, "cursor.moveToFirst");
    
                    if (!cursor.moveToFirst()) {
                            return null;
                    }
    
                    String title = cursor.getString(1);
                    String abs = cursor.getString(2);
                    String url = cursor.getString(3);
    
                    return new Article(id, title, abs, url);
            }
    
            public Article getArticleByPos(int pos) {
                    Uri uri = ContentUris.withAppendedId(Articles.CONTENT_POS_URI, pos);
    
                    String[] projection = new String[] {
                                    Articles.ID,
                        Articles.TITLE,
                        Articles.ABSTRACT,
                        Articles.URL
                    };
    
                    Cursor cursor = resolver.query(uri, projection, null, null, Articles.DEFAULT_SORT_ORDER);
                    if (!cursor.moveToFirst()) {
                            return null;
                    }
    
                    int id = cursor.getInt(0);
                    String title = cursor.getString(1);
                    String abs = cursor.getString(2);
                    String url = cursor.getString(3);
    
                    return new Article(id, title, abs, url);
            }
    }
             这个类首先在构造函数里面获得应用程序上下文的ContentResolver接口,然后通过就可以通过这个接口来访问ArticlesProvider中的文章信息了。成员函数insertArticle、updateArticle和removeArticle分别用来新增、更新和删除一个文章信息条目;成员函数getAllArticlese用来获取所有的文章信息;成员函数getArticleById和getArticleByPos分别根据文章的ID和位置来获得具体文章信息条目;成员函数getArticleCount直接使用ContentProvider的未公开接口call来获得文章信息条目的数量,注意,这个函数要源代码环境下编译才能通过。

            下面是程序主界面MainActivity类的实现,它实现在src/shy/luo/article/MainActivity.java文件中:

    package shy.luo.article;
    
    import shy.luo.providers.articles.Articles;
    import android.app.Activity;
    import android.content.Context;
    import android.content.Intent;
    import android.database.ContentObserver;
    import android.os.Bundle;
    import android.os.Handler;
    import android.util.Log;
    import android.view.LayoutInflater;
    import android.view.View;
    import android.view.ViewGroup;
    import android.widget.AdapterView;
    import android.widget.BaseAdapter;
    import android.widget.Button;
    import android.widget.ListView;
    import android.widget.TextView;
    
    public class MainActivity extends Activity implements View.OnClickListener, AdapterView.OnItemClickListener {
            private final static String LOG_TAG = "shy.luo.article.MainActivity";
    
            private final static int ADD_ARTICAL_ACTIVITY = 1;
            private final static int EDIT_ARTICAL_ACTIVITY = 2;
    
            private ArticlesAdapter aa = null;
            private ArticleAdapter adapter = null;
            private ArticleObserver observer = null;
    
            private ListView articleList = null;
            private Button addButton = null;
    
            @Override
            public void onCreate(Bundle savedInstanceState) {
                    super.onCreate(savedInstanceState);
                    setContentView(R.layout.main);
    
                    aa = new ArticlesAdapter(this);
    
                    articleList = (ListView)findViewById(R.id.listview_article);
                    adapter = new ArticleAdapter(this);
                    articleList.setAdapter(adapter);
                    articleList.setOnItemClickListener(this);
    
                    observer = new ArticleObserver(new Handler());
                    getContentResolver().registerContentObserver(Articles.CONTENT_URI, true, observer);
    
                    addButton = (Button)findViewById(R.id.button_add);
                    addButton.setOnClickListener(this);
    
                    Log.i(LOG_TAG, "MainActivity Created");
            }
    
            @Override
            public void onDestroy() {
                    super.onDestroy();
                    getContentResolver().unregisterContentObserver(observer);
            }
    
            @Override
            public void onClick(View v) {
                    if(v.equals(addButton)) {
                            Intent intent = new Intent(this, ArticleActivity.class);
                            startActivityForResult(intent, ADD_ARTICAL_ACTIVITY);
                    }
            }
    
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int pos, long id) {
                    Intent intent = new Intent(this, ArticleActivity.class);
    
                    Article article = aa.getArticleByPos(pos);
                    intent.putExtra(Articles.ID, article.getId());
                    intent.putExtra(Articles.TITLE, article.getTitle());
                    intent.putExtra(Articles.ABSTRACT, article.getAbstract());
                    intent.putExtra(Articles.URL, article.getUrl());
    
                    startActivityForResult(intent, EDIT_ARTICAL_ACTIVITY);
            }
    
            @Override
            public void onActivityResult(int requestCode,int resultCode, Intent data) {
                    super.onActivityResult(requestCode, resultCode, data);
    
                    switch(requestCode) {
                    case ADD_ARTICAL_ACTIVITY: {
                            if(resultCode == Activity.RESULT_OK) {
                                    String title = data.getStringExtra(Articles.TITLE);
                                    String abs = data.getStringExtra(Articles.ABSTRACT);
                                    String url = data.getStringExtra(Articles.URL);
    
                                    Article article = new Article(-1, title, abs, url);
                                    aa.insertArticle(article);
                            }
    
                            break;
                    }
    
                    case EDIT_ARTICAL_ACTIVITY: {
                            if(resultCode == Activity.RESULT_OK) {
                                    int action = data.getIntExtra(ArticleActivity.EDIT_ARTICLE_ACTION, -1);
                                    if(action == ArticleActivity.MODIFY_ARTICLE) {
                                            int id = data.getIntExtra(Articles.ID, -1);
                                            String title = data.getStringExtra(Articles.TITLE);
                                            String abs = data.getStringExtra(Articles.ABSTRACT);
                                            String url = data.getStringExtra(Articles.URL);
    
                                            Article article = new Article(id, title, abs, url);
                                            aa.updateArticle(article);
                                    } else if(action == ArticleActivity.DELETE_ARTICLE)     {
                                            int id = data.getIntExtra(Articles.ID, -1);
    
                                            aa.removeArticle(id);
                                    }
    
                            }
    
                            break;
                    }
                    }
            }
    
            private class ArticleObserver extends ContentObserver {
                    public ArticleObserver(Handler handler) {
                            super(handler);
                    }
    
                    @Override
                    public void onChange (boolean selfChange) {
                            adapter.notifyDataSetChanged();
                    }
            }
    
            private class ArticleAdapter extends BaseAdapter {
                    private LayoutInflater inflater;
    
                    public ArticleAdapter(Context context){
                              inflater = LayoutInflater.from(context);
                    }
    
                    @Override
                    public int getCount() {
                            return aa.getArticleCount();
                    }
    
                    @Override
                    public Object getItem(int pos) {
                            return aa.getArticleByPos(pos);
                    }
    
                    @Override
                    public long getItemId(int pos) {
                            return aa.getArticleByPos(pos).getId();
                    }
    
                    @Override
                    public View getView(int position, View convertView, ViewGroup parent) {
                            Article article = (Article)getItem(position);
    
                            if (convertView == null) {
                                    convertView = inflater.inflate(R.layout.item, null);
                            }
    
                            TextView titleView = (TextView)convertView.findViewById(R.id.textview_article_title);
                            titleView.setText("Title: " + article.getTitle());
    
                            TextView abstractView = (TextView)convertView.findViewById(R.id.textview_article_abstract);
                            abstractView.setText("Abstract: " + article.getAbstract());
    
                            TextView urlView = (TextView)convertView.findViewById(R.id.textview_article_url);
                            urlView.setText("URL: " + article.getUrl());
    
                            return convertView;
                    }
            }
    }

            在应用程序的主界面中,我们使用一个ListView来显示文章信息条目,这个ListView的数据源由ArticleAdapter类来提供,而ArticleAdapter类又是通过ArticlesAdapter类来获得ArticlesProvider中的文章信息的。在MainActivity的onCreate函数,我们还通过应用程序上下文的ContentResolver接口来注册了一个ArticleObserver对象来监控ArticlesProvider中的文章信息。一旦ArticlesProvider中的文章信息发生变化,就会通过ArticleAdapter类来实时更新ListView中的文章信息。

            下面是ArticleActivity类的实现,它实现在src/shy/luo/article/ArticleActivity.java文件中:

    package shy.luo.article;
    
    import shy.luo.providers.articles.Articles;
    import android.app.Activity;
    import android.content.Intent;
    import android.os.Bundle;
    import android.util.Log;
    import android.view.View;
    import android.widget.Button;
    import android.widget.EditText;
    
    public class ArticleActivity extends Activity implements View.OnClickListener {
            private final static String LOG_TAG = "shy.luo.article.ArticleActivity";
    
            public final static String EDIT_ARTICLE_ACTION = "EDIT_ARTICLE_ACTION";
            public final static int MODIFY_ARTICLE = 1;
            public final static int DELETE_ARTICLE = 2;
    
            private int articleId = -1;
    
            private EditText titleEdit = null;
            private EditText abstractEdit = null;
            private EditText urlEdit = null;
    
            private Button addButton = null;
            private Button modifyButton = null;
            private Button deleteButton = null;
            private Button cancelButton = null;
    
            @Override
            public void onCreate(Bundle savedInstanceState) {
                    super.onCreate(savedInstanceState);
                    setContentView(R.layout.article);
    
                    titleEdit = (EditText)findViewById(R.id.edit_article_title);
                    abstractEdit = (EditText)findViewById(R.id.edit_article_abstract);
                    urlEdit = (EditText)findViewById(R.id.edit_article_url);
    
                    addButton = (Button)findViewById(R.id.button_add_article);
                    addButton.setOnClickListener(this);
    
                    modifyButton = (Button)findViewById(R.id.button_modify);
                    modifyButton.setOnClickListener(this);
    
                    deleteButton = (Button)findViewById(R.id.button_delete);
                    deleteButton.setOnClickListener(this);
    
                    cancelButton = (Button)findViewById(R.id.button_cancel);
                    cancelButton.setOnClickListener(this);
    
                    Intent intent = getIntent();
                    articleId = intent.getIntExtra(Articles.ID, -1);
    
                    if(articleId != -1) {
                            String title = intent.getStringExtra(Articles.TITLE);
                            titleEdit.setText(title);
    
                            String abs = intent.getStringExtra(Articles.ABSTRACT);
                            abstractEdit.setText(abs);
    
                            String url = intent.getStringExtra(Articles.URL);
                            urlEdit.setText(url);
    
                            addButton.setVisibility(View.GONE);
                    } else {
    
                            modifyButton.setVisibility(View.GONE);
                            deleteButton.setVisibility(View.GONE);
                    }
    
                    Log.i(LOG_TAG, "ArticleActivity Created");
            }
    
            @Override
            public void onClick(View v) {
                    if(v.equals(addButton)) {
                            String title = titleEdit.getText().toString();
                            String abs = abstractEdit.getText().toString();
                            String url = urlEdit.getText().toString();
    
                            Intent result = new Intent();
                            result.putExtra(Articles.TITLE, title);
                            result.putExtra(Articles.ABSTRACT, abs);
                            result.putExtra(Articles.URL, url);
    
                            setResult(Activity.RESULT_OK, result);
                            finish();
                    } else if(v.equals(modifyButton)){
                            String title = titleEdit.getText().toString();
                            String abs = abstractEdit.getText().toString();
                            String url = urlEdit.getText().toString();
    
                            Intent result = new Intent();
                            result.putExtra(Articles.ID, articleId);
                            result.putExtra(Articles.TITLE, title);
                            result.putExtra(Articles.ABSTRACT, abs);
                            result.putExtra(Articles.URL, url);
                            result.putExtra(EDIT_ARTICLE_ACTION, MODIFY_ARTICLE);
    
                            setResult(Activity.RESULT_OK, result);
                            finish();
                    } else if(v.equals(deleteButton)) {
                            Intent result = new Intent();
                            result.putExtra(Articles.ID, articleId);
                            result.putExtra(EDIT_ARTICLE_ACTION, DELETE_ARTICLE);
    
                            setResult(Activity.RESULT_OK, result);
                            finish();
                    } else if(v.equals(cancelButton)) {
                            setResult(Activity.RESULT_CANCELED, null);
                            finish();
    
                    }
            }
    }
             在ArticleActivity窗口中,我们可以执行新增、更新和删除文章信息的操作。如果启动ArticleActivity时,没有把文章ID传进来,就说明要执行操作是新增文章信息;如果启动ArticleActivity时,把文章ID和其它信自都传进来了,就说明要执行的操作是更新或者删除文章,根据用户在界面点击的是更新按钮还是删除按钮来确定。

             程序使用到的界面文件定义在res/layout目录下,其中,main.xml文件定义MainActivity的界面,它的内容如下所示:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" 
        android:gravity="bottom">
            <ListView
                    android:id="@+id/listview_article"
                    android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_weight="1" 
                    android:background="@drawable/border"
                    android:choiceMode="singleChoice">
            </ListView>
            <LinearLayout
            android:orientation="horizontal"
            android:layout_height="wrap_content"
            android:layout_width="match_parent"
            android:gravity="center"
            android:layout_marginTop="10dp">
            <Button 
                    android:id="@+id/button_add"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:paddingLeft="15dp"
                    android:paddingRight="15dp"
                    android:text="@string/add">
            </Button>
        </LinearLayout>
    </LinearLayout>
             item.xml文件定义了ListView中每一个文章信息条目的显示界面,它的内容如下所示:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout
            xmlns:android="http://schemas.android.com/apk/res/android"
            android:orientation="vertical"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content">
            <TextView
                    android:id="@+id/textview_article_title"
                    android:layout_width="fill_parent"
                    android:layout_height="wrap_content">
            </TextView>
            <TextView
                    android:id="@+id/textview_article_abstract"
                    android:layout_width="fill_parent"
                    android:layout_height="wrap_content">
            </TextView>
        <TextView
                    android:id="@+id/textview_article_url"
                    android:layout_width="fill_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginBottom="10dp">
            </TextView>
    </LinearLayout>
             article.xml文件定义了ArticleActivity的界面,它的内容如下所示:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" 
        android:gravity="center">
        <LinearLayout
            android:orientation="horizontal"
            android:layout_height="wrap_content" 
            android:layout_width="fill_parent">
            <TextView 
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content" 
                    android:layout_marginRight="24dp"
                    android:text="@string/title">
            </TextView>
            <EditText 
                    android:id="@+id/edit_article_title"
                    android:layout_width="fill_parent"
                    android:layout_height="wrap_content">
            </EditText>
        </LinearLayout>
        <LinearLayout
            android:orientation="horizontal"
            android:layout_height="wrap_content" 
            android:layout_width="fill_parent">
            <TextView 
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content" 
                    android:text="@string/abs">
            </TextView>
            <EditText 
                    android:id="@+id/edit_article_abstract"
                    android:layout_width="fill_parent"
                    android:layout_height="wrap_content" >
            </EditText>
        </LinearLayout>
        <LinearLayout
            android:orientation="horizontal"
            android:layout_height="wrap_content" 
            android:layout_width="fill_parent">
            <TextView 
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content" 
                    android:layout_marginRight="27dp"
                    android:text="@string/url">
            </TextView>
            <EditText 
                    android:id="@+id/edit_article_url"
                    android:layout_width="fill_parent"
                    android:layout_height="wrap_content" >
            </EditText>
        </LinearLayout>
        <LinearLayout
            android:orientation="horizontal"
            android:layout_height="wrap_content" 
            android:layout_width="match_parent" 
            android:gravity="center"
            android:layout_marginTop="10dp">
            <Button 
                    android:id="@+id/button_modify"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="@string/modify">
            </Button>
            <Button 
                    android:id="@+id/button_delete"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="@string/delete">
            </Button>
            <Button 
                    android:id="@+id/button_add_article"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:paddingLeft="16dp"
                    android:paddingRight="16dp"
                    android:text="@string/add">
            </Button>
            <Button 
                    android:id="@+id/button_cancel"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="@string/cancel">
            </Button>
        </LinearLayout>
    </LinearLayout>
            在res/drawable目录下,有一个border.xml文件定义了MainActivity界面上的ListView的背景,它的内容如下所示:

    <?xml version="1.0" encoding="utf-8"?>
    <shape xmlns:android="http://schemas.android.com/apk/res/android"
            android:shape="rectangle">
            <solid android:color="#ff0000ff"/>
            <stroke android:width="1dp" 
                    android:color="#000000">
            </stroke>
            <padding android:left="7dp"  
                    android:top="7dp"
                    android:right="7dp"  
                    android:bottom="7dp">
            </padding>
            <corners android:radius="10dp" />
    </shape>
            程序使用到的字符串资源文件定义在res/values/strings.xml文件中,它的内容如下所示:

    <?xml version="1.0" encoding="utf-8"?>
    <resources>
        <string name="app_name">Article</string>
        <string name="article">Article</string>
        <string name="add">Add</string>
        <string name="modify">Modify</string>
        <string name="delete">Delete</string>
        <string name="title">Title:</string>
        <string name="abs">Abstract:</string>
        <string name="url">URL:</string>
        <string name="ok">OK</string>
        <string name="cancel">Cancel</string>
    </resources>
            接下来再来看程序的配置文件AndroidManifest.xml:

    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="shy.luo.article"
          android:versionCode="1"
          android:versionName="1.0">
        <application android:icon="@drawable/icon" android:label="@string/app_name">
            <activity android:name=".MainActivity"
                      android:label="@string/app_name">
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
            </activity>
            <activity 
                    android:name=".ArticleActivity"
                    android:label="@string/article">
            </activity>
        </application>
    </manifest>
            编译脚本Android.mk的内容如下所示:

    LOCAL_PATH:= $(call my-dir)
    include $(CLEAR_VARS)
    
    LOCAL_MODULE_TAGS := optional
    
    LOCAL_STATIC_JAVA_LIBRARIES := libArticlesProvider
    
    LOCAL_SRC_FILES := $(call all-subdir-java-files)
    
    LOCAL_PACKAGE_NAME := Article
    
    include $(BUILD_PACKAGE)
    ###################################################
    include $(CLEAR_VARS)
    
    LOCAL_PREBUILT_STATIC_JAVA_LIBRARIES := libArticlesProvider:./libs/ArticlesProvider.jar
    
    include $(BUILD_MULTI_PREBUILT)
            这个编译脚本包含了两个部分的指令,一个是把libs目录下的预编译静态库ArticlesProvider.jar编译成一本地静态库libArticlesProvider,它的相关库文件保存在out/target/common/obj/JAVA_LIBRARIES/libArticlesProvider_intermediates目录下;另一个就是编译我们的程序Article了,它通过LOCAL_STATIC_JAVA_LIBRARIES变量来引用前面的libArticlesProvider库,这个库包含了所有我们用来访问ArticlesProvider这个Content Provider中的数据的常量。

            下面我们就可以编译和打包这个应用程序了:

    USER-NAME@MACHINE-NAME:~/Android$ mmm packages/experimental/Article    
    USER-NAME@MACHINE-NAME:~/Android$ make snod 

            这样,打包好的Android系统镜像文件system.img就包含我们这里所创建的Article应用程序了。

            最后,就是运行模拟器来运行我们的例子了。关于如何在Android源代码工程中运行模拟器,请参考在Ubuntu上下载、编译和安装Android最新源代码一文。
            执行以下命令启动模拟器:

    USER-NAME@MACHINE-NAME:~/Android$ emulator  
            这个应用程序的主界面如下图所示:

             点击下面的Add按钮,可以添加新的文章信息条目:


             在前一个界面的文件列表中,点击某一个文章条目,便可以更新或者删除文章信息条目:


     
            这样,Content Provider的使用实例就介绍完了。这篇文章的目的是使读者对Content Provider有一个大概的了解和感性的认识,在下一篇文章中,我们将详细介绍Article应用程序是如何获得ArticlesProvider这个ContentProvider接口的,只有获得了这个接口之后,Article应用程序才能访问ArticlesProvider的数据,敬请关注。

    老罗的新浪微博:http://weibo.com/shengyangluo,欢迎关注!

    展开全文
  • 客户需求:应用开机启动,隐藏导航栏和下方虚拟按键,退出时需要输密确认才能退出应用 其他功能都实现了,并重载了返回键在退出应用时输密确认,但屏幕下方home键以及任务视图方形按键不能重载方法,只能做监听...
  • Android应用开发进阶与实践

    千人学习 2017-03-16 22:21:12
    本课程是Android应用开发的进阶篇,以实践为主,学习本课程要求学习者首先掌握Java基础,Android开发基础,此课程在此基础上进一步探索,此课程不适合没有Java和Android基础的人员。
  • Android | Android应用架构之MVVM模式

    万次阅读 多人点赞 2018-05-08 23:53:26
    早期的Android应用开发中,Activity/Fragment承担了过多的职责,它们不仅负责了应用界面的显示,而且负责了业务逻辑的处理。这样一来,Activity/Fragment很容易就变得臃肿、复杂,造成应用难以测试、维护和扩展。...

    前言

    早期的Android应用开发中,Activity/Fragment承担了过多的职责,它们不仅负责了应用界面的显示,而且负责了业务逻辑的处理。这样一来,Activity/Fragment很容易就变得臃肿、复杂,造成应用难以测试、维护和扩展。随着Android应用开发技术的不断发展和成熟,Android应用架构的设计得到了越来越多开发人员的关注和重视。目前,Android的应用架构主要有MVC、MVP和MVVM模式,本文将介绍一下MVVM模式。

    相关知识

    学习项目

    MVP模式

    MVVM模式可以说是MVP模式的进一步发展,所以先来了解一下MVP模式。

    MVP (Model-View-Presenter) 模式的结构如下图所示:

    MVP模式.png

    MVP模式将应用分为三层:Model层主要负责数据的提供,View层主要负责界面的显示,Presenter层主要负责业务逻辑的处理。

    在MVP模式中,Model层和View层不能直接通信,Presenter层负责充当中间人,实现Model层和View层之间的间接通信。View层和Presenter层互相持有对方的引用,实现View层和Presenter层之间的通信。

    MVP模式的主要优点是:分离了Model层和View层,分离了视图操作和业务逻辑,降低了耦合。

    MVVM模式

    MVVM (Model-View-ViewModel) 模式的结构如下图所示:

    MVVM模式.png

    MVVM模式与MVP模式一样,也将应用分为三层,并且各个对应的层的职责相似:

    • Model层,主要负责数据的提供。Model层提供业务逻辑的数据结构(比如,实体类),提供数据的获取(比如,从本地数据库或者远程网络获取数据),提供数据的存储。
    • View层,主要负责界面的显示。View层不涉及任何的业务逻辑处理,它持有ViewModel层的引用,当需要进行业务逻辑处理时通知ViewModel层。
    • ViewModel层,主要负责业务逻辑的处理。ViewModel层不涉及任何的视图操作。通过官方提供的Data Binding库,View层和ViewModel层中的数据可以实现绑定,ViewModel层中数据的变化可以自动通知View层进行更新,因此ViewModel层不需要持有View层的引用。ViewModel层可以看作是View层的数据模型和Presenter层的结合。

    MVVM模式与MVP模式最大的区别在于:ViewModel层不持有View层的引用。这样进一步降低了耦合,View层代码的改变不会影响到ViewModel层。

    MVVM模式相对于MVP模式主要有如下优点:

    • 进一步降低了耦合。ViewModel层不持有View层的引用,当View层发生改变时,只要View层绑定的数据不变,那么ViewModel层就不需要改变。而在MVP模式下,当View层发生改变时,操作视图的接口就要进行相应的改变,那么Presenter层就需要修改了。
    • 不用再编写很多样板代码。通过官方的Data Binding库,UI和数据之间可以实现绑定,不用再编写大量的findViewById()和操作视图的代码了。总之,Activity/Fragment的代码可以做到相当简洁。

    例子

    下面举一个简单的例子来实践MVVM模式。完整的项目代码可以去GitHub上查看:

    https://github.com/chongyucaiyan/MVVMDemo

    例子实现的主要功能是:点击按钮网络查询天气,查询成功后在界面上显示天气信息。主界面如下图所示:

    MVVMDemo界面.png

    MVVM模式的代码组织结构建议按照 业务功能 进行划分,具体操作是:每个业务功能独立一个包存放,每个业务功能包下面再按Model、View、ViewModel分包存放。所有的Model存放在model包下面,所有的Activity和Fragment存放在activity包下面,所有的ViewModel存放在viewmodel包下面。该例子比较简单,只有一个weather业务功能模块,最终的代码组织结构如下图所示:

    MVVMDemo代码组织结构.png

    编写Model

    查询杭州天气的URL为:

    http://www.weather.com.cn/data/cityinfo/101210101.html

    访问该URL将返回一串JSON字符串,如下所示:

    {"weatherinfo":{"city":"杭州","cityid":"101210101","temp1":"5℃","temp2":"20℃","weather":"晴转多云","img1":"n0.gif","img2":"d1.gif","ptime":"18:00"}}
    

    按照此JSON字符串,可以编写相应的实体类。WeatherData类的代码如下所示:

    public class WeatherData {
    
        private WeatherInfo weatherinfo;
    
        public WeatherInfo getWeatherinfo() {
            return weatherinfo;
        }
    
        public void setWeatherinfo(WeatherInfo weatherinfo) {
            this.weatherinfo = weatherinfo;
        }
    }
    

    WeatherInfo类的代码如下所示:

    public class WeatherInfo {
    
        private String city;
    
        private String cityid;
    
        private String temp1;
    
        private String temp2;
    
        private String weather;
    
        private String img1;
    
        private String img2;
    
        private String ptime;
    
        public String getCity() {
            return city;
        }
    
        public void setCity(String city) {
            this.city = city;
        }
    
        public String getCityid() {
            return cityid;
        }
    
        public void setCityid(String cityid) {
            this.cityid = cityid;
        }
    
        public String getTemp1() {
            return temp1;
        }
    
        public void setTemp1(String temp1) {
            this.temp1 = temp1;
        }
    
        public String getTemp2() {
            return temp2;
        }
    
        public void setTemp2(String temp2) {
            this.temp2 = temp2;
        }
    
        public String getWeather() {
            return weather;
        }
    
        public void setWeather(String weather) {
            this.weather = weather;
        }
    
        public String getImg1() {
            return img1;
        }
    
        public void setImg1(String img1) {
            this.img1 = img1;
        }
    
        public String getImg2() {
            return img2;
        }
    
        public void setImg2(String img2) {
            this.img2 = img2;
        }
    
        public String getPtime() {
            return ptime;
        }
    
        public void setPtime(String ptime) {
            this.ptime = ptime;
        }
    }
    

    编写ViewModel

    ViewModel不涉及任何的视图操作,只进行业务逻辑的处理。通过官方提供的Data Binding库,当ViewModel中的数据发生变化时,UI将自动更新。QueryWeatherViewModel的代码如下所示:

    public class QueryWeatherViewModel {
    
        private static final String TAG = "QueryWeatherViewModel";
    
        public final ObservableBoolean loading = new ObservableBoolean(false);
    
        public final ObservableBoolean loadingSuccess = new ObservableBoolean(false);
    
        public final ObservableBoolean loadingFailure = new ObservableBoolean(false);
    
        public final ObservableField<String> city = new ObservableField<>();
    
        public final ObservableField<String> cityId = new ObservableField<>();
    
        public final ObservableField<String> temp1 = new ObservableField<>();
    
        public final ObservableField<String> temp2 = new ObservableField<>();
    
        public final ObservableField<String> weather = new ObservableField<>();
    
        public final ObservableField<String> time = new ObservableField<>();
    
        private Call<WeatherData> mCall;
    
        public QueryWeatherViewModel() {
    
        }
    
        public void queryWeather() {
            loading.set(true);
            loadingSuccess.set(false);
            loadingFailure.set(false);
    
            mCall = RetrofitManager.get()
                    .create(QueryWeatherRequest.class)
                    .queryWeather();
            mCall.enqueue(new Callback<WeatherData>() {
    
                @Override
                public void onResponse(Call<WeatherData> call, Response<WeatherData> response) {
                    WeatherInfo weatherInfo = response.body().getWeatherinfo();
                    city.set(weatherInfo.getCity());
                    cityId.set(weatherInfo.getCityid());
                    temp1.set(weatherInfo.getTemp1());
                    temp2.set(weatherInfo.getTemp2());
                    weather.set(weatherInfo.getWeather());
                    time.set(weatherInfo.getPtime());
    
                    loading.set(false);
                    loadingSuccess.set(true);
                }
    
                @Override
                public void onFailure(Call<WeatherData> call, Throwable t) {
                    if (call.isCanceled()) {
                        Log.i(TAG, "call is canceled.");
                    } else {
                        loading.set(false);
                        loadingFailure.set(true);
                    }
                }
            });
        }
    
        public void cancelRequest() {
            if (mCall != null) {
                mCall.cancel();
            }
        }
    }
    

    编写View

    View不涉及任何的业务逻辑处理,只进行界面的显示。在xml布局文件中,通过官方提供的Data Binding库,将UI与ViewModel中的数据进行绑定,当ViewModel中的数据发生变化时,UI将自动更新。xml布局文件的代码如下所示:

    <?xml version="1.0" encoding="utf-8"?>
    <layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools">
    
        <data>
    
            <import type="android.view.View" />
    
            <variable
                name="viewModel"
                type="com.github.cyc.mvvmdemo.weather.viewmodel.QueryWeatherViewModel" />
        </data>
    
        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:padding="@dimen/default_content_padding"
            tools:context="com.github.cyc.mvvmdemo.weather.activity.QueryWeatherActivity">
    
            <Button
                android:id="@+id/btn_query_weather"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerHorizontal="true"
                android:text="@string/query_weather"
                android:enabled="@{viewModel.loading ? false : true}"
                android:onClick="@{() -> viewModel.queryWeather()}" />
    
            <RelativeLayout
                android:id="@+id/vg_weather_info"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_below="@id/btn_query_weather"
                android:layout_marginTop="@dimen/query_weather_margin"
                android:visibility="@{viewModel.loadingSuccess ? View.VISIBLE : View.GONE}">
    
                <TextView
                    android:id="@+id/tv_city"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:textStyle="bold"
                    android:text="@string/city" />
    
                <TextView
                    android:id="@+id/tv_city_value"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_toRightOf="@id/tv_city"
                    android:layout_alignBottom="@id/tv_city"
                    android:text="@{viewModel.city}"
                    tools:text="杭州" />
    
                <TextView
                    android:id="@+id/tv_city_id"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_below="@id/tv_city"
                    android:layout_marginTop="@dimen/query_weather_margin"
                    android:textStyle="bold"
                    android:text="@string/city_id" />
    
                <TextView
                    android:id="@+id/tv_city_id_value"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_toRightOf="@id/tv_city_id"
                    android:layout_alignBottom="@id/tv_city_id"
                    android:text="@{viewModel.cityId}"
                    tools:text="101210101" />
    
                <TextView
                    android:id="@+id/tv_temp"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_below="@id/tv_city_id"
                    android:layout_marginTop="@dimen/query_weather_margin"
                    android:textStyle="bold"
                    android:text="@string/temperature" />
    
                <TextView
                    android:id="@+id/tv_temp1_value"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_toRightOf="@id/tv_temp"
                    android:layout_alignBottom="@id/tv_temp"
                    android:text="@{viewModel.temp1}"
                    tools:text="5℃" />
    
                <TextView
                    android:id="@+id/tv_tilde"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_toRightOf="@id/tv_temp1_value"
                    android:layout_alignBottom="@id/tv_temp"
                    android:text="@string/tilde" />
    
                <TextView
                    android:id="@+id/tv_temp2_value"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_toRightOf="@id/tv_tilde"
                    android:layout_alignBottom="@id/tv_temp"
                    android:text="@{viewModel.temp2}"
                    tools:text="10℃" />
    
                <TextView
                    android:id="@+id/tv_weather"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_below="@id/tv_temp"
                    android:layout_marginTop="@dimen/query_weather_margin"
                    android:textStyle="bold"
                    android:text="@string/weather" />
    
                <TextView
                    android:id="@+id/tv_weather_value"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_toRightOf="@id/tv_weather"
                    android:layout_alignBottom="@id/tv_weather"
                    android:text="@{viewModel.weather}"
                    tools:text="" />
    
                <TextView
                    android:id="@+id/tv_time"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_below="@id/tv_weather"
                    android:layout_marginTop="@dimen/query_weather_margin"
                    android:textStyle="bold"
                    android:text="@string/release_time" />
    
                <TextView
                    android:id="@+id/tv_time_value"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_toRightOf="@id/tv_time"
                    android:layout_alignBottom="@id/tv_time"
                    android:text="@{viewModel.time}"
                    tools:text="10:00" />
            </RelativeLayout>
    
            <ProgressBar
                android:id="@+id/pb_progress"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerInParent="true"
                android:visibility="@{viewModel.loading ? View.VISIBLE : View.GONE}" />
    
            <TextView
                android:id="@+id/tv_query_failure"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerInParent="true"
                android:text="@string/query_failure"
                android:visibility="@{viewModel.loadingFailure ? View.VISIBLE : View.GONE}" />
        </RelativeLayout>
    </layout>
    

    在Activity中,通过官方提供的Data Binding库加载布局文件,创建ViewModel,并绑定View和ViewModel。QueryWeatherActivity的代码如下所示:

    public class QueryWeatherActivity extends AppCompatActivity {
    
        // ViewModel
        private QueryWeatherViewModel mViewModel;
    
        // DataBinding
        private ActivityQueryWeatherBinding mDataBinding;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            mDataBinding = DataBindingUtil.setContentView(this, R.layout.activity_query_weather);
            // 创建ViewModel
            mViewModel = new QueryWeatherViewModel();
            // 绑定View和ViewModel
            mDataBinding.setViewModel(mViewModel);
        }
    
        @Override
        protected void onDestroy() {
            super.onDestroy();
            // 取消请求
            mViewModel.cancelRequest();
        }
    }
    

    总结

    MVVM模式将应用分为三层:Model层主要负责数据的提供,View层主要负责界面的显示,ViewModel层主要负责业务逻辑的处理。各个层职责单一,结构清晰,应用可以很方便地进行测试、维护和扩展。

    参考

    展开全文
  • Android应用程序安装过程源代码分析

    万次阅读 多人点赞 2011-09-14 00:59:22
    Android系统在启动的过程中,会启动一个应用程序管理服务PackageManagerService,这个服务负责扫描系统中特定的目录,找到里面的应用程序文件,即以Apk为后缀的文件,然后对这些文件进解析,得到应用程序的相关信息...
  • Android系统中,Activity和Service是应用程序的核心组件,它们以松藕合的方式组合在一起构成了一个完整的应用程序,这得益于应用程序框架层提供了一套完整的机制来协助应用程序启动这些Activity和Service,以及...
  • 由CSDN、创新工场联合主办的MDCC 移动开发者大会...大会以移动领域热点技术+典型应用案例实战为主的内容主旨,增设移动直播、VR开发等热点技术实战专场,满足移动开发者的新需求,以促进移动开发领域的分享和交流。
  • Android 应用程序签名

    千次阅读 2012-09-04 13:39:44
    Android应用程序签名相关的理论知识包括:什么是签名、为什么要给应用程序签名、如何给应用程序签名等。1、什么是签名? 如果这个问题不是放在Android开发中来问,如果是放在一个普通的版块,我想大家都知道签名的...
  • 前言 为了方便大家的阅读以及自己的知识体系的建立,特意来写出这个引导文章。以前我是遇到什么写什么,想到什么写什么,从2016年开始我将围绕这个知识体系来写...1.Android应用层 Android网络编程(完结)
  • SurfaceFlinger服务负责绘制Android应用程序的UI,它的实现相当复杂,要从正面分析它的实现不是一件容易的事。既然不能从正面分析,我们就想办法从侧面分析。说到底,无论SurfaceFlinger服务有多复杂,它都是为...
  • android应用中有一个webView。 下面代码中我用toast显示加载进度: webView.setWebChromeClient(new WebChromeClient() { @Override public void onProgressChanged(WebView view, int progress) { ...
  • Android应用程序进程启动过程的源代码分析

    万次阅读 多人点赞 2011-09-09 01:01:32
    Android应用程序框架层创建的应用程序进程具有两个特点,一是进程的入口函数是ActivityThread.main,二是进程天然支持Binder进程间通信机制;这两个特点都是在进程的初始化过程中实现的,本文将详细分析Android应用...
  • Android 应用开发】GitHub 优秀的 Android 开源项目

    万次阅读 热门讨论 2014-01-09 17:35:15
    文章转载自 : ... : Elysee2014 主要介绍那些不错个性化的View,包括ListView、ActionBar、Menu、ViewPager、Gallery、GridView、ImageView、ProgressBar及其他如Dialog、Toast、Ed
  • 使用Kotlin开发Android应用

    万次阅读 2016-02-17 16:55:17
    Kotlin是一门基于JVM的编程语言,它正成长为Android开发中用于替代Java语言的继承者。Java是世界上使用最多的编程语言之一,当其他编程语言为更加便于开发者使用而不断进化时,Java并没有像预期那样及时跟进。  ...

空空如也

1 2 3 4 5 ... 20
收藏数 227,640
精华内容 91,056
热门标签
关键字:

android应用