分享 Android 开发性能优化的技术要点

 更新时间:2016年9月20日 19:54  点击:1432
在Android开发,性能是一个大问题,弄不好会使内存崩溃。本文我们分享总结的一些 Android 性能调优的技术点,Android如何做内存性能优化。

Android性能调优涉及到多方面的工作,因本人技术水平有限,目前只总结了以下部分,希望大家继续补充。

要点

使用异步

    保持 APP 的高度响应,不要在 UI 线程做耗时操作,多使用异步任务
    使用线程时要做好线程控制;使用队列、线程池
    谨慎使用糟糕的 AysncTask 、 Timer
    警惕异步任务引起的内存泄露
    应该异步任务分类,比如 HTTP ,图片下载,文件读写,每一类的异步任务维护一个任务队列,而不是每一个任务都开一个线程( Volley 表示我一个可以搞定这些全部 _(:з」∠)_)
    这些常用的任务应该做好优先级处理(一般 JSON 数据优先于图片等静态数据的请求)
    一般异步任务应该开启一个 SingleAsyncTask ,保证一时只有一个线程在工作
    HTTP 和图片下载尽量使用同一套网络请求
    使用 MVP 模式规范大型 Activity 类的行为,避免异步任务造成的内存泄露

避免内存泄露

    了解虚拟机内存回收机制
    频繁 GC 也会造成卡顿,避免不必要的内存开销
    错误的引用姿♂势造成的内存泄露(啊~要泄了~)
    常见的 Activity 泄露(单例、 Application 、后台线程、无限动画、静态引用)
    Bitmap 泄露( HoneyComb 这个问题之前压力好大)
    尽量使用 IntentService 代替 Service ,前者会自动 StopItself
    排查内存泄露问题的方法(我一直以来都是简单暴力的人肉 dump 检查大法)
    使用 LeakCanary 自动检查 Activity 泄露问题
    对内存负载要保持敏感( Sharp )

视图优化

    布局优化、减少层次, Include Merge
    使用 ViewStub 避免不必要的 LayoutInflate ,使用 GONE 代替重复 LayoutInflate 同一个布局
    避免过度绘制,应该减少不必要的布局背景;布局层次太深会造成过度绘制以及 Measure 、 Layout 等方法时间复杂度的指数增长
    使用过渡动画,比如给图片的呈现加一个轻量的淡入效果会让视觉上变得流畅许多
    避免过度的动画,不要让一个界面同时出现多出动画,比如 List 滚动时 Item 项要停止播放动画或者 GIF
    复杂动画使用 SurfaceView 或 TextureView
    尽量提供多套分辨率的图片,使用矢量图

Adapter 优化

    复用 convertView ,用 ViewHolder 代替频繁 findViewById
    不要重复 setListener ,要使用 v.getId 来复用 Listener ,不然会创建一堆 Listener 导致频繁 GC
    多布局要采用 MutilItemView ,而不是使用一个大布局然后动态控制需要现实的部分
    不要在 getView 方法做做耗时的操作
    快速滚动列表的时候,可以停止加载列表项的图片,停止列表项的动画,不要在这时候改变列表项的布局
    尽量用 RecyclerView (增量 Notify 和 RecycledViewPool 带你飞)

代码优化

    算法优化,减少时间复杂度,参考一些经典的优化算法
    尽量使用 int ,而不是 float 或者 double
    尽量采用基本类型,避免无必要的自动装箱和拆箱,浪费时间和空间
    选用合适的集合类(尽量以空间换时间)、选用 Android 家的 SparseArray,SparseBooleanArray 和 LongSparseArray
    避免创建额外的对象( StringBuilder )
    使用 SO 库完成一些比较独立的功能(高斯模糊)
    预处理(提前操作)一些比较耗时的初始化工作统一放到启动图处理
    懒加载(延迟处理)规避 Activity 的敏感生命周期
    Log 工具类,要在编译时删掉调试代码,而不是在运行时通过判断条件规避
    优先使用静态方法、公有方法还是公有方法?速度区别很大哦
    类内部直接对成员变量进行操作,不要使用 getter/setter 方法,调用方法耗额外的时间
    给内部类访问的外部类成员变量要声明称包内可访问,而不是私有,不然编译的时候还是会自动创建用于访问外部类成员变量的方法
    遍历集合时,使用 i++代替 Iterator ,后者需要额外的对象操作,应在循环体内避免这种情况
    如果一个基本类型或者 String 的值不会改变,尽量用 final static ,编译时会直接用变量的值替换变量,也就不需要在查询变量的值了

其他优化

    数据库优化:使用索引、使用异步线程
    网络优化 …… 一堆优秀的轮子
    避免过度使用依赖注入框架,大量的反射
    不过过度设计 /抽象,多态看起来很有设计感,代价就是额外的代码、空间、时间
    尽量不要开启多进程,进程的开销很大

APK 瘦身

    开启混淆
    使用 zipalign 工具优化 APK
    适当有损图片压缩、使用矢量图
    删除项目中冗余的资源,之前写过一些删除没有 res 资源的脚本
    动态加载模块化,项目拆分啊!

性能问题的排查方法

    GPU 条形图,没事开来看看淘宝
    过度绘制颜色,嗯,不要一篇姨妈红就好
    LeakCanary ,自动检测 Activity 泄露,挺好用的
    TraceView ( Device Monitor ), Systrace ,分析哪些代码占用的 CPU 时间太大,屡试不爽
    Lint ,检查不合理的 res 资源
    layoutopt (还是 optlayout ?),对当前布局提出优化建议,已被 lint 替代,但是还能用
    HierarchyViewer ,查看手机当前界面的布局层次,布局优化时常用(只用于模拟器,真机上用要 ROOT ,不想 ROOT 加得使用 ViewServer )
    StrictMode , UI 操作、网络操作等容易出现性能问题的地方,如果出现异常情况 StrictMode 会报警


Android内存性能优化


刚入门的童鞋肯能都会有一个疑问,Java不是有虚拟机了么,内存会自动化管理,我们就不必要手动的释放资源了,反正系统会给我们完成。其实Java中没有指针的概念,但是指针的使用方式依然存在,一味的依赖系统的gc,很容易就造成了内存的浪费。


Java基于垃圾回收的内存机制


Java的内存管理机制会自动回收无用对象所占用的内存,减轻手工管理内存的负担

1、C/C++: 从申请、使用、释放都需要手工管理

2、Java:无用的对象的内存会被自动回收


\

什么样的对象是无用的对象

1、Java通过引用来操作一个具体的对象,引用类似于C 中的指针。一个对象可以持有其他对象的引用。

2、从一组根对象(GC Roots)开始,按对象之前的引用关系遍历所有对象,在遍历过程中标记所有的可达对象。如果一个对象由根对象出发不可达,则将它作为垃圾收集。

GCRoot 都有哪些?

1、 Class:由系统的类加载器加载的类对象

2、 Static Fields

