标签: Android开发

Android开发用过的十大框架

Android开发用过的十大框架

 

本文系多方综合与转载整合,意在Android开发中能够知道和使用一些好用的第三方支持,省去自己的很多时间,下面涉及到的多为经过历史兴衰与检验的,江山代有人才出一代更比一代强,有些已经被更新更好用的所取代,但也很多地方仍可圈可点不乏参考,有些依然经典,整理出来请君一参.欢迎大家的指正,补充与交流.

1、AndBase框架

项目地址: https://code.jd.com/zhaoqp2010_m/andbase

1.andbase中包含了大量的开发常用手段。

如网络下载,多线程与线程池的管理,数据库ORM,图片缓存管理,图片文件下载上传,Http请求工具,常用工具类(字符串,日期,文件处理,图片处理工具类等),能够使您的应用在团队开发中减少冗余代码,很大的提高了代码的维护性与开发高效性,能很好的规避由于开发疏忽而导致常犯的错误。

2.andbase封装了大量的常用控件。

如list分页,下拉刷新,图片轮播,表格,多线程下载器,侧边栏,图片上传,轮子选择,图表,Tab滑动,日历选择器等。

3.强大的AbActivity,您没有理由不继承它。

继承它你能够获得一个简单强大可设置的操作栏,以及一系列的简单调用,如弹出框,提示框,进度框,副操作栏等。

4.提供效率较高图片缓存管理策略,使内存大幅度节省,利用率提高,效率提高。

程序中要管理大量的图片资源,andbase提供简单的方法,几步完成下载与显示,并支持缩放,裁剪,缓存功能。

5.封装了大量常见工具类。

包括日期,字符,文件,图片等各种处理函数, 多而全。

6.用andbase大量减少handler的使用,而采用回调函数,代码更整洁。

handler会产生大量代码,并且不好维护,andbase对handler进行了封装。

7.简单轻量支持注解自动建表的ORM框架(支持一/多对多的关联操作)。

写sql,建表,工作量大,andbase提供更傻瓜异步增删改查工具类。

8.异步请求http框架,网络请求标准化,支持文件上传下载,get,post,进度显示。

包含了异步与http请求的工具类,实用。

 

2、XUtil框架

项目地址:https://github.com/wyouflf/xUtils

主要有四大模块:

(1) 数据库模块:Android中的orm框架,一行代码就可以进行增删改查;
支持事务,默认关闭;
可通过注解自定义表名,列名,外键,唯一性约束,NOT NULL约束,CHECK约束等(需要混淆的时候请注解表名和列名);
支持绑定外键,保存实体时外键关联实体自动保存或更新;
自动加载外键关联实体,支持延时加载;
支持链式表达查询,更直观的查询语义,参考下面的介绍或sample中的例子。
(2) 注解模块:android中的ioc框架,完全注解方式就可以进行UI,资源和事件绑定;
新的事件绑定方式,使用混淆工具混淆后仍可正常工作;
目前支持常用的20种事件绑定,参见ViewCommonEventListener类和包com.lidroid.xutils.view.annotation.event。
(3) 网络模块:支持同步,异步方式的请求;
支持大文件上传,上传大文件不会oom;
支持GET,POST,PUT,MOVE,COPY,DELETE,HEAD,OPTIONS,TRACE,CONNECT请求;
下载支持301/302重定向,支持设置是否根据Content-Disposition重命名下载的文件;
返回文本内容的请求(默认只启用了GET请求)支持缓存,可设置默认过期时间和针对当前请求的过期时间。
(4) 图片缓存模块:加载bitmap的时候无需考虑bitmap加载过程中出现的oom和android容器快速滑动时候出现的图片错位等现象;
支持加载网络图片和本地图片;
内存管理使用lru算法,更好的管理bitmap内存;
可配置线程加载线程数量,缓存大小,缓存路径,加载显示动画等…

3、ThinkAndroid框架

项目地址:https://github.com/white-cat/ThinkAndroid
主要有以下模块:
(1)  MVC模块:实现视图与模型的分离。
(2)  ioc模块:android中的ioc模块,完全注解方式就可以进行UI绑定、res中的资源的读取、以及对象的初始化。
(3)  数据库模块:android中的orm框架,使用了线程池对sqlite进行操作。
(4)  http模块:通过httpclient进行封装http数据请求,支持异步及同步方式加载。
(5)  缓存模块:通过简单的配置及设计可以很好的实现缓存,对缓存可以随意的配置
(6)  图片缓存模块:imageview加载图片的时候无需考虑图片加载过程中出现的oom和android容器快速滑动时候出现的图片错位等现象。
(7)  配置器模块:可以对简易的实现配对配置的操作,目前配置文件可以支持Preference、Properties对配置进行存取。
(8)  日志打印模块:可以较快的轻易的是实现日志打印,支持日志打印的扩展,目前支持对sdcard写入本地打印、以及控制台打印
(9)  下载器模块:可以简单的实现多线程下载、后台下载、断点续传、对下载进行控制、如开始、暂停、删除等等。
(10) 网络状态检测模块:当网络状态改变时,对其进行检

 

4、LoonAndroid

项目地址:https://github.com/gdpancheng/LoonAndroid
主要有以下模块:
(1)  自动注入框架(只需要继承框架内的application既可)
(2)  图片加载框架(多重缓存,自动回收,*大限度保证内存的安全性)
(3)  网络请求模块(继承了基本上现在所有的http请求)
(4)  eventbus(集成一个开源的框架)
(5)  验证框架(集成开源框架)
(6)  json解析(支持解析成集合或者对象)
(7)  数据库(不知道是哪位写的 忘记了)
(8)  多线程断点下载(自动判断是否支持多线程,判断是否是重定向)
(9)  自动更新模块
(10) 一系列工具类

5、volley

项目地址 :https://github.com/smanikandan14/Volley-demo
(1)  JSON,图像等的异步下载;
(2)  网络请求的排序(scheduling)
(3)  网络请求的优先级处理
(4)  缓存
(5)  多级别取消请求
(6)  和Activity和生命周期的联动(Activity结束时同时取消所有网络请求)

6、android-async-http

项目地址:https://github.com/loopj/android-async-http
文档介绍:http://loopj.com/android-async-http/
(1) 在匿名回调中处理请求结果
(2) 在UI线程外进行http请求
(3) 文件断点上传
(4) 智能重试
(5) 默认gzip压缩
(6) 支持解析成Json格式
(7) 可将Cookies持久化到SharedPreferences

7、Afinal框架

项目地址:https://github.com/yangfuhai/afinal
主要有四大模块:
(1) 数据库模块:android中的orm框架,使用了线程池对sqlite进行操作。
(2) 注解模块:android中的ioc框架,完全注解方式就可以进行UI绑定和事件绑定。无需findViewById和setClickListener等。
(3) 网络模块:通过httpclient进行封装http数据请求,支持ajax方式加载,支持下载、上传文件功能。
(4) 图片缓存模块:通过FinalBitmap,imageview加载bitmap的时候无需考虑bitmap加载过程中出现的oom和android容器快速滑动时候出现的图片错位等现象。
FinalBitmap可以配置线程加载线程数量,缓存大小,缓存路径,加载显示动画等。FinalBitmap的内存管理使用lru算法,
没有使用弱引用(android2.3以后google已经不建议使用弱引用,android2.3后强行回收软引用和弱引用,详情查看android官方文档),
更好的管理bitmap内存。FinalBitmap可以自定义下载器,用来扩展其他协议显示网络图片,比如ftp等。同时可以自定义bitmap显示器,
在imageview显示图片的时候播放动画等(默认是渐变动画显示)。

总结框架结构,

ImageLoader框架(第8大框架)

 

UniversalImageLoader是用于加载图片的一个开源项目,在其项目介绍中是这么写的,

  • 支持多线程图片加载
  • 提供丰富的细节配置,比如线程池大小,HTPP请求项,内存和磁盘缓存,图片显示时的参数配置等等;
  • 提供双缓存
  • 支持加载过程的监听;
  • 提供图片的个性化显示配置接口;
  • Widget支持(这个,个人觉得没必要写进来,不过尊重原文)

其他类似的项目也有很多,但这个作为github上著名的开源项目被广泛使用。第三方的包虽然好用省力,可以有效避免重复造轮子,但是却隐藏了一些开发上的细节,如果不关注其内部实现,那么将不利于开发人员掌握核心技术,当然也谈不上更好的使用它,计划分析项目的集成使用和低层实现。

 

开源框架android-async-http(第9大框架)

官网:https://github.com/loopj/android-async-http

Android-async-http开源框架可以是我们轻松的获取网络数据或者向服务器发送数据,使用起来也简单,详细请看官网:

到官网下载zip包,解压,里面有完整的代码和各种版本的jar包和demo,源码在library里面,jar包在releases里面。项目更新速度很快,老版本的回调是一个普通类,*新版是一个接口。

 

KJFrameForAndroid框架(第10大框架)

参考:http://www.codeceo.com/article/android-orm-kjframeforandroid.html

KJFrameForAndroid是一款基于Android的ORM和 IOC应用开发框架,封装了很多Android开发中常用的功能,包括Android中对Bitmap的操作类库。KJFrameForAndroid的设计非常精简,利用KJFrameForAndroid,我们可以用*少的代码完成很多丰富的Android功能应用,为Android开发者节省许多不必要的开发时间。

KJFrameForAndroid总共分为五大模块:UILibrary,UtilsLibrary,HttpLibrary,BitmapLibrary,DBLibrary。

 

Android-async-http开源框架可以是我们轻松的获取网络数据或者向服务器发送数据,使用起来也简单,详细请看官网:

到官网下载zip包,解压,里面有完整的代码和各种版本的jar包和demo,源码在library里面,jar包在releases里面。项目更新速度很快,老版本的回调是一个普通类,*新版是一个接口。

Context都没弄明白,还怎么做Android开发?

Context都没弄明白,还怎么做Android开发?

Activity mActivity =new Activity()

作为Android开发者,不知道你有没有思考过这个问题,Activity可以new吗?Android的应用程序开发采用JAVA语言,Activity本质上也是一个对象,那上面的写法有什么问题呢?估计很多人说不清道不明。Android程序不像Java程序一样,随便创建一个类,写个main()方法就能运行,Android应用模型是基于组件的应用设计模式,组件的运行要有一个完整的Android工程环境,在这个环境下,Activity、Service等系统组件才能够正常工作,而这些组件并不能采用普通的Java对象创建方式,new一下就能创建实例了,而是要有它们各自的上下文环境,也就是我们这里讨论的Context。可以这样讲,Context是维持Android程序中各组件能够正常工作的一个核心功能类。

Context到底是什么

Context的中文翻译为:语境; 上下文; 背景; 环境,在开发中我们经常说称之为“上下文”,那么这个“上下文”到底是指什么意思呢?在语文中,我们可以理解为语境,在程序中,我们可以理解为当前对象在程序中所处的一个环境,一个与系统交互的过程。比如微信聊天,此时的“环境”是指聊天的界面以及相关的数据请求与传输,Context在加载资源、启动Activity、获取系统服务、创建View等操作都要参与。

那Context到底是什么呢?一个Activity就是一个Context,一个Service也是一个Context。Android程序员把“场景”抽象为Context类,他们认为用户和操作系统的每一次交互都是一个场景,比如打电话、发短信,这些都是一个有界面的场景,还有一些没有界面的场景,比如后台运行的服务(Service)。一个应用程序可以认为是一个工作环境,用户在这个环境中会切换到不同的场景,这就像一个前台秘书,她可能需要接待客人,可能要打印文件,还可能要接听客户电话,而这些就称之为不同的场景,前台秘书可以称之为一个应用程序。

如何生动形象的理解Context

上面的概念中采用了通俗的理解方式,将Context理解为“上下文”或者“场景”,如果你仍然觉得很抽象,不好理解。在这里我给出一个可能不是很恰当的比喻,希望有助于大家的理解:一个Android应用程序,可以理解为一部电影或者一部电视剧,Activity,Service,Broadcast Receiver,Content Provider这四大组件就好比是这部戏里的四个主角:胡歌,霍建华,诗诗,Baby。他们是由剧组(系统)一开始就定好了的,整部戏就是由这四位主演领衔担纲的,所以这四位主角并不是大街上随随便便拉个人(new 一个对象)都能演的。有了演员当然也得有摄像机拍摄啊,他们必须通过镜头(Context)才能将戏传递给观众,这也就正对应说四大组件(四位主角)必须工作在Context环境下(摄像机镜头)。那Button,TextView,LinearLayout这些控件呢,就好比是这部戏里的配角或者说群众演员,他们显然没有这么重用,随便一个路人甲路人乙都能演(可以new一个对象),但是他们也必须要面对镜头(工作在Context环境下),所以Button mButton=new Button(Context)是可以的。虽然不很恰当,但还是很容易理解的,希望有帮助。

源码中的Context

/**
 * Interface to global information about an application environment.  This is
 * an abstract class whose implementation is provided by
 * the Android system.  It
 * allows access to application-specific resources and classes, as well as
 * up-calls for application-level operations such as launching activities,
 * broadcasting and receiving intents, etc.
 */
public abstract class Context {
    /**
     * File creation mode: the default mode, where the created file can only
     * be accessed by the calling application (or all applications sharing the
     * same user ID).
     * @see #MODE_WORLD_READABLE
     * @see #MODE_WORLD_WRITEABLE
     */
    public static final int MODE_PRIVATE = 0x0000;

    public static final int MODE_WORLD_WRITEABLE = 0x0002;

    public static final int MODE_APPEND = 0x8000;

    public static final int MODE_MULTI_PROCESS = 0x0004;

    .
    .
    .
    }

源码中的注释是这么来解释Context的:Context提供了关于应用环境全局信息的接口。它是一个抽象类,它的执行被Android系统所提供。它允许获取以应用为特征的资源和类型,是一个统领一些资源(应用程序环境变量等)的上下文。就是说,它描述一个应用程序环境的信息(即上下文);是一个抽象类,Android提供了该抽象类的具体实现类;通过它我们可以获取应用程序的资源和类(包括应用级别操作,如启动Activity,发广播,接受Intent等)。既然上面Context是一个抽象类,那么肯定有他的实现类咯,我们在Context的源码中通过IDE可以查看到他的子类*终可以得到如下关系图:

%title插图%num

Context.png

