Android开辟-标题随笔

程序员 2024-9-17 14:32:25 36 0 来自 中国
迩来团体过了一下项目的代码,发现一些小细节标题和小瑕疵比力多,这些标题大多具有肯定的通性,顺手纪录一下。如果有人看到这篇文章,渴望能对你有资助。
Jetpack Collection vs Java Collection.

Map, Set等数据布局在项目中非常广泛的使用,很多环境下,这些数据布局需要存储的数据量都不大。
val map = mapOf<K, V>()val set = setOf<k, V>()着实Android为这些存储少量的数据的聚集做了专门的优化,而且从framework.jar剥离出来,放到Jetpack工具包中。这些优化主要在内存上,能够有效低落内存使用。以Java的HashMap为例,每条纪录使用Map.Entry来纪录。除了须要的
Node<K,V>[] table(hash桶)来管理数据,另有Set<Map.Entry<K,V>> entrySet如许的数据来提升易用性。在存储数据量比力少的环境下,这些额外的数据大概会比现实存储的数据占的内存还多。
Jetpack Collection中的ArrayMap和ArraySet等数据布局专门为这些小数据量的环境做了优化,去除了Entry这些辅助的类对象的开销。
现在简单分析一下ArrayMap的实现方式,ArrayMap使用两个数组来存储数据,一个是int[] hash,用来存储Key键的hash值,别的一个是Object[]用来存储Key和Value的值,而且在扩容的时间,体现的非常吝啬,由于ArrayMap扩容举动很自制,只是数组的Copy,使用的体系函数System::arraycopy,但是HashMap如果要扩容,代价就要大得多。
这里有个点特殊分析一下,int[] hash这个数组要保持有序,在有序的条件下,可以使用二分查找。标题是如安在不停新增数据的同时,怎样保持有序呢?原来在新增数据的时间,要举行类似于插入排序的使用,以是ArrayMap的put使用的时间复杂度,着实是O(n)吗?由于需要把数组中大于当前hash值的全部元素都今后移动。
比方,当前的int[] hash = {2, 5, 7, 9, 10}, 需要插入的Key的hash值是8,那么,9,10两个值需要今后移一位,然后在7背面设置为8。末了数组变为,
int[] hash = {2, 5, 7, 8, 9, 10}。
以是插入使用的时间复杂度真的为O(n)吗,着实我觉得不愿定,由于使用的是一块连续的内存,连续内存的使用可以使用memmove大概memcpy,插入hash值时间的可以做到O(1),但有一个二分查找的过程,以是插入时间复杂度为O(lgn)。以是这种机制比力得当在小数据量的时间,由于HashMap的插入时间复杂度是O(1),但是这个1代表的常量会比力大,在数据量较小的时间lg(n)会比这个1更有效。
着实另有别的一个角度来看这个标题,如果插入的时间复杂度真的为O(n)的话,就不消保持这个数组有序了,直接序次一次遍历查抄即可,而且少了保持数组有序的开销,以是System::arraycopy的时间复杂度必然为O(1)了。以是ArrayMap的插入使用的的时间复杂度为O(lgn)吗?着实我真也不确定,System::arraycopy着实是native函数实现的,需要去看一下Android的详细实现,Android的实现大概和Java的库实现也不一样。通用库函数的设计,函数详细实现方式着实是很难的一个权衡取舍,这个就不在这里说了。对于这个标题,我也不查源代码了,由于数据量很小,不消纠结这个标题。如果有人感爱好,可以求证一下,留个品评。
各人还记得SparseArray这个类吗?原理是ArrayMap一样,只是Key只能为Int,使用原始范例int,制止Integer的装箱,进一步提升效率。
现在这个Jetpack的Collection在徐徐丰富了,现在已经有10几个类了,在符合的场所使用,可以有效提升步调内存使用率,知道这些类的实现机制之后,也可以制止在不符合的时间使用这些类。
善用用位使用