3、 Thread:活着的线程

4、 Stack Local: java方法的局部变量或参数

5、 JNI Local: JNI方法中的局部引用

6、 JNI Global: 全局的JNI引用

7、 Monitor used: 用于同步的监控对象

8、Help by VM: 用于JVM特殊目的由GC保留的对象


\


\


\

Java程序中的内存泄漏

对象的内存在分配之后无法通过程序的执行逻辑释放对该对象的引用,不能被回收该对象所占内存

内存泄漏的危害

1、 引起OutOfMemoryError

2、 内存占用高时JVM虚拟机会频繁触发GC, 影响程序响应速度

3、内存占用大的程序容易被各种清理优化程序中止,用户也更倾向于卸载这些程序

Android应用的开发语言为Java,每个应用最大可使用的堆内存受到Android系统的限制

Android每一个应用的堆内存大小有限

1、 通常的情况为16M-48M

2、 通过ActivityManager的getMemoryClass()来查询可用堆内存限制

3、3.0(HoneyComb)以上的版本可以通过largeHeap=“true”来申请更多的堆内存

Nexus S(4.2.1):normal 192, largeHeap 512

4、如果试图申请的内存大于当前余下的堆内存就会引发OutOfMemoryError()

5、应用程序由于各方面的限制,需要注意减少内存占用,避免出现内存泄漏。

用MAT工具来检测内存泄漏

在试图窗口中新建一个Memory Analysis会出现一个


\

没有的可以去http://www.eclipse.org/mat/downloads.php安装一下MAT

在Android 的调试环境DDMS下,找到Heap dump


\


\

Dump下当前内存中的镜像文件,*****.hprof


\

能清楚的看到每一个部分暂用的内存大小。

也可以切换试图,group查看不同包不同类的占用细节。


\

Heap dump

? 包含了触发Heap dump生成的时刻Java进程的内存快照,主要内容为各个Java类和对象在堆内存中的分配情况

Memory Analyzer Tool (MAT)


常见内存泄露原因

Context对象泄漏

1、如果一个类持有Context对象的强引用,就需要检查其生存周期是否比Context对象更长。否则就可能发生Context泄漏。

2、View持有其创建所在Context对象的引用,如果将View对象传递给其它生存周期比View所在Context更长的强引用,就可能会引起内存泄漏。

例如View#setTag(int, Object)的内存泄漏https://code.google.com/p/android/issues/detail?id=18273

3、把Context对象赋给static变量。

避免Context对象泄漏Checklist

1、检查所有持有对Context对象强引用的对象的生命周期是否超出其所持有的Context对象的生命周期。

2、检查有没有把View传出到View所在Context之外的地方,如果有的话就需要检查生命周期。

3、工具类中最好不要有Context成员变量,尽量在调用函数时直接通过调用参数传入。如果必须有Context成员变量时,可以考虑使用WeakReference来引用Context对象。

4、View持有其创建所在Context对象的引用,如果将View对象传递给其它生存周期比View所在Context更长的强引用,就可能会引起内存泄漏。

5、 检查把Context或者View对象赋给static变量的地方,看是否有Context泄漏。

6、检查所有把View放入容器类的地方(特别是static容器类),看是否有内存泄漏。7、使用WeakHashMap也需要注意有没有value-key的引用。

7、尽量使用ApplicationContext。

Handler对象泄漏

1、发送到Handler的Message实际上是加入到了主线程的消息队列等待处理,每一个Message持有其目标Handler的强引用。

如我们通常使用的匿名内部类Handler


HandlermHandler = new Handler() {
    @Override
    public voidhandleMessage(Message msg) {
       mImageView.setImageBitmap(mBitmap);
    }
}


上面是一段简单的Handler的使用。当使用内部类(包括匿名类)来创建Handler的时候,Handler对象会隐式地持有一个外部类对象(通常是一个Activity)的引用,因为View会依附着一个Activity。而Handler通常会伴随着一个耗时的后台线程(例如从网络拉取图片)一起出现,这个后台线程在任务执行完毕(例如图片下载完毕)之后,通过消息机制通知Handler,然后Handler把图片更新到界面。然而,如果用户在网络请求过程中关闭了Activity,正常情况下,Activity不再被使用,它就有可能在GC检查时被回收掉,但由于这时线程尚未执行完,而该线程持有Handler的引用(不然它怎么发消息给 Handler?),这个Handler又持有Activity的引用,就导致该Activity无法被回收(即内存泄露),直到网络请求结束(例如图片下载完毕)。另外,如果你执行了Handler的postDelayed()方法,该方法会将你的Handler装入一个Message,并把这条 Message推到MessageQueue中,那么在你设定的delay到达之前,会有一条MessageQueue -> Message -> Handler -> Activity的链,导致你的Activity被持有引用而无法被回收。

当然,应为是Handler对外部持有引用的原因,我们就可以将Activity设置为一个弱引用,在不必要的时候,不再执行内部方法。



/**
 * @author zhoushengtao
 * @since 2013-12-16 下午3:25:36
 */
 
import android.app.Activity;
importandroid.content.Context;
importandroid.os.Handler;
importandroid.os.Message;
 
importjava.lang.ref.WeakReference;
 
publicclass WeakRefHandler extends Handler
{
    WeakReference<context> mWeakContext;
 
    public WeakRefHandler(Context context)
    {
        mWeakContext = newWeakReference<context>(context);
    }
 
    @Override
    public void handleMessage(Message msg)
    {
        if((mWeakContext.get() instanceofActivity )&& ((Activity)mWeakContext.get()).isFinishing())
                return ;
        if(mWeakContext==null){
            return ;
        }
        super.handleMessage(msg);
    }
}


2、Non-staticinner class 和anonymous class持有其outer class的引用。


Drawable.Callback引起的内存泄漏

Drawable对象持有Drawable.callback的引用。当把一个Drawable对象设置到一个View时,Drawable对象会持有该View的引用作为Drawable.Callback


\

避免Drawable.Callback引起内存泄漏

? 尽量不要在static成员中保存Drawable对象

? 对于需要保存的Drawable对象, 在需要时调用Drawable#setCallback(null).


\

其他内存泄漏<??#65533;"http://www.2cto.com/kf/ware/vc/" target="_blank" class="keylink">vc3Ryb25nPjwvcD4KPHAgYWxpZ249"left"> 1、Android DigitalClock引起的内存泄漏http://code.google.com/p/android/issues/detail?id=17015

2、使用Map容器类时,作为Key 的类没有正确的实现hashCode和equal函数

其他内存泄漏

? JNI程序中的内存泄漏

1、 Malloc/free。

2、 JNI Global reference

? Thread-Local Variable

1、 相当于Thread对象的成员变量, 可以存储线程相关的状态

2、 如果thread是alive状态,那么Thread-Local中的对象就无法被GC。

进程内存占用监测工具

Dumpsys

? $ dumpsys meminfo [pid]