Context类本身是一个纯abstract类,它有两个具体的实现子类:ContextImpl和ContextWrapper。其中ContextWrapper类,如其名所言,这只是一个包装而已,ContextWrapper构造函数中必须包含一个真正的Context引用,同时ContextWrapper中提供了attachBaseContext()用于给ContextWrapper对象中指定真正的Context对象,调用ContextWrapper的方法都会被转向其所包含的真正的Context对象。ContextThemeWrapper类,如其名所言,其内部包含了与主题(Theme)相关的接口,这里所说的主题就是指在AndroidManifest.xml中通过android:theme为Application元素或者Activity元素指定的主题。当然,只有Activity才需要主题,Service是不需要主题的,因为Service是没有界面的后台场景,所以Service直接继承于ContextWrapper,Application同理。而ContextImpl类则真正实现了Context中的所以函数,应用程序中所调用的各种Context类的方法,其实现均来自于该类。一句话总结:Context的两个子类分工明确,其中ContextImpl是Context的具体实现类,ContextWrapper是Context的包装类。Activity,Application,Service虽都继承自ContextWrapper(Activity继承自ContextWrapper的子类ContextThemeWrapper),但它们初始化的过程中都会创建ContextImpl对象,由ContextImpl实现Context中的方法。

一个应用程序有几个Context

其实这个问题本身并没有什么意义,关键还是在于对Context的理解,从上面的关系图我们已经可以得出答案了,在应用程序中Context的具体实现子类就是:Activity,Service,Application。那么Context数量=Activity数量+Service数量+1。当然如果你足够细心,可能会有疑问:我们常说四大组件,这里怎么只有Activity,Service持有Context,那Broadcast Receiver,Content Provider呢?Broadcast Receiver,Content Provider并不是Context的子类,他们所持有的Context都是其他地方传过去的,所以并不计入Context总数。上面的关系图也从另外一个侧面告诉我们Context类在整个Android系统中的地位是多么的崇高,因为很显然Activity,Service,Application都是其子类,其地位和作用不言而喻。

Context能干什么

Context到底可以实现哪些功能呢?这个就实在是太多了,弹出Toast、启动Activity、启动Service、发送广播、操作数据库等等都需要用到Context。

TextView tv = new TextView(getContext());

ListAdapter adapter = new SimpleCursorAdapter(getApplicationContext(), ...);

AudioManager am = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);getApplicationContext().getSharedPreferences(name, mode);

getApplicationContext().getContentResolver().query(uri, ...);

getContext().getResources().getDisplayMetrics().widthPixels * 5 / 8;

getContext().startActivity(intent);

getContext().startService(intent);

getContext().sendBroadcast(intent);

Context作用域

虽然Context神通广大,但并不是随便拿到一个Context实例就可以为所欲为,它的使用还是有一些规则限制的。由于Context的具体实例是由ContextImpl类去实现的,因此在*大多数场景下,Activity、Service和Application这三种类型的Context都是可以通用的。不过有几种场景比较特殊,比如启动Activity,还有弹出Dialog。出于安全原因的考虑,Android是不允许Activity或Dialog凭空出现的,一个Activity的启动必须要建立在另一个Activity的基础之上,也就是以此形成的返回栈。而Dialog则必须在一个Activity上面弹出(除非是System Alert类型的Dialog),因此在这种场景下,我们只能使用Activity类型的Context,否则将会出错。

%title插图%num

Context作用域.png

从上图我们可以发现Activity所持有的Context的作用域*广,无所不能。因为Activity继承自ContextThemeWrapper,而Application和Service继承自ContextWrapper,很显然ContextThemeWrapper在ContextWrapper的基础上又做了一些操作使得Activity变得更强大,这里我就不再贴源码给大家分析了,有兴趣的童鞋可以自己查查源码。上图中的YES和NO我也不再做过多的解释了,这里我说一下上图中Application和Service所不推荐的两种使用情况。
1:如果我们用ApplicationContext去启动一个LaunchMode为standard的Activity的时候会报错android.util.AndroidRuntimeException: Calling startActivity from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?这是因为非Activity类型的Context并没有所谓的任务栈,所以待启动的Activity就找不到栈了。解决这个问题的方法就是为待启动的Activity指定FLAG_ACTIVITY_NEW_TASK标记位,这样启动的时候就为它创建一个新的任务栈,而此时Activity是以singleTask模式启动的。所有这种用Application启动Activity的方式不推荐使用,Service同Application。
2:在Application和Service中去layout inflate也是合法的,但是会使用系统默认的主题样式,如果你自定义了某些样式可能不会被使用。所以这种方式也不推荐使用。
一句话总结:凡是跟UI相关的,都应该使用Activity做为Context来处理;其他的一些操作,Service,Activity,Application等实例都可以,当然了,注意Context引用的持有,防止内存泄漏。

如何获取Context

通常我们想要获取Context对象,主要有以下四种方法
1:View.getContext,返回当前View对象的Context对象,通常是当前正在展示的Activity对象。
2:Activity.getApplicationContext,获取当前Activity所在的(应用)进程的Context对象,通常我们使用Context对象时,要优先考虑这个全局的进程Context。
3:ContextWrapper.getBaseContext():用来获取一个ContextWrapper进行装饰之前的Context,可以使用这个方法,这个方法在实际开发中使用并不多,也不建议使用。
4:Activity.this 返回当前的Activity实例,如果是UI控件需要使用Activity作为Context对象,但是默认的Toast实际上使用ApplicationContext也可以。

getApplication()和getApplicationContext()

上面说到获取当前Application对象用getApplicationContext,不知道你有没有联想到getApplication(),这两个方法有什么区别?相信这个问题会难倒不少开发者。

%title插图%num

getApplication()&getApplicationContext().png

程序是不会骗人的,我们通过上面的代码,打印得出两者的内存地址都是相同的,看来它们是同一个对象。其实这个结果也很好理解,因为前面已经说过了,Application本身就是一个Context,所以这里获取getApplicationContext()得到的结果就是Application本身的实例。那么问题来了,既然这两个方法得到的结果都是相同的,那么Android为什么要提供两个功能重复的方法呢?实际上这两个方法在作用域上有比较大的区别。getApplication()方法的语义性非常强,一看就知道是用来获取Application实例的,但是这个方法只有在Activity和Service中才能调用的到。那么也许在*大多数情况下我们都是在Activity或者Service中使用Application的,但是如果在一些其它的场景,比如BroadcastReceiver中也想获得Application的实例,这时就可以借助getApplicationContext()方法了。

publicclassMyReceiverextendsBroadcastReceiver{

@Override
publicvoidonReceive(Contextcontext,Intentintent){
ApplicationmyApp=(Application)context.getApplicationContext();

}

}

Context引起的内存泄露

但Context并不能随便乱用,用的不好有可能会引起内存泄露的问题,下面就示例两种错误的引用方式。

错误的单例模式

public class Singleton {
    private static Singleton instance;
    private Context mContext;

    private Singleton(Context context) {
        this.mContext = context;
    }

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

这是一个非线程安全的单例模式,instance作为静态对象,其生命周期要长于普通的对象,其中也包含Activity,假如Activity A去getInstance获得instance对象,传入this,常驻内存的Singleton保存了你传入的Activity A对象,并一直持有,即使Activity被销毁掉,但因为它的引用还存在于一个Singleton中,就不可能被GC掉,这样就导致了内存泄漏。

View持有Activity引用

public class MainActivity extends Activity {
    private static Drawable mDrawable;

