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

《Android系统源代码情景分析》连载回忆录:灵感之源

$
0
0

       上个月,在花了一年半时间之后,写了55篇文章,分析完成了Chromium在Android上的实现,以及Android基于Chromium实现的WebView。学到了很多东西,不过也挺累的,平均不到两个星期一篇文章。本来想休息一段时间后,再继续分析Chromium使用的JS引擎V8。不过某天晚上,躺在床上睡不着,鬼使神差想着去创建一个个人站点,用来连载《Android系统源代码情景分析》一书的内容。

       事情是这样的,躺在床上睡不着,就去申请了一个域名,0xcc0xcd.com。域名申请到了,总不能不用吧。用来做什么呢?想起我写的那本书《Android系统源代码情景分析》,从2012年10月出版至今,也有四年多的时间了,得到了大家的厚受。不过网络上也逐渐的出现了一些盗版PDF。不用说,质量肯定很差。干脆我把这本书的内容在我的个人站点上放出来吧。后面征得了出版社的同意,就着手开始干了。

       网站名称为“进击的程序员”,主要是为了配合0xcc0xcd.com这个域名。从Windows时代过来的老司机可能一眼就能看出这个域名是什么意思。看不懂的,如果大家有兴趣,后面我也可以详细说说,怀念一下逝去的青春。

       从开始有想法,到把网站建好,以及将书前三章(准备知识、硬件抽象层、智能指针)的内容放上去,花了不到一个月的时间。在这不到一个月的时间里,学习到了挺多东西:申请域名、云服务器、域名解析、域名邮箱、网站备案以及开发网站等等。因为我一直都是做客户端开发,刚毕业几年做的是Windows客户端,后面做的是Android端,没有做过网站相关的开发,包含前端和后端,所以学习过程还是有些小波折。不过总体上来说还是比较顺利的。这也跟网站的技术选型有关吧。

       现在不是提倡做全栈工程师吗?这个建站过程也算是小小地实践了一把。怕时间久了会忘记一些关键细节和踩过的坑,所以就计划把建站连载书的过程记录下来。也希望能够帮助到有兴趣做全栈工程师的同学们。

       网站使用的是LNMP架构,如下图1所示:


图1 进击的程序员网站架构

       网站运行在云服务器上,系统装的是Ubuntu 14.04,除了Nginx、PHP和MySQL,还搭了一个GIT仓库,用来管理网站源码。这个GIT仓库除了用来管理网站源码,还用来将源码分布到网站中去。

       具体是这样的,在本地用自己的电脑开发网站(其实就是用vim编辑网页和PHP)。测试没有问题之后,就用git push命令将源码上传到GIT仓库。然后再登录到云服务器上,在网站根目录用git pull命令从GIT仓库中获得最新网站源码。

       此外,在本地还搭建了一个管理后台。这个管理后台就是用来给管理员管理网站的。主要就是操作一下数据库,例如查看数据、插入数据、更新数据等等。正规的网站会专门提供一些页面供管理员操作。鉴于这个网站不是很正规,管理员又是一个技术控,于是就直接使用Python脚本来实现这个管理后台了,想要什么功能就直接写个脚本。

      Oracle提供了一个Python版的MySQL数据库驱动库MySQL Connector/Python,通过它很容易用Python脚本操作MySQL中的数据。这样一个简单的管理后台就搭建起来了。

      整个网站的架构非常简单,可以非常快上手,同时它又五脏俱全。网站的前端主要用Ajax、jQuery开发,后端没有用什么高大尚的框架,基本上是徒手写的PHP。主要是考虑这个网站要做的事情很简单,就是连载《Android系统源代码情景分析》的内容,基本功能就是浏览和评论。所以就以最简单最快的方式实现。

      为了让大家利用碎片时间更好地阅读书的内容,网站在提供PC版的同时,也提供了移动版。移动版和PC版的功能是一样的,只是它们的页面表现形式不一样。所以网站在设计之初,就考虑了模块化和代码复用,用最小的成本获得同时实现PC端和移动端的功能。

      不知道为什么,说起PHP, 总是会想起“PHP是最好的语言”这句话。从这一个月的经历看,PHP是不是最好的语言不知道,但是用来建网站,PHP的确是最好的语言。用PHP和JS开发网站,效率比用Java/OC开发App,高多了。不过,网站的体验不如App。所以移动开发目前还是王道。

       接下来,我会用一个系列的文章分享整个建站过程,包括:

       1. 域名、云服务器、域名解析、网站备案、域名邮箱、CA证书申请

       2. LNMP开发环境搭建,包括如何配置SSL加密的HTTPS站点

       3. 支持SSH访问的GIT仓库搭建

       4. 网站基本功能开发,PC版和移动版代码复用

       5. 基于MySQL Connector/Python的管理后台开发

       欢迎大家关注!想在线阅读《Android系统源代码情景分析》一书的,点击进击的程序员进入!

作者:Luoshengyang 发表于2017/1/10 22:42:11 原文链接
阅读:40253 评论:22 查看评论

Android踩坑日记:Android字体属性及测量(FontMetrics)

$
0
0

Android字体属性及测量(FontMetrics)

  • 字体的几个参数,以Android API文档定义为尊,见下图

要点如下:

  1. 基准点是baseline
  2. Ascent是baseline之上至字符最高处的距离
  3. Descent是baseline之下至字符最低处的距离
  4. Leading文档说的很含糊,其实是上一行字符的descent到下一行的ascent之间的距离
  5. Top指的是指的是最高字符到baseline的值,即ascent的最大值
  6. bottom指的是最下字符到baseline的值,即descent的最大值
为了帮助理解,我特此搜索了不同的示意图。对照示意图,会很容易理解FontMetrics的参数。

图1

图2

图3

图4

图5

图6

  • 测试
    我们使用自定义的View和Textview里的字符串作为研究对象
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#000000"
    android:orientation="vertical"
    >
    <!--PaintView画字体-->
   <video.ketu.com.fontmeasure.PaintView
       android:id="@+id/v_fontview"
       android:layout_width="match_parent"
       android:layout_height="1dp"
       android:layout_weight="1"
       />
   <!--Textview设置文字-->
   <TextView
       android:id="@+id/tv_fontview1"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:text="中国话fgiqÃÇŸŒú"
       android:textColor="@android:color/white"
       android:textSize="80px"
       android:layout_weight="1"/>
</LinearLayout>

MainActivity.class

public class MainActivity extends AppCompatActivity {
    TextView textView;
    View paintView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        paintView = findViewById(R.id.v_fontview);
        textView = (TextView) findViewById(R.id.tv_fontview1);
        //String text = "中国话fgiqÃÇŸŒúcqazweyghnhgd;lc,kjssnhjjomoomcod";
        String text = "中国话fgiqÃÇŸŒú";
        /*设置字体带下80px*/
        textView.setTextSize(TypedValue.COMPLEX_UNIT_PX,80);
        textView.setText(text);
        Paint.FontMetrics fontMetrics = textView.getPaint().getFontMetrics();
        Log.d("textView", "fontMetrics.top        is:" + fontMetrics.top);
        Log.d("textView", "fontMetrics.ascent     is:" + fontMetrics.ascent);
        Log.d("textView", "fontMetrics.descent    is:" + fontMetrics.descent);
        Log.d("textView", "fontMetrics.bottom     is:" + fontMetrics.bottom);
        Log.d("textView", "fontMetrics.leading    is:" + fontMetrics.leading);
    }
}

PaintView.class

public class PaintView extends View {
    private Paint mPaint = new Paint();
    public PaintView(Context context) {
        super(context);
    }
    public PaintView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    public PaintView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }
    @Override
    protected void onDraw(Canvas canvas) {
        // TODO Auto-generated method stub
        mPaint.reset();
        mPaint.setColor(Color.WHITE);
        /*设置字体带下80px*/
        mPaint.setTextSize(80);
        //设置字体为斜体
        //mPaint.setTypeface(Typeface.create("", Typeface.ITALIC));
        // FontMetrics对象
        Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
        String text = "中国话fgiqÃÇŸŒú";
        // 计算每一个坐标
        float textWidth = mPaint.measureText(text);
        float baseX = 0;
        float baseY = 100;
        float topY = baseY + fontMetrics.top;
        float ascentY = baseY + fontMetrics.ascent;
        float descentY = baseY + fontMetrics.descent;
        float bottomY = baseY + fontMetrics.bottom;
        float leading = baseY + fontMetrics.leading;

        Log.d("paintview", "baseX    is:" + baseX);
        Log.d("paintview", "baseY    is:" + baseY);

        Log.d("paintview", "fontMetrics.top        is:" + fontMetrics.top);
        Log.d("paintview", "fontMetrics.ascent     is:" + fontMetrics.ascent);
        Log.d("paintview", "fontMetrics.descent    is:" + fontMetrics.descent);
        Log.d("paintview", "fontMetrics.bottom     is:" + fontMetrics.bottom);
        Log.d("paintview", "fontMetrics.leading    is:" + fontMetrics.leading);

        Log.d("paintview", "topY     is:" + topY);
        Log.d("paintview", "ascentY  is:" + ascentY);
        Log.d("paintview", "descentY is:" + descentY);
        Log.d("paintview", "bottomY  is:" + bottomY);

截面图


PaintView和TextView设置的字体大小都是80px,Log打印结果

PaintView结果

TextView结果


      Note 1:以上可见,字体属性类的FontMetrics类的top,ascent,descent,bottom,leading的值是正负数,是以基线baseline为0的相对值。当baseline是100时,各个参数的坐标就是正数。
所以对于文本框的文字的行高:fontMetrics.top-fontMetrics.bottom

      Note 2:以上的TextView的文本长度是单行,获得leading=0,那么如果TextView的文本是多行,并且设置行间距后,leading的变化

public class MainActivity extends AppCompatActivity {
    TextView textView;
    View paintView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        paintView = findViewById(R.id.v_fontview);
        textView = (TextView) findViewById(R.id.tv_fontview1);
        String text = "中国话fgiqÃÇŸŒúcqazweyghnhgd;lc,kjssnhjjomoomcod";
        //String text = "中国话fgiqÃÇŸŒú";
        /*设置字体带下80px*/
        textView.setTextSize(TypedValue.COMPLEX_UNIT_PX,80);
        textView.setText(text);

        Paint.FontMetrics fontMetrics = textView.getPaint().getFontMetrics();

        Log.d("textView", "fontMetrics.top        is:" + fontMetrics.top);
        Log.d("textView", "fontMetrics.ascent     is:" + fontMetrics.ascent);
        Log.d("textView", "fontMetrics.descent    is:" + fontMetrics.descent);
        Log.d("textView", "fontMetrics.bottom     is:" + fontMetrics.bottom);
        Log.d("textView", "fontMetrics.leading    is:" + fontMetrics.leading);
    }
<TextView
       android:id="@+id/tv_fontview1"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:text="中国话fgiqÃÇŸŒúcqazweyghnhgd;lc,kjssnhjjomoomcod"
       android:textColor="@android:color/white"
       android:lineSpacingExtra="5px"
       android:textSize="80px"
       android:layout_weight="1"/>


截面图

结果Log

Note:显然,即使是多行,并且设置5px行距的情况下,仍不能获得leading。

作者:tuke_tuke 发表于2017/9/20 14:17:45 原文链接
阅读:246 评论:0 查看评论

Android踩坑日记:Okhttp设置User-Agent你可能没遇到的坑

$
0
0

Okhttp设置User-Agent你可能没遇到的坑

  • Http Header之User-Agent
       User-Agent中文名为用户代理,是Http协议中的一部分,属于头域的组成部分,User Agent页简称UA。她是一个特殊字符串头,是一种想访问网站提供你说使用的浏览器类型和版本,操作系统和版本,浏览器内核等信息的标识,用户所访问的网站可以显示不同的排版,而为用户提供更好的体验或者进行信息统计

  • 获取OkHttp正确的User-Agent

   Okhttp走的并不是原生的http请求,因此他在header里面并没有真正的User-Agent,而是”okhttp/版本号”这样的字符串,因此后台需要统计信息,要求传入正确的User-Agent,那么我们如何User-Agent并设置给Okhttp?

    /**
     * 返回正确的UserAgent
     * @return
     */
    private  static String getUserAgent(){
        String userAgent = "";
        StringBuffer sb = new StringBuffer();
        userAgent = System.getProperty("http.agent");//Dalvik/2.1.0 (Linux; U; Android 6.0.1; vivo X9L Build/MMB29M)

        for (int i = 0, length = userAgent.length(); i < length; i++) {
            char c = userAgent.charAt(i);
            if (c <= '\u001f' || c >= '\u007f') {
                sb.append(String.format("\\u%04x", (int) c));
            } else {
                sb.append(c);
            }
        }

        LogUtils.v("User-Agent","User-Agent: "+ sb.toString());
        return sb.toString();
    }
  • 给Okhttp设置User-Agent:
new Request.Builder().url(url).headers(headers2).put(body).removeHeader("User-Agent").addHeader("User-Agent",getUserAgent()).build();
作者:tuke_tuke 发表于2017/9/20 14:45:32 原文链接
阅读:264 评论:0 查看评论

Android踩坑日记:自定义水平和圆形ProgressBar样式

$
0
0

自定义水平和圆形ProgressBar样式

1.自定义水平ProgressBar样式

  • ProgressBar分为两种,我们能明确看到进度,不确定的就是不清楚、不确定一个操作需要多长时间来完成,这个时候就需要用的不确定的ProgressBar了。
  • ProgressBar(Horizontal 才有,无进度的没有)有两个进度,一个是android:progress,另一个是android:secondaryProgress。后者主要是为缓存需要所涉及的,比如在看网络视频时候都会有一个缓存的进度条以及还要一个播放的进度,在这里缓存的进度就可以是android:secondaryProgress,而播放进度就是android:progress,有了secondProgress,可以很方便定制ProgressBar。

1.ProgressBar的样式设定其实有两种方式,在API文档中说明的方式如下:

  • Widget.ProgressBar.Horizontal
  • Widget.ProgressBar.Small
  • Widget.ProgressBar.Large
  • Widget.ProgressBar.Inverse
  • Widget.ProgressBar.Small.Inverse
  • Widget.ProgressBar.Large.Inverse


使用的时候可以这样:style=”@android:style/Widget.ProgressBar.Horizontal”
另外还有一种方式就是使用系统的attr,下面的方式是系统的style:

  • style=”?android:attr/progressBarStyle”
  • style=”?android:attr/progressBarStyleHorizontal”
  • style=”?android:attr/progressBarStyleInverse”
  • style=”?android:attr/progressBarStyleLarge”
  • style=”?android:attr/progressBarStyleLargeInverse”
  • style=”?android:attr/progressBarStyleSmall”
  • style=”?android:attr/progressBarStyleSmallInverse”
  • style=”?android:attr/progressBarStyleSmallTitle”

比如:

<ProgressBar
    android:id="@+id/progressBar"
    style="@android:style/Widget.ProgressBar.Horizontal"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />
ProgressBar
    android:id="@+id/progressBar"
    style="?android:attr/progressBarStyleHorizontal"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />

我们去看看style=”@android:style/Widget.ProgressBar.Horizontal” 的源码

 <style name="Widget.ProgressBar.Horizontal">
        <item name="indeterminateOnly">false</item>
        <!-- 进度条的背景,progress ,secondaryProgress 的颜色-->
        <item name="progressDrawable">@drawable/progress_horizontal</item>
        <item name="indeterminateDrawable">@drawable/progress_indeterminate_horizontal</item>
        <item name="minHeight">20dip</item>
        <item name="maxHeight">20dip</item>
        <item name="mirrorForRtl">true</item>
    </style>

下面看@android:drawable/progress_horizontal的源码

<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2008 The Android Open Source Project

     Licensed under the Apache License, Version 2.0 (the "License");
     you may not use this file except in compliance with the License.
     You may obtain a copy of the License at

          http://www.apache.org/licenses/LICENSE-2.0

     Unless required by applicable law or agreed to in writing, software
     distributed under the License is distributed on an "AS IS" BASIS,
     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     See the License for the specific language governing permissions and
     limitations under the License.
-->

<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <!--背景色-->
    <item android:id="@android:id/background">
        <shape>
            <corners android:radius="5dip" />
            <gradient
                    android:startColor="#ff9d9e9d"
                    android:centerColor="#ff5a5d5a"
                    android:centerY="0.75"
                    android:endColor="#ff747674"
                    android:angle="270"
            />
        </shape>
    </item>
      <!--第二进度条颜色-->
    <item android:id="@android:id/secondaryProgress">
        <clip>
            <shape>
                <corners android:radius="5dip" />
                <gradient
                        android:startColor="#80ffd300"
                        android:centerColor="#80ffb600"
                        android:centerY="0.75"
                        android:endColor="#a0ffcb00"
                        android:angle="270"
                />
            </shape>
        </clip>
    </item>
      <!--第一进度条颜色-->
    <item android:id="@android:id/progress">
        <clip>
            <shape>
                <corners android:radius="5dip" />
                <gradient
                        android:startColor="#ffffd300"
                        android:centerColor="#ffffb600"
                        android:centerY="0.75"
                        android:endColor="#ffffcb00"
                        android:angle="270"
                />
            </shape>
        </clip>
    </item>   
</layer-list>

三个条目,对应了background,progress,secondaryProgress。所以只需要修改对应项就可以了,如下在drawable文件中创建一个 progressbar_horizontal_custom.xml
所以其实我们要修改样式的话只需要修改android:progressDrawable”对应的资源就可以

<item name="android:progressDrawable">@drawable/progressbar_horizontal</item><!-- progress_horizontal -->

video_view_bottom_progressbar_background.xml

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">

    <item android:id="@android:id/background"
        android:height="2dp"
        android:gravity="center">
        <shape>
            <size android:height="2dp"/>
            <corners android:radius="1dp" />
            <solid android:color="#dcdcdc"/>

        </shape>
    </item>

    <item android:id="@android:id/secondaryProgress"
        android:height="2dp"
        android:gravity="center">
        <clip >
            <shape>
                <size android:height="2dp"/>
                <corners android:radius="5dp" />
                <solid android:color="@android:color/darker_gray"/><!--darker_gray-->
            </shape>
        </clip>
    </item>

    <item android:id="@android:id/progress"
        android:height="2dp"
        android:gravity="center">
        <clip >
            <shape>
                <size android:height="2dp"/>
                <corners android:radius="5dp" />
                <solid android:color="#FF455B"/>
            </shape>
        </clip>
    </item>
</layer-list>

在布局文件xml中使用:

<ProgressBar
        android:id="@+id/pb_video_bottom_progress"
        style="@android:style/Widget.ProgressBar.Horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        android:max="100"
        android:maxHeight="2dp"
        android:minHeight="2dp"
        android:progressDrawable="@drawable/video_view_bottom_progressbar_background"
        android:visibility="visible" />

总结:

1. 使用style=”@android:style/Widget.ProgressBar.Horizontal”
2. 重新自自定android:progressDrawable

2.自定义圆形ProgressBar样式


比如

<ProgressBar
    android:id="@+id/progressBar"
    style="@android:style/Widget.ProgressBar.Small"
    android:layout_gravity="center_vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />

我们先看Widget.Progress的源码:

<style name="Widget.ProgressBar.Small">
        <!--圆形背景-->
        <item name="indeterminateDrawable">@drawable/progress_small_white</item>
        <item name="minWidth">16dip</item>
        <item name="maxWidth">16dip</item>
        <item name="minHeight">16dip</item>
        <item name="maxHeight">16dip</item>
    </style>

@drawable/progress_small_white的源码:

<animated-rotate 
   xmlns:android="http://schemas.android.com/apk/res/android"
   android:drawable="@drawable/spinner_white_48"
   android:pivotX="50%"
   android:pivotY="50%"
   android:framesCount="12"
   android:frameDuration="100" />

发现其实就是一个不断重复的旋转动画是spinner_white_48的图片,所以我们只需要自定义name=”indeterminateDrawable”的属性,自定义自己的drawable使用animated-rotate 标签即可
总结:
1.修改indeterminateDrawable属性,编写animated-rotate 标签的drawable,替换系统的

作者:tuke_tuke 发表于2017/9/20 16:02:55 原文链接
阅读:243 评论:0 查看评论

Xcode 9.0在代码中任意键盘敲击不停build的解决

$
0
0

原来的项目在Xcode 8.3.3下行为正常,不过今天用Xcode 9.0打开后噩梦开始了,在代码中只要输入任何文字,哪怕是注释,Xcode都会立马编译项目,还顺带编译storyboard.这样一来结果就是:卡成狗了!

尝试重启Xcode,Mac均无效,难道要退回Xcode 8.3.3去?

经过一番搜索,原因却是出乎寻常的简单:在Storyboard里开启了自动刷新,并且你在某个视图中使用了IB_DESIGNABLE特性.

解决很简单,只要官关闭Storyboard中的自动刷新视图选项即可,不过在原来的Xcode 8.3.3里该选项貌似也是打开的,但并没有这个问题.

如果需要在IB中即时看到IB_DESIGNABLE的效果,你可以临时开启这一选项或手动刷新视图.

作者:mydo 发表于2017/9/20 16:53:16 原文链接
阅读:409 评论:0 查看评论

Android图形处理--PorterDuff.Mode那些事儿

$
0
0

我们在绘制图形图像的时候经常会用到 PorterDuff.Mode,它对我们绘制图形有很大的帮助,如果我们对它不甚了解甚至根本不理解,那会是很麻烦的事情,我这篇博客就是来给大家介绍一下 PorterDuff.Mode。

1、基本介绍

在介绍 PorterDuff.Mode 之前,我们首先要了解一下 Xfermode。Xfermode 被许多人称为过渡模式,就是指图像的饱和度、颜色值等参数的计算结果的图像表现。Xfermode 是用来做图形渲染的,可以通过修改 Paint 的 Xfermode 来影响在 Canvas 已有的图像上面绘制新的颜色的方式 。

Xfermode 包含三个子类:AvoidXfermode、PixelXorXfermode 和 PorterDuffXfermode,前两个已经被弃用了,我们就不管它们了。

PorterDuffXfermode 是一个非常强大的转换模式,使用它,可以使用图像合成的 18条 Porter-Duff 规则的任意一条来控制 Paint 与已有的 Canvas 图像进行交互的方式。

看到这个名字大家都应该知道了,这肯定和我们要讲的 PorterDuff.Mode 有关,有什么关系就要说到 PorterDuff.Mode 的本质是什么,在 PorterDuff 里有个枚举变量 Mode,它有18个值,分别对应了一条图形混合的模式。

我们通过在 PorterDuffXfermode 里设置 PorterDuff.Mode,再把 PorterDuffXfermode 传给 Paint,就能实现很多我们想要的图形效果。

2、各种模式效果

PorterDuff.Mode 的效果相当惊人,但只有充分了解每种模式的作用,我们才能百战不怠。

public enum Mode {
    /** [0, 0] */
    CLEAR       (0),
    /** [Sa, Sc] */
    SRC         (1),
    /** [Da, Dc] */
    DST         (2),
    /** [Sa + (1 - Sa)*Da, Rc = Sc + (1 - Sa)*Dc] */
    SRC_OVER    (3),
    /** [Sa + (1 - Sa)*Da, Rc = Dc + (1 - Da)*Sc] */
    DST_OVER    (4),
    /** [Sa * Da, Sc * Da] */
    SRC_IN      (5),
    /** [Sa * Da, Sa * Dc] */
    DST_IN      (6),
    /** [Sa * (1 - Da), Sc * (1 - Da)] */
    SRC_OUT     (7),
    /** [Da * (1 - Sa), Dc * (1 - Sa)] */
    DST_OUT     (8),
    /** [Da, Sc * Da + (1 - Sa) * Dc] */
    SRC_ATOP    (9),
    /** [Sa, Sa * Dc + Sc * (1 - Da)] */
    DST_ATOP    (10),
    /** [Sa + Da - 2 * Sa * Da, Sc * (1 - Da) + (1 - Sa) * Dc] */
    XOR         (11),
    /** [Sa + Da - Sa*Da,
         Sc*(1 - Da) + Dc*(1 - Sa) + min(Sc, Dc)] */
    DARKEN      (16),
    /** [Sa + Da - Sa*Da,
         Sc*(1 - Da) + Dc*(1 - Sa) + max(Sc, Dc)] */
    LIGHTEN     (17),
    /** [Sa * Da, Sc * Dc] */
    MULTIPLY    (13),
    /** [Sa + Da - Sa * Da, Sc + Dc - Sc * Dc] */
    SCREEN      (14),
    /** Saturate(S + D) */
    ADD         (12),
    OVERLAY     (15);

    Mode(int nativeInt) {
        this.nativeInt = nativeInt;
}

    /**
     * @hide
     */
    public final int nativeInt;
}

这就是 PorterDuff.Mode 的定义,注释的文字是说明该模式的 alpha 通道和颜色值的计算方式,要理解各个模式的计算方式需要先弄明白公式中各个元素的具体含义:

  1. Sa:全称为Source alpha,表示源图的Alpha通道。
  2. Sc:全称为Source color,表示源图的颜色。
  3. Da:全称为Destination alpha,表示目标图的Alpha通道。
  4. Dc:全称为Destination color,表示目标图的颜色。

当 Alpha 通道的值为1时,图像完全可见;当 Alpha 通道值为0时,图像完全不可见;当 Alpha 通道的值介于0和1之间时,图像只有一部分可见。Alpha 通道描述的是图像的形状,而不是透明度。

其实每一个值都是按一定的规则对像素点的颜色值进行加减乘除操作。但,千万不要用数学计算去套这些模式的操作结果,因为数学计算的结果和显示的颜色值是有一定的差别的。但,可以按数学计算公式去估算每一个模式中颜色值的大致变化。

其中的每一个值都是对src与dst的每一个像素点的颜色值进行操作。src指前景、上层,后绘制上的;dst指后景、下层,先绘制上去的。

以 SRC_OVER 的计算方式为例:[Sa + (1 - Sa)*Da, Rc = Sc + (1 - Sa)*Dc] 。它以逗号分成两部分,前面“Sa + (1 - Sa)*Da”计算的值代表 SRC_OVER 模式的 Alpha 通道,后面“Rc = Sc + (1 - Sa)*Dc”计算的是 SRC_OVER模式的颜色值,图形混合后的图片依靠这个矢量来计算 ARGB 的值。

1、Bitmap绘制

先来上几张图片,图文并茂我认为是最好的让人理解的方法。

两个图形一圆一方通过一定的计算产生了不同的合成效果,我们在实际工作中需要做图片处理时可以参考这张图的示意快速地选择合适的 Mode。

最常用的就是第一幅图的16种,ADD 和 OVERLAY 并不常使用。

在源图像与目标图像不相交的部分,源图像可以把 Da、Dc 视作0,目标图也是如此。

1、CLEAR

清除模式,[0, 0],即图像中所有像素点的 alpha 和颜色值均为0,用这个就只有我们设置的背景。

2、SRC

[Sa, Sc],只保留源图像的 alpha 和 color ,所以绘制出来只有源图,上图中就是蓝色正方形。

3、DST

[Da, Dc],只保留了目标图像的 alpha 和 color,所以绘制出来的只有目标图,上图中就是圆形。

4、SRC_OVER

[Sa + (1 - Sa)*Da, Rc = Sc + (1 - Sa)*Dc],看上去是在目标图像上层绘制源图像,如果源图与目标图的重合部分源图不透明,则完全覆盖目标图;如果有透明的部分则会计算它们的颜色,我们说过算得就是每个像素的颜色值,所以该像素点是否有值还要看 color。

5、DST_OVER

[Sa + (1 - Sa)*Da, Rc = Dc + (1 - Da)*Sc],与 SRC_OVER 相反,此模式是目标图像被绘制在源图像的上方,公式可以看到如果目标图不透明则这个像素的颜色就是目标图像的 color;透明则计算两图的颜色。

6、SRC_IN

[Sa * Da, Sc * Da],在两者相交的地方绘制源图像,并且绘制的效果会受到目标图像对应地方透明度的影响。这个公式更好理解。

7、DST_IN

[Sa * Da, Sa * Dc],理论和 SRC_IN 相同,在两者相交的地方绘制目标图像,并且绘制的效果会受到源图像对应地方透明度的影响。

8、SRC_OUT

[Sa * (1 - Da), Sc * (1 - Da)],从名字可以看出于 SRC_IN 相反,那可以理解为在不相交的地方绘制源图像。color 是 Sc * ( 1 - Da ) ,表示如果相交处的目标色的 alpha 是完全不透明的,这时候源图像会完全被过滤掉,否则会受到相交处目标色 alpha 影响,呈现出对应色值。

9、DST_OUT

[Da * (1 - Sa), Dc * (1 - Sa)],可以类比SRC_OUT , 在不相交的地方绘制目标图像,相交处根据源图像alpha进行过滤,完全不透明处完全过滤,完全透明则不过滤,半透明则受到相交处源图像 alpha 影响,呈现出对应色值。

10、SRC_ATOP

[Da, Sc * Da + (1 - Sa) * Dc],从公式看,源图像没有和目标图像相交的部分因为 Da 和 Dc 没有值,视作0,所以那个部分就不会被绘制。因此这个模式就是在源图像和目标图像相交处绘制源图像,不相交的地方只绘制目标图像的部分,并且相交处的效果会受到源图像和目标图像alpha的影响。

11、DST_ATOP

[Sa, Sa * Dc + Sc * (1 - Da)],源图像和目标图像相交处绘制目标图像,不相交的地方绘制源图像的部分,并且相交处的效果会受到源图像和目标图像alpha的影响。

12、XOR

[Sa + Da - 2 * Sa * Da, Sc * (1 - Da) + (1 - Sa) * Dc],在不相交的地方按原样绘制源图像和目标图像,相交的地方受到对应 alpha 和 color 的值影响,按公式进行计算,如果都完全不透明则相交处完全不绘制。

13、DARKEN

[Sa + Da - Sa*Da, Sc*(1 - Da) + Dc*(1 - Sa) + min(Sc, Dc)],min(Sc,Dc) 表示选择 Sc 和 Dc 中更接近 0 的值,也就是更暗的颜色。所以该模式处理过后,会感觉效果变暗,即进行对应像素的比较,取较暗值,如果色值相同则进行混合。

从公式上看,色值上如果都不透明则取较暗值,非完全不透明情况下使用上面算法进行计算,受到源图和目标图对应色值和 alpha 值影响。

14、LIGHTEN

[Sa + Da - Sa*Da, Sc*(1 - Da) + Dc*(1 - Sa) + max(Sc, Dc)],可以和 DARKEN 对比起来看,DARKEN 的目的是变暗,LIGHTEN 的目的则是变亮,如果在均完全不透明的情况下,色值取源色值和目标色值中的较大值,否则按上面算法进行计算。

15、MULTIPLY

[Sa * Da, Sc * Dc],类似于PS中的“正片叠底”效果,即查看每个通道中的颜色信息,并将基色与混合色复合。结果色总是较暗的颜色,这是二进制的与运算,黑色是0,白色是1,任何颜色与黑色复合产生黑色,任何颜色与白色复合保持不变,当用黑色或白色以外的颜色绘画时,绘画工具绘制的连续描边产生逐渐变暗的颜色。

16、SCREEN

[Sa + Da - Sa * Da, Sc + Dc - Sc * Dc],滤色,滤色模式与我们所用的显示屏原理相同,所以也有版本把它翻译成屏幕;简单的说就是保留两个图层中较白的部分,较暗的部分被遮盖;当一层使用了滤色(屏幕)模式时,图层中纯黑的部分变成完全透明,纯白部分完全不透明,其他的颜色根据颜色级别产生半透明的效果。

17、ADD

Saturate(S + D),饱和度叠加

18、OVERLAY

像素是进行 Multiply(正片叠底)混合还是 Screen(屏幕)混合,取决于底层颜色,但底层颜色的高光与阴影部分的亮度细节会被保留。

推荐简书的一篇文章,各个击破搞明白PorterDuff.Mode,这里面的经过混合模式处理后的图片效果比官方提供给我们的上面的图要好的多,理解起来要简单的多,对模式的说明也让我想通了很多,我这里的说明就是在他的基础上再加上我的解释,让其详细了一些,大家可以看看。

2、Canvas直接绘制

这里要注意一下,我们上面的说明是围绕于目标图和源图都是 Bitmap 的情况下,如果是 Canvas 直接绘制的话,模式的功能就是下面这幅图:

大致的意思是与上面相同的,在下面的例子也会出现直接用 Canvas 绘制的,大家来对照这幅图就能理解啦。

3、代码演示

我们学习 PorterDuff.Mode 就是为了将它运用到我们实际的开发当中,所以我们当然要知道在哪些地方能用到 PorterDuff.Mode,我总结了一些它的用法:

1、自定义loading样式

我相信大家在下载的时候很多时候都能看到下面的效果:

这里写图片描述

我们把这个当做下载文件的进度,当蓝色填满了整个图形,那就代表下载完成了,是不是觉得效果很不错,要实现这个效果很简单,我们只要用到 PorterDuff 的一种模式就能轻而易举的达到,我们来看代码:

public class LogoLoadingView extends View {

    private int width, height;
    private Paint paint;
    private Bitmap bitmap;
    private int currentTop;
    private RectF rectF;
    private PorterDuffXfermode porterDuffXfermode;

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

    public LogoLoadingView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public LogoLoadingView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {

        paint = new Paint();
        //设置抗锯齿
        paint.setAntiAlias(true);
        //设置填充样式
        paint.setStyle(Paint.Style.FILL);
        //设定是否使用图像抖动处理,会使绘制出来的图片颜色更加平滑和饱满,图像更加清晰
        paint.setDither(true);
        //加快显示速度,本设置项依赖于dither和xfermode的设置
        paint.setFilterBitmap(true);
        //从资源文件中解析获取Bitmap
        bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher);

        width = 200;
        height = 200;

        bitmap = Bitmap.createScaledBitmap(bitmap, width, height, true);

        porterDuffXfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);

        /**
         * 设置当前矩形的高度为0
         */
        currentTop = bitmap.getHeight();
        rectF = new RectF(0, currentTop, bitmap.getWidth(), bitmap.getHeight());
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        rectF.top = currentTop;

        /*
         * 将绘制操作保存到新的图层(更官方的说法应该是离屏缓存)
         */
        int sc = canvas.saveLayer(0, 0, width, height, paint, Canvas.ALL_SAVE_FLAG);

        //绘制目标图像(设置的安卓图标)
        canvas.drawBitmap(bitmap, 0, 0, null);

        paint.setXfermode(porterDuffXfermode);
        paint.setColor(Color.BLUE);

        //绘制
        canvas.drawRect(rectF, paint);
        paint.setXfermode(null);
        /**
         * 还原画布,与canvas.saveLayer配套使用
         */
        canvas.restoreToCount(sc);
        if (currentTop > 0) {
            currentTop -= 2;
            postInvalidate();
        }
    }
}