我看到代码里面有几处的代码大抵是如许的。
class ItemDecorationHorizontalScroll(private val px: Int) : RecyclerView.ItemDecoration() {    override fun getItemOffsets(outRect: Rect, itemPosition: Int, parent: RecyclerView) {        outRect.left = px / 2        outRect.top = px / 2        outRect.right = px / 2        outRect.bottom = px / 2    }}在这环境下,整数除法,而且除数是2,在这种环境下,可以使用位使用来提升效率。这些代码不是标题,更多的是一些更好的代码规范。
代码可以改为:
class ItemDecorationHorizontalScroll(private val px: Int) : RecyclerView.ItemDecoration() {    override fun getItemOffsets(outRect: Rect, itemPosition: Int, parent: RecyclerView) {        outRect.left = px shr 1        outRect.top = px shr 1        outRect.right = px shr 1        outRect.bottom = px shr 1    }}大概有些同砚会这么觉得,这些位使用太受条件限定了,如果不是乘除2的幂次呢,是不是就不能用位运算了呢?
着实吧,未必呢?比方,如下代码:
fun time(a: Int): Int {    return a * 3}现在是一个整数乘以3,不是2呢,也不是4,这个地方可以有办法来提升效率的吗?着实想的话,也是可以的。可以用一下的方法来。
fun time(a: Int): Int {    return (a shr 1) + a}通过一个位移使用和一个加法使用来到达乘3的目的,位移应该是有一个指令就可以做到,加法也比乘法要快得多。
然后呢,另有哪些场景下可以使用位使用呢?应用层面一个范例的场景是用一个32位的整数来标识32个状态,一个Int数值,每一位标识一个状态,如果为1,状态为true,反之为false。Android 中的Intent就用一个Int数值来标识Intent各种状态。用来标识Activity的launch mode就是通过这些位的组合来标识的。其他的暂时没想到。
耽误盘算