\

Procrank + Shell脚本

? #procrank

1、 VSS - Virtual Set Size 虚拟耗用内存(包含共享库占用的内存)

2、 RSS - Resident Set Size 实际使用物理内存(包含共享库占用的内存)

3、 PSS - Proportional Set Size 实际使用的物理内存(比例分配共享库占用的内存)

4、 USS - Unique Set Size 进程独自占用的物理内存(不包含共享库占用的内存)


\

Shell脚本

#!/bin/bash

while true; do

adbshell procrank " grep "com.qihoo360.mobilesafe"

sleep1

done


当然,部分机型的sh都是经过第三方手机商精简过的,很多命令都用不了。Procrank,就是一个经常被精简掉的命令。

鉴于此:

自己写了一个小工具,检测内存的实时变化,

Github地址:https://github.com/stchou/JasonTest


\


\

小结

1. 保存对象前要三思

I. 对象本身有无隐含的引用

II. 保存后何时能够回收

2. 要了解常见的隐含引用

I. anonymous class outer class

II. View to context

3. 要通过各种工具检查内存占用是否有异常

4. 创建大对象时,要检查它的生命周期


CustomShapeImageView 是一个可以自己定制ImageView形状的开源库,它通过使用 PorterDuffXfermode API,将图片及可缩放矢量图形(SVGs)结合实现多种可定制形状的ImageView。

CustomShapeImageView是Android开发的第三方类库,具有不限于圆形ImageView的多种形状ImageView,项目开发必备

github下载地址:https://github.com/MostafaGazar/CustomShapeImageView

1、首先源码中有一个第三方类库 :library

先要把Library导入到项目中,

如果你会导入季,直接跳过


关于Eclipse 和 IDEA 导入library库文件 的步骤

这里我们以PullToRefresh(上拉刷新下拉加载)组件的library为例

下载地址:

https://github.com/chrisbanes/Android-PullToRefresh


现在我们需要把library文件夹导入到Eclipse或者IDEA中去

一、IDEA 导入library库文件步骤

1、首先我们要有一个项目,没有的就创建一个吧

2、右击项目名称点击Open Module Settings(F4)


3、可以看到这样的界面


接下来在中间部分 点击绿色的加号 导入Module


找到要导入的library类库的目录


点击OK 后,,新的界面选择 第一个选项 Create module from existing sources,然后下一步知道import操作完成


4、然后就可以看到这样的界面,中间界面 多了一个library文件夹


5、接着点击最右边界面的绿色加号按钮 选择第三个Module Dependency,注意中间部分要选择你要导入library库文件的目录,即此时在中间界面选中demo文件夹,在按绿色按钮添加


6、可以看到有library文件夹可以选择 选择OK就行了 然后OK 结束设置


7、这是就可以看到你的项目里多了一个library文件夹


打开library文件夹可以看到文件夹内容都在,


8、我们在主Activity中添加一个library 提供的类检查是否导入成功,不报错可导入成功


二、Eclipse 导入library库文件步骤

1、导入


2、选择 Android/Existing Android Code Into Workspace


3、选择library文件夹目录 ,记得选中 Copy projects into workspace


4、可以看到项目目录多了library


5、右键library 选择properties (在最下面)

点击is Library --》ok


6、然后右击要导入library库文件的的项目 选择properties 添加Add 选择要导入的library文件夹


7、然后使用library库文件提供的类检测是否导入正确 (注意项目和library库文件需要在同一个目录下,即同一个工作空间)



继续讲解CustomShapeImageView类库


2、源码中res文件夹下有一个raw文件夹 复制到自己的项目中(选择性复制,是一些特殊的图形)

可以看到这里有一堆.svg格式的文件。

SVG可以算是目前最最火热的图片文件格式,这里作者已经给我们写好了几个特殊的图形

如果想要自定义更多形状的话,可以学习下SVG

1、shape_5.svg 五边形


2、shape_circle_2.svg 贝壳形


3、shape_flower.svg 花形


4、shape_heart.svg 心形


5、shape_star 星形1

6、shape_star 星形2

7、shape_star 星形3



3、接下来就是使用了

如果我们使用raw文件夹下的svg写的形状作为ImageView的形状

则:这里一个app:svg_raw_resource="@raw/shape_star_3" 这里就是指定图片形状为res/raw文件夹下的哪一个


<com.meg7.widget.SvgImageView
android:layout_width="50dp"
android:layout_height="50dp"
android:src="@drawable/hydrangeas"
app:svg_raw_resource="@raw/shape_star_3"
android:scaleType="centerCrop" />


重点来了:

实际开发,最常用的莫过于圆形图片了,那么上面几个都是特殊图形,要使用圆形ImageView怎么办?

看类库,几个源码


BaseImageView.java 是基本类

而CircleImageView.java类则是继承BaseImageView.java的类,用于圆形ImageView

使用: 这个就简单了,和普通ImageView一样使用,标签变了而已


android:layout_width="100dp"

android:layout_height="100dp"

android:src="@drawable/hydrangeas"

android:scaleType="centerCrop" />


长方形ImageView :RectangleImageView.java


android:layout_width="100dp"

android:layout_height="100dp"

android:src="@drawable/hydrangeas"

android:scaleType="centerCrop" />


以上便是主要需要的部分了。

源码中CustomShapeImageView.java看了下源码是默认显示圆形图片的ImageView,里面有圆形,方形,自定义形,看情况使用了,个人感觉,以上的几个使用起来就足够了。


本文分享在Android开发中使用TextView实现实现文字和图片混排(文字环绕图片)效果的实例,后面还有一个支持Span折叠的实例。

在平时我们做项目中,或许有要对一张图片或者某一个东西进行文字和图片说明,这时候要求排版美观,所以会出现文字和图片混排的情况,如图:

Android实现文字和图片混排(文字环绕图片)效果


这种情况就是上下两个文字说明是连续在一起的,这就要求我们计算上面的文字说明怎么和下面的文字说明连贯结合在一起呢,这就要求我们进行计算了,下面给出代码,代码中也有详细的注释,原理也很简单。

因为算是比较简单,直接就在activity中去计算了:

package com.example.test;
import android.app.Activity;
import android.graphics.Paint;
import android.os.Bundle;
import android.view.ViewTreeObserver;
import android.widget.ImageView;
import android.widget.TextView;
public class MainActivity extends Activity {
  boolean imageMeasured = false;
  TextView tv_right;
  TextView tv_bottom;
  static final String text = "叶凡:小说主角,与众老同学在泰山聚会时一同被九龙拉棺带离地球," +
      "进入北斗星域,得知自己是荒古圣叶凡 叶凡体。历险禁地,习得源术,斗圣地世家,战太古生物," +
      "重组天庭,叶凡辗转四方得到许多际遇和挑战,功力激增,眼界也渐渐开阔。一个浩大的仙侠世界," +
      "就以他的视角在读者面前展开。姬紫月:姬家小姐,出场年龄十七岁。被叶凡劫持一同经历青铜古殿历险," +
      "依靠碎裂的神光遁符解除禁制,反过来挟持叶凡一同进入太玄派寻找秘术。" +
      "在叶凡逃离太玄后姬紫月在孔雀王之乱中被华云飞追杀,又与叶凡[2]相遇,被叶凡护送回姬家" +
      ",渐渐对叶凡产生微妙感情。后成为叶凡的妻子,千载后于飞仙星成仙,在叶凡也进入仙路后再见庞博:" +
      "叶凡大学时最好的朋友,壮硕魁伟,直率义气。到达北斗星域后因服用了圣果被灵墟洞天作为仙苗," +
      "在青帝坟墓处为青帝十九代孙附体离去,肉身被锤炼至四极境界。后叶凡与黑皇镇压老妖神识," +
      "庞博重新掌控自己身躯,取得妖帝古经和老妖本体祭炼成的青莲法宝,习得妖帝九斩和天妖八式," +
      "但仍伪装成老妖留在妖族。出关后找上叶凡,多次与他共进退。星空古路开启后由此离开北斗," +
      "被叶凡从妖皇墓中救出,得叶凡授予者字秘、一气化三清,与叶凡同闯试炼古路,一起建设天庭";
  // 屏幕的高度
  int screenWidth = 0;
  // 总共可以放多少个字
  int count = 0;
  // textView全部字符的宽度
  float textTotalWidth = 0.0f;
  // textView一个字的宽度
  float textWidth = 0.0f;
  Paint paint = new Paint();
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    tv_right = (TextView) findViewById(R.id.test_tv_right);
    tv_bottom = (TextView) findViewById(R.id.test_tv_bottom);
    final ImageView imageView = (ImageView) findViewById(R.id.test_image);
    imageView.setImageResource(R.drawable.ee);
    screenWidth = getWindowManager().getDefaultDisplay().getWidth();
    /**
     * 获取一个字的宽度
     */
    textWidth = tv_right.getTextSize();
    paint.setTextSize(textWidth);
    /**
     * 因为图片一开始的时候,高度是测量不出来的,通过增加一个监听器,即可获取其图片的高度和长度
     */
    ViewTreeObserver vto = imageView.getViewTreeObserver();
    vto.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
      public boolean onPreDraw() {
        if (!imageMeasured) {
          imageMeasured = true;
          int height = imageView.getMeasuredHeight();
          int width = imageView.getMeasuredWidth();
          drawImageViewDone(width, height);
        }
        return imageMeasured;
      }
    });
  }
  private void drawImageViewDone(int width, int height) {
    // 一行字体的高度
    int lineHeight = tv_right.getLineHeight();
    // 可以放多少行
    int lineCount = (int) Math.ceil((double) height / (double) lineHeight);
    // 一行的宽度
    float rowWidth = screenWidth - width - tv_right.getPaddingLeft() - tv_right.getPaddingRight();
    // 一行可以放多少个字
    int columnCount = (int) (rowWidth / textWidth);
    // 总共字体数等于 行数*每行个数
    count = lineCount * columnCount;
    // 一个TextView中所有字符串的宽度和(字体数*每个字的宽度)
    textTotalWidth = (float) ((float) count * textWidth);
    measureText();
    tv_right.setText(text.substring(0, count));
    // 检查行数是否大于设定的行数,如果大于的话,就每次减少一个字符,重新计算行数与设定的一致
    while (tv_right.getLineCount() > lineCount) {
      count -= 1;
      tv_right.setText(text.substring(0, count));
    }
    tv_bottom.setPadding(0, lineCount * lineHeight - height, 0, 0);
    tv_bottom.setText(text.substring(count));
  }
  /**
   * 测量已经填充的长度,计算其剩下的长度
   */
  private void measureText() {
    String string = text.substring(0, count);
    float size = paint.measureText(string);
    int remainCount = (int) ((textTotalWidth - size) / textWidth);
    if (remainCount > 0) {
      count += remainCount;
      measureText();
    }
  }
}

其中xml文件布局如下:

<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent"
  android:orientation="vertical" >
  <RelativeLayout
    android:layout_width="fill_parent"
    android:layout_height="wrap_content" >
    <ImageView
      android:id="@+id/test_image"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:scaleType="fitXY" />
    <TextView
      android:id="@+id/test_tv_right"
      android:layout_width="fill_parent"
      android:layout_height="wrap_content"
      android:layout_toRightOf="@id/test_image"
      android:gravity="fill_horizontal"
      android:paddingLeft="7dp"
      android:textSize="16sp" />
    <TextView
      android:id="@+id/test_tv_bottom"
      android:layout_width="fill_parent"
      android:layout_height="wrap_content"
      android:layout_below="@id/test_image"
      android:gravity="fill_horizontal"
      android:textSize="16sp" />
  </RelativeLayout>
</ScrollView>

代码很少,原理也很简单,后来发现这种做法在大部分手机运行是完美的,但是少部分手机还是有点问题。是什么问题呢,是在我们测量textView的长度的是,因为是我们刚刚进行setText,然后马上进行测量,这样得到的结果是不正确的,所以大家可以优化一下。温馨提示,当我们setText之后,可以延时一些时间再去测量,这样获取的值就是挣钱的了,当然那个延迟的时间很短50毫秒就可以了,因为我们要相信textView的绘制速度还是很快的。



Android 文字环绕 图文混排 支持Span折叠

先直接上效果图


http://s5.sinaimg.cn/middle/47021dd4gb63d6d54ffa4&690


Android <wbr>文字环绕 <wbr>图文混排 <wbr>支持Span折叠


上图为实现目标,实现了Android图文混排,文字环绕,支持Span的识别,表情的嵌入,支持文字字体大小的设置等。

 


由于项目中需要用到图文混排技术,在此稍微研究了两天,出来一个效果还算不错的东西

图文混排技术,在不少Android应用中都已经实现,说穿了其实就是两个TextView加一个ImageView的布局罢了,代码里面实现下String的剪切就可以了,不过我这里的这个除了要实现混排效果外,还要支持Span,支持表情等,这就有点麻烦了。下面慢慢分解。先贴出RichTextImageView的布局。
    