大部分的工作都是用来初始化我们的画笔,这些个方法的效果就不详细说了,我们得到图片后,因为原本的图片比较小,所以我用 createScaledBitmap() 调整为我想要的大小。我们在这里要用到的是 SRC_IN 模式,上面说过它的效果是在目标图与源图相交的部分,不透明就以源图的颜色为准。因为我们要在图形上填充颜色,所以目标图应该是 Android 机器人。

在这里使用 PorterDuff.Mode 是 SRC_IN,因为这里我们是直接在 Canvas 上绘制矩形,所以对照的是 Canvas 的那幅图,这里 SRC_IN 的效果和 SRC_ATOP 一样。

我们要做的是让颜色逐渐填满图形,所以最开始矩形的高度应该是0。因为我们屏幕的坐标系中,Y 轴的坐标向下越来越大,所以我们设置矩形 top 为图形的 height,其实是把位置定在了图形的最下方。top 和 bottom 值相同,矩形高度就为0了。

在 onDraw() 方法中绘制,先绘制的是目标图,然后设置好 PorterDuff.Mode,再绘制矩形。要让颜色从图形底部填充,要改的就是矩形 top 的值,往上就 Y 轴坐标减小,所以我们让 top 逐渐减小,再让它重新绘制即可。

2、整合图片

有时候我们会想把两张图片融合在一起,得到看起来很炫酷的样子,这样的效果也是可以用 PorterDuff.Mode 做出来的:

我这里下载了两张图片:

我们要做的就是让莲花出现在背景图上。

public class SrcTopView extends View {

    private Paint mPaint;// 画笔
    private Bitmap bitmapDis, bitmapSrc;// 位图
    private PorterDuffXfermode porterDuffXfermode;// 图形混合模式

    private int x, y;// 位图绘制时左上角的起点坐标
    private int screenW, screenH;// 屏幕尺寸

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

    public SrcTopView(Context context, AttributeSet attrs) {
        super(context, attrs);

        // 实例化混合模式
        porterDuffXfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP);

        // 初始化画笔
        initPaint();

        // 初始化资源
        initRes(context);
    }

    /**
     * 初始化画笔
     */
    private void initPaint() {

        mPaint = new Paint();

        mPaint.setAntiAlias(true);

        mPaint.setStyle(Paint.Style.FILL);

        mPaint.setDither(true);

        mPaint.setFilterBitmap(true);
    }

    /**
     * 初始化资源
     */
    private void initRes(Context context) {
        // 获取位图
        bitmapDis = BitmapFactory.decodeResource(context.getResources(), R.drawable.picture1);
        bitmapSrc = BitmapFactory.decodeResource(context.getResources(), R.drawable.picture2);

        int[] screenSize = MeasureUtil.getScreenSize(context);

        // 获取屏幕尺寸
        screenW = screenSize[0];
        screenH = screenSize[1];

        /*
         * 计算位图绘制时左上角的坐标使其位于屏幕中心
         * 屏幕坐标x轴向左偏移位图一半的宽度
         * 屏幕坐标y轴向上偏移位图一半的高度
         */
        x = screenW / 2;
        y = screenH / 2;
    }

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

        int sc = canvas.saveLayer(0, 0, screenW, screenH, null, Canvas.ALL_SAVE_FLAG);

        // 先绘制dis目标图
        canvas.drawBitmap(bitmapDis, x - bitmapDis.getWidth() / 2,
                y - bitmapDis.getHeight() / 2, mPaint);

        // 设置混合模式
        mPaint.setXfermode(porterDuffXfermode);

        // 再绘制src源图
        canvas.drawBitmap(bitmapSrc, x - bitmapSrc.getHeight() / 2,
                y - bitmapSrc.getWidth() / 2, mPaint);

        // 还原混合模式
        mPaint.setXfermode(null);

        // 还原画布
        canvas.restoreToCount(sc);
    }
}
public class MeasureUtil {

    public static int[] getScreenSize(Context context) {
        DisplayMetrics metrics = new DisplayMetrics();
        metrics = context.getResources().getDisplayMetrics();

        int[] sizes = new int[] {
                metrics.widthPixels, metrics.heightPixels
        };

        return sizes;
    }
}

我们用 MeasureUtil 获得屏幕的宽高,在绘制 Bitmap 的时候设置好 left 和 top 的坐标,要注意的就是这个了,实现是很简单的。

3、裁图得到圆形圆角

我们也可以使用 PorterDuff.Mode 来让图片变成圆形或者出现圆角:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    setLayerType(LAYER_TYPE_SOFTWARE, null); //关闭硬件加速

    Bitmap src = BitmapFactory.decodeResource(getResources(), R.drawable.picture1);

    int radiu = Math.min(src.getHeight(), src.getWidth()) / 2;
    canvas.drawCircle(src.getWidth() / 2, src.getHeight() / 2, radiu, mPaint);

    //设置Xfermode
    mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));

    //源图
    canvas.drawBitmap(src, 0, 0, mPaint);

    //还原Xfermode
    mPaint.setXfermode(null);
}

得到圆角也就是改变一个方法的事情:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    setLayerType(LAYER_TYPE_SOFTWARE, null); //关闭硬件加速

    Bitmap src = BitmapFactory.decodeResource(getResources(), R.drawable.picture1);

    //得到目标图
    RectF rectF = new RectF(0, 0, src.getWidth(), src.getHeight());
    canvas.drawRoundRect(rectF, 70, 70, mPaint);

    //设置Xfermode
    mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));

    //源图
    canvas.drawBitmap(src, 0, 0, mPaint);

    //还原Xfermode
    mPaint.setXfermode(null);
}

关于 PorterDuff.Mode 的介绍就讲到这里,在刚了解它的时候,我们可能不能第一时间想到使用,这就需要我们大家的努力练习啦。

结束语:本文仅用来学习记录,参考查阅。

作者:HardWorkingAnt 发表于2017/9/20 20:41:42 原文链接
阅读:399 评论:0 查看评论

Android:这是一份全面 &清晰易懂的Application类使用指南

$
0
0

前言

  • Applicaiton类在 Android开发中非常常见,可是你真的了解Applicaiton类吗?
  • 本文将全面解析Applicaiton类,包括特点、方法介绍、应用场景和具体使用,希望你们会喜欢。

目录

示意图


1. 定义

  • 代表应用程序(即 Android App)的类,也属于Android中的一个系统组件
  • 继承关系:继承自 ContextWarpper

示意图


2. 特点

2.1 实例创建方式:单例模式

  • 每个Android App运行时,会首先自动创建Application 类并实例化 Application 对象,且只有一个
    Application类 是单例模式(singleton)类
  • 也可通过 继承 Application 类自定义Application 类和实例

2.2 实例形式:全局实例

即不同的组件(如Activity、Service)都可获得Application对象且都是同一个对象

2.3 生命周期:等于 Android App 的生命周期

Application 对象的生命周期是整个程序中最长的,即等于Android App的生命周期


3. 方法介绍

那么,该 Application 类有什么作用呢?下面,我将介绍Application 类的方法使用

示意图

3.1 onCreate()

  • 调用时刻: Application 实例创建时调用

    Android系统的入口是Application类的 onCreate(),默认为空实现

  • 作用

    1. 初始化 应用程序级别 的资源,如全局对象、环境配置变量、图片资源初始化、推送服务的注册等

    注:请不要执行耗时操作,否则会拖慢应用程序启动速度

  • 数据共享、数据缓存
    设置全局共享数据,如全局共享变量、方法等
    注:这些共享数据只在应用程序的生命周期内有效,当该应用程序被杀死,这些数据也会被清空,所以只能存储一些具备 临时性的共享数据
  • 具体使用

// 复写方法需要在Application子类里实现

private static final String VALUE = "Carson";
    // 初始化全局变量
    @Override
    public void onCreate()
    {
        super.onCreate();

        VALUE = 1;

    }
}

3.2 registerComponentCallbacks() & unregisterComponentCallbacks()

  • 作用:注册和注销 ComponentCallbacks2回调接口

    本质上是复写 ComponentCallbacks2回调接口里的方法从而实现更多的操作,具体下面会详细介绍

  • 具体使用

registerComponentCallbacks(new ComponentCallbacks2() {
// 接口里方法下面会继续介绍
            @Override
            public void onTrimMemory(int level) {

            }

            @Override
            public void onLowMemory() {

            }

            @Override
            public void onConfigurationChanged(Configuration newConfig) {

            }
        });

3.3 onTrimMemory()

  • 作用:通知 应用程序 当前内存使用情况(以内存级别进行识别)

    Android 4.0 后提供的一个API


示意图
  • 应用场景:根据当前内存使用情况进行自身的内存资源的不同程度释放,以避免被系统直接杀掉 & 优化应用程序的性能体验
    1. 系统在内存不足时会按照LRU Cache中从低到高杀死进程;优先杀死占用内存较高的应用
    2. 若应用占用内存较小 = 被杀死几率降低,从而快速启动(即热启动 = 启动速度快)
    3. 可回收的资源包括:
      a. 缓存,如文件缓存,图片缓存
      b. 动态生成 & 添加的View

典型的应用场景有两个:
示意图
  • 具体使用
registerComponentCallbacks(new ComponentCallbacks2() {

@Override
  public void onTrimMemory(int level) {

  // Android系统会根据当前内存使用的情况,传入对应的级别
  // 下面以清除缓存为例子介绍
    super.onTrimMemory(level);
  .   if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) {

        mPendingRequests.clear();
        mBitmapHolderCache.evictAll();
        mBitmapCache.evictAll();
    }

        });
  • 可回调对象 & 对应方法
Application.onTrimMemory()
Activity.onTrimMemory()
Fragment.OnTrimMemory()
Service.onTrimMemory()
ContentProvider.OnTrimMemory()

特别注意:onTrimMemory()中的TRIM_MEMORY_UI_HIDDENonStop()的关系

  • onTrimMemory()中的TRIM_MEMORY_UI_HIDDEN的回调时刻:当应用程序中的所有UI组件全部不可见时
  • ActivityonStop()回调时刻:当一个Activity完全不可见的时候
  • 使用建议:
    1. onStop()中释放与 Activity相关的资源,如取消网络连接或者注销广播接收器等
    2. onTrimMemory()中的TRIM_MEMORY_UI_HIDDEN中释放与UI相关的资源,从而保证用户在使用应用程序过程中,UI相关的资源不需要重新加载,从而提升响应速度
      注:onTrimMemoryTRIM_MEMORY_UI_HIDDEN等级是在onStop()方法之前调用

