导图社区 高级Android编程开发知识框架培训教案
高级Android编程开发知识框架培训教案,导图内容简洁、逻辑清晰、重点突出,希望对大家有所帮助~
编辑于2022-11-02 10:19:14 广东高级Android编程开发知识框架培训教案
1
第36天
Retrofit
RestFul
RESTful(Representational State Transfer)架构,就是目前最流行的一种互联网软件架构。一种软件架构风格,设计风格而不是标准,只是提供了一组设计原则和约束条件。它主要用于客户端和服务器交互类的软件。基于这个风格设计的软件可以更简洁,结构清晰、符合标准、易于理解、扩展方便,更易于实现缓存等机制,所以正得到越来越多网站的采用。
概要
Retrofit 将你的Http请求转化成一个接口,和Java领域的ORM(Object Relational Mapping)概念类似, ORM把结构化数据转换为Java对象,而Retrofit 把REST API返回的数据转化为Java对象方便操作。同时还封装了网络代码的调用
特点
性能最好,处理最快
使用REST API时非常方便;
传输层默认就使用OkHttp;
支持NIO;
拥有出色的API文档和社区支持
速度上比volley更快;
如果你的应用程序中集成了OKHttp,Retrofit默认会使用OKHttp处理其他网络层请求。
默认使用Gson
使用
HTTP请求方法
url类
所有请求都需要一个请求方法注解并以相对URL路径作为参数。内建了5个注解:GET, POST, PUT, DELETE, and HEAD
@get
向特定的资源发出请求。注意:GET方法不应当被用于产生“副作用”的操作中。
完整URL:http://api.zw.com:8080/api/v1.0/account/list/1/10
@GET("account/list")
@POST
向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST请求可能会导致新的资源的建立和/或已有资源的修改。
完整URL:http://api.zw.com:8080/api/v1.0/account/login
@POST("account/login")
@PUT
向指定资源位置上传其最新内容。
完整URL:http://api.zw.com:8080/api/v1.0/account/update
@PUT("account/update")
@DELETE
请求服务器删除Request-URI所标识的资源。
完整URL:http://api.zw.com:8080/api/v1.0/account/logout
@PUT("account/logout")
@HEAD
向服务器索要与GET请求相一致的响应,只不过响应体将不会被返回。这一方法可以在不必传输整个响应内容的情况下,就可以获取包含在响应消息头中的元信息。 @Headers("Cache-Control: max-age=60")
静态header
分为单个键值对
@Headers("Cache-Control: max-age=640000") @GET Call<ResponseBody> test();
多个键值对
@Headers({ "Accept: application/json", "User-Agent: zhangwei" }) @GET("users/{username}") Call<User> getUser(@Path("username") String username);
动态header
局部动态header
@GET("user") Call<User> getUser(@Header("Authorization") String authorization)
全局动态header
@GET("user") Call<User> getUser(@HeaderMap Map<String, String> headerMap)
全局动态header(适用于项目中的header规则一致的情况)
@Http
标记类
表单请求
@FormUrlEncoded
表示请求体是一个Form表单
@Multipart
表示请求体是一个支持文件上传的Form表单
Streaming(流)
表示响应提的数据以流的形式返回,如果没有使用该注解默认会把数据全部载入内存之后通过流获取,所以如果你返回数据比较大的就可以使用该注解,例如文件下载
参数声明
@Path动态改变URL
完整的请求URL:http://api.zw.com:8080/api/v1.0/account/list/1/10
@GET("account/list/{page}/{size}") Call<List<User>> getUserList(@Path("page") int page, @Path("size") int size);
注意:
一个完整的URL包括模式(或称协议)、服务器名称(或IP地址)、路径 http://api.zw.com:8080/api/v1.0/account/orders?page=1&size=20 只能替换http://api.zw.com:8080/api/v1.0/account/orders不能替换page=1&size=20
@Query查询
完整的请求URL:http://api.zw.com:8080/api/v1.0/account/orders?page=1&size=20
@GET("account/orders") Call<BaseEntity<List<Order>>> getOrderList(@Query("page") int page, @Query("size") int size);
@QueryMap
完整的请求URL:http://api.zw.com:8080/api/v1.0/account/orders?page=1&size=20
@GET("account/orders") Call<BaseEntity<List<Order>>> getOrderList(@QueryMap Map(String ,String)params);
@Multipart和@Part一起使用
也可以通过@Multipart注解方法来发送Mutipart请求。每个部分需要使用@Part来注解
文字和单文件同时上传
@Multipart @POST(ApiConstans.RequestUrl.URL_LOGIN_UPDATE_POST) Call<BaseEntity<User>> update(@Part("uid") RequestBody uid, @Part MultipartBody.Part file);
private void update() { File uploadFile = new File(pathList.get(0)); //创建请求的文件 RequestBody requestFile = RequestBody.create(MEDIA_TYPE_MULTIPART, uploadFile); //创建part MultipartBody.Part body = MultipartBody.Part.createFormData("file", uploadFile.getName(), requestFile); RequestBody uid = RequestBody.create(MEDIA_TYPE_MULTIPART, "1"); apiService.update(uid, body).enqueue(new Callback<BaseEntity<User>>() { @Override public void onResponse(Call<BaseEntity<User>> call, Response<BaseEntity<User>> response) { } @Override public void onFailure(Call<BaseEntity<User>> call, Throwable t) { } }); }
多个文件上传
@Multipart @POST(ApiConstans.RequestUrl.URL_UPDATE_FILES_POST) Call<BaseEntity<User>> uploadFiles(@PartMap Map<String, RequestBody> files);
/** * 多文件上传 */ private void uploadFiles() { Map<String, RequestBody> files = new HashMap<>(); for (int i = 0; i < pathList.size(); i++) { RequestBody requestBody = RequestBody.create(MEDIA_TYPE_PNG, new File(pathList.get(i))); files.put("file" + i + "\"; filename=\"" + new File(pathList.get(i)).getName() + "\"", requestBody); } apiService.uploadFiles(files).enqueue(new Callback<BaseEntity<User>>() { @Override public void onResponse(Call<BaseEntity<User>> call, Response<BaseEntity<User>> response) { Log.e("", ""); } @Override public void onFailure(Call<BaseEntity<User>> call, Throwable t) { Log.e("", ""); } }); }
@Body请求体
完整的请求URL:http://api.zw.com:8080/api/v1.0/account/register
@PUT(ApiConstans.RequestUrl.URL_REGISTER_POST) Call<BaseEntity<Boolean>> register(@Body User user);
标注的参数表示指定该参数作为Http请求的请求体,该参数(对象)将会被Retroofit实例指定的转换器转换,如果没有添加转换器,默认使用RequestBody。
@FormURLEncoded和@Field一起使用
方法也可以通过声明来发送form-encoded和multipart类型的数据。可以通过@FormUrlEncoded注解方法来发送form-encoded的数据。每个键值对需要用@Filed来注解键名,随后的对象需要提供值。
@FormUrlEncoded @POST("user/edit") Call<User> updateUser(@Field("firstName") String first, @Field("lastName") String last);
注解
Method
@GET 向特定的资源发出请求。注意:GET方法不应当被用于产生“副作用”的操作中。
@POST 向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST请求可能会导致新的资源的建立和/或已有资源的修改。
@PUT 向指定资源位置上传其最新内容。
@DELETE 请求服务器删除Request-URI所标识的资源。
Parameter
@Body注解指定一个对象作为Http请求的请求体
@Path
@Query 表示查询参数,很简单,不多说
@QueryMap 表示组合在一起的查询参数作为一个Map集合对象
@FormUrlEncoded发送表单数据,使用@Field注解和参数来指定每个表单项的Key,Value为参数的值。表单项过多时可以使用Map进行组合
First name: Last name:
@Multipart可以进行文件上传,使用@Part,@MapPart指定文件路径及类型
@HEAD 向服务器索要与GET请求相一致的响应,只不过响应体将不会被返回。这一方法可以在不必传输整个响应内容的情况下,就可以获取包含在响应消息头中的元信息。
Converter
在默认情况下Retrofit只支持将HTTP的响应体转换换为ResponseBody,而Converter就是Retrofit为我们提供用于将ResponseBody转换为我们想要的类型
官方转化器
如果需要使用自动将JSON格式数据封装为实体对象的功能(Gson解析),需要添加如下依赖: compile 'com.squareup.retrofit2:converter-gson:2.+'
如果需要使用自动将JSON格式数据封装为实体对象的功能(fastJson),需要添加如下依赖: compile 'org.ligboy.retrofit2:converter-fastjson-android:2.1.0'
如果需要打印Retrofit网络请求和返回结果等日志信息,需要添加如下依赖 compile 'com.squareup.okhttp3:logging-interceptor:3.1.2'
如果需要支持RxJava,为Retrofit提供RxJava的适配器,需要添加如下依赖 compile 'com.squareup.retrofit2:adapter-rxjava:2.+' compile 'io.reactivex:rxandroid:1.1.0'
自定义转化器
了解Converter接口及其作用
源码
public interface Converter<F, T> { // 实现从 F(rom) 到 T(o)的转换 T convert(F value) throws IOException; // 用于向Retrofit提供相应Converter的工厂 abstract class Factory { // 这里创建从ResponseBody其它类型的Converter,如果不能处理返回null // 主要用于对响应体的处理 public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) { return null; } // 在这里创建 从自定类型到ResponseBody 的Converter,不能处理就返回null, // 主要用于对Part、PartMap、Body注解的处理 public Converter<?, RequestBody> requestBodyConverter(Type type, Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) { return null; } // 这里用于对Field、FieldMap、Header、Path、Query、QueryMap注解的处理 // Retrfofit对于上面的几个注解默认使用的是调用toString方法 public Converter<?, String> stringConverter(Type type, Annotation[] annotations, Retrofit retrofit) { return null; } } }
CallAdapter
CallAdapter接口定义及各方法的作用
public interface CallAdapter<T> { // 直正数据的类型 如Call<T> 中的 T // 这个 T 会作为Converter.Factory.responseBodyConverter 的第一个参数 // 可以参照上面的自定义Converter Type responseType(); <R> T adapt(Call<R> call); // 用于向Retrofit提供CallAdapter的工厂类 abstract class Factory { // 在这个方法中判断是否是我们支持的类型,returnType 即Call<Requestbody>和`Observable<Requestbody>` // RxJavaCallAdapterFactory 就是判断returnType是不是Observable<?> 类型 // 不支持时返回null public abstract CallAdapter<?> get(Type returnType, Annotation[] annotations, Retrofit retrofit); // 用于获取泛型的参数 如 Call<Requestbody> 中 Requestbody protected static Type getParameterUpperBound(int index, ParameterizedType type) { return Utils.getParameterUpperBound(index, type); } // 用于获取泛型的原始类型 如 Call<Requestbody> 中的 Call // 上面的get方法需要使用该方法。 protected static Class<?> getRawType(Type type) { return Utils.getRawType(type); } } }
官方提供
com.squareup.retrofit2:adapter-guava:2.0.2
Java8 com.squareup.retrofit2:adapter-java8:2.0.2
rxjava com.squareup.retrofit2:adapter-rxjava:2.0.2
其他
其他方法
allbackExecutor(Executor) 指定Call.enqueue时使用的Executor,所以该设置只对返回值为Call的方法有效
callFactory(Factory) 设置一个自定义的okhttp3.Call.Factory,那什么是Factory呢?OkHttpClient就实现了okhttp3.Call.Factory接口,下面的client(OkHttpClient)最终也是调用了该方法,也就是说两者不能共用
client(OkHttpClient) 设置自定义的OkHttpClient,以前的Retrofit版本中不同的Retrofit对象共用同OkHttpClient,在2.0各对象各自持有不同的OkHttpClient实例,所以当你需要共用OkHttpClient或需要自定义时则可以使用该方法,如:处理Cookie、使用stetho 调式等
validateEagerly(boolean) 是否在调用create(Class)时检测接口定义是否正确,而不是在调用方法才检测,适合在开发、测试时使用
compile 'com.squareup.retrofit2:retrofit:2.0.1'
https://github.com/square/retrofit
http://api.app.miaomiaotv.cn/doc.html#z01
http://www.jianshu.com/p/308f3c54abdd(详解)
封装库https://github.com/NeglectedByBoss/RetrofitClient
http Header
HTTP的头域包括通用头、请求头、响应头和实体头四个部分。每个头域由一个域名,冒号(:)和域值三部分组成(说白了就是键值对)。
通用头
是客户端和服务器都可以使用的头部,可以在客户端、服务器和其他应用程序之间提供一些非常有用的通用功能,如Date头部
Cache-Control
指定请求和响应遵循的缓存机制
可选值
no-cache:指示请求或响应消息不能缓存,实际上是可以存储在本地缓存区中的,只是在与原始服务器进行新鲜度验证之前,缓存不能将其提供给客户端使用。
no-store:缓存应该尽快从存储器中删除文档的所有痕迹,因为其中可能会包含敏感信息。
max-age:在此值内的时间里就不会重新访问服务器
min-fresh:至少在未来规定秒内文档要保持新鲜,接受其新鲜生命期大于其当前 Age 跟 min-fresh 值之和的缓存对象。
max-stale:指示客户端可以接收过期响应消息,如果指定max-stale消息的值,那么客户端可以接收过期但在指定值之内的响应消息。
only-if-cached:只有当缓存中有副本存在时,客户端才会获得一份副本。
Public:指示响应可被任何缓存区缓存,可以用缓存内容回应任何用户。
Private:指示对于单个用户的整个或部分响应消息,不能被共享缓存处理,只能用缓存内容回应先前请求该内容的那个用户。
Pragma
Pragma头域用来包含实现特定的指令,最常用的是Pragma:no-cache。在HTTP/1.1协议中,它的含义和Cache-Control:no-cache相同
Connection
表示是否需要持久连接
Close:告诉WEB服务器或者代理服务器,在完成本次请求的响应后,断开连接,不要等待本次连接的后续请求了。
Keepalive:告诉WEB服务器或者代理服务器,在完成本次请求的响应后,保持连接,等待本次连接的后续请求。
Keep-Alive:如果浏览器请求保持连接,则该头部表明希望 WEB 服务器保持连接多长时间(秒),如Keep-Alive:300。
Date
Date头域表示消息发送的时间,服务器响应中要包含这个头部,因为缓存在评估响应的新鲜度时要用到,其时间的描述格式由RFC822定义。例如,Date:Mon, 31 Dec 2001 04:25:57 GMT。Date描述的时间表示世界标准时,换算成本地时间,需要知道用户所在的时区
Transfer-Encoding
WEB 服务器表明自己对本响应消息体(不是消息体里面的对象)作了怎样的编码,比如是否分块(chunked),例如:Transfer-Encoding: chunked
HTTP请求(HTTP Request)
请求头用于说明是谁或什么在发送请求、请求源于何处,或者客户端的喜好及能力。服务器可以根据请求头部给出的客户端信息,试着为客户端提供更好的响应
1.请求行
2.HTTP头
Accept:浏览器可接受的MIME类型。
Accept - Charset:浏览器可接受的字符集。
Accept - Encoding:浏览器能够进行解码的数据编码方式,比如gzip。Servlet能够向支持gzip的浏览器返回经gzip编码的HTML页面。许多情形下这可以减少5到10倍的下载时间。
Accept - Language:浏览器所希望的语言种类,当服务器能够提供一种以上的语言版本时要用到。
Authorization:授权信息,通常出现在对服务器发送的WWW - Authenticate头的应答中。
Connection:表示是否需要持久连接。如果Servlet看到这里的值为“Keep - Alive”,或者看到请求使用的是HTTP 1.1(HTTP 1.1默认进行持久连接),它就可以利用持久连接的优点,当页面包含多个元素时(例如Applet,图片),显著地减少下载所需要的时间。要实现这一点,Servlet需要在应答中发送一个Content - Length头,最简单的实现方法是:先把内容写入ByteArrayOutputStream,然后在正式写出内容之前计算它的大小。
Content - Length:表示请求消息正文的长度。
Cookie:这是最重要的请求头信息之一
From:请求发送者的email地址,由一些特殊的Web客户程序使用,浏览器不会用到它。
Host:初始URL中的主机和端口。
If - Modified - Since:只有当所请求的内容在指定的日期之后又经过修改才返回它,否则返回304“Not Modified”应答。
Pragma:指定“no - cache”值表示服务器必须返回一个刷新后的文档,即使它是代理服务器而且已经有了页面的本地拷贝。
Referer:包含一个URL,用户从该URL代表的页面出发访问当前请求的页面。
User - Agent:浏览器类型,如果Servlet返回的内容与浏览器类型有关则该值非常有用。
UA - Pixels,UA - Color,UA - OS,UA - CPU:由某些版本的IE浏览器所发送的非标准的请求头,表示屏幕大小、颜色深度、操作系统和CPU类型。
3.内容
HTTP响应(HTTP Response)
响应头:便于客户端提供信息,比如,客服端在与哪种类型的服务器进行交互,如Server头部。
1.状态行
2.HTTP头
3.返回内容
实体头
指的是用于应对实体主体部分的头部,比如,可以用实体头部来说明实体主体部分的数据类型,如Content-Type头部。
常用
Request Headers
Accept-Language:zh-CN,zh;q=0.8 浏览器期望的接受的语言种类
Accept-Encoding:gzip,deflate,sdch 浏览器能够够解码的数据编码方式
User-Agent:Mozilla/5.0xxxxx 主要表示客户端类型
Accept:text/html,xxxxxx…..客户端希望接收什么类型的数据
Pragma:no-cache
Cache-control:no-cache 设置网页缓存的使用方法
Response Headers
Content-Encoding 返回数据的压缩格式
Content-Length 返回数据量的大小
Content-Type 返回数据的类型
Last-Modified 资源最后一次修改的时间
Date 返回数据的时间
Server 服务器类型
总结
Fresco
简介
Fresco是Facebook 出品的一个强大的图片加载组件 Fresco 几行代码就可以搞图片异步下载
中文说明:http://www.fresco-cn.org/
GitHub地址:https://github.com/facebook/fresco
使用
第一步:添加Fresco到项目工程
Android Studio 或者 Gradle
dependencies { compile 'com.facebook.fresco:fresco:0.9.+' }
Eclipse ADT
从github官网下载Demo
从菜单 “文件(File)”,选择导入(Import)
展开 Android, 选择 "Existing Android Code into Workspace", 下一步。
浏览,选中刚才解压的的文件中的 frescolib 目录。
这5个项目应该都会被添加到工程: drawee, fbcore, fresco, imagepipeline, imagepipeline-okhttp。请确认前4个项目一定是被选中的。点击完成。
右键,项目,选择属性,然后选择 Android。
点击右下角的 Add 按钮,选择 fresco,点击 OK,再点击 OK。
第二步在AndroidManifest.xml 中添加以下权限
第三步 在Application中初始化
Fresco.initialize(Context context)
public static void initialize(Context context, ImagePipelineConfig imagePipelineConfig)
建议在Application中初始化
第四步布局文件中声明
添加命名空间 xmlns:fresco="http://schemas.android.com/apk/res-auto"
注意
强制性的宽高,你必须声明 android:layout_width 和 android:layout_height。如果没有在XML中声明这两个属性,将无法正确加载图像。
android:layout_height不支持 wrap_content 属性 如果大小不一致,假设使用的是 wrap_content,图像下载完之后,View将会重新layout,改变大小和位置。这将会导致界面跳跃。
固定宽高比
只有希望显示固定的宽高比时,可以使用wrap_content。
如果希望图片以特定的宽高比例显示,例如 4:3,可以在XML中指定:
也可以在代码中指定显示比例:mSimpleDraweeView.setAspectRatio(1.33f);
第五步加载图片
Uri uri = Uri.parse("https://raw.githubusercontent.com/facebook/fresco/gh-pages/static/fresco-logo.png"); SimpleDraweeView draweeView = (SimpleDraweeView) findViewById(R.id.my_image_view); draweeView.setImageURI(uri);
高级使用
缓存配置
内存配置
final MemoryCacheParams bitmapCacheParams = new MemoryCacheParams( FrescoConfig.MAX_MEMORY_CACHE_SIZE, // 内存缓存中总图片的最大大小,以字节为单位。 Integer.MAX_VALUE, // 内存缓存中图片的最大数量。 FrescoConfig.MAX_MEMORY_CACHE_SIZE, // 内存缓存中准备清除但尚未被删除的总图片的最大大小,以字节为单位。 Integer.MAX_VALUE, // 内存缓存中准备清除的总图片的最大数量。 Integer.MAX_VALUE); // 内存缓存中单个图片的最大大小。 //修改内存图片缓存数量,空间策略(这个方式有点恶心) Supplier<MemoryCacheParams> mSupplierMemoryCacheParams = new Supplier<MemoryCacheParams>() { @Override public MemoryCacheParams get() { return bitmapCacheParams; } };
小图片的磁盘配置
//小图片的磁盘配置 DiskCacheConfig diskSmallCacheConfig = DiskCacheConfig.newBuilder() .setBaseDirectoryPath(context.getApplicationContext().getCacheDir())//缓存图片基路径 .setBaseDirectoryName(IMAGE_PIPELINE_SMALL_CACHE_DIR)//文件夹名 .setMaxCacheSize(FrescoConfig.MAX_DISK_CACHE_SIZE)//默认缓存的最大大小。 .setMaxCacheSizeOnLowDiskSpace(MAX_SMALL_DISK_LOW_CACHE_SIZE)//缓存的最大大小,使用设备时低磁盘空间。 .setMaxCacheSizeOnVeryLowDiskSpace(MAX_SMALL_DISK_VERYLOW_CACHE_SIZE)//缓存的最大大小,当设备极低磁盘空间 // .setCacheErrorLogger(cacheErrorLogger)//日志记录器用于日志错误的缓存。 // .setCacheEventListener(cacheEventListener)//缓存事件侦听器。 // .setDiskTrimmableRegistry(diskTrimmableRegistry)//类将包含一个注册表的缓存减少磁盘空间的环境。 // .setVersion(version) .build();
默认图片的磁盘配置
//默认图片的磁盘配置 DiskCacheConfig diskCacheConfig = DiskCacheConfig.newBuilder() .setBaseDirectoryPath(Environment.getExternalStorageDirectory().getAbsoluteFile())//缓存图片基路径 .setBaseDirectoryName(IMAGE_PIPELINE_CACHE_DIR)//文件夹名 .setMaxCacheSize(FrescoConfig.MAX_DISK_CACHE_SIZE)//默认缓存的最大大小。 .setMaxCacheSizeOnLowDiskSpace(MAX_DISK_CACHE_LOW_SIZE)//缓存的最大大小,使用设备时低磁盘空间。 .setMaxCacheSizeOnVeryLowDiskSpace(MAX_DISK_CACHE_VERYLOW_SIZE)//缓存的最大大小,当设备极低磁盘空间 // .setCacheErrorLogger(cacheErrorLogger)//日志记录器用于日志错误的缓存。 // .setCacheEventListener(cacheEventListener)//缓存事件侦听器。 // .setDiskTrimmableRegistry(diskTrimmableRegistry)//类将包含一个注册表的缓存减少磁盘空间的环境。 // .setVersion(version) .build();
配置图片
ImagePipelineConfig.Builder configBuilder = ImagePipelineConfig.newBuilder(context) // .setAnimatedImageFactory(AnimatedImageFactory animatedImageFactory)//图片加载动画 .setBitmapMemoryCacheParamsSupplier(mSupplierMemoryCacheParams)//内存缓存配置(一级缓存,已解码的图片) .setMainDiskCacheConfig(diskCacheConfig)//磁盘缓存配置(总,三级缓存) .setSmallImageDiskCacheConfig(diskSmallCacheConfig)//磁盘缓存配置(小图片,可选~三级缓存的小图优化缓存)
属性
XML属性
fadeDuration 淡入淡出动画持续时间(单位:毫秒ms)
actualImageScaleType 实际图像的缩放类型
placeholderImage 占位图
placeholderImageScaleType 占位图的缩放类型
progressBarImage 进度图
progressBarImageScaleType 进度图的缩放类型
progressBarAutoRotateInterval 进度图自动旋转间隔时间(单位:毫秒ms)
failureImage 失败图
failureImageScaleType 失败图的缩放类型
retryImage 重试图
retryImageScaleType 重试图的缩放类型
backgroundImage 背景图
overlayImage 叠加图
pressedStateOverlayImage 按压状态下所显示的叠加图
roundAsCircle 设置为圆形图
roundedCornerRadius 圆角半径
roundTopLeft 左上角是否为圆角
roundTopRight 右上角是否为圆角
roundBottomLeft 左下角是否为圆角
roundBottomRight 右下角是否为圆角
roundingBorderWidth 圆形或者圆角图边框的宽度
roundingBorderColor 圆形或者圆角图边框的颜色
roundWithOverlayColor 圆形或者圆角图底下的叠加颜色(只能设置颜色)
viewAspectRatio 控件纵横比
ScaleType
center 居中,无缩放
centerCrop 保持宽高比缩小或放大,使得两边都大于或等于显示边界。居中显示。
focusCrop 同centerCrop, 但居中点不是中点,而是指定的某个点
centerInside 使两边都在显示边界内,居中显示。如果图尺寸大于显示边界,则保持长宽比缩小图片。
fitCenter 保持宽高比,缩小或者放大,使得图片完全显示在显示边界内。居中显示
fitStart 同上。但不居中,和显示边界左上对齐
fitEnd 同fitCenter, 但不居中,和显示边界右下对齐
fitXY 不保存宽高比,填充满显示边界
none 如要使用tile mode显示, 需要设置为none
推荐使用focusCrop
代码中设置XML属性
主要类
1、DraweeHierarchy
2、SettableDraweeHierarchy
3、GenericDraweeHierarchy
4、GenericDraweeHierarchyBuilder
设置
1. 占位图setPlaceholderImage
2. 正在加载图setProgressBarImage
3. 失败图setFailureImage
4. 重试图retryImage
5. 淡入淡出动画setFadeDuration
6. 背景图setBackgrounds
7. 叠加图setOverlay
8. 方法总结图
8.1.
注意事项
不要使用 ScrollViews
如果你想要在一个长的图片列表中滑动,你应该使用 RecyclerView,ListView,或 GridView。这三者都会在你滑动时不断重用子视图。Fresco 的 view 会接收系统事件,使它们能正确管理内存。
ScrollView 不会这样做。因此,Fresco view 不会被告知它们是否在屏幕上显示,并保持图片内存占用直到你的 Fragment 或 Activity 停止。你的 App 将会面临更大的 OOM 风险。
不要向下转换
不要试图把Fresco返回的一些对象进行向下转化,这也许会带来一些对象操作上的便利,但是也许在后续的版本中,你会遇到一些因为向下转换特性丢失导致的难以处理的问题。
不要使用getTopLevelDrawable
DraweeHierarchy.getTopLevelDrawable() 仅仅 应该在DraweeViews中用,除了定义View中,其他应用代码建议连碰都不要碰这个。
在自定义View中,也千万不要将返回值向下转换,也许下个版本,我们会更改这个返回值类型。
不要复用 DraweeHierarchies
永远不要把 DraweeHierarchy 通过 DraweeView.setHierarchy 设置给不同的View。DraweeHierarchy 是由一系列 Drawable 组成的。在 Android 中, Drawable 不能被多个 View 共享。
不要在多个DraweeHierarchy中使用同一个Drawable
原因同上。当然可以使用不同的资源ID,Android 实际会创建不同的 Drawable。
不要直接控制 hierarchy
不要直接使用 SettableDraweeHierarchy 方法(reset,setImage,...)。它们应该仅由 controller 使用。
不要直接给 DraweeView 设置图片。
目前 DraweeView 直接继承于 ImageView,因此它有 setImageBitmap, setImageDrawable 等方法。
如果利用这些方法直接设置一张图片,内部的 DraweeHierarchy 就会丢失,也就无法取到image pipeline 的任何图像了。
使用 DraweeView 时,请不要使用任何 ImageView 的属性
在后续的版本中,DraweeView 会直接从 View 派生。任何属于 ImageView 但是不属于 View 的方法都会被移除
源码解析
https://github.com/desmond1121/Fresco-Source-Analysis
第37天
GreenDao
概要
greenDAO是一个可以帮助 Android 开发者快速将Java对象映射到SQLite数据库的表单中的ORM(Object Relation Mapping)解决方案,通过使用一个简单的面向对象API,开发者可以对Java对象进行存储、更新、删除和查询
特点
一个精简的库
性能最大化
内存开销最小化
易于使用的 APIs
对 Android 进行高度优化
支持数据库加密
使用
配置build.gradle
Project的build.gradle
module的 build.gradle
apply plugin: 'org.greenrobot.greendao'
compile'org.greenrobot:greendao:3.0.1' compile'org.greenrobot:greendao-generator:3.0.0'
配置自定义路劲
greendao { schemaVersion 1 daoPackage 'com.zw.greendao.gen' targetGenDir 'src/main/java' }
schemaVersion: 数据库schema版本,也可以理解为数据库版本号
daoPackage:设置DaoMaster 、DaoSession、Dao包名
targetGenDir:设置DaoMaster 、DaoSession、Dao目录
targetGenDirTest:设置生成单元测试目录
generateTests:设置自动生成单元测试用例
基础
初始化
建议在Application中初始化
DaoMaster.DevOpenHelper devOpenHelper = new DaoMaster.DevOpenHelper(this, "testGreen", null); SQLiteDatabase db = devOpenHelper.getReadableDatabase(); DaoMaster master = new DaoMaster(db); DaoSession daoSession = master.newSession();
查询(Retrieve)
查询方式
使用原生的SQl(raw sql)语句;
也可以使用推荐的方法:使用greenDAO提供的QueryBuilder的API。
查询还支持结果延迟加载(lazy-loading),主要为操作较大查询结果是节约内存并提高性能。
整体概括
也支持返回list ---- 调用list()方法。
当不希望返回null作为结果时,则调用uniqueOrThrow()方法,当结果null时将直接抛出异常。返回多个结果
QueryBuilder
使用过sql语句查询的人都会有一种感触(主要针对不是专职开发数据库并对sql不是很熟练的人),写起来复杂、不能再第一时间发现问题(只能在运行过程中验证sql的正确性)、查找bug麻烦等等。QueryBuilder的出现就是为了解决sql使用的问题,提高开发效率
StudentDao studentDao = daoSession.getStudentDao(); QueryBuilder<Student> qb = studentDao.queryBuilder();
返回单个对象
Note note = qb.unique()
(如果没有返回null)
Note note = qb.uniqueOrThrow()
返回一个非null的实体。否则就会抛出一个DaoException
返回list
List<Student> list = qb.list()
所有entity均会被加载到内存中。结果仅是一个简单的ArrayList
LazyList<Student> students = qb.listLazy()
实体按照需求加载进入内存。一旦列表中的一个元素被第一次访问,它将被加载同时缓存以便以后使用。必须close
LazyList<Student> noteListUnCached = qb.listLazyUncached();
一个“虚拟”的实体列表:任何对列表元素的访问都会导致从数据库中加载,必须close
CloseableListIterator<Student> listIterator = qb.listIterator(); while (listIterator.hasNext()) { Student next = listIterator.next(); }
遍历通过需要的时候加载(lazily)获得的结果,数据没有缓存,必须close
Query
Query类代表了一个可以被重复执行的查询。在QueryBuilder内部其实也是会定义一个Query并执行完成查询,一旦你使用QueryBuilder构建了一个query,该query对象以后可以被重复使用。这种方式比总是重复创建query对象要高效。
实例化
Query query = studentDao.queryBuilder().where(StudentDao.Properties.Id.eq(1)).build();
多次执行查询
Query query = studentDao.queryBuilder().where(StudentDao.Properties.Id.eq(1)).build(); query.setParameter(0, 2); List<Student> studentList = query.list();
说明 查询id=1的对象 复用query.setParameter(0, 2); 修改参数0 id= 2
在多个线程中执行查询
如果你在多线程中使用了查询,你必须调用query的 forCurrentThread()为当前的线程获得一个query实例。从greenDAO 1.3开始, query的实例化被绑定到了那些创建query的线程身上。这样做保证了query对象设置参数时的安全性,避免其他线程的干扰。如果其他线程 试着在query对象上设置参数或者执行查询绑定到了其它线程,将会抛出异常。这样一来,你就不需要一个同步语句了。 事实上你应该避免使用lock,因为如果在并发的事务中使用了同一个query对象,可能会导致死锁。 为了完全避免那些潜在的死锁问题,greenDAO 1.3 引入了forCurrentThread方法,它会返回一个query对象的thread—local实例,该实例 在当前的线程中使用是安全的。当每一次调用 forCueerntThread()的时候,该参数会在builder构建query的时候,设置到初始化参数上
Query ctQuery = query.forCurrentThread();
使用原生sql查询
使用 query对象
String sql = "select * from student limit 5";//原生sql语句 List<Student> sqlList = studentDao.queryBuilder().where(new WhereCondition.StringCondition(sql)).build().list();
使用 xxDao
String sql = "select * from student limit 5";//原生sql语句 studentDao.queryRaw(sql);
使用xxDao占位符
String sql = "select * from student where name=? limit 5";//原生sql语句 studentDao.queryRaw(sql,"hehe");
添加(Create)
studentDao.insertxxx()
更新(Update)
studentDao.update();
studentDao.updateInTx();
studentDao.updateInTx(T... entities)
删除(Delete)
数据库升级
数据库版本升级有很多方法,按不同需求来处理。 其实数据库版本升级比较麻烦的就是数据的迁移 数据库升级的的基本思路就是执行sql语句去创建临时数据表,然后迁移数据,修改临时表名等
第三方库
https://github.com/yuweiguocn/GreenDaoUpgradeHelper
封装工具类
public class MigrationHelper { private static final String CONVERSION_CLASS_NOT_FOUND_EXCEPTION = "MIGRATION HELPER - CLASS DOESN'T MATCH WITH THE CURRENT PARAMETERS"; private static MigrationHelper instance; public static MigrationHelper getInstance() { if(instance == null) { instance = new MigrationHelper(); } return instance; } public void migrate(SQLiteDatabase db, Class>... daoClasses) { generateTempTables(db, daoClasses); DaoMaster.dropAllTables(db, true); DaoMaster.createAllTables(db, false); restoreData(db, daoClasses); } private void generateTempTables(SQLiteDatabase db, Class>... daoClasses) { for(int i = 0; i DaoConfig daoConfig = new DaoConfig(db, daoClasses[i]); String divider = ""; String tableName = daoConfig.tablename; String tempTableName = daoConfig.tablename.concat("_TEMP"); ArrayList properties = new ArrayList(); StringBuilder createTableStringBuilder = new StringBuilder(); createTableStringBuilder.append("CREATE TABLE ").append(tempTableName).append(" ("); for(int j = 0; j String columnName = daoConfig.properties[j].columnName; if(getColumns(db, tableName).contains(columnName)) { properties.add(columnName); String type = null; try { type = getTypeByClass(daoConfig.properties[j].type); } catch (Exception exception) { Crashlytics.logException(exception); } createTableStringBuilder.append(divider).append(columnName).append(" ").append(type); if(daoConfig.properties[j].primaryKey) { createTableStringBuilder.append(" PRIMARY KEY"); } divider = ","; } } createTableStringBuilder.append(");"); db.execSQL(createTableStringBuilder.toString()); StringBuilder insertTableStringBuilder = new StringBuilder(); insertTableStringBuilder.append("INSERT INTO ").append(tempTableName).append(" ("); insertTableStringBuilder.append(TextUtils.join(",", properties)); insertTableStringBuilder.append(") SELECT "); insertTableStringBuilder.append(TextUtils.join(",", properties)); insertTableStringBuilder.append(" FROM ").append(tableName).append(";"); db.execSQL(insertTableStringBuilder.toString()); } } private void restoreData(SQLiteDatabase db, Class>... daoClasses) { for(int i = 0; i DaoConfig daoConfig = new DaoConfig(db, daoClasses[i]); String tableName = daoConfig.tablename; String tempTableName = daoConfig.tablename.concat("_TEMP"); ArrayList properties = new ArrayList(); for (int j = 0; j String columnName = daoConfig.properties[j].columnName; if(getColumns(db, tempTableName).contains(columnName)) { properties.add(columnName); } } StringBuilder insertTableStringBuilder = new StringBuilder(); insertTableStringBuilder.append("INSERT INTO ").append(tableName).append(" ("); insertTableStringBuilder.append(TextUtils.join(",", properties)); insertTableStringBuilder.append(") SELECT "); insertTableStringBuilder.append(TextUtils.join(",", properties)); insertTableStringBuilder.append(" FROM ").append(tempTableName).append(";"); StringBuilder dropTableStringBuilder = new StringBuilder(); dropTableStringBuilder.append("DROP TABLE ").append(tempTableName); db.execSQL(insertTableStringBuilder.toString()); db.execSQL(dropTableStringBuilder.toString()); } } private String getTypeByClass(Class type) throws Exception { if(type.equals(String.class)) { return "TEXT"; } if(type.equals(Long.class) || type.equals(Integer.class) || type.equals(long.class)) { return "INTEGER"; } if(type.equals(Boolean.class)) { return "BOOLEAN"; } Exception exception = new Exception(CONVERSION_CLASS_NOT_FOUND_EXCEPTION.concat(" - Class: ").concat(type.toString())); Crashlytics.logException(exception); throw exception; } private static List getColumns(SQLiteDatabase db, String tableName) { List columns = new ArrayList(); Cursor cursor = null; try { cursor = db.rawQuery("SELECT * FROM " + tableName + " limit 1", null); if (cursor != null) { columns = new ArrayList(Arrays.asList(cursor.getColumnNames())); } } catch (Exception e) { Log.v(tableName, e.getMessage(), e); e.printStackTrace(); } finally { if (cursor != null) cursor.close(); } return columns; } }
调用
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { MigrationHelper.getInstance().migrate(db, UserDao.class, ItemDao.class); }
数据库加密
compile 'net.zetetic:android-database-sqlcipher:3.5.2'
Database readableDatabase = devOpenHelper.getEncryptedReadableDb("hehe");
混淆配置
-keepclassmembers class * extends de.greenrobot.dao.AbstractDao { public static java.lang.String TABLENAME; } -keep class **$Propertie
Sql语句关键字
SELECT 将资料从数据库中的表格内选出,两个关键字:从 (FROM) 数据库中的表格内选出(SELECT)。语法为SELECT "栏位名"
DISTINCT 在上述 SELECT 关键词后加上一个 DISTINCT 就可以去除选择出来的栏位中的重复,从而完成求得这个表格/栏位内有哪些不同的值的功能。语法为SELECT DISTINCT "栏位名" FROM "表格名"。
WHERE 这个关键词可以帮助我们选择性地抓资料,而不是全取出来。语法为SELECT "栏位名" FROM "表格名" WHERE "条件"
AND运算符 允许多个条件的存在,在一个SQL语句中的WHERE子句。语法为:SELECT "栏位名" FROM "表格名" WHERE "简单条件" AND "简单条件1"...AND "简单条件2"}
OR运算符 OR 表示多个条件满足其一 结合SQL语句的WHERE子句中的多个条件 语法为:SELECT "栏位名" FROM "表格名" WHERE "简单条件" OR "简单条件1"...OR"简单条件2"}
IN 在 SQL 中,在两个情况下会用到 IN 这个指令;这一页将介绍其中之一:与 WHERE 有关的那一个情况。在这个用法下,我们事先已知道至少一个我们需要的值,而我们将这些知道的值都放入 IN 这个子句。语法为:SELECT "栏位名" FROM "表格名" WHERE "栏位名" IN ('值一', '值二', ...)
BETWEEN IN 这个指令可以让我们依照一或数个不连续 (discrete)的值的限制之内抓出资料库中的值,而 BETWEEN 则是让我们可以运用一个范围 (range) 内抓出资料库中的值,语法为:SELECT "栏位名" FROM "表格名" WHERE "栏位名" BETWEEN '值一' AND '值二'
LIKE LIKE 是另一个在 WHERE 子句中会用到的指令。基本上, LIKE 能让我们依据一个模式(pattern) 来找出我们要的资料。语法为:SELECT "栏位名" FROM "表格名" WHERE "栏位名" LIKE {模式}
ORDER BY 我们经常需要能够将抓出的资料做一个有系统的显示。这可能是由小往大 (ascending) 或是由大往小(descending)。在这种情况下,我们就可以运用 ORDER BY 这个指令来达到我们的目的。语法为:SELECT "栏位名" FROM "表格名 [WHERE "条件"] ORDER BY "栏位名" [ASC, DESC]
COUNT 这个关键词能够帮我我们统计有多少笔资料被选出来,语法为:SELECT COUNT("栏位名") FROM "表格名"COUNT SELECT "函数名"("栏位名") FROM "表格名"
GROUP BY 语句用于结合合计函数,根据一个或多个列对结果集进行分组。语法为: SELECT "栏位 1", SUM("栏位 2") FROM "表格名" GROUP BY "栏位 1"
HAVING 该关键词可以帮助我们对函数产生的值来设定条件。语法为:SELECT "栏位 1", SUM("栏位 2") FROM "表格名" GROUP BY "栏位 1" HAVING (函数 条件)
ALIAS 我们可以通过 ALIAS 为列名称和表名称指定别名,语法为: SELECT "表格别名"."栏位 1" "栏位别名" FROM "表格名" "表格别名"
LIMIT 子句可以被用于强制 SELECT 语句返回指定的记录数。LIMIT 接受一个或两个数字参数。参数必须是一个整数常量。如果给定两个参数,第一个参数指定第一个返回记录行的偏移量,第二个参数指定返回记录行的最大数目语法为: SELECT * FROM table LIMIT [offset,] rows | rows OFFSET offset 举例: select * from table limit 5; --返回前5行 select * from table limit 0,5; --同上,返回前5行 select * from table limit 5,10; --返回6-15行 select * from table limit 30,-1; // 检索记录行 31-last.
join 用于多表中字段之间的联系,语法如下: select * from table1 INNER|LEFT|RIGHT JOIN table2 ON conditiona
ASC 升序排列 语法: SELECT * FROM table ORDER BY uid ASC 按照uid正序查询数据,也就是按照uid从小到大排列
DESC 降序排列 语法: SELECT * FROM table ORDER BY uid DESC 按照uid逆序查询数据,也就是按照uid从大到小排列
核心类
DaoMaster
DaoMaster 是所有GreenDao使用的开始。DaoMaster封装了Sqlitedatabase并且在指定的schema中管理DAO类。他通过静态方法创建和删除tables。他的内部类OpenHelper, DevOpenHelper继承自SQLiteOpenHelper,并在sqlite database中创建schema。
DaoSession
在指定的schema中管理所有的DAO对象。同样提供基本的持久化方法,比如对实体对象的insert、load、update、refresh、还有delete操作。(在新版本中,DaoSession同样负责保持identity scope,可以简单理解为查询对象的一种缓存技术,
DAOs
提供对实体的持久化和query,greenDao为每一个实体创建一个DAO.他提供比DaoSession更丰富的方法,比如count, loadAll, 和insertInTx(这个方法支持批量插入).
QueryBuilder
Query
Properties
注解
实体@Entity注解
schema:告知GreenDao当前实体属于哪个schema
active:标记一个实体处于活动状态,活动实体有更新、删除和刷新方法
nameInDb:在数据中使用的别名,默认使用的是实体的类名
indexes:定义索引,可以跨越多个列
createInDb:标记创建数据库表
基础属性注解
@Id :主键 Long型,可以通过@Id(autoincrement = true)设置自增长
@Property:设置一个非默认关系映射所对应的列名,默认是的使用字段名 举例:@Property (nameInDb="name")
@NotNul:设置数据库表当前列不能为空
@Transient :添加次标记之后不会生成数据库表的列
索引注解
@Index:使用@Index作为一个属性来创建一个索引,通过name设置索引别名,也可以通过unique给索引添加约束
@Unique:向数据库列添加了一个唯一的约束
关系注解
@ToOne:定义与另一个实体(一个实体对象)的关系
@ToMany:定义与多个实体对象的关系
其他
http://greenrobot.org/greendao/
https://github.com/greenrobot/greenDAO
ORM框架
官方性能对比图
greenDao
设计初衷
较小的文件体积,只集中在必要的部分上
对于Android高度优化
最小化内存开销
便于使用
性能最大化
优点
效率很高,插入和更新的速度是sqlite的2倍,加载实体的速度是ormlite的4.5倍。官网测试结果:http://greendao-orm.com/features/
文件较小(<100K),占用更少的内存 ,但是需要create Dao,
操作实体灵活:支持get,update,delete等操作
缺点
学习成本较高。其中使用了一个java工程根据一些属性和规则去generate一些基础代码,类似于javaBean但会有一些规则,另外还有QueryBuilder、Dao等API,所以首先要明白整个过程,才能方便使用。没有ORMLite那样封装的完整,不过greenDao的官网上也提到了这一点,正是基于generator而不是反射,才使得其效率高的多。
其他
GreenDao支持Protocol buffers协议数据的直接存储 ,如果通过protobuf协议和服务器交互,不需要任何的映射。
Protocol Buffers协议:以一种高效可扩展的对结构化数据进行编码的方式。google内部的RPC协议和文件格式大部分都是使用它。RPC:远程过程调用(Remote Procedure Call,RPC)是一个计算机通信协议,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。
ORMLite
基于注解和反射的的方式,导致ormlite性能有着一定的损失(注解其实也是利用了反射的原理)
优点:文档较全面,社区活跃,有好的维护,使用简单,易上手。
缺点:基于反射,效率较低
ActiveAndroid
ORMLite
第38天
RxJava
基础知识
历史
Rx是微软.Net的一个响应式扩展,Rx借助可观测的序列提供一种简单的方式来创建异步的,基于事件驱动的程序。2012年Netflix为了应对不断增长的业务需求开始将.NET Rx迁移到JVM上面。并于13年二月份正式向外展示了RxJava。从语义的角度来看,RxJava就是.NET Rx。从语法的角度来看,Netflix考虑到了对应每个Rx方法,保留了Java代码规范和基本的模式,Rx系列的库对于很多语言和平台的运用是非常广泛的,例如(.NET, Java, Scala, Clojure, JavaScript, Ruby, Python, C++, Objective-C/Cocoa, Groovy
定义
是一个异步操作库,是一个能让你用极其简洁的逻辑去处理繁琐复杂任务的异步事件库(逻辑简洁)
优点
RxJava可以用非常简洁的代码逻辑来解决复杂问题;而且即使业务逻辑的越来越复杂,它依然能够保持简洁!再配合上Lambda用简单的几行代码分分钟就解决你负责的业务问题
扩展的观察者模式
RxJava 的异步实现,是通过一种扩展的观察者模式来实现的
举例
Observable (被观察者,相当于View)、 Observer (观察者,相当于OnClickListener)、 subscribe ()(订阅,相当于setOnClickListener()方法)事件。Observable 和 Observer 通过 subscribe() 方法实现订阅关系,从而 Observable 可以在需要的时候发出事件来通知 Observer
RxJava 有四个基本概念
Observable (可观察者,即被观察者)
Observer (观察者)
subscribe (订阅)
Observable 和 Observer 通过 subscribe() 方法实现订阅关系,从而 Observable 可以在需要的时候发出事件来通知 Observer
使用
使用步骤
导包
compile 'io.reactivex:rxjava:x.y.z'
第一步创建观察者(Observer)
第二步创建被观察者(Observable)
第三步订阅(Subscription)
Observer
Observable
创建
just( ) — 将一个或多个对象转换成发射这个或这些对象的一个Observable
from( ) — 将一个Iterable, 一个Future, 或者一个数组转换成一个Observable
create( ) — 使用一个函数从头创建一个Observable
repeat( ) — 创建一个重复发射指定数据或数据序列的Observable
repeatWhen( ) — 创建一个重复发射指定数据或数据序列的Observable,它依赖于另一个Observable发射的数据
defer( ) — 只有当订阅者订阅才创建Observable;为每个订阅创建一个新的Observable
range( ) — 创建一个发射指定范围的整数序列的Observable
interval( ) — 创建一个按照给定的时间间隔发射整数序列的Observable
timer( ) — 创建一个在给定的延时之后发射单个数据的Observable
empty( ) — 创建一个什么都不做直接通知完成的Observable
error( ) — 创建一个什么都不做直接通知错误的Observable
never( ) — 创建一个不发射任何数据的Observable
变换
map( ) — 对序列的每一项都应用一个函数来变换Observable发射的数据序列
flatMap( ), concatMap( ), and flatMapIterable( ) — 将Observable发射的数据集合变换为Observables集合,然后将这些Observable发射的数据平坦化的放进一个单独的Observable
buffer( ) — 它定期从Observable收集数据到一个集合,然后把这些数据集合打包发射,而不是一次发射一个
switchMap( ) — 将Observable发射的数据集合变换为Observables集合,然后只发射这些Observables最近发射的数据
scan( ) — 对Observable发射的每一项数据应用一个函数,然后按顺序依次发射每一个值
groupBy( ) — 将Observable分拆为Observable集合,将原始Observable发射的数据按Key分组,每一个Observable发射一组不同的数据
window( ) — 定期将来自Observable的数据分拆成一些Observable窗口,然后发射这些窗口,而不是每次发射一项
cast( ) — 在发射之前强制将Observable发射的所有数据转换为指定类型
过滤
filter( ) — 过滤数据
takeLast( ) — 只发射最后的N项数据
last( ) — 只发射最后的一项数据
lastOrDefault( ) — 只发射最后的一项数据,如果Observable为空就发射默认值
takeLastBuffer( ) — 将最后的N项数据当做单个数据发射
skip( ) — 跳过开始的N项数据
skipLast( ) — 跳过最后的N项数据
take( ) — 只发射开始的N项数据
first( ) and takeFirst( ) — 只发射第一项数据,或者满足某种条件的第一项数据
firstOrDefault( ) — 只发射第一项数据,如果Observable为空就发射默认值
elementAt( ) — 发射第N项数据
elementAtOrDefault( ) — 发射第N项数据,如果Observable数据少于N项就发射默认值
sample( ) or throttleLast( ) — 定期发射Observable最近的数据
throttleFirst( ) — 定期发射Observable发射的第一项数据
throttleWithTimeout( ) or debounce( ) — 只有当Observable在指定的时间后还没有发射数据时,才发射一个数据
timeout( ) — 如果在一个指定的时间段后还没发射数据,就发射一个异常
distinct( ) — 过滤掉重复数据
distinctUntilChanged( ) — 过滤掉连续重复的数据
ofType( ) — 只发射指定类型的数据
ignoreElements( ) — 丢弃所有的正常数据,只发射错误或完成通知
结束
empty()结束发送数据
组合
startWith( ) — 在数据序列的开头增加一项数据
merge( ) — 将多个Observable合并为一个
mergeDelayError( ) — 合并多个Observables,让没有错误的Observable都完成后再发射错误通知
zip( ) — 使用一个函数组合多个Observable发射的数据集合,然后再发射这个结果
and( ), then( ), and when( ) — (rxjava-joins) 通过模式和计划组合多个Observables发射的数据集合
combineLatest( ) — 当两个Observables中的任何一个发射了一个数据时,通过一个指定的函数组合每个Observable发射的最新数据(一共两个数据),然后发射这个函数的结果
join( ) and groupJoin( ) — 无论何时,如果一个Observable发射了一个数据项,只要在另一个Observable发射的数据项定义的时间窗口内,就将两个Observable发射的数据合并发射
switchOnNext( ) — 将一个发射Observables的Observable转换成另一个Observable,后者发射这些Observables最近发射的数据
错误处理
onErrorResumeNext( ) — 指示Observable在遇到错误时发射一个数据序列
onErrorReturn( ) — 指示Observable在遇到错误时发射一个特定的数据
onExceptionResumeNext( ) — instructs an Observable to continue emitting items after it encounters an exception (but not another variety of throwable)指示Observable遇到错误时继续发射数据
retry( ) — 指示Observable遇到错误时重试
retryWhen( ) — 指示Observable遇到错误时,将错误传递给另一个Observable来决定是否要重新给订阅这个Observable
其他
repeat( ) — 创建一个重复发射指定数据或数据序列的Observable
defer( ) — 只有当订阅者订阅才创建Observable;为每个订阅创建一个新的Observable
range( ) — 创建一个发射指定范围的整数序列的Observable
interval( ) — 创建一个按照给定的时间间隔发射整数序列的Observable
timer( ) — 创建一个在给定的延时之后发射单个数据的Observable
error( ) — 创建一个什么都不做直接通知错误的Observable
repeat( ) — 创建一个重复发射指定数据或数据序列的Observable
repeatWhen( ) — 创建一个重复发射指定数据或数据序列的Observable,它依赖于另一个
线程控制(Scheduler)
概要
如果你想给Observable操作符链添加多线程功能,你可以指定操作符(或者特定的Observable)在特定的调度器(Scheduler)上执行。
某些ReactiveX的Observable操作符有一些变体,它们可以接受一个Scheduler参数。这个参数指定操作符将它们的部分或全部任务放在一个特定的调度器上执行。
使用ObserveOn和SubscribeOn操作符,你可以让Observable在一个特定的调度器上执行,ObserveOn指示一个Observable在一个特定的调度器上调用观察者的onNext, onError和onCompleted方法,SubscribeOn更进一步,它指示Observable将全部的处理过程(包括发射数据和通知)放在特定的调度器上执行。
种类
Subscription
常用操作符案例
just
/** * 将一个或多个对象转换成发射这个或这些对象的一个Observable */ public void just() { Observer<String> observer = new Observer<String>() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { } @Override public void onNext(String s) { System.out.println(s); } }; Observable<String> observable = Observable.just("just1", "just2"); observable.subscribe(observer); }
from
map
public void map() { ArrayList<People> list = new ArrayList<>(); for (int i = 0; i < 10; i++) { list.add(new People("测试数据" + i)); } Observable.from(list) .map(user -> user.getName()) .subscribe(name -> { System.out.println(name); }); }
flatMap
public void flatMap() { List<User> users = new ArrayList<>(); for (int i = 0; i < 5; i++) { User user = new User(); user.setUsername("管理员"); ArrayList<Permission> permissions = new ArrayList<>(); Permission p; p = new Permission(); p.setName("订单"); permissions.add(p); p = new Permission(); p.setName("管理员"); permissions.add(p); user.setPermissions(permissions); users.add(user); } Observable.from(users).flatMap(new Func1<User, Observable<Permission>>() { @Override public Observable<Permission> call(User user) { return Observable.from(user.getPermissions()); } }).subscribe(permission -> { String name = permission.getName(); System.out.println(name); }); }
zip
public void zip() { Observable<Integer> observable1 = Observable.just(1, 3, 30); Observable<Integer> observable2 = Observable.just(2, 17, 16, 22); Observable.zip(observable1, observable2, (integer, integer2) -> integer + integer2).subscribe(new Subscriber<Integer>() { @Override public void onCompleted() { System.out.println("Sequence complete."); } @Override public void onError(Throwable e) { System.err.println("Error: " + e.getMessage()); } @Override public void onNext(Integer value) { //输出 3 20 36 System.out.println("Next:" + value); } }); }
merge
public void merge() { Observable<Integer> odds = Observable.just(1, 3, 5); Observable<Integer> evens = Observable.just(2, 4, 6); Observable.merge(odds, evens) .subscribe(integer -> { //输出1 3 5 2 4 6 System.out.println(integer); }); }
RxAnroid
应用场景
接口合并(merge)
一个界面中需要的数据来自多个数据源(请求),而只有当所有的请求的响应数据都拿到之后才能渲染界面
构建多级缓存(concat)
缓存机制想必是众所周知。这里我们就以常见的三级缓存机制为例:首先从内存中获取数据,如果内存中不存在,则从硬盘中获取数据,如果硬盘中不存在数据,则从网络中获取数据
定时任务(timer)
周期任务(interval)
数据过滤(filter)
界面防抖动(throttleFirst)
面防抖动就是用于处理快速点击某控件导致重复打开界面的操作,比如点击某个button可以打开一个Activity,正常情况下,我们一旦点击了该Button便会等待该Activity。在应用响应比较慢,用户以为无响应而多次点击Button或者恶意快速点击的情况下,会重复打开同一个Activity,当用户想要退出该Activity的时候体验会非常差
老接口适配(just)
当你在为老项目添加rxjava支持的时候,难免需要将一些方法返回类型转为Observable.通过just操作符不需要对原方法进行任何修改便可实现
响应式界面
在信息填充界面时,我们经常会遇到只有填写完必要的信息之后,提交按钮才能被点击的情况。比如在登录界面时,只有我们填写完用户名和密码之后,登录按钮才能被点击。通过借助rxjava提供的combineLatest操作符我们可以容易的实现这种响应式界面
RxJava内存优化
内存优化
目前Scheduler提供了一下几种调度策略
Schedulers.immediate():默认的调度策略,不指定线程,也就是运行在当前线程
Schedulers。newThread():运行在一个新创建的线程当中,相当于new Thread()操作。
Schedulers.io():采用了线程池机制,内部维护了一个不限制线程数量的线程池,用于IO密集型操作。
Schedulers.computation():同样采用了线程池机制,只不过线程池中线程的数量取决与CPU的核数,以便实现最大性能。通常用于CPU密集型操作,比如图形处理
内存泄漏
尽管rxjava非常简单易用,但是随着订阅的增多内存开销也会随之增大,尤其是在配合使用网络请求的时候,稍不注意就容易造成内存泄漏。当我们不需要的时候,主动取消订阅
RxRetrofit
RxLifecycle
RxBinding
Platform bindings:compile 'com.jakewharton.rxbinding:rxbinding:0.4.+'
'support-v4' library bindings:compile 'com.jakewharton.rxbinding:rxbinding-support-v4:0.4.+'
'appcompat-v7' library bindings:compile 'com.jakewharton.rxbinding:rxbinding-appcompat-v7:0.4.+'
'design' library bindings: compile 'com.jakewharton.rxbinding:rxbinding-design:0.4.+'
'recyclerview-v7' library bindings: compile 'com.jakewharton.rxbinding:rxbinding-recyclerview-v7:0.4.0'
'leanback-v17' library bindings: compile 'com.jakewharton.rxbinding:rxbinding-leanback-v17:0.4.0'(TV)
RxCache
lambda表达式
背景
Lambda表达式是Java SE 8中一个重要的新特性。lambda表达式允许你通过表达式来代替功能接口。 lambda表达式就和方法一样,它提供了一个正常的参数列表和一个使用这些参数的主体(body,可以是一个表达式或一个代码块)
随着回调模式和函数式编程风格的日益流行,我们需要在Java中提供一种尽可能轻量级的将代码封装为数据(Model code as data)的方法
语法过于冗余
匿名类中的 this 和变量名容易使人产生误解
类型载入和实例创建语义不够灵活
无法捕获非 final 的局部变量
无法对控制流进行抽象
举例
//旧写法 view.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { } }); //新写法 view.setOnClickListener(view -> { });
//旧写法: new Thread(new Runnable() { @Override public void run() { System.out.println("Hello"); } }).start(); //新方法: new Thread( () -> System.out.println("Hello") ).start();
(int a, int b) -> { return a + b; }
() -> System.out.println("Hello World");
(String s) -> { System.out.println(s); }
() -> 42
() -> { return 3.1415 };
概要
函数式接口(Functional interfaces)
只拥有一个方法的接口称为 函数式接口
函数式接口
java.lang.Runnable
java.util.concurrent.Callable
java.security.PrivilegedAction
java.util.Comparator
java.io.FileFilter
java.beans.PropertyChangeListener
JAVA8新增函数式接口
Predicate<T>——接收 T 并返回 boolean
Consumer<T>——接收 T,不返回值
Function<T, R>——接收 T,返回 R
Supplier<T>——提供 T 对象(例如工厂),不接收值
UnaryOperator<T>——接收 T 对象,返回 T
BinaryOperator<T>——接收两个 T,返回 T
lambda表达式的语法由参数列表、箭头符号->和函数体组成。函数体既可以是一个表达式,也可以是一个语句块
表达式:表达式会被执行然后返回执行结果
语句块:语句块中的语句会被依次执行,就像方法中的语句一样
return语句会把控制权交给匿名方法的调用者
break和continue只能在循环中使用
如果函数体有返回值,那么函数体内部的每一条路径都必须返回值
结构
一个 Lambda 表达式可以有零个或多个参数
参数的类型既可以明确声明,也可以根据上下文来推断。例如:(int a)与(a)效果相同
所有参数需包含在圆括号内,参数之间用逗号相隔。例如:(a, b) 或 (int a, int b) 或 (String a, int b, float c)
空圆括号代表参数集为空。例如:() -> 42
当只有一个参数,且其类型可推导时,圆括号()可省略。例如:a -> return a*a
Lambda 表达式的主体可包含零条或多条语句
如果 Lambda 表达式的主体只有一条语句,花括号{}可省略。匿名函数的返回类型与该主体表达式一致
如果 Lambda 表达式的主体包含一条以上语句,则表达式必须包含在花括号{}中(形成代码块)。匿名函数的返回类型与代码块的返回类型一致,若没有返回则为空
配置
Android studio
安装jdk8和下载最新的AS2.1稳定版,JDK location选jdk8的路径。
更新Build-tools至最新的24.0.0 rc3以上(因为Jack工具需要用到这个最新的rc版)
在moudle的build.gradle
目标类型(Target typing)
表达式返回的类型
如何确定类型
编译器负责推导 lambda 表达式类型。它利用 lambda 表达式所在上下文 所期待的类型 进行推导,这个 被期待的类型 被称为 目标类型。lambda 表达式只能出现在目标类型为函数式接口的上下文中
确定类型的条件
T 是一个函数式接口
lambda 表达式的参数和 T 的方法参数在数量和类型上一一对应
lambda 表达式的返回值和 T 的方法返回值相兼容(Compatible)
lambda 表达式内所抛出的异常和 T 的方法 throws 类型相兼容
方法引用
静态方法引用:ClassName::methodName
实例上的实例方法引用:instanceReference::methodName
超类上的实例方法引用:super::methodName
类型上的实例方法引用:ClassName::methodName
构造方法引用:Class::new
数组构造方法引用:TypeName[]::new
目标类型的上下文
词法作用域
变量捕获
方法引用
方法引用的种类
默认方法和静态接口方法
继承默认方法
其他
https://github.com/ReactiveX/RxJava(github地址)
https://github.com/mcxiaoke/RxDocs(中文文档)
https://github.com/ReactiveX/RxAndroid
https://github.com/trello/RxLifecycle
https://github.com/JakeWharton/RxBinding
https://github.com/f2prateek/rx-preferences
https://github.com/pushtorefresh/storio
https://github.com/VictorAlbertos/RxCache
第39天
推送
友盟
极光
分享
友盟
ShareSDK
原生SDK
统计分析
友盟统计分析
百度统计分析
第40天
二维码
综合练习
组件通信库(事件总线)
场景描述
1. 在开发的过程中遇到过从Activity-A跳转到Activity-B,然后需要在Activity-B处理完某些工作之后回调Activity-A中的某个函数,但Activity又不能手动创建对象来设置一个Listener的情况。
2. 遇到在某个Service中更新Activity或Fragment中的界面等组件之间的交互问题
3. 彻底退出应用的功能实现,需要管理activity列表的烦恼
4. 后台下载需要通知各个组件的情况
5. Fragment之间的通信问题
作用
事件总线框架简化了Activity、Fragment、Service等组件之间的交互,让代码更简洁,耦合性更低,相比以往的广播或者序列化对象来传递,事件总线就简约和高效了很多。Event Bus模式也被称为Message Bus或者发布者/订阅者(publisher/subscriber)模式,可以让两个组件相互通信,但是他们之间并不相互知晓
1、它承担传输数据的作用
2、它可以解耦模块之间的耦合性
3、简化代码逻辑
4、相比传统方法要更加高效
5、消息可以在任意线程和位置发送
6、接受消息并执行逻辑的方法可以在任意线程运行(可以设置运行的线程)
7、消息与接收者可以是一对多的关系(类似广播)
基本使用流程
1、注册信息订阅者/接收者(可以理解为需要被调用的那一方)
2、定义事件(类似handle中的Message)
3、实现订阅者的方法(消息发送后被调用的方法)
4、发送消息
主流框架
Android EventBus
1. 定义:一个发布 / 订阅的事件总线
1.1. 主要功能是替代Intent,Handler,BroadCast在Fragment,Activity,Service,线程之间传递消息.优点是开销小,代码更优雅。以及将发送者和接收者解耦
2. 特点
1. 使用注解的方式,类似Otto,函数名称可以自定义
2. 订阅函数支持tag(类似广播接收器的Action)使得事件的投递更加准确,能适应更多使用场景。(可以直接设置执行线程)
3. 工作流程图
3.1.
4. 使用
1. 添加jar
2. 注册事件接收对象
1. 第一步注册对象 : EventBus.getDefault().register(this);
2. 第二步注销对象: EventBus.getDefault().unregister(this);
3. 通过Subscriber注解来标识事件接收对象中的接收方法
1. 接收方法,默认的tag,执行在UI线程
1.1. @Subscriber private void test(String title) { }
2. 含有tag,当用户post事件时,只有指定了"tag"的事件才会触发该函数,执行在UI线程
2.1. @Subscriber(tag = "tag") private void testWithTag(String str) { Log.e("", "tag, str); }
3. 含有tag,当用户post事件时,只有指定了"tag"的事件才会触发该函数, post函数在哪个线程执行,该函数就执行在哪个线程
3.1. @Subscriber(tag = "my_tag", mode=ThreadMode.POST) private void testWithMode(String test) { }
4. 含有tag,当用户post事件时,只有指定了"tag"的事件才会触发该函数,执行在一个独立的线程
4.1. @Subscriber(tag = "tag", mode = ThreadMode.ASYNC) private void testUserAsync(String str) { }
4. Activity, Fragment,Service中发布事件
4.1. EventBus.getDefault().post(test)); EventBus.getDefault().post(test, "tag");
5. 下载地址
1. Github地址:
2. https://github.com/bboyfeiyu/AndroidEventBus
3. 中文版文档:
4. https://github.com/bboyfeiyu/AndroidEventBus/blob/master/README-ch.md
EventBus(3.0)
1. 作用
1.1. EventBus是一个Android端优化的publish/subscribe消息总线,简化了应用程序内各组件间、组件与后台线程间的通信
2. 特点
1. 在EventBus中,使用约定来指定事件订阅者以简化使用。即所有事件订阅都都是以onEvent开头的函数,具体来说,函数的名字是onEvent,onEventMainThread,onEventBackgroundThread,onEventAsync这四个。简单理解就是需要被调用的方法名称需要按照上面的规则来写,不能自定义。(3.0以前) 新版中采用注解方式 @Subscribe(threadMode = ThreadMode.MainThread)
2. 效率高
3. 使用
1. 添加依赖
1.1. compile 'org.greenrobot:eventbus:3.0.0'
2. 添加加速索引(非必要)
2.1. 在项目gradle的dependencies中引入apt编译插件
2.1.1. classpath'com.neenbedankt.gradle.plugins:android-apt:1.8'
2.2. 在App的build.gradle中应用apt插件
2.2.1. applyplugin:'com.neenbedankt.android-apt' apt { arguments{ eventBusIndex"com.zw.MyEventBusIndex" } }
2.3. dependencies中引入EventBusAnnotationProcessor
2.3.1. apt'org.greenrobot:eventbus-annotation-processor:3.0.1'
2.4. EventBus提供了一个EventBusAnnotationProcessor注解处理器来在编译期通过读取@Subscribe()注解并解析, 处理其中所包含的信息,然后生成java类来保存所有订阅者关于订阅的信息,这样就比在运行时使用反射来获得这些订阅者的 信息速度要快.
3. 初始化
3.1. 默认单例
3.1.1. EventBus mEventBus = EventBus.getDefault();
3.2. 自定义
3.2.1. 应用生成好的索引时
3.2.1.1. EventBus mEventBus = EventBus.builder() .addIndex(new MyEventBusIndex()) .build();
3.2.2. 自定义的设置应用到默认单利中
3.2.2.1. EventBus mEventBus = EventBus.builder() .addIndex(newMyEventBusIndex()) .installDefaultEventBus();
4. 定义事件
4.1. public class EventMessage { public String msg; }
4.2. 注意:如果在EventBus 3.0中使用了索引加速,事件类的修饰符必须为public,不然编译时会报错:Subscriber method must be public。
5. 监听事件
5.1. 注册EventBus
5.1.1. mEventBus.register(Object);
5.2. 注销EventBus
5.2.1. mEventBus.unregister(Object);
5.3. @Subscribe(threadMode = ThreadMode.POSTING, priority =0, sticky =true) public void handleEvent ( EventMessage msg ){ Log.d(TAG,msg.info); }
5.4. 参数说明
5.4.1. threadMode: 回调所在的线程
5.4.1.1. ThreadMode总共四个:
5.4.1.2. NAIN UI主线程
5.4.1.3. BACKGROUND 后台线程
5.4.1.4. POSTING 和发布者处在同一个线程
5.4.1.5. ASYNC 异步线程
5.4.2. priority: 优先级
5.4.3. sticky: 是否接收黏性事件
5.4.3.1. EventBus.getDefault().post(new EventMessage());
6. 发送
6.1. 发送普通事件
6.2. 发送粘性事件
6.2.1. EventBus.getDefault().postSticky(new EventMessage());
7. 取消
7.1. mEventBus.cancelEventDelivery();
8. EventBus黏性事件
8.1. 如果不再需要该粘性事件我们可以移除
8.1.1. EventBus.getDefault().removeStickyEvent(new EventMessage());
8.2. 或者调用移除所有粘性事件
8.2.1. EventBus.getDefault().removeAllStickyEvents();
4. 下载地址
4.1. Github地址和文档:
4.2. https://github.com/greenrobot/EventBus
5. 源码解析
5.1. http://www.androidchina.net/4796.html
OTTO
1. Otto 是Android系统的一个Event Bus模式类库。用来简化应用组件间的通信
2. 特点
1. 使用注解的方式,@Subscribe 注解告诉Bus该函数订阅了一个事件,该事件的类型为该函数的参数类型;而@Produce注解告诉Bus该函数是一个事件产生者,产生的事件类型为该函数的返回值。函数名称可以自定义。
2. 订阅函数默认执行在主线程中
3. 使用
3.1. Bus bus = new Bus();
3.2. bus.register(this);
3.3. bus.unregister(this);
3.4. 订阅者
3.4.1. @Subscribe public void post(String msg) { }
3.4.2. 说明
3.4.2.1. 方法名可以随意
3.4.2.2. 参数是Object类型
3.4.2.3. 发送的参数必须与接收参数类型一致
3.5. 发布者
3.5.1. bus.post("发送消息");
3.6. 注意在实际开发中Bus最好自己封装成单例模式
3.6.1. public class BusProvider extends Bus { private static BusProvider bus; public static BusProvider getInstance() { if (bus == null) { bus = new BusProvider(); } return bus; } }
4. 下载地址
4.1. 官网:http://square.github.io/otto/
4.2. github:https://github.com/square/otto
特点
square的otto 使用注解,使用方便,但效率比不了EventBus
AndroidEventBus:使用注解,使用方便,但效率比不上EventBus。订阅函数支持tag(类似广播接收器的Action)使得事件的投递更加准确,能适应更多使用场景。
2
第41天
Android安全加密
加密
1. 基本概念
1. 明文:待加密的的信息
2. 密文:加密后的明文
3. 加密:明文转化为密文的过程
4. 加密密钥:通过加密算法进行的加密操作用的密钥
5. 解密 讲密文转化成明文的过程
6. 解密算法:密文转化成明文的算法
7. 解密密钥:通过解密算法进行解密操作的密钥
8. 密码协议:也称为安全协议,指以密码学为基础的消息交换的通信协议,目的是在网络环境中提供安全服务
9. 密码系统 :只用于加密解密的系统
10. 柯克霍夫原则:大多数民用保密都使用公开的算法。 但相对地,用于政府或军事机密的保密器通常也是保密的
2. 分类
1. 时间分类
1. 古典加密:已字符为基本加密单元
2. 现代加密:以信息块为基本加密单元
2. 保密内容算法
1. 受限制算法 算法的保密性基于保持算法的秘密
2. 基于密钥算法 算法公开,密钥保密
3. 密码体质分类
1. 对称加密 :指的加密密钥跟解密的密钥相同,
1.1. 对称加密是最快速、最简单的一种加密方式,加密(encryption)与解密(decryption)用的是同样的密钥(secret key)。对称加密有很多种算法,由于它效率很高,所以被广泛使用在很多加密协议的核心当中。 对称加密通常使用的是相对较小的密钥,一般小于256 bit。因为密钥越大,加密越强,但加密与解密的过程越慢
2. 非对称加密 指加密密钥与解密密钥不同,密钥分公钥跟私钥
2.1. 它使用了一对密钥,公钥(public key)和私钥(private key)。私钥只能由一方安全保管,不能外泄,而公钥则可以发给任何请求它的人。非对称加密使用这对密钥中的一个进行加密,而解密则需要另一个密钥。比如,你向银行请求公钥,银行将公钥发给你,你使用公钥对消息加密,那么只有私钥的持有人--银行才能对你的消息解密。与对称加密不同的是,银行不需要将私钥通过网络发送出去,因此安全性大大提高。 目前最常用的非对称加密算法是RSA算法
4. 明文处理方法分类
1. 分组密码 :指加密时讲明文分成固定长度的组,用同一密钥和算法对每一块加密,输出也是固定长度的密文,多用于网络加密
2. 流密码 也称序列密码,指加密时每次加密以为或者一个字节的明文
5. 其他
1. 哈希函数(散列函数)
1. 定义
1.1. 就是把任意长度的输入(又叫做预映射, pre-image),通过散列算法,变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,而不可能从散列值来唯一的确定输入值。简单的说就是一种将任意长度的消息压缩到某一固
2. 算法用途
1. 主要用于信息安全领域中加密算法,它把一些不同长度的信息转化成杂乱的128位的编码里,叫做HASH值. 也可以说,hash就是找到一种数据内容和数据存放地址之间的映射关系
2. 文件校验
1. 我们比较熟悉的校验算法有奇偶校验和CRC校验,这2种校验并没有抗数据篡改的能力,它们一定程度上能检测并纠正数据传输中的信道误码,但却不能防止对数据的恶意破坏
3. 数字签名
1. Hash 算法也是现代密码体系中的一个重要组成部分。由于非对称算法的运算速度较慢,所以在数字签名协议中,单向散列函数扮演了一个重要的角色。对 Hash 值,又称"数字摘要"进行数字签名,在统计上可以认为与对文件本身进行数字签名是等效的
4. 鉴权协议
1. 又被称作"挑战--认证模式:在传输信道是可被侦听,但不可被篡改的情况下,这是一种简单而安全的方法
3. 使用
3.1. Base64
1. 简介
1. 按照RFC2045的定义,Base64被定义为:Base64内容传送编码被设计用来把任意序列的8位字节描述为一种不易被人直接识别的形式。(The Base64 Content-Transfer-Encoding is designed to represent arbitrary sequences of octets in a form that need not be humanly readable.) BASE64 严格地说,属于编码格式,而非加密算法
2. RFC2045(多用途Internet邮件扩展)
2. 应用
2.1. 常见于邮件、http加密,截取http信息,你就会发现登录操作的用户名、密码字段通过BASE64加密的 ,上传图片
3. 使用
3.1. 见代码
3.2. MD5 加密
1. 简介
1.1. MD5将任意长度的“字节串”变换成一个128bit的大整数,并且它是一个不可逆的字符串变换算法,换句话说就是,即使你看到源程序和算法描述,也无法将一个MD5的值变换回原始的字符串
2. 算法的应用
2.1. MD5的典型应用是对一段Message(字节串)产生fingerprint(指纹),以防止被“篡改”。举个例子,你将一段话写在一个叫 readme.txt文件中,并对这个readme.txt产生一个MD5的值并记录在案,然后你可以传播这个文件给别人,别人如果修改了文件中的任何内容,你对这个文件重新计算MD5时就会发现。如果再有一个第三方的认证机构,用MD5还可以防止文件作者的“抵赖”,这就是所谓的数字签名应用。 MD5还广泛用于加密和解密技术上,在很多操作系统中,用户的密码是以MD5值(或类似的其它算法)的方式保存的, 用户Login的时候,系统是把用户输入的密码计算成MD5值,然后再去和系统中保存的MD5值进行比较,而系统并不“知道”用户的密码是什么
3. 使用
3.3. DES加密
1. 简介
1.1. DES是一种对称加密算法,所谓对称加密算法即:加密和解密使用相同密钥的算法。DES加密算法出自IBM的研究,后来被美国政府正式采用,之后开始广泛流传,但是近些年使用越来越少,因为DES使用56位密钥,以现代计算能力,24小时内即可被破解。虽然如此,在某些简单应用中,我们还是可以使用DES加密算法
2. 应用
1. 在网络中传输数据进行加密
3. 使用
1. 加密
2. private byte[] decrypt(byte[] src, String password) throws Exception { // DES算法要求有一个可信任的随机数源 SecureRandom random = new SecureRandom(); // 创建一个DESKeySpec对象 DESKeySpec desKey = new DESKeySpec(password.getBytes()); // 创建一个密匙工厂 SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES"); // 将DESKeySpec对象转换成SecretKey对象 SecretKey securekey = keyFactory.generateSecret(desKey); // Cipher对象实际完成解密操作 Cipher cipher = Cipher.getInstance("DES"); // 用密匙初始化Cipher对象 cipher.init(Cipher.DECRYPT_MODE, securekey, random); // 真正开始解密操作 return cipher.doFinal(src); }
3. 解密
4. private byte[] decrypt(byte[] src, String password) throws Exception { // DES算法要求有一个可信任的随机数源 SecureRandom random = new SecureRandom(); // 创建一个DESKeySpec对象 DESKeySpec desKey = new DESKeySpec(password.getBytes()); // 创建一个密匙工厂 SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES"); // 将DESKeySpec对象转换成SecretKey对象 SecretKey securekey = keyFactory.generateSecret(desKey); // Cipher对象实际完成解密操作 Cipher cipher = Cipher.getInstance("DES"); // 用密匙初始化Cipher对象 cipher.init(Cipher.DECRYPT_MODE, securekey, random); // 真正开始解密操作 return cipher.doFinal(src); }
5. 注意: 加密的key必须为8位或者8的倍数,否则在java加密过程中会报错
3.4. AES加密
1. 高级加密标准(英语:Advanced Encryption Standard,缩写:AES),在密码学中又称Rijndael加密法,是美国联邦政府采用的一种区块加密标准。这个标准用来替代原先的DES,已经被多方分析且广为全世界所使用。经过五年的甄选流程,高级加密标准由美国国家标准与技术研究院(NIST)于2001年11月26日发布于FIPS PUB 197,并在2002年5月26日成为有效的标准。2006年,高级加密标准已然成为对称密钥加密中最流行的算法之一。 严格地说,AES和Rijndael加密法并不完全一样(虽然在实际应用中二者可以互换),因为Rijndael加密法可以支持更大范围的区块和密钥长度:AES的区块长度固定为128 比特,密钥长度则可以是128,192或256比特;而Rijndael使用的密钥和区块长度可以是32位的整数倍,以128位为下限,256比特为上限。加密过程中使用的密钥是由Rijndael密钥生成方案产生。AES加密是DES加密的升级版
2. 应用
1. 在网络中传输数据进行加密,数据库关键字段等
3. 加密过程
1. AES 加密过程是在一个4×4的字节矩阵上运作,这个矩阵又称为“体(state)”,其初值就是一个明文区块(矩阵中一个元素大小就是明文区块中的一个 Byte)。(Rijndael加密法因支援更大的区块,其矩阵行数可视情况增加)加密时,各轮AES加密循环(除最后一轮外)均包含4个步骤:
2. AddRoundKey — 矩阵中的每一个字节都与该次循环的子密钥(round key)做XOR运算;每个子密钥由密钥生成方案产生。
3. SubBytes — 透过一个非线性的替换函数,用查找表的方式把每个字节替换成对应的字节。
4. ShiftRows — 将矩阵中的每个横列进行循环式移位。
5. MixColumns — 为了充分混合矩阵中各个直行的操作。这个步骤使用线性转换来混合每行内的四个字节。
4. 使用
1.
2. 注意:4.2以上的版本中,SecureRandom实例的获取方式发生了变化,因此为了兼容高版本,添加了版本判断。
3.5. RSA
1. 简介
1. RSA是第一个既能用于数据加密也能用于数字签名的算法。它易于理解和操作,也很流行。算法的名字以发明者的名字命名:Ron Rivest, Adi Shamir 和Leonard Adleman。但RSA的安全性一直未能得到理论上的证明。它经历了各种攻击,至今未被完全攻破。
2. 应用
2.1.
3. 使用
3.1.
3.6. SHA1
支付宝
开发准备
1、首先,我们需要前往支付宝开放平台,申请我们的支付功能:https://open.alipay.com/platform/home.htm
2、创建一个应用 提交给支付宝进行审核。
3、移动支付-接入指南
https://doc.open.alipay.com/doc2/detail.htm?spm=a219a.7629140.0.0.vCcVse&treeId=58&articleId=103541&docType=1
代码接入流程
1. 下载相关的sdk
1. 下载地址
2. https://doc.open.alipay.com/doc2/detail.htm?spm=a219a.7629140.0.0.P0wrvI&treeId=54&articleId=104509&docType=1
3.
2. 导入Alipay Jar包
1. 将下载的Jar包alipaySDK-xxx.jar复制到libs文件下,如果libs文件夹不存在则新建一个,然后右键Jar包,选择Add As library即可。
3. 配置Manifest文件
1. 添加权限
1.1.
1.2.
1.3.
1.4.
1.5.
2. 添加支付宝的H5支付页面(当手机没有安装支付宝时调用H5支付页面)
2.1.
4. 订单信息生成
1. 获取订单信息
1.1. String orderInfo = getOrderInfo("测试的商品", "该测试商品的详细描述", "0.01");
2. 签名
2.1. String sign = sign(orderInfo);
3. 仅需对sign 做URL编码
3.1. sign = URLEncoder.encode(sign, "UTF-8");
4. 生成完整的
4.1. 完整的符合支付宝参数规范的订单信息
5. 发起支付
1. // 构造PayTask 对象 PayTask alipay = new PayTask(MainActivity.this); // 调用支付接口,获取支付结果 String result = alipay.pay(payInfo, true);
6. 处理支付结果
1. 同步
1.1. private Handler mHandler = new Handler() { @SuppressWarnings("unused") public void handleMessage(Message msg) { switch (msg.what) { case SDK_PAY_FLAG: { PayResult payResult = new PayResult((String) msg.obj); String resultInfo = payResult.getResult();// 同步返回需要验证的信息 String resultStatus = payResult.getResultStatus(); // 判断resultStatus 为“9000”则代表支付成功,具体状态码代表含义可参考接口文档 if (TextUtils.equals(resultStatus, "9000")) { Toast.makeText(MainActivity.this, "支付成功", Toast.LENGTH_SHORT).show(); } else { // 判断resultStatus 为非"9000"则代表可能支付失败 // "8000"代表支付结果因为支付渠道原因或者系统原因还在等待支付结果确认,最终交易是否成功以服务端异步通知为准(小概率状态) if (TextUtils.equals(resultStatus, "8000")) { Toast.makeText(MainActivity.this, "支付结果确认中", Toast.LENGTH_SHORT).show(); } else { // 其他值就可以判断为支付失败,包括用户主动取消支付,或者系统返回的错误 Toast.makeText(MainActivity.this, "支付失败", Toast.LENGTH_SHORT).show(); } } break; } default: break; } } }
2. 异步获取支付结果
2.1. 有服务器发送支付是否成功
3. 注意:同步返回的结果必须放置到服务端进行验证 建议商户依赖异步通知
7. 其他参数
1. 由商户在支付宝注册生成 public static final String PARTNER = "";
2. // 商户收款账号 public static final String SELLER = "";
3. // 商户私钥,pkcs8格式 public static final String RSA_PRIVATE = "";
4. //支付宝处理完请求后,当前页面跳转到商户指定页面的路径,由公司后台服务器提供,可空 public static final String RETURN_URL = ""
微信
业务流程
支付业务流程图
步骤1: 用户进入商户APP,选择商品下单、确认购买,进入支付环节。商户服务后台生成支付订单,签名后将数据传输到APP端
步骤2:用户点击后发起支付操作,进入到微信界面,调起微信支付,出现确认支付界面
步骤3:用户确认收款方和金额,点击立即支付后出现输入密码界面可选择零钱或银行卡
步骤4:输入正确密码后,支付完成,用户端微信出现支付详情页面
步骤5:回跳到商户APP中,商户APP根据支付结果个性化展示订单处理结果
配置
https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=11_1
注册开发者账号(注册时需要填应用的包名和签名,注意这里的签名是App正式版的签名),待审核通过后,会得到一个AppID和AppSecret, 如果需要登录或者支付功能需要企业用户,不对个人开放,申请成功后会拿到一个商户id,后面生成sign时会用到
配置微信后台
商户在微信开放平台申请开发应用后,微信开放平台会生成APP的唯一标识APPID。由于需要保证支付安全,需要在开放平台绑定商户应用包名和应用签名,设置好后才能正常发起支付。设置界面在【开放平台】中的栏目【管理中心 / 修改应用 / 修改开发信息】里面,如图8.8红框内所示。
图片
下载官方jar包
使用
配置回调activity
调用支付
生成订单
请求服务器,有服务器生成订单
从后台服务器获得微信支付参数
调起支付需要的参数
https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=9_12
结构图
partnerId、prapayId、packageValue、nonceStr、timeStamp、sign等都是由服务器端生成
调用微信支付
支付结果回调
微信会回调 WXPayEntryActivity 的public void onResp(BaseResp resp)方法,所以后续操作,放在这个回调函数中操作就可以了。
resp.errCode== 0 :表示支付成功 resp.errCode== -1 :表示支付失败 resp.errCode== -2 :表示取消支付
这个类中的布局是可以自定义的,如果你不需要展示什么布局,而是要跳转页面,删除setContentView(R.layout.xxx)即可
官方直接弹出dialog提示,如果不需要可以删除,可以根据自己的业务需求确定
问题总结
在集成微信支付时,遇到第一次可以调起微信的支付页面,之后再调用支付,总是返回到支付结果页,返回的errorCode总是为 -1。
在申请微信支付接口时,需要填写app 包名称和签名。
demo 的包名称换成申请时填写的包名称。
用提交的签名的keystore文件打包。
注意事项
1. 首先如果要使用微信支付的话,必须先到微信开放平台注册应用,具体地址为https://open.weixin.qq.com/,注册时需要填应用的包名和签名,注意这里的签名是App正式版的签名,可以找一个已上线的包或打一个正式包,使用微信提供的工具(签名工具下载地址为https://open.weixin.qq.com/zh_CN/htmledition/res/dev/download/sdk/Gen_Signature_Android.apk)来获取,获取后填上即可。待审核通过后,会得到一个AppID和AppSecret,AppID分享和支付都要用到,AppSecret没什么实际用途,此时微信分享能力是直接拥有的,支付能力还要额外申请,其中涉及到财务信息等,最好让公司财务部门去申请,申请成功后会拿到一个商户id,后面生成sign时会用到。只有所有审核都通过后,才可调用微信支付功能,这点是前提。
2. 微信分享和微信支付SDK是同一个架包,名为libammsdk.jar
3. 测试微信支付时,务必对自己的App做正式签名,因为一开始就在微信平台注册过签名信息,微信SDK会做校验,只有这样才能调起微信分享和微信支付,直接debug版的包则绝对调不起来,这点务必注意,很多人是跌在这里了!当初做微信分享曾遇到过,所以会很留心,也因为如此,如果微信分享能调起来,微信支付不行,那就不要怀疑签名问题了
4. 还是签名,网上有人说要注意大小写,这点其实是不必的。在微信开放平台看到审核通过的App的签名是大写的,而用微信签名获取工具获得的则显示小写,这个没关系,不要贸然改动平台注册信息,不然又可能导致漫长的审核等待,上面也说了,微信分享如可以,那就不是签名问题
5. 对于IWXAPI实例的创建,官方代码为: IWXAPI api = WXAPIFactory.createWXAPI(context, null);这样写就可以,如果调用另一个工厂方法:IWXAPI api = WXAPIFactory.createWXAPI(context, APP_ID, false);也是OK的
6. req.packageValue=”Sign=WXPay”,一般都是这样写死这个参数值。也有人说写成req.packageValue=”prepay_id=” + prepayid,经测试Android两种写法都是可以调起微信支付的,至少最新版本SDK是可以的,以后则不清楚,官方也建议写Sign=WXPay
7. 生成sign时特别需要注意,首先将key-value键值对拼成字符串,注意key都要小写,如appid,noncestr,package,partnerid,prepayid,timestamp,key,并且名字得按上述名称,我们遇到的错误就是因为partnerid写成了partnerId,prepayid写成了PrepayId,当然我们是在服务端写的,如果在客户端生成sign的话,也需要注意大小写及名称,详细信息请参考官方文档。还有这里的key并非AppID或AppSectet,而是在商户平台设置的,官方描述为“key设置路径:微信商户平台(pay.weixin.qq.com)-->账户设置-->API安全-->密钥设置”。对于noncestr,申请prepayid和生成sign时两次需要用到,由于iOS同事看到相关文章说noncestr前后需要一致,因此这个随机字符串我们是设置成一样的了,这样做Android平台也是OK的,不过个人感觉这里可以不一致,由于这个逻辑在服务器端,我并没有验证,方便的同学可以验证下
8. 对于errCode返回-1,有人说清除微信缓存或切换账户就好了,这种解决方案治标不治本啊,根本不能算解决方案。虽然我没遇到能用这方法解决的问题,但目测是签名的问题
9. 网上有人说需要给调用支付的Activity配置如下intent-filter逻辑上来看,根本不会跳这个界面啊,所以当然是非必需的
第42天
直播
基础概念
什么叫视频?
概念
静止的画面叫图像(picture)。连续的图像变化每秒超过24帧(frame)画面以上时,根椐视觉暂留原理,人眼无法辨别每付单独的静态画面,看上去是平滑连续的视觉效果。这样的连续画面叫视频。连续图像变化每秒低于24帧画面时,人眼有不连续的感觉叫动画(cartoon)
视频
内容元素
图像 ( Image )
音频 ( Audio )
元信息(Metadata)
对视频信息的说明
编码格式
视频(Video)
主流编码标准
MPEG 标准(由MPEG制定)
Moving Picture Experts Group,动态图像专家组
MPEG-1
MPEG-2
MPEG-3
MPEG-4
MPEG-7
MPEG-21
ITU-T 标准(由VCEG制定)
国际电信联盟电信标准分局
H.261
H.262
H.263
H.263v2
H.264(主流)
H265
主流标准对比
音频(Audio)
AAC
容器封装
格式
FLV
一种新的视频格式,全称为FlashVideo。由于它形成的文件极小、加载速度极快
MP4
编码采用的容器,基于 QuickTime MOV 开发,具有许多先进特性;实际上是对Apple公司开发的MOV格式(也称Quicktime格式)的一种改进
MOV
QuickTime 的容器,恐怕也是现今最强大的容器,甚至支持虚拟现实技术,Java等,它的变种 MP4,3GP都没有这么厉害;广泛应用于Mac OS操作系统,在Windows操作系统上也可兼容,但是远比不上AVI格式流行
AVI
最常见的音频视频容器,音频视频交错(Audio Video Interleaved)允许视频和音频交错在一起同步播放.
3GP
3GPP视频采用的格式, 主要用于流媒体传送;3GP其实是MP4格式的一种简化版本,是手机视频格式的绝对主流.
RMVB
MKV
ASF
WMV
封装流程
视频 Video 文件,从结构上讲,都是这样一种组成方式 由图像和音频构成最基本的内容元素; 图像经过视频编码压缩格式处理(通常是 H.264); 音频经过音频编码压缩格式处理(通常是 AAC); 注明相应的元信息(Metadata);
结构图
什么叫直播
什么叫直播?
就是将视频内容的最小颗粒 ( I / P / B 帧),基于时间序列,以光速进行传送的一种技术 直播就是将每一帧数据 ( Video / Audio / Data Frame ),打上时间戳 ( Timestamp ) 后进行流式传输的过程。发送端源源不断的采集音视频数据,经过编码、封包、推流,再经过中继分发网络进行扩散传播,播放端再源源不断地下载数据并按时序进行解码播放。如此就实现了 “边生产、边传输、边消费” 的直播过程
名词解释
编码
所谓视频编码方式就是指通过特定的压缩技术,将某个视频格式的文件转换成另一种视频格式文件的方式。视频流传输中最为重要的编解码标准有国际电联的H.261、H.263、H.264,运动静止图像专家组的M-JPEG和国际标准化组织运动图像专家组的MPEG系列标准
I frame
常被成为关键帧,k-frame,帧内编码帧 又称intra picture,I 帧通常是每个 GOP(MPEG 所使用的一种视频压缩技术)的第一个帧,经过适度地压缩,做为随机访问的参考点,可以当成图象。I帧可以看成是一个图像经过压缩后的产物。
P frame
前向预测编码帧 又称predictive-frame,通过充分将低于图像序列中前面已编码帧的时间冗余信息来压缩传输数据量的编码图像,也叫预测帧
B frame
双向预测内插编码帧 又称bi-directional interpolated prediction frame,既考虑与源图像序列前面已编码帧,也顾及源图像序列后面已编码帧之间的时间冗余信息来压缩传输数据量的编码图像,也叫双向预测帧
PTS
Presentation Time Stamp。PTS主要用于度量解码后的视频帧什么时候被显示出来
DTS
Decode Time Stamp。DTS主要是标识读入内存中的bit流在什么时候开始送入解码器中进行解码。
这里可以添加一个IPB的图片展示,类似于如何预测如何显示的
什么叫点播
把视频内容已经存放在服务器上,可以进行回放,类似于播放录像,但是录像是远程服务器的,而不是本地视频文件
流媒体
概念
指采用流式传输的方式在Internet / Intranet播放的媒体格式.流媒体的数据流随时传送随 时播放,只是在开始时有些延迟边下载边播入的流式传输方式不仅使启动延时大幅度地缩短,而且对系统缓存容量的需求也大大降低,极大地减少用户用在等待的时间
流媒体协议
RTSP+RTP
Real-time Transport Protocol)是用于Internet上针对多媒体数据流的一种传输层协议。RTP协议详细说明了在互联网上传递音频和视频的标准数据包格式,现在一般直播用的比较少
RTMP(Real Time Messaging Protocol)
实时消息传送协议是Adobe Systems公司为Flash播放器和服务器之间音频、视频和数据传输 开发的开放协议,现在直播当中用的比较多,是一个比较流行的流媒体协议
HLS(HTTP Live Streaming)
实现的基于HTTP的流媒体传输协议,可实现流媒体的直播和点播,主要应用在iOS系统,为iOS设备(如iPhone、iPad)提供音视频直播和点播方案。HLS点播,基本上就是常见的分段HTTP点播,不同在于,它的分段非常小。
相对于常见的流媒体直播协议,例如RTMP协议、RTSP协议,HLS直播最大的不同在于,直播客户端获取到的,并不是一个完整的数据流。HLS协议在服务器端将直播数据流存储为连续的、很短时长的媒体文件(MPEG-TS格式)TS片段(flv和aac合成的),而客户端则不断的下载并播放这些小文件,因为服务器端总是会将最新的直播数据生成新的小文件,这样客户端只要不停的按顺序播放从服务器获取到的文件,就实现了直播。由此可见,基本上可以认为,HLS是以点播的技术方式来实现直播。由于数据通过HTTP协议传输,所以完全不用考虑防火墙或者代理的问题,而且分段文件的时长很短,客户端可以很快的选择和切换码率,以适应不同带宽条件下的播放。不过HLS的这种技术特点,决定了它的延迟一般总是会高于普通的流媒体直播协议
MMS (Microsoft Media Server Protocol)
微软媒体服务器协议,用来访问并流式接收 Windows Media 服务器中 .asf 文件的一种协议。MMS 协议用于访问 Windows Media 发布点上的单播内容
结构图
码流
数据传输时单位时间传送的数据位数,可以理解其为取样率,单位时间内取样率越大,精度就越高,处理出来的文件就越接近原始文件,但是文件体积与取样率是成正比的如何用最低的码率达到最少的失真,一般我们用的单位是kbps即千位每秒
帧率
帧/秒(frames per second)的缩写,也称为帧速率,测量用于保存、显示动态视频的信息数量。每一帧都是静止的图象,快速连续地显示帧便形成了运动的假象。每秒钟帧数 (fps) 愈多,所显示的动作就会愈流畅,可理解为1秒钟时间里刷新的图片的帧数,也可以理解为图形处理器每秒钟能够刷新几次,也就是指每秒钟能够播放(或者录制)多少格画面。
音频采样率
音频采样率是指录音设备在一秒钟内对声音信号的采样次数,采样频率越高声音的还原就越真实越自然。在当今的主流采集卡上,采样频率一般共分为22.05KHz、44.1KHz、48KHz三个等级,22.05KHz只能达到FM广播的声音品质,44.1KHz则是理论上的CD音质界限,48KHz则更加精确一些
直播系统
礼物
聊天
弹幕(注意高并发量)
弹幕开发
1. 集成
1.1. repositories { jcenter() } dependencies { compile 'com.github.ctiao:DanmakuFlameMaster:0.4.9' }
2. 布局文件定义
2.1.
3. 初始化
3.1. private BaseDanmakuParser mParser;//解析器对象 private IDanmakuView mDanmakuView; //实例化 mDanmakuView = (IDanmakuView) findViewById(R.id.sv_danmaku); private DanmakuContext mContext; mContext = DanmakuContext.create(); // 设置弹幕的最大显示行数 HashMap<Integer, Integer> maxLinesPair = new HashMap<Integer, Integer>(); maxLinesPair.put(BaseDanmaku.TYPE_SCROLL_RL, 3); // 滚动弹幕最大显示3行 // 设置是否禁止重叠 HashMap<Integer, Boolean> overlappingEnablePair = new HashMap<Integer, Boolean>(); overlappingEnablePair.put(BaseDanmaku.TYPE_SCROLL_LR, true); overlappingEnablePair.put(BaseDanmaku.TYPE_FIX_BOTTOM, true); mContext.setDanmakuStyle(IDisplayer.DANMAKU_STYLE_STROKEN, 3) //设置描边样式 .setDuplicateMergingEnabled(false) .setScrollSpeedFactor(1.2f) //是否启用合并重复弹幕 .setScaleTextSize(1.2f) //设置弹幕滚动速度系数,只对滚动弹幕有效 .setCacheStuffer(new SpannedCacheStuffer(), mCacheStufferAdapter) // 图文混排使用SpannedCacheStuffer 设置缓存绘制填充器,默认使用{@link SimpleTextCacheStuffer}只支持纯文字显示, 如果需要图文混排请设置{@link SpannedCacheStuffer}如果需要定制其他样式请扩展{@link SimpleTextCacheStuffer}|{@link SpannedCacheStuffer} .setMaximumLines(maxLinesPair) //设置最大显示行数 .preventOverlapping(overlappingEnablePair); //设置防弹幕重叠,null为允许重叠 if (mDanmakuView != null) { mParser = createParser(this.getResources().openRawResource(R.raw.comments)); //创建解析器对象,从raw资源目录下解析comments.xml文本 mDanmakuView.setCallback(new master.flame.danmaku.controller.DrawHandler.Callback() { @Override public void updateTimer(DanmakuTimer timer) { } @Override public void drawingFinished() { } @Override public void danmakuShown(BaseDanmaku danmaku) { } @Override public void prepared() { mDanmakuView.start(); } }); mDanmakuView.prepare(mParser, mContext); mDanmakuView.showFPS(false); //是否显示FPS mDanmakuView.enableDanmakuDrawingCache(true);
3.2.
3.3.
3.4.
3.5.
4. 创建解析器对象
4.1. private BaseDanmakuParser createParser(InputStream stream) { if (stream == null) { return new BaseDanmakuParser() { @Override protected Danmakus parse() { return new Danmakus(); } }; } ILoader loader = DanmakuLoaderFactory.create(DanmakuLoaderFactory.TAG_BILI); try { loader.load(stream); } catch (IllegalDataException e) { e.printStackTrace(); } BaseDanmakuParser parser = new BiliDanmukuParser(); IDataSource<?> dataSource = loader.getDataSource(); parser.load(dataSource); return parser; } 注: DanmakuLoaderFactory.create(DanmakuLoaderFactory.TAG_BILI) //xml解析 DanmakuLoaderFactory.create(DanmakuLoaderFactory.TAG_ACFUN) //json文件格式解析
5. 自定义弹幕背景和边距
5.1. private static class BackgroundCacheStuffer extends SpannedCacheStuffer { // 通过扩展SimpleTextCacheStuffer或SpannedCacheStuffer个性化你的弹幕样式 final Paint paint = new Paint(); @Override public void measure(BaseDanmaku danmaku, TextPaint paint) { danmaku.padding = 10; // 在背景绘制模式下增加padding super.measure(danmaku, paint); } @Override public void drawBackground(BaseDanmaku danmaku, Canvas canvas, float left, float top) { paint.setColor(0x8125309b); //弹幕背景颜色 canvas.drawRect(left + 2, top + 2, left + danmaku.paintWidth - 2, top + danmaku.paintHeight - 2, paint); } @Override public void drawStroke(BaseDanmaku danmaku, String lineText, Canvas canvas, float left, float top, Paint paint) { // 禁用描边绘制 } }
6. 添加文本弹幕
6.1. private void addDanmaku(boolean islive) { BaseDanmaku danmaku = mContext.mDanmakuFactory.createDanmaku(BaseDanmaku.TYPE_SCROLL_RL); if (danmaku == null || mDanmakuView == null) { return; } danmaku.text = "这是一条弹幕" + System.nanoTime(); danmaku.padding = 5; danmaku.priority = 0; //0 表示可能会被各种过滤器过滤并隐藏显示 //1 表示一定会显示, 一般用于本机发送的弹幕 danmaku.isLive = islive; //是否是直播弹幕 danmaku.time = mDanmakuView.getCurrentTime() + 1200; //显示时间 danmaku.textSize = 25f * (mParser.getDisplayer().getDensity() - 0.6f); danmaku.textColor = Color.RED; danmaku.textShadowColor = Color.WHITE; //阴影/描边颜色 danmaku.borderColor = Color.GREEN; //边框颜色,0表示无边框 mDanmakuView.addDanmaku(danmaku); }
7. 添加图文混排弹幕
7.1. private void addDanmaKuShowTextAndImage(boolean islive) { BaseDanmaku danmaku = mContext.mDanmakuFactory.createDanmaku(BaseDanmaku.TYPE_SCROLL_RL); Drawable drawable = getResources().getDrawable(R.drawable.ic_launcher); drawable.setBounds(0, 0, 100, 100); SpannableStringBuilder spannable = createSpannable(drawable); danmaku.text = spannable; danmaku.padding = 5; danmaku.priority = 1; // 一定会显示, 一般用于本机发送的弹幕 danmaku.isLive = islive; danmaku.time = mDanmakuView.getCurrentTime() + 1200; danmaku.textSize = 25f * (mParser.getDisplayer().getDensity() - 0.6f); danmaku.textColor = Color.RED; danmaku.textShadowColor = 0; // 重要:如果有图文混排,最好不要设置描边(设textShadowColor=0),否则会进行两次复杂的绘制导致运行效率降低 danmaku.underlineColor = Color.GREEN; mDanmakuView.addDanmaku(danmaku); } /** * 创建图文混排模式 * @param drawable * @return */ private SpannableStringBuilder createSpannable(Drawable drawable) { String text = "bitmap"; SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(text); ImageSpan span = new ImageSpan(drawable);//ImageSpan.ALIGN_BOTTOM); spannableStringBuilder.setSpan(span, 0, text.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE); spannableStringBuilder.append("图文混排"); spannableStringBuilder.setSpan(new BackgroundColorSpan(Color.parseColor("#8A2233B1")), 0, spannableStringBuilder.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE); return spannableStringBuilder; }
8. 弹幕的隐藏/显示,暂停/继续
8.1. //显示弹幕 mDanmakuView.show();
8.2. //隐藏弹幕 mDanmakuView.hide();
8.3. //暂停 if (mDanmakuView != null && mDanmakuView.isPrepared()) { mDanmakuView.pause(); }
8.4. //继续 if (mDanmakuView != null && mDanmakuView.isPrepared() && mDanmakuView.isPaused()) { mDanmakuView.resume(); }
9. 弹幕的定时发送
9.1. Boolean b = (Boolean) mBtnSendDanmakus.getTag(); timer.cancel(); if (b == null || !b) { mBtnSendDanmakus.setText(R.string.cancel_sending_danmakus); timer = new Timer(); timer.schedule(new AsyncAddTask(), 0, 1000); mBtnSendDanmakus.setTag(true); } else { mBtnSendDanmakus.setText(R.string.send_danmakus); mBtnSendDanmakus.setTag(false); } Timer timer = new Timer(); class AsyncAddTask extends TimerTask { @Override public void run() { for (int i = 0; i < 20; i++) { addDanmaku(true); SystemClock.sleep(20); } } }
10. 创建图文混排的填充适配器
10.1. private BaseCacheStuffer.Proxy mCacheStufferAdapter = new BaseCacheStuffer.Proxy() { private Drawable mDrawable; /** * 在弹幕显示前使用新的text,使用新的text * @param danmaku * @param fromWorkerThread 是否在工作(非UI)线程,在true的情况下可以做一些耗时操作(例如更新Span的drawblae或者其他IO操作) * @return 如果不需重置,直接返回danmaku.text */ @Override public void prepareDrawing(final BaseDanmaku danmaku, boolean fromWorkerThread) { if (danmaku.text instanceof Spanned) { // 根据你的条件检查是否需要需要更新弹幕 // FIXME 这里只是简单启个线程来加载远程url图片,请使用你自己的异步线程池,最好加上你的缓存池 new Thread() { @Override public void run() { String url = "http://www.bilibili.com/favicon.ico"; InputStream inputStream = null; Drawable drawable = mDrawable; if (drawable == null) { try { URLConnection urlConnection = new URL(url).openConnection(); inputStream = urlConnection.getInputStream(); drawable = BitmapDrawable.createFromStream(inputStream, "bitmap"); mDrawable = drawable; } catch (MalformedURLException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { IOUtils.closeQuietly(inputStream); } } if (drawable != null) { drawable.setBounds(0, 0, 100, 100); SpannableStringBuilder spannable = createSpannable(drawable); danmaku.text = spannable; if (mDanmakuView != null) { mDanmakuView.invalidateDanmaku(danmaku, false); } return; } } }.start(); } } @Override public void releaseResource(BaseDanmaku danmaku) { // TODO 重要:清理含有ImageSpan的text中的一些占用内存的资源 例如drawable } };
11. 退出时释放资源
11.1. @Override protected void onDestroy() { super.onDestroy(); if (mDanmakuView != null) { // dont forget release! mDanmakuView.release(); mDanmakuView = null; } }
12.
12.1.
12.2.
12.3.
12.4.
12.5.
13.
13.1.
13.2.
13.3.
13.4.
13.5.
弹幕开源库:https://github.com/Bilibili/DanmakuFlameMaster
即时通讯
友盟即时通信
支付
任务
安全
内容审核
支付系统安全
国家政策
移动直播
热门直播APP
直播模型
技术流程
采集
视频采集
概要
图像采集的图片结果组合成一组连续播放的动画,即构成视频中可肉眼观看的内容。图像的采集过程主要由摄像头等设备拍摄成 YUV 编码的原始数据,然后经过编码压缩成 H.264 等格式的数据分发出去。常见的视频封装格式有:MP4、3GP、AVI、MKV、WMV、MPG、VOB、FLV、SWF、MOV、RMVB 和 WebM 等
技术要点
图像格式:通常采用 YUV 格式存储原始数据信息,其中包含用 8 位表示的黑白图像灰度值,以及可由 RGB 三种色彩组合成的彩色图像。
传输通道:正常情况下视频的拍摄只需 1 路通道,随着 VR 和 AR 技术的日渐成熟,为了拍摄一个完整的 360° 视频,可能需要通过不同角度拍摄,然后经过多通道传输后合成。
分辨率:随着设备屏幕尺寸的日益增多,视频采集过程中原始视频分辨率起着越来越重要的作用,后续处理环节中使用的所有视频分辨率的定义都以原始视频分辨率为基础。视频采集卡能支持的最大点阵反映了其分辨率的性能。
采样频率:采样频率反映了采集卡处理图像的速度和能力。在进行高度图像采集时,需要注意采集卡的采样频率是否满足要求。采样率越高,图像质量越高,同时保存这些图像信息的数据量也越大
采集的方式
摄像头采集
目前摄像头采集是社交直播中最常见的采集方式,比如主播使用手机的前置和后置摄像头拍摄。在现场直播场景中,也有专业的摄影、摄像设备用来采集
核心代码
Camera
主要管理相机设备
1 Obtain an instance of Camera from open(int). 获得相机的实例
2 Get existing (default) settings with getParameters(). 获得设置属性的对象
3 If necessary, modify the returned Camera.Parameters object and call setParameters(Camera.Parameters). 设置属性
4 If desired, call setDisplayOrientation(int). 设置角度
5 Important: Pass a fully initialized SurfaceHolder to setPreviewDisplay(SurfaceHolder). Without a surface, the camera will be unable to start the preview. 设置预览区
6 Important: Call startPreview() to start updating the preview surface. Preview must be started before you can take a picture.打开预览区
7 When you want, call takePicture(Camera.ShutterCallback, Camera.PictureCallback, Camera.PictureCallback, Camera.PictureCallback) to capture a photo. Wait for the callbacks to provide the actual image data. 拍照,等待回调方法返回图片数据
8 After taking a picture, preview display will have stopped. To take more photos, call startPreview() again first.拍照完毕后停下预览,如果需要继续拍照,可以重新调用打开预览的方法。
9 Call stopPreview() to stop updating the preview surface.
10 Important: Call release() to release the camera for use by other applications. Applications should release the camera immediately in onPause() (and re-open() it inonResume()). 释放相机资源
To quickly switch to video recording mode, use these steps: 录制视频
11 Obtain and initialize a Camera and start preview as described above. 首先初始化相机,打开预览区
12 Call unlock() to allow the media process to access the camera. 调用unlock方法将media 和Camera两个进程串起来
13 Pass the camera to setCamera(Camera). See MediaRecorder information about video recording. 把相机设置到MeidaRecorder 中
14 When finished recording, call reconnect() to re-acquire and re-lock the camera. 录制结束后重新设置相机
15 If desired, restart preview and take more photos or videos. 如果想要继续拍照或者录制视频,需要重新打开预览区
16 Call stopPreview() and release() as described above. 像拍照步骤一样,最后需要调用停止预览、释放资源。
步骤
1. 通过Camera.open()来获取Camera实例。
2. 创建Preview类,需要继承SurfaceView类并实现SurfaceHolder.Callback接口。
3. 为相机设置Preview
4. 设置预览回调接口
5. 释放Camera。
重要API
open():通过open方法获取Camera实例。
setPreviewDisplay(SurfaceHolder):设置预览拍照
startPreview():开始预览
stopPreview():停止预览
release():释放Camera实例
Parameters
setPictureFormat() 方法用于设置相机照片的格式,其参数是一个字符型参数,位于PixelFormat类中,如:PixelFormat.JPEG。
setSceneMode() 方法用于设置相机场景类型,其参是是一个字符型参数,位于Parameters类中,以SCENE_MODE_开头。
setZoom() 方法用于设置相机焦距,其参数是一个整型的参数,该参数的范围是0到Camera.getParameters().getMaxZoom()。
setPictureSize() 方法用于设置相机照片的大小,参数为整型。
setWhiteBalance() 方法用于设置相机照片白平衡,其参数是一个字符型,位于Parameters类中,以WHITE_BALANCE开头。
setJpegQuality() 方法用于设置相机照片的质量,其参数是一个整型参数,取值范围为1到100。
setFlashMode() 方法用于设置闪光灯的类型,其参数是一个字符型参数,位于Parameters类中,以FLASH_MODE_开头。
setColorEffect() 方法用于设置照片颜色特效的类型,其参数是一个字符型参数,位于Parameters类中,以EFFECT_开头。
Mainfest
权限
1.声明使用照相机的权限
注:如果你是调用系统Camera程序的话,就不必声明
2.Camera特性,你必须在AndroidMainfest.xml中声明照相机特性
如果你的程序可能需要使用照相机,但并不是一定的,那么可以设置android:required属性
3.存储权限,如果你的程序想在扩展存储设备上(如sd卡)存储你的照片或者拍摄的视频,必须声明如下权限:
4.音频录制权限, 为了录制音频或者视频,必须在AndroidMainfest.xml文件中设置如下权限:
屏幕方向
//禁止横屏 android:configChanges="orientation|keyboardHidden" android:screenOrientation="portrait"
第三方
其他
https://developer.android.com/guide/topics/media/camera.html#intents
基本操作
Camera摄像头的基本操作
Mainfest
权限
1.声明使用照相机的权限
注:如果你是调用系统Camera程序的话,就不必声明
2.Camera特性,你必须在AndroidMainfest.xml中声明照相机特性
如果你的程序可能需要使用照相机,但并不是一定的,那么可以设置android:required属性
3.存储权限,如果你的程序想在扩展存储设备上(如sd卡)存储你的照片或者拍摄的视频,必须声明如下权限:
4.音频录制权限, 为了录制音频或者视频,必须在AndroidMainfest.xml文件中设置如下权限:
屏幕方向
//禁止横屏 android:configChanges="orientation|keyboardHidden" android:screenOrientation="portrait"
初始化相机
打开摄像头,使用open方法
设置摄像头的预览数据界面
获取到Camera.Parameters参数信息
在把添加好的参数信息设置回去,调用startPreview开始预览效果了
销毁相机
第一步:将摄像头的预览清空
第二步:停止预览效果
第三步:释放摄像头,因为系统默认只能同时开启一个摄像头不管是前置摄像头还是后置摄像头,所以不用的时候一定要释放
第四步:置空摄像头对象
Camera摄像头方向和数据尺寸
摄像头方向有两种设置方法
Camera类本身的setDisplayOrientation方法(Camera的预览方向设置了,默认是:横屏方向,旋转度是0,如果想竖着拍摄的话,需要逆时针旋转90度即可)
Camera.Parameters类的setRotation方法
设置尺寸大小也有两个方法
setPreviewSize
setPictureSize
注意:关于尺寸问题,其实是有指定限制的,同样可以通过一个方法来获取Camera所支持的尺寸大小:getSupportedPreviewSizes
Camera摄像头的前置和后置区分
Camera摄像头的数据格式
Camera摄像头的对焦拍照
Camera摄像头的数据采集以及处理
SurfaceView
作用用于控制预览界面 可以直接从内存或者DMA等硬件接口取得图像数据,是个非常重要的绘图容器。 它的特性是:可以在主线程之外的线程中向屏幕绘图上。这样可以避免画图任务繁重的时候造成主线程阻塞,从而提高了程序的反应速度。在游戏开发中多用到SurfaceView,游戏中的背景、人物、动画等等尽量在画布canvas中画出
SurfaceHolder.Callback接口:用于处理预览的事件,需实现如下三个方法:
surfaceCreated(SurfaceHolderholder):预览界面创建时调用,每次界面改变后都会重新创建,需要获取相机资源并设置SurfaceHolder。
surfaceChanged(SurfaceHolderholder, int format, int width, int height):预览界面发生变化时调用,每次界面发生变化之后需要重新启动预览。
surfaceDestroyed(SurfaceHolderholder):预览销毁时调用,停止预览,释放相应资源。
屏幕录制
屏幕录制采集的方式在游戏直播场景中非常常见
音频采集
概要
音频数据既能与图像结合组合成视频数据,也能以纯音频的方式采集播放,后者在很多成熟的应用场景如在线电台和语音电台等起着非常重要的作用。音频的采集过程主要通过设备将环境中的模拟信号采集成 PCM 编码的原始数据,然后编码压缩成 MP3 等格式的数据分发出去。常见的音频压缩格式有:MP3,AAC,OGG,WMA,Opus,FLAC,APE,m4a 和 AMR 等。
核心代码
int bufferSize = AudioRecord.getMinBufferSize(frequency, channelConfiguration, audioEncoding); AudioRecord audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC,frequency, channelConfiguration, audioEncoding, bufferSize); short[] buffer = new short[bufferSize]; audioRecord.startRecording(); isRecording = true ; while (isRecording) { int bufferReadResult = audioRecord.read(buffer, 0, bufferSize); for (int i = 0; i < bufferReadResult; i++) publisAudio(buffer) 这里将buffer做处理成AAC,然后发送
预处理
主要是针对图像处理 例如:美颜,滤镜
难点:图形处理主要用到gpu加速,需要考虑能耗效果的平衡
视频处理<难点>
美颜
水印
滤镜
自定义滤镜
音频处理
混音
降噪
特效
编码
为什么要编码?
原始视频数据存储空间大,一个 1080P 的 7s 视频需要 817 MB
原始视频数据传输占用带宽大,10Mbps 的带宽传输上述 7s 视频需要 11 分钟
而经过 H.264 编码压缩之后,视频大小只有 708 k ,10 Mbps 的带宽仅仅需要 500 ms ,可以满足实时传输的需求,所以从视频采集传感器采集来的原始视频势必要经过视频编码
原理
核心思想去冗余信息
空间冗余:图像相邻像素之间有较强的相关性
视频本质上讲是一系列图片连续快速的播放,最简单的压缩方式就是对每一帧图片进行压缩,这种编码方式只有帧内编码,利用空间上的取样预测来编码。形象的比喻就是把每帧都作为一张图片,采用 JPEG 的编码格式对图片进行压缩,这种编码只考虑了一张图片内的冗余信息压缩
时间冗余:视频序列的相邻图像之间内容相似
编码冗余:不同像素值出现的概率不同
视觉冗余:人的视觉系统对某些细节不敏感
编码方式
硬编码
使用非CPU进行编码,如显卡GPU、专用的DSP、FPGA、ASIC芯片等
软编码
大部分硬件都支持,h.256,aac等,android处理器繁杂,兼容性问题
使用CPU进行编码
对比
软编码:实现直接、简单,参数调整方便,升级易,但CPU负载重,性能较硬编码低,低码率下质量通常比硬编码要好一点。 硬编码:性能高,低码率下通常质量低于硬编码器,但部分产品在GPU硬件平台移植了优秀的软编码算法(如X264)的,质量基本等同于软编码。
难点:平衡分辨率,码率,使得体积画质最优,参数组合必须设置合理
传输
主要是传输协议的选择,CDN技术,云服务器
推流
通过Camera的预览回调获得图像数据
FFmepgFrameRecord的record(Frame frame)方法
通过AudioRecord 得到音频数据,然后编码发送到
recorder.recordSamples(audioData);
解码
硬解码
软解码
渲染
涉及分辨率适配,服务器端客服端配合优化视频音频同步,稳定等功能,音频优化等技术
开源技术
服务器
测试地址:http://ossrs.net/players/srs_player.html?stream=livestream&port=19350
开源
simple rtmp server(SRS)
red5
crtmpserver
erlyvideo
haXevideo
FluorineFX
nginx-rtmp
Cumulus
Server
Mistserver
javacv
概要
JavaCV 提供了在计算机视觉领域的封装库,包括:OpenCV、ARToolKitPlus、libdc1394 2.x 、PGR FlyCapture和FFmpeg。此外,该工具可以很容易地使用Java平台的功能。 JavaCV 还带有硬件加速的全屏幕图像显示(CanvasFrame),易于在多个内核中执行并行代码(并行),用户友好的几何和色彩的相机和投影仪校准(GeometricCalibrator,ProCamGeometricCalibrator,ProCamColorCalibrator ),检测和特征点(ObjectFinder),一类是实现投影,摄像系统(直接图像对齐设置匹配主要GNImageAligner,ProjectiveTransformer,ProjectiveGainBiasTransformer,ProCamTransformer 和ReflectanceInitializer),以及在 JavaCV 类杂项功能
使用
导包
android { //防止程序打包时重复导包出错 packagingOptions { exclude 'META-INF/maven/org.bytedeco.javacpp-presets/opencv/pom.properties' exclude 'META-INF/maven/org.bytedeco.javacpp-presets/opencv/pom.xml' exclude 'META-INF/maven/org.bytedeco.javacpp-presets/ffmpeg/pom.properties' exclude 'META-INF/maven/org.bytedeco.javacpp-presets/ffmpeg/pom.xml' } }
//防止在各平台下开发导致找不到相应的jar configurations { all*.exclude group: 'org.bytedeco', module: 'javacpp-presets' }
dependencies { compile 'org.bytedeco.javacpp-presets:opencv:3.1.0-1.2:android-arm' compile 'org.bytedeco.javacpp-presets:opencv:3.1.0-1.2:android-x86' compile 'org.bytedeco.javacpp-presets:ffmpeg:3.0.2-1.2:android-arm' compile 'org.bytedeco.javacpp-presets:ffmpeg:3.0.2-1.2:android-x86' compile 'org.bytedeco:javacv:1.2' }
代码
FFmpegFrameRecorder 推流核心类
recorder = new FFmpegFrameRecorder(rtmpUrl, imageWidth, imageHeight, 1); //设置视频编码 28 指代h.264 recorder.setVideoCodec(28); //设置视频的封装容器 recorder.setFormat("flv"); //设置采样频率 recorder.setSampleRate(sampleAudioRateInHz); // 设置帧率,即每秒的图像数 recorder.setFrameRate(frameRate);
工具类
private void YUV420spRotate90(byte[] dst, byte[] src, int srcWidth, int srcHeight) { int nWidth = 0, nHeight = 0; int wh = 0; int uvHeight = 0; if (srcWidth != nWidth || srcHeight != nHeight) { nWidth = srcWidth; nHeight = srcHeight; wh = srcWidth * srcHeight; uvHeight = srcHeight >> 1; } //旋转Y int k = 0; for (int i = 0; i < srcWidth; i++) { int nPos = 0; for (int j = 0; j < srcHeight; j++) { dst[k] = src[nPos + i]; k++; nPos += srcWidth; } } for (int i = 0; i < srcWidth; i += 2) { int nPos = wh; for (int j = 0; j < uvHeight; j++) { dst[k] = src[nPos + i]; dst[k + 1] = src[nPos + i + 1]; k += 2; nPos += srcWidth; } } return; } //逆时针旋转90度: private void YUV420spRotateNegative90(byte[] dst, byte[] src, int srcWidth, int height) { int nWidth = 0, nHeight = 0; int wh = 0; int uvHeight = 0; if (srcWidth != nWidth || height != nHeight) { nWidth = srcWidth; nHeight = height; wh = srcWidth * height; uvHeight = height >> 1; } //旋转Y int k = 0; for (int i = 0; i < srcWidth; i++) { int nPos = srcWidth - 1; for (int j = 0; j < height; j++) { dst[k] = src[nPos - i]; k++; nPos += srcWidth; } } for (int i = 0; i < srcWidth; i += 2) { int nPos = wh + srcWidth - 1; for (int j = 0; j < uvHeight; j++) { dst[k] = src[nPos - i - 1]; dst[k + 1] = src[nPos - i]; k += 2; nPos += srcWidth; } } return; }
其他
Github地址:https://github.com/bytedeco/javacv
https://github.com/bytedeco/javacpp-presets
API:http://bytedeco.org/javacv/apidocs/org/bytedeco/javacv/package-summary.html
渲染播放
vlc 有多个平台的
ijkplayer
第三方SDK
腾讯直播:https://www.qcloud.com/doc/product/267
暴风直播
网易直播:http://vcloud.163.com/live.html
七牛直播:http://developer.qiniu.com/article/pili/product-detail.html
百度直播:http://www.rongcloud.cn/docs/
ucloud
https://docs.ucloud.cn/video/ulive/index.html
AnyRTC
https://github.com/AnyRTC/AnyRTC-RTMP(开源)
测试拉流地址
rtmp://live.hkstv.hk.lxdns.com/live/hks
视频播放
第43天
地图
百度
高德
第44天
即时通信
友盟
环信
融云
第45天
学生自由选择项目(讲师指导)
其他(选讲内容)
图表
HelloCharts
http://jcodecraeer.com/a/anzhuokaifa/androidkaifa/2014/1107/1930.html
MPAndroidChart
https://github.com/PhilJay/MPAndroidChart
http://jcodecraeer.com/a/anzhuokaifa/androidkaifa/2014/0506/1614.html
xUtils3.0
NoHttp
https://github.com/yanzhenjie/NoHttp
ExoPlayer
https://github.com/google/ExoPlayer
项目简单架构搭建
项目流程图
项目需求分析
项目产品开发计划书
总体方案
项目设计阶段
设计原型图
项目实施阶段
环境搭建
开发工具
编写代码
单元测试
项目测试阶段
集成、功能、 系统测试
新软件调优、新机 型适配支持
项目验收阶段
产品验收
功能开发
架构搭建篇
什么是架构
业界从来没有一个统一的定义,在企业应用架构模式中只提到了两点,1.最高层次的系统分解; 2.系统中不易改变的决定 从技术角度来说可以遵循以下原则
维护性
一个好的软件肯定是能方便维护的,出了问题能快速定位,需要修改时能快速修改,并且在一定程度上不会说一修改就一堆bug,如何做到可维护性可以从以下几点考虑
1.代码规范
一份代码如果没有遵循任何规范,那么我相信它的可维护性是很差的,就算是你一个人做出来的,估计过了几个月去修改的时候也会冒出一句这TM是什么鬼
2.框架稳定性
很多时候很多开源框架刚出来的时候,也许功能十分强大,但是毕竟刚出来,没有经过充分的测试,所以还是会或多或少存在一个不稳定因子,所以建议在选择框架时尽量选择成熟稳定的框架,哪怕功能和性能的确比不上刚出来的框架。当然这也不是说完全不用刚出来的框架,毕竟都不用,那么它也永远成熟不了,至于到底用不用和怎么用,本文的框架选择和使用篇也会详细分析说明
3.封装
本来想说AOP的,但是个人觉得很多专业性的名词也比不上一些通俗的形容,毕竟本文主要的目的是让人理解,不是提出理论。封装这个在Android中是经常使用的,简单来说就是把一些常用的、通用的东西进行一个封装,通过统一入口进行调用,这样出问题就只需要修改一个地方,就能全部修改过来。
4.耦合
如何解耦合,解耦合这个东西也要适度,不要因为解决一点耦合,花了大量的代码,浪费了大量的性能,所以解耦合这个东西就一定要把握这个度,过度设计是有问题的设计,这点我是赞同的。同时很多时候封装会导致一些耦合的问题,比如我曾经一个项目中就有个这个一个情况:
扩展性
扩展性简单来说就是当程序需要新的功能时,能否对其进行扩展以及扩展的难度来判断,如何提高扩展性,可以从以下几点考虑
1.抽象接口
这点相信大家都经常遇到,比如Android的点击事件,你想要实现什么样的点击效果,自己实现一个点击监听,然后设置给控件就可以了
2.元素重用
很多时候,很多功能模块可能使用到相同或者类似的元素,如何Android中的一些布局,这些如果抽取出公共部分在进行扩展的时候方便对其快速扩展,当然这个是项目一开始并不能预见的,所以需要在开发中不断的去重构
3.单一职责
这个跟其实就是面向对象的单一职责,比如前面提到的Dialog,一开始只有选择身高的功能,看起来是单一职责,但是其实相关处理也包含在其中,所以那个Dialog类本身职责已经不单纯,当然如果一直只有这一个,这样做没有任何问题,也能看做单一职责,但是如果有多个选择和处理的时候,就必须对其重构了
4.替换性
替换性包含里氏代换但是也不仅仅是里氏代换,比如常见的Android布局不同,但是其显示内容大致相同,这样写布局的时候就可以对相同内容的控件指定相同的id,这样就算替换布局,也不用重写ViewHolder,当然对于Adapter也仅仅只需要替换相应的布局就ok
5.耦合
同上
安全性
个人觉得数据安全性并不仅仅是数据安全,还有程序的一些操作安全性(简单来说就是避免程序出现一些非崩溃性异常)
1.数据安全性
数据安全就包括数据抓取、数据拦截以及数据修改。当然这些并不能完全避免,只能是由我们写出尽量安全的代码,比如关键数据使用HTTPS以及对数据进行md5验证完整性,对于数据修改,可以通过多文件多地址保存文件修改记录,来确定保存的数据是否被修改,毕竟Android只要获取root权限,就能对很多文件进行修改了
2.操作安全性
简单来说经常遇到的一个问题,比如按钮的点击事件,有可能这个点击事件是请求网络或者打开Activity,这样就会存在事件还未处理完成再次收到事件,只要你一直猛点,肯定可以的,所以这样就需要我们队控件事件进行一些封装,比如打开界面的,可以在点击后禁用按钮,界面打开完成后才启用,请求网络的可以在开始就禁用按钮,请求结果反馈了才启用(不管是请求成功或者失败)
切入性
切入性就是当另外一个从未接触过此项目的人,能快速进入这个项目进行开发,当然想要切入性好,前面的维护和扩展是必须要满足的,如何增加切入性的可以从以下几点考虑
1.文档
开发都不喜欢写文档,这是肯定的,但是每当我们去接手一个项目的时候,发现没有文档估计就要开始骂娘了,所以文档不仅要写,还要写的规范。我认为开发中必须要有的几个文档:代码规范文档(比如包名规范,文件命名规范,id命名规范等等,具体依据项目情况而定)、接口文档
2.注释
每个类必须要有注释,方法也要有注释,同时也要标注好方法最后修改人是谁,这样出现疑问或者问题别人就知道该去找谁了,当然有些方法是不需要有注释的,比如重写父类的方法,只是如果方法内逻辑很复杂,可以在方法中添加一些对逻辑的说明。当然注释也不是越多越好,具体注释该怎么写,Google最清楚,不能Google百度也行
3.包名
什么类放什么包简单,但是一但一个包中的类太多,也是非常不方便,所以正确的分包也是非常重要的,目前常见的Android分包包括针对功能分包(不是指程序功能,而是指UI,http,bean这些功能),还有就是模块分包(这就是程序的功能了,比如login,user等),当然具体怎么分包需要团队协商,防止一个包中类太多,一个程序很大的情况下,采用的分包是先功能分包,再模块分包
软件设计模式
MVC
M
获取数据
v
layout
c
控制层
activity
MVP
软件设计的目的
1. 易于维护
2. 易于测试
3. 松耦合度
4. 复用性高
5. 健壮稳定
MVP软件设计模式
1. 什么是MVP?
1.1. View层主要是用于展示数据并对用户行为做出反馈。在Android平台上,他可以对应为Activity, Fragment,View或者对话框。
1.2. Presenter层可以向View层提供来自数据访问层的数据,除此以外,他也会处理一些后台事务。 也就是说在mvp模式中,view只单纯的展示ui,所有的业务逻辑处理放在Presenter中(通过接口与Molde,View)交互。
1.3. Model是数据访问层,往往是数据库接口或者服务器的API。
2. 为什么采用mvp的架构?
2.1. 现在开发安卓人员都会遇到这种问题,那就是在一个逻辑非常的复杂的Activity中,代码量尝尝会达到数百甚至上千行,造成了activity既像view,但是某一定意义更像mvc的Controller。最终,你会发现“所有的事物都被连接到了一起” ,这样的结果就是造就“万能的对象”。
3. 四个要素
3.1. View :负责绘制UI元素、与用户进行交互(在Android中体现为Activity);
3.2. View interface :需要View实现的接口,View通过View interface与Presenter进行交互,降低耦合,方便进行单元测试;
3.3. Model :负责存储、检索、操纵数据(有时也实现一个Model interface用来降低耦合);
3.4. Presenter :作为View与Model交互的中间纽带,处理与用户交互的负责逻辑
4. 作用
1. view – UI显示层
1. 提供UI交互
2. 在presenter的控制下修改UI。
3. 将业务事件交由presenter处理。 注意. View层不存储数据,不与Model层交互。
2. presenter – 逻辑处理层
1. 对UI的各种业务事件进行相应处理。也许是与Model层交互,也许自己进行一些计算,也许控制后台Task,Service
2. 对各种订阅事件进行响应,修改UI。
3. 临时存储页面相关数据。注意. Presenter内不出现View引用。
3. model – 数据层
1. 从网络,数据库,文件,传感器,第三方等数据源读写数据。
2. 对外部的数据类型进行解析转换为APP内部数据交由上层处理。
3. 对数据的临时存储,管理,协调上层数据请求
5. MVP的优缺点
5.1. 缺点
5.1.1. 额外的代码复杂度及学习成本
5.2. 优点
5.2.1. 降低耦合度
5.2.2. 模块职责划分明显
5.2.3. 利于测试驱动开发
5.2.4. 代码复用
5.2.5. 隐藏数据
5.2.6. 代码灵活性
6. 应用结构图
6.1.
项目使用注意实现
Presenter持有Activity对象可能导致的内存泄漏问题
参考资料
http://www.jianshu.com/p/b49958e1889d(T-MVP)
MVVM
databinding
通用框架
功能
SplashActivity
App一般都有一个启动界面,称为Splash界面
作用
启动界面的本意是以友好用户界面来掩饰后台缓冲加载,让用户用平和等待的心情进入正常应用界面.但是因为启动界面是放在开始,在这个特殊的点上,可以做更多的事情,如应用宣传,展示广告等等.所以,这个启动界面的元素,可多可少,就看你的用意和需求.例如提供额外信息,如市场专版,独家首发等
传统静态页面
适合用户量比较小的应用
实现方式
见案例
首页广告
用户量比较大或者免费的应用
实现方式
见案例
http://www.jianshu.com/p/c7201d00b7bf
问题
只显示一次启动页
打开应用之后用户按返回按钮例如微信
@Override public void onBackPressed() { // super.onBackPressed(); 不要调用父类的方法 Intent intent = new Intent(Intent.ACTION_MAIN); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.addCategory(Intent.CATEGORY_HOME); startActivity(intent); }
全屏切换非全屏问题
一般这个启动界面都是做成全屏的,即隐藏掉系统状态栏,然后再跳转到主界面。由于主界面是非全屏的,保留着状态栏,这个过程就发生了从全屏界面切换到非全屏界面。但是在页面切换过程中,会出现先显示页面内容,然后由于状态栏的出现,整个界面下移一个状态栏高度的问题
效果图
/Users/zhangwei/Desktop/新课件相关/课程/项目篇/图片/splash效果图片
解决方案
//必须在设置setContentView之前 private void smoothSwitchScreen() { // 5.0以上修复了此bug if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { ViewGroup rootView = ((ViewGroup) this.findViewById(android.R.id.content)); int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android"); int statusBarHeight = getResources().getDimensionPixelSize(resourceId); rootView.setPadding(0, statusBarHeight, 0, 0); getWindow().addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN); getWindow().addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); } }
启动页面留白
应用在启动时, 系统会加载数据, 需要启动完成Application的onCreate方法, Activity的onCreate与onResume方法, 会产生短暂的留白, 不可避免.也可能是黑色, 与应用的主题有关, light是白色, dark是黑色.一些主流应用都会解决这一问题, 如微信\QQ\网易云音乐等, 但某些应用可能并未注意. 去掉留白需要修改启动页面的主题样式, 设置windowBackground属性, 默认是白色, 可以修改为其他颜色或展示Logo图片
第三方案例
https://github.com/Ramotion/paper-onboarding-android
https://github.com/w446108264/XhsWelcomeAnim
https://github.com/Ramotion/paper-onboarding-android
https://github.com/Cleveroad/slidingtutorial-android
GuideActivity
素材
http://huaban.com/explore/yindaoye/
开源库
https://github.com/ongakuer/CircleIndicator
https://github.com/stephentuso/welcome-android
https://github.com/PaoloRotolo/AppIntro(推荐)
https://github.com/Cleveroad/slidingtutorial-android
MainActivity
用户指引view
参考资料
https://www.google.com/design/spec/components/bottom-navigation.html(翻墙)谷歌底部设计规范
主界面设计
移动端常见导航设计总结
标签式导航
简介
即我们说的tab式导航,是移动应用中最普遍、最常用的导航模式,适合在相关的几类信息中间频繁的调转。这类信息优先级较高、用户使用频繁,彼此之间相互独立,通过标签式导航的引导,用户可以迅速的实现页面之间的切换且不会迷失方向,简单而高效。需要注意的是标签式导航根据逻辑和重要性控制在5个以内,多余5个用户难以记忆而且容易迷失
分类
底部tab式导航
底部tab式导航(QQ、微信、淘宝、微博、美团、京东)
底部tab式导航(QQ、微信、淘宝、微博、美团、京东)
这是符合拇指热区的导航方式。那啥叫拇指热区呢?当你走在路上、单手持握手机并操作;站在公交车上,一手拉扶手,另一只手操作等等这些场景时,你最常用的操作就是右手单手持握并操作手机,因此,对于手机来说,为触摸进行交互设计,主要针对的就是拇指。 但在手机操作中,拇指的可控范围有限,缺乏灵活度。尤其是在如今的大屏手机上,拇指的可控范围还不到整个屏幕的三分之一——主要集中在屏幕底部、与拇指相对的另外一边。当用右手持握手机的时候(左撇子毕竟是少数,我们还是要为主流用户设计,拇指的热区如下图所示)(红色:拇指点击盲区;橙色:拇指能触及的区域需用史上吃奶的劲;绿色:拇指点击的最佳区域)
顶部tab式导航
QQ音乐、酷我音乐、新闻类app
底部tab的扩展导航
抽屉式导航(侧导航)
适用的场景
1 分支类目超过3个,这种导航多少都能装的下
2 某一类目的层级较深, 比如Facebook的news feed ,
3 用户使用时,某一类要频繁访问,使用频率明显超过其他
优劣势
劣势 有部分类目,当前页面不可见,需要用户寻找,增加了认知成本。
优势
可以容纳多个分支类目,隐藏多余的类目,使当前页面简洁
类目之间的切换成本较高
案例展示
宫格式导航(常见但不常用)
使用场景
1 这种导航适用于几个功能没有交叉的应用,
2 功能较多,较分散。
优缺点
优势:类目清晰,容纳很多类目无压力。
劣势: 首屏没有内容,只有入口。
案例展示
复合导航
应用场景
典型的就是新版QQ的导航,底部Tab和抽屉导航共用,抽屉部分放置不常用的个人各项信息,主体的应用采用底部Tab导航模式。 支付包的客户端,大框架采用底部Tab的模式,Tab支付宝项,采用九宫格式的模块设计,放置了很多功能模块,入口非常清晰。 和支付宝类似的,还有一些旅游类应用,去哪儿,携程等。 三种导航共存,比如美团的客户端,底部Tab做大框架导航,团购的类目中,有九宫格式的入口,也有list模式展示比较详细的信息
案例展示
总结每一种导航都有自己使用的范围,看了很多应用的设计,没必要只局限于一种导航模式。根据每一种应用要呈现和表达的内容选择。在每一个模块选用合适的呈现方式
主界面常用Tab总结
FragmentNavigator
https://github.com/Aspsine/FragmentNavigator
BottomNavigation
底部导航栏使用
导入依赖工程
官方网站
https://github.com/krpiotrek/BottomNavigation
TabLayout
系统自带
FragmentTabhost
Dagger2
简介
它就是解决Android或java中依赖注入的一个类库(DI类库)
基础概念
依赖关系
当一个对象需要或依靠其他对象处理工作时,这就是一个依赖关系。依赖关系可以通过让依赖对象创建被依赖者或者使用工厂方法创建一个被依赖者来解决。在依赖注入的情况下,然而,向类提供的依赖需要避免类来创建它们。这样你创建的软件就是松耦合和高可维护性的
依赖注入
就是目标类(目标类需要进行依赖初始化的类,下面都会用目标类一词来指代)中所依赖的其他的类的初始化过程,不是通过手动编码的方式创建,而是通过技术手段可以把其他的类的已经初始化好的实例自动注入到目标类中。
注解
JSR-330?
为了最大程度的提高代码的复用性、测试性和维护性,java的依赖注入为注入类中的使用定义了一整套注解(和接口)标准。Dagger1和Dagger2(还有Guice)都是基于这套标准,给程序带来了稳定性和标准的依赖注入方法
android-apt
配合Android studio处理注解的处理器
作用
只允许配置编译时注解处理器依赖,不包括打包时的apk和依赖库
设置源路径,检测生成的代码注解处理器的正确性
优点
1、依赖的注入和配置独立于组件之外,注入的对象在一个独立、不耦合的地方初始化,这样在改变注入对象时,我们只需要修改对象的实现方法,而不用大改代码库。
2、依赖可以注入到一个组件中:我们可以注入这些依赖的模拟实现,这样使得测试更加简单。
3、app中的组件不需要知道有关实例创建和生命周期的任何事情,这些由我们的依赖注入框架管理的。
使用
Dagger2带来的好处
增加开发效率、省去重复的代码
首先new一个实例的过程是一个重复的简单体力劳动,dagger2完全可以把new一个实例的工作做了,因此我们把主要精力集中在关键业务上、同时也能增加开发效率上。省去写单例的方法,并且也不需要担心自己写的单例方法是否线程安全,自己写的单例是懒汉模式还是饿汉模式。因为dagger2都可以把这些工作做了。
更好的管理类实例
每个app中的ApplicationComponent管理整个app的全局类实例,所有的全局类实例都统一交给ApplicationComponent管理,并且它们的生命周期与app的生命周期一样。每个页面对应自己的Component,页面Component管理着自己页面所依赖的所有类实例。因为Component,Module,整个app的类实例结构变的很清晰。
解耦
假如不用dagger2的话,一个类的new代码是非常可能充斥在app的多个类中的,假如该类的构造函数发生变化,那这些涉及到的类都得进行修改。设计模式中提倡把容易变化的部分封装起来
配置
配置工程的build.gradle
引入依赖库
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'//目前最新版本是1.8
注意不可以使用classpath 'com.neenbedankt.gradle.plugins:android-apt:1.+'
配置moudle下的build.gradle
添加apt插件
apply plugin:android-apt 或 apply plugin: 'com.neenbedankt.android-apt'
添加相关依赖库
compile 'com.google.dagger:dagger:2.+'
apt 'com.google.dagger:dagger-compiler:2.+'
dagger-compiler用于代码生成
注意只能用apt开头:因为注入的时候你需要引用一个dagger2编译时生成的Component class,然而这class你根本引用不到,经过搜索发现,dagger2的compiler不能用provided依赖,需要使用android-apt的插件,用apt关键字依赖
provided 'org.glassfish:javax.annotation:10.0-b28'
java注解库,用于Dagger之外需求的额外注解
dagger2会在编译的时候生成一些.java文件,里面会有个@Generated注解,这个注解是javax.annotation包中的
gradle版本2.2
compile 'com.google.dagger:dagger:2.x'
annotationProcessor 'com.google.dagger:dagger-compiler:2.x'
API详细使用
@Inject
通常在需要依赖的地方使用这个注解。换句话说,你用它告诉Dagger这个类或者字段需要依赖注入。这样,Dagger就会构造一个这个类的实例并满足他们的依赖。
作用域
Constructor Injection(构造方法注入)
在Constructor加上@Inject
表示Constructor的参数需要dependency
这些参数可以被使用在privte或final字段
在methods上加上@Inject
在methods上加上@Inject
表示method的参数需要dependency
injection发生在对象被完全建立之后
在fields上加上@Inject
在fields上加上@Inject
field不能为private或是final
injection发生在对象完全建立之后
在anroid最常用到
示例代码
在Constructor上使用
在fields上使用
在methods上使用
@Inject并不是在任何情况下都可以:
接口类型不能被构造
第三方的类不能被注释构造。
可配置的对象必须被配置好
@Module
Modules类里面的方法专门提供依赖,所以我们定义一个类,用@Module注解,这样Dagger在构造类的实例的时候,就知道从哪里去找到需要的 依赖。modules的一个重要特征是它们设计为分区并组合在一起(比如说,在我们的app中可以有多个组成在一起的modules)。
示例代码
@Provides
在@Module 中使用,我们定义的方法用这个注解,用于告诉 Dagger 我们需要构造实例并提供依赖
为什么要使用@Provides?
因为默认情况下,Dagger满足依赖关系是通过调用构造方法得到的实例,比如上面的Person类使用。但是有时因为@Inject 的局限性,我们不能使用@Inject。比如构造方法有多个、三方库中的类我们不知道他是怎么实现的等等
@Component
Components从根本上来说就是一个注入器,也可以说是@Inject和@Module的桥梁,它的主要作用就是连接这两个部分
示例代码
难点
如果整个应用程序中只有一个Component会很难去维护,而且实际开发中需求是不会一成不变的,会导致该类不停的修改,
如何划分
全局的Component
负责管理整个app的全局类实例,而且是单例模式
每个页面对应一个Component
比如一个Activity页面定义一个Component,一个Fragment定义一个Component。当然这不是必须的,有些页面之间的依赖的类是一样的,可以公用一个Component
说明
一个App有很多功能模块组成,而一个功能模块又由很多页面组成,一个页面又由很多类组成,每个页面都组织着自己的需要依赖的类,一个页面就是一堆类的组织者,如果我们定义过多的Component将会会造成管理、维护就很非常困难
如何组织
依赖方式
一个Component是依赖于一个或多个Component,Component中的dependencies属性就是依赖方式的具体实现
包含方式
一个Component是包含一个或多个Component的,被包含的Component还可以继续包含其他的Component。这种方式特别像Activity与Fragment的关系。SubComponent就是包含方式的具体实现。
继承方式
官网没有提到该方式,具体没有提到的原因我觉得应该是,该方式不是解决类实例共享的问题,而是从更好的管理、维护Component的角度,把一些Component共有的方法抽象到一个父类中,然后子Component继承。
效果图
@Qualifier @Name
有些时候,单纯类型(指这些基本的@Inject....等等)是不能够满足指定依赖的需求的。在这种情况下,我们可以添加限定符注释. 这种注释本身有一个@Qualifier注释
@Scope
自定义Scope注解,来限定通过Module和Inject方式创建的类的实例的生命周期能够与目标类的生命周期相同。或者可以这样理解:通过自定义Scope注解可以更好的管理创建的类实例的生命周期。
其他重要类
Lazy(懒加载)
等到调用的时候才注入
Provider(提供者注入)
有些情况下, 你需要多个对象实例, 而不是仅仅注入一个对象实例。这时你可以利用Provider实现, 每次调用Provider的get()函数将返回新的<T>的对象实例
工作流程
1. 识别依赖对象和它的依赖。
2. 创建带@Module注解的类,对每个返回依赖的方法使用@Provides注解。
3. 在依赖对象上使用@Inject注解来请求依赖。
4. 使用@Componet注解创建一个接口并增加第二步中创建的带@Module注解的类。
5. 创建一个@Component接口的对象来实例化自带依赖的依赖对象
注解API
@Inject: 通常在需要依赖的地方使用这个注解。换句话说,你用它告诉Dagger这个类或者字段需要依赖注入。这样,Dagger就会构造一个这个类的实例并满足他们的依赖。
@Module: Modules类里面的方法专门提供依赖,所以我们定义一个类,用@Module注解,这样Dagger在构造类的实例的时候,就知道从哪里去找到需要的 依赖。modules的一个重要特征是它们设计为分区并组合在一起(比如说,在我们的app中可以有多个组成在一起的modules)。
@Provide: 在modules中,我们定义的方法是用这个注解,以此来告诉Dagger我们想要构造对象并提供这些依赖。
@Component: Components从根本上来说就是一个注入器,也可以说是@Inject和@Module的桥梁,它的主要作用就是连接这两个部分。
Components可以提供所有定义了的类型的实例,比如:我们必须用@Component注解一个接口然后列出所有的@Modules组成该组件,如果缺失了任何一块都会在编译的时候报错。所有的组件都可以通过它的modules知道依赖的范围。
@Scope: Scopes可是非常的有用,Dagger2可以通过自定义注解限定注解作用域
这是一个非常强大的特点,因为就如前面说的一样,没必要让每个对象都去了解如何管理他们的实例。在scope的例子中,我们用自定义的@PerActivity注解一个类,所以这个对象存活时间就和 activity的一样。简单来说就是我们可以定义所有范围的粒度(@PerFragment, @PerUser, 等等)。
Qualifier: 当类的类型不足以鉴别一个依赖的时候,我们就可以使用这个注解标示。
例如:在Android中,我们会需要不同类型的context,所以我们就可以定义 qualifier注解“@ForApplication”和“@ForActivity”,这样当注入一个context的时候,我们就可以告诉 Dagger我们想要哪种类型的context
其他
github地址:https://github.com/google/dagger
api文档:http://google.github.io/dagger/api/2.0/
stackoverflow
http://stackoverflow.com/questions/tagged/dagger-2?page=1&sort=newest&pagesize=15
常见错误
如果在项目中同时用了Butterknife, 在Build时会报注释冲突
解决方法:(在module下build.gradle文件中添加如下代码)
packagingOptions { exclude 'META-INF/services/javax.annotation.processing.Processor' }
NDK入门开发
DataBinder
配置
必备条件
Android Studio 为 1.3.0以上才能使用 ,Android SDK manager 来下载最新的 Support repository
确保 Android 的 Gradle 插件版本不低于 1.5.0-alpha1:
在module下 android{}标签中添加
android { dataBinding { enabled true } }
使用
首先
正常创建工程如果要使用 data binder,xml的布局文件就不再单纯地展示 UI 元素,还需要定义 UI 元素用到的变量。所以,它的根节点不再是一个 ViewGroup,而是变成了 layout,并且新增了一个节点 data
示例代码
<layout xmlns:android="http://schemas.android.com/apk/res/android"> <data> </data> <!--原先的根节点--> <LinearLayout> .... </LinearLayout> </layout>
其次
创建POJO(Plain Ordinary Java Object)
简单的Java对象
public class User { private String name; private String address; public User() { } public User(String name, String address) { this.name = name; this.address = address; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
定义 Variable
也支持
说明
type 属性就是我们在 Java 文件中定义的 User 类
name相当于user的实例化对象
绑定 Variable
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main); User user = new User(); user.setName("呵呵"); user.setPwd("1111"); binding.setUser(user);
自定义类名
Layout文件其他属性
Import
零个或多个import元素可能在data元素中使用。这些只用在你的layout文件中添加引用,就像在Java中
示例代码
设置别名(alias)
类名有冲突时,其中一个类名可以重命名为alias
表达式中使用static属性和方法
Variables
在data中可以使用任意数量的variable元素。每一个variable元素描述了一个用于layout文件中Binding表达式的属性
示例代码
自定义 Binding 类名称
默认情况下,Binding类的命名是基于所述layout文件的名称,用大写开头,除去下划线(_)以及(_)后的第一个字母大写,然后添加“Binding”后缀。这个类将被放置在一个模块封装包里的databinding封装包下。例如,所述layout文件activity_main.xml将生成ActivityMainBinding。如果模块包是com.yz.app.databinder,那么它将被放置在com.yz.app.databinder.databinding
如何设置?Binding类可通过调整data元素中的class属性来重命名或放置在不同的包中
xml文件中代码
<data class="MainBinding"> ... </data>
java代码
//在模块封装包的databinding包中会生成名为MainBinding的Binding类。 MainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
果要想让该类生成在不同的包种可以使用
//项目主包下面 <data class="<data class=".MainBinding"> ... </data>"> ... </data>
//生成到指定的包下 <data class="com.yz.app.MainBinding"> ... </data>
Includes
通过使用application namespace以及在属性中的Variable名字从容器layout中传递Variables到一个被包含的layout:
示例代码
includes_layout.xml
activity_includes.xml
表达式
常用表达式跟Java表达式很像
数学 + - / * %
字符串连接 +
逻辑 && ||
二进制 & | ^
一元运算 + - ! ~
移位 >> >>> <<
比较 == > < >= <=
instanceof
分组 ()
null
Cast
方法调用
数据访问 []
三元运算 ?:
示例代码
android:text="@{String.valueOf(index ++)}"
android:visibility="@{user.login ? View.VISIBLE: View.GONE}"
Null合并操作
?? - 左边的对象如果它不是null,选择左边的对象;或者如果它是null,选择右边的对象:
android:text="@{user.name ?? user.pwd }"
避免 NullPointerException
Data Binding代码生成时自动检查是否为nulls来避免出现null pointer exceptions错误。例如,在表达式@{user.name}中,如果user是null,user.name会赋予它的默认值(null)。如果你引用user.age,age是int类型,那么它的默认值是0。
滑动播放VideoListPlayer
https://github.com/waynell/VideoListPlayer
蓝牙
传感器
Wifi
VR
虚拟现实(Virtual Reality),就是利用电脑模拟产生一个三维空间的虚拟世界,提供用户关于视觉等感官的模拟,让用户如同身临其境一般,可以及时、没有限制地观察三维空间内的物体。当用户进行位置移动时,电脑可以立即进行复杂的运现在的大部分VR技术都是视觉体验,一般是通过电脑屏幕、特殊显示设备或立体显示设备获得的。在一些高级的触觉系统中还包含了触觉信息,也叫作力反馈算,将精确的三维世界视频传回产生临场感。
官方案例
Demo地址
https://github.com/googlevr/gvr-android-sdk
案例介绍
samples目录中有四个Demo
1. SDK-controllerclient(Daydream的控制端)
2. SDK-simplepanowidget(全景图)
3. SDK-simplevideowidget(全景视频 也就是VR视频)
4. SDK-treasurehunt(寻宝项目)这个是我们想要的,其他暂时不要管
OpenGL
程序更新
重要概念
动态加载
在程序运行的时候,加载一些程序自身原本不存在的可执行文件并运行这些文件里的代码逻辑。
增量更新
做更新的时候更新部分,将旧版本的apk和差异包合并成为一个新的apk,引导客户安装
优缺点
优点
节省流量,加快更新速度
缺点
针对不同版本要生成不同的差异包
不安装官方版本无法做更新
开源项目
https://github.com/cundong/SmartAppUpdates
热修复
修改了小部分代码,不用重新发包,在用户不知情的情况下,给app打补丁,让app按照补丁的逻辑运行
插件化
它可以在无需安装、修改的情况下运行APK文件,此机制对改进大型APP的架构,实现多团队协作开发具有一定的好处
拆分apk,形成宿主+寄生关系
开源项目
DynamicLoadApk(携程)
Atlas(阿里)
DroidPlugin(360)
组件化
把常用的模块代码,抽取lib工程或者jar达到复用的效果
MultiDex
dex内方法数目超过65536(short长度范围-32768~32767),会提示错误,就需要拆分dex
定义
用户不用重新下载一个新的apk安装,而是直接下载一个补丁包,通过补丁来替换一些出现bug的类, 当然下载补丁的过程用户一般是感觉不到的,表面上看是直接修复了bug。 在早期的android系统中,为了优化dex,所有的method会存放在一张表里面,表的大小位short,也就是65535(65K)现在android代码非常多,超过65K很正常,这个时候就需要一种解决方案来解决这个问题。简单来说就是将编译好的class文件分拆成2个dex文件,绕过65k的限制
原理
什么是Dex
简单说就是优化后的android版.exe。每个apk安装包里都有。相对于PC上的java虚拟机能运行.class;android上的Davlik虚拟机能运行.dex
热修复就是利用android中的 DexClassLoader 类加载器,动态加载补丁dex,替换有bug的类
热门的开源框架
AndFix
全称是Android hot-fix。是阿里开源的一个热补丁框架,允许APP在不重新发布版本的情况下修复线上的bug。支持Android 2.3 到 6.0,并且支持arm 与 X86系统架构的设备。完美支持Dalvik与ART的Runtime,补丁文件是以 .apatch 结尾的文件
Nuwa
dexposed
参考资料
http://www.jianshu.com/p/2c80234a43c4