    @Override
    protected void onCreate(Bundle saveInstanceState) {
        super.onCreate(saveInstanceState);
        setContentView(R.layout.activity_main);
        ImageView iv = new ImageView(this);
        mDrawable = getResources().getDrawable(R.drawable.ic_launcher);
        iv.setImageDrawable(mDrawable);
    }
}
就是Drawable被设置到ImageView上的时候,ImageView会将自身设置到Drawable的callback上去,因为View是实现了Drawable.Callback接口, 这样当Drawable需要刷新的时候,可以调用.这个Callback,然后通知View重新绘制该Drawable. 所以引用的正确顺序应该Drawable->View->Context, 是这样导致的泄

有一个静态的Drawable对象当ImageView设置这个Drawable时,ImageView保存了mDrawable的引用,而ImageView传入的this是MainActivity的mContext,因为被static修饰的mDrawable是常驻内存的,MainActivity是它的间接引用,MainActivity被销毁时,也不能被GC掉,所以造成内存泄漏。

正确使用Context

一般Context造成的内存泄漏,几乎都是当Context销毁的时候,却因为被引用导致销毁失败,而Application的Context对象可以理解为随着进程存在的,所以我们总结出使用Context的正确姿势:
1:当Application的Context能搞定的情况下,并且生命周期长的对象,优先使用Application的Context。
2:不要让生命周期长于Activity的对象持有到Activity的引用。
3:尽量不要在Activity中使用非静态内部类,因为非静态内部类会隐式持有外部类实例的引用,如果使用静态内部类,将外部实例引用作为弱引用持有。

总结

总之Context在Android系统中的地位很重要,它几乎无所不能,但它也不是你想用就能随便用的,谨防使用不当引起的内存问题。

来推荐几本对职业生涯影响较大的技术书籍

开发过程中,有时候会觉得当时要是没有读过那本书,现在肯定想不到要这样做。由此可以推定,因为曾经没有读过某一本书,会导致想不到要怎样做。所以不如大家都相互推荐一下自己读过的好书,拓宽自己的知识领域。

我先推荐几本对我影响比较大的书:

《算法》基本功,理解常用的组件的原理,比如 redis 。 面试也很有用

《 Head First 设计模式(中文版)》业务开发利器。可以写出更优雅的代码

《 MySQL 技术内幕》充分理解 mysql,每次阅读都有新收获

《 Go 语言设计与实现》 深入了解 golang 。 面试也很有用

《架构整洁之道》 如何避免代码不断膨胀导致项目不可维护,非常有效

85 条回复    2021-08-16 15:57:38 +08:00

mazhimazh
    1

mazhimazh   1 天前   ❤️ 73

对我影响比较大的书《深入解析 Java 编译器:源码剖析与实例详解》《深入剖析 Java 虚拟机:源码剖析与实例详解》,为什么对我影响比较大呢?因为是我写的
youjianchuiyan
    2

youjianchuiyan   1 天前

《 unix 网络编程》
musi
    3

musi   1 天前

来推荐几本爱民老师的书
《程序原本》
《我的架构思想》
letianqiu
    4

letianqiu   1 天前

@mazhimazh 《深入剖析 Java 虚拟机:源码剖析与实例详解》预计什么时候上市?
SearchDream
    5

SearchDream   1 天前 via iPhone

TCP/IP 详解
xylxAdai
    6

xylxAdai   1 天前   ❤️ 6

《深入理解计算机系统》,我觉得每个学习计算机的都应该看一下。
HENQIGUAI
    7

HENQIGUAI   1 天前

《程序员之禅》《禅与摩托车维修艺术》《黑客与画家》
milkleeeeee
    8

milkleeeeee   1 天前

我来个不那么高大上的……大学的时候买了本《 JavaScript 权威指南(第六版)》自学,从此开始了前端职业生涯。
mazhimazh
    9

mazhimazh   1 天前

@letianqiu 这个月底吧,估计
bug403
    10

bug403   1 天前

没那末大影响,一本小说 《疯狂的程序员》*影 讲外 @挂

agagega
    11

agagega   1 天前 via iPhone

《疯狂的程序员》
《 C++简明教程》
《 C++沉思录》
《编码》
《程序员修炼之道》
《 UNIX 编程艺术》

CrazyRundong
    12

CrazyRundong   1 天前 via iPhone

应该是大二时看的《 MATLAB 在数学建模中的应用》,觉醒了内心的程序员之魂 (bushi),随后开启了数学建模-推荐系统-xgboost-传统 cv-人工智障的升级打怪之路
btnokami
    13

btnokami   1 天前 via iPhone   ❤️ 2

Design Data Intensive Application,真的神书
codyfeng
    14

codyfeng   1 天前 via Android

Effective C++
More Effective C++
shiny
    15

shiny   1 天前

UNIX 环境高级编程:服务器环境不再神秘
重来:开启了对工作方法的思考,启蒙
禅与摩托车维修艺术:始于技术,超脱技术细节,思考哲学问题
Hallelu
    16

Hallelu   1 天前

@mazhimazh 哈哈哈哈哈哈 有被装到
letianqiu
    17

letianqiu   23 小时 44 分钟前

@mazhimazh 到时候会支持一下。
Pagliacii
    18

Pagliacii   23 小时 41 分钟前   ❤️ 1

SICP
ruchee
    19

ruchee   22 小时 12 分钟前

《精通正则表达式(第三版)》:读一遍此书,写正则手到擒来,再也不用到处复制粘贴
xiaket
    20

xiaket   22 小时 7 分钟前

Pro Django
wandehul
    21

wandehul   21 小时 45 分钟前   ❤️ 1

<<知音>><<故事会>>难道不配拥有姓名吗
qping
    22

qping   21 小时 42 分钟前

@milkleeeeee #8 作为一个后端,看了 javascript 设计模式 ,觉得大有收益
Cbdy
    23

Cbdy   21 小时 42 分钟前 via Android

Unix 编程艺术
Issacx
    24

Issacx   21 小时 37 分钟前

我加一本《 Thinking in Java 》,从这里我开始理解面向对象编程。
enGrave93
    25

enGrave93   21 小时 34 分钟前 via Android

《 Java 并发编程之美》,《 Android 开发艺术探索》,《算法(第 4 版)》
feather12315
    26

feather12315   17 小时 55 分钟前 via Android

@ruchee #19 读完了也忘光了。

程序员的自我修养、Linux 高级环境编程、Linux Inside 、Intel 微处理器 /计算机组成原理、龙书。

feather12315
    27

feather12315   17 小时 54 分钟前 via Android

@ruchee #19 强推 regex101.com ,有了这个才是手到擒来
csfreshman
    28

csfreshman   17 小时 52 分钟前

UNIX 高级环境编程 和 SICP,大三 大四啃了一年,以为自己看懂了,工作后常伴左右温故知新
morty0
    29

morty0   17 小时 42 分钟前

designing data-intensive applications
MeatIndustry
    30

MeatIndustry   17 小时 20 分钟前 via iPhone

收集一波大家的神书…
Arthurccc
    31

Arthurccc   17 小时 1 分钟前

好贴。希望大家踊跃。
WangTx1996
    32

WangTx1996   17 小时 0 分钟前 via iPhone

SICP 和 CSAPP
chevalier
    33

chevalier   16 小时 38 分钟前

《黑客与画家》
《构建高性能 Web 站点》郭欣
《 C Primer Plus 》 Stephen Prata
《 Go 预言学习笔记》雨痕
katsusan
    34

katsusan   16 小时 33 分钟前

CSAPP+APUE+DDIA
aguesuka
    35

aguesuka   16 小时 22 分钟前

“The HoTT Bokk”
levelworm
    36

levelworm   15 小时 15 分钟前 via Android

@mazhimazh 大佬能不能说一说学习和工作的经历?感觉很多人虽然有很久的工作经验,但是技术上并没有多少提高。
NetCobra
    37

NetCobra   12 小时 12 分钟前   ❤️ 1

《人月神话》
《代码整洁之道》
chenyu0532
    38

chenyu0532   9 小时 32 分钟前

算法 head first 设计模式 + 若干本设计模式的书 代码整洁之道。
可能我的业务比较简单吧,我越来越觉得设计模式*重要,算法知道怎么回事就行了,在面试中比较有用 。
whywaoxaks
    39

whywaoxaks   9 小时 24 分钟前

小时候家里书架上的 谭浩强的《 basic 语言》。。
没这本书,估计不会对编程感兴趣。。
acerlawson
    40

acerlawson   9 小时 22 分钟前 via iPhone

CSAPP+OSTEP+CA:AQA
tonzeng
    41

tonzeng   9 小时 0 分钟前

《从删库到跑路》.jpg
xin053
    42

xin053   8 小时 42 分钟前

《软件调试》
BrainOnline
    43

BrainOnline   8 小时 42 分钟前

《陈景润传》
BrainOnline
    44

BrainOnline   8 小时 40 分钟前

#43 小学时候读的这本书,然后开启自己对数学的兴趣,否则之前是偏文科一些。
ffLoveJava
    45

ffLoveJava   8 小时 37 分钟前

先 Mark 一下 过会我在=商场
Rebely
    46

Rebely   8 小时 34 分钟前

流畅的 python
mazhimazh
    47

mazhimazh   8 小时 34 分钟前

@levelworm 我工作也其实接近 10 年了吧,前 6 年都是做计算广告的,本来打算把计算广告的业务走通,后来觉得个人的性格不适合做业务,适合做技术,所以职业规划就变为了走技术,精通一个点了,下定决心研究虚拟机,为了让学习有产出就写了 2 本书,现在也做虚拟机相关工作,所以说只要决定了,就要好好准备,等机会来了就能抓住上车了
yunyuyuan
    48

yunyuyuan   8 小时 33 分钟前

《如何讨取富婆欢心》
zjj19950716
    49

zjj19950716   8 小时 29 分钟前

代码大全
coldmonkeybit
    50

coldmonkeybit   8 小时 27 分钟前

应该是《操作系统导论》,我非科班
necodba
    51

necodba   8 小时 26 分钟前

金鳞岂是池中物…
wangxn
    52

wangxn   8 小时 25 分钟前 via Android

深入浅出 MFC
深入 C++ 对象模型
前者入门,后者深入。都是侯捷写的或者翻译的书。
raptor
    53

raptor   8 小时 25 分钟前

Modern C++ Design: 看了半本决定放弃用了十来年的 C++,因为觉得这样的 C++不是我想要的,不是我玩它,是它玩我,后来改用 Python 十几年,表示还是这个好。

其它影响比较大的就是《人月神话》《人件》《软件需求》这类。

Longerrrr
    54

Longerrrr   8 小时 24 分钟前

编码
searene
    55

searene   8 小时 24 分钟前

Designing Data-Intensive Applications
nspih
    56

nspih   8 小时 13 分钟前

脊椎康复指南
mazhimazh
    57

mazhimazh   8 小时 9 分钟前

《如何与产品经理友好相处》
xhldtc
    58

xhldtc   7 小时 58 分钟前

对人生影响较大的书籍:《英雄志》
gaodq
    59

gaodq   7 小时 47 分钟前

《数据密集型应用系统设计》
https://book.douban.com/subject/30329536/
shanghai1943
    60

shanghai1943   7 小时 46 分钟前

《代码整洁之道》 《黑客与画家》《 程序员的修炼之道:从小工到专家 》《 Eeffective java 》
abc635073826
    61

abc635073826   7 小时 38 分钟前

《如何活到 80 岁》《如何活到 90 岁》《如何比别人活的长》
weiwenhao
    62

weiwenhao   7 小时 30 分钟前

《代码整洁之道》《计算机程序的构造与解释》《球状闪电》《凡人修仙传》
chigeyaowaner
    63

chigeyaowaner   7 小时 28 分钟前

《 程序员的修炼之道:从小工到专家 》+1,这本改变了我很多,每次搬家还要带着走。第二版比*版的内容做了一些扩充,*版有些内容在第二版里做了删减。无论是曳光弹还是简单设计等等,都很受用,也很经典,现在还会推荐给我的学弟学妹们。
不想看书的还可以看视频,有些内容讲的还是很不错的: https://www.zentao.net/redirect-index-19380.html,唯一的不足就是视频输出频率快,经常需要按暂停。个人还是希望书籍看完再看一些视频或者一些点评。

还有一本《代码整洁之道》,讲了很多关于代码整洁的重要性和实践,还给出了一些工具,只要遵循这些规则,就能编写出干净的代码,从而有效提升代码质量。这本书也是及其推荐的一本。

viator42
    64

viator42   7 小时 25 分钟前

「吃掉那只青蛙 : 拒*穷忙,把时间留给*重要的事」
4771314
    65

4771314   7 小时 23 分钟前   ❤️ 1

《颈椎病康复指南》
RudyS
    66

RudyS   7 小时 21 分钟前

Ayn Rand: 《源泉》《理想》《阿特拉斯耸耸肩》
silently9527
    67

silently9527   7 小时 12 分钟前

《程序员健康指南》《 MySQL 是怎样运行的 : 从根儿上理解 MySQL 》《算法第四版》
silently9527
    68

silently9527   7 小时 8 分钟前   ❤️ 1

《全国富婆通讯录》
meshell
    69

meshell   7 小时 8 分钟前

代码大全
0xZhangKe
    70

0xZhangKe   6 小时 50 分钟前

重构 /改善既有代码设计
Loserzhu
    71

Loserzhu   6 小时 38 分钟前

csapp
yutonliu
    72

yutonliu   6 小时 37 分钟前

前列腺养生保健
lovedebug
    73

lovedebug   6 小时 37 分钟前

技术科普书籍 《信息简史》
yingo
    74

yingo   6 小时 21 分钟前

apue,这本书直接看出快感来了..
Brentwans
    75

Brentwans   6 小时 11 分钟前

《谭浩强 c 语言程序设计》无出其右
TUNGH
    76

TUNGH   6 小时 6 分钟前

@mazhimazh 递茶
huZhao
    77

huZhao   5 小时 45 分钟前   ❤️ 1

《颈椎病的预防》,《一本书读懂颈椎病》,《痔疮》,《近视眼》,《减肥》,《如何比别人活的长》
tonghuashuai
    78

tonghuashuai   5 小时 25 分钟前

《 Redis 设计与实现》 – 当时在通勤的地铁上花了几天看完的,现在想想这本书真的是简单易懂读起来没有压力但又干货满满的一本小书
chairuosen
    79

chairuosen   5 小时 3 分钟前

代码大全
zhoudaiyu
    80

zhoudaiyu   4 小时 21 分钟前   ❤️ 1

运维向:
1 、Kubernetes in Action (顾名思义,讲 K8S 的,深入浅出,没有生硬的感觉,我的 K8S 入门书。马上出第二版了)
2 、Systems Performance – Enterprise and the Cloud (讲了一些 Linux 下的性能调优的,还有一些监控工具的,很不错)
3 、Fluent Python ( Python 进阶了,当初刚做运维学了几个月 Python 我就飘了,然后看了这本书仿佛觉得我学了假的 Python,第二本的英文版已经可以在 Safari 上看了)
4 、Wireshark 网络分析就这么简单(运维不懂网络有点说不过去了,这本书直接从例子入手讲一些网络的知识,推荐)
Phariel
    81

Phariel   4 小时 14 分钟前

我*近在看这本
编码:隐匿在计算机软硬件背后的语言

对于信息通讯产业人士比较有帮助

Klingon
    82

Klingon   4 小时 9 分钟前

严肃诚恳的推荐《荀子·劝学》
wzxlovesy
    83

wzxlovesy   3 小时 32 分钟前 via Android

OSTEP
naruco
    84

naruco   2 小时 14 分钟前

在没有扎实基础的前提下,于引擎搜索各类奇技淫巧都是在浪费时间;
我就是个例子,表面上解决了很多问题,实际狗屁不通。
《荀子·劝学》 +1
看了几句,甚好!
fkdtz
    85

fkdtz   1 小时 49 分钟前

《编码 : 隐匿在计算机软硬件背后的语言》

写了 N 年代码之后偶然看到这本书,让我认识到原来之前一直都在计算机的门外徘徊,这本书让我摸到了计算机的大门 。

这本书让人从信息的本质去思考:写这么多代码,归根结底是在干嘛?

Android无需权限显示悬浮窗, 兼谈逆向分析app

前言

*近UC浏览器中文版出了一个快速搜索的功能, 在使用其他app的时候, 如果复制了一些内容, 屏幕顶部会弹一个窗口, 提示一些操作, 点击后跳转到UC, 显示这个悬浮窗不需要申请android.permission.SYSTEM_ALERT_WINDOW权限.

如下图, 截图是在使用Chrome时截的, 但是屏幕顶部却有UC的view浮在屏幕上. 我使用的是小米, 我并没有给UC授悬浮窗权限, 所以我看到这个悬浮窗时是很震惊的.

%title插图%num

截图

悬浮窗原理

做过悬浮窗功能的人都知道, 要想显示悬浮窗, 要有一个服务运行在后台, 通过getSystemService(Context.WINDOW_SERVICE)拿到WindowManager, 然后向其中addViewaddView第二个参数是一个WindowManager.LayoutParamsWindowManager.LayoutParams中有一个成员type, 有各种值, 一般设置成TYPE_PHONE就可以悬浮在很多view的上方了, 但是调用这个方法需要申请android.permission.SYSTEM_ALERT_WINDOW权限, 在很多机型上, 这个权限的名字叫悬浮窗, 比如小米手机上默认是禁用这个权限的, 有些恶意app会用这个权限弹广告, 而且很难追查是哪个应用弹的. 如果这个权限被禁用, 那么结果就是悬浮窗无法展示, 比如有道词典复制查词功能, 在小米手机上经常没用, 其实是用户没有授权, 而且应用也没有引导用户给它打开授权.

现在UC能突破这个限制, 我很好奇它是怎么做到的.

研究实现

Android开发有点蛋疼的地方就是太容易被反编译, 但有时这也成为我们研究别人app的一种手段.

反编译

使用apktool可以很轻松的反编译UC.

找代码

逆向别人的app, 比较关键的地方是怎么找代码, 因为代码基本上都是混淆的, 直接看肯定是看不懂的, 只能去找, 突破口一般在字符资源上, 比如我们看到上图中的快速搜索是UC的字符, 那么我们到res/values/strings.xml去找快速搜索, 就可以找到下面的内容

<string name="dark_search_banner_search">快速搜索</string>

这里我们拿到了快速搜索对应的名字dark_search_banner_search, Android在编译时会给每个资源分配一个id, 我们grep一下这个字符资源的名字就能知道id是多少, 一般在R.javares/values/public.xml中有定义, 我直接到public.xml中找到了它的id

<public type="string" name="dark_search_banner_search" id="0x7f070049" />

有了字符资源的id 0x7f070049, 我们再在代码里面grep一下这个id, 就能知道哪几个文件使用了这个字符资源.

之所以这么确定是在代码里, 是因为UC在我们复制的内容不同时, 悬浮窗标题会不一样, 一定是在代码里控制的, 结果如下

./com/uc/browser/b/f.smali

结果可能和大家不一样, 但是一定会找到一个被混淆的smali文件

看代码

这一部应该是*恶心的. smali代码和java代码的关系, 就像汇编代码和C++代码, 但是smali比汇编代码要容易理解的多, 不然也不会有那么多公司故意将代码写在C++层了.

虽然代码都被混淆了, 而且以我们不熟悉的方式出现, 但我们可以根据一些蛛丝马迹来判断代码的执行, 比如Framework的类和API是不能被混淆的, 这也是我们能看懂smali的原因之一, 我们可以结合这些面包屑来还原整个app代码, 当然这需要我们对smali很熟悉, 如果不熟悉smali, 至少要对Android的API熟悉. 因为有时实在看不懂, 我们要靠猜来还原一段代码的逻辑.

首先在代码里面找到0x7f070049, 发现了如下代码

    (省略)
    const v3, 0x7f070049

    invoke-virtual {v1, v3}, Landroid/content/res/Resources;->getString(I)Ljava/lang/String;

    move-result-object v1

    iput-object v1, v0, Lcom/uc/browser/b/a;->dpC:Ljava/lang/String;

    :cond_9

    (省略)

    invoke-virtual {v0, v1}, Lcom/uc/browser/b/a;->o(Landroid/graphics/drawable/Drawable;)V
    :try_end_2
    .catch Ljava/lang/Exception; {:try_start_2 .. :try_end_2} :catch_0

    goto/16 :goto_0
    (省略)

这是0x7f070049出现之后的一部分代码, 一路看下来, 其实都是在取值赋值, 就拿0x7f070049来说:

#使v3寄存器的值为0x7f070049
    const v3, 0x7f070049
#v1是Resources实例, 调用它的getString方法, 方法的参数是v3中的值
    invoke-virtual {v1, v3}, Landroid/content/res/Resources;->getString(I)Ljava/lang/String;
#将结果存入v1寄存器
    move-result-object v1

其实就是我们常用的getResources().getString
其实如果一直这么看下去, 会发现毫无头绪, 剩下的代码一直在干差不多的事情, 所以我只截取了这部分, 注意*后一行

goto/16 :goto_0

也就是说, 有可能代码转到goto_0那儿去了, 那么看看goto_0那里又写了些什么

    :goto_0
    (省略)

    const-string v1, "window"

    invoke-virtual {v0, v1}, Landroid/content/Context;->getSystemService(Ljava/lang/String;)Ljava/lang/Object;

    move-result-object v0

    check-cast v0, Landroid/view/WindowManager;

    invoke-interface {v0}, Landroid/view/WindowManager;->getDefaultDisplay()Landroid/view/Display;

    move-result-object v0

    invoke-virtual {v0}, Landroid/view/Display;->getWidth()I

    move-result v0

    iget-object v1, v10, Lcom/uc/browser/b/a;->dpx:Landroid/view/WindowManager$LayoutParams;