<?xml version="1.0" encoding="utf-8"?>
<!-- com.demonzym.richtextdemo.RichTextImageView -->
<com.demonzym.richtextdemo.RichTextImageView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical"
    android:id="@+id/richview" >

    <LinearLayout
        android:id="@+id/linearLayout1"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content" >
       
        <RelativeLayout
            android:id="@+id/layout_preimage_isgif_left"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="0"
            android:visibility="gone" >

            <ImageView
                android:id="@+id/preimage_statues_left"
                android:layout_width="134dip"
                android:layout_height="134dip"
                android:background="@drawable/preview_back"
                android:cropToPadding="true"
                android:scaleType="centerCrop"
                android:src="@drawable/preview_back_small" />

            <ImageView
                android:id="@+id/preimage_isgif_left"
                android:layout_width="24dip"
                android:layout_height="17dip"
                android:layout_alignBottom="@+id/preimage_statues"
                android:layout_alignRight="@+id/preimage_statues"
                android:layout_marginBottom="9dip"
                android:layout_marginRight="7dip" />
        </RelativeLayout>

        <com.demonzym.richtextdemo.RichTextView
            android:id="@+id/lefttext"
            android:layout_width="wrap_content"
            android:layout_height="fill_parent"
            android:layout_weight="1"/>

        <RelativeLayout
            android:id="@+id/layout_preimage_isgif_right"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="0"
            android:visibility="gone" >

            <ImageView
                android:id="@+id/preimage_statues_right"
                android:layout_width="134dip"
                android:layout_height="134dip"
                android:background="@drawable/preview_back"
                android:cropToPadding="true"
                android:scaleType="centerCrop"
                android:src="@drawable/preview_back_small" />

            <ImageView
                android:id="@+id/preimage_isgif_right"
                android:layout_width="24dip"
                android:layout_height="17dip"
                android:layout_alignBottom="@+id/preimage_statues"
                android:layout_alignRight="@+id/preimage_statues"
                android:layout_marginBottom="9dip"
                android:layout_marginRight="7dip" />
        </RelativeLayout>
    </LinearLayout>

    <com.demonzym.richtextdemo.RichTextView
        android:id="@+id/bottomtext"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"/>

</com.demonzym.richtextdemo.RichTextImageView>

 

布局中的RichTextView是另外封装的一个实现Span的TextView,就是实现效果图中表情啊,@啊,#话题# 之类的,了解Span的都懂的,就不细说了。读者研究这个图文混排的时候,可以直接用TextView替代。

 

RichTextImageView  即本文中的图文混排的View。继承于LinearLayout,该类实现的关键代码如下:

 

public void setText(String t) {
  mTopText.setText("");
  mBottomText.setText("");
  mTopText.setVisibility(View.INVISIBLE);
  mBottomText.setVisibility(View.GONE);
  bRequestBottomLayout = true;

  if (t == null)
   t = "";

//  Log.e("setText", t);
 
  mTextContent = t;

  mTopText.setText(mTextContent);

  requestLayout();
 
 }

 


public void setImage(int id) {
  mImageContent = id;
  mPreviewImage.setImageResource(id);
  mPreview.setVisibility(View.VISIBLE);

  if (!Util.isStringEmpty(mTextContent))
   setText(mTextContent);
 }

 

private void iniViews() {
  mLinearLayout = (LinearLayout) findViewById(R.id.linearLayout1);
  mTopText = (RichTextView) findViewById(R.id.lefttext);
  mBottomText = (RichTextView) findViewById(R.id.bottomtext);

  if(IMAGE_LOCATION == IMAGE_LOCATION_RIGHT){
   mPreview = (RelativeLayout) findViewById(R.id.layout_preimage_isgif_right);
   mPreviewImage = (ImageView) findViewById(R.id.preimage_statues_right);
   mPreviewImageIsGif = (ImageView) findViewById(R.id.preimage_isgif_right);
  }else if(IMAGE_LOCATION == IMAGE_LOCATION_LEFT){
   mPreview = (RelativeLayout) findViewById(R.id.layout_preimage_isgif_left);
   mPreviewImage = (ImageView) findViewById(R.id.preimage_statues_left);
   mPreviewImageIsGif = (ImageView) findViewById(R.id.preimage_isgif_left);   
  }
 }
 


 public void setImageLocation(int l){
  if(l != IMAGE_LOCATION_RIGHT && l != IMAGE_LOCATION_LEFT)
   return;
  else{
   IMAGE_LOCATION = l;
   mPreview.setVisibility(View.GONE);
   iniViews();
   setImage(mImageContent);
  }
 }

 