3.4 onLowMemory()

  • 作用:监听 Android系统整体内存较低时刻
  • 调用时刻:Android系统整体内存较低时
registerComponentCallbacks(new ComponentCallbacks2() {

  @Override
            public void onLowMemory() {

            }

        });
  • 应用场景:Android 4.0前 检测内存使用情况,从而避免被系统直接杀掉 & 优化应用程序的性能体验

    类似于 OnTrimMemory()

  • 特别注意:OnTrimMemory() & OnLowMemory() 关系

    1. OnTrimMemory()OnLowMemory() Android 4.0后的替代 API
    2. OnLowMemory() = OnTrimMemory()中的TRIM_MEMORY_COMPLETE级别
    3. 若想兼容Android 4.0前,请使用OnLowMemory();否则直接使用OnTrimMemory()即可

3.5 onConfigurationChanged()

  • 作用:监听 应用程序 配置信息的改变,如屏幕旋转等
  • 调用时刻:应用程序配置信息 改变时调用
  • 具体使用
registerComponentCallbacks(new ComponentCallbacks2() {

            @Override
            public void onConfigurationChanged(Configuration newConfig) {
              ...
            }

        });
  • 该配置信息是指 :Manifest.xml文件下的 Activity标签属性android:configChanges的值,如下:
<activity android:name=".MainActivity">
      android:configChanges="keyboardHidden|orientation|screenSize"
// 设置该配置属性会使 Activity在配置改变时不重启,只执行onConfigurationChanged()
// 上述语句表明,设置该配置属性可使 Activity 在屏幕旋转时不重启
 </activity>

3.6 registerActivityLifecycleCallbacks() & unregisterActivityLifecycleCallbacks()

  • 作用:注册 / 注销对 应用程序内 所有Activity的生命周期监听
  • 调用时刻:当应用程序内 Activity生命周期发生变化时就会调用

    实际上是调用registerActivityLifecycleCallbacks()ActivityLifecycleCallbacks接口里的方法

  • 具体使用

// 实际上需要复写的是ActivityLifecycleCallbacks接口里的方法
registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
            @Override
            public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
                Log.d(TAG,"onActivityCreated: " + activity.getLocalClassName());
            }

            @Override
            public void onActivityStarted(Activity activity) {
                Log.d(TAG,"onActivityStarted: " + activity.getLocalClassName());
            }

            @Override
            public void onActivityResumed(Activity activity) {
                Log.d(TAG,"onActivityResumed: " + activity.getLocalClassName());
            }

            @Override
            public void onActivityPaused(Activity activity) {
                Log.d(TAG,"onActivityPaused: " + activity.getLocalClassName());
            }

            @Override
            public void onActivityStopped(Activity activity) {
                Log.d(TAG, "onActivityStopped: " + activity.getLocalClassName());
            }

            @Override
            public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
            }

            @Override
            public void onActivityDestroyed(Activity activity) {
                Log.d(TAG,"onActivityDestroyed: " + activity.getLocalClassName());
            }
        });

<-- 测试:把应用程序从前台切到后台再打开,看Activcity的变化 -->
 onActivityPaused: MainActivity
 onActivityStopped: MainActivity
 onActivityStarted: MainActivity
 onActivityResumed: MainActivity

3.7 onTerminate()

调用时刻:应用程序结束时调用

但该方法只用于Android仿真机测试,在Android产品机是不会调用的


4. 应用场景

Applicaiton类的方法可以看出,Applicaiton类的应用场景有:(已按优先级排序)

  • 初始化 应用程序级别 的资源,如全局对象、环境配置变量等
  • 数据共享、数据缓存,如设置全局共享变量、方法等
  • 获取应用程序当前的内存使用情况,及时释放资源,从而避免被系统杀死
  • 监听 应用程序 配置信息的改变,如屏幕旋转等
  • 监听应用程序内 所有Activity的生命周期

5. 具体使用

  • 若需要复写实现上述方法,则需要自定义 Application
  • 具体过程如下

步骤1:新建Application子类

即继承 Application

public class CarsonApplication extends Application
  {
    ...
    // 根据自身需求,并结合上述介绍的方法进行方法复写实现

    // 下面以onCreate()为例
  private static final String VALUE = "Carson";
    // 初始化全局变量
    @Override
    public void onCreate()
    {
        super.onCreate();

        VALUE = 1;

    }

  }

步骤2:配置自定义的Application子类

Manifest.xml文件中 <application>标签里进行配置

Manifest.xml

<application

        android:name=".CarsonApplication"
        // 此处自定义Application子类的名字 = CarsonApplication

</application>

步骤3:使用自定义的Application类实例

private CarsonApplicaiton app;

// 只需要调用Activity.getApplication() 或Context.getApplicationContext()就可以获得一个Application对象
app = (CarsonApplication) getApplication();

// 然后再得到相应的成员变量 或方法 即可
app.exitApp();

至此,关于 Applicaiton 类已经讲解完毕。


6. 总结

  • 我用一张图总结上述文章

示意图


请帮顶 / 评论点赞!因为你的鼓励是我写作的最大动力!

作者:carson_ho 发表于2017/9/21 9:04:31 原文链接
阅读:2646 评论:1 查看评论

iOS11问题: 定位服务在iOS11系统上不能使用?

$
0
0

iOS11问题: 定位服务在iOS11系统上不能使用?

这里写图片描述

Q:我刚刚用iOS11 SDK重新构建了应用程序,发现定位服务现在根本不起作用。

原因:A:因为苹果现在增加了一项新的隐私保护功能 NSLocationAlwaysAndWhenInUseUsageDeion

并且原有的 NSLocationAlwaysUsageDeion 被降级为 NSLocationWhenInUseUsageDeion

想要达到之前 NSLocationAlwaysUsageDeion 的定位效果,需要在info.plist文件中添加 NSLocationAlwaysAndWhenInUseUsageDeionNSLocationWhenInUseUsageDeion 两个就可以了。否则,徒劳无功,你的App依旧不支持Always authorization。

你在使用这个新Key时,位置服务可能仍然不起作用,在我进一步的搜索之后,发现这个gem与所有其他的调试信息混杂在一起:

这个App在没有usage deion的情况下能访问敏感隐私数据。App的info.plist必须包含NSLocationAlwaysAndWhenInUseUsageDeionNSLocationWhenInUseUsageDeion keys中使用字符串值向用户解释该应用如何使用这些数据

This app has attempted to access privacy-sensitive data without a usage deion. The app's Info.plist must contain both NSLocationAlwaysAndWhenInUseUsageDeion and NSLocationWhenInUseUsageDeion keys with string values explaining to the user how the app uses this data

这与更新CLLocationManager.h文件中的注释有很大矛盾。

查看plist权限可以看到新增:

这里写图片描述

解决办法:

添加新Key NSLocationAlwaysAndWhenInUseUsageDeion和旧Key NSLocationWhenInUseUsageDeion的时候,定位服务就能正常使用了.


iOS开发者交流群:①446310206 ②446310206

作者:qq_31810357 发表于2017/9/21 9:05:38 原文链接
阅读:470 评论:0 查看评论

Android开发笔记(一百五十)自动识别验证码图片

$
0
0
若问目前IT领域最炙手可热的技术方向,必属人工智能(简称AI)无疑。前有谷歌的阿法狗完胜围棋世界冠军柯洁,后有微软小冰出版了诗集《阳光失了玻璃窗》,一时间沸沸扬扬,似乎人工智能无所不能,从而掀起了人民大众了解和关注AI的大潮。

虽然人工智能看起来仿佛刚刚兴起,但是它的相关产品早已普遍应用,在工业制造领域,有越来越多的机器人用于自动化生产;在家庭生活领域,则有智能锁、扫地机器人等助力智能家居。这些智能产品的背后,离不开人工智能的几项基本技术,包括计算机视觉、自然语言处理、数据挖掘与分析等等。这几项技术的应用说明如下:
1、计算机视觉,包括图像识别,视频识别等技术,可应用于指纹识别、人脸识别、无人驾驶汽车等等;
2、自然语言处理,包括音频识别、语义分析等技术,可应用于机器翻译、语音速记、信息检索等等;
3、数据挖掘与分析,包括大数据的相关处理技术,可应用于商品推荐、天气预报、红绿灯优化等等;
上述的几个人工智能应用,看似牛逼,可是这跟Android开发有什么关系呢?其实手机App很早就用上了相关的智能技术,还记得12306网站的神奇验证码吧,买张热点地区的火车票一直是个老大难,常常在火车站售票窗口排了许久的队伍,终于排到你的时候却发现目的地的火车票卖光了。特别是春运的时候,即使不到售票窗口排队,而是到12306网站买票,也常常因为各种操作问题贻误下单,于是各种抢票插件应运而生,帮助用户自动登录、自动选择乘车日期和起止站点、自动下单抢票。抢票插件的核心功能之一,便是自动识别登录过程中的验证码图片,原本这个验证码图片是用来阻止程序自动登录的,然而道高一尺魔高一丈,任你采取图片验证码又如何,抢票插件照样能够识别出图片所呈现出来的形状。注意,这里提到的识别图片中的验证码,即为人工智能的一项初级应用。

验证码图片识别,最简单的是数字验证码,因为数字只有从0到9一共十个字符,并且每个数字的形状也比较简单,所以本文就从数字验证码的识别着手,拨开高大上的迷雾,谈谈人工智能的初级应用。
先来看看一张再普通不过的验证码图片:

这张验证码图片蕴含的数字串为8342,拿到该图片,接下来要进行以下步骤的处理:
首先对该图片进行裁剪操作,去掉外围的空白区域,把每个数字所处的区域单独抠出来。如下图所示,四个数字被红框圈出了四段图片:

然后对每个数字方块再做切割,一般是按照九宫格切为九块,分别是左上角、正上方、右上角、正左边、中央、正右边、左下角、正下方、右下角,切割后的效果如下图所示:

之所以把数字方块切成九块,是因为每个数字的形状在不同方位各有侧重点,比如数字3在正左边是空白的,而数字8的正左边有线条;又比如数字6在右上角空白、在右下角有线条,而数字9在右上角有线条、在右下角空白。分别判断九个方位上的线条像素,即可筛选符合条件的数字,进而推断出最可能的数字字符。

一般情况下,图片中的数字颜色较深,其它区域颜色较浅,通过判断每个方格上的像素点颜色深浅,就能得知该方格是否有线条经过。获取像素点的颜色深浅主要有以下几个步骤:
1、调用Bitmap对象的getPixel,获得指定xy坐标的像素点,代码如下:
Color color = bitmap.getPixel(x, y);
2、调用Integer类的toHexString方法,把Color对象转换为String对象,代码如下:
String colorStr = Integer.toHexString(color);
3、第2步得到的String对象是个长度为8的字符串,其中前两位表示透明度,3到4位表示红色浓度,5到6位表示绿色浓度,7到8位表示蓝色浓度,另外需要把16进制的颜色浓度转换为10进制的浓度数值,示例代码如下:
	public static int getRed(String colorStr) {
		return Integer.parseInt(colorStr.substring(2, 4), 16);
	}

	public static int getGreen(String colorStr) {
		return Integer.parseInt(colorStr.substring(4, 6), 16);
	}

	public static int getBlue(String colorStr) {
		return Integer.parseInt(colorStr.substring(6, 8), 16);
	}

下面列举几个识别验证码的效果图,第一张是浅色背景的验证码图片,由于数字整齐故而识别成功率很高:

第二张是深色背景的验证码图片,经过调节颜色的深浅对比度,识别成功率也很高:

最后一张的验证码图片不那么整齐了,每个数字都有三种对齐方式,分别是立正、向左倾斜、向右倾斜,此时数字识别难度加大,原先的算法识别成功率不高,需要加以优化:



点此查看Android开发笔记的完整目录


__________________________________________________________________________
本文现已同步发布到微信公众号“老欧说安卓”,打开微信扫一扫下面的二维码,或者直接搜索公众号“老欧说安卓”添加关注,更快更方便地阅读技术干货。
作者:aqi00 发表于2017/9/21 9:16:20 原文链接
阅读:167 评论:0 查看评论

kotlin学习笔记——类、函数、接口

$
0
0

Kotlin学习笔记系列:http://blog.csdn.net/column/details/16696.html


1、类
类是空的可以省略大括号, 如:
class A(name: String)
注意:kotlin与java的一个不同点,代码句结尾不必加“;”号


2、类继承
默认是final的,所以只能继承那些明确声明open或abstract的类。


3、函数
一切kotlin函数都会返回一个值,如果没有指定默认返回一个Unit类
可以省略结尾分号
当返回值用一个表达式即可计算出来,可以使用等号代替大括号,如: 
 fun add(x: Int, y: Int) Int = x + y


4、默认参数
为参数指定一个默认值,这样这个参数就成了可选,如:
fun toast(msg: String, length: Int = Toast.LENGTH_LONG){
     ...
}
使用时就可以不传length这个参数,如:
toast("test toast")


5、通过参数名传参
假设函数:
 fun toast(msg: String, tag: String = "default", length: Int = Toast.LENGTH_LONG)
可以这样使用:
toast(msg = "hello", length = 1000)
这样可以忽略中间某个默认参数


6、实例化
实例化省略new关键字,如
var layoutManager = LinearLayoutManager(this)


7、伴随对象companion object
在kotlin中使用companion object来定义一些静态属性、常量和静态函数,这个对象被这个类所有对象共享,类似java中的静态变量和方法, 如:
class Circle(var r: Float){
     companion object{
          var pi = 3.14f
     }

     fun getLength(): Float{
          return 2 * pi * r
     }
}



8、接口
kotlin的接口与java接口不同之处在于kotlin的借口可以包含代码,如:
interface TestInterface {

    var i : Int

    fun test(){
        i = 3
    }

    fun interfaceMethod()
}
可以看到在接口中可以定义变量和函数,而实现接口的子类可以直接使用变量和函数,如:
class TestInterfaceClass(override var i: Int) : TestInterface{
    override fun interfaceMethod() {
        test()
        i = 4
    }
}


9、别名
在java中,如果有两个相同名称的类,在同一个类中使用时,其中一个类需要使用完整包名。
在kotlin中,可以指定一个别名来避免使用完整包名,如:
import com.test.common.Product
import com.test.model.Product as ProdectModel
这样在代码中只要使用ProductModel就可以了,如:
var productA = Product()
var productB = ProdectModel()
但是注意,有了别名的类就不能在该类中使用原有的类名了。


10、修饰符
在kotlin中默认的修饰符是public,节约了很多时间和字符。

kotlin中的修饰符有private\protected\internal\public
其中internal是整个module可见的,当然它依赖于所在领域的可见性(比如private类下的internal函数)。这里的module就是android studio中的module概念,是指可以单独编译、运行、测试的独立功能性模块。

所有构造器默认都是public的,但是可以用下面的语法来修改可见性:
class C private contructor(a: Int){ ... }


11、内部类
在kotlin中内部类如果是普通类,则无法访问外部类成员(类似java中的static静态内部类),如
class ModelA{
     val a = 1
     class ModelB{
          val b = a
     }
}

上面的val b = a代码就会报错无法编译。
如果想在内部类访问外部类成员,需要用inner修饰,如
class ModelA{
     val a = 1
     inner class ModelB{
          val b = a
     }
}

这样就可以正常编译运行。



作者:chzphoenix 发表于2017/9/21 9:32:10 原文链接
阅读:266 评论:0 查看评论

Android跳转各种系统设置界面-总结

$
0
0

开发中总会有一种需求,需要我们跳转系统设置界面,引导用户打开所需的设置.

用法

用法很简单,一行代码搞定

 startActivity(new Intent(Settings.ACTION_SETTINGS));

但是这个参数是一直改变的.这样就可以跳转系统的各种设置界面.,该类的Api路径如图所示.


这里写图片描述


API文档飞机票戳我跳转

下边是整理出的对照表,涵盖了大部分界面,当然有些特殊界面是没办法打开的,例如NFC等设置界面,需要手机硬件支持.


常量字段 示意
ACTION_SETTINGS 系统设置界面
ACTION_APN_SETTINGS APN设置界面
ACTION_LOCATION_SOURCE_SETTINGS 定位设置界面
ACTION_AIRPLANE_MODE_SETTINGS 更多连接方式设置界面
ACTION_DATA_ROAMING_SETTINGS 双卡和移动网络设置界面
ACTION_ACCESSIBILITY_SETTINGS 无障碍设置界面
ACTION_SYNC_SETTINGS 同步设置界面
ACTION_ADD_ACCOUNT 添加账户界面
ACTION_NETWORK_OPERATOR_SETTINGS 选取运营商的界面
ACTION_SECURITY_SETTINGS 安全设置界面
ACTION_PRIVACY_SETTINGS 备份重置设置界面
ACTION_VPN_SETTINGS VPN设置界面,可能不存在
ACTION_WIFI_SETTINGS 无线网设置界面
ACTION_WIFI_IP_SETTINGS WIFI的IP设置
ACTION_BLUETOOTH_SETTINGS 蓝牙设置
ACTION_CAST_SETTINGS 投射设置
ACTION_DATE_SETTINGS 日期时间设置
ACTION_SOUND_SETTINGS 声音设置
ACTION_DISPLAY_SETTINGS 显示设置
ACTION_LOCALE_SETTINGS 语言设置
ACTION_VOICE_INPUT_SETTINGS 辅助应用和语音输入设置
ACTION_INPUT_METHOD_SETTINGS 语言和输入法设置
ACTION_USER_DICTIONARY_SETTINGS 个人字典设置界面
ACTION_INTERNAL_STORAGE_SETTINGS 存储空间设置的界面
ACTION_SEARCH_SETTINGS 搜索设置界面
ACTION_APPLICATION_DEVELOPMENT_SETTINGS 开发者选项设置
ACTION_DEVICE_INFO_SETTINGS 手机状态信息的界面
ACTION_DREAM_SETTINGS 互动屏保设置的界面
ACTION_NOTIFICATION_LISTENER_SETTINGS 通知使用权设置的界面
ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS 勿扰权限设置的界面
ACTION_CAPTIONING_SETTINGS 字幕设置的界面
ACTION_PRINT_SETTINGS 打印设置界面
ACTION_BATTERY_SAVER_SETTINGS 节电助手界面
ACTION_HOME_SETTINGS 主屏幕设置界面

以上是我从红米note4一个一个打开的,不能打开的我没有写上去,需要各位自己看APi文档了.

联系方式

本人技术有限,还有很多不完美的地方,欢迎指出.(写作不易,谢谢您的star支持)
* QQ:152046273
* Email:yukuoyuan@hotmail.com
* CSDN博客地址
* Github博客地址
* Github地址

作者:EaskShark 发表于2017/9/21 10:21:19 原文链接
阅读:268 评论:0 查看评论

Android--图片加载处理(内存溢出和三级缓存)

$
0
0

最简单的解决办法,用现成的框架,推荐glide和picasso

一、glide下载地址:https://github.com/bumptech/glide

用法:在build.gradle中加入:

repositories {
  mavenCentral()
  maven { url 'https://maven.google.com' }
}

dependencies {
  compile 'com.github.bumptech.glide:glide:4.1.1'
  annotationProcessor 'com.github.bumptech.glide:compiler:4.1.1'
}

使用:

ImageView imageView = (ImageView) findViewById(R.id.my_image_view);

  Glide.with(this).load("http://goo.gl/gEgYUd").into(imageView);

this:上下文  load里面要加载的图片网址


二、picasso下载地址:https://github.com/square/picasso

用法:在build.gradle中加入:

compile 'com.squareup.picasso:picasso:2.5.2'

使用:

Picasso.with(context).load("http://i.imgur.com/DvpvklR.png").resize(50, 50).into(imageView);