    iput v0, v1, Landroid/view/WindowManager$LayoutParams;->width:I

    iget-object v0, v10, Lcom/uc/browser/b/a;->dpx:Landroid/view/WindowManager$LayoutParams;

    invoke-virtual {v10}, Lcom/uc/browser/b/a;->getContext()Landroid/content/Context;

    move-result-object v1

    invoke-virtual {v1}, Landroid/content/Context;->getResources()Landroid/content/res/Resources;

    move-result-object v1

    const v2, 0x7f0d0022

    invoke-virtual {v1, v2}, Landroid/content/res/Resources;->getDimension(I)F

    move-result v1

    float-to-int v1, v1

    iput v1, v0, Landroid/view/WindowManager$LayoutParams;->height:I

    iget-object v0, v10, Lcom/uc/browser/b/a;->mWindowManager:Landroid/view/WindowManager;

    iget-object v1, v10, Lcom/uc/browser/b/a;->dpx:Landroid/view/WindowManager$LayoutParams;

    invoke-interface {v0, v10, v1}, Landroid/view/WindowManager;->addView(Landroid/view/View;Landroid/view/ViewGroup$LayoutParams;)V

其实看到const-string v1, "window", 我们就应该有所警惕了, 这可能是关键代码了. 为什么这么说? 因为悬浮窗的实现里面, 需要获取WindowManager, 从而需要调用Context.getSystemService(Context.WINDOW_SERVICE), 而官方文档写了Context.WINDOW_SERVICE就是常量window. 而后我们看到代码中构造了WindowManager.LayoutParams, *终在addView时传入.

看到这里, 我也觉得很奇怪, 我在悬浮窗原理中写的是我知道的实现悬浮窗的方法, UC的实现好像跟我调用的是相同的API, 也没看到反射之类可能展示奇技淫巧的代码, 为什么UC就可以不需要权限直接显示悬浮窗呢?

猜测

我认为addView的第二个参数WindowManager.LayoutParams可能是关键, 所以我需要知道UC是如何构造这个WindowManager.LayoutParams的.

由于是系统的类, 无法混淆, 直接搜索LayoutParams就找到了下面的代码

iget-object v1, v10, Lcom/uc/browser/b/a;->dpx:Landroid/view/WindowManager$LayoutParams;

这句话就是把v10的值赋给v1v10com/uc/browser/b/a的成员dpx, 那么打开com/uc/browser/b/a.smali看看dpx到底是怎么构造的.

    (省略)

.field dpx:Landroid/view/WindowManager$LayoutParams;

    (省略)
    .line 68
    new-instance v0, Landroid/view/WindowManager$LayoutParams;

    invoke-direct {v0}, Landroid/view/WindowManager$LayoutParams;-><init>()V

    iput-object v0, p0, Lcom/uc/browser/b/a;->dpx:Landroid/view/WindowManager$LayoutParams;

    .line 69
    if-eqz p2, :cond_0

    .line 70
    iget-object v0, p0, Lcom/uc/browser/b/a;->dpx:Landroid/view/WindowManager$LayoutParams;

    const/16 v1, 0x7d5

    iput v1, v0, Landroid/view/WindowManager$LayoutParams;->type:I

    .line 74
    :goto_0
    iget-object v0, p0, Lcom/uc/browser/b/a;->dpx:Landroid/view/WindowManager$LayoutParams;

    const/4 v1, 0x1

    iput v1, v0, Landroid/view/WindowManager$LayoutParams;->format:I
    (省略)

这里的代码就很简单的, 我*先看的是下面这段

    const/16 v1, 0x7d5

    iput v1, v0, Landroid/view/WindowManager$LayoutParams;->type:I

这两句代码就是把WindowManager.LayoutParams.type字段设成0x7d5, 官网上写了0x000007d5是WindowManager.LayoutParams.TYPE_TOAST的值.

验证

实际测试了一下, 将type设置成TYPE_TOAST果然有奇效, 不需要android.permission.SYSTEM_ALERT_WINDOW权限就能显示一个悬浮窗.

之前我一直以为调用了系统WindowManager.addView需要android.permission.SYSTEM_ALERT_WINDOW权限, 但实际上调用这个方法是不需要权限的, 在Android源码中有这么一段

public int checkAddPermission(WindowManager.LayoutParams attrs) {
    int type = attrs.type;

    if (type < WindowManager.LayoutParams.FIRST_SYSTEM_WINDOW
            || type > WindowManager.LayoutParams.LAST_SYSTEM_WINDOW) {
        return WindowManagerImpl.ADD_OKAY;
    }
    String permission = null;
    switch (type) {
        case TYPE_TOAST:
            // XXX right now the app process has complete control over
            // this...  should introduce a token to let the system
            // monitor/control what they are doing.
            break;
        case TYPE_INPUT_METHOD:
        case TYPE_WALLPAPER:
            // The window manager will check these.
            break;
        case TYPE_PHONE:
        case TYPE_PRIORITY_PHONE:
        case TYPE_SYSTEM_ALERT:
        case TYPE_SYSTEM_ERROR:
        case TYPE_SYSTEM_OVERLAY:
            permission = android.Manifest.permission.SYSTEM_ALERT_WINDOW;
            break;
        default:
            permission = android.Manifest.permission.INTERNAL_SYSTEM_WINDOW;
    }
    if (permission != null) {
        if (mContext.checkCallingOrSelfPermission(permission)
                != PackageManager.PERMISSION_GRANTED) {
            return WindowManagerImpl.ADD_PERMISSION_DENIED;
        }
    }
    return WindowManagerImpl.ADD_OKAY;
}

可以猜到这个方法是往系统的WindowManageraddView的时候做权限检查用的, 那个type就是我们在构造WindowManager.LayoutParams时赋值的type, 可以看到, 除了TYPE_TOAST, 其他都是要权限的, 而且非常喜感的是, 代码中的注释还说他们现在对这种type毫无限制, 应该引入标记来限制开发者.

处理兼容性

在这篇文章刚刚公布的时候, 就有同学反馈悬浮窗无法接收事件, 刚开始我并没有特别在意, 在廖祜秋大神做了一个demo之后, 这篇文章阅读量又涨了不少, 随即收到更多反馈事件的问题, 我今天晚上借了台MIUI V5 4.2.2实测了一下, 这台机器上UC的快速搜索功能也无法正常使用.

在这个ROM上表现为:
使用TYPE_PHONE这类需要权限的type时, 只有在app处于前台时能显示悬浮窗, 且能正常接受触摸事件. 如果在应用详情里面授悬浮窗权限, 则工作完全正常.
(这里是MIUI V5对悬浮窗的特殊处理, 现在的ROM, 包括MIUI V6上, 如果不授权, 无法显示任何悬浮窗)
使用TYPE_TOAST这个不需要权限的type时, 悬浮窗正常显示, 但不能接受触摸事件.

我重新检查了一下smali代码, 发现UC是有分版本处理的, 不过因为smali代码的规则问题, 很难直接看出来, 我把分析过程写出来, 顺便解释一下smali的语法, 供大家以后逆向时拿来参考.

这次我是在OS X上反编译的, 所以变量名可能略有区别.

接着上面com/uc/browser/b/a.smali中查看dpx的构造过程, 代码如下:

.field dpx:Landroid/view/WindowManager$LayoutParams;

(省略)

# direct methods
.method public constructor <init>(Landroid/content/Context;Z)V
    .locals 7

    (省略)

    .line 68
    new-instance v0, Landroid/view/WindowManager$LayoutParams;

    invoke-direct {v0}, Landroid/view/WindowManager$LayoutParams;-><init>()V

    iput-object v0, p0, Lcom/uc/browser/b/a;->dpx:Landroid/view/WindowManager$LayoutParams;

    .line 69
    if-eqz p2, :cond_0

    .line 70
    iget-object v0, p0, Lcom/uc/browser/b/a;->dpx:Landroid/view/WindowManager$LayoutParams;

    const/16 v1, 0x7d5

    iput v1, v0, Landroid/view/WindowManager$LayoutParams;->type:I

为了方便说明, 我遵循smali的规则, 它用.line XX, 我们就说这是第XX行的代码.

上面是我之前分析得到UC使用的是TYPE_TOAST的地方, 证据就是第70行的const/16 v1, 0x7d5, 但是要知道, smali代码没有跳转的话, 就是从上往下执行, 我们看第69行的代码如下:

.line 69
if-eqz p2, :cond_0

这句话的意思是如果p2等于0, 控制流跳转到cond_0, 否则就是继续顺序往下执行. 也就是说UC只有在p2 != 0条件满足的时候才会使用TYPE_TOAST, 我们看看cond_0对应的代码.

    .line 72
    :cond_0
    iget-object v0, p0, Lcom/uc/browser/b/a;->dpx:Landroid/view/WindowManager$LayoutParams;

    const/16 v1, 0x7d2

    iput v1, v0, Landroid/view/WindowManager$LayoutParams;->type:I

这里很简单, 就是将0x7d2赋给了type, 官网写了0x000007d2TYPE_PHONE, 也就是说UC在某种情况下还是会用需要权限的老方法展示悬浮窗.

现在问题是条件是什么, 关键在p2, 在smali里面, 有两种寄存器命名规则, 一种叫v命名规则, 另一种是p命名规则, 当然只是命名规则而已, 在使用apktool时是可以选的. 这里是p命名规则.

我刚才分析的赋值过程, 所在的方法是下面这个, 我在刚才的代码片段中也保留了这个部分.

# direct methods
.method public constructor <init>(Landroid/content/Context;Z)V
    .locals 7

这就是com/uc/browser/b/a的构造方法, dpx就是在构造方法里初始化的, .locals 7告诉我们这个方法中将出现7个局部寄存器(local register), 名字是v0, v1…v6, 而这个方法的参数有3个, 隐式告诉我们这个方法中将出现3个参数寄存器(parameter register), 名字分别是p0, p1, p2.

我是怎么知道这个方法有3个参数的呢. smali中非静态方法, 都隐含一个参数p0, 指向自身, 和Java中的this是一个意思, 而方法的参数写在括号里, 也就是Landroid/content/Context;Z, 其中Landroid/content/Context;很明显就是Android中的Context, 值存储在p1里, 而Z对应的是Android中的boolean, p2就是他了.

也就是说, type是用TYPE_TOAST还是用TYPE_PHONE, 取决于这个构造方法的第二个参数, 那到底谁构造了com/uc/browser/b/a呢? 可以去代码里面搜形如new-instance ***, Lcom/uc/browser/b/a;的代码. 更保险的做法是搜Lcom/uc/browser/b/a然后一个一个的看.

我在com/uc/browser/b/f.smali里面找到了下面的代码:

    .prologue
    const/4 v0, 0x0

    const/4 v1, 0x1

    (省略)

    new-instance v3, Lcom/uc/browser/b/a;

    iget-object v4, v9, Lcom/uc/browser/b/e;->mContext:Landroid/content/Context;

    sget v5, Landroid/os/Build$VERSION;->SDK_INT:I

    const/16 v6, 0x13

    if-lt v5, v6, :cond_0

    move v0, v1

    :cond_0
    invoke-direct {v3, v4, v0}, Lcom/uc/browser/b/a;-><init>(Landroid/content/Context;Z)V

这段代码首先是创建了com/uc/browser/b/a的实例, 存储在v3中, 从另一处拿到了一个Context存储在v4中, 然后拿到了当前系统的android.os.Build.VERSION.SDK_INT存储在v5中, 此时将v6的值设为0x13, 千万别粗心看成13了, 我好几次都觉得这是13, 其实是十进制的19, 接下来是一个条件分支, 如果v5的值小于v6, 也就是说android.os.Build.VERSION.SDK_INT < 19, 直接跳转到cond_0, 否则先将v1的值赋给v0, 再顺序执行.

这句代码

invoke-direct {v3, v4, v0}, Lcom/uc/browser/b/a;-><init>(Landroid/content/Context;Z)V

就是调用v3的构造方法, 参数是v4和v0, 分析一下上面这段代码的逻辑就是:
如果当前系统API level小于19, 那么第二个参数就是0, 否则就是1.

而这第二个参数的值就是之前我们分析的p2的值, UC只有在p2 != 0条件满足的时候才会使用TYPE_TOAST, 把整个逻辑串起来就是:

UC在API level >= 19的时候, 使用TYPE_TOAST, 其他情况使用TYPE_PHONE(需要权限).

可能是为了规避在低版本TYPE_TOAST不能接受事件的问题.

 

实测效果

我之前写的一个app有悬浮窗播放功能, 支持拖动窗口和点击暂停, 关闭窗口等等, 在4.4.4上实测功能正常.

%title插图%num

无权限悬浮窗演示gif

感谢微博上关注的大神廖祜秋, 他做了个demo, 虽然交互和UC不同, 可以参考一下实现.

%title插图%num

廖祜秋大神的demo

关于这个, 他也写了一篇Android 悬浮窗的小结

其他补充

评论区的浮海大虾同学有更多补充如下:

TYPE_TOAST一直都可以显示, 但是用TYPE_TOAST显示出来的在2.3上无法接收点击事件, 因此还是无法随意使用.
下面是我之前研究后台线程显示对话框的时候记得笔记, 大家可以看看我们项目中有需求需要在后台任务中显示Dialog, 项目*初的做法是用Activity模拟Dialog, 一个Activity已经承载了近20种Dialog, 代码混乱至*. 后来我发现Dialog可以通过改变Window Type实现不依赖Activity显示, 然后就很兴奋的要在使用这种方式来作为新的实现方式.
*初WindowType是WindowManager.LayoutParams.TYPE_SYSTEM_ALERT, 可是这是悬浮窗了, MIUI会默认禁止(真他妈操蛋,也没有任何提示)*终放弃. 后来试着换成了WindowManager.LayoutParams.TYPE_TOAST, 起初效果很好,MIUI也不禁止了, 哪里都能显示, 这下开心了. 可是后来又发现在2.3上不能接收点击事件, 也就是说Dialog上的按钮不能点击, 这他妈就很操蛋了, 又放弃了. 又试了试其他的Type都不能满足需求, 结果如下:TYPE_SEARCH_BAR: 未知
TYPE_ACCESSIBILITY_OVERLAY: 拒*使用
TYPE_APPLICATION: 只能配合Activity在当前APP使用TYPE_APPLICATION_ATTACHED_DIALOG: 只能配合Activity在当前APP使用
TYPE_APPLICATION_MEDIA: 无法使用(什么也不显示)
TYPE_APPLICATION_PANEL: 只能配合Activity在当前APP使用(PopupWindow默认就是这个Type)
TYPE_APPLICATION_STARTING: 无法使用(什么也不显示)
TYPE_APPLICATION_SUB_PANEL: 只能配合Activity在当前APP使用TYPE_BASE_APPLICATION: 无法使用(什么也不显示)
TYPE_CHANGED: 只能配合Activity在当前APP使用
TYPE_INPUT_METHOD: 无法使用(直接崩溃)
TYPE_INPUT_METHOD_DIALOG: 无法使用(直接崩溃)
TYPE_KEYGUARD_DIALOG: 拒*使用
TYPE_PHONE: 属于悬浮窗(并且给一个Activity的话按下HOME键会出现看不到桌面上的图标异常情况)
TYPE_TOAST: 不属于悬浮窗, 但有悬浮窗的功能, 缺点是在Android2.3上无法接收点击事件
TYPE_SYSTEM_ALERT: 属于悬浮窗, 但是会被禁止

尾声

现在我们都知道了如何在不申请权限的情况下显示悬浮窗, 我相信以中国Android开发者的脑洞, 一定会有很多有趣或恶心的功能被开发出来, 一方面我自己觉得这个东西很有用, 可以实现一些很神奇的功能, 另一方面又担心这个API被滥用, *终不得不限制权限.

还有就是, 逆向分析仅用于学习

 

Android开发——内存优化 图片处理

