-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcontent.json
1 lines (1 loc) · 93.5 KB
/
content.json
1
[{"title":"Kotlin coroutines on Android","date":"2021-02-19T07:19:42.000Z","path":"2021/02/19/Kotlin-coroutines-on-Android/","text":"What is coroutines?维基百科的定义维基百科上对协程的定义: 1协程是计算机程序的一类组件,推广了协作式多任务的子程序,允许执行被挂起与被恢复。 从线程说起 我们都知道线程是抢占式的,你可以给线程设置优先级,但是 CPU 实际执行的是哪个线程,我们并不能控制,只能通过锁的方式来保证程序的逻辑正确。 协程是协作式的,是编程语言层级的,而非系统层级。当我们切换协程时,不涉及任何系统调用,因此可以把协程看作是轻量的线程。 Kotlin 中的协程Kotlin 语言从1.3开始支持协程。Kotlin 中的协程在概念上与其他语言类似,但并没有像其他语言提供 async 、await 关键字,而是以扩展库的形式存在,其中最重要的就是挂起函数(可中断函数),即函数可以在某个时候暂停,并在未来某个时候恢复。 挂起函数代码示例: 挂起函数使用 suspend 关键字来修饰,相比于常规函数,挂起函数多了两个操作,suspend 和 resume。 suspend:挂起(暂停),暂停当前协程,并保存所有的局部变量。 resume:让暂停的协程从暂停处继续执行。 协程提供了一种全新的处理异步任务的方式。使得我们可以像写同步代码那样去写异步代码,减少了回调地狱,代码的可读性也得到了增强。同时协程还内置了取消操作,这对于 Android 开发者来说无疑是个福音。 How do I use coroutines?Step 1:引入协程依赖库1implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2' Step2:启动一个协程12345678910println("Start")// Start a coroutineGlobalScope.launch { delay(1000) println("Hello")}Thread.sleep(2000) // wait for 2 secondsprintln("Stop") 可中断函数只允许在协程或者另一个可中断函数中调用。 Step3:使用 async 启动协程12345val deferred = GlobalScope.async { "Hello" }val deferred2 = GlobalScope.async { "World" }runBlocking { println(deferred.await() + deferred2.await())} Step4:使用 Dispather 来进行线程调度首先我们需要清楚一点:协程可以在主线程中运行,suspend 函数并不代表后台执行。 那么协程是在哪个线程中运行呢? 123GlobalScope.launch { Log.d(TAG, "thread = ${Thread.currentThread().name}")} 协程通过调度器(Dispather)来实现线程的调度,目前 Kotlin 提供了下面三个调度器来给我们使用: 线程调度代码示例: 小思考: printThreadName() 函数运行在哪了线程呢,是 IO 线程还是 Main 线程? Step5:理解和使用协程作用域从上面的代码中我们可以看到协程的启动需要一个作用域,这在 Android 开发中是非常有用且必要的。当 Activity 或者 Fragment 销毁时,我们可以通过与该 Activity 或 Fragment 绑定的作用域来取消协程,从而避免产生内存泄漏。 常用的作用域一览表: Scope 描述 使用场景 GlobalScope 全局作用域 基本不使用,除非协程是全局的 viewModelScope 与 viewModel 绑定的作用域 App 内大部分场景 lifecycleScope 与 Lifecycle 对象绑定的作用域,如 Activity 或 Fragment 与生命周期相关的一些场景,使用 Fragment 时,需要区分 lifecycleScope 和 viewLifecycleOwner.lifecycleScope 自己创建一个新的 scope: Step6:取消协程当启动多个协程时,我们可以通过取消启动该协程的作用域来取消协程,而不需要单独管理每个协程。 有的时候我们并不想取消该作用域下的所有协程,只想取消某一个,应该怎么做? 协程构造器会返回一个**Job** 对象,我们可以通过该 Job 来管理协程的生命周期,当然也包含了取消操作。通过 Job 取消的方式不会对兄弟协程产生任何影响。 Step7:协程中的异常处理Kotlin 中的异常在学习协程的异常处理之前,我们先来看一下 Kotlin 中的异常。不知道大家有没有使用 String.toInt() 函数。我们先来看一下这个扩展函数的实现: 直接调用了 Java 中的 parseInt 方法,这就很有意思了。 我们可以看到 parseInt 方法是会抛出异常的,这很容易理解,不是所有的 string 对象都是整形的数字。Java 中通过抛出异常来处理这种情况。 但是在我们在 Kotlin 调用 String.toInt 时,IDE 并没有提示我们捕获异常,这是为什么?原因很简单,Kotlin 中没有受检查的异常(Checked Exceptions)。碰到这种情况,需要我们自己捕获异常,否则就会 Crash。这可能也是大部分人诟病 Kotlin 的一点。关于 Checked Exceptions 的设计好坏与否,这里不做讨论,有兴趣的同学可以参考这篇文章。 如果协程发生了异常当一个协程由于异常运行失败,它会传播该异常到它的父级,随后它的父级会取消所有相关的子协程,并继续向上传递。 这个行为看上去是比较合理的,但有的时候我们并不希望这么做。假如我们有一个和 UI 相关的 Scope,这个 scope 的一个子协程抛出了异常,导致整个 scope 被取消,由于取消后的作用域无法启动新的协程,这显然是不能被接受的。 怎么解决这个问题呢?只需要在创建 scope 时,使用 SupervisorJob 而不是 Job。 当使用 SupervisorJob 时,一个子协程抛出了异常,并不会取消它的父级和兄弟级,也不会传播异常,需要子协程自己处理异常,如果异常没有处理,会传递到 CoroutineContext 中的 CoroutineExceptionHandler 中,如果 CoroutineExceptionHandler 也没有设置,那么会直接 Crash。 怎么处理异常使用 try/cath 处理 Why should I use coroutines? 轻量级:可以同时启动成百上千个协程,但却不会阻塞主线程 更少的内存泄漏:使用结构化的并发在同一作用域内执行多个任务 内置取消操作:方便在页面销毁时释放资源,减少内存泄漏 Jetpack 支持:Room、Paging 等官方支持库全面支持协程 Coroutines ❤️️ View大部分情况下,我们都会使用协程来处理多线程问题,比如网络、数据库等 IO 任务。其实协程也可以处理单线程的异步任务。单线程的异步任务听上去很陌生,但是你肯定用过它。Android 中最典型的单线程任务就是与 View 相关的一些操作,比如 View 的绘制和动画 。 Jetpack 中提供了很多扩展函数来帮我们简化代码,如 View.doOnPreDraw(),Animator.doOnEnd()这些扩展函数只是帮我们将老的回调方式转化成了 Kotlin 友好的 lambda API,虽然看上去很优雅,但是并没有解决嵌套回调的问题。 suspendCancellableCoroutinesuspendCancellableCoroutine 可以从两个维度来执行协程的取消操作 在异步操作完成之前取消协程 在协程被挂起的时候,异步 UI 操作被取消或抛出异常 等待 View 布局完成 等待动画执行完成 实战演练可能你会说这有什么用,不就是换了一种写法吗,不用协程也可以实现。是的,你确实可以实现,但碰到下面这种情况你就会觉得有用了。 需求:动画A和动画B同时执行,等两个动画都执行完成后,再执行动画C,动画C结束后再重复上述过程。 我们来看下不使用协程该怎么实现: 使用协程怎么实现: 是不是使用协程更加的便捷和方便呢? QA: 我能在 Java 代码中使用协程吗? 协程稳定吗,可以在生产环境中使用吗? 协程难不难,学习成本是不是很大?","tags":[]},{"title":"从 LiveData 到流","date":"2020-05-26T06:39:09.000Z","path":"2020/05/26/from-livedata-to-stream/","text":"什么是 LiveData? 一个具有生命周期感知能力的可观察的数据存储类。 生命周期感知能力 可观察 数据存储 Activity 生命周期的状态和事件 LiveData 只会将更新通知给处于活跃状态的观察者,活跃状态指观察者的生命周期在STARTED或者 RESUMED。当观察者处于 DESTROYED时会自动移除该观察者。 为什么使用 LiveData? 无需手动处理生命周期 不会发生内存泄漏 始终保持页面数据为最新 页面间共享数据 官方出品,与 Room 配合食用更佳 LiveData 什么时候会通知更新 数据发生改变时 组件从非活跃状态更改为活跃状态 第二次从非活跃状态更改为活跃状态时不会更新 从更新的时机来看,我们需要在组件的 onCreate 方法中执行观察方法,确保数据可以通知。比起在 onResume 方法中手动更新数据更加的高效。 LiveData 中的操作符 map: 与 RxJava 中的 map 类似 switchMap: 与 RxJava 中的 flatMap 类似 distinctUntilChanged: 与 RxJava 中的 distinctUntilChanged 类似,只有当数据本身发生改变时才通知下游。 MediatorLiveData: 当上述操作符无法满足需求时,可使用 MediatorLiveData 来自定义操作符。 LiveData 的错误用法 使用 Fragment 作为 LifecycleOwner 传入 observe 方法 在 onCreate 方法中获取网络数据❎ 错误的做法:如果 Activity 重建,会重新去请求一次数据,这是没有必要的。✅ 只在需要更新数据时,才去请求网络数据。 暴露可变的 LiveData 对象给外部 为什么项目中会有 Event 和 _EventObserver_? 从前面 LiveData 的更新时机我们不难发现,observe 方法会回调多次。但是对于 toast、startActivity、showDialog 等事件我们只会消费一次。 Event和EventObserver保证了事件只会被处理一次。 是不是任何场景下都可以使用 LiveData? 当然不是,只要当数据与 UI 有相关关系时才应该使用 LiveData。 讨论:对于一些入口是否显示的控制应该使用 LiveData 吗 LiveData 与 RxJava虽然 LiveData 和 RxJava 都属于观察者模式的实现,但是他们的本质是完全不同的。 LiveData 只是数据的储存类,本身不支持复杂的线程操作和事件序列,也没有异常处理。与 RxJava 相比,具有体积小,学习成本低等优点。 RxJava 是一个完整的基于事件的观察序列,用来处理异步操作的框架。使用 RxJava 可以方便的切换线程,同时 RxJava 拥有的众多操作符也可以让逻辑变的清晰可循。配合 Retrofit 大大地简化了网络操作。但是学习曲线较陡,大部分人只会一些简单的操作。 在 Android 中可以使用 https://github.com/uber/AutoDispose 来避免 RxJava 存在的内存泄漏。 LiveData 与 EventBusEventBus 也是一种观察者模式的实现,但她更多的被应用于模块间的通信,比如一些全局性的事件通知,类似登录这样的。而不是像 LiveData 一样只关心当前页面的数据。 Cold or Hotcold observable 只有当事件被订阅时才会执行发射数据的代码。当有多个订阅者时,他们之间的事件是相互独立的。 hot observable 事件的发生于订阅的时机无关,多个订阅者可以订阅同一数据源。 举个形象一些的 🌰,cold 是耳机,hot 是音响 LIveData 是 Hot observable,RxJava 大部分情况下是 Cold observable。 关于 Cold 和 Hot 的理解,可以参考这篇文章: https://medium.com/@benlesh/hot-vs-cold-observables-f8094ed53339 subscribe 和 observe","tags":[]},{"title":"Android 项目编译速度优化","date":"2019-07-22T12:44:27.000Z","path":"2019/07/22/Android-build-speed-opt/","text":"Android 编译这件小事什么是编译?我们每天都要经历数十次的编译,那么什么是编译呢?简单来说,就是把高级语言转化为机器或虚拟机能识别的低级语言的过程。在 Android 中,就是把我们认识的 Java 或 Kotlin 语言转化为 Android 虚拟机可以执行的 Dalvik 字节码的过程。 Android 中的编译过程 编译器将源代码转换为Dex文件,其他内容转化为已编译资源 打包器把Dex文件和已编译资源合成 apk。 打包器使用签名签署 apk。 打包器使用 zipalign 工具进行优化。编译速度 大致了解了编译过程之后,我们最关心的应该就是编译速度了。编译速度的快慢直接决定了研发效率的高低,尤其是多人协作的项目。每个人节省1分钟,5个人就是5分钟,每天平均编译20次,那就是100分钟,1小时40分钟。这么看来,按时下班也有了希望。 关于编译速度,我们最关心的应该是编译 debug 包的速度,这直接影响我们的开发效率。从我们写完代码到安装到手机上生效,其中包含了两个时间,一个是编译时间,一个是安装时间。编译时间中,我们最关心的应该是增量编译(incremental build)的时间,开发阶段大部分的编译均属于此类。那么如何提高增量编译的速度才是核心。至于安装时间,可以使用好一点的测试机和升级新的工具链解决。 编译速度的优化 硬件加速,使用配置高的电脑进行开发工作。 保持 Android SDK Tools、Android Gradle Plugin 插件 为最新版本,享受 Google 的最新优化成果。 项目模块化。模块化的项目可以享受到 Gradle 并行编译的好处。 启用 Instant Run。作为 Google 官方的加速增量编译的方案,虽然一直被人诟病,但一些情况下还是可以提升编译速度的。 在开发阶段禁用某些 SDK,比如 Crashlytics。 在开发阶段使用静态的编译配置参数。例如我们项目中基本都存在 BuildConfig BUILD_TIME 参数,在 debug 环境下可直接写成0。 设置 minSdkVersion 为 21。根据各产品需求而定,minSdkVersion 为16的可以在开发阶段做此设置。 将项目中的图片转换为webp。编译阶段无需进行压缩,从而加快编译速度。 在开发阶段禁止自动压缩图片。发布版本前需手动设置为 true,无法使用构建类型和产品风味修改此属性。123aaptOptions { cruncherEnabled false } 加速 gradle 的一些设置12345678910// 开启并行编译,在多 module 项目中很实用org.gradle.parallel=true// 开启 gradle 进程守护,无需每次编译都启动 gradle 进程org.gradle.daemon=true// 开启按需配置org.gradle.configureondemand=true// 设置 gradle 最大内存,并非越大越好,根据官方数据 1g 或 2g 最佳org.gradle.jvmargs=-Xmx2g// 开启 gradle 构建缓存org.gradle.caching=true 如果项目中有使用 Kotlin 语言,尽量使用最新版本的 Kotlin。在 Kotlin 1.1.1 版本默认支持 JVM 的增量编译,1.2.20 默认支持 Gradle Build Cache。 开启 kapt 的 各种加速设置 编译缓存(1.2.20)在build.gradle文件中添加:123kapt { useBuildCache = true} 并行任务(1.2.60)在 gradle.properties文件中添加:1kapt.use.worker.api=true 编译回避(1.3.20)在 gradle.properties文件中添加:1kapt.include.compile.classpath=false 增量编译(1.3.30)在 gradle.properties文件中添加:1kapt.incremental.apt=true 使用 –profile 命令分析编译过程,找到耗时的 Task,逐个进行优化。 针对模块化项目的编译优化建议 创建纯粹的 Java/Kotlin 库,因为纯粹的 Java/Kotlin 库的 Gradle Task 依赖树非常简单。 只应用需要的 Gradle 插件。 分离AP(Annotation Processors)到单独的 Module。从 Gradle 4.7 版本,支持 AP 的增量编译;从 Kotlin 1.3.30 版本,kapt 也支持增量编译。在 gradle.properties 中添加1kapt.incremental.apt=true 只在 app module 中使用 lint。123lintOptions { tasks.lint.enabled = false } 使用 api 或 implementation 而不是 compile。 使用 ext 或 buildSrc 来管理版本依赖。","tags":[]},{"title":"31DaysOfKotlin","date":"2019-06-23T09:40:39.000Z","path":"2019/06/23/31DaysOfKotlin/","text":"#31 天学习 Kotlin — 第一周回顾我们写的 Kotlin 代码越多,我们越喜欢她!Kotlin 的现代语言特性和 Android KTX 使我们的 Android 代码更加的简洁,清晰和优雅。我们 (@FMuntenescu 和 @objcode)启动了 #31DaysOfKotlin 系列作为分享我们最喜爱的 Kotlin 和 Android KTX 特性的一种方式,希望你和我们一样,越来越喜欢她。 在前 7 天的时间里,我们专注于基础知识。 Day 1: Elvis 操作符需要处理代码中的空值?可以使用 elvis 操作符,避免您的 “空情况” (null-erplate)。这只是替换空作为值或者返回事件情况的一个小语法。文档: Elvis operator. 12val name: String = person.name ?: “unknown”val age = person.age ?: return Day 2: 字符串模板格式化字符串?将 $ 符放在变量名的前面表达字符串中的变量和表达式。使用 ${expression} 求表达式的值。文档: string templates. 1234val language = “Kotlin”// “Kotlin has 6 characters”val text = “$language has ${language.length} characters” Day 3: 解构声明Android KTX 使用解构来指定颜色的组件值。 你可以在类中使用解构,或者扩展现有的类来添加解构。文档: destructuring declarations. 12345678// now with prismsval (red, green, blue) = color// destructuring for squaresval (left, top, right, bottom) = rect// or more pointedlyval (x, y) = point Day 4: When 表达式强大的 switch!Kotlin 的 When 表达式几乎可以匹配任何东西。字面值,枚举,数字范围。您甚至可以调用任意的函数!文档: when 1234567891011class Train(val cargo: Number?) { override fun toString(): String { return when (cargo) { null, 0 -> "empty" 1 -> "tiny" in 2..10 -> "small" is Int -> "big inty" else -> "$cargo" } }} Day 5: 循环,范围表达式与解构for 循环在与其他两种 Kotlin 特性一起使用时可以获得超级能力:范围表达式和解构。 文档: ranges, destructuring. 12345678910111213141516// iterating in the range 1 to 100for(i in 1..100) {}// iterating backwards, in the range 100 to 1for(i in 100 downTo 1){}// iterating over an array, getting every other elementval array = arrayOf(“a”, “b”, “x”)for(i in 1 until array.size step 2 ){}// iterating over an array with the item index and destructuringfor((index, element) in array.withIndex()) {}// iterating over a mapval map = mapOf(1 to “one”, 2 to “two”)for( (key, value) in map){} Day 6: 属性在 Kotlin 中,类可以具有可变和只读属性,默认情况下生成 getter 和 setter。如果需要,你也可以自定义。文档: properties. 12345678910111213141516class User { // properties val id: String = “” // immutable. just getter var name: String = “” // default getter and setter var surname: String = “” // custom getter, default setter get() = surname.toUpperCase() // custom getter declaration var email: String = “” // default getter, custom setter set(value) { // custom setter declaration // “value” = name of the setter parameter // “field” = property’s backing field; generated if(isEmailValid(value)) field = value }} Day 7: Data 类 and equality创建一个处理数据的类?将它们标记为 “Data” 类。并默认实现生成 equals() 方法 - 相当于 hashCode(),toString() 和 copy(),并检查结构是否相等。文档: data classes, equality 12345678910111213141516data class User( val name: String, val email: String, val address: Address)public class UserListDiffCallback: DiffUtil.Callback() { override fun areContentsTheSame( oldItemPosition: Int, newItemPosition: Int ): Boolean { // use the generated equals method return newUserList[newItemPosition] == oldUserList[oldItemPosition])} 本周重点介绍了 Kotlin 的基础:处理空错误,简化循环和条件,改进 getter 和 setter,以及删除样板。 下周我们将深入了解更多 Kotlin 功能! #31 天学习 Kotlin — 第二周回顾在第二部分,我们将继续探索 Kotlin - 深入研究密封类和内联等。 Day 8: 可见性在 Kotlin 中一切都是默认 public 的。并且 Kotlin 还有一套丰富的可见性修饰符,例如:private, protected, internal。它们每个都以不同的方式降低了可见性。 文档: visibility modifiers 12345678910111213141516171819// public by defaultval isVisible = true// only in the same fileprivate val isHidden = true// internal to compilation ‘module’internal val almostVisible = trueclass Foo { // public by default val isVisible = true // visible to my subclasses protected val isInheritable = true // only in the same class private val isHidden = true} Day 9: 默认参数方法参数的数量是否太多?在函数中指定默认参数值。使用命名参数使代码更具可读性。 文档: default arguments 12345678910111213141516// parameters with default valuesclass BulletPointSpan( private val bulletRadius: Float = DEFAULT_BULLET_RADIUS, private val gapWidth: Int = DEFAULT_GAP_WIDTH, private val color: Int = Color.BLACK)// using only default valuesval bulletPointSpan = BulletPointSpan()// passing a value for the first argument, others defaultval bulletPointSpan2 = BulletPointSpan( resources.getDimension(R.dimen.radius))// using a named parameter for the last argument, others defaultval bulletPointSpan3 = BulletPointSpan(color = Color.RED) Day 10: Sealed 类Kotlin 的 sealed 类可以让你轻松地处理错误数据,当结合 LiveData 时, 你可以用一个 LiveData 同时代表成功和失败的情况,这比用两个变量要好。文档: sealed classes 123456789101112sealed class NetworkResultdata class Success(val result: String): NetworkResult()data class Failure(val error: Error): NetworkResult()// one observer for success and failureviewModel.data.observe(this, Observer { data -> data ?: return@Observer // skip nulls when(data) { is Success -> showResult(data.result) // smart cast to Success is Failure -> showError(data.error) // smart cast to Failure }}) 你也可以将 sealed 类用在 RecyclerView 的 adapter 中。它们非常适合 ViewHolders —— 用一组清晰的类型明确地分派给每个 holder。用作表达式时,如果有类型不匹配,编译器将会报错。 123456789101112// use Sealed classes as ViewHolders in a RecyclerViewAdapteroverride fun onBindViewHolder( holder: SealedAdapterViewHolder?, position: Int) { when (holder) { // compiler enforces handling all types is HeaderHolder -> { holder.displayHeader(items[position]) // smart cast here } is DetailsHolder -> { holder.displayDetails(items[position]) // smart cast here } }} 使用 RecyclerViews 时,如果我们有很多来自 RecyclerView 中 item 的回调,比如一个点击,分享和删除 item 的项目,我们也可以使用 sealed 类。一个回调就可以处理所有的事情! 1234567891011121314151617sealed class DetailItemClickEventdata class DetailBodyClick(val section: Int): DetailItemClickEvent()data class ShareClick(val platform: String): DetailItemClickEvent()data class DeleteClick(val confirmed: Boolean): DetailItemClickEvent()class MyHandler : DetailItemClickInterface { override fun onDetailClicked(item: DetailItemClickEvent) { when (item) { // compiler enforces handling all types is DetailBodyClick -> expandBody(item.section) is ShareClick -> shareOn(item.platform) is DeleteClick -> { if (item.confirmed) doDelete() else confirmDetele() } } }} Day 11: 懒加载懒加载是个好东西!通过使用懒加载,可以省去昂贵的属性初始化成本,直到它们真正需要。计算值然后保存并为了未来任何时候的调用。文档: lazy 123val preference: String by lazy { sharedPreferences.getString(PREFERENCE_KEY)} Day 12: LateinitAndroid 中,在 onCreate 或者其它的回调初始化对象,但在 Kotlin 中不为空的对象必须初始化。那么怎么办呢?可以输入 lateinit。来承诺最终将会初始化。拉钩保证它是空安全的。 文档: lateinit 1234567891011class MyActivity : AppCompatActivity() { // non-null, but not initalized lateinit var recyclerView: RecyclerView override fun onCreate(savedInstanceState: Bundle?) { // … // initialized here recyclerView = findViewById(R.id.recycler_view) }} Day 13: Require 和 check你方法的参数是有效的吗?用 require 在使用前可以检查它们,如果它们是无效的将会抛出 IllegalArgumentException。文档: require 123456fun setName(name: String) { // calling setName(“”) throws IllegalArgumentException require(name.isNotEmpty()) { “Invalid name” } // …} 您的封闭类的状态是否正确?可以使用 check 来验证。如果检查的值为 false,它将抛出 IllegalStateException。文档: check 12345fun User.logOut(){ // When not authenticated, throws IllegalStateException check(isAuthenticated()) { “User $email is not authenticated” } isAuthenticated = false} Day 14: 内联等不及要使用 lambdas 来生成一个新的接口?kotlin 可以让你定义一个 inline 的方法 – 这意味着调用将替换方法体,用很非常简单的方法来生成 lambda 的接口。文档: inline functions 1234567891011121314151617// define an inline function that takes a function argumentinline fun onlyIf(check: Boolean, operation: () -> Unit) { if (check) { operation() }}// call it like thisonlyIf(shouldPrint) { // call: pass operation as a lambda println(“Hello, Kotlin”)}// which will be inlined to thisif (shouldPrint) { // execution: no need to create lambda println(“Hello, Kotlin”)} 本周深入研究了 Kotlin 的特性:可见性,默认参数,密封类,懒加载,延迟初始化,require 和 check,以及强大的内联。 下周我们将深入了解更多的 Kotlin 特性并开始探索 Android KTX。 #31 天学习 Kotlin — 第三周回顾第 3 周分为 Kotlin 特性和使用 Android KTX 使 Android 代码更优雅的几种方式。 Day 15: 操作符重载用操作符重载可以更快速写 Kotlin。像 Path,Range 或 SpannableStrings 这样的对象允许像加或减这样的操作。通过 Kotlin,你可以实现自己的操作符。 文档: operator overloading, Android KTX usage example. 1234567891011// Definition/** Adds a span to the entire text. */inline operator fun Spannable.plusAssign(span: Any) =setSpan(span, 0, length, SPAN_INCLUSIVE_EXCLUSIVE)// Use it like thisval spannable = “Eureka!!!!”.toSpannable()spannable += StyleSpan(BOLD) // Make the text bold with +=spannable += UnderlineSpan() // Make the text underline with += Day 16: 顶层方法和参数类的工具方法?将它们添加到源文件的顶层。在 Java 中,它们被编译为该类的静态方法。文档: basic syntax. 123456789// Define a top-level function that creates a DataBinding Adapter for a RecyclerView@BindingAdapter(“userItems”)fun userItems(recyclerView: RecyclerView, list: List?){ //update the RecyclerView with the new list …}class UsersFragment: Fragment{...} 你是否为你的类定义了静态常量?使它们成为顶层属性。它们将被编译为字段和静态访问器。 12345678910// Define a top-level property for Room databaseprivate const val DATABASE_NAME = “MyDatabase.db”private fun makeDatabase(context: Context): MyDatabase { return Room.databaseBuilder( context, MyDatabase::class.java, DATABASE_NAME ).build()} Day 17: 在没有迭代器的情况下迭代类型迭代器用在了有趣的地方!Android KTX 将迭代器添加到 ViewGroup 和 SparseArray。要定义迭代器扩展请使用 operator 关键字。 Foreach 循环将使用扩展名!文档: for loops, Android KTX usage example. 12345678910// Example from Android KTXfor(view in viewGroup) { }for(key in sparseArray) {}// Your projectoperator Waterfall.iterator() { // add an iterator to a waterfall class}for(items in myClass) {} // Now waterfall has iterations! Day 18: 简单的 Content Values将 ContentValues 的强大功能与 Kotlin 的简洁性相结合。使用 Android KTX 只传递一个 Pair <StringKey,Value> 创建 ContentValues。Android KTX 实现. 123456val contentValues = contentValuesOf( “KEY_INT” to 1, “KEY_LONG” to 2L, “KEY_BOOLEAN” to true, “KEY_NULL” to null) Day 19: DSLsDSL 可以通过使用类型安全的构建器来完成。它们简化了 API ;你也可以借助extension lambdas 和type safe builders等功能来构建它们。 123456789101112html { head { title {+”This is Kotlin!” } } body { h1 {+”A DSL in Kotlin!”} p {+”It’s rather” b {+”bold.” } +”don’t you think?” } }} Spek 是一个构建为 Kotlin DSL 的测试库。Spek 不使用 @Annotations,而是提供了一种不依赖于反射的类型安全的方式来声明测试代码。 12345678910111213@RunWith(JUnitPlatform::class)class MyTest : Spek({ val subject = Subject() given("it ’ s on fire") { subject.lightAFire() it("should be burning") { assertTrue(subject.isBurning()) } it("should not be cold") { assertFalse(subject.isCold()) } }}) Android 上 Kotlin 的另一个 DSL 是 Anko。Anko 允许您使用声明性代码构建 Android 视图。 123456frameLayout { button("Light a fire") { onClick { lightAFire() }} Day 20: 简单的 Bundle准备去通过简洁的方式(bundle creator in Android KTX)去创建 bundle,不调用 putString,putInt,或它们的 20 个方法中的任何一个。一次调用让您生成一个新的 Bundle,它甚至可以处理 Arrays! 1234567val bundle = bundleOf( "KEY_INT " *to *1, "KEY_LONG" *to *2L, "KEY_BOOLEAN" *to *true, "KEY_NULL" *to *null "KEY_ARRAY" to arrayOf(1, 2)) Day 21: 简化 postDelayedLambdas 非常贴心。使用最后一个参数调用语法您可以取消回调,Callable 和 Runnable. 例如 Android KTX 贴心的用一个小包装来处理 postDelayed。 12345678910// Android KTX APIfun Handler.postDelayed( delay: Int, token: Any? = null, action: () -> Unit)// Call it like this — no need for a Runnable in your codehandler.postDelayed(50) { // pass a lambda to postDelayed} 本周重点介绍了一些基本的 Kotlin 特性,如运算符重载,顶层函数和参数以及迭代器,我们讨论了一个高级功能:领域特定语言(DSL),并展示了使用 Android KTX 如何编写更简洁的代码。content values, bundles 和 callbacks。 #31 天学习 Kotlin — 第四周回顾第 4 周学习更多的语言基础知识,然后在某方面使用 Android KTX 使您的代码更简洁和可读! Day 22: 在 Java 语言中调用 Kotlin在同一个项目中使用 Kotlin 和 Java?你有没有顶层函数或属性的类呢?默认情况下,编译器将生成类名为 YourFileKt 的类。通过使用 @file:JvmName 注释文件来更改它。文档: package level functions 123456789101112131415161718// File: ShapesGenerator.ktpackage com.shapesfun generateSquare(): Square {…}fun generateTriangle(): Triangle {…}// Java usage:Square square = ShapesGeneratorKt.generateSquare();// File: ShapesGenerator.kt@file:JvmName(“ShapesGenerator”)package com.shapesfun generateSquare(): Square {…}fun generateTriangle(): Triangle {…}// Java usage:Square square = ShapesGenerator.generateSquare(); Day 23: 具体化具体化概念的一个例子:Android KTX 中的 Context.systemService() 使用泛化通过泛型传递 “真实” 类型。没有通过 getSystemService。文档: reified type parameters Android KTX: Context.systemService() 12345678910// the old wayval alarmManager = context.getSystemService(AlarmManager::class.java)// the reified wayval alarmManager: AlarmManager = context.systemService()// the magic from Android KTX… with “real” type Tinline fun Context.systemService() = getSystemService(T::class.java) Day 24: Delegates通过 by 把你的工作委托给另一个类。通过类继承,并将属性访问器逻辑与委托者属性重用。 文档: delegation and delegated properties 123456789101112131415161718class MyAnimatingView : View( /* … */ ) { // delegated property. // Uses the getter and setter defined in InvalidateDelegate var foregroundX by InvalidateDelegate(0f)}// A View Delegate which invalidates// View.postInvalidateOnAnimation when set.class InvalidateDelegate(var value: T) { operator fun getValue(thisRef: View, property: KProperty<*>) = value operator fun setValue(thisRef: View, property: KProperty<*>, value: T) { this.value = value thisRef.postInvalidateOnAnimation() }} Day 25: 扩展函数没有更多的 Util 类。通过扩展函数来扩展类的功能。把你要扩展的类的名字放在你添加的方法的名字前面。 文档: extension functions 扩展功能的一些特性: 不是成员函数 不以任何方式修改原始类 通过静态类型信息解决编译时间 会被编译为静态函数 不是多态 例如: String.toUri() 12345// Extend String with toUriinline fun String.toUri(): Uri = Uri.parse(this)// And call it on any String!val myUri = “www.developer.android.com".toUri() Day 26: Drawable.toBitmap() 轻松转换如果你曾经将 Drawable 转换为 Bitmap,那么你需要多少样板代码呢? Android KTX 具有一系列功能,可以使您的代码在使用 graphics 包中类时更加简洁。 文档: graphics 12345// get a drawable from resourcesval myDrawable = ContextCompat.getDrawable(context, R.drawable.icon)// convert the drawable to a bitmapval bitmap = myDrawable.toBitmap() Day 27: Sequences, lazy 和 generators序列是从未存在的列表。序列是迭代器的表亲,一次只能懒散地产生一个值。这在使用 map 和 fifter 时非常重要 - 它们将创建序列,而不是为每一步都复制列表!文档: sequences 123456val sequence = List(50) { it * 5 }.asSequence()sequence.map { it * 2 } // lazy (iterate 1 element) .filter { it % 3 == 0 } // lazy (iterate 1 element) .map { it + 1 } // lazy (iterate 1 element) .toList() // eager (make a new list) 你可以从列表中创建序列或指定下一个功能。如果您永远不会终止一个序列,它可以是无限长的而不会耗尽内存。使用 Kotlin 中的协程也可以用生成器! 文档: generators 123456789101112// make a sequence from a listlist.asSequence().filter { it == “lazy” }// or, reach infinity functionallyval natural = generateSequence(1) { it + 1 }// or, cooperate with coroutinesval zeros = buildSequence { while (true) { yield (0) }} Day 28: 更简单的 spans功能强大但是很难使用 - 这就是 text styel Spans API 给人的感觉。 Android KTX 为一些最常见的 span 添加了扩展功能,并使 API 更易于使用。Android KTX: spannable string builder 12345678910val string = buildSpannedString { append(“no styling text”) bold { append(“bold”) italic { append(“bold and italic”) } } inSpans(RelativeSizeSpan(2f), QuoteSpan()){ append(“double sized quote text”) }} Day 29: Parcelize喜欢 Parcelable 的速度,但不喜欢写所有的代码?和 @Parcelize 打个招呼吧。Spec: Parcelize 123456789@Parcelizedata class User(val name: String, val occupation: Work): Parcelable// build.gradleandroidExtensions { //Enable experimental Kotlin features // in gradle to enable Parcelize experimental = true} Day 30: updatePadding 扩展通过默认参数扩展现有的 API 通常会让人感到高兴。 Android KTX 允许你使用默认参数在 View 的一侧设置 Padding。一个函数可以节省很多代码! Android KTX: View.updatePadding 12345view.updatePadding(left = newPadding)view.updatePadding(top = newPadding)view.updatePadding(right = newPadding)view.updatePadding(bottom = newPadding)view.updatePadding(top = newPadding, bottom = newPadding) Day 31: 范围外 run, let, with, apply让我们运行一些标准的 Kotlin 函数!简短而强大,run,let,with 和 appy 都有一个接收器 (this),可能有一个参数 (it) 并可能有一个返回值。差异如下: run 1234567val string = “a”val result = string.run { // this = “a” // it = not available 1 // Block return value // result = 1} let 1234567val string = “a”val result = string.let { // this = this@MyClass // it = “a” 2 // Block return value // result = 2} with 1234567val string = “a”val result = with(string) { // this = “a” // it = not available 3 // Block return value // result = 3} apply 1234567val string = “a”val result = string.apply { // this = “a” // it = not available 4 // Block return value unused // result = “a”} 本周介绍了一些语言特性,例如 interop,refied 和 sequence,然后我们转向 Android KTX,展示了它帮助你编写简洁易读的代码的一些方法。 为了完成这个系列,我们介绍了功能强大的 Kotlin scope 函数。","tags":[{"name":"Kotlin","slug":"Kotlin","permalink":"http://www.imzhiqiang.com/tags/Kotlin/"}]},{"title":"应用内资源文件优化","date":"2019-03-02T12:41:03.000Z","path":"2019/03/02/In-app-resource-opt/","text":"使用 lint 检查无用资源使用lint检测工具检查无用代码,注意不要使用一键删除,避免以下两个问题: 通过反射获取资源,可以将反射资源加入lint.xml,避过lint检测 koltin目前对lint检测支持并不友好 使用 Gradle 插件压缩图片利用 Gradle 插件我们可以在打包时把一些较大的图片资源压缩,从而达到瘦身的目的。插件项目地址加入压缩图片资源插件, 将链接中的文件置于工程目录下,同时在项目中加入一下配置 1234567891011121314151617181920212223//project build.gradledependencies { classpath 'com.smallsoho.mobcase:McImage:1.5.1'}// app build.gradleapply plugin: 'McImage'McImageConfig { isCheckSize true //Whether to detect image size,default true optimizeType "Compress" //Optimize Type,"ConvertWebp" or "Compress",default "Compress", "CompressWebp" is a better compression ratio but it don't support api < 18 maxSize 1*1024*1024 //big image size threshold,default 1MB enableWhenDebug false //switch in debug build,default true isCheckPixels true // Whether to detect image pixels of width and height,default true maxWidth 1000 //default 1000 maxHeight 1000 //default 1000 whiteList = [ //do not do any optimization for the images who in the list "icon_launcher.png" ] mctoolsDir "$rootDir" isSupportAlphaWebp false //Whether support convert the Image with Alpha chanel to Webp,default false, the images with alpha chanels will be compressed.if config true, its need api level >=18 or do some compatible measures multiThread true //Whether open multi-thread processing,default true bigImageWhiteList = [] //do not detect big size or large pixels for the images who in the list} 使用微信的 AndResGuard 混淆压缩资源文件AndResGuard 可以实现资源混淆,把冗长的资源路径变短,从而减小 apk 的大小。 使用Gradle插件使用wx的AndResGuard插件, 配置如下: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657//project build.gradledependencies { classpath 'com.tencent.mm:AndResGuard-gradle-plugin:1.2.21' } // app build.gradleapply plugin: 'AndResGuard'andResGuard { // mappingFile = file("./resource_mapping.txt") mappingFile = null use7zip = true useSign = true // It will keep the origin path of your resources when it's true keepRoot = false // If set, name column in arsc those need to proguard will be kept to this value fixedResName = "arg" // It will merge the duplicated resources, but don't rely on this feature too much. // it's always better to remove duplicated resource from repo mergeDuplicatedRes = true whiteList = [ // your icon "R.drawable.icon", // for fabric "R.string.com.crashlytics.*", // for google-services "R.string.google_app_id", "R.string.gcm_defaultSenderId", "R.string.default_web_client_id", "R.string.ga_trackingId", "R.string.firebase_database_url", "R.string.google_api_key", "R.string.google_crash_reporting_api_key", "R.string.project_id", ] compressFilePattern = [ "*.png", "*.jpg", "*.jpeg", "*.gif", ] sevenzip { artifact = 'com.tencent.mm:SevenZip:1.2.21' //path = "/usr/local/bin/7za" } /** * Optional: if finalApkBackupPath is null, AndResGuard will overwrite final apk * to the path which assemble[Task] write to **/ // finalApkBackupPath = "${project.rootDir}/final.apk" /** * Optional: Specifies the name of the message digest algorithm to user when digesting the entries of JAR file * Only works in V1signing, default value is "SHA-1" **/ // digestalg = "SHA-256"} 使用命令行工具参考官方文档 使用Webp替换png/jpg使用Android Studio自带的工具可以很方便把项目中的png和jpg替换为webp。详见下图: 使用tinypng压缩png/jpg对于minSdkVersion < 18 的App,不能将带有Alpha通道的png图片转成webp,需要使用tinypng进行图片压缩。tinypng官方提供了在线压缩的方式,但是这种方式明显不适合我们的Android工程。这里我们使用开源的命令行工具tinypng-cli来帮助我们完成压缩替换的工作。 由于tinypng-cli依赖于nodejs,所以要先安装nodejs。下载地址:https://nodejs.org/en/download/ 使用npm安装tinypng-cli。1npm install -g tinypng-cli 向tinypng申请API Key,只需填写名字和邮箱即可。 设置全局的API Key:在当前用户的home目录下,新建一个.tinypng的文件,将申请的APK Key写入该文件。 进入要压缩的目录,运行命令,大功告成!更多用法参考:https://github.com/websperts/tinypng-cli#usage","tags":[]},{"title":"使用ACTION_IMAGE_CAPTURE可能存在的风险","date":"2017-09-04T03:16:11.000Z","path":"2017/09/04/action-image-capture-issue/","text":"很多的 Android App 中都有使用相机拍摄用户头像的功能。大部分开发者都会使用MediaStore.ACTION_IMAGE_CAPTURE来满足这一需求。这可以节省很多时间,不需要单独开发相机 UI,直接调用系统相机;不需要向系统请求 Camera 权限。正如官方文档里面说的那样,Taking Photos Simply。然而在最近的一次的测试中,我发现并没有那么简单。因为运行了几年的代码竟然发生了 Crash。具体的 log 如下: java.lang.SecurityException: Permission Denial: starting Intent { act=android.media.action.IMAGE_CAPTURE cat=[android.intent.category.DEFAULT] flg=0x3 cmp=com.google.android.GoogleCamera/com.android.camera.activity.CaptureActivity clip={text/uri-list U:content://com.imzhiqiang.example.fileprovider/imageCache/tmp_avatar.jpg} (has extras) } from ProcessRecord{bf70afd 18107:com.imzhiqiang.example/u0a108} (pid=18107, uid=10108) with revoked permission android.permission.CAMERA 看上去是因为没有处理运行时权限导致的 Crash。Interesting! 我并没有在 manifest 文件中声明 Camera 的权限,为什么会出现没有处理 Camera 运行时权限的问题呢?随后我想到了可能是引用的 library 中声明了该权限。在 Android Studio 中查看了 Merged Manifest,果然是这样。图中深色背景的权限是我自己声明的,下面的权限是第三方的 library 声明的。 然而这和 Intent 又有什么关系?使用 ACTION_IMAGE_CAPTURE 不是可以避免请求 Camera 权限吗?经过几番周折过后,最后终于在官方文档中找到了答案。 虽然很难理解 Google 这样的做法,不过总算找到了问题的根本所在。在对 Camera 权限进行正确的处理后,终于正常运行了。 结论 如果没有在 manifest 文件中声明 Camera 权限,使用 ACTION_IMAGE_CAPTURE 不需要对 Camera 权限做运行时权限处理,代码正常运行。如果声明了就必须要做权限处理。 使用 Intent 的 action 时,一定要仔细阅读官方文档,避免类似的风险。 引用第三方 library 时,除了熟悉其内部原理和源码外,还要注意它在 manifest 文件中添加的东西。 扩展阅读 Taking Photos Not So Simply: How I Got Bitten By ACTION_IMAGE_CAPTURE Use intent ACTION_IMAGE_CAPTURE to launch camera app requires CAMERA permission The ACTION_IMAGE_CAPTURE Fallacy","tags":[{"name":"Image Capture","slug":"Image-Capture","permalink":"http://www.imzhiqiang.com/tags/Image-Capture/"}]},{"title":"Glide中的缓存","date":"2017-03-05T07:49:32.000Z","path":"2017/03/05/Cache-in-Glide/","text":"本文主要介绍了如何配置和管理 Glide 中的缓存,其中大部分内容都可以直接在官方 Wiki 中找到,这里只是进行了整理和汇总。言归正传,Glide 支持图片的二级缓存(并不是三级缓存,因为从网络加载并不属于缓存),即内存缓存和磁盘缓存。 磁盘缓存一般的图片缓存指的就是磁盘缓存,把网络上的图片缓存到本地,这样就不需要每次都从网络加载,既提高了加载速度,又为用户节省了流量。Glide 在默认情况下是开启磁盘缓存的,而且提供了丰富的 API 来让开发者自己配置和管理磁盘缓存。 缓存位置和大小开发者可以通过构建一个自定义的GlideModule来配置 Glide 磁盘缓存的位置和大小。最简单的方法如下: 1234567891011121314public class DiskCacheMoudle implements GlideModule { @Override public void applyOptions(Context context, GlideBuilder builder) { builder.setDiskCache( new InternalCacheDiskCacheFactory(context, "glide_cache", 100 * 1024 * 1024)); //builder.setDiskCache( // new ExternalCacheDiskCacheFactory(context, "glide_cache", 100 * 1024 * 1024)); } @Override public void registerComponents(Context context, Glide glide) { }} 其中 InternalCache 和 ExternalCache 都最多接收 3 个参数:第一个参数为 Context,没啥好说的;第二个为缓存的目录名称;第三个为缓存大小,单位是 Byte。它们之间唯一的不同就在于 InternalCache 构建的缓存是在应用的内部储存,而 ExternalCache 则是在外部储存。内部储存中的缓存文件是其他应用程序是无法获取到的,更加安全。关于内部储存和外部储存的更多内容,请点击这里查看官方文档。 如果不想把缓存放在上面的两个位置怎么办?Glide 当然也支持,具体通过 DiskLruCacheFactory 来实现: 1234567builder.setDiskCache( new DiskLruCacheFactory(new DiskLruCacheFactory.CacheDirectoryGetter() { @Override public File getCacheDirectory() { return getMyCacheLocationBlockingIO(); } }), 100 * 1024 * 1024); Note: getMyCacheLocationBlockingIO 方法返回的文件不能为空,而且必须是一个已经创建好的文件目录,不可以是文件。 缓存策略与其他图片加载库的缓存机制不同,Glide 缓存图片时默认只缓存最终加载的那张图片。举个栗子,你要加载的图片分辨率为 1000x1000,但是最终显示该图片的 ImageView 大小只有 500x500,那么 Glide 就会只缓存 500x500 的小图。这也是在从磁盘缓存中加载图片时 Glide 比 Picasso 快的原因。Glide 目前提供了四种缓存策略: DiskCacheStrategy.NONE 不缓存文件 DiskCacheStrategy.SOURCE 只缓存原图 DiskCacheStrategy.RESULT 只缓存最终加载的图(默认的缓存策略) DiskCacheStrategy.ALL 同时缓存原图和结果图 缓存算法在 Glide 中磁盘缓存默认使用的是 LRU(Least Recently Used)算法。如果你想使用其他的缓存算法,就只能通过实现 DiskCache 接口来完成了。 内存缓存使用内存缓存可以获得更快的图片加载速度,因为减少了耗时的 IO 操作。众所周知,Bitmap 是 Android 中的内存大户,频繁的创建和回收 Bitmap 必然会引起内存抖动。Glide 中有一个叫做 BitmapPool 的类,可以复用其中的 Bitmap 对象,从而避免 Bitmap 对象的创建,减小内存开销。当配置内存缓存时,我们也应该同时配置 BitmapPool 的大小。具体方法也是通过自定义的 GlideModule 来实现的: 12builder.setMemoryCache(new LruResourceCache(yourSizeInBytes));builder.setBitmapPool(new LruBitmapPool(sizeInBytes)); 一般情况下,开发者是不需要自己去指定它们的大小的,因为 Glide 已经帮我们做好了。默认的内存缓存和 bitmapPool 的大小由MemorySizeCalculator根据当前设备的屏幕大小和可用内存计算得到。同时 Glide 还支持动态的缓存大小调整,在存在大量图片的 Activity/Fragment 中,开发者可以通过 setMemoryCategory 方法来提高 Glide 的内存缓存大小,从而加快图片的加载速度。 1Glide.get(context).setMemoryCategory(MemoryCategory.HIGH); MemoryCategory 有 3 个值可供选择: MemoryCategory.HIGH(初始缓存大小的 1.5 倍) MemoryCategory.NORMAL(初始缓存大小的 1 倍) MemoryCategory.LOW(初始缓存大小的 0.5 倍) 在有些情况下我们不希望做内存缓存(比如加载 GIF 图片),这个时候可以调用 skipMemoryCache(true)方法跳过内存缓存。 如何缓存动态 Url 的图片一般情况下我们从网络上获取到的图片 Url 都是静态的,即一张图片对应一个 Url。那么如果是一张图片对应多个 Url 呢?缓存不就没有意义了。因为图片加载库都是拿图片的 Url 来作为缓存的 key 的,Glide 也不例外,只是会更加复杂一些。如果你开启了 Glide 的 log,就会在控制台看到 Glide 是如何指定缓存 key 的。关于如何打开 log,请参考这篇文章。一般来说,Glide 的 key 由图片的 url、view 的宽和高、屏幕的尺寸大小和 signature 组成。 在什么情况下才会出现动态的 Url 呢?一个很典型的例子就是因为图片的安全问题在原来图片的 Url 后面加上访问凭证。访问凭证与时间关联,这样一来,在不同时间同一图片的 Url 就会不同,缓存就会失效。以七牛的私有空间为例,我们来看看如何去缓存这类图片。从七牛关于私有空间的文档中可以得到:最终的 Url = 原 Url + ?e=过期时间 + token=下载凭证。那么就只需要在 Glide 缓存时将 Url 中“?”后面的字符串截去就可以了。 首先新建一个叫做 QiNiuImage 的类: 1234567891011121314151617181920public class QiNiuImage { private final String imageUrl; public QiNiuImage(String imageUrl) { this.imageUrl = imageUrl; } public String getImageUrl() { return imageUrl; } public String getImageId() { if (imageUrl.contains("?")) { return imageUrl.substring(0, imageUrl.lastIndexOf("?")); } else { return imageUrl; } }} 其中 getImageUrl 方法返回真实的 Url,getImageId 方法返回未添加下载凭证前的 Url。然后再自定义一个实现ModelLoader接口的 QiNiuImageLoader: 1234567891011121314151617181920212223public class QiNiuImageLoader implements StreamModelLoader<QiNiuImage> { @Override public DataFetcher<InputStream> getResourceFetcher(final QiNiuImage model, int width, int height) { return new HttpUrlFetcher(new GlideUrl(model.getImageUrl())) { @Override public String getId() { return model.getImageId(); } }; } public static class Factory implements ModelLoaderFactory<QiNiuImage, InputStream> { @Override public ModelLoader<QiNiuImage, InputStream> build(Context context, GenericLoaderFactory factories) { return new QiNiuImageLoader(); } @Override public void teardown() { /* no op */ } }} 其中 HttpUrlFetcher 的 getId 方法就是组成缓存的 key 的重要部分。这也是我们的核心原理。将这个 ModelLoader 注册到 GlideModule 中,并在 AndroidManifest.xml 中注册: 1234567891011public class QiNiuModule implements GlideModule { @Override public void applyOptions(Context context, GlideBuilder builder) { } @Override public void registerComponents(Context context, Glide glide) { glide.register(QiNiuImage.class, InputStream.class, new QiNiuImageLoader.Factory()); }} 123<meta-data android:name="com.yourpackagename.QiNiuModule" android:value="GlideModule"/> 最后只需要在加载此类图片时,使用下面这段代码就可以了。即使图片的 token 更换了也不会重新从网络上下载而是直接读取本地缓存。 123Glide.with(context) .load(new QiNiuImage(imageUrl) .into(imageView); 参考资料: https://github.com/bumptech/glide/issues/607 https://github.com/bumptech/glide/issues/501","tags":[{"name":"Cache","slug":"Cache","permalink":"http://www.imzhiqiang.com/tags/Cache/"},{"name":"Glide","slug":"Glide","permalink":"http://www.imzhiqiang.com/tags/Glide/"}]},{"title":"Android多媒体框架中几个重要的类","date":"2016-09-24T02:10:09.000Z","path":"2016/09/24/Several-important-classes-in-Android-media-framework/","text":"与 iOS 中强大的 AVFoundation 框架相比,Android framework 中提供的有关多媒体处理的类可谓屈指可数,但总比没有好吧。今天我们就来谈谈这几个类。 这里提到的多媒体处理主要是指音视频处理。包括音视频的裁剪、合并;视频画面的各种变换,旋转、缩放、翻转;视频滤镜;音视频的播放,快速、慢速、倒序播放等等。 主要涉及到的类有: MediaExtractor:媒体提取器 MediaCodec:编解码器 MediaMuxer:媒体混合器 MediaMetadataRetriever: 获取音视频信息的类 MediaFormat: 包含音视频帧信息的类 GLSurfaceView: 展示 openGL 渲染的 View OpenGL 相关的类 MediaExtractor 在 Android4.1(API16)加入。可以从一段音视频中提取出一帧一帧的数据,与 MediaMuxer 配合使用可以完成视频的裁剪和合并,与 MediaCodec、GLSurfaceView 配合使用可以完成视频的播放。 MediaCodec 在 Android4.1(API16)加入。在 Android4.3(API18)提供输入可以为 Surface。在 Android5.0(API21)又增加了异步处理模式。它是一个低等级的媒体编解码器,可以作为编码器,也可以作为解码器。\b 可攻可受,嘿嘿嘿。是音视频处理中最为核心的类。 由于相关文档在以前不是很完善,来自 Android 媒体团队的 fadden(现已不在)维护了一个网站 http://bigflake.com/mediacodec/ 。上面有大量的相关资源。此外在 stackoverflow 上面只要是 MeidaCodec 相关的问题随处可见 fadden 的身影。感谢 fadden。现在官方文档已经相当详细了,不过都是英文的,对于阅读困难的人,国内也有人进行了翻译。地址在[这里](http://www.cnblogs.com/xiaoshubao/archive/2016/04/11/5368183.html) 。 MediaMuxer 在 Android4.3(API18)。可以合成 MP4 格式的视频,输入源通常为从 MediaExtractor 或者 MediaCodec 提供的已编码的数据。 MediaMetadataRetriever 主要用来获取视频的方向信息。在合成视频时,可纠正视频方向。此外还可以获取视频某一帧画面的 bitmap,前提必须是 android 支持的视频格式。 MediaFormat 内部持有一个包含音视频帧信息的 map。 GLSurfaceView 可与 MediaPlayer 配合,完成视频变换的各种效果实时预览。 OpenGL 是一门单独的技术,然而关于其在 Android 中使用的文档和列子实在太少。唯一的一本书《OpenGL ES 应用开发实践指南 Android 卷》在网上也买不到,只能去淘宝买复印版。。。","tags":[{"name":"多媒体","slug":"多媒体","permalink":"http://www.imzhiqiang.com/tags/%E5%A4%9A%E5%AA%92%E4%BD%93/"},{"name":"MediaCodec","slug":"MediaCodec","permalink":"http://www.imzhiqiang.com/tags/MediaCodec/"}]},{"title":"如何调试Glide加载图片","date":"2015-12-01T07:23:23.000Z","path":"2015/12/01/How-to-debug-glide/","text":"前言与其他图片加载库不同,在 Glide 加载图片的过程中默认是没有任何 log 输出的。这样使得加载失败的原因难以调试。到底是网络错误还是图片根本就不存在亦或者解码出错,我们不得而知。当然官方也给出了调试的方法,这篇文章就来介绍下如何调试 Glide 加载图片,内容主要是对官方 wiki 的翻译。 正文在 Glide 加载图片过程中出现异常时,默认是没有 log 输出的。但是 Glide 给开发者提供了两种方法来查看或者响应这些异常。 调试为了在异常发生时可以看到它们,你可以打开 Glide 中处理所有媒体加载响应的类 GenericRequest 的 log 开关。很简单,在命令行运行下面的指令即可: 1adb shell setprop log.tag.GenericRequest DEBUG 如果你将 DEBUG 替换为 VERBOSE,还可以看到详细的请求时间日志。 如果你想禁用 log 输出,执行: 1adb shell setprop log.tag.GenericRequest ERROR 调试工作流 为了查看 Glide 内部引擎是何时、如何加载图片的,你可以启用这些 log: 123adb shell setprop log.tag.Engine VERBOSEadb shell setprop log.tag.EngineJob VERBOSEadb shell setprop log.tag.DecodeJob VERBOSE 启用这些 log 可以帮助你了解到为什么某些资源没有从内存中加载?为什么从外部 url 加载要重新下载数据?同时这也有助于了解在磁盘缓存时哪些参数需要配置。启用 DecodeJob log 还可以帮助你了解自定义变换/解码器/编码器等相关问题。 请求监听器虽然启动调试日志很简单,但前提是你可以访问到设备。为了完善 Glide 已存在或更复杂的错误日志系统,你可以使用 RequestListener 类。当加载请求失败时 onException()方法就会被调用,并给出造成失败的异常或者 null(在解码器无法从它获取到的数据中解码出任何有用的东西时)。你可以通过 listener() API 为每一个请求添加一个监听器。 确保 onException()方法的返回值为 false,避免覆盖 Glide 默认的错误处理(比如加载失败的错误图片占位)。 这里有一个实现快速调试的列子: 12345678910111213// 示例: .listener(new LoggingListener<String, GlideDrawable>())public class LoggingListener<T, R> implements RequestListener<T, R> { @Override public boolean onException(Exception e, Object model, Target target, boolean isFirstResource) { android.util.Log.d("GLIDE", String.format(Locale.ROOT, "onException(%s, %s, %s, %s)", e, model, target, isFirstResource), e); return false; } @Override public boolean onResourceReady(Object resource, Object model, Target target, boolean isFromMemoryCache, boolean isFirstResource) { android.util.Log.d("GLIDE", String.format(Locale.ROOT, "onResourceReady(%s, %s, %s, %s, %s)", resource, model, target, isFromMemoryCache, isFirstResource)); return false; }} 确保在发布应用时移除掉所有的调试 log! 更多的 log 指令这个列表是基于 Glide 3.6.0 版本的,并不完整。 12345678910111213141516171819202122232425262728293031cd .../android-sdk/platform-toolsadb shell setprop log.tag.AnimatedGifEncoder VERBOSEadb shell setprop log.tag.AssetUriFetcher VERBOSEadb shell setprop log.tag.BitmapEncoder VERBOSEadb shell setprop log.tag.BufferedIs VERBOSEadb shell setprop log.tag.ByteArrayPool VERBOSEadb shell setprop log.tag.CacheLoader VERBOSEadb shell setprop log.tag.ContentLengthStream VERBOSEadb shell setprop log.tag.DecodeJob VERBOSEadb shell setprop log.tag.DiskLruCacheWrapper VERBOSEadb shell setprop log.tag.Downsampler VERBOSEadb shell setprop log.tag.Engine VERBOSEadb shell setprop log.tag.EngineRunnable VERBOSEadb shell setprop log.tag.GenericRequest VERBOSEadb shell setprop log.tag.GifDecoder VERBOSEadb shell setprop log.tag.GifEncoder VERBOSEadb shell setprop log.tag.GifHeaderParser VERBOSEadb shell setprop log.tag.GifResourceDecoder VERBOSEadb shell setprop log.tag.Glide VERBOSEadb shell setprop log.tag.ImageHeaderParser VERBOSEadb shell setprop log.tag.ImageVideoDecoder VERBOSEadb shell setprop log.tag.IVML VERBOSEadb shell setprop log.tag.LocalUriFetcher VERBOSEadb shell setprop log.tag.LruBitmapPool VERBOSEadb shell setprop log.tag.MediaStoreThumbFetcher VERBOSEadb shell setprop log.tag.MemorySizeCalculator VERBOSEadb shell setprop log.tag.PreFillRunner VERBOSEadb shell setprop log.tag.ResourceLoader VERBOSEadb shell setprop log.tag.RMRetriever VERBOSEadb shell setprop log.tag.StreamEncoder VERBOSEadb shell setprop log.tag.TransformationUtils VERBOSE","tags":[{"name":"Glide","slug":"Glide","permalink":"http://www.imzhiqiang.com/tags/Glide/"},{"name":"调试","slug":"调试","permalink":"http://www.imzhiqiang.com/tags/%E8%B0%83%E8%AF%95/"}]},{"title":"记录一次v4包中SwipeRefreshLayout的“坑爹”事件","date":"2015-11-19T05:29:16.000Z","path":"2015/11/19/An-accident-about-SwipeRefreshLayout-in-support-v4-package/","text":"当 android support v4 包和 v7 包版本不一样时,v4 包中的 SwipeRefreshLayout 和 v7 包中的 RecyclerView 不能很好地一起工作,会导致 SwipeRefreshLayout 下拉刷新时动画卡住的情况,类似下面的情况: 详情请参考: http://stackoverflow.com/questions/33032036/swiperefreshlayout-freezes-on-api-4-2-2。 如果你的项目中用到了photoview,并且 targetSdk 还是 22 的话,请不要使用默认的 Gradle 引用: 1compile 'com.commit451:PhotoView:1.2.4' 因为这个版本的 photoview 默认依赖了 support v4 23.0.1 版本,使得 23.0.1 版本中的 SwipeRefreshLayout 和 22 版本中的 RecyclerView 不能很好地协调工作。 可以使用下面的引用来引入 photoview: 1compile 'com.github.chrisbanes.photoview:library:1.2.4' 如果不是特意去查看,你还很难发现这个细节,因为 targetSdk 为 22 的情况下是不会引用 23.0.1 版本的 v4 包的,除非你项目中有其他的 library 引用了。 小结在引入官方 support 库时,务必保证版本一致;在引入第三方库时,务必查看第三方库的依赖库,免坑。","tags":[{"name":"坑","slug":"坑","permalink":"http://www.imzhiqiang.com/tags/%E5%9D%91/"},{"name":"SwipeRefreshLayout","slug":"SwipeRefreshLayout","permalink":"http://www.imzhiqiang.com/tags/SwipeRefreshLayout/"}]},{"title":"使用Glide加载图片系列之一从不同的数据源加载图片","date":"2015-10-27T14:12:47.000Z","path":"2015/10/27/Load-images-from-difference-data-sources/","text":"与其他图片加载库相同,Glide 除了可以加载网络图片之外,也可以加载本地图片。甚至还可以从各种各样奇葩的数据源中加载图片。 加载网络图片很多情况下,我们使用图片加载库就是为了加载网络图片。网络操作是一个很复杂的东西。试想一下,如果没有图片加载库,我们就要手动去下载图片,缓存图片,最后再从文件里面读取 bitmap 并设置到 Imageview 里面。这还算好的,要是在 Listview 里面你会更头疼的。原因我就不说了,你懂的~~再加上各种各样的 Bitmap 操作,保准你再也不想撸代码了。而且 Bitmap 这东西还很占内存,伺候不好,很容易就会引发 OOM,app 吧唧就闪退了!! 图片加载库的优势就在于此。简简单单一句话,下载,缓存,加载统统搞定。简直就是美好一生的东西。而 Glide 就是这样使人美好一生的东西之一。 说了这么多,Glide 如何加载网络图片?很简单,就上次的三句话: 123456ImageView targetImageView = (ImageView) findViewById(R.id.imageView);String internetUrl = "http://i.imgur.com/idojSYm.png";Glide .with(context) .load(internetUrl) .into(targetImageView); 木有什么乱七八糟的东西,直接传入要加载图片的 url 就可以了。那么图片加载库有很多,为什么选择 Glide 呢?很简单,因为它流畅,不卡,尤其是在 Listview 中。嗯,就是酱~ 加载本地图片下表是.load()可以传入的参数及说明 参数 说明 .load(String string) string 可以为一个文件路径、uri 或者 url .load(Uri uri) uri 类型 .load(File file) 文件 .load(Integer resourceId) 资源 Id,R.drawable.xxx 或者 R.mipmap.xxx .load(byte[] model) byte[]类型 .load(T model) 自定义类型 从上面可以看到 Glide 不仅可以加载网络图片,还可以加载本地图片。可接受的参数有文件路径,uri,文件,资源 id 等。基本上满足了大部分的需求。虽然加载本地图片不像网络图片那样复杂,但我还是建议使用 Glide 来加载本地图片。因为它是内存友好的,而且还会“偷偷地”帮我们做很多事情。比如内存缓存,Bitmap 复用,修正照片方向等。当然为了满足各种各样的需求,仅仅加载图片是不够的,你还需要对图片进行各种各样的变换,也就是 Transformation。后面我们会详细了解的。 加载自定义数据源前面的表格中有一个是我们不熟悉的,就是.load(T model),即自定义的数据源类型。那么如何去实现呢? 实际上,加载自定义数据源主要是通过 ModelLoader 接口来实现的。由于没有在实际项目中用到过,这方面的经验比较少。想深入了解的,可以参考这篇文章。 不过从官方 Wiki 上来看,设计 ModelLoader 接口的初衷用来加载不同尺寸的图片的。众所周知,Android 设备屏幕分辨率千奇百怪,大到 2K,小到 320p。如果在低分辨率的手机上加载大图,不仅损耗用户流量,而且很容易造成 OOM;在高分辨的手机上,加载小图又会出现模糊的情况,用户体验极差。很多时候,为了省事,很多 app 都会选择一个中间分辨率,然后自适应大小。当然这样做无可厚非,但是有更好的办法,我们为什么不去尝试呢? 那么如何使用 Glide 来实现这一具体需求呢?首先你要实现自己的 ModelLoader,比较简单的方法是继承BaseGlideUrlLoader。 12345678910public interface MyDataModel { public String buildUrl(int width, int height);}public class MyUrlLoader extends BaseGlideUrlLoader<MyDataModel> { @Override protected String getUrl(MyDataModel model, int width, int height) { // Construct the url for the correct size here. return model.buildUrl(width, height); }} 接下来我们可以这样来加载图片: 12345678910111213Glide.with(this) .using(new MyUrlLoader(this)) .load(new MyDataModel() { @Override public String buidUrl(int width, int height) { if (width >= 600) { return url1; } else { return url2; } } }) .into(imageView); .using(new MyUrlLoader(this)):使用我们自己的 ModeLoader;.load(new MyDataModel()):加载我们自定义的数据源 这里需要解释下 getUrl 的三个参数:model:你加载的数据源width:你加载的图片的宽度(px)height:你加载的图片的高度(px) 这样,我们在高分率的设备上加载大图的 url1,在低分辨率的设备上加载小图 url2。从而实现了根据不同手机上的像素值大小加载不同尺寸的图片的需求。 当然如果你不想每次都是用.using(new MyUrlLoader()),就需要实现一个自定义的ModelLoaderFactory并在 GlideModule 中注册。 12345678public class MyGlideModule implements GlideModule { ... @Override public void registerComponents(Context context, Glide glide) { glide.register(MyDataModel.class, InputStream.class, new MyUrlLoader.Factory()); }} 同时也要在 AndroidManifest.xml 声明 123<meta-data android:name="com.mypackage.MyGlideModule" android:value="GlideModule" /> 如果你有多个自定义的 GlideModule 类,那么也要在 AndroidManifest.xml 中声明多个 GlideModule。 对于上面的加载不同尺寸的图片,Google 的 2014 年 I/O 大会 App 中有一篇文章专门用来介绍这个的,地址在这里。大概原理是这样子的: 在服务端有下面的几个可以加载的 url: URL 图片大小 myserver.com/images/__w-200-400-600-800-1000__/session1.jpg 原始尺寸 myserver.com/images/w200/session1.jpg 200px myserver.com/images/w400/session1.jpg 400px myserver.com/images/w600/session1.jpg 600px myserver.com/images/w800/session1.jpg 800px myserver.com/images/w1000/session1.jpg 1000px 那么客户端如何根据不同的手机分辨率去加载不同的 url 呢? Google 是这样做的,下面是核心代码: 123456789101112131415161718192021222324//定义正则表达式private static final Pattern PATTERN = Pattern.compile("__w-((?:-?\\\\d+)+)__");@Overrideprotected String getUrl(String model, int width, int height) { Matcher m = PATTERN.matcher(model); int bestBucket = 0; if (m.find()) { String[] found = m.group(1).split("-");//拿到可以加载的尺寸数组 for (String bucketStr : found) { bestBucket = Integer.parseInt(bucketStr); if (bestBucket >= width) {//刚好大于要加载的尺寸,直接跳出循环 // the best bucket is the first immediately // bigger than the requested width break; } } if (bestBucket > 0) {//返回合适尺寸的url model = m.replaceFirst("w"+bestBucket); } } return model;} 大概的步骤如下: 1.根据服务端可加载的图片 url 定义正则表达式 2.根据正则匹配,获取到可以加载的图片尺寸数组 3.根据要加载的 Imageview 的大小,选择合适的尺寸的 url 4.拼接 url 并返回 上面的例子中有 200,400,600,800,1000 是可以加载的,如果你要加载的 Imageview 的大小为 600px,当遍历数组到 600 时,就会直接跳出循环,返回 600px 大小图片的 url,Glide 就会加载 600px 的图片。 最后送上一个小 demo:https://github.com/Alluretears/GldieDemo","tags":[{"name":"Glide","slug":"Glide","permalink":"http://www.imzhiqiang.com/tags/Glide/"},{"name":"数据源","slug":"数据源","permalink":"http://www.imzhiqiang.com/tags/%E6%95%B0%E6%8D%AE%E6%BA%90/"}]},{"title":"使用Glide加载图片系列之零初识Glide","date":"2015-10-27T14:04:06.000Z","path":"2015/10/27/Hello-Glide/","text":"废话不多说,开撸(Junk Words No More Say,Let’s LOL) 咋添加 Glide 依赖呀?一句话(One Word): 1compile 'com.github.bumptech.glide:glide:3.6.1' 不多说(No More Say)。 PhotoShop:如果你还在用一颗利普斯(eclipse),请前往 Glide 在 github 上的发布页,下载最新的 jar 包吧,地址是 sha?自己找 →_→ 最简单的加载方式123456ImageView targetImageView = (ImageView) findViewById(R.id.imageView);String internetUrl = "http://i.imgur.com/idojSYm.png";Glide .with(context) .load(internetUrl) .into(targetImageView); 这都嘛意思啊?莫慌,我来装下 B: .with(Context context),此处的 context 就是你熟悉的 context,没啥不一样的。就是有一点,里面可以是 fragment。 .load(String imageUrl),你要加载的图片地址,当然也可以是 uri 或者 file,亦或是 R.drawable.xxxx。 .into(ImageView targetImageView),你要加载的 Imageview 控件,当然也可以一个 view,不过那都是后话。","tags":[{"name":"Glide","slug":"Glide","permalink":"http://www.imzhiqiang.com/tags/Glide/"},{"name":"图片加载库","slug":"图片加载库","permalink":"http://www.imzhiqiang.com/tags/%E5%9B%BE%E7%89%87%E5%8A%A0%E8%BD%BD%E5%BA%93/"}]},{"title":"Glide :一个专注于平滑滚动的图片加载和缓存库","date":"2015-08-23T08:19:12.000Z","path":"2015/08/23/Some-tips-with-Glide/","text":"在图片加载库烂大街的今天,选择一个适合自己使用的图片加载库已经成为了每一个 Android 开发者的必经之路。现在市面上知名的图片加载库有UIL,Picasso,Volley ImageLoader,Fresco以及我们今天的主角Glide。它们各有千秋,不能评定谁一定比谁好,只能说哪一个更适合你。 我的理解下面我来谈一下个人对这些图片加载库的理解,如有错误,还望指教。 Universal Image Loader:一个强大的图片加载库,包含各种各样的配置,最老牌,使用也最广泛。 Picasso: Square 出品,必属精品。和 OkHttp 搭配起来更配呦! Volley ImageLoader:Google 官方出品,可惜不能加载本地图片~ Fresco:Facebook 出的,天生骄傲!不是一般的强大。 Glide:Google 推荐的图片加载库,专注于流畅的滚动。 更多详情请看 stackoverflow 上这个问题。 初试 Glide下面进入今天的主题,相信之前很多同学都看到过这篇介绍 Glide 的文章,中文版在这里。文中从各个方面介绍和比较了 Glide 与 Picasso,总体来说二者极为相似,有着近乎相同的 API 的使用风格。但 Glide 在缓存策略和加载 GIF 方面略胜一筹。最后作者也极力推荐了这个库。 而且据说在 Google 新出的 Photos 应用中,到处可见 Glide 的踪迹。看到这里,你是不是已经迫不及待的想试一试这个库呢?就在你下定决心尝试一记的时候,你又听说 Yelp app(据说是美国的大众点评)也在使用这个吊炸天的库。你的心中激动万分,发四一定要使用这个库。说干就干,打开 Android Studio,在 builde.gradle 里面添加上 1compile 'com.github.bumptech.glide:glide:3.6.1' 然后全局搜索图片加载的地方,全部换成了下面的代码: 12345Glide.with(mContext) .load(url) .placeholder(R.drawable.loading_spinner) .crossFade() .into(myImageView); 在经过漫长的编译过程之后,再次打开 APP,看到有着渐现效果的图片呈现在你的面前,你不禁叫道:“wocao,真 TM 帅!为什么我以前没有发现呢?”。 不过在你使用了几天之后你会发现一些问题: 为什么 有的图片第一次加载的时候只显示占位图,第二次才显示正常的图片呢? 为什么 我总会得到类似 You cannot start a load for a destroyed activity 这样的异常呢? 为什么 我不能给加载的图片 setTag()呢? 为什么?为什么?这么 NB 的库竟然会有这么多的问题。没错,这就是我今天要讲的重点。怎么避免上面的问题发生。 一些解决方案1.如果你刚好使用了这个圆形Imageview 库或者其他的一些自定义的圆形 Imageview,而你又刚好设置了占位的话,那么,你就会遇到第一个问题。如何解决呢?方案一: 不设置占位;方案二:使用 Glide 的 Transformation API 自定义圆形 Bitmap 的转换。这里是一个已有的例子;方案三:使用下面的代码加载图片: 123456789Glide.with(mContext) .load(url) .placeholder(R.drawable.loading_spinner) .into(new SimpleTarget<Bitmap>(width, height) { @Override public void onResourceReady(Bitmap bitmap, GlideAnimation anim) { // setImageBitmap(bitmap) on CircleImageView } }; 2.至于第二个问题,请记住一句话:不要再非主线程里面使用 Glide 加载图片,如果真的使用了,请把 context 参数换成 getApplicationContext。更多的细节请参考这个 issue。 3.为什么不能设置 Tag,是因为你使用的姿势不对哦。如何为 ImageView 设置 Tag 呢?且听我细细道来。方案一:使用 setTag(int,object)方法设置 tag,具体用法如下:Java 代码是酱紫的: 12345678Glide.with(context).load(urls.get(i).getUrl()).fitCenter().into(imageViewHolder.image); imageViewHolder.image.setTag(R.id.image_tag, i); imageViewHolder.image.setOnClickListener(new View.OnClickListener() { @Override int position = (int) v.getTag(R.id.image_tag); Toast.makeText(context, urls.get(position).getWho(), Toast.LENGTH_SHORT).show(); } }); 同时在 values 文件夹下新建 ids.xml,添加 1<item name="image_tag" type="id"/> 大功告成! 方案二:从 Glide 的 3.6.0 之后,新添加了全局设置的方法。具体方法如下:先实现 GlideMoudle 接口,全局设置 ViewTaget 的 tagId: 1234567891011public class MyGlideMoudle implements GlideModule{ @Override public void applyOptions(Context context, GlideBuilder builder) { ViewTarget.setTagId(R.id.glide_tag_id); } @Override public void registerComponents(Context context, Glide glide) { }} 同样,也需要在 ids.xml 下添加 id 1<item name="glide_tag_id" type="id"/> 最后在 AndroidManifest.xml 文件里面添加 123<meta-data android:name="com.yourpackagename.MyGlideMoudle" android:value="GlideModule" /> 又可以愉快的玩耍了,嘻嘻`(∩_∩)′。 方案三:写一个继承自 ImageViewTaget 的类,复写它的 get/setRequest 方法。 12345678910111213141516171819202122232425Glide.with(context).load(urls.get(i).getUrl()).fitCenter().into(new ImageViewTarget<GlideDrawable>(imageViewHolder.image) { @Override protected void setResource(GlideDrawable resource) { imageViewHolder.image.setImageDrawable(resource); } @Override public void setRequest(Request request) { imageViewHolder.image.setTag(i); imageViewHolder.image.setTag(R.id.glide_tag_id,request); } @Override public Request getRequest() { return (Request) imageViewHolder.image.getTag(R.id.glide_tag_id); } }); imageViewHolder.image.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { int position = (int) v.getTag(); Toast.makeText(context, urls.get(position).getWho(), Toast.LENGTH_SHORT).show(); } }); 一些使用技巧1.Glide.with(context).resumeRequests()和 Glide.with(context).pauseRequests() 当列表在滑动的时候,调用 pauseRequests()取消请求,滑动停止时,调用 resumeRequests()恢复请求。这样是不是会好些呢? 2.Glide.clear() 当你想清除掉所有的图片加载请求时,这个方法可以帮助到你。 3.ListPreloader 如果你想让列表预加载的话,不妨试一下 ListPreloader 这个类。 一些基于 Glide 的优秀库1.glide-transformations 一个基于 Glide 的 transformation 库,拥有裁剪,着色,模糊,滤镜等多种转换效果,赞的不行不行的~~ 2.GlidePalette 一个可以在 Glide 加载时很方便使用 Palette 的库。","tags":[{"name":"Glide","slug":"Glide","permalink":"http://www.imzhiqiang.com/tags/Glide/"},{"name":"图片加载库","slug":"图片加载库","permalink":"http://www.imzhiqiang.com/tags/%E5%9B%BE%E7%89%87%E5%8A%A0%E8%BD%BD%E5%BA%93/"}]},{"title":"Retrofit系列之二:Retrofit的基本身份验证(译)","date":"2015-07-16T11:44:11.000Z","path":"2015/07/16/Retrofit-Basic-Authentication-on-Android/","text":"这是 Retrofit 系列文章的第二篇。将介绍了如何使用 Retrofit 完成用户名/邮箱和密码的身份验证。 在前面的章节里,我们已经创建了一个初始版本的 API/HTTP 请求客户端。我们会在此基础上增加基本的身份验证功能。 集成基本的身份验证让我们更新下 ServiceGenerator 类,创建一个添加验证的方法。下面的代码片段是基于上面的类的。同时也为下面 1.9 版本的代码添加了 2.0 的示例。如果你依赖于 Retrofit 2.0 版本,可以直接跳过 1.9 的例子,直接看第二个代码块:) Retrofit 1.9123456789101112131415161718192021222324252627282930313233public class ServiceGenerator { public static final String API_BASE_URL = "http://your.api-base.url"; private static RestAdapter.Builder builder = new RestAdapter.Builder() .setEndpoint(API_BASE_URL) .setClient(new OkClient(new OkHttpClient())); public static <S> S createService(Class<S> serviceClass) { return createService(serviceClass, null, null); } public static <S> S createService(Class<S> serviceClass, String username, String password) { if (username != null && password != null) { // concatenate username and password with colon for authentication String credentials = username + ":" + password; // create Base64 encodet string final String basic = "Basic " + Base64.encodeToString(credentials.getBytes(), Base64.NO_WRAP); builder.setRequestInterceptor(new RequestInterceptor() { @Override public void intercept(RequestFacade request) { request.addHeader("Authorization", basic); request.addHeader("Acceppt", "application/json"); } }); } RestAdapter adapter = builder.build(); return adapter.create(serviceClass); }} Retrofit 212345678910111213141516171819202122232425262728293031323334353637383940public class ServiceGenerator { public static final String API_BASE_URL = "http://your.api-base.url"; private static OkHttpClient httpClient = new OkHttpClient(); private static Retrofit.Builder builder = new Retrofit.Builder() .baseUrl(API_BASE_URL) .addConverterFactory(GsonConverterFactory.create()); public static <S> S createService(Class<S> serviceClass) { return createService(serviceClass, null, null); } public static <S> S createService(Class<S> serviceClass, String username, String password) { if (username != null && password != null) { String credentials = username + ":" + password; final String basic = "Basic " + Base64.encodeToString(credentials.getBytes(), Base64.NO_WRAP); httpClient.interceptors().add(new Interceptor() { @Override public Response intercept(Interceptor.Chain chain) throws IOException { Request original = chain.request(); Request.Builder requestBuilder = original.newBuilder() .header("Authorization", basic); .header("Accept", "applicaton/json"); .method(original.method(), original.body()); Request request = requestBuilder.build(); return chain.proceed(request); } }); } Retrofit retrofit = builder.client(httpClient).build(); return retrofit.create(serviceClass); }} 新的方法有两个参数:用户名 和 密码。当然你也可以用邮箱来代替用户名。与在第一个方法中创建客户端的方法是一样的:使用 RestAdapter (Retrofit 2 中为 Retrofit)类设置基本的 url 和 OkHttpClient。 不同的是:我们使用一个请求拦截器 RequestInterceptor (Retrofit 2 中为 Interceptor )来设置基础 url Http 请求的头信息。如果提供了用户名和密码,会执行新的方法。如果用户名和密码不正确时,会创建第一个方法中的客户端。这是我们简化第一个方法的原因。 在验证部分我们必须调整用户名/邮箱和密码的格式。基础身份验证需要一个冒号连接两个值的字符串。此外,新创建的字符串必须经过 Base64 编码。 几乎任何一个 webservice 和 API 的验证都是在 HTTP 的请求头里面的。这是我们在请求头里面设置编码值的原因。 如果你想接收特殊格式的服务器响应时,Accent 头是很重要的。例如,我们想接收 JSON 格式的响应,因为 Retrofit 和 Google 的 GSON 配合会将 java 对象序列化为 JSON,反过来也可以。 用法调用我们之前写的 ServiceGenerator 类的新方法。例如,下面代码定义的 LoginService。 Retrofit 1.91234public interface LoginService { @POST("/login") User basicLogin();} Retrofit 21234public interface LoginService { @POST("/login") Call<User> basicLogin();} 上面的接口只包含一个 baseLogin 的方法。该方法返回 User 类型,没有任何的查询参数。 现在你可以通过给定的证书(用户名和密码)创建你的 HTTP 客户端了。 Retrofit 1.9123LoginService loginService = ServiceGenerator .createService(LoginService.class, "user", "secretpassword");User user = loginService.login(); Retrofit 21234LoginService loginService = ServiceGenerator .createService(LoginService.class, "user", "secretpassword");Call<User> call = loginService.login();User user = call.execute().body(); ServiceGenerator 的方法会创建一个包含验证信息的 HTTP 客户端。一旦你调用 loginService 的 basicLogin 方法,被提供的证书将会被自动传递到你定义的基础 url 了。 接下来是什么下篇文章将介绍如何使用 Retrofit 实现 OAuth。 原文地址:https://futurestud.io/blog/android-basic-authentication-with-retrofit/","tags":[{"name":"Retrofit","slug":"Retrofit","permalink":"http://www.imzhiqiang.com/tags/Retrofit/"},{"name":"身份验证","slug":"身份验证","permalink":"http://www.imzhiqiang.com/tags/%E8%BA%AB%E4%BB%BD%E9%AA%8C%E8%AF%81/"},{"name":"Authentication","slug":"Authentication","permalink":"http://www.imzhiqiang.com/tags/Authentication/"}]},{"title":"Retrofit系列之一:开始创建Retrofit客户端(译)","date":"2015-07-09T15:07:16.000Z","path":"2015/07/09/Retrofit-Getting-Started-and-Create-an-Android-Client/","text":"这是 Retrofit 系列文章的第一篇。该系列通过通过几个用例来探讨 Retrofit 的功能和扩展。 这篇博文中,我们将通过 Retrofit 的基础知识来创建一个 API 或者 Http 请求的 Android 客户端。 然而,这篇文章并不包含太多关于 Retrofit 和如何开始使用她的信息,如果你想了解更多,请访问该项目的主页。 什么是 Retrofit据 Retrofit 的官方页面描述,她是 一个类型安全的 Android 或 Java 的 REST 客户端 你可以使用注解来描述 Http 请求,URL 参数替换,查询参数支持。这些都是默认集成的。此外,她还提供了多种多样的请求体和文件上传。 如何声明(API)请求请访问 Retrofit 的主页阅读她的 API 文档说明,了解如何发起请求。通过清晰的代码示例你会获取到所有的重要信息。 准备你的 Android 工程现在让我们的手回到键盘开始干活。如果你已经创建好工程,请直接看下一段。不然你就在你喜爱的 IDE 里面新建一个项目。我们更倾向于 Gradle 的构建系统,如果你使用 Maven 也是可以的。 声明依赖:Gradle 或者 Maven现在让我们为自己的工程添加 Retrofit 的依赖。选择你的构建系统在 pom.xml 或者 build.gradle 里面声明 Retrofit 的依赖。当构建系统开始编译你的代码时,他会自动下载并为工程添加依赖库。我们也倾向于把 Retrofit 和 OkHTTP 结合使用。 Retrofit 1.9pom.xml 12345678910<dependency> <groupId>com.squareup.retrofit</groupId> <artifactId>retrofit</artifactId> <version>1.9.0</version></dependency><dependency> <groupId>com.squareup.okhttp</groupId> <artifactId>okhttp</artifactId> <version>2.4.0</version></dependency> build.gradle 1234dependencies { compile 'com.squareup.retrofit:retrofit:1.9.0' compile 'com.squareup.okhttp:okhttp:2.4.0'} Retrofit 2如果你正在使用 Retrofit 2 请使用下面的依赖。 pom.xml 12345<dependency> <groupId>com.squareup.retrofit</groupId> <artifactId>retrofit</artifactId> <version>2.2.0-beta2</version></dependency> build.gradle 1234dependencies { // Retrofit & OkHttp compile 'com.squareup.retrofit:retrofit:2.0.0-beta2'} Retrofit 2 现在已经默认使用 OkHttp 作为网络层。你不需要为你的工程添加 OkHttp 的依赖了,除非你需要一个特定的版本。 现在你的工程已经集成了 Retrofit,让我们创造一个持久的 Android API/HTTP 客户端。 可持续的 Android 客户端在研究已存在的 Retrofit 的客户端时,Bart Kiers 的仓库出现了。事实上,它是一个使用 Retrofit 的 Oauth 验证实例。它为一个可持续的 Android 客户端提供了必要的基本原理。这是我们为什么使用它作为一个稳定的基础。在未来的博文中,我们会进一步扩展验证功能。 下面的类定义了我们 Android 客户端的基础:ServiceGenerator Service GeneratorServiceGenerator 是我们 API/HTTP 客户端的核心.现在, 她仅仅定义了一个方法为给定的类或者接口创建一个基本的 REST adapter。这里是代码: Retrofit 1.91234567891011121314public class ServiceGenerator { public static final String API_BASE_URL = "http://your.api-base.url"; private static RestAdapter.Builder builder = new RestAdapter.Builder() .setEndpoint(API_BASE_URL) .setClient(new OkClient(new OkHttpClient())); public static <S> S createService(Class<S> serviceClass) { RestAdapter adapter = builder.build(); return adapter.create(serviceClass); }} Retrofit 212345678910111213141516public class ServiceGenerator { public static final String API_BASE_URL = "http://your.api-base.url"; private static OkHttpClient httpClient = new OkHttpClient(); private static Retrofit.Builder builder = new Retrofit.Builder() .baseUrl(API_BASE_URL) .addConverterFactory(GsonConverterFactory.create()); public static <S> S createService(Class<S> serviceClass) { Retrofit retrofit = builder.client(httpClient).build(); return retrofit.create(serviceClass); }} ServiceGenerator 类使用 Retrofit 的 RestAdapter-Builder 来创建一个新的 REST 客户端通过一个被给定基础 url 的 API。例如,GitHub 的 API 基础 url 是 https://developer.github.com/v3/ 。serviceClass 定义了 API 请求的类或者接口。下面展示了 Retrofit 的用法和如何定义一个实例客户端。 JSON MappingRetrofit 1.9 默认使用 Google 的 Gson。你所要做的就是定义你的响应对象类,响应会自动映射为 Java 对象。 当使用 Retrofit 2 时,你需要为 Retrofit 对象添加一个 converter。这就是我们为什么会集成 GSON 到在 Retrofit 构建过程中要调用 .addConverterFactory(GsonConverterFactory.create()) 的原因了。 使用 Retrofit好, 现在让我们定义一个从 github 请求数据的客户端实例。首先,我们需要创建一个接口并定义一个所需的方法。 GitHub 客户端下面的代码定义了 GitHubClient 和一个请求某仓库贡献者列表的方法。它说明了 Retrofit 参数替代功能的用法。(当调用方法时,指定路径中的 owner 和 repo 将会被替换为给定的变量) Retrofit 1.91234567public interface GitHubClient { @GET("/repos/{owner}/{repo}/contributors") List<Contributor> contributors( @Path("owner") String owner, @Path("repo") String repo );} Retrofit 21234567public interface GitHubClient { @GET("/repos/{owner}/{repo}/contributors") Call<List<Contributor>> contributors( @Path("owner") String owner, @Path("repo") String repo );} 这是 Contributor 类的定义.该类包含了响应数据映射所需的属性。 1234static class Contributor { String login; int contributions; } 关于前面提到的 JSON 映射:GitHubClient 定义了一个名为 contributors 返回类型为 List<Contributor> 的方法。Retrofit 确保服务器响应可以正确的得到映射。(在响应与给定的类匹配时) API 实例请求下面的代码片段说明了 ServiceGenerator 实例化你客户端的用法。Github 客户端通过调用方法来获取仓库贡献者的列表。这段代码是一个 Retrofit GitHub 客户端示例的修改版。 你需要为 ServiceGenerator 手动定义一个基本的 url https://developer.github.com/v3/ 。 Retrofit 1.912345678910111213public static void main(String... args) { // Create a very simple REST adapter which points the GitHub API endpoint. GitHubClient client = ServiceGenerator.createService(GitHubClient.class); // Fetch and print a list of the contributors to this library. List<Contributor> contributors = client.contributors("fs_opensource", "android-boilerplate"); for (Contributor contributor : contributors) { System.out.println( contributor.login + " (" + contributor.contributions + ")"); }} Retrofit 2123456789101112131415public static void main(String... args) { // Create a very simple REST adapter which points the GitHub API endpoint. GitHubClient client = ServiceGenerator.createService(GitHubClient.class); // Fetch and print a list of the contributors to this library. Call<List<Contributor>> call = client.contributors("fs_opensource", "android-boilerplate"); List<Contributor> contributors = call.execute().body(); for (Contributor contributor : contributors) { System.out.println( contributor.login + " (" + contributor.contributions + ")"); }} 接下来是什么下篇文章将阐述如何使用 Retrofit 实现一个基本验证。 很高兴您可以阅读 Retrofit 系列的第一篇文章:) 原文地址:https://futurestud.io/blog/retrofit-getting-started-and-android-client/","tags":[{"name":"Retrofit","slug":"Retrofit","permalink":"http://www.imzhiqiang.com/tags/Retrofit/"},{"name":"REST","slug":"REST","permalink":"http://www.imzhiqiang.com/tags/REST/"},{"name":"HTTP请求","slug":"HTTP请求","permalink":"http://www.imzhiqiang.com/tags/HTTP%E8%AF%B7%E6%B1%82/"}]},{"title":"HelloWorld","date":"2015-06-20T09:45:18.000Z","path":"2015/06/20/HelloWorld/","text":"端午节突然想用 Github Pages 搭一个自己的博客。折腾了一天,终于搞定了。感谢 Github,提供了一个免费的空间;感谢 hexo 的创造者,帮我解决了很多头疼的事情;感谢网上的各路大神,让我一路披荆斩棘,最后成功搭建博客。 以后这就是额的小天地了,先来篇 HelloWorld,然后安安静静地做自己的码农吧! 最后感谢黎小腾同学的 Yilia 主题,我很喜欢!","tags":[{"name":"博客","slug":"博客","permalink":"http://www.imzhiqiang.com/tags/%E5%8D%9A%E5%AE%A2/"},{"name":"hexo","slug":"hexo","permalink":"http://www.imzhiqiang.com/tags/hexo/"},{"name":"处女篇","slug":"处女篇","permalink":"http://www.imzhiqiang.com/tags/%E5%A4%84%E5%A5%B3%E7%AF%87/"},{"name":"随笔","slug":"随笔","permalink":"http://www.imzhiqiang.com/tags/%E9%9A%8F%E7%AC%94/"}]}]