三、内存溢出解决原理:(转自:http://www.cnblogs.com/Free-Thinker/p/6078765.html)

方案一、读取图片时注意方法的调用,适当压缩  尽量不要使用setImageBitmapsetImageResourceBitmapFactory.decodeResource来设置一张大图,因为这些函数在完成decode后,最终都是通过java层的createBitmap来完成的,需要消耗更多内存。 因此,改用先通过BitmapFactory.decodeStream方法,创建出一个bitmap,再将其设为ImageView的  source,decodeStream最大的秘密在于其直接调用JNI>>nativeDecodeAsset()来完成decode,无需再使用java层的createBitmap,从而节省了java层的空间。

         InputStream is = this.getResources().openRawResource(R.drawable.pic1);

         BitmapFactory.Options options = new  BitmapFactory.Options();

         options.inJustDecodeBounds =  false;

         options.inSampleSize =  10;   // widthhight设为原来的十分一

         Bitmap btp =  BitmapFactory.decodeStream(is, null,  options);


如果在读取时加上图片的Config参数,可以跟有效减少加载的内存,从而跟有效阻止抛out of Memory异常。

 

   /**

     *  以最省内存的方式读取本地资源的图片

     *  @param context

     *  @param resId

     *  @return

      */

    public  static  Bitmap readBitMap(Context  context, int resId){ 

         BitmapFactory.Options opt = new  BitmapFactory.Options();

         opt.inPreferredConfig =  Bitmap.Config.RGB_565;

         opt.inPurgeable = true;

         opt.inInputShareable = true;

         //  获取资源图片

        InputStream is =  context.getResources().openRawResource(resId);

         return  BitmapFactory.decodeStream(is, null, opt);

         }


另外,decodeStream直接拿图片来读取字节码,  不会根据机器的各种分辨率来自动适应,使用了decodeStream之后,需要在hdpi和mdpi,ldpi中配置相应的图片资源,  否则在不同分辨率机器上都是同样大小(像素点数量),显示出来的大小就不对了。
方案二、在适当的时候及时回收图片占用的内存  通常Activity或者Fragment在onStop/onDestroy时候就可以释放图片资源:  

 if(imageView !=  null &&  imageView.getDrawable() != null){     

      Bitmap oldBitmap =  ((BitmapDrawable) imageView.getDrawable()).getBitmap();    

       imageView.setImageDrawable(null);    

      if(oldBitmap !=  null){    

            oldBitmap.recycle();     

            oldBitmap =  null;   

      }    

 }   

 //  Other code.

 System.gc();


在释放资源时,需要注意释放的Bitmap或者相关的Drawable是否有被其它类引用。如果正常的调用,可以通过Bitmap.isRecycled()方法来判断是否有被标记回收;而如果是被UI线程的界面相关代码使用,就需要特别小心避免回收有可能被使用的资源,不然有可能抛出系统异常: E/AndroidRuntime: java.lang.IllegalArgumentException: Cannot draw recycled  bitmaps 并且该异常无法有效捕捉并处理。
方案三、不必要的时候避免图片的完整加载 只需要知道图片大小的情形下,可以不完整加载图片到内存。 在使用BitmapFactory压缩图片的时候,BitmapFactory.Options设置inJustDecodeBounds为true后,再使用decodeFile()等方法,可以在不分配空间状态下计算出图片的大小。示例:   

 BitmapFactory.Options opts =  new  BitmapFactory.Options();     

 //  设置inJustDecodeBounds为false     

 opts.inJustDecodeBounds = false   

 //  使用decodeFile方法得到图片的宽和高    

 BitmapFactory.decodeFile(path,  opts);    

 //  打印出图片的宽和高

 Log.d("example", opts.outWidth + "," + opts.outHeight);

(ps:原理其实就是通过图片的头部信息读取图片的基本信息)
方案四、优化Dalvik虚拟机的堆内存分配  堆(HEAP)是VM中占用内存最多的部分,通常是动态分配的。堆的大小不是一成不变的,通常有一个分配机制来控制它的大小。比如初始的HEAP是4M大,当4M的空间被占用超过75%的时候,重新分配堆为8M大;当8M被占用超过75%,分配堆为16M大。倒过来,当16M的堆利用不足30%的时候,缩减它的大小为8M大。重新设置堆的大小,尤其是压缩,一般会涉及到内存的拷贝,所以变更堆的大小对效率有不良影响。 Heap  Utilization是堆的利用率。当实际的利用率偏离这个百分比的时候,虚拟机会在GC的时候调整堆内存大小,让实际占用率向个百分比靠拢。使用  dalvik.system.VMRuntime类提供的setTargetHeapUtilization方法可以增强程序堆内存的处理效率。  

 private final static float  TARGET_HEAP_UTILIZATION = 0.75f;    

 //  在程序onCreate时就可以调用

 VMRuntime.getRuntime().setTargetHeapUtilization(TARGET_HEAP_UTILIZATION);


方案五、自定义堆(Heap)内存大小  对于一些Android项目,影响性能瓶颈的主要是Android自己内存管理机制问题,目前手机厂商对RAM都比较吝啬,对于软件的流畅性来说RAM对性能的影响十分敏感,除了优化Dalvik虚拟机的堆内存分配外,我们还可以强制定义自己软件的对内存大小,我们使用Dalvik提供的  dalvik.system.VMRuntime类来设置最小堆内存为例:  

 private final static int  CWJ_HEAP_SIZE = 6 * 1024 * 1024  ;

 VMRuntime.getRuntime().setMinimumHeapSize(CWJ_HEAP_SIZE);  //  设置最小heap内存为6MB大小。


但是上面方法还是存在问题,函数setMinimumHeapSize其实只是改变了堆的下限值,它可以防止过于频繁的堆内存分配,当设置最小堆内存大小超过上限值(Max Heap  Size)时仍然采用堆的上限值,对于内存不足没什么作用。  
在默认情况下android进程的内存占用量为16M,因为Bitmap他除了java中持有数据外,底层C++的  skia图形库还会持有一个SKBitmap对象,因此一般图片占用内存推荐大小应该不超过8M。这个可以调整,编译源代码时可以设置参数。

参考资料:http://www.tuicool.com/articles/yemM7zf

方案六:在Manifest.xml文件里面的<application  里面添加Android:largeHeap="true"

简单粗暴。这种方法允许应用需要耗费手机很多的内存空间,但却是最快捷的解决办法

四、三级缓存实现原理:(转自:http://blog.csdn.net/lovoo/article/details/51456515)

实现图片缓存也不难,需要有相应的cache策略。这里我采用 内存-文件-网络 三层cache机制,其中内存缓存包括强引用缓存和软引用缓存(SoftReference),其实网络不算cache,这里姑且也把它划到缓存的层次结构中。当根据url向网络拉取图片的时候,先从内存中找,如果内存中没有,再从缓存文件中查找,如果缓存文件中也没有,再从网络上通过http请求拉取图片。在键值对(key-value)中,这个图片缓存的key是图片url的hash值,value就是bitmap。所以,按照这个逻辑,只要一个url被下载过,其图片就被缓存起来了。

但这里不使用SoftReference,而使用LruCache进行图片的缓存 
为什么使用LruCache: 
这个类非常适合用来缓存图片,它的主要算法原理是把最近使用的对象用强引用存储在 LinkedHashMap 中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除。

在过去,我们经常会使用一种非常流行的内存缓存技术的实现,即软引用或弱引用 (SoftReference or WeakReference)。但是现在已经不再推荐使用这种方式了,因为从 Android 2.3 (API Level 9)开始,垃圾回收器会更倾向于回收持有软引用或弱引用的对象,这让软引用和弱引用变得不再可靠。另外,Android 3.0 (API Level 11)中,图片的数据会存储在本地的内存当中,因而无法用一种可预见的方式将其释放,这就有潜在的风险造成应用程序的内存溢出并崩溃。

具体实现:

1)在构造方法里初始化LruCache mCache

if (mCache == null) {
            // 最大使用的内存空间
            int maxSize = (int) (Runtime.getRuntime().freeMemory() / 4);
            mCache = new LruCache<String, Bitmap>(maxSize) {
                @Override
                protected int sizeOf(String key, Bitmap value) {
                    return value.getRowBytes() * value.getHeight();
                }
            };
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

2)去内存中取

Bitmap bitmap = mCache.get(url);
        if (bitmap != null) {
            // 直接显示
            iv.setImageBitmap(bitmap);
            return;
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

3)去硬盘上取

bitmap = loadBitmapFromLocal(url);
        if (bitmap != null) {
            // 直接显示
            iv.setImageBitmap(bitmap);
            return;
            }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

4)从网络加载

loadBitmapFromNet(iv, url);
详细代码:

public class ImageHelper {
    // 内存缓存池
    // private Map<String, SoftReference<Bitmap>> mCache = new
    // LinkedHashMap<String, SoftReference<Bitmap>>();

    // LRUCahce 池子
    private static LruCache<String, Bitmap> mCache;
    private static Handler mHandler;
    private static ExecutorService mThreadPool;
    private static Map<ImageView, Future<?>> mTaskTags = new LinkedHashMap<ImageView, Future<?>>();
    private Context mContext;

    public ImageHelper(Context context) {
        this.mContext = context;
        if (mCache == null) {
            // 最大使用的内存空间
            int maxSize = (int) (Runtime.getRuntime().freeMemory() / 4);
            mCache = new LruCache<String, Bitmap>(maxSize) {
                @Override
                protected int sizeOf(String key, Bitmap value) {
                    return value.getRowBytes() * value.getHeight();
                }
            };
        }

        if (mHandler == null) {
            mHandler = new Handler();
        }

        if (mThreadPool == null) {
            // 最多同时允许的线程数为3个
            mThreadPool = Executors.newFixedThreadPool(3);
        }
    }

    public void display(ImageView iv, String url) {
        // 1.去内存中取
        Bitmap bitmap = mCache.get(url);
        if (bitmap != null) {
            // 直接显示
            iv.setImageBitmap(bitmap);
            return;
        }

        // 2.去硬盘上取
        bitmap = loadBitmapFromLocal(url);
        if (bitmap != null) {
            // 直接显示
            iv.setImageBitmap(bitmap);
            return;
        }

        // 3. 去网络获取图片
        loadBitmapFromNet(iv, url);
    }

    private void loadBitmapFromNet(ImageView iv, String url) {
        // 开线程去网络获取
        // 使用线程池管理
        // new Thread(new ImageLoadTask(iv, url)).start();

        // 判断是否有线程在为 imageView加载数据
        Future<?> futrue = mTaskTags.get(iv);
        if (futrue != null && !futrue.isCancelled() && !futrue.isDone()) {
            System.out.println("取消 任务");
            // 线程正在执行
            futrue.cancel(true);
            futrue = null;
        }

        // mThreadPool.execute(new ImageLoadTask(iv, url));
        futrue = mThreadPool.submit(new ImageLoadTask(iv, url));
        // Future 和 callback/Runable
        // 返回值,持有正在执行的线程
        // 保存
        mTaskTags.put(iv, futrue);
        System.out.println("标记 任务");
    }

    class ImageLoadTask implements Runnable {

        private String mUrl;
        private ImageView iv;

        public ImageLoadTask(ImageView iv, String url) {
            this.mUrl = url;
            this.iv = iv;
        }

        @Override
        public void run() {
            // HttpUrlconnection
            try {
                // 获取连接
                HttpURLConnection conn = (HttpURLConnection) new URL(mUrl).openConnection();

                conn.setConnectTimeout(30 * 1000);// 设置连接服务器超时时间
                conn.setReadTimeout(30 * 1000);// 设置读取响应超时时间

                // 连接网络
                conn.connect();

                // 获取响应码
                int code = conn.getResponseCode();

                if (200 == code) {
                    InputStream is = conn.getInputStream();

                    // 将流转换为bitmap
                    Bitmap bitmap = BitmapFactory.decodeStream(is);

                    // 存储到本地
                    write2Local(mUrl, bitmap);

                    // 存储到内存
                    mCache.put(mUrl, bitmap);

                    // 图片显示:不可取
                    // iv.setImageBitmap(bitmap);
                    mHandler.post(new Runnable() {

                        @Override
                        public void run() {
                            // iv.setImageBitmap(bitmap);

                            display(iv, mUrl);
                        }
                    });
                }
            } catch (Exception e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }

    /**
     * 本地种去去图片
     * 
     * @param url
     */
    private Bitmap loadBitmapFromLocal(String url) {
        // 去找文件,将文件转换为bitmap
        String name;
        try {
            name = MD5Encoder.encode(url);

            File file = new File(getCacheDir(), name);
            if (file.exists()) {

                Bitmap bitmap = BitmapFactory.decodeFile(file.getAbsolutePath());

                // 存储到内存
                mCache.put(url, bitmap);
                return bitmap;
            }

        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        return null;
    }

    private void write2Local(String url, Bitmap bitmap) {
        String name;
        FileOutputStream fos = null;
        try {
            name = MD5Encoder.encode(url);
            File file = new File(getCacheDir(), name);
            fos = new FileOutputStream(file);

            // 将图像写到流中
            bitmap.compress(CompressFormat.JPEG, 100, fos);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (fos != null) {
                try {
                    fos.close();
                    fos = null;
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private String getCacheDir() {
        String state = Environment.getExternalStorageState();
        File dir = null;
        if (Environment.MEDIA_MOUNTED.equals(state)) {
            // 有sd卡
            dir = new File(Environment.getExternalStorageDirectory(), "/Android/data/" + mContext.getPackageName()
                    + "/icon");
        } else {
            // 没有sd卡
            dir = new File(mContext.getCacheDir(), "/icon");

        }

        if (!dir.exists()) {
            dir.mkdirs();
        }

        return dir.getAbsolutePath();
    }
}

使用方法:

ImageView iv = (contentView)findViewById(R.id.iv);
String url = "http://localhost:8080/web/1.jpg";
new IamgeHelper(this).display(iv,url);


作者:chaoyu168 发表于2017/9/21 15:50:47 原文链接
阅读:129 评论:0 查看评论

解剖网络请求框架Volley

$
0
0

转载请标明出处:【顾林海的博客】


Volley介绍

Volley是Google推出的网络请求库,包含的特性有JSON、图像等的异步下载、网络请求的排序(scheduling)、网络请求的优先级处理、缓存、多级别取消请求、和Activity和生命周期的联动(Activity结束时同时取消所有网络请求),文章会先将Volley的基本使用,最后会从全局者的角度讲解Volley框架的具体流程以及缓存的相关知识。

Volley用法


StringRequest的用法

StringRequest是Request的子类,用于向服务器请求字符串的操作,定义StringRequest之前需要定义请求队列RequestQueue,RequestQueue内部会保存所有的请求,并以相应的算法并发的执行,因此RequestQueue全局定义一个就可以了,避免资源的消耗。这里我把RequestQueue的初始化放在Application中。

public class MyApplication extends Application {

    //Volley的全局请求队列
    public static RequestQueue sRequestQueue;

    /**
     * @return Volley全局请求队列
     */
    public static RequestQueue getRequestQueue() {
        return sRequestQueue;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        //实例化Volley全局请求队列
        sRequestQueue = Volley.newRequestQueue(getApplicationContext());
    }
}


使用StringRequest步骤如下:

  1. 初始化RequestQueue。
  2. 创建StringRequest。
  3. 将StringRequest添加到请求队列中。


public class MainActivity extends AppCompatActivity {

    StringRequest request;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        request = new StringRequest(Request.Method.GET, "http://www.sojson.com/open/api/weather/json.shtml?city=北京",
                new Response.Listener<String>() {
                    @Override
                    public void onResponse(String s) {
                        Logger.json(s);
                    }
                }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError volleyError) {

            }
        });
    }

    public void getWeather(View view) {
        MyApplication.getRequestQueue().add(request);
    }
}


运行效果如下:

这里写图片描述


如果请求的方式是POST,提交的参数如何传递呢,可以在StringRequest的匿名类中重写getParams()方法,代码如下:

request = new StringRequest(Request.Method.POST, "url",
        new Response.Listener<String>() {
            @Override
            public void onResponse(String s) {
                Logger.json(s);
            }
        }, new Response.ErrorListener() {
    @Override
    public void onErrorResponse(VolleyError volleyError) {

    }
}) {
    @Override
    protected Map<String, String> getParams() throws AuthFailureError {
        HashMap<String, String> params = new HashMap<>();
        params.put("params", "value");
        return params;
    }
};


扩展GsonRequest的用法


Request是抽象类,除了使用StringRequest,我们还可以定义一个GsonRequest继承自Request,使用Gson进行json转实体类操作。


public class GsonRequest<T> extends Request<T> {

    private final Response.Listener<T> mListener;

    private Gson mGson;

    private Class<T> mClass;

    public GsonRequest(int method, String url, Class<T> clazz, Response.Listener<T> listener,
                       Response.ErrorListener errorListener) {
        super(method, url, errorListener);
        mGson = new Gson();
        mClass = clazz;
        mListener = listener;
    }

    public GsonRequest(String url, Class<T> clazz, Response.Listener<T> listener,
                       Response.ErrorListener errorListener) {
        this(Method.GET, url, clazz, listener, errorListener);
    }

    @Override
    protected Response<T> parseNetworkResponse(NetworkResponse response) {
        try {
            String jsonString = new String(response.data,
                    HttpHeaderParser.parseCharset(response.headers));
            return Response.success(mGson.fromJson(jsonString, mClass),
                    HttpHeaderParser.parseCacheHeaders(response));
        } catch (UnsupportedEncodingException e) {
            return Response.error(new ParseError(e));
        }
    }

    @Override
    protected void deliverResponse(T response) {
        mListener.onResponse(response);
    }

}


使用方式:

request = new GsonRequest(Request.Method.GET, "http://www.sojson.com/open/api/weather/json.shtml?city=北京", WeatherResp.class,
        new Response.Listener<WeatherResp>() {
            @Override
            public void onResponse(WeatherResp response) {
                Toast.makeText(MainActivity.this, response.city, Toast.LENGTH_SHORT).show();
            }
        }, new Response.ErrorListener() {
    @Override
    public void onErrorResponse(VolleyError error) {

    }
});


运行效果如下:

这里写图片描述


网上对于Volley如何使用的文章非常多,这里给出一部分的使用方式,大家对Volley的使用感兴趣的话,可以查阅相关文章。

Volley框架解读


RequestQueue创建与开启


RequestQueue称为请求队列,顾名思义,所有的请求都会被添加到这个队列中去,通过Volley类的静态方法newRequestQueue(Context context,HttpStack stack)创建RequestQueue。

在RequestQueue类中定义了四个集合属性:

com.android.volley.RequestQueue:
/**
 * 重复请求集合(当前请求需要缓存数据时,如果当前mWaitingRequests集合中已经存在该请求,
 * 就将此次请求添加到mWaitingRequests中key为此次请求地址的队列中,等待下次请求)
 */
private final Map<String, Queue<Request<?>>> mWaitingRequests =
        new HashMap<String, Queue<Request<?>>>();

/**
 * 当前请求的队列
 */
private final Set<Request<?>> mCurrentRequests = new HashSet<Request<?>>();

/**
 * 缓存队列(存放在该队列中,优先执行缓存调度线程)
 * 无界的阻塞队列,按任务优先级
 */
private final PriorityBlockingQueue<Request<?>> mCacheQueue =
        new PriorityBlockingQueue<Request<?>>();

/**
 * 网络队列
 */
private final PriorityBlockingQueue<Request<?>> mNetworkQueue =
        new PriorityBlockingQueue<Request<?>>();


该图为mWaitingRequest集合存放的数据:

这里写图片描述

1. 等待队列 mWaitingRequest是以键值对存放的集合(HashMap<String,Queue<Request<?>>>),以请求(Request)的url为key,value是一个队列,内部存放需要缓存数据的请求,并且该请求已经被在缓存队列中。打个比方,我们创建了一个需要缓存数据的Request,第一次添加时会被存放到缓存队列中并交由缓存调度线程执行,如果此次请求没有结束,后续请求同一个url的Request会被存放到mWaitingRequest中以该url为key的队列中等待下一次执行。
2. mCacheQueue队列存放的是需要缓存数据的Request,使用了优先级队列PriorityBlockingQueue<Request<?>>,队列中存放的Request类必须实现Comparable接口,并通过该接口的copmareTo方法对缓存队列进行排序(按优先级排序,如果优先级相同,按序列号排序)。
3. mNetworkQueue队列存放的是需要执行网络请求的Request,与mCacheQueue一样使用了优先级队列PriorityBlockingQueue为Request进行排序。
4. mCurrentRequest集合存放的是所有的请求,通过add(Request<T> request)方法添加。

知道了RequestQueue中四个重要的集合属性的用途后,我们看看RequestQueue被创建之前需要准备哪些。


com.android.volley.Volley:
public static RequestQueue newRequestQueue(Context context, HttpStack stack) {
    //缓存目录
    File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);

    //AndroidHttpClient实例时的http请求消息头
    String userAgent = "volley/0";
    try {
        String packageName = context.getPackageName();
        PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);
        userAgent = packageName + "/" + info.versionCode;//packageName/version
    } catch (NameNotFoundException e) {
    }

    if (stack == null) {
        if (Build.VERSION.SDK_INT >= 9) {//2.3.2
            stack = new HurlStack();//HttpURLConnection实现类
        } else {
            stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent));//HttpClient实现类
        }
    }

    Network network = new BasicNetwork(stack);

    RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
    queue.start();

    return queue;
}


newRequestQueue方法主要做了以下几件事:

1. 设置缓存数据的目录(磁盘缓存具体实现由DiskBaseCache类实现)。
2. 选取执行网络请求的方式(按照Android版本,低于2.3.2选择HttpClient,反之选择HttpURLConnection)。
3. 通过RequestQueue的start()方法,开启缓存调度线程和网络调度线程。
4. 最终返回该RequestQueue实例。

Volley的关键实现就是由RequestQueue中start()方法开启的缓存调度线程和网络调度线程,这两种调度线程实现的。我们继续查看RequestQueue中两个重要的属性。


/**
 * 线程数
 */
private static final int DEFAULT_NETWORK_THREAD_POOL_SIZE = 4;

/**
 * 网络调度线程组
 */
private NetworkDispatcher[] mDispatchers;

/**
 * 缓存调度线程
 */
private CacheDispatcher mCacheDispatcher;


  1. mDispatchers是一个数组,默认长度为4,内部存放网络调度线程NetworkDispatcher,该类继承自Thread类,并实现 run()方法,主要处理网络请求队列中的Request。
  2. mCacheDispatcher是缓存调度线程,CacheDispatcher也继承自Thread类,并实现run()方法,主要处理缓存队列中的请求。

数据的分发处理


Volley中所有的Request请求,无论是从缓存中获取数据,还是通过网络请求,都是在子线程中操作的,那么这里就引出一个问题,数据获取到后是如何刷新界面的,我们都知道子线程是不能操作UI的,那Volley是如何处理的呢?


数据的分发处理


Volley中所有的Request请求,无论是从缓存中获取数据,还是通过网络请求,都是在子线程中操作的,那么这里就引出一个问题,数据获取到后是如何刷新界面的,我们都知道子线程是不能操作UI的,那Volley是如何处理的呢?

这里写图片描述


在上面RequestQueue的构造器中看的mDelivery初始化时传入了UI线程的Handler,也就是说在Volley中子线程刷新UI是通过ExecutorDelivery类来实现的,内部是通过Looper.getMainLooper()获取UI线程的Handler并发送数据的。

ExecutorDelivery的职责是数据的分发,实现了ResponseDelivery接口:


public interface ResponseDelivery {
    public void postResponse(Request<?> request, Response<?> response);

    public void postResponse(Request<?> request, Response<?> response, Runnable runnable);

    public void postError(Request<?> request, VolleyError error);
}


ResponseDelivery接口定义了三个方法,前两个方法是请求成功后数据的分发,最后一个是错误信息的分发。ExecutorDelivery实现了ResponseDelivery接口的三个方法,用于数据的分发,那么这三个方法实现我们有必要了解下,ExecutorDelivery 到底是如何通过Handler来分发数据的。


com.android.volley.ExecutorDelivery:

private final Executor mResponsePoster;

@Override
public void postResponse(Request<?> request, Response<?> response) {
    postResponse(request, response, null);
}

@Override
public void postResponse(Request<?> request, Response<?> response, Runnable runnable) {
    request.markDelivered();
    request.addMarker("post-response");
    mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, runnable));
}

@Override
public void postError(Request<?> request, VolleyError error) {
    request.addMarker("post-error");
    Response<?> response = Response.error(error);
    mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, null));
}


很显然,mResponsePoster是一个线程池,通过mResponsePoster.execute(Runnable command)方法执行Runnable的run()方法,上面的ResponseDeliveryRunnable就是一个实现了Runnable接口的类并实现了run()方法,说好的通过Handler来执行数据的分发,那么Handler的数据分发是在哪里执行的呢?其实在ExecutorDelivery的构造器中初始化mResponsePoster时通过实现Executor接口的execute(Runnable command)方法,并在这个方法中通过handler.post(Runnable r)进行数据的分发。

com.android.volley.ExecutorDelivery:

private final Executor mResponsePoster;

public ExecutorDelivery(final Handler handler) {
    mResponsePoster = new Executor() {
        @Override
        public void execute(Runnable command) {
            handler.post(command);
        }
    };
}