 用缓存避免内存泄漏

很常见的一个例子就是图片的三级缓存结构,分别为网络缓存,本地缓存以及内存缓存。在内存缓存逻辑类中,通常会定义这样的集合类。

[java]
  1. private HashMap<String, Bitmap> mMemoryCache = new HashMap<String, Bitmap>();//String类为该图片对应url  

三级缓存结构过程介绍:

在用户切换到展示图片的界面时,当然是优先判断内存缓存是否为Null,不为空直接展示图片,若为空,同样的逻辑去判断本地缓存(不为空便设置内存缓存并展示图片),本地缓存再为空才会根据该图片的url用网络下载类去下载该图片并展示图片(当然了,下载到图片后会有设置本地缓存以及内存缓存的操作)。

内存泄漏的问题就出现在内存缓存中:只要HashMap对象实例被引用,而Bitmap对象又都是强引用,Bitmap中图片越来越多,即便是内存溢出了,垃圾回收器也不会处理。

 

解决方案:

(1)我们可以选择使用软引用,从而在内存不足时,垃圾回收器更容易回收Bitmap垃圾。

[java]
  1. private HashMap<String, SoftReference<Bitmap>> mMemoryCache = new HashMap<String, SoftReference<Bitmap>>();  

(2)Android2.3以后,SoftReference不再可靠。垃圾回收期更容易回收它,不再是内存不足时才回收软引用。那么缓存机制便失去了意义。

Google官方建议使用LruCache作为缓存的集合类。其实内部封装了LinkedHashMap。内部原理是一直判断集合大小是否超出给定的*大值,超出就把*早*少使用的对象踢出集合。

[java]
  1. private LruCache<String, Bitmap> mMemoryCache = new LruCache<String, Bitmap>  
  2. ((int)(Runtime.getRuntime().maxMemory()/8)){   
  3. //用*大内存的1/8分配给这个集合使用  
  4. //让这个集合知道每个图片的大小  
  5. @Override  
  6. protected int sizeOf(String key, Bitmap value){  
  7. int byteCount = value.getRowBytes() * value.getHeight();//计算图片大小,每行字节数*高度  
  8. return byteCount;  
  9.   }
  10. };

 

  优化Bitmap避免内存泄漏

Android中很多控件比如ListView/GridView/ViewPaper通常都会包含很多图片,特别是快速滑动的时候可能加载大量的图片,因此对图片进行优化处理显得尤为重要。

 

  图片质量压缩

 

[java]
  1. public static Bitmap compressImage(Bitmap bitmap){    
  2.     ByteArrayOutputStream baos = new ByteArrayOutputStream();    
  3.     //质量压缩方法,参数100表示不压缩,把压缩后的数据存放到baos中    
  4.     bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos);    
  5.     int options = 100;    
  6.     //循环判断如果压缩后图片大小>50kb就继续压缩    
  7.     while ( baos.toByteArray().length/1024 > 50) {    
  8.          //清空baos    
  9.          baos.reset();
  10.          bitmap.compress(Bitmap.CompressFormat.JPEG, options, baos);
  11.          options -= 10;//每次都减少10    
  12.     }
  13.     //把压缩后的数据baos存放到ByteArrayInputStream中    
  14.     ByteArrayInputStream isBm = new ByteArrayInputStream(baos.toByteArray());    
  15.     //把ByteArrayInputStream数据生成图片    
  16.     Bitmap newBitmap = BitmapFactory.decodeStream(isBm, null, null);    
  17.     return newBitmap;            
  18. }

 

  图片尺寸裁剪

如果说没有裁剪,下载下来是200*200,而ImageView本身是100*100的,具体原因可以参考这篇文章,这样就浪费了一部分内存。

使用BitmapFactory.Options设置inSampleSize就可以缩小图片。如果该值为2,则缩略图的宽和高都是原始图片的1/2,图片的大小就为原始大小的1/4(小于等于1不缩放)。具体方法如下:

 

既然有了inSampleSize的概念,我们就要对比实际图片大小和ImageView控件的大小,如果使用内存直接处理实际图片的Bitmap从而得到实际大小的话,就失去了图片尺寸裁剪的意义,因为内存已经被消耗了。因此BitmapFactory.Options提供了inJustDecodeBounds标志位,当它被设置为true后,再使用decode系列方法时,并不会真正的分配内存空间,这样解码出来的Bitmap为null,但是可以计算出原始图片的真实宽高,即options.outWidth和options.outHeight。通过这两个值,就可以知道图片是否过大了。

 

 

[java]
  1. BitmapFactory.Options options = new BitmapFactory.Options();    
  2. options.inJustDecodeBounds = true;    
  3. BitmapFactory.decodeResource(getResources(), R.id.myimage, options);
  4. int imageHeight = options.outHeight;    
  5. int imageWidth = options.outWidth;    
  6. String imageType = options.outMimeType;

 

这里提供了一个calculateInSampleSize()工具方法来帮我们根据实际情况动态计算合适的inSampleSize。

[java]
  1. public static int calculateInSampleSize( //参2和3为ImageView期待的图片大小  
  2.             BitmapFactory.Options options, int reqWidth, int reqHeight) {    
  3.     // 图片的实际大小  
  4.     final int height = options.outHeight;    
  5.     final int width = options.outWidth;    
  6.     //默认值  
  7.     int inSampleSize = 1;    
  8.     //动态计算inSampleSize的值  
  9.     if (height > reqHeight || width > reqWidth) {    
  10.         final int halfHeight = height/2;  
  11.         final int halfWidth = width/2;  
  12.         while( (halfHeight/inSampleSize) >= reqHeight && (halfWidth/inSampleSize) >= reqWidth){  
  13.             inSampleSize *= 2;  
  14.         }
  15.     }
  16.     return inSampleSize;    
  17. }

 

创建一个完整的缩略图方案:

[java]
  1. public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,    
  2.         int reqWidth, int reqHeight) {    
  3.     final BitmapFactory.Options options = new BitmapFactory.Options();    
  4.     options.inJustDecodeBounds = true;    
  5.     BitmapFactory.decodeResource(res, resId, options);
  6.     // 计算inSampleSize,因为前面已经设置过标志位并调用了decode方法,所以参数option包含了真实宽高信息  
  7.     options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
  8.     // 别忘记将opts.inJustDecodeBound设置回false,否则获取的bitmap对象还是null   
  9.     options.inJustDecodeBounds = false;    
  10.     //重新加载图片  
  11.     return BitmapFactory.decodeResource(res, resId, options);    
  12. }

 

当我们在使用ImageView进行设置图片资源时:

[java]
  1. mImageView.setImageBitmap( //ImageView所期望的图片大小为100*100像素  
  2.     decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));    

 

  改变图片颜色模式

%title插图%num

Android默认的颜色格式是ARGB_8888,在不要求透明度的情况下可以改成RGB_565,这样每个像素占用的内从可从4byte将为2byte。

 

一张分辨率为1920×1080的图片,如果Bitmap使用ARGB_8888格式显示的话,占用的内存将是1920x1080x4个字节,将近8M内存。

 

  及时回收资源

 

(1)当界面不可见时我们应当将所有和界面相关的资源进行释放。

我们可以在Activity中重写onTrimMemory()方法,通过switch这个方法中的level参数,判断它是不是等于TRIM_MEMORY_UI_HIDDEN,就说明用户已经离开了我们的程序,此时就可以进行UI相关资源释放操作了,如下所示:

[java]
  1. @Override    
  2. public void onTrimMemory(int level) {    
  3.     super.onTrimMemory(level);    
  4.     switch (level) {    
  5.     case TRIM_MEMORY_UI_HIDDEN:    
  6.         // 进行资源释放操作    
  7.         break;    
  8.     }
  9. }

比如Android3.0开始支持的属性动画中有一类无限循环的动画,它会通过View间接持有Activity的引用,如果没有在onDestroy中停止动画(animator.cancel()),就会泄漏当前的Activity。说起动画,还有一点就是减少帧动画的使用。

(2)Google也建议在onStop()方法中释放资源,但是和上面的释放UI资源是有区别的,因为onStop()方法只是当一个Activity不可见的时候就会调用,比如说用户打开了我们程序中的另一个ActivityB。在onStop()方法中适合去关闭一些读写文件的资源、数据库操作相关的资源等等。

但是像UI相关的资源应该一直要等到onTrimMemory(TRIM_MEMORY_UI_HIDDEN)这个回调之后才去释放,否则从ActivityB回到ActivityA,UI相关的资源会重新加载。

 

至此关于Android内存泄漏的内容总结完毕。

Android开发常见的Activity中内存泄漏及解决办法

上一篇文章楼主提到由Context引发的内存泄漏,在这一篇文章里,我们来谈谈Android开发中常见的Activity内存泄漏及解决办法。本文将会以“为什么”“怎么解决”的方式来介绍这几种内存泄漏。
在开篇之前,先来了解一下什么是内存泄漏。

什么是内存泄漏?

内存泄漏是当程序不再使用到的内存时,释放内存失败而产生了无用的内存消耗。内存泄漏并不是指物理上的内存消失,这里的内存泄漏是值由程序分配的内存但是由于程序逻辑错误而导致程序失去了对该内存的控制,使得内存浪费。

怎样会导致内存泄漏?

  • 资源对象没关闭造成的内存泄漏,如查询数据库后没有关闭游标cursor
  • 构造Adapter时,没有使用 convertView 重用
  • Bitmap对象不在使用时调用recycle()释放内存
  • 对象被生命周期长的对象引用,如activity被静态集合引用导致activity不能释放

在接下来的篇幅里,我们重点讲有关Activity常见的内存泄漏。

内存泄漏1:静态Activities(static Activities)

代码如下:
MainActivity.Java

public class MainActivity extends AppCompatActivity {
    private static MainActivity activity;
    TextView saButton;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        saButton = (TextView) findViewById(R.id.text);
        saButton.setOnClickListener(new View.OnClickListener() {
            @Override public void onClick(View v) {
                setStaticActivity();
                nextActivity();
            }
        });
    }
    void setStaticActivity() {
        activity = this;
    }

    void nextActivity(){
        startActivity(new Intent(this,RegisterActivity.class));
        SystemClock.sleep(1000);
        finish();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        //使用LeakCanary观察是否有内存泄漏
        MyApplication.getRefWatcher().watch(this);
    }
}

LeakCanary检测出的内存泄漏:

这里写图片描述

为什么?
在上面代码中,我们声明了一个静态的Activity变量并且在TextView的OnClick事件里引用了当前正在运行的Activity实例,所以如果在activity的生命周期结束之前没有清除这个引用,则会引起内存泄漏。因为声明的activity是静态的,会常驻内存,如果该对象不清除,则垃圾回收器无法回收变量。

怎么解决?
*简单的方法是在onDestory方法中将静态变量activity置空,这样垃圾回收器就可以将静态变量回收。

@Override
    protected void onDestroy() {
        super.onDestroy();
        activity = null;
        //使用LeakCanary观察是否有内存泄漏
        MyApplication.getRefWatcher().watch(this);
    }

内存泄漏2:静态View

代码如下:
MainActivity.java

    ...
    private static View view;
    TextView saButton;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        saButton = (TextView) findViewById(R.id.text);
        saButton.setOnClickListener(new View.OnClickListener() {
            @Override public void onClick(View v) {
                setStaticView();
                nextActivity();
            }
        });
    }
    void setStaticView() {
        view = findViewById(R.id.sv_view);
    }
    ...

LeakCanary检测到的内存泄漏

这里写图片描述

为什么?
上面代码看似没有问题,在Activity里声明一个静态变量view,然后初始化,当Activity生命周期结束了内存也释放了,但是LeakCanary却显示出现了内存泄漏,为什么?问题出在这里,View一旦被加载到界面中将会持有一个Context对象的引用,在这个例子中,这个context对象是我们的Activity,声明一个静态变量引用这个View,也就引用了activity,所以当activity生命周期结束了,静态View没有清除掉,还持有activity的引用,因此内存泄漏了。

怎么解决?
在onDestroy方法里将静态变量置空。

@Override
protected void onDestroy() {
    super.onDestroy();
    view = null;
    MyApplication.getRefWatcher().watch(this);
} 

内存泄漏3:内部类

代码如下:
MainActivity.java

private static Object inner;
void createInnerClass() {
    class InnerClass {
    }
    inner = new InnerClass();
}

View icButton = findViewById(R.id.ic_button);
icButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
        createInnerClass();
        nextActivity();
    }
});

使用LeakCanary检测到的内存泄漏:

这里写图片描述

为什么?
非静态内部类会持有外部类的引用,在上面代码中内部类持有Activity的引用,因此inner会一直持有Activity,如果Activity生命周期结束没有清除这个引用,这样就发生了内存泄漏。

怎么解决?
因为非静态内部类隐式持有外部类的强引用,所以我们将内部类声明成静态的就可以了。

void createInnerClass() {
    static class InnerClass {
    }
    inner = new InnerClass();
}

内存泄漏4:匿名类

void startAsyncTask() {
    new AsyncTask<Void, Void, Void>() {
        @Override protected Void doInBackground(Void... params) {
            while(true);
        }
    }.execute();
}

super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
View aicButton = findViewById(R.id.at_button);
aicButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
        startAsyncTask();
        nextActivity();
    }
});

使用LeakCanary检测到的内存泄漏:

这里写图片描述

为什么?
上面代码在activity中创建了一个匿名类AsyncTask,匿名类和非静态内部类相同,会持有外部类对象,这里也就是activity,因此如果你在Activity里声明且实例化一个匿名的AsyncTask对象,则可能会发生内存泄漏,如果这个线程在Activity销毁后还一直在后台执行,那这个线程会继续持有这个Activity的引用从而不会被GC回收,直到线程执行完成。

怎么解决?
自定义静态AsyncTask类,并且让AsyncTask的周期和Activity周期保持一致,也就是在Activity生命周期结束时要将AsyncTask cancel掉。

内存泄漏5:Handler

代码如下:
MainActivity.java

...
void createHandler() {
    new Handler() {
        @Override public void handleMessage(Message message) {
            super.handleMessage(message);
        }
    }.postDelayed(new Runnable() {
        @Override public void run() {
            while(true);
        }
    }, 1000);
}

...
View hButton = findViewById(R.id.h_button);
hButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
        createHandler();
        nextActivity();
    }
});
...

为什么?
当Android Application启动以后,framework会首先帮助我们完成UI线程的消息循环,也就是在UI线程中,Loop、MessageQueue、Message等等这些实例已经由framework帮我们实现了。所有的Application主要事件,比如Activity的生命周期方法、Button的点击事件都包含在这个Message里面,这些Message都会加入到MessageQueue中去,所以,UI线程的消息循环贯穿于整个Application生命周期,所以当你在UI线程中生成Handler的实例,就会持有Loop以及MessageQueue的引用。并且在Java中非静态内部类和匿名内持有外部类的引用,而静态内部类则不会持有外部类的引用。

怎么解决?
可以由上面的结论看出,产生泄漏的根源在于匿名类持有Activity的引用,因此可以自定义Handler和Runnable类并声明成静态的内部类,来解除和Activity的引用。

内存泄漏6:Thread

代码如下:
MainActivity.java

void spawnThread() {
    new Thread() {
        @Override public void run() {
            while(true);
        }
    }.start();
}

View tButton = findViewById(R.id.t_button);
tButton.setOnClickListener(new View.OnClickListener() {
  @Override public void onClick(View v) {
      spawnThread();
      nextActivity();
  }
});

为什么?
同AsyncTask一样,这里就不过多赘述。

怎么解决?
那我们自定义Thread并声明成static这样可以吗?其实这样的做法并不推荐,因为Thread位于GC根部,DVM会和所有的活动线程保持hard references关系,所以运行中的Thread*不会被GC无端回收了,所以正确的解决办法是在自定义静态内部类的基础上给线程加上取消机制,因此我们可以在Activity的onDestroy方法中将thread关闭掉。

内存泄漏7:Timer Tasks

代码如下:
MainActivity.java

void scheduleTimer() {
    new Timer().schedule(new TimerTask() {
        @Override
        public void run() {
            while(true);
        }
    },1000);
}

View ttButton = findViewById(R.id.tt_button);
ttButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
        scheduleTimer();
        nextActivity();
    }
});

为什么?
这里内存泄漏在于Timer和TimerTask没有进行Cancel,从而导致Timer和TimerTask一直引用外部类Activity。

怎么解决?
在适当的时机进行Cancel。

内存泄漏8:Sensor Manager

代码如下:
MainActivity.java

void registerListener() {
       SensorManager sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
       Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);
       sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_FASTEST);
}

View smButton = findViewById(R.id.sm_button);
smButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
        registerListener();
        nextActivity();
    }
});

为什么?
通过Context调用getSystemService获取系统服务,这些服务运行在他们自己的进程执行一系列后台工作或者提供和硬件交互的接口,如果Context对象需要在一个Service内部事件发生时随时收到通知,则需要把自己作为一个监听器注册进去,这样服务就会持有一个Activity,如果开发者忘记了在Activity被销毁前注销这个监听器,这样就导致内存泄漏。

怎么解决?
在onDestroy方法里注销监听器。

总结

在开发中,内存泄漏*坏的情况是app耗尽内存导致崩溃,但是往往真实情况不是这样的,相反它只会耗尽大量内存但不至于闪退,可分配的内存少了,GC便会更多的工作释放内存,GC是非常耗时的操作,因此会使得页面卡顿。我们在开发中一定要注意当在Activity里实例化一个对象时看看是否有潜在的内存泄漏,一定要经常对内存泄漏进行检测。

关于Android Context 你必须知道的一切

1、Context概念

其实一直想写一篇关于Context的文章,但是又怕技术不如而误人子弟,于是参考了些资料,今天准备整理下写出来,如有不足,请指出,参考资料会在醒目地方标明。

Context,相信不管是*天开发Android,还是开发Android的各种老鸟,对于Context的使用一定不陌生~~你在加载资源、启动一个新的Activity、获取系统服务、获取内部文件(夹)路径、创建View操作时等都需要Context的参与,可见Context的常见性。大家可能会问到底什么是Context,Context字面意思上下文,或者叫做场景,也就是用户与操作系统操作的一个过程,比如你打电话,场景包括电话程序对应的界面,以及隐藏在背后的数据;

但是在程序的角度Context又是什么呢?在程序的角度,我们可以有比较权威的答案,Context是个抽象类,我们可以直接通过看其类结构来说明答案:

%title插图%num

可以看到Activity、Service、Application都是Context的子类;

也就是说,Android系统的角度来理解:Context是一个场景,代表与操作系统的交互的一种过程。从程序的角度上来理解:Context是个抽象类,而Activity、Service、Application等都是该类的一个实现。

在仔细看一下上图:Activity、Service、Application都是继承自ContextWrapper,而ContextWrapper内部会包含一个base context,由这个base context去实现了*大多数的方法。

先扯这么多,有能力了会从别的角度去审视Context,加油~

 

2、Context与ApplicationContext

看了标题,千万不要被误解,ApplicationContext并没有这个类,其实更应该叫做:Activity与Application在作为Context时的区别。嗯,的确是这样的,大家在需要Context的时候,如果是在Activity中,大多直接传个this,当在匿名内部类的时候,因为this不能用,需要写XXXActivity.this,很多哥们会偷懒,直接就来个getApplicationContext。那么大家有没有想过,XXXActivity.this和getApplicationContext的区别呢?

XXXActivity和getApplicationContext返回的肯定不是一个对象,一个是当前Activity的实例,一个是项目的Application的实例。既然区别这么明显,那么各自的使用场景肯定不同,乱使用可能会带来一些问题。

下面开始介绍在使用Context时,需要注意的问题。

 

3、引用的保持

大家在编写一些类时,例如工具类,可能会编写成单例的方式,这些工具类大多需要去访问资源,也就说需要Context的参与。

在这样的情况下,就需要注意Context的引用问题。

例如以下的写法:

[java] view plain copy
  1. package com.mooc.shader.roundimageview;  
  2. import android.content.Context;  
  3. public class CustomManager  
  4. {
  5.     private static CustomManager sInstance;  
  6.     private Context mContext;  
  7.     private CustomManager(Context context)  
  8.     {
  9.         this.mContext = context;  
  10.     }
  11.     public static synchronized CustomManager getInstance(Context context)  
  12.     {
  13.         if (sInstance == null)  
  14.         {
  15.             sInstance = new CustomManager(context);  
  16.         }
  17.         return sInstance;  
  18.     }
  19.     //some methods   
  20.     private void someOtherMethodNeedContext()  
  21.     {
  22.     }
  23. }

对于上述的单例,大家应该都不陌生(请别计较getInstance的效率问题),内部保持了一个Context的引用;

这么写是没有问题的,问题在于,这个Context哪来的我们不能确定,很大的可能性,你在某个Activity里面为了方便,直接传了个this;这样问题就来了,我们的这个类中的sInstance是一个static且强引用的,在其内部引用了一个Activity作为Context,也就是说,我们的这个Activity只要我们的项目活着,就没有办法进行内存回收。而我们的Activity的生命周期肯定没这么长,所以造成了内存泄漏。

那么,我们如何才能避免这样的问题呢?

有人会说,我们可以软引用,嗯,软引用,假如被回收了,你不怕NullPointException么。

把上述代码做下修改:

[java] view plain copy
  1. public static synchronized CustomManager getInstance(Context context)  
  2.     {
  3.         if (sInstance == null)  
  4.         {
  5.             sInstance = new CustomManager(context.getApplicationContext());  
  6.         }
  7.         return sInstance;  
  8.     }

这样,我们就解决了内存泄漏的问题,因为我们引用的是一个ApplicationContext,它的生命周期和我们的单例对象一致。

这样的话,可能有人会说,早说嘛,那我们以后都这么用不就行了,很遗憾的说,不行。上面我们已经说过,Context和Application Context的区别是很大的,也就是说,他们的应用场景(你也可以认为是能力)是不同的,并非所有Activity为Context的场景,Application Context都能搞定。

下面就开始介绍各种Context的应用场景。

 

4、Context的应用场景

%title插图%num

 

大家注意看到有一些NO上添加了一些数字,其实这些从能力上来说是YES,但是为什么说是NO呢?下面一个一个解释:

数字1:启动Activity在这些类中是可以的,但是需要创建一个新的task。一般情况不推荐。

数字2:在这些类中去layout inflate是合法的,但是会使用系统默认的主题样式,如果你自定义了某些样式可能不会被使用。

数字3:在receiver为null时允许,在4.2或以上的版本中,用于获取黏性广播的当前值。(可以无视)

注:ContentProvider、BroadcastReceiver之所以在上述表格中,是因为在其内部方法中都有一个context用于使用。

 

好了,这里我们看下表格,重点看Activity和Application,可以看到,和UI相关的方法基本都不建议或者不可使用Application,并且,前三个操作基本不可能在Application中出现。实际上,只要把握住一点,凡是跟UI相关的,都应该使用Activity做为Context来处理;其他的一些操作,Service,Activity,Application等实例都可以,当然了,注意Context引用的持有,防止内存泄漏。

 

5、总结

好了,到此,Context的分析基本完成了,希望大家在以后的使用过程中,能够稍微考虑下,这里使用Activity合适吗?会不会造成内存泄漏?这里传入Application work吗?

 

Android开发之给控件设置圆角边框

先上效果图:

%title插图%num

具体步骤:

1.在drawable文件夹下新建一个xml文件。

2.在里面填上以下内容:

  1. <?xml version=”1.0″ encoding=”utf-8″?>
  2. <!–自定义的控件圆角背景–>
  3. <shape xmlns:android=“http://schemas.android.com/apk/res/android”>
  4. <solid android:color=“@color/white”/>
  5. <padding android:top=“10px” android:bottom=“10px”/>
  6. <corners android:radius=“50px”/>
  7. <stroke android:width=“1px” android:color=“#f08200”/>
  8. </shape>

3.在xml文件中使用:

%title插图%num

4.注释:

corners ———-圆角

gradient ———-渐变

padding ———-内容离边界距离

size ————大小

solid  ———-填充颜色

stroke ———-描边

注意的是corners的属性bottomLeftRadius为右下角、bottomRightRadius为左下角

shape制作虚线

%title插图%num

没有dashGap属性则为实线

  1. <?xml version=”1.0″ encoding=”utf-8″?>
  2. <shape xmlns:android=“http://schemas.android.com/apk/res/android”
  3. android:shape=“line” >
  4. <stroke
  5. android:dashGap=“3dp”
  6. android:dashWidth=“8dp”
  7. android:width=“1dp”
  8. android:color=“#63a219” />
  9. <size android:height=“1dp” />
  10. </shape>

4.0以上虚线变实线在xml文件中增加:

  1. <TextView
  2. android:layout_width=“match_parent”
  3. android:layout_height=“wrap_content”
  4. android:background=“@drawable/xuxian”
  5. android:layerType=“software” />

 

shape制作渐变

%title插图%num

  1. <?xml version=”1.0″ encoding=”utf-8″?>
  2. <shape xmlns:android=“http://schemas.android.com/apk/res/android” >
  3. <gradient
  4. android:angle=“270.0”
  5. android:endColor=“#ffffff”
  6. android:startColor=“#000000” />
  7. </shape>

 

Android进阶之_实现滑动的7种方式详解

在android开发中,滑动对一个app来说,是非常重要的,流畅的滑动操作,能够给用户带来用好的体验,那么本次就来讲讲android中实现滑动有哪些方式。其实滑动一个View,本质上是移动一个View,改变其当前所属的位置,要实现View的滑动,就必须监听用户触摸的事件,且获取事件传入的坐标值,从而动画的改变位置而实现滑动。

android坐标系
首先要知道android的坐标系与我们平常学习的坐标系是不一样的,在android中是将左上方作为坐标原点,向右为x抽正方向,向下为y抽正方向,像在触摸事件中,getRawX(),getRawY()获取到的就是Android坐标中的坐标.

视图坐标系
android开发中除了上面的这种坐标以外,还有一种坐标,叫视图坐标系,他的原点不在是屏幕左上方,而是以父布局坐上角为坐标原点,像在触摸事件中,getX(),getY()获取到的就是视图坐标中的坐标.

触摸事件–MotionEvent
触摸事件MotionEvent在用户交互中,有非常重要的作用,因此必须要掌握他,我们先来看看Motievent中封装的一些常用的触摸事件常亮:

//单点触摸按下动作
public static final int ACTION_DOWN = 0;
//单点触摸离开动作
public static final int ACTION_UP = 1;
//触摸点移动动作
public static final int ACTION_MOVE = 2;
//触摸动作取消
public static final int ACTION_CANCEL = 3;
//触摸动作超出边界
public static final int ACTION_OUTSIDE = 4;
//多点触摸按下动作
public static final int ACTION_POINTER_DOWN = 5;
//多点触摸离开动作
public static final int ACTION_POINTER_UP = 6;

以上是比较常用的一些触摸事件,通常情况下,我们会在OnTouchEvent(MotionEvent event)方法中通过event.getAction()方法来获取触摸事件的类型,其代码模式如下:

@Override
public boolean onTouchEvent(MotionEvent event)
{
//获取当前输入点的坐标,(视图坐标)
float x = event.getX();
float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//处理输入按下事件
break;
case MotionEvent.ACTION_MOVE:
//处理输入的移动事件
break;
case MotionEvent.ACTION_UP:
//处理输入的离开事件
break;
}
return true; //注意,这里必须返回true,否则只能响应按下事件
}

以上只是一个空壳的架构,遇到的具体的场景,也有可能会新增多其他事件,或是用不到这么多事件等等,要根据实际情况来处理。在介绍如何实现滑动之前先来看看android中给我们提供了那些常用的获取坐标值,相对距离等的方法,主要是有以下两个类别:

View 提供的获取坐标方法

getTop(): 获取到的是View自身的顶边到其父布局顶边的距离

getBottom(): 获取到的是View自身的底边到其父布局顶边的距离

getLeft(): 获取到的是View自身的左边到其父布局左边的距离

getRight(): 获取到的是View自身的右边到其父布局左边的距离

MotionEvent提供的方法

getX(): 获取点击事件距离控件左边的距离,即视图坐标

getY(): 获取点击事件距离控件顶边的距离,即视图坐标

getRawX(): 获取点击事件距离整个屏幕左边的距离,即*对坐标

getRawY(): 获取点击事件距离整个屏幕顶边的距离,即*对坐标

介绍上面一些基本的知识点后,下面我们就来进入正题了,android中实现滑动的其中方法:

实现滑动的7种方法
其实不管是哪种滑动,他们的基本思路是不变的,都是:当触摸View时,系统记下当前的触摸坐标;当手指移动时,系统记下移动后的触摸点坐标,从而获得相对前一个点的偏移量,通过偏移量来修改View的坐标,并不断的更新,重复此动作,即可实现滑动的过程。
首先我们先来定义一个View,并置于LinearLayout中,我们的目的是要实现View随着我们手指的滑动而滑动,布局代码如下:

<?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:orientation=”vertical”>

<com.liaojh.scrolldemo.DragView
android:layout_width=”100dp”
android:layout_height=”100dp”
android:background=”#88ffffff”/>

</LinearLayout>

layout方法
我们知道,在进行View绘制时,会调用layout()方法来设置View的显示位置,而layout方法是通过left,top,right,bottom这四个参数来确定View的位置的,所以我们可以通过修改这四个参数的值,从而修改View的位置。首先我们在onTouchEvent方法中获取触摸点的坐标:

float x = event.getX();
float y = event.getY();

接着在ACTION_DOWN的时候记下触摸点的坐标值:

case MotionEvent.ACTION_DOWN:
//记录按下触摸点的位置
mLastX = x;
mLastY = y;
break;

*后在ACTION_MOVE的时候计算出偏移量,且将偏移量作用到layout方法中:

case MotionEvent.ACTION_MOVE:
//计算偏移量(此次坐标值-上次触摸点坐标值)
int offSetX = (int) (x – mLastX);
int offSetY = (int) (y – mLastY);

//在当前left,right,top.bottom的基础上加上偏移量
layout(getLeft() + offSetX,
getTop() + offSetY,
getRight() + offSetX,
getBottom() + offSetY
);
break;

这样每次在手指移动的时候,都会调用layout方法重新更新布局,从而达到移动的效果,完整代码如下:

package com.liaojh.scrolldemo;

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

/**
* @author LiaoJH
* @DATE 15/11/7
* @VERSION 1.0
* @DESC TODO
*/
public class DragView extends View
{
private float mLastX;
private float mLastY;

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

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

public DragView(Context context, AttributeSet attrs, int defStyleAttr)
{
super(context, attrs, defStyleAttr);
}

@Override
public boolean onTouchEvent(MotionEvent event)
{
//获取当前输入点的坐标,(视图坐标)
float x = event.getX();
float y = event.getY();
switch (event.getAction())
{
case MotionEvent.ACTION_DOWN:
//记录按下触摸点的位置
mLastX = x;
mLastY = y;
break;
case MotionEvent.ACTION_MOVE:
//计算偏移量(此次坐标值-上次触摸点坐标值)
int offSetX = (int) (x – mLastX);
int offSetY = (int) (y – mLastY);

//在当前left,right,top.bottom的基础上加上偏移量
layout(getLeft() + offSetX,
getTop() + offSetY,
getRight() + offSetX,
getBottom() + offSetY
);

break;
}
return true;
}
}

当然也可以使用getRawX(),getRawY()来获取*对坐标,然后使用*对坐标来更新View的位置,但要注意,在每次执行完ACTION_MOVE的逻辑之后,一定要重新设置初始坐标,这样才能准确获取偏移量,否则每次的偏移量都会加上View的父控件到屏幕顶边的距离,从而不是真正的偏移量了。

@Override
public boolean onTouchEvent(MotionEvent event)
{
//获取当前输入点的坐标,(*对坐标)
float rawX = event.getRawX();
float rawY = event.getRawY();
switch (event.getAction())
{
case MotionEvent.ACTION_DOWN:
//记录按下触摸点的位置
mLastX = rawX;
mLastY = rawY;
break;
case MotionEvent.ACTION_MOVE:
//计算偏移量(此次坐标值-上次触摸点坐标值)
int offSetX = (int) (rawX – mLastX);
int offSetY = (int) (rawY – mLastY);

//在当前left,right,top.bottom的基础上加上偏移量
layout(getLeft() + offSetX,
getTop() + offSetY,
getRight() + offSetX,
getBottom() + offSetY
);

//重新设置初始位置的值
mLastX = rawX;
mLastY = rawY;
break;
}
return true;
}

offsetLeftAndRight()与offsetTopAndBottom()
这个方法相当于系统提供了一个对左右,上下移动的API的封装,在计算出偏移量之后,只需使用如下代码设置即可:

offsetLeftAndRight(offSetX);
offsetTopAndBottom(offSetY);

偏移量的计算与上面一致,只是换了layout方法而已。

LayoutParams
LayoutParams保存了一个View的布局参数,因此可以在程序中通过动态的改变布局的位置参数,也可以达到滑动的效果,代码如下:

LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) getLayoutParams();
lp.leftMargin = getLeft() + offSetX;
lp.topMargin = getTop() + offSetY;
setLayoutParams(lp);

使用此方式时需要特别注意:通过getLayoutParams()获取LayoutParams时,需要根据View所在的父布局的类型来设置不同的类型,比如这里,View所在的父布局是LinearLayout,所以可以强转成LinearLayout.LayoutParams。

在通过改变LayoutParams来改变View的位置时,通常改变的是这个View的Margin属性,其实除了LayoutParams之外,我们有时候还可以使用ViewGroup.MarginLayoutParams来改变View的位置,代码如下:

ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) getLayoutParams();
lp.leftMargin = getLeft() + offSetX;
lp.topMargin = getTop() + offSetY;
setLayoutParams(lp);
//使用这种方式的好处就是不用考虑父布局类型

scrollTo与scrollBy
在一个View中,系统提供了scrollTo与scrollBy两种方式来改变一个View的位置,其中scrollTo(x,y)表示移动到一个具体的坐标点(x,y),而scrollBy(x,y)表示移动的增量。与前面几种计算偏移量相同,使用scrollBy来移动View,代码如下:

scrollBy(offSetX,offSetY);

然后我们拖动View,发现View并没有移动,这是为杂呢?其实,方法没有错,view也的确移动了,只是他移动的不是我们想要的东西。scrollTo,scrollBy方法移动的是view的content,即让view的内容移动,如果是在ViewGroup中使用scrollTo,scrollBy方法,那么移动的将是所有的子View,而如果在View中使用的话,就是view的内容,所以我们需要改一下我们之前的代码:

((View)getParent()).scrollBy(offSetX, offSetY);

这次是可以滑动了,但是我们发现,滑动的效果跟我们想象的不一样,完全相反了,这又是为什么呢?其实这是因为android中对于移动参考系选择的不同从而实现这样的效果,而我们想要实现我们滑动的效果,只需将偏移量设置为负值即可,代码如下:

((View) getParent()).scrollBy(-offSetX, -offSetY);

同样的在使用*对坐标时,使用scrollTo也可以达到这样的效果。

scroller
如果让一个View向右移动200的距离,使用上面的方式,大家应该发现了一个问题,就是移动都是瞬间完成的,没有那种慢慢平滑的感觉,所以呢,android就给我们提供了一个类,叫scroller类,使用该类就可以实现像动画一样平滑的效果。

其实它实现的原理跟前面的scrooTo,scrollBy方法实现view的滑动原理类似,它是将ACTION_MOVE移动的一段位移划分成N段小的偏移量,然后再每一个偏移量里面使用scrollBy方法来实现view的瞬间移动,这样在整体的效果上就实现了平滑的效果,说白了就是利用人眼的视觉暂留特性。

下面我们就来实现这么一个例子,移动view到某个位置,松开手指,view都吸附到左边位置,一般来说,使用Scroller实现滑动,需经过以下几个步骤:

初始化Scroller

//初始化Scroller,使用默认的滑动时长与插值器
mScroller = new Scroller(context);

重写computeScroll()方法

该方法是Scroller类的核心,系统会在绘制View的时候调用draw()方法中调用该方法,这个方法本质上是使用scrollTo方法,通过Scroller类可以获取到当前的滚动值,这样我们就可以实现平滑一定的效果了,一般模板代码如下:

@Override
public void computeScroll()
{
super.computeScroll();
//判断Scroller是否执行完成
if (mScroller.computeScrollOffset()) {
((View)getParent()).scrollTo(
mScroller.getCurrX(),
mScroller.getCurrY()
);
//调用invalidate()computeScroll()方法
invalidate();
}
}

Scroller类提供中的方法:

computeScrollOffset(): 判断是否完成了真个滑动

getCurrX(): 获取在x抽方向上当前滑动的距离

getCurrY(): 获取在y抽方向上当前滑动的距离

startScroll开启滑动

*后在需要使用平滑移动的事件中,使用Scroller类的startScroll()方法来开启滑动过程,startScroller()方法有两个重载的方法:

– public void startScroll(int startX, int startY, int dx, int dy)

– public void startScroll(int startX, int startY, int dx, int dy, int duration)

可以看到他们的区别只是多了duration这个参数,而这个是滑动的时长,如果没有使用默认时长,默认是250毫秒,而其他四个坐标则表示起始坐标与偏移量,可以通过getScrollX(),getScrollY()来获取父视图中content所滑动到的点的距离,不过要注意这个值的正负,它与scrollBy,scrollTo中说的是一样的。经过上面这三步,我们就可以实现Scroller的平滑一定了。

继续上面的例子,我们可以在onTouchEvent方法中监听ACTION_UP事件动作,调用startScroll方法,其代码如下:

case MotionEvent.ACTION_UP:
//第三步
//当手指离开时,执行滑动过程
ViewGroup viewGroup = (ViewGroup) getParent();
mScroller.startScroll(
viewGroup.getScrollX(),
viewGroup.getScrollY(),
-viewGroup.getScrollX(),
0,
800
);
//刷新布局,从而调用computeScroll方法
invalidate();
break;

属相动画
使用属性动画同样可以控制一个View的滑动,下面使用属相动画来实现上边的效果(关于属相动画,请关注其他的博文),代码如下:

case MotionEvent.ACTION_UP:
ViewGroup viewGroup = (ViewGroup) getParent();
//属性动画执行滑动
ObjectAnimator.ofFloat(this, “translationX”, viewGroup.getScrollX()).setDuration(500)
.start();
break;

ViewDragHelper
一看这个类的名字,我们就知道他是与拖拽有关的,猜的没错,通过这个类我们基本可以实现各种不同的滑动,拖放效果,他是非常强大的一个类,但是它也是*为复杂的,但是不要慌,只要你不断的练习,就可以数量的掌握它的使用技巧。下面我们使用这个类来时实现类似于QQ滑动侧边栏的效果,相信广大朋友们多与这个现象是很熟悉的吧。

先来看看使用的步骤是如何的:

初始化ViewDragHelper

ViewDragHelper这个类通常是定义在一个ViewGroup的内部,并通过静态方法进行初始化,代码如下:

//初始化ViewDragHelper
viewDragHelper = ViewDragHelper.create(this,callback);

它的*个参数是要监听的View,通常是一个ViewGroup,第二个参数是一个Callback回调,它是整个ViewDragHelper的逻辑核心,后面进行具体介绍。

拦截事件

重写拦截事件onInterceptTouchEvent与onTouchEvent方法,将事件传递交给ViewDragHelper进行处理,代码如下:

@Override
public boolean onInterceptTouchEvent(MotionEvent ev)
{
//2. 将事件交给ViewDragHelper
return viewDragHelper.shouldInterceptTouchEvent(ev);
}

@Override
public boolean onTouchEvent(MotionEvent event)
{
//2. 将触摸事件传递给ViewDragHelper,不可少
viewDragHelper.processTouchEvent(event);
return true;
}

处理computeScroll()方法

前面我们在使用Scroller类的时候,重写过该方法,在这里我们也需要重写该方法,因为ViewDragHelper内部也是使用Scroller类来实现的,代码如下:

//3. 重写computeScroll
@Override
public void computeScroll()
{
//持续平滑动画 (高频率调用)
if (viewDragHelper.continueSettling(true))
// 如果返回true, 动画还需要继续执行
ViewCompat.postInvalidateOnAnimation(this);
}

处理回调Callback

通过如下代码创建一个Callback:

private ViewDragHelper.Callback callback = new ViewDragHelper.Callback()
{
@Override
//此方法中可以指定在创建ViewDragHelper时,参数ViewParent中的那些子View可以被移动
//根据返回结果决定当前child是否可以拖拽
// child 当前被拖拽的View
// pointerId 区分多点触摸的id
public boolean tryCaptureView(View child, int pointerId)
{
//如果当前触摸的view是mMainView时开始检测
return mMainView == child;
}

@Override
//水平方向的滑动
// 根据建议值 修正将要移动到的(横向)位置 (重要)
// 此时没有发生真正的移动
public int clampViewPositionHorizontal(View child, int left, int dx)
{
//返回要滑动的距离,默认返回0,既不滑动
//参数参考clampViewPositionVertical
f (child == mMainView)
{
if (left > 300)
{
left = 300;
}
if (left < 0)
{
left = 0;
}
}
return left;
}

@Override
//垂直方向的滑动
// 根据建议值 修正将要移动到的(纵向)位置 (重要)
// 此时没有发生真正的移动
public int clampViewPositionVertical(View child, int top, int dy)
{
//top : 垂直向上child滑动的距离,
//dy: 表示比较前一次的增量,通常只需返回top即可,如果需要精确计算padding等属性的话,就需要对left进行处理
return super.clampViewPositionVertical(child, top, dy); //0
}
};

到这里就可以拖拽mMainView移动了。

下面我们继续来优化这个代码,还记得之前我们使用Scroller时,当手指离开屏幕后,子view会吸附到左边位置,当时我们监听ACTION_UP,然后调用startScroll来实现的,这里我们使用ViewDragHelper来实现。