这个是一个很常见,但是很少引起人的注意,由于很长时间内,代码都是如许的。后来函数式编程比力盛行之后,耽误盘算才徐徐引起人的注意。先来看一段代码:
LogUtils.d(TAG, "Load done, totalListSize: ${list.size}, resultListSize: ${resultList.size}, code: $code, msg: $msg, hasMore: $hasMore")这是一段常见的打Log的代码,也是从项目中直接拿出来的。在Debug版本中,会打出相应的信息,但是在Release的版本中,Log不会打出。缘故起因是一样平常都会在函数中作做控制。一个大概的环境是用一个变量来控制。
fun d(tag: String, message: String) {    if (DEBUG) {        android.util.Log.d(tag, message)    }}如果DEBUG为false,固然终极Log不会输出,但是已经产生了很多的无勤奋。Log Message已经被创建出来的,而且可以看到,有些环境下,代价还是不小的。有多个String被创建,如果类似的代码很多的话,还是会有不少不须要的开销的。
这种环境下,耽误盘算就能派上用场了。如果能把Log Message的创建耽误到真正使用的时间,就可以了。怎样做到呢?我们如许来界说函数:
fun d(tag: String, supplier: () -> String) {    if (DEBUG) {        Log.d(tag, supplier())    }}如许只有认真正使用的时间,才实验创建Log message。由于Java和Kotlin现在对函数式编程支持的很美满,语言自己已经界说了一些类,如Supplier,函数也可以写成如下的样子。
fun d(tag: String, Supplier<String> supplier) {    if (DEBUG) {        Log.d(tag, supplier.get())    }}调用的时间,相应的代码可以写成如许。
LogUtils.d(TAG) {    "Load done, totalListSize: ${list.size}, resultListSize: ${resultList.size}, code: $code, msg: $msg, hasMore: $hasMore"}固然,这个标题的终极解法方法应该是C/C++的宏界说,但是惋惜Java/Kotlin并不支持。
耽误盘算,着实在应用层应用非常广泛。假设有一个页面,有多个Tab,每个Tab著名称等相应的信息,另有一个Fragment与之相对于。但是只有当用户切换到相应的Tab时,Fragment才举行相应创建,如许可以进步效率。由于有大概用户不切换Tab就退出界面了。这种环境下,需要全部的Tab都要先创建出来,由于全部Tab的信息都要在开始的时间体现出来,但是对于的Fragment需要耽误创建。大概有其他更好的方法,但是一个比力符合的方式是代码如下写:
public class Tab {    public String titleName;    public Drawable drawable;    // 其他的一些属性    public Supplier<Fragment> action;}通过Tab类,把Tab相关的信息都封装起来,这个是最大的优点,封装很完备,包罗Fragment,但是Fragment并不立即创建,而是通过耽误化,到真正需要使用的时间才举行创建。
类似的耽误技术的使用很多,不再举其他例子了。
设置Collection的Capacity

先摘录一段代码,而且我信赖,绝大多数的工程师写类似的代码都是如许写的。
fun newInstance(param: String?): ContentFragment {    val fragment = ContentFragment()    val args = Bundle()    args.putString(ARG_PARAM, param)    fragment.arguments = args    return fragment}看不出有什么标题吧,我感觉至少99%的工程师都是如许写的。起首如许的写法没有标题,至少很多Android的官方的Demo也是如许写的。但是如果明白这段代码究竟干了什么事变,我们着实会发现有更好的写法。
起首,我们来看Bundle类,Bundle里面存储是Key-Value的数据,以是里面应该会包含一个Map,这个会是Kotlin函数mapOf<Key, Value>()返回的HashMap吗?
如果你看了本篇文章的第一节就会知道,Bundle是用来存储很少量的数据,绝大多数环境下,数据量是小于5的。以是Bundle内部着实是一个ArrayMap。
ArrayMap初始化的Capacity是多少呢,初始化为0。以是一开始举行put的时间,就会举行扩容,第一次是扩充Capacity为4。然后呢,着实我们只是需要Capacity为1,我们浪费了3。着实浪费的不是3,而是9。为什么呢,缘故起因就是第一节所说的,ArrayMap里面有两个数组,一个是hash值的数组,别的一个是key和value的数组,没错,key和value是存在同一个数组里面的。以是当Capacity = 4的环境下,第一个数组长度为4,第二个数组是8。如果put了一个key-value的环境下,第一个数组被用了1,第二个数组被用了2,统共浪费的空间为(4-1) + (8-2) = 9。浪费的空间竟然比现实使用的空间还要多。
如果这个Bundle会存5个数据呢,在放入第5个数据时间,ArrayMap又会举行扩容,然后有会造成空间的浪费。
如果一开始你知道会有多少数据的话,就直接把Capacity设置好,如许是不是会更好呢。不会有扩容的性能浪费,也不会有空间的浪费。
是不是有人会说,着实我早就不如许写代码了,Kotlin着实提供了一个函数叫bundleOf(),上面的代码可以写成如许。
fun newInstance(param: String?): ContentFragment {    val fragment = ContentFragment()    fragment.arguments = bundleOf(ARG_PARAM to param)    return fragment}诚然,Kotlin的bundleOf函数里面也思量了Capacity的标题。直接会设置正确的值。但是这个标题不是ArrayMap才会有的标题,在最经常用的ArrayList也会有同样的标题。以是我们在使用Collection的时间,如果我们知道详细的包含的数量,提前设置是个好主意。如果不是完全确定,但是大抵知道范围,着实也可以资助我们选择符合的值,至少最大水平的镌汰扩容造成的开销。
别的,bundleOf()这个函数好是好,但是平白无故的天生了本来不应被创建的Pair对象,然后又多了一次函数调用开销。
好吧,我感觉我是想多了。但是,我感觉上面的代码另有一个小标题,我们合在下一节继续讲。
滥用Kotlin语言特性

1. 扩展函数的滥用

先来看一段代码,代码里面使用了一些Kotlin的语言特性。
override fun onUpdateView(b: ViewDataBinding, data: SuggestCard) {    b.takeIf { it is HomeItemCategorySubSquareBinding }        ?.let { it as HomeItemCategorySubSquareBinding }        ?.let {            (repo.repoId == data.plid).let { isSelected ->                it.ivCardPlay.background = AppCompatResources.getDrawable(                    context,                    if (isSelected) R.drawable.home_ic_card_playing                    else R.drawable.home_selector_card_play,                )            }        }}第一眼看到这段代码,还是需要轻微认真的看一下,才知道这段代码究竟要干嘛,但是知道了究竟要干嘛之后,就会感觉这段代码写的不好,显啰嗦。似乎一个小孩,学会了一个新技能,然后碰到不管什么事变,都想要用这个新技能来办理全部的标题。
Kotlin着实提供了很多很好用的工具,但是这些工具需要工程师来举行公道的选择和组合。固然条件是能够,对这些工具富足认识。有些时间,着实用最质朴的方式写的代码,才最轻便有效。重写一下上面的代码如下:
override fun onUpdateView(b: ViewDataBinding, data: SuggestCard) {    if (b is HomeItemCategorySubSquareBinding) {        val resourceId = if (repo.repoId == data.plid) R.drawable.home_ic_card_playing else R.drawable.home_selector_card_play        b.ivCardPlay.background = AppCompatResources.getDrawable(context, resourceId)    }}这个代码写的一目了然,代码量也少了,也提升了代码的维护性。
2. 安全调用运算符

Kotlin的?.的是个好东西,把之前Java代码的判空使用的模板代码给清除掉了,代码更轻便。
?.背面做了什么事变,如果去看下编译之后的Java字节码,就会知道,着实?.还是会被编译成之前工程师手动写的判空代码。这个是Kotlin的设计原则,清除全部的模板代码。让工程师只写最紧张的逻辑代码,也使代码最大水平的轻便化。但是,这仅仅是Kotlin语言设计者的精美愿望,在看到下面的代码之后。
fun parseParams(intent: Intent?) {    val p1 = intent?.data?.getQueryParameter("key1")    val p2 = intent?.data?.getQueryParameter("key2")    val p3 = intent?.data?.getQueryParameter("key3")    val p4 = intent?.data?.getQueryParameter("key4")}代码看起来,也挺轻便的啊,但是这轻便的背后,是intent和intent的data被重复判空了4次。知道一个工具的使用,最好要相识一下这个工具到底是怎么工作呢。
终极的目的,可以让代码先在工程师的脑筋里先辈行编译,看到编译后的代码,然后再在脑筋里运行一下。没标题了,再在呆板上实验编译和运行。我看到这个代码的时间,立即就能看到被编译成Java字节码的样子,本来美感的代码就被破坏了。
3. 可空范例和不可空范例

这是Kotlin比Java更先辈的一个点,在Java 8中,Java使用Optional的类来想到达Kotlin想要做的事变,但是Kotlin的方案更方便,也更直接。
val str1: Stringval str2: String?一个变量,根据设计者的设计,可以拥有差别的范例。在Java中的String范例的变量,可以是一个正常的字符串,也可以为null,但是这两个值是截然差别的。工程师需要自己来做查抄,才可知道到底存储的什么值。换句话说,在Java中,你没法设计一个变量,让这个变量只存储详细的值,不能存储null。
Kotlin的可空范例办理了Java存成的标题。在这个可空范例的逻辑中,可空范例并不是非空范例的封装,string和string?是两个差别的范例。前者使用的环境是,代表一个变量,这个变量肯定是具有值的,如果这个值为null,步调就不能继续往下走,而是应该抛出非常,告知调用者,让调用者来处置处罚这个非常环境。string?这个范例体现的是,这个变量为null是正常的,可以继续往下走。
好了,我们来看上一节末了说的标题。
fun newInstance(param: String?): ContentFragment {    val fragment = ContentFragment()    fragment.arguments = bundleOf(ARG_PARAM to param)    return fragment}这里的参数,param: String?的范例,是否有想过,这个范例是可空范例,还是不可空范例?不管选择什么范例,提前是这个范例选择之前,肯定要根据现实环境来举行判定。也就是这个ContentFragment这个ARG_PARAM在设计的时间,到底是怎么定位的。从全部的代码来看,这个应该是一个必须的量,不能存在为空的大概。
我看到很多的代码,对于选择可空范例和非空范例,过于随意。
再举个例子,还记得在Fragment中获取Context的怎样获取吗?
Context requireContext()Context getContext()如果在Fragment onViewCreated获取Context,应该通过调用哪个函数获取?在这个环境下,应该使用第一个,由于在你的假想中,这个地方返回就不能为空,你也不消对这个Context做判空处置处罚。如果确实由于某种缘故起因,requireContext()返回的就是为null的,不做判空,步调不就崩了吗?如果真如所说,这种环境下,就应该让步调瓦解,由于步调状态已经不对了,是无法挽回的不对,这个时间越是积极挽回,就只能错的越多。别的,如果你往主线程的消息队列里面Post了一个使命,在这个使命中,你需要获取Context,你肯定要选择第二个,由于在这个环境下,Context是可以为空的,为空不代表步调堕落了。由于正常环境下,这个Context也可以为空,只是体现当前的Fragment已经detach了之前的Activity。Kotlin在这种环境下,欺压你对可空变量举行处置处罚。
4. 属性的Get方法

这也是一个常见的,容易滥用的地方。
val mColumnInterval: Int    get() = resources.getDimensionPixelSize(R.dimen.xxx)好好的界说一个属性岂非不好吗?非得使用get(),如果使用get()的话,每次使用到这个属性,都会实验这个方法,每次盘算出来的都是同一个值,为什么不把这个好好的存储在变量中。以下是编译之后的代码:
public final int getMColumnInterval() {   return this.getResources().getDimensionPixelSize(R.dimen.xxx);}这个是范例的滥用。不但是Get方法,另有lazy by的机制,也是经常会用到并不符合的地方。
滥用标题总结

Kotlin的各种语法糖也甜也不甜。花里胡哨的语法,粉饰了这些语法背面现实的处置处罚过程。我们需要明白这些语法背后做的事变,根据我们现实要办理的标题,公道选择,偶然候,像无锋重剑一样,大巧不工。
标题总结

着实另有很多其他的标题,好比在RecycleView的onBindView函数里面会创建对象,如许在滑动过程中就会有大量短生命周期的对象产生,严肃的还会有内存抖动的环境。差别代码分支可以归并的标题,另有同一个代码分支里面,重复盘算标题。对一些常见的编程标题,没有使用高效的方案等。由于这些都是一些小标题,小标题都有通性,现在总结了一点供各人参考。
如果要写出好的代码,我一直提倡代码需要先在脑筋里先辈行编译,再在脑筋里运行一下,之后才是在呆板上面编译和运行。
您需要登录后才可以回帖 登录 | 立即注册

Powered by CangBaoKu v1.0 小黑屋藏宝库It社区( 冀ICP备14008649号 )

GMT+8, 2024-10-18 22:36, Processed in 0.197977 second(s), 32 queries.© 2003-2025 cbk Team.

快速回复 返回顶部 返回列表