通过给mResponsePoster传入ResponseDeliveryRunnable实例,这个实例最后被handler的post方法处理,归根究底缓存数据和网络请求到的数据都是通过ExecutorDelivery类来分发的,而数据分发的手段是通过Handler来实现,这是因为获取缓存数据和网络请求都是在子线程中操作的,在子线程中并不建议操作UI。

ResponseDeliveryRunnable是ExecutorDelivery的内部类,最后请求结果Response交由它的run()方法实现。

private class ResponseDeliveryRunnable implements Runnable {
    private final Request mRequest;
    private final Response mResponse;
    private final Runnable mRunnable;

    public ResponseDeliveryRunnable(Request request, Response response, Runnable runnable) {
        mRequest = request;
        mResponse = response;
        mRunnable = runnable;//null
    }

    @SuppressWarnings("unchecked")
    @Override
    public void run() {
        if (mRequest.isCanceled()) {
            mRequest.finish("canceled-at-delivery");
            return;
        }

        //响应结果
        if (mResponse.isSuccess()) {
            //通过Request的deliverResponse方法发请求结果
            mRequest.deliverResponse(mResponse.result);
        } else {
            //通过Request的deliverError方法分发请求结果
            mRequest.deliverError(mResponse.error);
        }

        //判断当前Request是否结束
        if (mResponse.intermediate) {
            mRequest.addMarker("intermediate-response");
        } else {
            mRequest.finish("done");
        }

        if (mRunnable != null) {
            mRunnable.run();
        }
    }
}


run方法中如果请求被设置成取消状态就调用执行取消操作不分发数据,在请求没有被取消的情况下,请求成功,通过Request的deliverResponse方法分发数据,请求失败通过Request的deliverError方法分发错误信息。数据分发完,判断Response的intermediate状态,如果intermediate为true,执行mRunnable.run()(执行缓存调度线程中,在需要刷新数据的情况下,Request会被添加到网络请求队列中,这时intermediate 会被设置成true,这里的添加操作被放入Runnable的run()方法中,并将Runnable实例通过ExecutorDelivery的postResponse方法传入),否则执行取消操作。

大家是否还记得使用Volley进行网络请求时,会先创建一个Request,创建时会传入两个监听,一个是请求成功Response.Listener<T>,另一个是请求失败Response.ErrorListener(),请求成功的Listener会在Request的deliverResponse()方法中执行回调,而请求失败的Listener会在Request的deliverError方法中执行回调,可以查看具体的StringRequest类。


com.android.volley.toolbox.StringRequest:
@Override
protected void deliverResponse(String response) {
    mListener.onResponse(response);
}


错误监听的回调在StringRequest的父类Request类中:

com.android.volley.Request:
public void deliverError(VolleyError error) {
    if (mErrorListener != null) {
        mErrorListener.onErrorResponse(error);
    }
}


StringRequest中的deliverResponse和deliverError方法用于数据的回调,而这两个方法的调用就是通过ExecutorDelivery类来操作的。由此整个数据的分发已经很清晰了,总结流程:

这里写图片描述


到了这里我们知道了请求队列中的四个队列的作用,以及缓存调度线程和网络调度线程的作用,最后讨论了数据分发类ExecutorDelivery的实现原理。那么接下来看看缓存调度线程类CacheDispatcher和网络调度线程类NetworkDispatcher的具体实现。

缓存调度线程


CacheDispatcher流程图:

这里写图片描述


com.android.volley.CacheDispatcher:
run()方法:

while (true) {
    try {
        //1、从请求队列中获取一个Request
        final Request<?> request = mCacheQueue.take();
        request.addMarker("cache-queue-take");

        //2、判断当前Request是否被设置为取消状态
        if (request.isCanceled()) {
            //2.1结束请求
            request.finish("cache-discard-canceled");
            continue;
        }

        //3、从磁盘中获取缓存
        Cache.Entry entry = mCache.get(request.getCacheKey());
        if (entry == null) {
            //3.1、本地缓存空,将Request添加到网络请求队列中,重新执行网络请求
            request.addMarker("cache-miss");
            mNetworkQueue.put(request);
            continue;
        }

        //4、判断缓存是否过期(根据服务器响应头中获取设置的)
        if (entry.isExpired()) {
            //4.1、如果缓存已经过期了,将Request添加到网络请求队列中,重新执行网络请求
            request.addMarker("cache-hit-expired");
            request.setCacheEntry(entry);
            mNetworkQueue.put(request);
            continue;
        }

        request.addMarker("cache-hit");
        //5、磁盘中获取的数据会通过Request的parserNetworkResponse方法包装成Response
        Response<?> response = request.parseNetworkResponse(
                new NetworkResponse(entry.data, entry.responseHeaders));
        request.addMarker("cache-hit-parsed");

        //6、判断当缓存数据是否需要刷新(根据服务器响应头中获取设置的)
        if (!entry.refreshNeeded()) {
            //6.1、不需要刷新原始数据,调用ExecutorDelivery响应传递类的postResponse方法
            // 进行结果的分发
            mDelivery.postResponse(request, response);
        } else {
            request.addMarker("cache-hit-refresh-needed");
            //6.2、会将老的数据呈现给用户,再进行网络请求,这样优化用户体验
            request.setCacheEntry(entry);
            //6.3、将Request添加到网络请求队列中,重新执行网络请求
            response.intermediate = true;
            mDelivery.postResponse(request, response, new Runnable() {
                @Override
                public void run() {
                    try {
                        //添加到网络请求队列
                        mNetworkQueue.put(request);
                    } catch (InterruptedException e) {
                        // Not much we can do about this.
                    }
                }
            });
        }

    } catch (InterruptedException e) {
        if (mQuit) {
            return;
        }
        continue;
    }
}


CacheDispatcher继承自Thread,也就是说缓存数据获取是在子线程中操作的,通过上图,总结缓存调度线程的执行逻辑:

  • 整个缓存调度线程会不停的从缓存队列中获取Request。
  • 通过判断该Request是否被取消,如果Request已经被设置成取消,那么跳过此次操作,继续从缓存队列中获取Request。
  • 如果Request没有被取消,会判断磁盘中是否存在。
  • 如果磁盘缓存中数据不存在,将Request添加到网络请求队列中,重新执行网络请求并跳过此次操作,继续从缓存队列中获取Request。
  • 如果磁盘缓存中数据存在,判断缓存是否过期。
  • 如果缓存过期,将Request添加到网络请求队列中,重新执行网络请求并跳过此次操作,继续从缓存队列中获取Request。
  • 如果缓存没有过期,判断磁盘缓存数据是否刷新。
  • 如果磁盘缓存数据不需要刷新,通过ExecutorDelivery进行数据的分发。
  • 如果磁盘缓存数据需要刷新,先将旧数据刷新到界面中,然后将Request添加到网络请求队列中,重新执行网络请求。

网络调度线程


NetworkDispatcher流程图:


这里写图片描述


com.android.volley.NetworkDispatcher:
@Override
public void run() {
    //设置当前线程的优先级为后台线程
    Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
    Request<?> request;
    while (true) {
        try {
            //1、从请求队列中获取一个Request
            request = mQueue.take();
        } catch (InterruptedException e) {
            if (mQuit) {
                return;
            }
            continue;
        }
        try {
            request.addMarker("network-queue-take");
            //2、判断当前Request是否被设置为取消状态
            if (request.isCanceled()) {
                request.finish("network-discard-cancelled");
                continue;
            }

            //添加流量监控
            addTrafficStatsTag(request);

            //3、网络请求
            NetworkResponse networkResponse = mNetwork.performRequest(request);
            request.addMarker("network-http-complete");

            //4、服务器返回304(资源没有被修改),并且请求已交付
            if (networkResponse.notModified && request.hasHadResponseDelivered()) {
                request.finish("not-modified");
                continue;
            }

            //5、网络请求获取的数据会通过Request的parserNetworkResponse方法包装成Response
            Response<?> response = request.parseNetworkResponse(networkResponse);
            request.addMarker("network-parse-complete");

            //6、判断Request是否需要缓存数据并且数据不为空
            if (request.shouldCache() && response.cacheEntry != null) {
                //6.1、如果Request需要缓存数据并且请求数据不为空,
                // 调用DiskBasedCache类的put方法将我们的请求结果保存在磁盘上
                mCache.put(request.getCacheKey(), response.cacheEntry);
                request.addMarker("network-cache-written");
            }
            //7、请求交付
            request.markDelivered();
            //8、分发数据
            mDelivery.postResponse(request, response);
        } catch (VolleyError volleyError) {
            //8.1请求错误,分发错误信息
            parseAndDeliverNetworkError(request, volleyError);
        } catch (Exception e) {
            //8.2发送异常,分发异常信息
            mDelivery.postError(request, new VolleyError(e));
        }
    }
}

private void parseAndDeliverNetworkError(Request<?> request, VolleyError error) {
    error = request.parseNetworkError(error);
    mDelivery.postError(request, error);
}


NetworkDispatcher 继承自Thread,也就是说网络请求是在子线程中操作的,通过上图,总结网络调度线程的执行逻辑:

  • 整个网络调度线程会不停的从网络请求队列中获取Request。
  • 通过判断该Request是否被取消,如果Request已经被设置成取消,那么跳过此次操作,继续从网络请求队列中获取Request。
  • 如果Request没有被取消,进行网络请求。
  • 服务器返回304(资源没有修改),并Request已经交付,结束此次Request,跳过下面的操作,继续从网络请求队列中获取Request。
  • 服务器没有返回304,Request也没有交付,将请求到的数据通过Request的parserNetworkResponse方法包装成Response。
  • 判断Request是否需要缓存,并数据不为空,如果需要缓存,数据也不为空,进行磁盘缓存处理。
  • 磁盘缓存处理后,请求交付,分发数据。
  • 不需要缓存,直接请求交付,分发数据。

缓存


Volley框架的整体流程已经全部讲完,我们回过头来看看Volley的缓存是如何处理,在Volley中使用缓存可以通过Request的setShouldCache(true)方法。


com.android.volley.Request:
/**
 * 如果设置成false,说明每次请求都会进行网络请求,否则走缓存调度线程。
 * @return This Request object to allow for chaining.
 */
public final Request<?> setShouldCache(boolean shouldCache) {
    mShouldCache = shouldCache;
    return this;
}

public final boolean shouldCache() {
    return mShouldCache;
}


给Request的mShouldCache设置为true,随后会在缓存调度线程中判断Request是否需要从缓存中获取数据,以及在网络调度线程中是否需要缓存数据。

网络调度线程中的数据缓存


//3、网络请求
NetworkResponse networkResponse = mNetwork.performRequest(request);
request.addMarker("network-http-complete");

//4、服务器返回304(资源没有被修改),并且请求已交付
if (networkResponse.notModified && request.hasHadResponseDelivered()) {
    request.finish("not-modified");
    continue;
}

//5、网络请求获取的数据会通过Request的parserNetworkResponse方法包装成Response
Response<?> response = request.parseNetworkResponse(networkResponse);
request.addMarker("network-parse-complete");

//6、判断Request是否需要缓存数据并且数据不为空
if (request.shouldCache() && response.cacheEntry != null) {
    //6.1、如果Request需要缓存数据并且请求数据不为空,
    // 调用DiskBasedCache类的put方法将我们 的请求结果保存在磁盘上
    mCache.put(request.getCacheKey(), response.cacheEntry);
    request.addMarker("network-cache-written");
}


这段代码在上面分析网络调度线程时已经讲过了,这里要回过头重新分析,缓存数据时首先得知道缓存了哪些数据,因此我们得从网络请求也就是第3处往下分析,那么这里的mNetword是BasicNetwork的实例,执行BasicNetwork的performRequest(request)方法时会返回NetworkResponse实例。为了简化代码,这里截取几段performRequest方法部分代码分析。

这里写图片描述

这里写图片描述

截图中第一段箭头指的是执行mHttpStck的performRequest(request,headers)方法发起网络请求并返回HttpResponse,后面就是构造NetworkResponse实例,构造实例分两种情况。

1. 请求到的状态码为304,说明服务器资源没有修改,也间接表明之前请求过了,那么直接使用缓存中的数据,如果缓存中的数据为空,那么NetworkResponse构造函数的第二个参数传null。
2. 请求到的状态码不为304,直接使用HttpResponse的返回的数据,来构造NetworkResponse实例。

NetworkResponse的构造器如下:

/**
 * @param statusCode  http状态码
 * @param data        相应体
 * @param headers     返回的响应头或者为null
 * @param notModified 如果服务器返回了304,数据已在缓存中,则为true,否则false
 */
public NetworkResponse(int statusCode, byte[] data, Map<String, String> headers,
                       boolean notModified) {
    this.statusCode = statusCode;
    this.data = data;
    this.headers = headers;
    this.notModified = notModified;
}


网络请求到的数据会被转成字节数组,后面可以根据业务需求转换成相应的类型,继续回到网络调度线程的那段代码:

//5、网络请求获取的数据会通过Request的parserNetworkResponse方法包装成Response
Response<?> response = request.parseNetworkResponse(networkResponse);
request.addMarker("network-parse-complete");

//6、判断Request是否需要缓存数据并且数据不为空
if (request.shouldCache() && response.cacheEntry != null) {
    //6.1、如果Request需要缓存数据并且请求数据不为空,
    // 调用DiskBasedCache类的put方法将我们 的请求结果保存在磁盘上
    mCache.put(request.getCacheKey(), response.cacheEntry);
    request.addMarker("network-cache-written");
}


networkResponse实例拿到后,会被传递给Request的parseNetworkResponse(networkResponse)方法中,这里就是各个Request转换具体类型的地方,我们把上面扩展GsonRequest的parseNetworkResponse方法贴在这里:

    @Override
    protected Response<T> parseNetworkResponse(NetworkResponse response) {
        try {
            String jsonString = new String(response.data,
                    HttpHeaderParser.parseCharset(response.headers));
            return Response.success(mGson.fromJson(jsonString, mClass),
                    HttpHeaderParser.parseCacheHeaders(response));
        } catch (UnsupportedEncodingException e) {
            return Response.error(new ParseError(e));
        }
    }


Response是个泛型类,泛型参数T就是转换后的具体类型,GsonRequest的paresNetworkResponse方法将response的data转换成String类型,再通过GSON转实体类,HttpHeaderParser是一个Http头信息解析工具类,该工具类会解析头信息中的是否包含Date字段,来判断响应是否来自缓存,将响应中Date首部的值与当前时间进行比较,如果响应中的日期值比较早,客户端通常就可以认为是来自缓存的,除了解析Date字段,还解析了Cache-Control字段中是否包含no-cache或是no-store,如果这两个字段有一个存在或是全部存在,表明数据内容不被存储,以及Expires过期时间、Etag信息等等。最终通过Response的静态方法success包装成Response对象。

Response构造器:

private Response(T result, Cache.Entry cacheEntry) {
    this.result = result;
    this.cacheEntry = cacheEntry;
    this.error = null;
}

result指最终解析类型;cacheEntry保存的是返回数据,包括Http的头信息;error指的是错误信息。

Response的组成部分我们已经知道了,继续回到之前的网络调度线程代码:

//6、判断Request是否需要缓存数据并且数据不为空
if (request.shouldCache() && response.cacheEntry != null) {
    //6.1、如果Request需要缓存数据并且请求数据不为空,
    // 调用DiskBasedCache类的put方法将我们 的请求结果保存在磁盘上
    mCache.put(request.getCacheKey(), response.cacheEntry);
    request.addMarker("network-cache-written");
}


当Request需要缓存数据就会通过DiskBasedCache的put(String,Entry)方法来存储数据。

@Override
public synchronized void put(String key, Entry entry) {
    //判断缓存的数据是否超过最大缓存,如果超过最大缓存,就遍历删除文件,直到小于最大缓存数。
    pruneIfNeeded(entry.data.length);
    //创建缓存文件(文件头命名规则:url一半字符的哈希值与url后半段字符的哈希值进行拼接)
    File file = getFileForKey(key);
    try {
        FileOutputStream fos = new FileOutputStream(file);
        CacheHeader e = new CacheHeader(key, entry);
        //写入相关信息
        boolean success = e.writeHeader(fos);
        if (!success) {
            fos.close();
            VolleyLog.d("Failed to write header for %s", file.getAbsolutePath());
            throw new IOException();
        }
        //写入请求数据
        fos.write(entry.data);
        fos.close();
        //内存缓存(不包含请求数据)
        putEntry(key, e);
        return;
    } catch (IOException e) {
    }
    boolean deleted = file.delete();
    if (!deleted) {
        VolleyLog.d("Could not clean up file %s", file.getAbsolutePath());
    }
}
private void putEntry(String key, CacheHeader entry) {
    if (!mEntries.containsKey(key)) {
        //不包含key,总长度累加
        mTotalSize += entry.size;
    } else {
        //包含key,刷新总长度
        CacheHeader oldEntry = mEntries.get(key);
        mTotalSize += (entry.size - oldEntry.size);
    }
    //保存(entry不包含请求数据data[])
    mEntries.put(key, entry);
}


Volley并没有做三级缓存,上面代码中的内存缓存存储的是一些头信息,用来判断磁盘缓存是否存在此次请求的数据,代码注释也比较清晰。

缓存调度线程中的缓存数据获取


开启缓存调度线程时会先调用DiskBasedCache的initialize方法进行初始化。

/**
 * 读取缓存文件的相关头信息
 */
@Override
public synchronized void initialize() {
    ////创建缓存目录,默认当前应用缓冲目录下的volley
    if (!mRootDirectory.exists()) {
        if (!mRootDirectory.mkdirs()) {
            VolleyLog.e("Unable to create cache dir %s", mRootDirectory.getAbsolutePath());
        }
        return;
    }
    //获取缓存目录下的所有文件
    File[] files = mRootDirectory.listFiles();
    if (files == null) {
        return;
    }
    for (File file : files) {
        FileInputStream fis = null;
        try {
            fis = new FileInputStream(file);
            //读取相关Http头信息
            CacheHeader entry = CacheHeader.readHeader(fis);
            entry.size = file.length();
            //将磁盘缓存的文件信息保存在mEntries中
            putEntry(entry.key, entry);
        } catch (IOException e) {
            if (file != null) {
                file.delete();
            }
        } finally {
            try {
                if (fis != null) {
                    fis.close();
                }
            } catch (IOException ignored) {
            }
        }
    }
}


初始化的目的主要是查看磁盘缓存了哪些数据,并把缓存文件中的头信息读取出来,并以键值对的形式保存在mEntries中,根据头信息用来判断服务器的数据是否过期,或者是否返回304等等,并根据mEntries中的请求地址判断当前请求是否缓存过。

private final Map<String, CacheHeader> mEntries =
        new LinkedHashMap<String, CacheHeader>(16, .75f, true);


同时mEntries的类型是LinkedHashMap,内部实现了Lru算法。

从磁盘缓存中获取到相关头信息后,在缓存调度线程中就可以通过以下代码判断,哪些是过期,哪些需要刷新:

//3、从磁盘中获取缓存
Cache.Entry entry = mCache.get(request.getCacheKey());
if (entry == null) {
    //3.1、本地缓存空,将Request添加到网络请求队列中,重新执行网络请求
    request.addMarker("cache-miss");
    mNetworkQueue.put(request);
    continue;
}

//4、判断缓存是否过期(根据服务器响应头中获取设置的)
if (entry.isExpired()) {
    //4.1、如果缓存已经过期了,将Request添加到网络请求队列中,重新执行网络请求
    request.addMarker("cache-hit-expired");
    request.setCacheEntry(entry);
    mNetworkQueue.put(request);
    continue;
}

request.addMarker("cache-hit");
//5、磁盘中获取的数据会通过Request的parserNetworkResponse方法包装成Response
Response<?> response = request.parseNetworkResponse(
        new NetworkResponse(entry.data, entry.responseHeaders));
request.addMarker("cache-hit-parsed");

//6、判断当缓存数据是否需要刷新(根据服务器响应头中获取设置的)
if (!entry.refreshNeeded()) {
    //6.1、不需要刷新原始数据,调用ExecutorDelivery响应传递类的postResponse方法
    // 进行结果的分发
    mDelivery.postResponse(request, response);
} else {
    request.addMarker("cache-hit-refresh-needed");
    //6.2、会将老的数据呈现给用户,再进行网络请求,这样优化用户体验
    request.setCacheEntry(entry);
    //6.3、将Request添加到网络请求队列中,重新执行网络请求
    response.intermediate = true;
    mDelivery.postResponse(request, response, new Runnable() {
        @Override
        public void run() {
            try {
                //添加到网络请求队列
                mNetworkQueue.put(request);
            } catch (InterruptedException e) {
                // Not much we can do about this.
            }
        }
    });
}


上面第4步判断缓存是否过期,如果过期, 就会上次请求时服务器返回的Last-Modified/ETag一起传递给服务器。就像上面这样,过期或刷新会给Request的setCacheEntry方法传入参数Cache.Entry对象,最后会将Request放入网络请求队列中。回顾上面网络请求时,其中有一段代码就是将上次请求时服务器返回的Last-Modified/ETag一起传递给服务器,具体实现在BasicNetwork的performRequest方法代码中。


这里写图片描述


图中用红线框起来的那段代码就是判断过期或刷新时是否将上次请求时服务器返回的Last-Modified/ETag一起传递给服务器的代码,addCacheHeaders方法如下:


private void addCacheHeaders(Map<String, String> headers, Cache.Entry entry) {
    if (entry == null) {
        return;
    }

    if (entry.etag != null) {
        headers.put("If-None-Match", entry.etag);
    }

    if (entry.serverDate > 0) {
        Date refTime = new Date(entry.serverDate);
        headers.put("If-Modified-Since", DateUtils.formatDate(refTime));
    }
}


关于If-Modified-Since 和 If-None-Match含义如下(摘自网络):