在ViewDragHelper.Callback中,系统提供了这么一个方法—onViewReleased(),我们可以通过重写这个方法,来实现之前的操作,当然这个方法内部也是通过Scroller来实现的,这也是为什么我们要重写computeScroll方法的原因,实现代码如下:

@Override
//拖动结束时调用
public void onViewReleased(View releasedChild, float xvel, float yvel)
{
if (mMainView.getLeft() < 150)
{
// 触发一个平滑动画,关闭菜单,相当于Scroll的startScroll方法
if (viewDragHelper.smoothSlideViewTo(mMainView, 0, 0))
{
// 返回true代表还没有移动到指定位置, 需要刷新界面.
// 参数传this(child所在的ViewGroup)
ViewCompat.postInvalidateOnAnimation(DragLayout.this);
}
}
else
{
//打开菜单
if (viewDragHelper.smoothSlideViewTo(mMainView, 300, 0)) ;
{
ViewCompat.postInvalidateOnAnimation(DragLayout.this);
}
}
super.onViewReleased(releasedChild, xvel, yvel);
}

 

当滑动的距离小于150时,mMainView回到原来的位置,当大于150时,滑动到300的位置,相当于打开了mMenuView,而且滑动的时候是很平滑的。此外还有一些方法:

@Override
public void onViewCaptured(View capturedChild, int activePointerId)
{
// 当capturedChild被捕获时,调用.
super.onViewCaptured(capturedChild, activePointerId);
}

@Override
public int getViewHorizontalDragRange(View child)
{
// 返回拖拽的范围, 不对拖拽进行真正的限制. 仅仅决定了动画执行速度
return 300;
}

@Override
//当View位置改变的时候, 处理要做的事情 (更新状态, 伴随动画, 重绘界面)
// 此时,View已经发生了位置的改变
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy)
{
// changedView 改变位置的View
// left 新的左边值
// dx 水平方向变化量
super.onViewPositionChanged(changedView, left, top, dx, dy);
}

说明:里面还有很多关于处理各种事件方法的定义,如:

onViewCaptured():用户触摸到view后回调

onViewDragStateChanged(state):这个事件在拖拽状态改变时回调,比如:idle,dragging等状态

onViewPositionChanged():这个是在位置改变的时候回调,常用于滑动时伴随动画的实现效果等

对于里面的方法,如果不知道什么意思,则可以打印log,看看参数的意思。

总结
这里介绍的就是android实现滑动的七种方法,至于使用哪一种好,就要结合具体的项目需求场景了,毕竟硬生生的实现这个效果,而不管用户的使用体验式不切实际的,这里面个人觉得比较重要的是Scroller类的使用。属性动画以及ViewDragHelper类,特别是*后一个,也是*难*复杂的,但也是甩的*多的。

终于写完了,好累的赶脚~~~
————————————————

Android开发:使用CardView实现卡片式设计

开头引用一段官网的介绍

A FrameLayout with a rounded corner background and shadow.

CardView uses elevation property on Lollipop for shadows and falls back to a custom emulated shadow implementation on older platforms.

Due to expensive nature of rounded corner clipping, on platforms before Lollipop, CardView does not clip its children that intersect with rounded corners. Instead, it adds padding to avoid such intersection (See setPreventCornerOverlap(boolean) to change this behavior).

Before Lollipop, CardView adds padding to its content and draws shadows to that area. This padding amount is equal to maxCardElevation + (1 - cos45) * cornerRadius on the sides and maxCardElevation * 1.5 + (1 - cos45) * cornerRadius on top and bottom.

Since padding is used to offset content for shadows, you cannot set padding on CardView. Instead, you can use content padding attributes in XML or setContentPadding(int, int, int, int) in code to set the padding between the edges of the CardView and children of CardView.

Note that, if you specify exact dimensions for the CardView, because of the shadows, its content area will be different between platforms before Lollipop and after Lollipop. By using api version specific resource values, you can avoid these changes. Alternatively, If you want CardView to add inner padding on platforms Lollipop and after as well, you can callsetUseCompatPadding(boolean) and pass true.

To change CardView’s elevation in a backward compatible way, use setCardElevation(float). CardView will use elevation API on Lollipop and before Lollipop, it will change the shadow size. To avoid moving the View while shadow size is changing, shadow size is clamped by getMaxCardElevation(). If you want to change elevation dynamically, you should call setMaxCardElevation(float)when CardView is initialized.

简单的效果图:

%title插图%num

简略版介绍

Apps often need to display data in similarly styled containers. These containers are often used in lists to hold each item’s information. The system provides the CardView API as an easy way for you show information inside cards that have a consistent look across the platform. These cards have a default elevation above their containing view group, so the system draws shadows below them. Cards provide an easy way to contain a group of views while providing a consistent style for the container.

开始使用

  1. Add the dependencies

The CardView widget is part of the v7 Support Libraries. To use it in your project, add the following dependency to your app module’s build.gradle file:

Cardview 是Android 5.0 才引入的,所以需要导入这个依赖包。

  1. dependencies {
  2. implementation ‘com.android.support:cardview-v7:27.1.1’
  3. }
  1. Create Cards

In order to use the CardView you need to add it to your layout file. Use it as a view group to contain other views. In this example, the CardView contains a single TextView to display some information to the user.

XML代码就是前面分析的那个,这里不再重复了。

The cards are drawn to the screen with a default elevation, which causes the system to draw a shadow underneath them. You can provide a custom elevation for a card with the card_view:cardElevation attribute. This will draw a more pronounced shadow with a larger elevation, and a lower elevation will result in a lighter shadow. CardView uses real elevation and dynamic shadows on Android 5.0 (API level 21) and above and falls back to a programmatic shadow implementation on earlier versions.

Use these properties to customize the appearance of the CardView widget:

  • To set the corner radius in your layouts, use the card_view:cardCornerRadius attribute.
  • To set the corner radius in your code, use the CardView.setRadius method.
  • To set the background color of a card, use the card_view:cardBackgroundColor attribute.

For more information, see the API reference for CardView.

关于Cards的设计规范可以参考官网介绍:https://material.google.com/components/cards.html#

为了更好地实现这种 Cards UI 的设计,Google在v7包中引进了一种全新的控件:CardVew,本文将从开发的角度介绍CardView的一些常见使用细节。

Google用一句话介绍了CardView:一个带圆角和阴影背景的FrameLayout。CardView在Android Lollipop(API 21)及以上版本的系统中适配较好,本文我们以一个具体的例子来学习CardView的基本使用和注意事项,效果图如下:

%title插图%num

这是一个list列表,列表中的item使用了卡片式设计,主要利用CardView控件实现,下面来分析一下布局文件的核心代码。

  1. <LinearLayout xmlns:android=“http://schemas.android.com/apk/res/android”
  2. xmlns:tools=“http://schemas.android.com/tools”
  3. xmlns:card_view=“http://schemas.android.com/apk/res-auto”
  4. >
  5. <!– A CardView that contains a TextView –>
  6. <android.support.v7.widget.CardView
  7. xmlns:card_view=“http://schemas.android.com/apk/res-auto”
  8. android:id=“@+id/card_view”
  9. android:layout_gravity=“center”
  10. android:layout_width=“200dp”
  11. android:layout_height=“200dp”
  12. card_view:cardCornerRadius=“4dp”>
  13. <TextView
  14. android:id=“@+id/info_text”
  15. android:layout_width=“match_parent”
  16. android:layout_height=“match_parent” />
  17. </android.support.v7.widget.CardView>
  18. </LinearLayout>

可以看出,核心部分在于CardView的属性使用,下面我们针对几个特殊的属性逐一分析,深化了解。

关于Z轴

Android5.0 引入了Z轴的概念,可以让组件呈现3D效果.

%title插图%num

排版技巧

前面我们说过,CardView从本质上属于FrameLayout,而CardView通常包含了较多的内容元素,为了方便地排版布局中的各个元素,一般借助于其他基本布局容器,比如这里我们使用了一个RelativeLayout作为CardView的唯一Child。

Shadow Padding

在Android Lollipop之前的系统,CardView会自动添加一些额外的padding空间来绘制阴影部分,这也导致了以Lollipop为分界线的不同系统上CardView的尺寸大小不同。为了解决这个问题,有两种方法:*种,使用不同API版本的dimension资源适配(也就是借助values和values-21文件夹中不同的dimens.xml文件);第二种,就是使用cardUseCompatPadding属性,设置为true(默认值为false),让CardView在不同系统中使用相同的padding值。

圆角覆盖

这也是一个解决系统兼容的问题。在pre-Lollipop平台(API 21版本之前)上,CardView不会裁剪内容元素以满足圆角需求,而是使用添加padding的替代方案,从而使内容元素不会覆盖CardView的圆角。而控制这个行为的属性就是cardPreventCornerOverlap,默认值为true。在本例中我们设置了该属性为false。这里我们看一下,在pre-Lollipop平台中,不同cardPreventCornerOverlap值的效果对比图(左false,右true):

%title插图%num

显然,默认值下自动添加padding的方式不可取,所以需要设置该属性值为false。需要注意的一点是,该属性的设置在Lollipop及以上版本的系统中没有任何影响,除非cardUseCompatPadding的值为true。

Ripple效果

Cards一般都是可点击的,为此我们使用了foreground属性并使用系统的selectableItemBackground值,同时设置clickable为true(如果在java代码中使用了cardView.setOnClickListener,就可以不用写clickable属性了),从而达到在Lollipop及以上版本系统中实现点击时的涟漪效果(Ripple)。在pre-Lollipop版本中,则是一个普通的点击变暗的效果。

lift-on-touch

根据官网Material motion部分对交互动作规范的指导,Cards、Button等视图应该有一个触摸抬起(lift-on-touch)的交互效果,也就是在三维立体空间上的Z轴发生位移,从而产生一个阴影加深的效果,与Ripple效果共同使用,官网给了一个很好的示例图:

%title插图%num

 

在实现这个效果也很简单,可以在res/drawable目录下建立一个lift_on_touch.xml文件,内容如下:

  1. <?xml version=”1.0″ encoding=”utf-8″?>
  2. <!– animate the translationZ property of a view when pressed –>
  3. <selector xmlns:android=“http://schemas.android.com/apk/res/android”>
  4. <item
  5. android:state_enabled=“true”
  6. android:state_pressed=“true”>
  7. <set>
  8. <objectAnimator
  9. android:duration=“@android:integer/config_shortAnimTime”
  10. android:propertyName=“translationZ”
  11. android:valueTo=“6dp”
  12. android:valueType=“floatType”/>
  13. </set>
  14. </item>
  15. <item>
  16. <set>
  17. <objectAnimator
  18. android:duration=“@android:integer/config_shortAnimTime”
  19. android:propertyName=“translationZ”
  20. android:valueTo=“0”
  21. android:valueType=“floatType”/>
  22. </set>
  23. </item>
  24. </selector>

即通过属性动画动态改变translationZ值,沿着Z轴,从0dp到6dp变化。这里的6dp值也是有出处的,参考Google I/O 2014 app和Assign Elevation to Your Views。然后将其赋值给android:stateListAnimator属性即可。由于stateListAnimator属性只适用于Lollipop及以上版本,为了隐藏xml中的版本警告,可以指定tools:targetApi="lollipop"

关于这个功能,需要补充说明一点。这里的lift_on_touch.xml,严格意义上来讲,属于anim资源,同时适用于API 21及以上版本,所以按道理上来讲应该将其放置在res/anim-v21目录下,然后使用@anim/lift_on_touch赋值给stateListAnimator属性,而不是例子中的@drawable/lift_on_touch方法。但是放置在res/anim-v21目录下会产生一个“错误”提示:

<selector style="box-sizing: border-box;">XML file should be in either “animator” or “drawable”,not “anim”</selector>

虽然这个“错误”不影响编译运行,但是对于追求完美主义的程序员们来说还是碍眼,所以本例中我选择将其放在了res/drawable目录下,大家可以自行斟酌使用。

关于对lift-on-touch效果的理解,YouToBe网站有个视频解说,感兴趣的话可以参看看,地址如下:
DesignBytes: Paper and Ink: The Materials that Matter

总结说明

CardView还有一些其他属性可供使用,比如cardElevation设置阴影大小,contentPadding代替普通android:padding属性等,比较基础,本文就不一一介绍了,大家可以在官网上参考学习。从上面的介绍可以看出,在使用CardView时基本上都会用到一些标准配置的属性,我们可以借助style属性,将其封装到styles.xml文件中,统一管理,比如:

  1. <style name=“AppCardView” parent=“@style/CardView.Light”>
  2. <item name=“cardPreventCornerOverlap”>false</item>
  3. <item name=“cardUseCompatPadding”>true</item>
  4. <item name=“android:foreground”>?attr/selectableItemBackground</item>
  5. <item name=“android:stateListAnimator” tools:targetApi=“lollipop”>@anim/lift_up</item>
  6. ……
  7. </style>

 

友情链接: SITEMAP | 旋风加速器官网 | 旋风软件中心 | textarea | 黑洞加速器 | jiaohess | 老王加速器 | 烧饼哥加速器 | 小蓝鸟 | tiktok加速器 | 旋风加速度器 | 旋风加速 | quickq加速器 | 飞驰加速器 | 飞鸟加速器 | 狗急加速器 | hammer加速器 | trafficace | 原子加速器 | 葫芦加速器 | 麦旋风 | 油管加速器 | anycastly | INS加速器 | INS加速器免费版 | 免费vqn加速外网 | 旋风加速器 | 快橙加速器 | 啊哈加速器 | 迷雾通 | 优途加速器 | 海外播 | 坚果加速器 | 海外vqn加速 | 蘑菇加速器 | 毛豆加速器 | 接码平台 | 接码S | 西柚加速器 | 快柠檬加速器 | 黑洞加速 | falemon | 快橙加速器 | anycast加速器 | ibaidu | moneytreeblog | 坚果加速器 | 派币加速器 | 飞鸟加速器 | 毛豆APP | PIKPAK | 安卓vqn免费 | 一元机场加速器 | 一元机场 | 老王加速器 | 黑洞加速器 | 白石山 | 小牛加速器 | 黑洞加速 | 迷雾通官网 | 迷雾通 | 迷雾通加速器 | 十大免费加速神器 | 猎豹加速器 | 蚂蚁加速器 | 坚果加速器 | 黑洞加速 | 银河加速器 | 猎豹加速器 | 海鸥加速器 | 芒果加速器 | 小牛加速器 | 极光加速器 | 黑洞加速 | movabletype中文网 | 猎豹加速器官网 | 烧饼哥加速器官网 | 旋风加速器度器 | 哔咔漫画 | PicACG | 雷霆加速