@Override
 protected void onLayout(boolean changed, int l, int t, int r, int b) {
  // TODO Auto-generated method stub
  super.onLayout(changed, l, t, r, b);

  if (Util.isStringEmpty(mTextContent))
   return;

//  Log.e("top text 宽 高",
//    mTopText.getWidth() + " " + mTopText.getHeight());
//  Log.e("top text 单行高度", "" + mTopText.getLineHeight());
 
//  Rect rect = getStringRect(mTextContent, mTopText.getPaint());
//  Log.e("文本的宽 高", rect.width() + " " + rect.height());
 
  // toptext最多能显示的行数
  int topTextMaxRows = mTopText.getHeight() / mTopText.getLineHeight();
    
  // 显示这些内容真实占用的行数
  int textRows = mTopText.getLineCount();
//  Log.e("top text最多能显示的行数", "" + topTextMaxRows);
//  Log.e("当前文本实际占用的行数", "" + textRows);
 
  //由于文本可能带有表情,重新计算显示行数,以保证显示正确
  Rect lineR = new Rect();
  int realH = 0;     //toptext真实高度
  int i = 0;
  for(i = 0; i < topTextMaxRows; i++){
   try{
    mTopText.getLineBounds(i, lineR);
   }catch(IndexOutOfBoundsException e){
    break;
   }
   realH += lineR.height();
   if(realH >= mTopText.getHeight())
    break;
  }
//  Log.e("当前view实际能显示的行数为", "" + i);
  topTextMaxRows = i;
 
  //如果toptext显示不下的话,显示到bottomtext里面去
  if (textRows >= topTextMaxRows && bRequestBottomLayout) {
   // toptext最后一个可见字符的位置
   int lastindex = mTopText.getLayout().getLineVisibleEnd(
     topTextMaxRows - 1);

   
   
   ClickableSpan[] cs = mTopText.getSpans();
   int spanstart = 0;
   int spanend = 0;
   spanstring = "";
   STATE = 0; // 1网页 2人名 3话题
   for (ClickableSpan c : cs) {
    spanstart = mTopText.getSpanStart(c);
    spanend = mTopText.getSpanEnd(c);
    if (spanstart <= lastindex && spanend > lastindex) {
     if (c instanceof LinkClickableSpan) {
//      Log.e("转角span类型", "网页");
      spanstring = ((LinkClickableSpan) c).getLink();
      STATE = 1;
     }
     if (c instanceof NameClickableSpan) {
//      Log.e("转角span类型", "人名");
      spanstring = ((NameClickableSpan) c).getName();
      STATE = 2;
     }
     if (c instanceof TopicClickableSpan) {
//      Log.e("转角span类型", "话题");
      spanstring = ((TopicClickableSpan) c).getTopic();
      STATE = 3;
     }
     break;
    }
   }
   

   mTopText.setText(mTextContent.substring(0, lastindex));
   mTopText.setVisibility(View.VISIBLE);
   // 当行数不是整数时,调整内容会有下面半行没遮住,所以干脆都处理完以后再显示出来
   mBottomText.setText(mTextContent.substring(lastindex,
     mTextContent.length()) + mBottomText.getText().toString());
   if (mBottomText.getText().length() > 0 && bRequestBottomLayout){
    mBottomText.setVisibility(View.VISIBLE);
    mHandler.post(new Runnable() {
     
     @Override
     public void run() {
      // TODO Auto-generated method stub
      mBottomText.requestLayout();
      bRequestBottomLayout = false;
     }
    });
   }

   

   if (STATE != 0 || !bRequestBottomLayout) {

    Log.e("spanstring", spanstring);

//    Log.e("", "移除转角span");
    mTopText.getSpannable().removeSpan(mTopText.getSpans()[mTopText.getSpans().length - 1]);

    switch (STATE) {
    case 1:
     mTopText.getSpannable().setSpan(
       mTopText.new LinkClickableSpan(spanstring),
       spanstart,
       lastindex,
       Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
     mBottomText.getSpannable().setSpan(
       mBottomText.new LinkClickableSpan(spanstring),
       0,
       spanend - lastindex,
       Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
//     Log.e("", "网页");
     break;
    case 2:
     mTopText.getSpannable().setSpan(
       mTopText.new NameClickableSpan(spanstring),
       spanstart,
       lastindex,
       Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
     mBottomText.getSpannable().setSpan(
       mBottomText.new NameClickableSpan(spanstring),
       0,
       spanend - lastindex,
       Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
//     Log.e("", "人名");
     break;
    case 3:
     mTopText.getSpannable().setSpan(
       mTopText.new TopicClickableSpan(spanstring),
       spanstart,
       lastindex,
       Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
     mBottomText.getSpannable().setSpan(
       mBottomText.new TopicClickableSpan(spanstring),
       0,
       spanend - lastindex,
       Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
//     Log.e("", "话题");
     break;
    }
    
    mTopText.setText(mTopText.getSpannable(), BufferType.SPANNABLE);
    mBottomText.setText(mBottomText.getSpannable(), BufferType.SPANNABLE);
   }
   

  }
 
 }

 

说明下难点:

第一,转角处可能存在Span,比如一个话题 #话题话题话题话题话题话题话题话题话题#,可能一半内容在mTopText里面的最后一行显示,而另一半显示不下了,这就需要剪切剩余的String显示到mBottomText里面去,可是Span的内容就会出错,这就需要重新设置下转角处的Span效果了

第二,由于存在图片Span,会导致行高不正确,所以需要在获取到mTopText能显示的最多行数以后,重新判断一次是否正确,如果不正确的话,需要重新调整,不然mTopText会有一部分内容显示不下。


该View实现完成后,在布局中使用include条用,代码中find出来就可以直接使用了


不考虑Span的话,直接将代码中转角处理Span部分删除即可使用,我注释的挺清楚了

下面我们一起来看一个关于SpannableString与SpannableStringBuilder使用例子了,希望这个例子能够对各位同学带来有效帮助的哦。

1、SpannableString、SpannableStringBuilder与String的关系

首先SpannableString、SpannableStringBuilder基本上与String差不多,也是用来存储字符串,但它们俩的特殊就在于有一个SetSpan()函数,能给这些存储的String添加各种格式或者称样式(Span),将原来的String以不同的样式显示出来,比如在原来String上加下划线、加背景色、改变字体颜色、用图片把指定的文字给替换掉,等等。所以,总而言之,SpannableString、SpannableStringBuilder与String一样, 首先也是传字符串,但SpannableString、SpannableStringBuilder可以对这些字符串添加额外的样式信息,但String则不行。

注意:如果这些额外信息能被所用的方式支持,比如将SpannableString传给TextView;也有对这些额外信息不支持的,比如前一章讲到的Canvas绘制文字,对于不支持的情况,SpannableString和SpannableStringBuilder就是退化为String类型,直接显示原来的String字符串,而不会再显示这些附加的额外信息。

2、SpannableString与SpannableStringBuilder区别

它们的区别在于 SpannableString像一个String一样,构造对象的时候传入一个String,之后再无法更改String的内容,也无法拼接多个 SpannableString;而SpannableStringBuilder则更像是StringBuilder,它可以通过其append()方法来拼接多个String;

3、SetSpan()

void setSpan (Object what, int start, int end, int flags)

函数意义:给SpannableString或SpannableStringBuilder特定范围的字符串设定Span样式,可以设置多个(比如同时加上下划线和删除线等),Falg参数标识了当在所标记范围前和标记范围后紧贴着插入新字符时的动作,即是否对新插入的字符应用同样的样式。

参数说明:

object what :对应的各种Span,后面会提到;
int start:开始应用指定Span的位置,索引从0开始
int end:结束应用指定Span的位置,特效并不包括这个位置。比如如果这里数为3(即第4个字符),第4个字符不会有任何特效。从下面的例子也可以看出来。
int flags:取值有如下四个
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE:前后都不包括,即在指定范围的前面和后面插入新字符都不会应用新样式
Spannable.SPAN_EXCLUSIVE_INCLUSIVE :前面不包括,后面包括。即仅在范围字符的后面插入新字符时会应用新样式
Spannable.SPAN_INCLUSIVE_EXCLUSIVE :前面包括,后面不包括。
Spannable.SPAN_INCLUSIVE_INCLUSIVE :前后都包括。

下面写了个小demo,先看一下效果图:

代码如下所示:

 代码如下 复制代码

public class MainActivity extends Activity implements OnClickListener {
    private TextView tv;
    private Button underline_btn;
    private Button strike_btn;
    private Button style_btn;
    private Button font_btn;
    private Button color_btn1;
    private Button color_btn2;
    private Button url_btn;
    private Button image_btn;
    private Button maskfilte_btn;
    private Button Rasterizer_btn;
    private Button spannablestringbuilder;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        tv = (TextView) this.findViewById(R.id.tv);
        underline_btn = (Button) this.findViewById(R.id.underline_btn);
        strike_btn = (Button) this.findViewById(R.id.strike_btn);
        style_btn = (Button) this.findViewById(R.id.style_btn);
        font_btn = (Button) this.findViewById(R.id.font_btn);
        color_btn1 = (Button) this.findViewById(R.id.color_btn1);
        color_btn2 = (Button) this.findViewById(R.id.color_btn2);
        url_btn = (Button) this.findViewById(R.id.url_btn);
        image_btn = (Button) this.findViewById(R.id.image_btn);
        maskfilte_btn = (Button) this.findViewById(R.id.maskfilte_btn);
        Rasterizer_btn = (Button) this.findViewById(R.id.Rasterizer_btn);
        spannablestringbuilder = (Button) this.findViewById(R.id.spannablestringbuilder);
 
        underline_btn.setOnClickListener(this);
        strike_btn.setOnClickListener(this);
        style_btn.setOnClickListener(this);
        font_btn.setOnClickListener(this);
        color_btn1.setOnClickListener(this);
        color_btn2.setOnClickListener(this);
        url_btn.setOnClickListener(this);
        image_btn.setOnClickListener(this);
        maskfilte_btn.setOnClickListener(this);
        Rasterizer_btn.setOnClickListener(this);
        spannablestringbuilder.setOnClickListener(this);
 
    }
 
    @Override
    public void onClick(View v) {
        switch (v.getId()) {
        case R.id.underline_btn:
            addUnderLineSpan();
            break;
        case R.id.strike_btn:
            addStrikeSpan();
            break;
        case R.id.style_btn:
            addStyleSpan();
            break;
        case R.id.font_btn:
            addFontSpan();
            break;
        case R.id.color_btn1:
            addForeColorSpan();
            break;
        case R.id.color_btn2:
            addBackColorSpan();
            break;
        case R.id.url_btn:
            addUrlSpan();
            break;
        case R.id.image_btn:
            addImageSpan();
            break;
        case R.id.maskfilte_btn:
            addmaskfilteSpan();
            break;
        case R.id.Rasterizer_btn:
            addRasterizerSpan();
            break;
        case R.id.spannablestringbuilder:
            addspannablestringbuilderSpan();
            break;
        }
    }
    /**
     * spannablestringbuilder
     */
    private void addspannablestringbuilderSpan() {
        SpannableStringBuilder ss=new SpannableStringBuilder("红色超链接斜体删除线绿色下划线图片:.");
           //用颜色标记文本
        ss.setSpan(new ForegroundColorSpan(Color.RED), 0, 2,
                //setSpan时需要指定的 flag,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE(前后都不包括).
                Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        //用超链接标记文本
        ss.setSpan(new URLSpan("tel:4155551212"), 2, 5,
                Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        //用样式标记文本(斜体)
        ss.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), 5, 7,
                Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        //用删除线标记文本
        ss.setSpan(new StrikethroughSpan(), 7, 10,
                Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        //用下划线标记文本
        ss.setSpan(new UnderlineSpan(), 10, 16,
                Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        //用颜色标记
        ss.setSpan(new ForegroundColorSpan(Color.GREEN), 10, 12,
                Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        //获取Drawable资源
        Drawable d = getResources().getDrawable(R.drawable.ic_launcher);
        d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
        //创建ImageSpan
        ImageSpan span = new ImageSpan(d, ImageSpan.ALIGN_BASELINE);
        //用ImageSpan替换文本
        ss.setSpan(span, 18, 19, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
        tv.append("\n");
        tv.setText(ss);
        tv.setMovementMethod(LinkMovementMethod.getInstance()); //实现文本的滚动
    }
 
    /*
     * Spannable.SPAN_EXCLUSIVE_EXCLUSIVE:前后都不包括,即在指定范围的前面和后面插入新字符都不会应用新样式
     * Spannable.SPAN_EXCLUSIVE_INCLUSIVE :前面不包括,后面包括。即仅在范围字符的后面插入新字符时会应用新样式
     * Spannable.SPAN_INCLUSIVE_EXCLUSIVE :前面包括,后面不包括。
     * Spannable.SPAN_INCLUSIVE_INCLUSIVE :前后都包括。
     */
    /**
     * 光栅效果
     */
    private void addRasterizerSpan() {
        SpannableString spanText = new SpannableString("StrikethroughSpan");
        spanText.setSpan(new StrikethroughSpan(), 0, 7, Spannable.
        SPAN_INCLUSIVE_EXCLUSIVE);
        tv.append("\n");
        tv.append(spanText);
        
    }
    /**
     * 修饰效果
     */
    private void addmaskfilteSpan() {
        SpannableString spanText = new SpannableString("benzlocke6666666");
        int length = spanText.length();
        //模糊(BlurMaskFilter)
        MaskFilterSpan maskFilterSpan = new MaskFilterSpan(new BlurMaskFilter(3, Blur.OUTER));
        spanText.setSpan(maskFilterSpan, 0, length - 10, Spannable.
        SPAN_INCLUSIVE_EXCLUSIVE);
        //浮雕(EmbossMaskFilter)
        maskFilterSpan = new MaskFilterSpan(new EmbossMaskFilter(new float[]{1,1,3}, 1.5f, 8, 3));
        spanText.setSpan(maskFilterSpan, length - 10, length, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
        tv.append("\n");
        tv.append(spanText);
        
    }
 
    /**
     * 超链接
     */
    private void addUrlSpan() {
        SpannableString spanString = new SpannableString("超链接");
        URLSpan span = new URLSpan("tel:0123456789");
        spanString.setSpan(span, 0, 3, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        tv.append("\n");
        tv.append(spanString);
        
        tv.setMovementMethod(LinkMovementMethod.getInstance());
    }
 
    /**
     * 文字背景颜色
     */
    private void addBackColorSpan() {
        SpannableString spanString = new SpannableString("文字背景颜色");
        BackgroundColorSpan span = new BackgroundColorSpan(Color.YELLOW);
        spanString.setSpan(span, 0, 6, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        tv.append("\n");
        tv.append(spanString);
    }
 
    /**
     * 文字颜色
     */
    private void addForeColorSpan() {
        SpannableString spanString = new SpannableString("文字颜色");
        ForegroundColorSpan span = new ForegroundColorSpan(Color.BLUE);
        spanString.setSpan(span, 0, 4, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        tv.append("\n");
        tv.append(spanString);
    }
 
    /**
     * 字体大小
     */
    private void addFontSpan() {
        SpannableString spanString = new SpannableString("36号字体");
        AbsoluteSizeSpan span = new AbsoluteSizeSpan(36);
        spanString.setSpan(span, 0, 5, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        tv.append("\n");
        tv.append(spanString);
    }
 
    /**
     * 粗体,斜体
     */
    private void addStyleSpan() {
        SpannableString spanString = new SpannableString("ABCDEF");
        StyleSpan span = new StyleSpan(Typeface.BOLD_ITALIC);
        spanString.setSpan(span, 0, 3, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        tv.append("\n");
        tv.append(spanString);
    }
 
    /**
     * 删除线
     */
    private void addStrikeSpan() {
        SpannableString spanString = new SpannableString("删除线");
        StrikethroughSpan span = new StrikethroughSpan();
        spanString.setSpan(span, 0, 3, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        tv.append("\n");
        tv.append(spanString);
    }
 
    /**
     * 下划线
     */
    private void addUnderLineSpan() {
        SpannableString spanString = new SpannableString("下划线");
        UnderlineSpan span = new UnderlineSpan();
        spanString.setSpan(span, 0, 3, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        tv.append("\n");
        tv.append(spanString);
    }
 
    /**
     * 图片
     */
    private void addImageSpan() {
        SpannableString spanString = new SpannableString(" ");
        Drawable d = getResources().getDrawable(R.drawable.ic_launcher);
        d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
        ImageSpan span = new ImageSpan(d, ImageSpan.ALIGN_BASELINE);
        spanString.setSpan(span, 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        tv.append("\n");
        tv.append(spanString);
    }
}

下文来为各位介绍一篇关于Fiddler 对 Android手机进行抓包的例子,希望这篇文章能够对各位同学带来帮助,具体如下所示。

Fiddler是一个http协议调试代理工具,它能够记录并检查所有你的电脑和互联网之间的http通讯,设置断点,查看所有的“进出”Fiddler的数据(指cookie,html,js,css等文件,这些都可以让你胡乱修改的意思)。 Fiddler 要比其他的网络调试器要更加简单,因为它不仅仅暴露http通讯还提供了一个用户友好的格式。

我们在电脑上进行网络访问请求调试的时候通常在浏览器中可以使用F12,但是如果要看手机访问时进行了哪些请求那就不那么简单了,而使用 Fiddler 则可以很方便的看到数据请求与响应。

下面介绍 Fiddler 的安装与使用。

前提:电脑和手机保持在同一局域网内

1. 安装Fiddler


安装程序:

2. 安装完成后启动软件,依次点击 Tools --> Fiddler Options...  如下图:

3. 点击 Connections , 将 Allow remote computers connect 勾选上,监听端口为8888,如图:

4.依次点击关闭后,重启软件。

5. 查看电脑的IP,我这里是 192.168.1.4

6. 在手机上,将手机代理设置为电脑上设置 192.168.1.4  和 8888   如下图:

7. 如下图,已经开始工作了:

[!--infotagslink--]

相关文章

  • Android子控件超出父控件的范围显示出来方法

    下面我们来看一篇关于Android子控件超出父控件的范围显示出来方法,希望这篇文章能够帮助到各位朋友,有碰到此问题的朋友可以进来看看哦。 <RelativeLayout xmlns:an...2016-10-02
  • Android开发中findViewById()函数用法与简化

    findViewById方法在android开发中是获取页面控件的值了,有没有发现我们一个页面控件多了会反复研究写findViewById呢,下面我们一起来看它的简化方法。 Android中Fin...2016-09-20
  • Android模拟器上模拟来电和短信配置

    如果我们的项目需要做来电及短信的功能,那么我们就得在Android模拟器开发这些功能,本来就来告诉我们如何在Android模拟器上模拟来电及来短信的功能。 在Android模拟...2016-09-20
  • 夜神android模拟器设置代理的方法

    夜神android模拟器如何设置代理呢?对于这个问题其实操作起来是非常的简单,下面小编来为各位详细介绍夜神android模拟器设置代理的方法,希望例子能够帮助到各位。 app...2016-09-20
  • android自定义动态设置Button样式【很常用】

    为了增强android应用的用户体验,我们可以在一些Button按钮上自定义动态的设置一些样式,比如交互时改变字体、颜色、背景图等。 今天来看一个通过重写Button来动态实...2016-09-20
  • Android WebView加载html5页面实例教程

    如果我们要在Android应用APP中加载html5页面,我们可以使用WebView,本文我们分享两个WebView加载html5页面实例应用。 实例一:WebView加载html5实现炫酷引导页面大多...2016-09-20
  • 深入理解Android中View和ViewGroup

    深入理解Android中View和ViewGroup从组成架构上看,似乎ViewGroup在View之上,View需要继承ViewGroup,但实际上不是这样的。View是基类,ViewGroup是它的子类。本教程我们深...2016-09-20
  • Android自定义WebView网络视频播放控件例子

    下面我们来看一篇关于Android自定义WebView网络视频播放控件开发例子,这个文章写得非常的不错下面给各位共享一下吧。 因为业务需要,以下代码均以Youtube网站在线视...2016-10-02
  • Android用MemoryFile文件类读写进行性能优化

    java开发的Android应用,性能一直是一个大问题,,或许是Java语言本身比较消耗内存。本文我们来谈谈Android 性能优化之MemoryFile文件读写。 Android匿名共享内存对外A...2016-09-20
  • Android设置TextView竖着显示实例

    TextView默认是横着显示了,今天我们一起来看看Android设置TextView竖着显示如何来实现吧,今天我们就一起来看看操作细节,具体的如下所示。 在开发Android程序的时候,...2016-10-02
  • Angular性能优化之第三方组件和懒加载技术

    这篇文章主要介绍了Angular性能优化之第三方组件和懒加载技术,对性能优化感兴趣的同学,可以参考下...2021-05-11
  • android.os.BinderProxy cannot be cast to com解决办法

    本文章来给大家介绍关于android.os.BinderProxy cannot be cast to com解决办法,希望此文章对各位有帮助呀。 Android在绑定服务的时候出现java.lang.ClassCastExc...2016-09-20
  • 带你了解PHP7 性能翻倍的关键

    20岁老牌网页程序语言PHP,最快将在10月底释出PHP 7新版,这是十年来的首次大改版,最大特色是在性能上的大突破,能比前一版PHP 5快上一倍,PHP之父Rasmus Lerdorf表示,甚至能比HHVM虚拟机下的PHP程序性能更快。HHVM 是脸书为自...2015-11-24
  • Android 实现钉钉自动打卡功能

    这篇文章主要介绍了Android 实现钉钉自动打卡功能的步骤,帮助大家更好的理解和学习使用Android,感兴趣的朋友可以了解下...2021-03-15
  • Android 开发之布局细节对比:RTL模式

    下面我们来看一篇关于Android 开发之布局细节对比:RTL模式 ,希望这篇文章对各位同学会带来帮助,具体的细节如下介绍。 前言 讲真,好久没写博客了,2016都过了一半了,赶紧...2016-10-02
  • JavaScript提高网站性能优化的建议(二)

    这篇文章主要介绍了JavaScript提高网站性能优化的建议(二)的相关资料,需要的朋友可以参考下...2016-07-29
  • 利用 Chrome Dev Tools 进行页面性能分析的步骤说明(前端性能优化)

    这篇文章主要介绍了利用 Chrome Dev Tools 进行页面性能分析的步骤说明(前端性能优化),本文给大家介绍的非常想详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下...2021-02-24
  • Android中使用SDcard进行文件的读取方法

    首先如果要在程序中使用sdcard进行存储,我们必须要在AndroidManifset.xml文件进行下面的权限设置: 在AndroidManifest.xml中加入访问SDCard的权限如下: <!--...2016-09-20
  • Android开发之PhoneGap打包及错误解决办法

    下面来给各位简单的介绍一下关于Android开发之PhoneGap打包及错误解决办法,希望碰到此类问题的同学可进入参考一下哦。 在我安装、配置好PhoneGap项目的所有依赖...2016-09-20
  • 用Intel HAXM给Android模拟器Emulator加速

    Android 模拟器 Emulator 速度真心不给力,, 现在我们来介绍使用 Intel HAXM 技术为 Android 模拟器加速,使模拟器运行度与真机比肩。 周末试玩了一下在Eclipse中使...2016-09-20