If-Modified-Since,和 Last-Modified 一样都是用于记录页面最后修改时间的 HTTP 头信息,只是 Last-Modified 是由服务器往客户端发送的 HTTP 头,而 If-Modified-Since 则是由客户端往服务器发送的头,可 以看到,再次请求本地存在的 cache 页面时,客户端会通过 If-Modified-Since 头将先前服务器端发过来的 Last-Modified 最后修改时间戳发送回去,这是为了让服务器端进行验证,通过这个时间戳判断客户端的页面是否是最新的,如果不是最新的,则返回新的内容,如果是最新的,则 返回 304 告诉客户端其本地 cache 的页面是最新的,于是客户端就可以直接从本地加载页面了,这样在网络上传输的数据就会大大减少,同时也减轻了服务器的负担。
If-None-Match,它和ETags(HTTP协议规格说明定义ETag为“被请求变量的实体值”,或者是一个可以与Web资源关联的记号)常用来判断当前请求资源是否改变。类似于Last-Modified和HTTP-IF-MODIFIED-SINCE。但是有所不同的是Last-Modified和HTTP-IF-MODIFIED-SINCE只判断资源的最后修改时间,而ETags和If-None-Match可以是资源任何的任何属性,不如资源的MD5等。
ETags和If-None-Match的工作原理是在HTTP Response中添加ETags信息。当客户端再次请求该资源时,将在HTTP Request中加入If-None-Match信息(ETags的值)。如果服务器验证资源的ETags没有改变(该资源没有改变),将返回一个304状态;否则,服务器将返回200状态,并返回该资源和新的ETags。
ETag如何帮助提升性能?
聪明的服务器开发者会把ETags和GET请求的“If-None-Match”头一起使用,这样可利用客户端(例如浏览器)的缓存。因为服务器首先产生ETag,服务器可在稍后使用它来判断页面是否已经被修改。本质上,客户端通过将该记号传回服务器要求服务器验证其(客户端)缓存。
其过程如下:
1.客户端请求一个页面(A)。
2.服务器返回页面A,并在给A加上一个ETag。
3.客户端展现该页面,并将页面连同ETag一起缓存。
4.客户再次请求页面A,并将上次请求时服务器返回的ETag一起传递给服务器。
5.服务器检查该ETag,并判断出该页面自上次客户端请求之后还未被修改,直接返回响应304(未修改——Not Modified)和一个空的响应体。


总结


如果阅读的源码十分庞大,这时我们应该分清主次,以主要流程为主,至于一些细节,可以在了解完整体框架思想后再回过头来细细研究。
Volley整体来说,代码不是很多,在阅读完源码后,可以发现很多功能都是以面向接口形式编写的,这样写的好处时便于扩展,比如HttpStack定义了网络请求的具体业务,可以通过实现HttpStack实现自己的网络请求操作,也可以把OkHttp引入其中。
再比如Cache定义了缓存存储的具体细节,可以通过实现Cache接口实现自己的缓存方案。包括模板方法模式Request定义了请求的操作逻辑,将请求到的数据交由子类处理,在子类中可以将数据转换成业务需要的数据样式(String、bitmap、Gson实体类),当然阅读完Volley源码所得到知识不止这些,通过源码,我们对怎样编写一套网络框架有更加清晰的认识,对线程的使用、HttpURLConnection和HttpClient的使用、数据的传递方式、缓存方式、接口编程甚至是设计模式也有了更加清晰的认识。

作者:GULINHAI12 发表于2017/9/21 16:30:27 原文链接
阅读:213 评论:0 查看评论

android activity各种生命周期演示

$
0
0

前言:做android开发也有三年了,前几天遇到一个bug。就是两个比较复杂的activity频繁来回切换,出现应用程序无响应了。这种测试类似于压力测试。毕竟出现了问题,还是挺尴尬的。最终发现的原因是finish之后,onDestroy里面有些释放资源的代码没有执行,又重新进入。后面了解了之后,就把比较重要释放资源放在finish那里先执行,就没出现应用程序无响应。后面有时间,又立马写demo测试activity各种的生命周期。

本文的源码下载:http://download.csdn.net/download/qq_16064871/9989161

关于屏幕旋转的可参考:Android 屏幕旋转生命周期以及处理方法




1、启动activity流程



2、返回键

点击返回键和手动调用finish的生命周期是一样的。


3、屏幕旋转

这个是生命周期重新绘制的。需要保存数据放这个函数onSaveInstanceState

然后拿出来是这个函数onRestoreInstanceState


4、点击home键


5、唤醒程序


6、切换activity


7、activity返回到上一个activity


从这图上可以看出onDestroy是在后面才释放资源的。

8、旋转屏幕生命周期不重写

需要在AndroidManifest.xml添加 android:configChanges="keyboardHidden|orientation|screenSize"

        <activity
            android:name="com.example.activitytest.TestActivity"
            android:configChanges="keyboardHidden|orientation|screenSize"
            android:label="@string/app_name" >
        </activity>


在activity里面重写onConfigurationChanged也可以,没有什么需求操作就不重写了。


9、注意

Activity.finish()
Call this when your activity is done and should be closed. 
在你的activity动作完成的时候,或者Activity需要关闭的时候,调用此方法。
当你调用此方法的时候,系统只是将最上面的Activity移出了栈,并没有及时的调用onDestory()方法,其占用的资源也没有被及时释放。因为移出了栈,所以当你点击手机上面的“back”按键的时候,也不会找到这个Activity。


System.exit(0)
这玩意是退出整个应用程序的,是针对整个Application的。将整个进程直接KO掉。
使用时,可以写在onDestory()方法内,亦可直接在想退出的地方直接调用:
如:System.exit(0); 或 android.os.Process.killProcess(android.os.Process.myPid());


10、测试代码

package com.example.activitytest;

import android.app.Activity;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;

public class MainActivity extends Activity implements OnClickListener{

	private final String TAG = "Show";

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		Log.i(TAG, "MainActivity onCreate");
		
		findViewById(R.id.button1).setOnClickListener(this);
		findViewById(R.id.button2).setOnClickListener(this);
	}
	
	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		getMenuInflater().inflate(R.menu.main, menu);
		return true;
	}

	@Override
	public boolean onOptionsItemSelected(MenuItem item) {
		int id = item.getItemId();
		if (id == R.id.action_settings) {
			return true;
		}else if (id == android.R.id.home) {
			
		}
		return super.onOptionsItemSelected(item);
	}

	@Override
	public void finish() {
		// TODO Auto-generated method stub
		Log.i(TAG, "MainActivity finish");
		super.finish();
	}

	@Override
	protected void onDestroy() {
		// TODO Auto-generated method stub
		Log.i(TAG, "MainActivity onDestroy");
		super.onDestroy();
	}

	@Override
	protected void onPause() {
		// TODO Auto-generated method stub
		Log.i(TAG, "MainActivity onPause");
		super.onPause();
	}

	@Override
	protected void onRestart() {
		// TODO Auto-generated method stub
		Log.i(TAG, "MainActivity onRestart");
		super.onRestart();
	}
	
	@Override
	protected void onStart() {
		// TODO Auto-generated method stub
		Log.i(TAG, "MainActivity onStart");
		super.onStart();
	}
	
	@Override
	protected void onResume() {
		// TODO Auto-generated method stub
		Log.i(TAG, "MainActivity onResume");
		super.onResume();
	}
	
	@Override
	public void onConfigurationChanged(Configuration newConfig) {
		// TODO Auto-generated method stub
		Log.i(TAG, "MainActivity onConfigurationChanged");
		super.onConfigurationChanged(newConfig);
	}

	@Override
	protected void onRestoreInstanceState(Bundle savedInstanceState) {
		// TODO Auto-generated method stub
		Log.i(TAG, "MainActivity onRestoreInstanceState");
		super.onRestoreInstanceState(savedInstanceState);
	}

	@Override
	protected void onSaveInstanceState(Bundle outState) {
		// TODO Auto-generated method stub
		Log.i(TAG, "MainActivity onSaveInstanceState");
		super.onSaveInstanceState(outState);
	}

	@Override
	protected void onStop() {
		// TODO Auto-generated method stub
		Log.i(TAG, "MainActivity onStop");
		super.onStop();
	}

	@Override
	public void onClick(View arg0) {
		switch (arg0.getId()) {
		case R.id.button1:
			finish();
			break;
		case R.id.button2:
			startActivity(new Intent(this, TestActivity.class));
			break;
		default:
			break;
		}
		
	}
}


package com.example.activitytest;

import android.app.Activity;
import android.content.res.Configuration;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;

public class TestActivity extends Activity{

	private final String TAG = "Show";

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_test);
		Log.i(TAG, "TestActivity onCreate");
	}
	
	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		getMenuInflater().inflate(R.menu.main, menu);
		return true;
	}

	@Override
	public boolean onOptionsItemSelected(MenuItem item) {
		int id = item.getItemId();
		if (id == R.id.action_settings) {
			return true;
		}else if (id == android.R.id.home) {
			
		}
		return super.onOptionsItemSelected(item);
	}

	@Override
	public void finish() {
		// TODO Auto-generated method stub
		Log.i(TAG, "TestActivity finish");
		super.finish();
	}

	@Override
	protected void onDestroy() {
		// TODO Auto-generated method stub
		Log.i(TAG, "TestActivity onDestroy");
		super.onDestroy();
	}

	@Override
	protected void onPause() {
		// TODO Auto-generated method stub
		Log.i(TAG, "TestActivity onPause");
		super.onPause();
	}

	@Override
	protected void onRestart() {
		// TODO Auto-generated method stub
		Log.i(TAG, "TestActivity onRestart");
		super.onRestart();
	}
	
	@Override
	protected void onStart() {
		// TODO Auto-generated method stub
		Log.i(TAG, "TestActivity onStart");
		super.onStart();
	}
	
	@Override
	protected void onResume() {
		// TODO Auto-generated method stub
		Log.i(TAG, "TestActivity onResume");
		super.onResume();
	}
	
	@Override
	public void onConfigurationChanged(Configuration newConfig) {
		// TODO Auto-generated method stub
		Log.i(TAG, "TestActivity onConfigurationChanged");
		super.onConfigurationChanged(newConfig);
	}

	@Override
	protected void onRestoreInstanceState(Bundle savedInstanceState) {
		// TODO Auto-generated method stub
		Log.i(TAG, "TestActivity onRestoreInstanceState");
		super.onRestoreInstanceState(savedInstanceState);
	}

	@Override
	protected void onSaveInstanceState(Bundle outState) {
		// TODO Auto-generated method stub
		Log.i(TAG, "TestActivity onSaveInstanceState");
		super.onSaveInstanceState(outState);
	}

	@Override
	protected void onStop() {
		// TODO Auto-generated method stub
		Log.i(TAG, "TestActivity onStop");
		super.onStop();
	}

}




作者:qq_16064871 发表于2017/9/21 17:47:51 原文链接
阅读:49 评论:0 查看评论

Android--多线程断点续传

$
0
0

什么是多线程下载?

多线程下载其实就是迅雷,BT一些下载原理,通过多个线程同时和服务器连接,那么你就可以榨取到较高的带宽了,大致做法是将文件切割成N块,每块交给单独一个线程去下载,各自下载完成后将文件块组合成一个文件,程序上要完成做切割和组装的小算法。

多线程下载文件的过程是: 

  (1)首先获得下载文件的长度,然后设置本地文件的长度。

      HttpURLConnection.getContentLength();//获取下载文件的长度

     RandomAccessFile file = new RandomAccessFile("QQWubiSetup.exe","rwd");

       file.setLength(filesize);//设置本地文件的长度

 

  (2)根据文件长度和线程数计算每条线程下载的数据长度和下载位置。

      如:文件的长度为6M,线程数为3,那么,每条线程下载的数据长度为2M,每条线程开始下载的位置如下图所示。

  

   例如10M大小,使用3个线程来下载,

        线程下载的数据长度   (10%3 == 0 ? 10/3:10/3+1) ,第1,2个线程下载长度是4M,第三个线程下载长度为2M

         下载开始位置:线程id*每条线程下载的数据长度 = ?

        下载结束位置:(线程id+1)*每条线程下载的数据长度-1=?

 

  (3)使用HttpRange头字段指定每条线程从文件的什么位置开始下载,下载到什么位置为止,

         如:指定从文件的2M位置开始下载,下载到位置(4M-1byte)为止

           代码如下:HttpURLConnection.setRequestProperty("Range", "bytes=2097152-4194303");

 

  (4)保存文件,使用RandomAccessFile类指定每条线程从本地文件的什么位置开始写入数据。

RandomAccessFile threadfile = new RandomAccessFile("QQWubiSetup.exe ","rwd");

threadfile.seek(2097152);//从文件的什么位置开始写入数据


代码实现附上大神写的:http://www.jb51.net/article/104456.htm

http://blog.csdn.net/u013626215/article/details/51281190



推荐这个OKGO框架,完美实现:https://github.com/jeasonlzy/okhttp-OkGo

作者:chaoyu168 发表于2017/9/21 22:14:34 原文链接
阅读:100 评论:0 查看评论

iOS11: 使用Xcode9后的11条小建议 韩俊强的博客

$
0
0

Xcode9已在9月20号推出, 相信很多人充满期待, 那么新版Xcode给我们带来哪些新东西呢? 下载后发现很多人哀声载道, 很大一部分是不适应新的编译器, 那么我们我们该如何去调整呢? 耐心看完本文或许你能找到一些答案!

1.模拟器的变化

相信很多人不太习惯新版模拟器, 那么如何恢复呢, 看下图:是不是切换很随意.

2.Jump to Definition 点击对象跳转

在XCode9之前,在变量或方法上, 按CMD+单击, 是直接Jump to Definition,但是现在, 是弹出这个菜单, 对于跳转到变量的定义,就多了一步了,开始可能会觉得不方便 对于想直接跳转到变量定义,现在是 control+command+单击, 也可以:


鼠标用户: 对准你的对象,Command+鼠标右键
触摸板用户: 对准你的对象, Command+双指点击

如果你有强迫症, 非要找回之前一模一样的感觉, 我也可以帮你哦:

是不是很爽啊, 找回初恋的感觉!

3.折叠代码

Xcode9之前:


局部折叠(折叠一个函数):Command+Option+Left/Right
全局折叠(折叠当前文件下的全部函数): Shift+Command+Option+Left/Right
折叠注释块:(/* */之间的文字):Ctrl+Shift+Command+Left/Right

现在:Fold,可以用来折叠方法:

4.代码编译器可以放大/缩小自由切换

你还为每周的团队代码分享因屏幕小看不清代码而纠结吗? 这里就解决了这个问题!


cmd +/- 可以实现编译器的放大缩小

5.无限开发真机调试

目前仅支持ios11的真机,使用较简单,只需在window->Devices and Simulators 下连真机勾选Connect via network, 需要注意的是,必须在一个局域网下:

6.XCode内置的git系统

Source Control的极大增强


支持Github账户, XCode -> Preferences -> Accouts 可以登录你的GitHub账户, 登录后如下:

通过Xcode的菜单 Source Control 最下方的clone, 就可以clone你的github上的工程了,是不是非常方便呢?

关于source control 的其他操作自己去体验吧, 这里不做过多介绍.

7.模拟器可以多开了,并且,模拟器可以登录

这里不做过多介绍

8.代码重构

对一个方法或者变量的重命名, 在方法上CMD+单击, 出现的菜单, 选择rename 可以看到, 它把重命名会出现的改动,比较直观哈!

9.Folder和Group的同步问题

在此之前,我们在XCode中,更改Folder的名字,在FInder中工程对应的文件夹的名字并不会同步的改变,这会造成我们重命名文件夹变得非常不方便,最终要先在XCode中移除,然后在Finder中重命名,再添加回Xcode


现在 在Xcode9中重名命Folder,Finder中的也同步的改变了 我们之前建议一个虚拟的group,并不会在对应的文件夹中建立真实的目录
Xcode9中,默认行为改变了, 变成了会建立对应的真实文件夹, 如果你需要像之前那样只是建立虚拟的group, 选择New Group without Folder 即可!

Show in Finder 可以看到,建立了真实的文件夹:

你可能你会担心, 区分不了这个group到底是虚拟的,还是实际的, 苹果给出了标识来区分的, 虚拟的左下角有个小的三角形, 如图:

10.意外警告

如果你收到 This block declaration is not a prototype
Insert ‘void’ 这个警告,如何彻底关闭呢?


Build Settings -> Strict Prototypes 设置为NO即可, 这只是个临时方案.

11.创建新的颜色 asset catalog

通常我们会 New image set, 现在可以 New color set, 然后填充 rgb alpha 值, 具体怎么操作呢?


Assets.xcassets -> + -> New color set-> 填充RGB及alpha

代码调用方法:

UIColor *customColor;
    // colorNamed: iOS11才有的, 要做版本判断.
    if (@available(iOS 11, *)) { 
        // customColor是自定义颜色的文件名字.
        customColor = [UIColor colorNamed:@"customColor"]; 
    } else {
        customColor = [UIColor colorWithRed:1 green:0.427 blue:1.0 alpha:1.0];
    }


iOS开发者交流群:①446310206 ②446310206

推荐资源:

iOS-Swift-Developers

iOS-OC-Developer

作者:qq_31810357 发表于2017/9/22 10:50:24 原文链接
阅读:125 评论:0 查看评论

Android-性能优化-内存优化

$
0
0

Android-性能优化-内存优化

概述

JVM 内存分配机制

JVM 垃圾回收机制

DVM 与 JVM 的区别

  • 虚拟机区别

Dalvik 虚拟机(DVM)是 Android 系统在 java虚拟机(JVM)基础上优化得到的,DVM 是基于寄存器的,而 JVM 是基于栈的,由于寄存器高效快速的特性,DVM 的性能相比 JVM 更好。

  • 字节码区别

Dalvik 执行 .dex 格式的字节码文件,JVM 执行的是 .class 格式的字节码文件,Android 程序在编译之后产生的 .class 文件会被 aapt 工具处理生成 R.class 等文件,然后 dx 工具会把 .class 文件处理成 .dex 文件,最终资源文件和 .dex 文件等打包成 .apk 文件。

OOM 代码相关优化

当应用程序申请的 java heap 空间超过 Dalvik VM HeapGrowthLimit 时溢出。 OOM 并不代表内存不足,只要申请的 heap 超过 Dalvik VM HeapGrowthLimit 时,即使内存充足也会溢出。 效果是能让较多进程常驻内存。

  • Bitmap

Bitmap 非常消耗内存,而且在 Android 中,读取 bitmap 时, 一般分配给虚拟机的图片堆栈只有 8M,所以经常造成 OOM 问题。 所以有必要针对 Bitmap 的使用作出优化:

  1. 图片显示:加载合适尺寸的图片,比如显示缩略图的地方不要加载大图。
  2. 图片回收:使用完 bitmap,及时使用 Bitmap.recycle() 回收。

问题:Android 不是自身具备垃圾回收机制吗?此处为何要手动回收?

Bitmap 对象不是 new 生成的,而是通过 BitmapFactory 生产的。 而且通过源码可发现是通过调用 JNI 生成 Bitma p对象(nativeDecodeStream()等方法)。 所以,加载 bitmap 到内存里包括两部分,Dalvik 内存和 Linux kernel 内存。 前者会被虚拟机自动回收。 而后者必须通过 recycle() 方法,内部调用 nativeRecycle() 让 linux kernel 回收。

  1. 捕获 OOM 异常:程序中设定如果发生 OOM 的应急处理方式。
  2. 图片缓存:内存缓存、硬盘缓存等
  3. 图片压缩:直接使用 ImageView 显示 Bitmap 时会占很多资源,尤其当图片较大时容易发 生OOM。 可以使用 BitMapFactory.Options 对图片进行压缩。
  4. 图片像素:android 默认颜色模式为 ARGB_8888,显示质量最高,占用内存最大。 若要求不高时可采用 RGB_565 等模式。
  5. 图片大小:图片 长度×宽度×单位像素 所占据字节数。

我们知道 ARGB 指的是一种色彩模式,里面 A 代表 Alpha,R 表示 Red,G 表示 Green,B 表示 Blue。 所有的可见色都是由红绿蓝组成的,所以红绿蓝又称为三原色,每个原色都存储着所表示颜色的信息值,下表中对四种颜色模式的详细描述,以及每种色彩模式占用的字节数。

模式 描述 占用字节
ALPHA Alpha 由 8 位组成 1B
ARGB_4444 4 个 4 位组成 16 位,每个色彩元素站 4 位 2B
ARGB_8888 4 个 8 为组成 32 位,每个色彩元素站 8 位(默认) 4B
RGB_565 R 为 5 位,G 为 6 位,B 为 5 位共 16 位,没有Alpha 2B
  • 对象引用类型

    1. 强引用(Strong Reference):JVM宁愿抛出OOM,也不会让GC回收的对象
    2. 软引用(Soft Reference) :只有内存不足时,才会被GC回收。
    3. 弱引用(weak Reference):在GC时,一旦发现弱引用,立即回收
    4. 虚引用(Phantom Reference):任何时候都可以被 GC 回收,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。 程序可以通过判断引用队列中是否存在该对象的虚引用,来了解这个对象是否将要被回收。 可以用来作为 GC 回收 Object 的标志。
  • 缓存池

对象池:如果某个对象在创建时,需要较大的资源开销,那么可以将其放入对象池,即将对象保存起来,下次需要时直接取出使用,而不用再次创建对象。当然,维护对象池也需要一定开销,故要衡量。

线程池:与对象池差不多,将线程对象放在池中供反复使用,减少反复创建线程的开销。

内存泄露相关优化

当一个对象已经不需要再使用了,本该被回收时,而有另外一个正在使用的对象持有它的引用从而导致它不能被回收,这导致本该被回收的对象不能被回收而停留在堆内存中,这就产生了内存泄漏。

  • 单例造成的内存泄漏

单例模式非常受开发者的喜爱,不过使用的不恰当的话也会造成内存泄漏,由于单例的静态特性使得单例的生命周期和应用的生命周期一样长,这就说明了如果一个对象已经不需要使用了,而单例对象还持有该对象的引用,那么这个对象将不能被正常回收,这就导致了内存泄漏。

如下这个典例:

public class AppManager {
    private static AppManager instance;
    private Context context;
    private AppManager(Context context) {
        this.context = context;
    }
    public static AppManager getInstance(Context context) {
        if (instance != null) {
            instance = new AppManager(context);
        }
        return instance;
    }
}

这是一个普通的单例模式,当创建这个单例的时候,由于需要传入一个 Context,所以这个 Context 的生命周期的长短至关重要:

  1. 传入的是 Application 的 Context:这将没有任何问题,因为单例的生命周期和 Application 的一样长。
  2. 传入的是 Activity 的 Context:当这个 Context 所对应的 Activity 退出时,由于该 Context 和 Activity 的生命周期一样长(Activity 间接继承于 Context),所以当前 Activity 退出时它的内存并不会被回收,因为单例对象持有该 Activity 的引用。

所以正确的单例应该修改为下面这种方式:

public class AppManager {
    private static AppManager instance;
    private Context context;
    private AppManager(Context context) {
        this.context = context.getApplicationContext();
    }
    public static AppManager getInstance(Context context) {
        if (instance != null) {
            instance = new AppManager(context);
        }
        return instance;
    }
}

这样不管传入什么 Context 最终将使用 Application 的 Context,而单例的生命周期和应用的一样长,这样就防止了内存泄漏。

  • 非静态内部类创建静态实例造成的内存泄漏

有的时候我们可能会在启动频繁的Activity中,为了避免重复创建相同的数据资源,可能会出现这种写法:

public class MainActivity extends AppCompatActivity {
    private static TestResource mResource = null;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if(mResource == null){
            mResource = new TestResource();
        }
        //...
    }
    class TestResource {
    //...
    }
}

这样就在 Activity 内部创建了一个非静态内部类的单例,每次启动 Activity 时都会使用该单例的数据,这样虽然避免了资源的重复创建,不过这种写法却会造成内存泄漏,因为非静态内部类默认会持有外部类的引用,而又使用了该非静态内部类创建了一个静态的实例,该实例的生命周期和应用的一样长,这就导致了该静态实例一直会持有该 Activity 的引用,导致 Activity 的内存资源不能正常回收。

正确的做法为:

将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用 Context,请使用 ApplicationContext。

  • Handler 造成的内存泄漏

Handler 的使用造成的内存泄漏问题应该说最为常见了,平时在处理网络任务或者封装一些请求回调等 api 都应该会借助 Handler 来处理,对于 Handler 的使用代码编写一不规范即有可能造成内存泄漏,如下示例:

public class MainActivity extends AppCompatActivity {
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
        //...
        }
    };
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        loadData();
    }
    private void loadData(){
        //...request
        Message message = Message.obtain();
        mHandler.sendMessage(message);
    }
}

这种创建 Handler 的方式会造成内存泄漏,由于 mHandler 是 Handler 的非静态匿名内部类的实例,所以它持有外部类 Activity 的引用,我们知道消息队列是在一个 Looper 线程中不断轮询处理消息,那么当这个 Activity 退出时消息队列中还有未处理的消息或者正在处理消息,而消息队列中的 Message 持有 mHandler 实例的引用,mHandler 又持有 Activity 的引用,所以导致该 Activity 的内存资源无法及时回收,引发内存泄漏,所以另外一种做法为:

public class MainActivity extends AppCompatActivity {
    private MyHandler mHandler = new MyHandler(this);
    private TextView mTextView ;
    private static class MyHandler extends Handler {
        private WeakReference<Context> reference;
        public MyHandler(Context context) {
        reference = new WeakReference<>(context);
        }
        @Override
        public void handleMessage(Message msg) {
            MainActivity activity = (MainActivity) reference.get();
            if(activity != null){
            activity.mTextView.setText("");
            }
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTextView = (TextView)findViewById(R.id.textview);
        loadData();
    }

    private void loadData() {
        //...request
        Message message = Message.obtain();
        mHandler.sendMessage(message);
    }
}

创建一个静态 Handler 内部类,然后对 Handler 持有的对象使用弱引用,这样在回收时也可以回收 Handler 持有的对象,这样虽然避免了 Activity 泄漏,不过 Looper 线程的消息队列中还是可能会有待处理的消息,所以我们在 Activity 的 Destroy 时或者 Stop 时应该移除消息队列中的消息,更准确的做法如下:

public class MainActivity extends AppCompatActivity {
    private MyHandler mHandler = new MyHandler(this);
    private TextView mTextView ;
    private static class MyHandler extends Handler {
        private WeakReference<Context> reference;
        public MyHandler(Context context) {
        reference = new WeakReference<>(context);
        }
        @Override
        public void handleMessage(Message msg) {
            MainActivity activity = (MainActivity) reference.get();
            if(activity != null){
            activity.mTextView.setText("");
            }
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTextView = (TextView)findViewById(R.id.textview);
        loadData();
    }

    private void loadData() {
        //...request
        Message message = Message.obtain();
        mHandler.sendMessage(message);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mHandler.removeCallbacksAndMessages(null);
    }
}

使用 mHandler.removeCallbacksAndMessages(null); 是移除消息队列中所有消息和所有的 Runnable。 当然也可以使用 mHandler.removeCallbacks(); 或 mHandler.removeMessages(); 来移除指定的 Runnable 和 Message。

  • 线程造成的内存泄漏

对于线程造成的内存泄漏,也是平时比较常见的,异步任务和 Runnable 都是一个匿名内部类,因此它们对当前 Activity 都有一个隐式引用。 如果 Activity 在销毁之前,任务还未完成,那么将导致 Activity 的内存资源无法回收,造成内存泄漏。 正确的做法还是使用静态内部类的方式,如下:

static class MyAsyncTask extends AsyncTask<Void, Void, Void> {
    private WeakReference<Context> weakReference;

    public MyAsyncTask(Context context) {
        weakReference = new WeakReference<>(context);
    }

    @Override
    protected Void doInBackground(Void... params) {
        SystemClock.sleep(10000);
        return null;
    }

    @Override
    protected void onPostExecute(Void aVoid) {
        super.onPostExecute(aVoid);
        MainActivity activity = (MainActivity) weakReference.get();
        if (activity != null) {
        //...
        }
    }
}
static class MyRunnable implements Runnable{
    @Override
    public void run() {
        SystemClock.sleep(10000);
    }
}
//——————
new Thread(new MyRunnable()).start();
new MyAsyncTask(this).execute();

这样就避免了 Activity 的内存资源泄漏,当然在 Activity 销毁时候也应该取消相应的任务 AsyncTask::cancel(),避免任务在后台执行浪费资源。

  • 资源使用完未关闭

BraodcastReceiver,ContentObserver,FileObserver,Cursor,Callback等在 Activity onDestroy 或者某类生命周期结束之后一定要 unregister 或者 close 掉,否则这个 Activity 类会被 system 强引用,不会被内存回收。

不要直接对 Activity 进行直接引用作为成员变量,如果不得不这么做,请用 private WeakReference mActivity 来做,相同的,对于Service 等其他有自己声明周期的对象来说,直接引用都需要谨慎考虑是否会存在内存泄露的可能。

其他优化

  • 常用数据结构优化

    1. ArrayMap 及 SparseArray 是 android 的系统 API,是专门为移动设备而定制的。 用于在一定情况下取代 HashMap 而达到节省内存的目的。 对于 key 为 int 的 HashMap 尽量使用 SparceArray 替代,大概可以省 30% 的内存,而对于其他类型,ArrayMap 对内存的节省实际并不明显,10% 左右,但是数据量在 1000 以上时,查找速度可能会变慢。
    2. 在有些时候,代码中会需要使用到大量的字符串拼接的操作,这种时候有必要考虑使用 StringBuilder 来替代频繁的 “+”。
  • 枚举

Android 平台上枚举是比较争议的,在较早的 Android 版本,使用枚举会导致包过大,使用枚举甚至比直接使用 int 包的 size 大了 10 多倍。 在 stackoverflow 上也有很多的讨论, 大致意思是随着虚拟机的优化,目前枚举变量在 Android 平台性能问题已经不大,而目前 Android 官方建议,使用枚举变量还是需要谨慎,因为枚举变量可能比直接用 int 多使用 2 倍的内存。

  • View 复用

    1. 使用 ListView 时 getView 里尽量复用 conertView,同时因为 getView 会频繁调用,要避免频繁地生成对象。 优先考虑使用 RecyclerView 代替 ListView。
    2. 重复的布局优先使用 ,使用 减少 view 的层级,对于可以延迟初始化的页面,使用 。
  • 谨慎使用多进程

现在很多 App 都不是单进程,为了保活,或者提高稳定性都会进行一些进程拆分,而实际上即使是空进程也会占用内存(1M 左右),对于使用完的进程,服务都要及时进行回收。

  • 系统资源

尽量使用系统组件,图片甚至控件的 id。 例如:@android:color/xxx,@android:style/xxx。

使用工具检查内存泄漏

即使在编码时将上述情况都考虑了,往往会有疏忽的地方,更何况通常情况下是团队开发。 所以不仅仅要在编码时考虑内存优化的情况,当出现内存泄漏时,更有效更准确的定位问题才是最重要的方式。 内存泄漏不像 bug,排查起来相对复杂一些,下面介绍下常用的检查方式。

使用 Lint 代码静态检查

Lint 是 Android Studio 自带的工具,使用很简单找到 Analyze -> Inspect Code 然后选择想要扫面的区域即可。

这里写图片描述

选择 Lint 扫描区域。

这里写图片描述

对可能引起性能问题的代码,Lint 都会进行提示。

这里写图片描述

使用 Android Studio 自带的 Monitor Memory 检查

一般在 Android Studio 的底部可以找到 Android Monitor。

这里写图片描述

可以看到当前 App的内存变动比较大,很有可能出现了内存泄漏。 点击 Dump Java Heap,等一段时间会自动生成 Heap Snapshot 文件。

这里写图片描述

在 Captures 中可以找到 hprof 文件。

这里写图片描述

在右侧找到 Analyzer Tasks 并打开,点击图中 Perform Analysis 按钮开始分析。

这里写图片描述

通过分析结果可以看到 TestActivity 泄漏了,从左侧 Reference Tree 中可以看到是 TestActivity 中的 context 泄露了。

这里写图片描述

我们来看下代码:

public class TestActivity extends AppCompatActivity {

    private static Context context;


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

        context = this;

    }
}

代码中 context 为静态的引用了当前 Activity 造成了当前 Activity 无法释放。

一般的通过 使用 Android Studio 自带的 Monitor Memory 可以定位到内存泄漏所在的类,更详细的信息需要借助 Memory Analyzer Tool(MAT)工具。

使用 Memory Analyzer Tool 检查

首先下载 Memory Analyzer Tool 下载地址

在 Android Studio 中先将 hprof 文件导出为 MAT 可以识别的 hprof 文件。

这里写图片描述

打开刚才导出的文件。

这里写图片描述

经过分析后会显示如下,Leak Suspectss 是一个关于内存泄露猜想的饼图,Problem Suspect 1 是泄露猜想的描述。

这里写图片描述

Overview 是一个概况图,把内存的消耗以饼状图形式显示出来,鼠标在每个饼块区域划过或者点击,就会在 Inspector 栏目显示这块区域的相关信息。 MAT 从多角度提供了内存分析,其中包括 Histogram、 Dominator Tree、 Leak Suspects 和 Top consumers 等。
这里写图片描述

这里我们使用 Histogram 进行分析,切换到 Histogram 页面。 这个页面主要有 4 个列,Class Name、 Objects、 Shallow Heap 和 Retained Heap。 其中 Class Name 是全类名,Objects 是这个类的对象实例个数。 Shallow Heap 是对象本身占用内存大小,非数组的常规对象,本身内存占用很小,所以这个对泄露分析作用不大。 Retained Heap 指当前对象大小和当前对象能直接或间接引用的对象大小的总和,这个栏目是分析重点。

这里写图片描述

内存分析是分析的整个系统的内存泄露,而我们只要查找我们 App 的内存泄露情况。 这无疑增加了很多工作,不过幸亏 Histogram 支持正则表达式查找,在 Regex 中输入我们的包名进行过滤,直奔和我们 App 有关的内存泄露。

这里写图片描述

过滤后就显示了我们 App 相关内存信息,按 Retained Heap 大小排列下,发现 MainActivity 和 TestActivity 这两个类问题比较大。 TestActivity 的问题更突出些,所以先从 TestActivity 下手。

首先看下是哪里的引用导致了 TestActivity 不能被 GC 回收。 右键使用 Merge Shortest Paths to GC Roots 显示距 GC Root 最短路径,当然选择过程中要排除软引用和弱引用,因为这些标记的一般都是可以被回收的。

这里写图片描述

进入结果页查看。

这里写图片描述

可以看到 TestActivity 不能被 GC 回收是因为 context 没有释放的原因。 我们再来看下代码:

public class TestActivity extends AppCompatActivity {

    private static Context context;


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

        context = this;

    }
}

使用 LeakCanary 检查

项目地址:https://github.com/square/leakcanary

使用方式很简单,参考项目里面的介绍即可。

这里写图片描述

ANR

  • 什么是 ANR

    1. ANR:Application Not Responding,即应用无响应。
    2. 为用户在主线程长时间被阻塞是提供交互,提高用户体验。
    3. Android 系统自身的一种检测机制。
  • ANR 的类型

ANR 一般有三种类型:

  1. KeyDispatchTimeout(5 seconds) : 主要类型按键或触摸事件在特定时间内无响应
  2. BroadcastTimeout(10 seconds) : BroadcastReceiver 在特定时间内无法处理完成
  3. ServiceTimeout(20 seconds) : 小概率类型 Service 在特定的时间内无法处理完成

    • ANR 产生的原因

超时时间的计数一般是从按键分发给 app 开始。 超时的原因一般有两种:

  1. 当前的事件没有机会得到处理(即 UI 线程正在处理前一个事件,没有及时的完成或者 looper 被某种原因阻塞住了)
  2. 当前的事件正在处理,但没有及时完成。

    • ANR 出现流程分析
  3. 输入时间响应超时导致ANR流程

在系统输入管理服务进程(InputManagerService)中有一个线程(InputDispathcerThread)专门管理输入事件的分发,在该线程处理输入事件的过程中,回调用 InputDispatcher 对象方法不断的检测处理过程是否超时,一旦超时,则会通过一些列的回调调用 InputMethod 对象的 notifyANR 方法,其会最终出发 AMS 中 handler 对象的 SHOW_NOT_RESPONDING_MSG 这个事件,显示ANR对话框。

  1. 广播发生ANR流程

广播分为三类:普通的,有序的,异步的。 只有有序(ordered)的广播才会发生超时,而在 AndroidManifest 中注册的广播都会被当做有序广播来处理,会被放在广播的队列中串行处理。 AMS 在处理广播队列时,会设置一个超时时间,当处理一个广播达到超时时间的限制时,就会触发 BroadcastQueue 类对象的 processNextBroadcast 方法来判断是否超时,如果超时,就会终止该广播,触发ANR对话框。

  1. UI线程

UI 线程主要包括如下:

Activity : onCreate(), onResume(), onDestroy(), onKeyDown(), onClick(), etc 生命周期方法里。
AsyncTask : onPreExecute(), onProgressUpdate(), onPostExecute(), onCancel, etc 这些异步更改 UI 界面的方法里。
Mainthread handler : handleMessage(), post*(runnable r), getMainLooper(), etc 通过 handler 发送消息到主线程的 looper,即占用主线程 looper 的。

  • ANR 执行流程

了解 ANR 执行流程有利于我们制定 ANR 监控策略和获取 ANR 的相关信息,ANR 的执行步骤如下:

  1. 系统捕获到 ANR 发生;
  2. Process 依次向本进程及其他正在运行的进程发送 Linux 信号量 3;
  3. 进程接收到 Linux 信号量,并向 /data/anr/traces.txt 中写入进程信息;
  4. Log 日志打印 ANR 信息;
  5. 进程进入 ANR 状态(此时可以获取到进程 ANR 信息);
  6. 弹出 ANR 提示框;
  7. 提示框消失,进程回归正常状态。

由于向 /data/anr/traces.txt 文件中写入信息耗时较长,从 Input ANR 触发到弹出 ANR 提示框一般在 10s 左右(不同 rom 时间不同)。

  • 发生 ANR 如何定位

当 App 的进程发生 ANR 时,系统让活跃的 Top 进程都进行了一下 dump,进程中的各种 Thread 就都 dump 到这个 trace 文件里了,所以 trace 文件中包含了每一条线程的运行时状态。 traces.txt 的文件放在 /data/anr/ 下. 可以通过 adb 命令将其导出到本地:

$ adb pull data/anr/traces.txt .

通过分析 traces.txt 文件,查找 App 包名关键信息来定位 ANR。

参考资料

Android Bitmap的内存大小是如何计算的?

Android性能优化之常见的内存泄漏

使用新版Android Studio检测内存泄露和性能

Android 应用内存泄漏的定位、分析与解决策略

Android 系统稳定性 - ANR

更多文章

https://github.com/jeanboydev/Android-ReadTheFuckingSourceCode

作者:freekiteyu 发表于2017/9/22 11:16:56 原文链接
阅读:71 评论:0 查看评论

Gradle for Android(一)

$
0
0

第一篇( 从 Gradle 和 AS 开始 )

如果你是名Android开发新手,或者是名从eclipse切换到Android studio的新手,那么我强烈建议您follow我的文章,正如封面所见,利用gradle构建工具来自动构建你的Android项目。废话不多说,我们直接开始吧。


今天主要介绍Android studio工具的使用,以及cradle基础入门,使用cradle wrapper和如何从eclipse迁移到Android studio。

这篇文章记于2015.12.30,Android studio正式版本已经开发到1.5,而预览版已经到了2.0,所以转到Android studio吧。

当你第一次打开Android studio的时候,有一个视图显示你即将创建的环境以及确保你使用了最新的Android SDK和必要的google依赖包,同时会让你选择是否创建AVD,这样你就可以使用模拟器了。在这儿多说几句:

  1. 尽量使用Android studio 2.0,因为它真的变快了,而其模拟器的速度也提升了不少,我使用至今,也无发现任何bug,所以完全不用担心。
  2. 如果使用模拟器开发Android,尽量使用Genymotion模拟器,尽管其现在的Android6.0仍然有很多bug,但是在其以下版本,其速度还是非常快的,利用模拟器开发,为虚拟机安装文件夹浏览器,是及时查看SQLite表文件利器,具体操作办法,可以google。
  3. 尽量使用最新的23.0.0以上的构建版本。

理解基本的Gradle

如果你想创建一个Android project基于gradle,那么你必须写一个构建脚本,这个文件通常称之为build.grade,你可能已经觉察到了,当我们查看这一脚本,gradle会为我们提供很多默认的配置以及通常的默认值,而这极大的简化了我们的工作,例如ant和maven,使用他们的时候,我们需要编写大量的配置文件,而这很恶心。而gradle得默认配置,如果你需要使用自己的配置,完全可以简单的去重写他们就好。

Gradle脚本不是像传统的xml文件那样,而是一种基于Groovy的动态DSL,而Groovy语言是一种基于jvm的动态语言。

你完全不用担心,你在使用gradle的时候,还需要去学习Groovy语言,该语言很容易阅读,并且如果你已经学习过java的话,学习Groovy将不会是难事,如果你想开始创建自己的tasks和插件,那么你最好对Groovy有一个较深的理解,然而由于其基于jvm,所以你完全可能通过纯正的java代码或者其他任何基于jvm的语言去开发你自己的插件,关于插件开发,我们后续将会有相关介绍。

Project和tasks

在grade中的两大重要的概念,分别是project和tasks。每一次构建都是有至少一个project来完成,所以Android studio中的project和Gradle中的project不是一个概念。每个project有至少一个tasks。每一个build.grade文件代表着一个project。tasks在build.gradle中定义。当初始化构建进程,gradle会基于build文件,集合所有的project和tasks,一个tasks包含了一系列动作,然后它们将会按照顺序执行,一个动作就是一段被执行的代码,很像Java中的方法。

构建的生命周期

一旦一个tasks被执行,那么它不会再次执行了,不包含依赖的Tasks总是优先执行,一次构建将会经历下列三个阶段:

  1. 初始化阶段:project实例在这儿创建,如果有多个模块,即有多个build.gradle文件,多个project将会被创建。
  2. 配置阶段:在该阶段,build.gradle脚本将会执行,为每个project创建和配置所有的tasks。
  3. 执行阶段:这一阶段,gradle会决定哪一个tasks会被执行,哪一个tasks会被执行完全依赖开始构建时传入的参数和当前所在的文件夹位置有关。

build.gradle的配置文件

基于grade构建的项目通常至少有一个build.gradle,那么我们来看看Android的build.gradle:

buildscript {
   repositories {
        jcenter()
   }
   dependencies {
       classpath 'com.android.tools.build:gradle:1.2.3'
 } 
}

这个就是实际构建开始的地方,在仓库地址中,我们使用了JCenter,JCenter类似maven库,不需要任何额外的配置,grade还支持其他几个仓库,不论是远程还是本地仓库。

构建脚本也定义了一个Android构建工具,这个就是Android plugin的来源之处。Android plugin提供了所有需要去构建和测试的应用。每个Android应用都需要这么一个插件:

apply plugin: 'com.android.application'

插件用于扩展gradle脚本的能力,在一个项目中使用插件,这样该项目的构建脚本就可以定义该插件定义好的属性和使用它的tasks。

注意:当你在开发一个依赖库,那么你应该使用’com.android.library’,并且你不能同时使用他们2个,这将导致构建失败,一个模块要么使用Android application或者Android library插件,而不是二者。

当使用Android 插件的时候,Android标签将可以被使用,如下所示:

android {
       compileSdkVersion 22
       buildToolsVersion "22.0.1"
}

更多的属性我们将在第二章中进行讨论。

项目结构

和eclipse对比来看,Android studio构建的结构有很大的不同:

 MyApp
   ├── build.gradle
   ├── settings.gradle
   └── app
       ├── build.gradle
       ├── build
       ├── libs
       └── src
           └── main
               ├── java
               │   └── com.package.myapp
               └── res
                   ├── drawable
                   ├── layout
                   └── etc.

grade项目通常在根文件夹中包含一个build.gradle,使用的代码在app这个文件夹中,这个文件夹也可以使用其他名字,而不必要定义为app,例如当你利用Android studio创建一个project针对一个手机应用和一个Android wear应用的时候,模块将被默认叫做application和wearable。

gradle使用了一个叫做source set的概念,官方解释:一个source set就是一系列资源文件,其将会被编译和执行。对于Android项目,main就是一个source set,其包含了所有的资源代码。当你开始编写测试用例的时候,你一般会把代码放在一个单独的source set,叫做androidTest,这个文件夹只包含测试。

开始使用Gradle Wrapper

grade只是一个构建工具,而新版本总是在更迭,所以使用Gradle Wrapper将会是一个好的选择去避免由于gradle版本更新导致的问题。Gradle Wrapper提供了一个windows的batch文件和其他系统的shell文件,当你使用这些脚本的时候,当前gradle版本将会被下载,并且会被自动用在项目的构建,所以每个开发者在构建自己app的时候只需要使用Wrapper。所以开发者不需要为你的电脑安装任何gradle版本,在mac上你只需要运行gradlew,而在windows上你只需要运行gradlew.bat。

你也可以利用命令行./gradlew -v来查看当前gradle版本。下列是wrapper的文件夹:

   myapp/
   ├── gradlew
   ├── gradlew.bat
   └── gradle/wrapper/
       ├── gradle-wrapper.jar
       └── gradle-wrapper.properties

可以看到一个bat文件针对windows系统,一个shell脚本针对mac系统,一个jar文件,一个配置文件。配置文件包含以下信息:

#Sat May 30 17:41:49 CEST 2015
   distributionBase=GRADLE_USER_HOME
   distributionPath=wrapper/dists
   zipStoreBase=GRADLE_USER_HOME
   zipStorePath=wrapper/dists
   distributionUrl=https://services.gradle.org/distributions/
   gradle-2.4-all.zip

你可以改变该url来改变你的gradle版本。

使用基本的构建命令

使用你的命令行,导航到你的项目,然后输入:

$ gradlew tasks

这一命令将会列出所以可运行的tasks,你也可以添加–all参数,来查看所有的task。当你在开发的时候,构建项目,你需要运行assemble task通过debug配置:

$ gradlew assembleDebug

该任务将会创建一个debug版本的app,同时Android插件会将其保存在MyApp/app/build/ outputs/apk目录下。

除了assemble,还有三个基本的命令:

  • check 运行所以的checks,这意味着运行所有的tests在已连的设备或模拟器上。
  • build 是check和assemble的集合体。
  • clean 清楚项目的output文件。

保持旧的eclipse文件结构

关于如何将eclipse项目导入Android studio本文不再介绍。

android {
 sourceSets {
   main {
     manifest.srcFile 'AndroidManifest.xml'
     java.srcDirs = ['src']
     resources.srcDirs = ['src']
     aidl.srcDirs = ['src']
     renderscript.srcDirs = ['src']
     res.srcDirs = ['res']
     assets.srcDirs = ['assets']
}
 androidTest.setRoot('tests')
} 

}

在grade文件中配置,将会保存eclipse目录结构,当然,如果你有任何依赖的jar包,你需要告诉gradle它在哪儿,假设jar包会在一个叫做libs的文件夹内,那么你应该这么配置:

dependencies {
   compile fileTree(dir: 'libs', include: ['*.jar'])

该行意为:将libs文件夹中所有的jar文件视为依赖包。

总结

通过本文,我们可以学习到gradle的优势,以及为什么要使用gradle,我们简单的看了看Android studio,以及其是如何帮助我们生成build文件。

同时我们学习了Gradle Wrapper,其让我们维护以及分享项目变得更加简单,我们知道了如何创建一个新的项目在Android studio中,以及如何从eclispe迁移到Android studio并且保持目录结构。

同时我们学习了最基本的gradle task和命令行命令。在下一篇文章中,我们将会定制自己的build文件。

作者:gsg8709 发表于2017/9/22 13:46:53 原文链接
阅读:2 评论:0 查看评论

Gradle for Android(二)

$
0
0

第二篇( Build.gradle入门 )

在这一章,我们将学习以下内容:

  • 理解Gradle文件
  • 编写简单的构建任务
  • 自制构建脚本

理解Gradle脚本

当然我们现在讨论的所有内容都是基于Android studio的,所以请先行下载相关工具。当我们创建一个新的工程,Android studio会默认为我们创建三个gradle文件,两个build.gradle,一个settings.gradle,build.gradle分别放在了根目录和moudle目录下,下面是gradle文件的构成图:

 MyApp
   ├── build.gradle
   ├── settings.gradle
   └── app
       └── build.gradle

setting.gradle解析

当你的app只有一个模块的时候,你的setting.gradle将会是这样子的:

include ':app'

setting.gradle文件将会在初始化时期执行,关于初始化时期,可以查看上一篇博客,并且定义了哪一个模块将会被构建。举个例子,上述setting.gradle包含了app模块,setting.gradle是针对多模块操作的,所以单独的模块工程完全可以删除掉该文件。在这之后,Gradle会为我们创建一个Setting对象,并为其包含必要的方法,你不必知道Settings类的详细细节,但是你最好能够知道这个概念。

根目录的build.gradle

该gradle文件是定义在这个工程下的所有模块的公共属性,它默认包含二个方法:

buildscript {
     repositories {
         jcenter() 
     }
      dependencies {
          classpath 'com.android.tools.build:gradle:1.2.3'
      }
}
allprojects {
     repositories {
          jcenter() 
     }
}

buildscript方法是定义了全局的相关属性,repositories定义了jcenter作为仓库。一个仓库代表着你的依赖包的来源,例如maven仓库。dependencies用来定义构建过程。这意味着你不应该在该方法体内定义子模块的依赖包,你仅仅需要定义默认的Android插件就可以了,因为该插件可以让你执行相关Android的tasks。

allprojects方法可以用来定义各个模块的默认属性,你可以不仅仅局限于默认的配置,未来你可以自己创造tasks在allprojects方法体内,这些tasks将会在所有模块中可见。

模块内的build.gradle

模块内的gradle文件只对该模块起作用,而且其可以重写任何的参数来自于根目录下的gradle文件。该模块文件应该是这样:

 apply plugin: 'com.android.application'
   android {
       compileSdkVersion 22
       buildToolsVersion "22.0.1"
       defaultConfig {
           applicationId "com.gradleforandroid.gettingstarted"
           minSdkVersion 14
           targetSdkVersion 22
           versionCode 1
           versionName "1.0"
       }
       buildTypes {
           release {
               minifyEnabled false
               proguardFiles getDefaultProguardFile
                ('proguard-android.txt'), 'proguard-rules.pro'
           }
        } 
    }
    dependencies {
       compile fileTree(dir: 'libs', include: ['*.jar'])
       compile 'com.android.support:appcompat-v7:22.2.0'
     }

插件

该文件的第一行是Android应用插件,该插件我们在上一篇博客已经介绍过,其是google的Android开发团队编写的插件,能够提供所有关于Android应用和依赖库的构建,打包和测试。

Android

该方法包含了所有的Android属性,而唯一必须得属性为compileSdkVersion和buildToolsVersion:

  • compileSdkVersion:编译该app时候,你想使用到的api版本。
  • buildToolsVersion:构建工具的版本号。

构建工具包含了很多实用的命令行命令,例如aapt,zipalign,dx等,这些命令能够被用来产生多种多样的应用程序。你可以通过sdk manager来下载这些构建工具。

defaultConfig方法包含了该app的核心属性,该属性会重写在AndroidManifest.xml中的对应属性。

defaultConfig {
       applicationId "com.gradleforandroid.gettingstarted"
       minSdkVersion 14
       targetSdkVersion 22
       versionCode 1
       versionName "1.0"
}

第一个属性是applicationId,该属性复写了AndroidManifest文件中的包名package
name,但是关于applicationId和package
name有一些不同。在gradle被用来作为Android构建工具之前,package
name在AndroidManifest.xml有两个作用:其作为一个app的唯一标示,并且其被用在了R资源文件的包名。

Gradle能够很轻松的构建不同版本的app,使用构建变种。举个例子,其能够很轻松的创建一个免费版本和付费版本的app。这两个版本需要分隔的标示码,所以他们能够以不同的app出现在各大应用商店,当然他们也能够同时安装在一个手机中。资源代码和R文件必须拥有相同的包名,否则你的资源代码将需要改变,这就是为什么Android开发团队要将package name的两大功能拆分开。在AndroidManifest文件中定义的package name依然被用来作为包名和R文件的包名。而applicationid将被用在设备和各大应用商店中作为唯一的标示。

接下来将是minSdkVersion和targetSdkVersion。这两个和AndroidManifest中的很像。minSdkVersion定义为最小支持api。

versionCode将会作为版本号标示,而versionName毫无作用。

所有的属性都是重写了AndroidManifest文件中的属性,所以你没必要在AndroidManifest中定义这些属性了。

buildTypes方法定义了如何构建不同版本的app,我们将在下一篇博客中有所介绍。

依赖包

依赖模块作为gradle默认的属性之一(这也是为什么其放在了Android的外面),为你的app定义了所有的依赖包。默认情况下,我们依赖了所有在libs文件下的jar文件,同时包含了AppCompat这个aar文件。我们将会在下一篇博客中讨论依赖的问题。

让我们开始tasks吧

如果你想知道你多少tasks可以用,直接运行gradlew tasks,其会为你展示所有可用的tasks。当你创建了一个Android工程,那么将包含Android tasks,build tasks,build setup tasks,help tasks,install tasks,verification tasks等。

基本的tasks

android插件依赖于Java插件,而Java插件依赖于base插件。

base插件有基本的tasks生命周期和一些通用的属性。

base插件定义了例如assemble和clean任务,Java插件定义了check和build任务,这两个任务不在base插件中定义。

这些tasks的约定含义:

  • assemble: 集合所有的output
  • clean: 清除所有的output
  • check: 执行所有的checks检查,通常是unit测试和inst* rumentation测试
  • build: 执行所有的assemble和check
  • Java插件同时也添加了source sets的概念。

Android tasks

android插件继承了这些基本tasks,并且实现了他们自己的行为:

  • assemble 针对每个版本创建一个apk
  • clean 删除所有的构建任务,包含apk文件
  • check 执行Lint检查并且能够在Lint检测到错误后停止执行脚本
  • build 执行assemble和check

默认情况下assemble tasks定义了assembleDebug和assembleRelease,当然你还可以定义更多构建版本。除了这些tasks,android 插件也提供了一些新的tasks:

  • connectedCheck 在测试机上执行所有测试任务
  • deviceCheck 执行所有的测试在远程设备上
  • installDebug和installRelease 在设备上安装一个特殊的版本
  • 所有的install task对应有uninstall 任务

build task依赖于check任务,但是不依赖于connectedCheck或者deviceCheck,执行check任务的使用Lint会产生一些相关文件,这些报告可以在app/build/outputs中查看:

android studio的tasks

你根本不必要去执行gradle脚本在命令行中,Android studio有其对应的工具:
image

在这个界面,你要做的就是双击了。当然你也可以在Android studio中打开命令行,执行相关命令,具体操作就不介绍了。
image

自定义构建

当你在Android studio中自定义了gradle文件,需要更新project:
image

其实该按钮,执行了generateDebugSources tasks,该任务会生成所有必要的classes文件。

BuildConfig和resources
android {
    buildTypes {
        debug {
            buildConfigField "String", "API_URL",
               "\"http://test.example.com/api\""
               buildConfigField "boolean", "LOG_HTTP_CALLS", "true"
     }
       release {
            buildConfigField "String", "API_URL",
                "\"http://example.com/api\""
               buildConfigField "boolean", "LOG_HTTP_CALLS","false"
     } 
 }

类似这些定义的常量,当定义了这些属性后,你完全可以在代码中使用:BuildConfig.API_URL和BuildConfig.LOG_HTTP

最近,Android tools team也让其里面定义string变为可能:

android {
       buildTypes {
           debug {
               resValue "string", "app_name", "Example DEBUG"
           }
           release {
               resValue "string", "app_name", "Example"
            } 
       }
}

你可以在代码中使用这些string。其中“”不是必须得。

全局设置

如果你有很多模块在一个工程下,你可以这么定义你的project文件。

allprojects {
       apply plugin: 'com.android.application'
       android {
           compileSdkVersion 22
           buildToolsVersion "22.0.1"
       }
 }

这只会在你的所有模块都是Android app应用的时候有效。你需要添加Android 插件才能访问Android的tasks。更好的做法是你在全局的gradle文件中定义一些属性,然后再模块中运用它们。比如你可以在根目录下这么定义:

 ext {
       compileSdkVersion = 22
       buildToolsVersion = "22.0.1"
}  

那么你在子模块中就可以使用这些属性了:

android {
       compileSdkVersion rootProject.ext.compileSdkVersion
       buildToolsVersion rootProject.ext.buildToolsVersion
 }

Project properties文件

上述方法是一种办法,当然还有很多办法:

  • ext方法
  • gradle.properties文件
  • -p参数

    ext {
                  local = 'Hello from build.gradle'
                }
                   task printProperties << {
                   println local  // Local extra property
                   println propertiesFile // Property from file
                   if (project.hasProperty('cmd')) {
                       println cmd  // Command line property
                     }
                }     
    

当然你可以在gradle.properties中定义:

propertiesFile = Hello from gradle.properties

你也可以输入命令行:

$ gradlew printProperties -Pcmd='Hello from the command line'
:printProperties
Hello from build.gradle
Hello from gradle.properties
Hello from the command line

总结

在这篇博客中,我们细致的查看了Android studio生成的三个gradle文件,现在你应该能够自己去创建自己的gradle文件,我们还学习了最基本的构建任务,学习了Android 插件以及其tasks。

在接下来的几年里,Android开发生态将会爆炸性增长,很多有趣的依赖库将会让每个人去使用,在下一篇博客里面,我们将看看我们能有几种方式添加我们的依赖库,这样我们才能够避免造轮子。

作者:gsg8709 发表于2017/9/22 13:48:08 原文链接
阅读:3 评论:0 查看评论

Gradle for Android(三)

$
0
0

第三篇( 依赖管理 )

依赖管理

依赖管理是Gradle最闪耀的地方,最好的情景是,你仅仅只需添加一行代码在你的build文件,Gradle会自动从远程仓库为你下载相关的jar包,并且保证你能够正确使用它们。Gradle甚至可以为你做的更多,包括当你在你的工程里添加了多个相同的依赖,gradle会为你排除掉相同的jar包。在这一章我们将学习以下内容:

  • 仓库
  • 本地依赖
  • 详解依赖这一概念

仓库

当我们讨论依赖的时候,我们通常说的是远程仓库,就像那些依赖库专门用来提供给其他开发者使用的依赖库。手动管理依赖将会为你带来很大麻烦。你必须定位到该依赖文件位置,然后下载jar文件,复制该文件到你的项目,然后引用它们。通常这些jar文件还没有具体的版本号,所以你还必须去记忆它们的版本号,这样当需要更新的时候,你才会知道需要替换成哪个版本。你同时必须将该依赖包放在svn或者git上,这样你的其他同事才可以不用手动去下载这些依赖jar。

使用远程仓库可以解决这些问题,一个仓库可以被视为一些文件的集合体。Gradle不会默认为你的项目添加任何仓库。所以你需要把它们添加到repositories方法体内。如果是使用的是Android studio,那么工具已经为你准备好了这一切:

 repositories { 
    jcenter()
 }

Gradle支持三种不同的仓库,分别是:Maven和Ivy以及文件夹。依赖包会在你执行build构建的时候从这些远程仓库下载,当然Gradle会为你在本地保留缓存,所以一个特定版本的依赖包只需要下载一次。
一个依赖需要定义三个元素:group,name和version。group意味着创建该library的组织名,通常这会是包名,name是该library的唯一标示。version是该library的版本号,我们来看看如何申明依赖:

  dependencies { 
         compile 'com.google.code.gson:gson:2.3' 
         compile 'com.squareup.retrofit:retrofit:1.9.0'
  }

上述的代码是基于groovy语法的,所以其完整的表述应该是这样的:

  dependencies { 
        compile group: 'com.google.code.gson', name: 'gson', version:'2.3' 
        compile group: 'com.squareup.retrofit', name: 'retrofit' version: '1.9.0' 
  }

为你的仓库预定义

为了方便,Gradle会默认预定义三个maven仓库:Jcenter和mavenCentral以及本地maven仓库。你可以同时申明它们:

repositories { 
     mavenCentral() 
     jcenter() 
     mavenLocal() 
}

Maven和Jcenter仓库是很出名的两大仓库。我们没必要同时使用他们,在这里我建议你们使用jcenter,jcenter是maven中心库的一个分支,这样你可以任意去切换这两个仓库。当然jcenter也支持了https,而maven仓库并没有。

本地maven库是你曾使用过的所有依赖包的集合,当然你也可以添加自己的依赖包。默认情况下,你可以在你的home文件下找到.m2的文件夹。除了这些仓库外,你还可以使用其他的公有的甚至是私有仓库。

远程仓库

有些组织,创建了一些有意思的插件或者library,他们更愿意把这些放在自己的maven库,而不是maven中心库或jcenter。那么当你需要是要这些仓库的时候,你只需要在maven方法中加入url地址就好:

repositories { 
    maven { 
        url "http://repo.acmecorp.com/maven2" 
        }
}

同样的,Ivy仓库也可以这么做。Apache Ivy在ant世界里是一个很出名的依赖管理工具。如果你的公司有自己的仓库,如果他们需要权限才能访问,你可以这么编写:

repositories { 
  maven { 
      url "http://repo.acmecorp.com/maven2" 
      credentials { 
            username 'user' password 'secretpassword' 
      } 
  } 
}

注意:这不是一个好主意,最好的方式是把这些验证放在Gradle properties文件里,这些我们已经介绍过在第二章。

本地依赖


可能有些情况,你需要手动下载jar包,或者你想创建自己的library,这样你就可以复用在不同的项目,而不必将该library publish到公有或者私有库。在上述情况下,可能你不需要网络资源,接下来我将介绍如何是使用这些jar依赖,以及如何导入so包,如何为你的项目添加依赖项目。

文件依赖

如果你想为你的工程添加jar文件作为依赖,你可以这样:

dependencies { 
  compile files('libs/domoarigato.jar')
}

如果你这么做,那会很愚蠢,因为当你有很多这样的jar包时,你可以改写为:
dependencies { compile fileTree(‘libs’) }

默认情况下,新建的Android项目会有一个lib文件夹,并且会在依赖中这么定义(即添加所有在libs文件夹中的jar):

dependencies { 
    compile fileTree(dir: 'libs', include: ['*.jar'])
}

这也意味着,在任何一个Android项目中,你都可以把一个jar文件放在到libs文件夹下,其会自动的将其添加到编译路径以及最后的APK文件。

native包(so包)

用c或者c++写的library会被叫做so包,Android插件默认情况下支持native包,你需要把.so文件放在对应的文件夹中:

app ├── AndroidManifest.xml 
    └── jniLibs 
        ├── armeabi 
        │   └── nativelib.so  
        ├── armeabi-v7a 
        │   └── nativelib.so 
        ├── mips 
        │   └── nativelib.so 
        └── x86 
            └── nativelib.so 

aar文件

如果你想分享一个library,该依赖包使用了Android api,或者包含了Android 资源文件,那么aar文件适合你。依赖库和应用工程是一样的,你可以使用相同的tasks来构建和测试你的依赖工程,当然他们也可以有不同的构建版本。应用工程和依赖工程的区别在于输出文件,应用工程会生成APK文件,并且其可以安装在Android设备上,而依赖工程会生成.aar文件。该文件可以被Android应用工程当做依赖来使用。
创建和使用依赖工程模块
不同的是,你需要加不同的插件:

apply plugin: 'com.android.library' 

我们有两种方式去使用一个依赖工程。一个就是在你的工程里面,直接将其作为一个模块,另外一个就是创建一个aar文件,这样其他的应用也就可以复用了。
如果你把其作为模块,那你需要在settings.gradle文件中添加其为模块:

include ':app', ':library' 

在这里,我们就把它叫做library吧,如果你想使用该模块,你需要在你的依赖里面添加它,就像这样:

dependencies { 
    compile project(':library') 
}

使用aar文件

如果你想复用你的library,那么你就可以创建一个aar文件,并将其作为你的工程依赖。当你构建你的library项目,aar文件将会在 build/output/aar/下生成。把该文件作为你的依赖包,你需要创建一个文件夹来放置它,我们就叫它aars文件夹吧,然后把它拷贝到该文件夹里面,然后添加该文件夹作为依赖库:

repositories { 
      flatDir { dirs 'aars'
     }
}

这样你就可以把该文件夹下的所有aar文件作为依赖,同时你可以这么干:

dependencies { compile(name:'libraryname', ext:'aar')}

这个会告诉Gradle,在aars文件夹下,添加一个叫做libraryname的文件,且其后缀是aar的作为依赖。

依赖的概念


配置

有些时候,你可能需要和sdk协调工作。为了能顺利编译你的代码,你需要添加SDK到你的编译环境。你不需要将sdk包含在你的APK中,因为它早已经存在于设备中,所以配置来啦,我们会有5个不同的配置:

  • compile
  • apk
  • provided
  • testCompile
  • androidTestCompile

compile是默认的那个,其含义是包含所有的依赖包,即在APK里,compile的依赖会存在。

apk的意思是apk中存在,但是不会加入编译中,这个貌似用的比较少。

provided的意思是提供编译支持,但是不会写入apk。

testCompile和androidTestCompile会添加额外的library支持针对测试。
这些配置将会被用在测试相关的tasks中,这会对添加测试框架例如JUnit或者Espresso非常有用,因为你只是想让这些框架们能够出现在测试apk中,而不是生产apk中。

除了这些特定的配置外,Android插件还为每个构建变体提供了配置,这让debugCompile或者releaseProvided等配置成为可能。如果你想针对你的debug版本添加一个logging框架,这将很有用。这些内容的详细介绍,我会在下一个博客里详细介绍。

动态版本

在一些情形中,你可能想使用最新的依赖包在构建你的app或者library的时候。实现他的最好方式是使用动态版本。我现在给你们展示几种不同的动态控制版本方式:

 dependencies { 
      compile 'com.android.support:support-v4:22.2.+' 
      compile 'com.android.support:appcompat-v7:22.2+' 
      compile 'com.android.support:recyclerview-v7:+'
}

第一行,我们告诉gradle,得到最新的生产版本。第二行,我们告诉gradle,我们想得到最新的minor版本,并且其最小的版本号是2. 第三行,我们告诉gradle,得到最新的library。
你应该小心去使用动态版本,如果当你允许gradle去挑选最新版本,可能导致挑选的依赖版本并不是稳定版,这将会对构建产生很多问题,更糟糕的是你可能在你的服务器和私人pc上得到不同的依赖版本,这直接导致你的应用不同步。
如果你在你的build.gradle中使用了动态版本,Android studio将会警告你关于动态版本的潜在问题,就像你下面看到的这样:

Android studio UI操作依赖库

在使用Android studio中,最简单的添加新依赖包的方法是使用工程结构弹框。从文件按钮中打开界面,导航到依赖包导航栏,然后你就可以看到你当前的依赖包了:

当你想添加新的依赖包的时候,可以点击绿色的小按钮,你可以添加其他模块,文件,甚至是上网搜索。

使用Android studio的界面让你能够很简单的浏览你项目中的所有依赖,并且添加新的依赖包。你不必在build.gradle中手动的添加代码了,并且你可以直接搜索JCenter库中的依赖资源。

总结

在这一章里,我们了解了多种方式添加依赖,我们学习了什么是仓库,以及如何使用他们,同时学习了如何在不使用仓库的情况下使用jar文件。
现在知道了依赖包的属性配置,动态版本控制等。
我们也谈到了关于在多个环境下构建app变种,在下一章,我们将会学习到什么是构建变种,以及为什么他们很重要,构建变种将会使得开发测试以及分发app变得更加容易。理解变种的工作原理可以加快你的开发和分发效率。

作者:gsg8709 发表于2017/9/22 13:49:03 原文链接
阅读:3 评论:0 查看评论
Viewing all 5930 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>