注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

Android IPC 之 Messenger

绑定服务(Bound Services)概述 绑定服务是client-server接口中的服务器。它允许组件(例如活动)绑定到服务、发送请求、接收响应和执行进程间通信(IPC)。 绑定服务通常仅在它为另一个应用程序组件提供服务时才存在,并且不会无限期地在后台运...
继续阅读 »

绑定服务(Bound Services)概述


绑定服务是client-server接口中的服务器。它允许组件(例如活动)绑定到服务、发送请求、接收响应和执行进程间通信(IPC)。 绑定服务通常仅在它为另一个应用程序组件提供服务时才存在,并且不会无限期地在后台运行


💥 基础知识


绑定服务是 Service 类的实现,它允许其他应用程序绑定到它并与之交互。 要为服务提供绑定,你必须实现 onBind() 回调方法。 此方法返回一个 IBinder 对象,该对象定义了客户端可用于与服务交互的编程接口。


🔥 Messenger


💥 概述


一提到IPC 很多人的反应都是 AIDL,其实如果仅仅是多进程单线程,那么你可以使用 Messenger 为你的服务提供接口。


使用 Messenger 比使用 AIDL 更简单,因为 Messenger 会将所有对服务的调用排入队列


对于大多数应用程序,该服务不需要执行多线程,因此使用 Messenger 允许该服务一次处理一个调用。如果你的 服务多线程很重要,那你就要用到ALDL了。


💥 使用 Messenger 步骤




  • 1、该 Service 实现了一个 Handler,该 Handler 接收来自客户端的每次调用的回调。




  • 2、该服务使用 Handler 创建一个 Messenger 对象(它是对 Handler 的引用)。




  • 3、Messenger 创建一个 IBinder,该服务从 onBind() 返回给客户端。




  • 4、客户端使用 IBinder 来实例化 Messenger(引用服务的Handler),客户端使用 Handler 来向服务发送 Message 对象。




  • 5、服务在其 Handler 的 handleMessage() 中接收每个消息。




💥 实例(Client到Server数据传递)


🌀 MessengerService.java


public class MessengerService extends Service {
public static final int MSG_SAY_HELLO = 0;
//让客户端向IncomingHandler发送消息。
Messenger messenger = null;

//当绑定到服务时,我们向我们的Messenger返回一个接口,用于向服务发送消息。
public IBinder onBind(Intent intent) {
MLog.e("MessengerService:onBind");
//创建 Messenger 对象(对 Handler 的引用)
messenger = new Messenger(new IncomingHander(this));
//返回支持此Messenger的IBinder。
return messenger.getBinder();
}
//实现了一个 Handler
static class IncomingHander extends Handler {
private Context appliacationContext;
public IncomingHander(Context context) {
appliacationContext = context.getApplicationContext();
}

@Override
public void handleMessage(Message msg) {
switch (msg.what){
case MSG_SAY_HELLO:
Bundle bundle = msg.getData();
String string = bundle.getString("name");
//处理来自客户端的消息
MLog.e("handleMessage:来自Acitvity的"+string);
break;
case 1:

break;
default:
super.handleMessage(msg);
}
}
}
}

🌀 AndroidMainfest.xml


        <service android:name=".ipc.MessengerService"
android:process="com.scc.ipc.messengerservice"
android:exported="true"
android:enabled="true"/>

使用 android:process 属性 创建不同进程。


🌀 MainActivity.class


public class MainActivity extends ActivityBase implements View.OnClickListener {
Messenger mService = null;
Messenger messenger = null;
private boolean bound;
private ViewStub v_stud;

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

ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
//从原始 IBinder 创建一个 Messenger,该 IBinder 之前已使用 getBinder 检索到。
mService = new Messenger(service);
bound = true;
}

@Override
public void onServiceDisconnected(ComponentName name) {
bound = false;
}
};

@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_bind_service:
bindService(new Intent(MainActivity.this, MessengerService.class), connection, Context.BIND_AUTO_CREATE);
break;
case R.id.btn_send_msg:
Message message = Message.obtain(null, MessengerService.MSG_SAY_HELLO);
Bundle bundle = new Bundle();
bundle.putString("name","Scc");
message.setData(bundle);
try {
mService.send(message);
} catch (RemoteException e) {
e.printStackTrace();
}
break;

}
}
@Override
protected void onStop() {
super.onStop();
if (bound) {
unbindService(connection);
bound = false;
}
}
}

🌀 运行效果如下



两个进程也存在着,也完成了进程间的通信,并把数据传递过去了。


💥 实例(Server将数据传回Client)


我不仅想将消息传递给 Server ,还想让 Server 将数据处理后传会Client。


🌀 MessengerService.java


public class MessengerService extends Service {
/** 用于显示和隐藏我们的通知。 */
ArrayList<Messenger> mClients = new ArrayList<Messenger>();
/** 保存客户端设置的最后一个值。 */
int mValue = 0;

/**
* 数组中添加 Messenger (来自客户端)。
* Message 的 replyTo 字段必须是应该发送回调的客户端的 Messenger。
*/
public static final int MSG_REGISTER_CLIENT = 1;

/**
* 数组中删除 Messenger (来自客户端)。
* Message 的 replyTo 字段必须是之前用 MSG_REGISTER_CLIENT 给出的客户端的 Messenger。
*/
public static final int MSG_UNREGISTER_CLIENT = 2;
/**
* 用于设置新值。
* 这可以发送到服务以提供新值,并将由服务发送给具有新值的任何注册客户端。
*/
public static final int MSG_SET_VALUE = 3;
//让客户端向IncomingHandler发送消息。
Messenger messenger = null;

//当绑定到服务时,我们向我们的Messenger返回一个接口,用于向服务发送消息。
public IBinder onBind(Intent intent) {
MLog.e("MessengerService-onBind");
//创建 Messenger 对象(对 Handler 的引用)
messenger = new Messenger(new IncomingHander(this));
//返回支持此Messenger的IBinder。
return messenger.getBinder();
}
//实现了一个 Handler
class IncomingHander extends Handler {
private Context appliacationContext;
public IncomingHander(Context context) {
appliacationContext = context.getApplicationContext();
}

@Override
public void handleMessage(Message msg) {
switch (msg.what){
case MSG_REGISTER_CLIENT:
mClients.add(msg.replyTo);
break;
case MSG_UNREGISTER_CLIENT:
mClients.remove(msg.replyTo);
break;
case MSG_SET_VALUE:
mValue = msg.arg1;
for (int i=mClients.size()-1; i>=0; i--) {
try {
mClients.get(i).send(Message.obtain(null,
MSG_SET_VALUE, mValue, 0));
} catch (RemoteException e) {
// 客户端没了。 从列表中删除它;
//从后往前安全,从前往后遍历数组越界。
mClients.remove(i);
}
}
default:
super.handleMessage(msg);
}
}
}
}

🌀 MainActivity.java


public class MainActivity extends ActivityBase implements View.OnClickListener {
Messenger mService = null;
Messenger messenger = null;
private boolean bound;
private ViewStub v_stud;

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

ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
//从原始 IBinder 创建一个 Messenger,该 IBinder 之前已使用 getBinder 检索到。
mService = new Messenger(service);
bound = true;
}

@Override
public void onServiceDisconnected(ComponentName name) {
bound = false;
}
};
static class ReturnHander extends Handler {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MessengerService.MSG_SET_VALUE:
//我要起飞:此处处理
MLog.e("Received from service: " + msg.arg1);
break;
default:
super.handleMessage(msg);
}
}
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_bind_service:
bindService(new Intent(MainActivity.this, MessengerService.class), connection, Context.BIND_AUTO_CREATE);
break;
case R.id.btn_send_msg:
try {
mMessenger = new Messenger(new ReturnHander());
Message msg = Message.obtain(null,
MessengerService.MSG_REGISTER_CLIENT);
msg.replyTo = mMessenger;
//先发一则消息添加Messenger:msg.replyTo = mMessenger;
mService.send(msg);

// Give it some value as an example.
msg = Message.obtain(null,
MessengerService.MSG_SET_VALUE, this.hashCode(), 0);
//传入的arg1值:this.hashCode()
mService.send(msg);
} catch (RemoteException e) {
e.printStackTrace();
}
break;

}
}
@Override
protected void onStop() {
super.onStop();
if (bound) {
unbindService(connection);
bound = false;
}
}
}

🌀 运行效果如下



我们在MainActivity 的 Handler.sendMessger()中接收到了来自 MesengerService 的消息 。


本次 Messenger 进程间通信齐活,这只是个简单的Demo。最后咱们看一波源码。


🔥 Messenger 源码


Messenger.java


public final class Messenger implements Parcelable {
private final IMessenger mTarget;
public Messenger(Handler target) {
mTarget = target.getIMessenger();
}
public void send(Message message) throws RemoteException {
mTarget.send(message);
}
public IBinder getBinder() {
return mTarget.asBinder();
}
...
public Messenger(IBinder target) {
mTarget = IMessenger.Stub.asInterface(target);
}
}

然后你会发现 只要代码还是在 IMessenger 里面,咱们去找找。


IMessenger.aidl


package android.os;

import android.os.Message;

/** @hide */
oneway interface IMessenger {
void send(in Message msg);
}

new Messenger(Handler handelr)


这里其实是用Handler 调用 getIMessenger() 。咱们去Handler.class里面转转。


    @UnsupportedAppUsage
final IMessenger getIMessenger() {
synchronized (mQueue) {
if (mMessenger != null) {
return mMessenger;
}
mMessenger = new MessengerImpl();
return mMessenger;
}
}
//创建了Messenger实现类
private final class MessengerImpl extends IMessenger.Stub {
public void send(Message msg) {
msg.sendingUid = Binder.getCallingUid();
//Messenger调用send()方法,通过Handler发送消息。
//然后在服务端通过Handler的handleMessge(msg)接收这个消息。
Handler.this.sendMessage(msg);
}
}

new Messenger(IBinder target)


package android.os;
/** @hide */
public interface IMessenger extends android.os.IInterface
{
/** Default implementation for IMessenger. */
public static class Default implements android.os.IMessenger
{
@Override public void send(android.os.Message msg) throws android.os.RemoteException
{
}
@Override
public android.os.IBinder asBinder() {
return null;
}
}
/** Local-side IPC implementation stub class. */
public static abstract class Stub extends android.os.Binder implements android.os.IMessenger
{
/** Construct the stub at attach it to the interface. */
public Stub()
{
this.attachInterface(this, DESCRIPTOR);
}
/**
* Cast an IBinder object into an android.os.IMessenger interface,
* generating a proxy if needed.
*/
public static android.os.IMessenger asInterface(android.os.IBinder obj)
{
if ((obj==null)) {
return null;
}
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
//判断是否在同一进程。
if (((iin!=null)&&(iin instanceof android.os.IMessenger))) {
//同一进程
return ((android.os.IMessenger)iin);
}
//代理对象
return new android.os.IMessenger.Stub.Proxy(obj);
}
@Override public android.os.IBinder asBinder()
{
return this;
}
...
}
public void send(android.os.Message msg) throws android.os.RemoteException;
}

看了上面代码你会发现这不就是个aidl吗? 什么是aidl,咱们下一篇继续讲到。


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

一天高中的女同桌突然问我是不是程序猿

背景 昨天一个我高中的女同桌突然发微信问我“你是不是程序猿 我有问题求助”, 先是激动后是茫然再是冷静,毕业多年不见联系,突然发个信息求助,感觉大脑有点反应不过来... 再说我一个搞Android的也不咋会python啊(不是说Java不能实现,大家懂的,人...
继续阅读 »

背景


昨天一个我高中的女同桌突然发微信问我“你是不是程序猿 我有问题求助”,


image-20211015101843733.png


先是激动后是茫然再是冷静,毕业多年不见联系,突然发个信息求助,感觉大脑有点反应不过来... 再说我一个搞Android的也不咋会python啊(不是说Java不能实现,大家懂的,人生苦短,我用python),即使如此,
为了大家的面子,为了程序猿们的脸,不就简单的小Python嘛,必须答应!


梳理需求


现有excel表格记录着 有效图片的名字,如:


image-20211015103418631.png


要从一个文件夹里把excel表格里记录名字的图片筛选出来;


需求也不是很难,代码思路就有了:



  1. 读取Excel表格第一列的信息并放入A集合

  2. 遍历文件夹下所有的文件,判断文件名字是否存在A集合

  3. 存在A集合则拷贝到目标文件夹


实现(Python 2.7)


读取Excel表格

加载Excel表格的方法有很多种,例如pandasxlrdopenpyxl,我这里选择openpyxl库,
先安装库



pip install openpyxl



代码如下:


from openpyxl import load_workbook

def handler_excel(filename=r'C:/Users/xxx/Desktop/haha.xlsx'):
   # 根据文件路径加载一个excel表格,这里包含所有的sheet
   excel = load_workbook(filename)
   # 根据sheet名称加载对应的table
   table = excel.get_sheet_by_name('Sheet1')
   imgnames = []
   # 读取所有列
   for column in table.columns:
       for cell in column:
           imgnames.append(cell.value+".png")
# 选择图片
   pickImg(imgnames)

遍历文件夹读取文件名,找到target并拷贝

使用os.listdir 方法遍历文件,这里注意windows环境下拿到的unicode编码,需要GBK重新解码


def pickImg(pickImageNames):
   # 遍历所有图片集的文件名
   for image in os.listdir(
           r"C:\Users\xxx\Desktop\work\img"):
       # 使用gbk解码,不然中文乱码
       u_file = image.decode('gbk')
       print(u_file)
       if u_file in pickImageNames:
           oldname = r"C:\Users\xxx\Desktop\work\img/" + image
           newname = r"C:\Users\xxx\Desktop\work\target/" + image
           # 文件拷贝
           shutil.copyfile(oldname, newname)

简单搞定!没有砸程序猿的招牌,豪横的把成果发给女同桌,结果:


image-20211015112550343.png


换来有机会请你吃饭,微信都不带回的,哎 ,xdm,小丑竟是我自己!
小丑竟是我自己什么梗-小丑竟是我自己是什么意思出自什么-55手游网


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

美团面试官问我一个字符的String.length()是多少,我说是1,面试官说你回去好好学一下吧

public class testT { public static void main(String [] args){ String A = "hi你是乔戈里"; System.out.println(A.lengt...
继续阅读 »


public class testT {
public static void main(String [] args){
String A = "hi你是乔戈里";
System.out.println(A.length());
}
}复制代码


以上结果输出为7。







小萌边说边在IDEA中的win环境下选中String.length()函数,使用ctrl+B快捷键进入到String.length()的定义。


    /**
* Returns the length of this string.
* The length is equal to the number of <a href="Character.html#unicode">Unicode
* code units</a> in the string.
*
* @return the length of the sequence of characters represented by this
* object.
*/
public int length() {
return value.length;
}复制代码


接着使用google翻译对这段英文进行了翻译,得到了大体意思:返回字符串的长度,这一长度等于字符串中的 Unicode 代码单元的数目。


小萌:乔戈里,那这又是啥意思呢?乔哥:前几天我写的一篇文章:面试官问你编码相关的面试题,把这篇甩给他就完事!)里面对于Java的字符使用的编码有介绍:


Java中 有内码和外码这一区分简单来说



  • 内码:char或String在内存里使用的编码方式。
  • 外码:除了内码都可以认为是“外码”。(包括class文件的编码)



而java内码:unicode(utf-16)中使用的是utf-16.所以上面的那句话再进一步解释就是:返回字符串的长度,这一长度等于字符串中的UTF-16的代码单元的数目。




代码单元指一种转换格式(UTF)中最小的一个分隔,称为一个代码单元(Code Unit),因此,一种转换格式只会包含整数个单元。UTF-X 中的数字 X 就是各自代码单元的位数。


UTF-16 的 16 指的就是最小为 16 位一个单元,也即两字节为一个单元,UTF-16 可以包含一个单元和两个单元,对应即是两个字节和四个字节。我们操作 UTF-16 时就是以它的一个单元为基本单位的。


你还记得你前几天被面试官说菜的时候学到的Unicode知识吗,在面试官让我讲讲Unicode,我讲了3秒说没了,面试官说你可真菜这里面提到,UTF-16编码一个字符对于U+0000-U+FFFF范围内的字符采用2字节进行编码,而对于字符的码点大于U+FFFF的字符采用四字节进行编码,前者是两字节也就是一个代码单元,后者一个字符是四字节也就是两个代码单元!


而上面我的例子中的那个字符的Unicode值就是“U+1D11E”,这个Unicode的值明显大于U+FFFF,所以对于这个字符UTF-16需要使用四个字节进行编码,也就是使用两个代码单元!


所以你才看到我的上面那个示例结果表示一个字符的String.length()长度是2!



来看个例子!


public class testStringLength {
public static void main(String [] args){
String B = "𝄞"; // 这个就是那个音符字符,只不过由于当前的网页没支持这种编码,所以没显示。
String C = "\uD834\uDD1E";// 这个就是音符字符的UTF-16编码
System.out.println(C);
System.out.println(B.length());
System.out.println(B.codePointCount(0,B.length()));
// 想获取这个Java文件自己进行演示的,可以在我的公众号【程序员乔戈里】后台回复 6666 获取
}
}复制代码



可以看到通过codePointCount()函数得知这个音乐字符是一个字符!




几个问题:0.codePointCount是什么意思呢?1.之前不是说音符字符是“U+1D11E”,为什么UTF-16是"uD834uDD1E",这俩之间如何转换?2.前面说了UTF-16的代码单元,UTF-32和UTF-8的代码单元是多少呢?



一个一个解答:


第0个问题:


codePointCount其实就是代码点数的意思,也就是一个字符就对应一个代码点数。


比如刚才音符字符(没办法打出来),它的代码点是U+1D11E,但它的代理单元是U+D834和U+DD1E,如果令字符串str = "u1D11E",机器识别的不是音符字符,而是一个代码点”/u1D11“和字符”E“,所以会得到它的代码点数是2,代码单元数也是2。


但如果令字符str = "uD834uDD1E",那么机器会识别它是2个代码单元代理,但是是1个代码点(那个音符字符),故而,length的结果是代码单元数量2,而codePointCount()的结果是代码点数量1.


第1个问题




上图是对应的转换规则:



  • 首先 U+1D11E-U+10000 = U+0D11E
  • 接着将U+0D11E转换为二进制:0000 1101 0001 0001 1110,前10位是0000 1101 00 后10位是01 0001 1110
  • 接着套用模板:110110yyyyyyyyyy 110111xxxxxxxxxx
  • U+0D11E的二进制依次从左到右填入进模板:110110 0000 1101 00 110111 01 0001 1110
  • 然后将得到的二进制转换为16进制:d834dd1e,也就是你看到的utf-16编码了



第2个问题




  • 同理,UTF-32 以 32 位一个单元,它只包含这一种单元就够了,它的一单元自然也就是四字节了。
  • UTF-8 的 8 指的就是最小为 8 位一个单元,也即一字节为一个单元,UTF-8 可以包含一个单元,二个单元,三个单元及四个单元,对应即是一,二,三及四字节。






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

领导:谁再用定时任务实现关闭订单,立马滚蛋!

在电商、支付等领域,往往会有这样的场景,用户下单后放弃支付了,那这笔订单会在指定的时间段后进行关闭操作,细心的你一定发现了像某宝、某东都有这样的逻辑,而且时间很准确,误差在1s内;那他们是怎么实现的呢? 一般的做法有如下几种定时任务关闭订单rocketmq延迟...
继续阅读 »

在电商、支付等领域,往往会有这样的场景,用户下单后放弃支付了,那这笔订单会在指定的时间段后进行关闭操作,细心的你一定发现了像某宝、某东都有这样的逻辑,而且时间很准确,误差在1s内;那他们是怎么实现的呢?


一般的做法有如下几种

定时任务关闭订单

rocketmq延迟队列

rabbitmq死信队列

时间轮算法

redis过期监听


一、定时任务关闭订单(最low)


一般情况下,最不推荐的方式就是关单方式就是定时任务方式,原因我们可以看下面的图来说明


image.png


我们假设,关单时间为下单后10分钟,定时任务间隔也是10分钟;通过上图我们看出,如果在第1分钟下单,在第20分钟的时候才能被扫描到执行关单操作,这样误差达到10分钟,这在很多场景下是不可接受的,另外需要频繁扫描主订单号造成网络IO和磁盘IO的消耗,对实时交易造成一定的冲击,所以PASS


二、rocketmq延迟队列方式


延迟消息
生产者把消息发送到消息服务器后,并不希望被立即消费,而是等待指定时间后才可以被消费者消费,这类消息通常被称为延迟消息。
在RocketMQ开源版本中,支持延迟消息,但是不支持任意时间精度的延迟消息,只支持特定级别的延迟消息。
消息延迟级别分别为1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h,共18个级别。


发送延迟消息(生产者)


/**
* 推送延迟消息
*
@param topic
*
@param body
*
@param producerGroup
*
@return boolean
*/

public boolean sendMessage(String topic, String body, String producerGroup)
{
try
{
Message recordMsg = new Message(topic, body.getBytes());
producer.setProducerGroup(producerGroup);

//设置消息延迟级别,我这里设置14,对应就是延时10分钟
// "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h"
recordMsg.setDelayTimeLevel(14);
// 发送消息到一个Broker
SendResult sendResult = producer.send(recordMsg);
// 通过sendResult返回消息是否成功送达
log.info("发送延迟消息结果:======sendResult:{}", sendResult);
DateFormat format =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
log.info("发送时间:{}", format.format(new Date()));

return true;
}
catch (Exception e)
{
e.printStackTrace();
log.error("延迟消息队列推送消息异常:{},推送内容:{}", e.getMessage(), body);
}
return false;
}

消费延迟消息(消费者)


/**
* 接收延迟消息
*
* @param topic
* @param consumerGroup
* @param messageHandler
*/

public void messageListener(String topic, String consumerGroup, MessageListenerConcurrently messageHandler)
{
ThreadPoolUtil.execute(() ->
{
try
{
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer();
consumer.setConsumerGroup(consumerGroup);
consumer.setVipChannelEnabled(false);
consumer.setNamesrvAddr(address);
//设置消费者拉取消息的策略,*表示消费该topic下的所有消息,也可以指定tag进行消息过滤
consumer.subscribe(topic, "*");
//消费者端启动消息监听,一旦生产者发送消息被监听到,就打印消息,和rabbitmq中的handlerDelivery类似
consumer.registerMessageListener(messageHandler);
consumer.start();
log.info("启动延迟消息队列监听成功:" + topic);
}
catch (MQClientException e)
{
log.error("启动延迟消息队列监听失败:{}", e.getErrorMessage());
System.exit(1);
}
});
}

实现监听类,处理具体逻辑


/**
* 延迟消息监听
*
*/

@Component
public class CourseOrderTimeoutListener implements ApplicationListener
{

@Resource
private MQUtil mqUtil;

@Resource
private CourseOrderTimeoutHandler courseOrderTimeoutHandler;

@Override
public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent)
{
// 订单超时监听
mqUtil.messageListener(EnumTopic.ORDER_TIMEOUT, EnumGroup.ORDER_TIMEOUT_GROUP, courseOrderTimeoutHandler);
}
}

/**
* 实现监听
*/

@Slf4j
@Component
public class CourseOrderTimeoutHandler implements MessageListenerConcurrently
{

@Override
public ConsumeConcurrentlyStatus consumeMessage(List list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
for (MessageExt msg : list)
{
// 得到消息体
String body = new String(msg.getBody());
JSONObject userJson = JSONObject.parseObject(body);
TCourseBuy courseBuyDetails = JSON.toJavaObject(userJson, TCourseBuy.class);

// 处理具体的业务逻辑,,,,,

DateFormat format =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
log.info("消费时间:{}", format.format(new Date()));

return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
}

这种方式相比定时任务好了很多,但是有一个致命的缺点,就是延迟等级只有18种(商业版本支持自定义时间),如果我们想把关闭订单时间设置在15分钟该如何处理呢?显然不够灵活。


三、rabbitmq死信队列的方式


Rabbitmq本身是没有延迟队列的,只能通过Rabbitmq本身队列的特性来实现,想要Rabbitmq实现延迟队列,需要使用Rabbitmq的死信交换机(Exchange)和消息的存活时间TTL(Time To Live)


死信交换机
一个消息在满足如下条件下,会进死信交换机,记住这里是交换机而不是队列,一个交换机可以对应很多队列。


一个消息被Consumer拒收了,并且reject方法的参数里requeue是false。也就是说不会被再次放在队列里,被其他消费者使用。
上面的消息的TTL到了,消息过期了。


队列的长度限制满了。排在前面的消息会被丢弃或者扔到死信路由上。
死信交换机就是普通的交换机,只是因为我们把过期的消息扔进去,所以叫死信交换机,并不是说死信交换机是某种特定的交换机


消息TTL(消息存活时间)
消息的TTL就是消息的存活时间。RabbitMQ可以对队列和消息分别设置TTL。对队列设置就是队列没有消费者连着的保留时间,也可以对每一个单独的消息做单独的设置。超过了这个时间,我们认为这个消息就死了,称之为死信。如果队列设置了,消息也设置了,那么会取值较小的。所以一个消息如果被路由到不同的队列中,这个消息死亡的时间有可能不一样(不同的队列设置)。这里单讲单个消息的TTL,因为它才是实现延迟任务的关键。


byte[] messageBodyBytes = "Hello, world!".getBytes();  
AMQP.BasicProperties properties = new AMQP.BasicProperties();
properties.setExpiration("60000");
channel.basicPublish("my-exchange", "queue-key", properties, messageBodyBytes);

可以通过设置消息的expiration字段或者x-message-ttl属性来设置时间,两者是一样的效果。只是expiration字段是字符串参数,所以要写个int类型的字符串:当上面的消息扔到队列中后,过了60秒,如果没有被消费,它就死了。不会被消费者消费到。这个消息后面的,没有“死掉”的消息对顶上来,被消费者消费。死信在队列中并不会被删除和释放,它会被统计到队列的消息数中去


处理流程图


image.png


创建交换机(Exchanges)和队列(Queues)


创建死信交换机


image.png


如图所示,就是创建一个普通的交换机,这里为了方便区分,把交换机的名字取为:delay


创建自动过期消息队列
这个队列的主要作用是让消息定时过期的,比如我们需要2小时候关闭订单,我们就需要把消息放进这个队列里面,把消息过期时间设置为2小时


image.png


创建一个一个名为delay_queue1的自动过期的队列,当然图片上面的参数并不会让消息自动过期,因为我们并没有设置x-message-ttl参数,如果整个队列的消息有消息都是相同的,可以设置,这里为了灵活,所以并没有设置,另外两个参数x-dead-letter-exchange代表消息过期后,消息要进入的交换机,这里配置的是delay,也就是死信交换机,x-dead-letter-routing-key是配置消息过期后,进入死信交换机的routing-key,跟发送消息的routing-key一个道理,根据这个key将消息放入不同的队列


创建消息处理队列
这个队列才是真正处理消息的队列,所有进入这个队列的消息都会被处理


image.png


消息队列的名字为delay_queue2
消息队列绑定到交换机
进入交换机详情页面,将创建的2个队列(delayqueue1和delayqueue2)绑定到交换机上面


image.png
自动过期消息队列的routing key 设置为delay
绑定delayqueue2


image.png


delayqueue2 的key要设置为创建自动过期的队列的x-dead-letter-routing-key参数,这样当消息过期的时候就可以自动把消息放入delay_queue2这个队列中了
绑定后的管理页面如下图:


image.png


当然这个绑定也可以使用代码来实现,只是为了直观表现,所以本文使用的管理平台来操作
发送消息


String msg = "hello word";  
MessageProperties messageProperties = newMessageProperties();
messageProperties.setExpiration("6000");
messageProperties.setCorrelationId(UUID.randomUUID().toString().getBytes());
Message message = newMessage(msg.getBytes(), messageProperties);
rabbitTemplate.convertAndSend("delay", "delay",message);

设置了让消息6秒后过期
注意:因为要让消息自动过期,所以一定不能设置delay_queue1的监听,不能让这个队列里面的消息被接受到,否则消息一旦被消费,就不存在过期了


接收消息
接收消息配置好delay_queue2的监听就好了


package wang.raye.rabbitmq.demo1;
import org.springframework.amqp.core.AcknowledgeMode;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.ChannelAwareMessageListener;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
publicclassDelayQueue{
/** 消息交换机的名字*/
publicstaticfinalString EXCHANGE = "delay";
/** 队列key1*/
publicstaticfinalString ROUTINGKEY1 = "delay";
/** 队列key2*/
publicstaticfinalString ROUTINGKEY2 = "delay_key";
/**
* 配置链接信息
* @return
*/

@Bean
publicConnectionFactory connectionFactory() {
CachingConnectionFactory connectionFactory = newCachingConnectionFactory("120.76.237.8",5672);
connectionFactory.setUsername("kberp");
connectionFactory.setPassword("kberp");
connectionFactory.setVirtualHost("/");
connectionFactory.setPublisherConfirms(true); // 必须要设置
return connectionFactory;
}
/**
* 配置消息交换机
* 针对消费者配置
FanoutExchange: 将消息分发到所有的绑定队列,无routingkey的概念
HeadersExchange :通过添加属性key-value匹配
DirectExchange:按照routingkey分发到指定队列
TopicExchange:多关键字匹配
*/

@Bean
publicDirectExchange defaultExchange() {
returnnewDirectExchange(EXCHANGE, true, false);
}
/**
* 配置消息队列2
* 针对消费者配置
* @return
*/

@Bean
publicQueue queue() {
returnnewQueue("delay_queue2", true); //队列持久
}
/**
* 将消息队列2与交换机绑定
* 针对消费者配置
* @return
*/

@Bean
@Autowired
publicBinding binding() {
returnBindingBuilder.bind(queue()).to(defaultExchange()).with(DelayQueue.ROUTINGKEY2);
}
/**
* 接受消息的监听,这个监听会接受消息队列1的消息
* 针对消费者配置
* @return
*/

@Bean
@Autowired
publicSimpleMessageListenerContainer messageContainer2(ConnectionFactory connectionFactory) {
SimpleMessageListenerContainer container = newSimpleMessageListenerContainer(connectionFactory());
container.setQueues(queue());
container.setExposeListenerChannel(true);
container.setMaxConcurrentConsumers(1);
container.setConcurrentConsumers(1);
container.setAcknowledgeMode(AcknowledgeMode.MANUAL); //设置确认模式手工确认
container.setMessageListener(newChannelAwareMessageListener() {
publicvoid onMessage(Message message, com.rabbitmq.client.Channel channel) throwsException{
byte[] body = message.getBody();
System.out.println("delay_queue2 收到消息 : "+ newString(body));
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); //确认消息成功消费
}
});
return container;
}
}

这种方式可以自定义进入死信队列的时间;是不是很完美,但是有的小伙伴的情况是消息中间件就是rocketmq,公司也不可能会用商业版,怎么办?那就进入下一节


四、时间轮算法


image.png


(1)创建环形队列,例如可以创建一个包含3600个slot的环形队列(本质是个数组)


(2)任务集合,环上每一个slot是一个Set
同时,启动一个timer,这个timer每隔1s,在上述环形队列中移动一格,有一个Current Index指针来标识正在检测的slot。


Task结构中有两个很重要的属性:
(1)Cycle-Num:当Current Index第几圈扫描到这个Slot时,执行任务
(2)订单号,要关闭的订单号(也可以是其他信息,比如:是一个基于某个订单号的任务)


假设当前Current Index指向第0格,例如在3610秒之后,有一个订单需要关闭,只需:
(1)计算这个订单应该放在哪一个slot,当我们计算的时候现在指向1,3610秒之后,应该是第10格,所以这个Task应该放在第10个slot的Set中
(2)计算这个Task的Cycle-Num,由于环形队列是3600格(每秒移动一格,正好1小时),这个任务是3610秒后执行,所以应该绕3610/3600=1圈之后再执行,于是Cycle-Num=1


Current Index不停的移动,每秒移动到一个新slot,这个slot中对应的Set,每个Task看Cycle-Num是不是0:
(1)如果不是0,说明还需要多移动几圈,将Cycle-Num减1
(2)如果是0,说明马上要执行这个关单Task了,取出订单号执行关单(可以用单独的线程来执行Task),并把这个订单信息从Set中删除即可。
(1)无需再轮询全部订单,效率高
(2)一个订单,任务只执行一次
(3)时效性好,精确到秒(控制timer移动频率可以控制精度)


五、redis过期监听


1.修改redis.windows.conf配置文件中notify-keyspace-events的值
默认配置notify-keyspace-events的值为 ""
修改为 notify-keyspace-events Ex 这样便开启了过期事件


2. 创建配置类RedisListenerConfig(配置RedisMessageListenerContainer这个Bean)


package com.zjt.shop.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;


@Configuration
public class RedisListenerConfig {

@Autowired
private RedisTemplate redisTemplate;

/**
*
@return
*/

@Bean
public RedisTemplate redisTemplateInit() {

// key序列化
redisTemplate.setKeySerializer(new StringRedisSerializer());

//val实例化
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());

return redisTemplate;
}

@Bean
RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
return container;
}

}

3.继承KeyExpirationEventMessageListener创建redis过期事件的监听类


package com.zjt.shop.common.util;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.zjt.shop.modules.order.service.OrderInfoService;
import com.zjt.shop.modules.product.entity.OrderInfoEntity;
import com.zjt.shop.modules.product.mapper.OrderInfoMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.listener.KeyExpirationEventMessageListener;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.stereotype.Component;


@Slf4j
@Component
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {

public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
super(listenerContainer);
}

@Autowired
private OrderInfoMapper orderInfoMapper;

/**
* 针对redis数据失效事件,进行数据处理
*
@param message
*
@param pattern
*/

@Override
public void onMessage(Message message, byte[] pattern) {
try {
String key = message.toString();
//从失效key中筛选代表订单失效的key
if (key != null && key.startsWith("order_")) {
//截取订单号,查询订单,如果是未支付状态则为-取消订单
String orderNo = key.substring(6);
QueryWrapper queryWrapper = new QueryWrapper<>();
queryWrapper.eq("order_no",orderNo);
OrderInfoEntity orderInfo = orderInfoMapper.selectOne(queryWrapper);
if (orderInfo != null) {
if (orderInfo.getOrderState() == 0) { //待支付
orderInfo.setOrderState(4); //已取消
orderInfoMapper.updateById(orderInfo);
log.info("订单号为【" + orderNo + "】超时未支付-自动修改为已取消状态");
}
}
}
} catch (Exception e) {
e.printStackTrace();
log.error("【修改支付订单过期状态异常】:" + e.getMessage());
}
}
}

4:测试
通过redis客户端存一个有效时间为3s的订单:


image.png


结果:


image.png


总结:
以上方法只是个人对于关单的一些想法,可能有些地方有疏漏,请在公众号直接留言进行指出,当然如果你有更好的关单方式也可以随时沟通交流


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

搜索历史记录的实现-Android

前言最近一个客户想要实现搜索中搜索历史的功能,其实这个功能听起来很简单,实际上里面有很多逻辑在里面,一开始写的时候脑子蒙蒙的,最后提给客户的时候一堆毛病,这一次来详细梳理一下,也分享一下我的思路主要逻辑搜索后保存当前内容将最新的搜索记录在最前面搜索历史记录可以...
继续阅读 »

前言

最近一个客户想要实现搜索中搜索历史的功能,其实这个功能听起来很简单,实际上里面有很多逻辑在里面,一开始写的时候脑子蒙蒙的,最后提给客户的时候一堆毛病,这一次来详细梳理一下,也分享一下我的思路

主要逻辑

  1. 搜索后保存当前内容
  2. 将最新的搜索记录在最前面
  3. 搜索历史记录可以点击并执行搜索功能,并将其提到最前面

我里面使用了ObjectBox作为数据存储,因为实际项目用的Java所以没用Room,而且Room好像第一次搜索至少要200ms,不过可以在某个activity随便搜索热启动一下.GreenDao使用有点麻烦,查询条件没有什么太大需求,直接用ObjectBox了,而且使用超级简单

Code

ObjectBox的工具类

public class ObjectBoxUtils {
public static BoxStore init() {
BoxStore boxStore = null;
try {
boxStore = MyApplication.getBoxStore();
if (boxStore == null) {
boxStore = MyObjectBox.builder().androidContext(MyApplication.applicationContext).build();
MyApplication.setBoxStore(boxStore);
}
} catch (Exception e) {
}
return boxStore;
}


public static <T> List<T> getAllData(Class clazz) {
try {
BoxStore boxStore = init();
if (boxStore != null && !boxStore.isClosed()) {
Box<T> box = boxStore.boxFor(clazz);

return box.getAll();
}
} catch (Exception e) {
}
return new ArrayList<>();
}


/**
* 添加数据
*/
public static <T> long addData(T o, Class c) {
try {
BoxStore boxStore = init();
if (boxStore != null && !boxStore.isClosed()) {
return boxStore.boxFor(c).put(o);
}
} catch (Throwable e) {
}
return 0;
}


public static HistoryBean getHistroyBean(String name) {
try {
BoxStore boxStore = init();
if (boxStore != null && !boxStore.isClosed()) {
Box<HistoryBean> box = boxStore.boxFor(HistoryBean.class);
HistoryBean first = box.query().equal(HistoryBean_.name, name).build().findFirst();
return first;
}
} catch (Exception e) {
}
return null;
}
}

其实我在Application就初始化了ObjectBox,但是实际项目中有时候会初始化失败,导致直接空指针,所有每次调用我都会判断一下是否初始化了,没有的话就进行相应操作

Activity

class HistoryActivity : AppCompatActivity() {
private var list: MutableList<HistoryBean>? = null
private var inflate: ActivityHistoryBinding? = null
private var historyAdapter: HistoryAdapter? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
inflate = ActivityHistoryBinding.inflate(layoutInflater)
setContentView(inflate?.root)
list = ObjectBoxUtils.getAllData(HistoryBean::class.java)
list?.sort()
inflate!!.rv.layoutManager = LinearLayoutManager(this, RecyclerView.HORIZONTAL, false)
historyAdapter = HistoryAdapter(this, list)
inflate!!.rv.adapter = historyAdapter


inflate!!.et.setOnEditorActionListener(object : TextView.OnEditorActionListener {
override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean {
saveHistory(inflate!!.et.text.toString())
return true
}

})
}

/**
* 保存搜索历史
*
*/
fun saveHistory(keyWord: String) {
//查询本地是否有name为参数中的数据
var histroyBean: HistoryBean? = ObjectBoxUtils.getHistroyBean(keyWord)
val currentTimeMillis = System.currentTimeMillis()
//没有就新创建一个
if (histroyBean == null) {
histroyBean = HistoryBean(currentTimeMillis, keyWord, currentTimeMillis)
} else {
//有的话就更新时间,也就说明了两种情况,第一 重复搜索了,搜索肯定要排重嘛,第二就是我们点击历史记录了,因此更新下时间
histroyBean.setTime(currentTimeMillis)
}
//把新/旧数据保存到本地
ObjectBoxUtils.addData(histroyBean, HistoryBean::class.java)
//每一次操作都从数据库拿取数据,性能消耗很低,就这么一个小模块没必要上纲上线
list?.clear()
list?.addAll(ObjectBoxUtils.getAllData(HistoryBean::class.java))
//实体Bean重写了Comparable,排序一下
list?.sort()
historyAdapter?.notifyDataSetChanged()
}
}

相应注释都在代码里,说实话kotlin用的好难受啊,还是自己语法学的不行,一个小东西卡我好久,导致我Application里面直接删除用Java重写了

实体类

@Entity
public class HistoryBean implements Comparable<HistoryBean> {
@Id(assignable = true)
public long id;

public HistoryBean(long id, String name, long time) {
this.id = id;
this.name = name;
this.time = time;
}

public String name;

public long time;

public long getId() {
return id;
}

public void setId(long id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public long getTime() {
return time;
}

public void setTime(long time) {
this.time = time;
}


@Override
public int compareTo(HistoryBean o) {
return (int) (o.time-time);
}
}

实体类重写了CompareTo,因为集合的sort实际上也是调用了ComparteTo,我们直接重写相应逻辑就简化业务层很多代码

效果

历史记录.gif

嗯,效果还不错,继续学习令人脑壳痛的自定义View去了


收起阅读 »

动态代理的使用-功能增强

背景接手某项目时碰到切换主线程的逻辑, 原项目代码流程如下:xxPresenter 会创建observer直接用于二方库的 SDKService (通常在子线程中回调),记为 innerObserverxxActivit...
继续阅读 »

背景

接手某项目时碰到切换主线程的逻辑, 原项目代码流程如下:

切换主线程时序图.png

  1. xxPresenter 会创建observer直接用于二方库的 SDKService (通常在子线程中回调),记为 innerObserver
  2. xxActivity 也需要创建observer用于主线程回调, 记为 uiObserver
  3. xxPresenter 在收到 innerObserver 的回调后通过主线程handler进行线程切换, 最终触发 uiObserver 的对应方法
  4. 业务需求回调方法都在 xxActivity 主线程中执行后续操作, innerObserver 几乎仅用于线程切换而已

存在的问题

  1. 如图第2/3步, 对于同一类型的observer, 需要在 activity , presenter中各实现一次, presenter中会产生大量模板代码
  2. 如图第6步, 收到 SDKService 回调后, presenter需要构建Message, 设置各回调实参, 这完全依赖开发人员手动配置, 效率低下且易发生错误, 灵活度低
  3. 对应的, 第11步通过handler线程切换时, 又需要从 message 中依次还原各实参, 这一步同样依赖开发人员手动处理
  4. observer变化时(如形参列表顺序/类型发生变更), 均需要同步更新 prenter 和 handler
  5. 我司项目最多时, 某个SDKService有将近100个observer需要设置, 部分observer的方法数甚至超过45个, 导致单纯在 Presenter 中创建observer的空白匿名内部类时, 代码就超过100行, 模板代码过多
  6. ...

改造思路

根据已知条件:

  1. 各observer均为接口 interface 类型
  2. presenter 中实现的 innerObserver 仅用于进行线程切换,最终触发UI层创建的observer而已 --> 即:有统一的功能增强逻辑

自然联想到 代理模式 中的动态代理:

代理模式-图侵删,来源于C语言中文网

  1. 创建一个 ThreadSwitcher 辅助类, 可根据传入的 observer 的类型Class,自动生成动态代理类对象,即之前的 innerObserver, 然后作用于sdk中 --> 此步骤可节省prsetner中因 new observer(){} 产生的大量模板代码, 且在observer接口发生变更时, 也不需要修改代码,自动完成适配, 伪代码如下:
    Observer innerOb = ThreadSwitcher.generateInnerObserver(Observer.class)

  2. ThreadSwitcher 类同时透出接口供UI层传入用于主线程的observer, 缓存在 Map<Class,IObserver> 中, 供后续切换主线程时使用

  3. 当下层sdk回调动态代理对象时, 最终都会触发 InvocationHandler#invoke 方法, 其方法签名如下, 我们只需要在其方法体中构造runnable, 按需post到主线程中即可:

// package java.lang.reflect.InvocationHandler.java
/**
* @param method 接口中被触发的回调方法
* @param args 方法实参列表
*/

public Object invoke(Object proxy, Method method, Object[] args);
  1. 构造的runnable时, 需查找UI层注入的observer,并触发对应的方法, 而由于 InvocationHandler中已告知我们方法 method 及其实参 args , 因此可直接通过 method.invoke(uiObserver,args) 来触发 uiObserver 的对应方法, 具体代码见下一节

动态代理的使用

import java.lang.reflect.InvocationHandler
import java.lang.reflect.Method
import java.lang.reflect.Proxy

object ThreadSwitcher {
// ui层注入的observer, 会在主线程中回调
val uiObserverMap = mutableMapOf<Class<*>, Any>()
val targetHandler: Handler = Handler(Looper.mainLooper())

private fun runOnUIThread(runnable: Runnable) {
// 此处省略切换主线程代码,创建一个mainLooper的handler, post Runnable即可
}

// 生成代理类
fun <O> generateInnerObserver(clz: Class<O>): O? {
// 固定写法, 传入classLoader 和 待实现的接口列表, 以及核心的 InvocationHandler 的实现, 在其内部进行功能增强
return Proxy.newProxyInstance(clz.classLoader, arrayOf(clz), object : InvocationHandler {
override fun invoke(proxy: Any?, method: Method?, args: Array<out Any>?): Any? {

// 1. 构造runnable, 用于主线程切换
val runnable = Runnable {

// 3. 查找 uiObserver, 若存在则触发
uiObserverMap[clz]?.let { uiObserver ->
val result = method?.invoke(uiObserver, args)
result
}
}

// 2. 将runnable抛主线程
runOnUIThread(runnable)

// 4. 触发method方法得到的返回值, 根据实际类型构造, void时返回null, 此处仅做示意
return null
}
}) as O // 按需强转为实现的接口类型
}
}

具体封装实现可参考如下链接:

改造后的流程如下:

改造后的时序图.png

源码分析

动态代理的实现很简单, 两三行代码就可以搞定, 系统肯定做了很多封装, 把脏活累活给做了, 我们简单看下

从入口方法开始: java.lang.reflect.Proxy#newProxyInstance

// package java.lang.reflect.Proxy.java  基于api 29
private static final Class<?>[] constructorParams = { InvocationHandler.class };

public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h){
final Class<?>[] intfs = interfaces.clone();

// 从缓存中查找已生成过的class类型,若不存在则进行生成
Class<?> cl = getProxyClass0(loader, intfs);

// 反射调用构造方法 Proxy(InvocationHandler), 创建并返回实例
final Constructor<?> cons = cl.getConstructor(constructorParams);
final InvocationHandler ih = h;
if (!Modifier.isPublic(cl.getModifiers())) {
cons.setAccessible(true);
}
return cons.newInstance(new Object[]{h});
}

private static final WeakCache<ClassLoader, Class<?>[], Class<?>>
proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());

/**
* 创建代理类class
*/

private static Class<?> getProxyClass0(ClassLoader loader,Class<?>... interfaces) {
// 接口方法数限制
if (interfaces.length > 65535) { throw new IllegalArgumentException("interface limit exceeded"); }

// 优先从缓存中获取已创建过的代理类, 若不存在, 则创建
return proxyClassCache.get(loader, interfaces);
}

关键的 proxyClassCache 是个二级缓存类(WeakCache), 通过调用其 get 方法得到最终的实现类, 其构造方法签名如下:

// java.lang.reflect.WeakCache.java

/**
* Construct an instance of {@code WeakCache}
*
* @param subKeyFactory a function mapping a pair of
* {@code (key, parameter) -> sub-key}
* @param valueFactory a function mapping a pair of
* {@code (key, parameter) -> value}
* @throws NullPointerException if {@code subKeyFactory} or
* {@code valueFactory} is null.
*/

public WeakCache(BiFunction<K, P, ?> subKeyFactory, BiFunction<K, P, V> valueFactory) {

通过参数名也可以猜到最终是通过 valueFactory 生成的, 我们回到 Proxy 类看下:

// java.lang.reflect.Proxy.java

private static final WeakCache<ClassLoader, Class<?>[], Class<?>>
proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());

/**
* A factory function that generates, defines and returns the proxy class given
* the ClassLoader and array of interfaces.
*/

private static final class ProxyClassFactory
implements BiFunction<ClassLoader, Class<?>[], Class<?>>
{
// 所有动态代理类名的前缀
private static final String proxyClassNamePrefix = "$Proxy";

// 每一个动态代理类类名中唯一的数字,可猜测最终是分层的代理类名就是: $Proxy+数字
private static final AtomicLong nextUniqueNumber = new AtomicLong();

@Override
public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {
// 省略部分代码: 对传入的接口数组进行一些校验

String proxyPkg = null; // 最终实现类所在的包路径
int accessFlags = Modifier.PUBLIC | Modifier.FINAL; // 生成的代理类默认访问权限是: public final

// 对接口数组校验: 若待实现的接口是非public的, 则最终实现的代理类也是非public的,并且非public的接口需要在同一个包下
for (Class<?> intf : interfaces) {
int flags = intf.getModifiers();
if (!Modifier.isPublic(flags)) {
accessFlags = Modifier.FINAL;
String name = intf.getName();
int n = name.lastIndexOf('.');
String pkg = ((n == -1) ? "" : name.substring(0, n + 1));
if (proxyPkg == null) {
proxyPkg = pkg;
} else if (!pkg.equals(proxyPkg)) {
throw new IllegalArgumentException(
"non-public interfaces from different packages");
}
}
}

// 若待实现的接口均为 public, 则使用默认的包路径
if (proxyPkg == null) { proxyPkg = ""; }

{
List<Method> methods = getMethods(interfaces); // 递归获取所有接口(包括其父接口)的方法,并手动添加了 equals/hashCode/toString 三个方法
Collections.sort(methods, ORDER_BY_SIGNATURE_AND_SUBTYPE); // 对所有接口方法排序
validateReturnTypes(methods); // 校验接口方法: 确保同名方法得返回类型一致
List<Class<?>[]> exceptions = deduplicateAndGetExceptions(methods); // 去除重复的方法,并获取每个方法对应的异常值信息

Method[] methodsArray = methods.toArray(new Method[methods.size()]);
Class<?>[][] exceptionsArray = exceptions.toArray(new Class<?>[exceptions.size()][]);

long num = nextUniqueNumber.getAndIncrement(); // 生成当前代理实现类的数字信息
String proxyName = proxyPkg + proxyClassNamePrefix + num; // 拼接生成代理类名,默认为: $Proxy+数字

return generateProxy(proxyName, interfaces, loader, methodsArray, exceptionsArray); // 通过native方法生成代理类Class
}
}

@FastNative
private static native Class<?> generateProxy(String name, Class<?>[] interfaces, ClassLoader loader, Method[] methods, Class<?>[][] exceptions);

/**
* 根据传入的接口class信息,获取所有的接口方法,并额外添加 equals/hashCode/toString 三个方法
*/

private static List<Method> getMethods(Class<?>[] interfaces) {
List<Method> result = new ArrayList<Method>();
try {
result.add(Object.class.getMethod("equals", Object.class));
result.add(Object.class.getMethod("hashCode", EmptyArray.CLASS));
result.add(Object.class.getMethod("toString", EmptyArray.CLASS));
} catch (NoSuchMethodException e) {
throw new AssertionError();
}

getMethodsRecursive(interfaces, result); // 通过递归反射的方式一次获取接口所有的方法
return result;
}
}

动态代理生成的类长啥样?

上面我们简单分析了下动态代理的源码, 我们可以知道/推测得到以下信息:

  1. 生成的代理类叫做 $ProxyN 其中 N 是一个数字,随代理类的增加而递增
  2. $ProxyN 实现了所有接口方法,并自动添加了 equals/hashCode/toString 三个方法,因此: --> a. 动态代理生成类应可以强转为任何传入的接口类型 --> b. 额外增加的三个方法通常会影响对象的比较,需要手动赋值区分
  3. 触发动态代理类的方法最终都会回调 InvocationHandler#invoke 方法,而 InvocationHandler 是通过 Proxy#newProxyInstance 传入的,因此: --> 猜测生成 $ProxyN 类应是继承自 Proxy 类

猜测归猜测, 最好能导出生成的 $ProxyN 看下实际代码:

  1. 网上查到的通常是使用 JVM 提供的 sun.misc.ProxyGenerator 类, 但这个类在android中不存在,手动拷贝对应jar包到android中使用也有问题
  2. 尝试使用字节码操作库或者 Class#getResourceAsStream 等方式也失败了, 终究是JVM上的工具, 在android虚拟机上无法直接使用
  3. 最终退而求其次, 先通过反射获取 $ProxyN 的类结构, 至于方法的调用则通过 InvocationHandler#invoke 方法中打印堆栈来查看
// 1. 自定义一个接口如下
package org.lynxz.utils.observer
interface ICallback {
fun onCallback(a: Int, b: Boolean, c: String?)
}

// 2. 通过反射获取类结构
package org.lynxz.utils.reflect.ReflectUtilTest
@Test
fun oriProxyTest() {
val proxyObj = Proxy.newProxyInstance(
javaClass.classLoader,
arrayOf(ICallback::class.java)
) { proxy, method, args -> // InvocationHandler#invoke 方法体
RuntimeException("===> 调用堆栈:${method?.name}").printStackTrace() // 3. 打印调用堆栈信息
args?.forEachIndexed { index, any -> // 4. 打印方法得实参
LoggerUtil.w(TAG, "===> 方法参数: $index - $any")
}
ReflectUtil.generateDefaultTypeValue(method!!.returnType) // 根据方法返回类型生成对应数据
}

// ProxyGeneratorImpl 是自定义的通过反射获取类结构的实现类, 具体代码请查看上面给出的github仓库
LoggerUtil.w(TAG, "===>类结构:\n${ProxyGeneratorImpl(proxyObj.javaClass).generate()}")
if (proxyObj is ICallback) { // 强转生成的动态代理类为自定义的接口
proxyObj.onCallback(1, true, "hello") // 触发接口方法,以便触发 InvocationHandler#invoke 方法, 进而打印堆栈
}
}

最终得到日志如下, 验证了之前的猜测:

// ===>类结构:
public final class $Proxy6 extends java.lang.reflect.Proxy implements ICallback{
public static final Class[] NFC;
public static final Class[][] NFD;
public $Proxy6(Class){...}
public final boolean equals(Object){...} // 方法体的内容不可知, 此处用省略号替代
public final int hashCode(){...}
public final String toString(){...}
public final void onCallback(int,boolean,String){...}
}

// 调用堆栈:
===> 调用堆栈:onCallback
at org.lynxz.utils.reflect.ReflectUtilTest$oriProxyTest$proxyObj$1.invok(ReflectUtilTest.kt:86) // 对应上方代码: RuntimeException("===> 调用堆栈:${method?.name}").printStackTrace()
at java.lang.reflect.Proxy.invoke(Proxy.java:913) // 触发 Proxy#invoke 方法, 其内部直接触发 InvocationHandler#invoke 方法
at $Proxy6.onCallback(Unknown Source) // 对应上方代码: proxyObj.onCallback(1, true, "hello")

// 打印方法实参数据, 序号 - 值, 与我们传入的相同
===> 方法参数: 0 - 1
===> 方法参数: 1 - true
===> 方法参数: 2 - hello

Proxy#invoke 源码, 就是简单的触发 InvocationHandler#invoke 而已

// java.lang.reflect.Proxy.java
protected InvocationHandler h;
protected Proxy(InvocationHandler h) {
Objects.requireNonNull(h);
this.h = h;
}

// 直接触发 invocationHandler 方法
// 而 InvocationHandler 是通过 Proxy#newProxyInstance 传入的, 最终传到 $Proxy6 的构造方法
private static Object invoke(Proxy proxy, Method method, Object[] args) throws Throwable {
InvocationHandler h = proxy.h; // 此处的proxy就是上面动态代理生成 `$Proxy6` 类
return h.invoke(proxy, method, args);
}

收起阅读 »

smali语言之locals和registers的区别

介绍对于dalviks字节码寄存器都是32位的,它能够表示任何类型,2个寄存器用于表示64位的类型(Long and Double)。作用声明于方法内部(必须).method public getName()V .registers 6 retu...
继续阅读 »

介绍

对于dalviks字节码寄存器都是32位的,它能够表示任何类型,2个寄存器用于表示64位的类型(Long and Double)。

作用

声明于方法内部(必须)

.method public getName()V
.registers 6

return-void
.end method

.registers和locals基本区别

在一个方法(method)中有两中方式指定有多少个可用的寄存器。指令.registers指令指定了在这个方法中有多少个可用的寄存器,

指令.locals指明了在这个方法中非参(non-parameter)寄存器的数量。然而寄存器的总数也包括保存方法参数的寄存器。

参数是如何传递的?

1.如果是非静态方法

例如,你写了一个非静态方法LMyObject;->callMe(II)V。这个方法有2个int参数,但在这两个整型参数前面还有一个隐藏的参数LMyObject;也就是当前对象的引用,所以这个方法总共有3个参数。 假如在一个方法中包含了五个寄存器(V0-V4),如下:

.method public callMe(II)V
const-string v0,"1"
const-string v1,"1"

return-void
.end method

那么只需用.register指令指定5个,或者使用.locals指令指定2个(2个local寄存器+3个参数寄存器)。如下:

.method public callMe(II)V
.registers 5
const-string v0,"1"
const-string v1,"1"
v3==>p0
V4==>P1
V5==>P2

return-void
.end method

或者
.method public callMe(II)V
.locals 2
const-string v0,"1"
const-string v1,"1"
return-void
.end method

该方法被调用的时候,调用方法的对象(即this引用)会保存在V2中,第一个参数在V3中,第二个参数在v4中。

2.如果是静态方法

那么参数少了对象引用,除此之外和非静态原理相同,registers为4 locals依然是2

关于寄存器命名规则

v命名法

上面的例子中我们使用的是v命名法,也就是在本地寄存器后面依次添加参数寄存器,

但是这种命名方式存在一种问题:假如我后期想要修改方法体的内容,涉及到增加或者删除寄存器,由于v命名法需要排序的局限性,那么会造成大量代码的改动,有没有一种办法让我们只改动registers或者locals的值就可以了呢, 答案是:有的

v命名法之外,还有一种命名法叫做p命名法

p命名法

p命名法只能给方法参数命名,不能给本地变量命名

假如有一个非静态方法如下:

.method public print(Ljava/lang/String;Ljava/lang/String;I)V

以下是p命名法参数对应表:

p0this
p1第一个参数Ljava/lang/String;
p2第二个参数Ljava/lang/String;
p3第三个参数I

如前面提到的,long和double类型都是64位,需要2个寄存器。当你引用参数的时候一定要记住,例如:你有一个非静态方法

LMyObject;->MyMethod(IJZ)V

方法的参数为int、long、bool。所以这个方法的所有参数需要5个寄存器。

p0this
p1I
p2, p3J
p4Z

另外当你调用方法后,你必须在寄存器列表,调用指令中指明,两个寄存器保存了double-wide宽度的参数。

注意:在默认的baksmali中,参数寄存器将使用P命名方式,如果出于某种原因你要禁用P命名方式,而要强制使用V命名方式,应当使用-p/--no-parameter-registers选项。

总结

  • locals和registers都可以表示寄存器数量,locals指定本地局部变量寄存器个数,registers是locals和参数寄存器数量的总数,两者使用任选其一
  • 同时,寄存器命名一共分两种,一种是v命名法,另一种是p命名法
v0the first local register
v1the second local register
v2p0the first parameter register
v3p1the second parameter register
v4p2the third parameter register

收起阅读 »

Swift 数组

iOS
Swift 数组Swift 数组使用有序列表存储同一类型的多个值。相同的值可以多次出现在一个数组的不同位置中。Swift 数组会强制检测元素的类型,如果类型不同则会报错,Swift 数组应该遵循像Array<Element>这样的形式,其中Elem...
继续阅读 »

Swift 数组

Swift 数组使用有序列表存储同一类型的多个值。相同的值可以多次出现在一个数组的不同位置中。

Swift 数组会强制检测元素的类型,如果类型不同则会报错,Swift 数组应该遵循像Array<Element>这样的形式,其中Element是这个数组中唯一允许存在的数据类型。

如果创建一个数组,并赋值给一个变量,则创建的集合就是可以修改的。这意味着在创建数组后,可以通过添加、删除、修改的方式改变数组里的项目。如果将一个数组赋值给常量,数组就不可更改,并且数组的大小和内容都不可以修改。


创建数组

我们可以使用构造语法来创建一个由特定数据类型构成的空数组:

var someArray = [SomeType]()

以下是创建一个初始化大小数组的语法:

var someArray = [SomeType](repeating: InitialValue, count: NumbeOfElements)

以下实例创建了一个类型为 Int ,数量为 3,初始值为 0 的空数组:

var someInts = [Int](repeating: 0, count: 3)

以下实例创建了含有三个元素的数组:

var someInts:[Int] = [10, 20, 30]

访问数组

我们可以根据数组的索引来访问数组的元素,语法如下:

var someVar = someArray[index]

index 索引从 0 开始,即索引 0 对应第一个元素,索引 1 对应第二个元素,以此类推。

我们可以通过以下实例来学习如何创建,初始化,访问数组:

import Cocoa

var someInts = [Int](repeating: 10, count: 3)

var someVar = someInts[0]

print( "第一个元素的值 \(someVar)" )
print( "第二个元素的值 \(someInts[1])" )
print( "第三个元素的值 \(someInts[2])" )

以上程序执行输出结果为:

第一个元素的值 10
第二个元素的值 10
第三个元素的值 10

修改数组

你可以使用 append() 方法或者赋值运算符 += 在数组末尾添加元素,如下所示,我们初始化一个数组,并向其添加元素:

import Cocoa

var someInts = [Int]()

someInts
.append(20)
someInts
.append(30)
someInts
+= [40]

var someVar = someInts[0]

print( "第一个元素的值 \(someVar)" )
print( "第二个元素的值 \(someInts[1])" )
print( "第三个元素的值 \(someInts[2])" )

以上程序执行输出结果为:

第一个元素的值 20
第二个元素的值 30
第三个元素的值 40

我们也可以通过索引修改数组元素的值:

import Cocoa

var someInts = [Int]()

someInts
.append(20)
someInts
.append(30)
someInts
+= [40]

// 修改最后一个元素
someInts
[2] = 50

var someVar = someInts[0]

print( "第一个元素的值 \(someVar)" )
print( "第二个元素的值 \(someInts[1])" )
print( "第三个元素的值 \(someInts[2])" )

以上程序执行输出结果为:

第一个元素的值 20
第二个元素的值 30
第三个元素的值 50

遍历数组

我们可以使用for-in循环来遍历所有数组中的数据项:

import Cocoa

var someStrs = [String]()

someStrs
.append("Apple")
someStrs
.append("Amazon")
someStrs
.append("Runoob")
someStrs
+= ["Google"]

for item in someStrs {
print(item)
}

以上程序执行输出结果为:

Apple
Amazon
Runoob
Google

如果我们同时需要每个数据项的值和索引值,可以使用 String 的 enumerate() 方法来进行数组遍历。实例如下:

import Cocoa

var someStrs = [String]()

someStrs
.append("Apple")
someStrs
.append("Amazon")
someStrs
.append("Runoob")
someStrs
+= ["Google"]

for (index, item) in someStrs.enumerated() {
print("在 index = \(index) 位置上的值为 \(item)")
}

以上程序执行输出结果为:

 index = 0 位置上的值为 Apple
index = 1 位置上的值为 Amazon
index = 2 位置上的值为 Runoob
index = 3 位置上的值为 Google

合并数组

我们可以使用加法操作符(+)来合并两种已存在的相同类型数组。新数组的数据类型会从两个数组的数据类型中推断出来:

import Cocoa

var intsA = [Int](repeating: 2, count:2)
var intsB = [Int](repeating: 1, count:3)

var intsC = intsA + intsB

for item in intsC {
print(item)
}

以上程序执行输出结果为:

2
2
1
1
1

count 属性

我们可以使用 count 属性来计算数组元素个数:

import Cocoa

var intsA = [Int](count:2, repeatedValue: 2)
var intsB = [Int](count:3, repeatedValue: 1)

var intsC = intsA + intsB

print("intsA 元素个数为 \(intsA.count)")
print("intsB 元素个数为 \(intsB.count)")
print("intsC 元素个数为 \(intsC.count)")

以上程序执行输出结果为:

intsA 元素个数为 2
intsB
元素个数为 3
intsC
元素个数为 5

isEmpty 属性

我们可以通过只读属性 isEmpty 来判断数组是否为空,返回布尔值:

import Cocoa

var intsA = [Int](count:2, repeatedValue: 2)
var intsB = [Int](count:3, repeatedValue: 1)
var intsC = [Int]()

print("intsA.isEmpty = \(intsA.isEmpty)")
print("intsB.isEmpty = \(intsB.isEmpty)")
print("intsC.isEmpty = \(intsC.isEmpty)")

以上程序执行输出结果为:

intsA.isEmpty = false
intsB
.isEmpty = false
intsC
.isEmpty = true
收起阅读 »

Swift 字符(Character)

iOS
Swift 的字符是一个单一的字符字符串字面量,数据类型为 Character。以下实例列出了两个字符实例:import Cocoa let char1: Character = "A" let char2: Character = "B" print("...
继续阅读 »

Swift 的字符是一个单一的字符字符串字面量,数据类型为 Character。

以下实例列出了两个字符实例:

import Cocoa

let char1: Character = "A"
let char2: Character = "B"

print("char1 的值为 \(char1)")
print("char2 的值为 \(char2)")

以上程序执行输出结果为:

char1 的值为 A
char2
的值为 B

如果你想在 Character(字符) 类型的常量中存储更多的字符,则程序执行会报错,如下所示:

import Cocoa

// Swift 中以下赋值会报错
let char: Character = "AB"

print("Value of char \(char)")

以上程序执行输出结果为:

error: cannot convert value of type 'String' to specified type 'Character'
let char: Character = "AB"

空字符变量

Swift 中不能创建空的 Character(字符) 类型变量或常量:

import Cocoa

// Swift 中以下赋值会报错
let char1: Character = ""
var char2: Character = ""

print("char1 的值为 \(char1)")
print("char2 的值为 \(char2)")

以上程序执行输出结果为:

 error: cannot convert value of type 'String' to specified type 'Character'
let char1: Character = ""
^~
error
: cannot convert value of type 'String' to specified type 'Character'
var char2: Character = ""

遍历字符串中的字符

Swift 的 String 类型表示特定序列的 Character(字符) 类型值的集合。 每一个字符值代表一个 Unicode 字符。

Swift 3 中的 String 需要通过 characters 去调用的属性方法,在 Swift 4 中可以通过 String 对象本身直接调用,例如:

Swift 3 中:

import Cocoa

for ch in "Runoob".characters {
print(ch)
}

Swift 4 中:

import Cocoa

for ch in "Runoob" {
print(ch)
}

以上程序执行输出结果为:

R
u
n
o
o
b

字符串连接字符

以下实例演示了使用 String 的 append() 方法来实现字符串连接字符:

import Cocoa

var varA:String = "Hello "
let varB:Character = "G"

varA
.append( varB )

print("varC = \(varA)")

以上程序执行输出结果为:

varC  =  Hello G
收起阅读 »

Swift 字符串

iOS
Swift 字符串是一系列字符的集合。例如 "Hello, World!" 这样的有序的字符类型的值的集合,它的数据类型为 String。创建字符串你可以通过使用字符串字面量或 String 类的实例来创建一个字符串:import Cocoa //...
继续阅读 »

Swift 字符串是一系列字符的集合。例如 "Hello, World!" 这样的有序的字符类型的值的集合,它的数据类型为 String


创建字符串

你可以通过使用字符串字面量或 String 类的实例来创建一个字符串:

import Cocoa

// 使用字符串字面量
var stringA = "Hello, World!"
print( stringA )

// String 实例化
var stringB = String("Hello, World!")
print( stringB )

以上程序执行输出结果为:

Hello, World!
Hello, World!

空字符串

你可以使用空的字符串字面量赋值给变量或初始化一个String类的实例来初始值一个空的字符串。 我们可以使用字符串属性 isEmpty 来判断字符串是否为空:

import Cocoa

// 使用字符串字面量创建空字符串
var stringA = ""

if stringA.isEmpty {
print( "stringA 是空的" )
} else {
print( "stringA 不是空的" )
}

// 实例化 String 类来创建空字符串
let stringB = String()

if stringB.isEmpty {
print( "stringB 是空的" )
} else {
print( "stringB 不是空的" )
}

以上程序执行输出结果为:

stringA 是空的
stringB 是空的

字符串常量

你可以将一个字符串赋值给一个变量或常量,变量是可修改的,常量是不可修改的。

import Cocoa

// stringA 可被修改
var stringA = "菜鸟教程:"
stringA += "http://www.runoob.com"
print( stringA )

// stringB 不能修改
let stringB = String("菜鸟教程:")
stringB += "http://www.runoob.com"
print( stringB )

以上程序执行输出结果会报错,因为 stringB 为常量是不能被修改的:

error: left side of mutating operator isn't mutable: 'stringB' is a 'let' constant
stringB += "http://www.runoob.com"

字符串中插入值

字符串插值是一种构建新字符串的方式,可以在其中包含常量、变量、字面量和表达式。 您插入的字符串字面量的每一项都在以反斜线为前缀的圆括号中:

import Cocoa

var varA = 20
let constA = 100
var varC:Float = 20.0

var stringA = "\(varA) 乘于 \(constA) 等于 \(varC * 100)"
print( stringA )

以上程序执行输出结果为:

20 乘于 100 等于 2000.0

字符串连接

字符串可以通过 + 号来连接,实例如下:

import Cocoa

let constA = "菜鸟教程:"
let constB = "http://www.runoob.com"

var stringA = constA + constB

print( stringA )

以上程序执行输出结果为:

菜鸟教程:http://www.runoob.com

字符串长度

字符串长度使用 String.count 属性来计算,实例如下:

Swift 3 版本使用的是 String.characters.count

import Cocoa

var varA = "www.runoob.com"

print( "\(varA), 长度为 \(varA.count)" )

以上程序执行输出结果为:

http://www.runoob.com, 长度为 14

字符串比较

你可以使用 == 来比较两个字符串是否相等:

import Cocoa

var varA = "Hello, Swift!"
var varB = "Hello, World!"

if varA == varB {
print( "\(varA) 与 \(varB) 是相等的" )
} else {
print( "\(varA) 与 \(varB) 是不相等的" )
}

以上程序执行输出结果为:

Hello, Swift! 与 Hello, World! 是不相等的

Unicode 字符串

Unicode 是一个国际标准,用于文本的编码,Swift 的 String 类型是基于 Unicode建立的。你可以循环迭代出字符串中 UTF-8 与 UTF-16 的编码,实例如下:

import Cocoa

var unicodeString = "菜鸟教程"

print("UTF-8 编码: ")
for code in unicodeString.utf8 {
print("\(code) ")
}

print("\n")

print("UTF-16 编码: ")
for code in unicodeString.utf16 {
print("\(code) ")
}

以上程序执行输出结果为:

UTF-8 编码: 
232
143
156
233
184
159
230
149
153
231
168
139
UTF-16 编码:
33756
40479
25945
31243

字符串函数及运算符

Swift 支持以下几种字符串函数及运算符:

序号函数/运算符 & 描述
1

isEmpty

判断字符串是否为空,返回布尔值

2

hasPrefix(prefix: String)

检查字符串是否拥有特定前缀

3

hasSuffix(suffix: String)

检查字符串是否拥有特定后缀。

4

Int(String)

转换字符串数字为整型。 实例:

let myString: String = "256"
let myInt: Int? = Int(myString)

5

String.count

Swift 3 版本使用的是 String.characters.count

计算字符串的长度

6

utf8

您可以通过遍历 String 的 utf8 属性来访问它的 UTF-8 编码

7

utf16

您可以通过遍历 String 的 utf8 属性来访问它的 utf16 编码

8

unicodeScalars

您可以通过遍历String值的unicodeScalars属性来访问它的 Unicode 标量编码.

9

+

连接两个字符串,并返回一个新的字符串

10

+=

连接操作符两边的字符串并将新字符串赋值给左边的操作符变量

11

==

判断两个字符串是否相等

12

<

比较两个字符串,对两个字符串的字母逐一比较。

13

!=

比较两个字符串是否不相等。

收起阅读 »

Swift 实战技巧

iOS
Swift实战技巧从OC转战到Swift,差别还是蛮大的,本文记录了我再从OC转到Swift开发过程中遇到的一些问题,然后把我遇到的这些问题记录形成文章,大体上是一些Swift语言下面的一些技巧,希望对有需要的人有帮助OC调用方法的处理给OC调用的方法需要添加...
继续阅读 »

Swift实战技巧

从OC转战到Swift,差别还是蛮大的,本文记录了我再从OC转到Swift开发过程中遇到的一些问题,然后把我遇到的这些问题记录形成文章,大体上是一些Swift语言下面的一些技巧,希望对有需要的人有帮助

  • OC调用方法的处理

给OC调用的方法需要添加@objc标记,一般的action-target的处理方法,通知的处理方法等需要添加@objc标记

@objc func onRefresh(){
self.refreshCallback?()
}

  • 处理SEL选择子

使用方法型如 #selector(方法名称)
eg.

`#selector(self.onRefresh))`  

更加详细的介绍可以看这篇文章: http://swifter.tips/selector/

下面是使用MJRefresh给mj_headermj_footer添加回调处理函数的例子

self.mj_header.setRefreshingTarget(self, refreshingAction: #selector(self.onRefresh))
self.mj_footer.setRefreshingTarget(self, refreshingAction: #selector(self.onLoadMore))

  • try关键字的使用

可能发生异常的方法使用try?方法进行可选捕获异常

let jsonStr=try?String(contentsOfFile: jsonPath!)

  • 类对象参数和类对象的参数值

AnyClass作为方法的形参,类名称.self(modelClass.self)作为实参

    func registerCellNib(nib:UINib,modelClass:AnyClass){
self.register(nib, forCellReuseIdentifier: String(describing:modelClass.self))
}

...
self.tableView?.registerCellNib(nib: R.nib.gameCell(), modelClass: GameModel.self)

  • 线程间调用

主线程使用DispatchQueue.main,全局的子线程使用DispatchQueue.global(),方法可以使用syncasyncasyncAfter等等

下面是网络请求线程间调用的例子

let _  = URLSession.shared.dataTask(with: url, completionHandler: { [weak self] (data, response, error) in
guard let weakSelf = self else {
return
}
if error == nil {
if let json = try? JSONSerialization.jsonObject(with: data!, options: .mutableContainers) {
let data = json as! [Any]
DispatchQueue.main.async {
weakSelf.suggestions = data[1] as! [String]
if weakSelf.suggestions.count > 0 {
weakSelf.tableView.reloadData()
weakSelf.tableView.isHidden = false
} else {
weakSelf.tableView.isHidden = true
}
}
}
}
}).resume()

  • 闭包中使用weak防止循环引用的语法
URLSession.shared.dataTask(with: requestURL) {[weak self] (data, response, error) in
guard let weakSelf = self else {
return
}
weakSelf.tableView.reloadData()
}

  • 逃逸闭包和非逃逸闭包
    逃逸闭包,在方法执行完成之后才调用的闭包称为逃逸闭包,一般在方法中做异步处理耗时的任务,任务完成之后把结果使用闭包进行回调处理使用的闭包为逃逸闭包,需要显示的使用@escaping关键字修饰
    非逃逸闭包,在方法执行完成之前调用的闭包称为逃逸闭包,比如snapkit框架使用的闭包是在方法执行完成之后就已经处理完毕了
    Swift3之后闭包默认都是非逃逸(@noescape,不能显示声明),并且这种类型是不能显示使用@noescape关键字修饰的
    // 模拟网络请求,completion闭包是异步延迟处理的,所以需要添加`@escaping`进行修饰
class func fetchVideos(completion: @escaping (([Video]) -> Void)) {
DispatchQueue.global().async {
let video1 = Video.init(title: "What Does Jared Kushner Believe", channelName: "Nerdwriter1")
let video2 = Video.init(title: "Moore's Law Is Ending. So, What's Next", channelName: "Seeker")
let video3 = Video.init(title: "What Bill Gates is afraid of", channelName: "Vox")
var items = [video1, video2, video3]
items.shuffle()
DispatchQueue.main.asyncAfter(deadline: DispatchTime.init(uptimeNanoseconds: 3000000000), execute: {
completion(items)
})
}
}

  • Notification.Name的封装处理

swift3中Notification的名字是一种特殊的Notification.Name类型,下面使用enum进行封装处理,并且创建一个NotificationCenter的扩展,处理通知消息的发送

// 定义Notification.Name枚举
enum YTNotification: String {
case scrollMenu
case didSelectMenu
case openPage
case hideBar

var stringValue: String {
return "YT" + rawValue
}

// 枚举成员返回对应的Notification.Name类型
var notificationName: NSNotification.Name {
return Notification.Name.init(stringValue)
}
}

extension NotificationCenter {
func yt_post(custom notification: YTNotification, object anObject: Any?, userInfo aUserInfo: [AnyHashable : Any]? = nil) {
self.post(name: notification.notificationName, object: anObject, userInfo: aUserInfo)
}
}

使用方法
添加通知观察者使用的是YTNotification枚举成员的notificationName返回的Notification.Name类型的值
发送消息使用的是YTNotification枚举成员

// 添加通知观察
NotificationCenter.default.addObserver(self, selector: #selector(self.changeTitle(notification:)), name: YTNotification.scrollMenu.notificationName, object: nil)

// 发送消息
NotificationCenter.default.yt_post(custom: YTNotification.scrollMenu, object: nil, userInfo: ["length": scrollIndex])

lazy惰性加载属性,只有在使用的时候才初始化变量

    // 闭包的方式
let menuTitles = ["History", "My Videos", "Notifications", "Watch Later"]
lazy var menuItems : [MenuItem] = {
var tmenuItems = [MenuItem]()
for menuTitle in menuTitles {
let menuItem = MenuItem(iconImage: UIImage.init(named: menuTitle)!, title: menuTitle)
tmenuItems.append(menuItem)
}
return tmenuItems
}()

// 普通方式,
lazy var titles = ["A", "B"]

  • 类型判断

使用is判断类型以及使用if-let和as?判断类型

// MARK:- 类型检查例子
let sa = [
Chemistry(physics: "固体物理", equations: "赫兹"),
Maths(physics: "流体动力学", formulae: "千兆赫"),
Chemistry(physics: "热物理学", equations: "分贝"),
Maths(physics: "天体物理学", formulae: "兆赫"),
Maths(physics: "微分方程", formulae: "余弦级数")]

var chemCount = 0
var mathsCount = 0
for item in sa {
// 如果是一个 Chemistry 类型的实例,返回 true,相反返回 false。 相当于isKindOfClass
if item is Chemistry {
chemCount += 1
} else if item is Maths {
mathsCount += 1
}
}

// 使用if-let和as?判断类型
for item in sa {
// 如果是一个 Chemistry 类型的实例,返回 true,相反返回 false。 相当于isKindOfClass
if let _ = item as? Chemistry {
chemCount += 1
} else if let _ = item as? Maths {
mathsCount += 1
}
}

使用switch-case和as判断类型

// Any可以表示任何类型,包括方法类型
var exampleany = [Any]()
exampleany.append(12)
exampleany.append(3.14159)
exampleany.append("Any 实例")
exampleany.append(Chemistry(physics: "固体物理", equations: "兆赫"))

// 使用switch-case和as判断类型
for item2 in exampleany {
switch item2 {
case let someInt as Int:
print("整型值为 \(someInt)")
case let someDouble as Double where someDouble > 0:
print("Pi 值为 \(someDouble)")
case let someString as String:
print("\(someString)")
case let phy as Chemistry:
print("主题 '\(phy.physics)', \(phy.equations)")
default:
print("None")
}
}

  • Swift使用KVC,执行KVC操作的变量需要添加@objc标记
class Feed: NSObject, HandyJSON  {
// 使用KVC添加@objc关键字
@objc var id = 0
var type = ""
var payload: PayLoad?
var user: PostUser?

required override init() {}
}

  • swift中CGRect类型的操作

swift中简化了CGRect类型的操作,比如有一个CGRect的类型实例为frame,以下例举了OC中对应的在swift下的语法

OCSwift
CGRectGetMaxX(frame)frame.maxX
CGRectGetMinY(frame)frame.minY
CGRectGetMidX(frame)frame.midX
CGRectGetWidth(frame)frame.width
CGRectGetHeight(frame)frame.height
CGRectContainsPoint(frame, point)frame.contains(point)
  • Swift中指针的处理

详细的介绍可以查看这篇文章:http://swifter.tips/unsafe/
下面是一个使用OC库RegexKitLite中的一个例子,block中返回值是指针类型的,需要转换为对应的swift对象类型

func composeAttrStr(text: String) -> NSAttributedString {
// 表情的规则
let emotionPattern = "\\[[0-9a-zA-Z\\u4e00-\\u9fa5]+\\]";
// @的规则
let atPattern = "@[0-9a-zA-Z\\u4e00-\\u9fa5-_]+";
// #话题#的规则
let topicPattern = "#[0-9a-zA-Z\\u4e00-\\u9fa5]+#";
// url链接的规则
let urlPattern = "\\b(([\\w-]+://?|www[.])[^\\s()<>]+(?:\\([\\w\\d]+\\)|([^[:punct:]\\s]|/)))";
let pattern = "\(emotionPattern)|\(atPattern)|\(topicPattern)|\(urlPattern)"

var textParts = [TextPart]()

(text as! NSString).enumerateStringsMatched(byRegex: pattern) { (captureCount: Int, capString: UnsafePointer<NSString?>?, capRange: UnsafePointer<NSRange>?, stop: UnsafeMutablePointer<ObjCBool>?) in
let captureString = capString?.pointee as! String
let captureRange = capRange?.pointee as! NSRange

let part = TextPart()
part.text = captureString
part.isSpecial = true
part.range = captureRange
textParts.append(part)
}
// ...
}

  • 只有类才能实现的protocol 有一种场景,protocol作为delegate,需要使用weak关键字修饰的时候,需要指定delegate的类型为ptotocol型,这个ptotocol需要添加class修饰符,比如下面的这个protocol,因为类类型的对象才有引用计数,才有weak的概念,没有引用计数的struct型是没有weak概念的
/// ImageViewer和ImageCell交互使用的协议
protocol YTImageCellProtocol : class {
// Cell的点击事件,处理dismiss
func imageCell(_ imageCell: YTImageCell, didSingleTap: Bool);
}
收起阅读 »

手摸手教你用webpack搭建TS开发环境

前言 最近在学习typescript,也就是我们常说的TS,它是JS的超集。具体介绍就不多说了,今天主要是带大家用webpack从零搭建一个TS开发环境。直接用传统的tsc xx.ts文件进行编译的话太繁琐,不利于我们开发,经过这次手动配置,我们也能知道vue...
继续阅读 »

前言


最近在学习typescript,也就是我们常说的TS,它是JS的超集。具体介绍就不多说了,今天主要是带大家用webpack从零搭建一个TS开发环境。直接用传统的tsc xx.ts文件进行编译的话太繁琐,不利于我们开发,经过这次手动配置,我们也能知道vue3内部对TS的webpack进行了怎样的配置,废话不多说进入正题。


Node 编译TS


先讲讲如何运行ts文件吧,最传统的方式当然是直接输入命令



tsc xxx.ts



当然你必须得先安装过ts,如果没有请执行以下命令



npm install typescript -g



安装后查看下版本



tsc --version



这样我们就能得到编译后的js文件了,然后我们可以通过node指令



node xxx.js



进行查看,当然也可以新建一个HTML页面引入编译后的js文件


我们从上可以发现有点小复杂,那可不可以直接通过Node直接编译TS呢?接来下就是介绍这种方法

使用ts-node 就可以得到我们想要的效果

安装



npm install ts-node -g



另外ts-node需要依赖 tslib 和 @types/node 两个包,也需要下载



npm install tslib @types/node -g



现在,我们可以直接通过 ts-node 来运行TypeScript的代码



ts-node xxx.ts



如果遇到很多ts文件,那我们用这种方法也会觉得繁琐,所以我们最好是用webpack搭建一个支持TS开发环境,这样才是最好的解决方案。


webpack搭建准备工作


先新建一个文件夹

下载 webpack webpack-cli



npm install webpack webpack-cli -D



下载 ts tsloader(编译ts文件)



npm install typescript ts-loader -D



下载 webpack-dev-server(搭建本地服务器)



npm install webpack-dev-server -D



下载 html模板插件



npm install html-webpack-plugin -D



初始化webpack



npm init



初始化ts



tsc --init



新建配置文件 webpack.config.js


初始化后文件结构如下图所示,当然还有一些测试ts和html需要自己手动创建下
image.png


webpack 配置


配置之前我们先去package.json中添加两个运行和打包指令


image.png


webpack.config.js


代码中有详细说明哦


const path = require('path')//引入内置path方便得到绝对路径
const HtmlWebpackPlugin = require('html-webpack-plugin')//引入模板组件


module.exports = {
mode: 'development',//开发模式
entry: './src/main.ts',//入口文件地址
output: {
path: path.resolve(__dirname, "./dist"),//出口文件,即打包后的文件存放地址
filename: 'bundle.js' //文件名
},
devServer: {

},
resolve: {
extensions:['.ts', '.js', '.cjs', '.json'] //配置文件引入时省略后缀名
},
module: {
rules: [
{
test: /\.ts$/, //匹配规则 以ts结尾的文件
loader: 'ts-loader' //对应文件采用ts-loader进行编译
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './index.html' //使用模板地址
})
]
}

配置完成我们可以进行测试了,执行指令



npm run serve



打包指令



npm run build



End


看完的话点个赞吧~~


QQ图片20200210181218.jpg



收起阅读 »

用 JS 写算法时你应该知道的——数组不能当队列使用!!

在初学 JS 时,发现数组拥有 shift()、unshift()、pop()、push() 这一系列方法,而不像 Java 或 CPP 中分别引用队列、栈等数据结构,还曾偷偷窃喜。现在想想,这都是以高昂的复杂度作为代价的QAQ。 举个例子 - BFS 一般队...
继续阅读 »

在初学 JS 时,发现数组拥有 shift()unshift()pop()push() 这一系列方法,而不像 Java 或 CPP 中分别引用队列、栈等数据结构,还曾偷偷窃喜。现在想想,这都是以高昂的复杂度作为代价的QAQ。


举个例子 - BFS


一般队列的应用是在 BFS 题目中使用到。BFS(Breath First Search)广度优先搜索,作为入门算法,基本原理大家应该都了解,这里不再细说。


LeetCode 1765. 地图中的最高点



给你一个大小为 m x n 的整数矩阵 isWater ,它代表了一个由 陆地 和 水域 单元格组成的地图。


如果 isWater[i][j] == 0 ,格子 (i, j) 是一个 陆地 格子。
如果 isWater[i][j] == 1 ,格子 (i, j) 是一个 水域 格子。
你需要按照如下规则给每个单元格安排高度:



  • 每个格子的高度都必须是非负的。

  • 如果一个格子是是 水域 ,那么它的高度必须为 0 。

  • 任意相邻的格子高度差 至多 为 1 。当两个格子在正东、南、西、北方向上相互紧挨着,就称它们为相邻的格子。(也就是说它们有一条公共边)


找到一种安排高度的方案,使得矩阵中的最高高度值 最大 。


请你返回一个大小为 m x n 的整数矩阵 height ,其中 height[i][j] 是格子 (i, j) 的高度。如果有多种解法,请返回 任意一个 。



常规 BFS 题目,从所有的水域出发进行遍历,找到每个点离水域的最近距离即可。常规写法,三分钟搞定。


/**
* @param {number[][]} isWater
* @return {number[][]}
*/
var highestPeak = function(isWater) {
// 每个水域的高度都必须是0
// 一个格子离最近的水域的距离 就是它的最大高度
let n = isWater.length, m = isWater[0].length;
let height = new Array(n).fill().map(() => new Array(m).fill(-1));
let q = [];
for (let i = 0; i < n; i++) {
for (let j = 0; j < m; j++) {
if (isWater[i][j] === 1) {
q.push([i, j]);
height[i][j] = 0;
}
}
}
let dir = [[0, 1], [0, -1], [1, 0], [-1, 0]];
while (q.length) {
for (let i = q.length - 1; i >= 0; i--) {
let [x, y] = q.shift();
for (let [dx, dy] of dir) {
let nx = x + dx, ny = y + dy;
if (nx < n && nx >= 0 && ny < m && ny >= 0 && height[nx][ny] === -1) {
q.push([nx, ny]);
height[nx][ny] = height[x][y] + 1;
}
}
}
}
return height;
};

然后,超时了……


调整一下,


/**
* @param {number[][]} isWater
* @return {number[][]}
*/
var highestPeak = function(isWater) {
// 每个水域的高度都必须是0
// 一个格子离最近的水域的距离 就是它的最大高度
let n = isWater.length, m = isWater[0].length;
let height = new Array(n).fill().map(() => new Array(m).fill(-1));
let q = [];
for (let i = 0; i < n; i++) {
for (let j = 0; j < m; j++) {
if (isWater[i][j] === 1) {
q.push([i, j]);
height[i][j] = 0;
}
}
}
let dir = [[0, 1], [0, -1], [1, 0], [-1, 0]];
while (q.length) {
let tmp = [];
for (let i = q.length - 1; i >= 0; i--) {
let [x, y] = q[i];
for (let [dx, dy] of dir) {
let nx = x + dx, ny = y + dy;
if (nx < n && nx >= 0 && ny < m && ny >= 0 && height[nx][ny] === -1) {
tmp.push([nx, ny]);
height[nx][ny] = height[x][y] + 1;
}
}
}
q = tmp;
}
return height;
};

ok,这回过了,而且打败了 90% 的用户。


image.png


那么问题出在哪里呢?shift()!!!


探究 JavaScript 中 shift() 的实现


在学习 C++ 的时候,队列作为一个先入先出的数据结构,入队和出队肯定都是O(1)的时间复杂度,用链表


让我们查看下 V8 中 shift() 的源码


简单实现就是


function shift(arr) {
let len = arr.length;
if (len === 0) {
return;
}
let first = arr[0];
for (let i = 0; i < len - 1; i++) {
arr[i] = arr[i + 1];
}
arr.length = len - 1;
return first;
}

所以,shift()O(N) 的!!! 吐血 QAQ


同理,unshift() 也是 O(N) 的,不过,pop()push()O(1),也就是说把数组当做栈是没有问题的。


我就是想用队列怎么办!


没想到作为一个 JSer,想好好地用个队列都这么难……QAQ


找到了一个队列实现,详情见注释。


/*

Queue.js

A function to represent a queue

Created by Kate Morley - http://code.iamkate.com/ - and released under the terms
of the CC0 1.0 Universal legal code:

http://creativecommons.org/publicdomain/zero/1.0/legalcode

*/

/* Creates a new queue. A queue is a first-in-first-out (FIFO) data structure -
* items are added to the end of the queue and removed from the front.
*/
function Queue(){

// initialise the queue and offset
var queue = [];
var offset = 0;

// Returns the length of the queue.
this.getLength = function(){
return (queue.length - offset);
}

// Returns true if the queue is empty, and false otherwise.
this.isEmpty = function(){
return (queue.length == 0);
}

/* Enqueues the specified item. The parameter is:
*
* item - the item to enqueue
*/
this.enqueue = function(item){
queue.push(item);
}

/* Dequeues an item and returns it. If the queue is empty, the value
* 'undefined' is returned.
*/
this.dequeue = function(){

// if the queue is empty, return immediately
if (queue.length == 0) return undefined;

// store the item at the front of the queue
var item = queue[offset];

// increment the offset and remove the free space if necessary
if (++ offset * 2 >= queue.length){
queue = queue.slice(offset);
offset = 0;
}

// return the dequeued item
return item;

}

/* Returns the item at the front of the queue (without dequeuing it). If the
* queue is empty then undefined is returned.
*/
this.peek = function(){
return (queue.length > 0 ? queue[offset] : undefined);
}

}

把最初代码中的数组改为 Queue,现在终于可以通过了。:)



收起阅读 »

如何“优雅”地修改 node_modules 下的代码?

在实际开发过程中当我们遇到 node_modules 中的 A 包有 bug 时候,通常开发者有几个选择: 方法一:给 A 包提 issue 等待他人修复并发布:做好石沉大海或修复周期很长的准备。 方法二:给 A 包提 mr 自行修复并等待发布:很棒,不过你最...
继续阅读 »

在实际开发过程中当我们遇到 node_modules 中的 A 包有 bug 时候,通常开发者有几个选择:


方法一:给 A 包提 issue 等待他人修复并发布:做好石沉大海或修复周期很长的准备。


方法二:给 A 包提 mr 自行修复并等待发布:很棒,不过你最好祈祷作者发版积极,并且新版本向下兼容。


方法三:把 A 包的源码拖出来自己维护:有点暴力且事后维护成本较高,不过应急时也能勉强接受。


等等,可如果出问题的包是“幽灵依赖”呢,比如项目的依赖链是: A -> B -> C,此时 C 包有 bug。那么上面三个方法的改动需要同时影响到 A、B、C 三个包,修复周期可能就更长了,可是你今晚就要上线啊,这可怎么办?


1


上线要紧,直接手动修改 node_modules 下的代码给缺陷包打个临时补丁吧,可问题又来了,改动只能在本地生效,构建却在云端, 积极的同学开始写起了脚本,然后陷入一个个坑里...


上述场景下即可考虑使用 patch-package 这个包,假设我们现在的源码结构如下所示:


├── node_modules  
│ └── lodash
│ └── toString.js
├── src
│ └── app.js // 依赖 lodash 的 toString 方法
└── package.json

node_modules/lodash/toString.js


var baseToString = require('./_baseToString')

function toString(value) {
return value == null ? '' : baseToString(value);
}

module.exports = toString;

src/app.js


const toString = require('lodash/toString')
console.log(toString(123));

假设现在需要修改 node_modules/lodash/toString.js 文件,只需要遵循以下几步即可“优雅”完成修改:


第一步:安装依赖


yarn add patch-package postinstall-postinstall -D

第二步:修改 node_modules/lodash/toString.js 文件


function toString(value) {
console.log('it works!!!'); // 这里插入一行代码
return value == null ? '' : baseToString(value);
}

module.exports = toString;

第三步:生成修改文件


npx patch-package lodash

这一步运行后会生成 patches/lodash+4.17.21.patch,目录结构变成下面这样:


├── node_modules  
│ └── lodash
│ └── toString.js
├── patches
│ └── lodash+4.17.21.patch
├── src
│ └── app.js
└── package.json

其中 .patch 文件内容如下:


diff --git a/node_modules/lodash/toString.js b/node_modules/lodash/toString.js
index daaf681..8308e76 100644
--- a/node_modules/lodash/toString.js
+++ b/node_modules/lodash/toString.js
@@ -22,6 +22,7 @@ var baseToString = require('./_baseToString');
* // => '1,2,3'
*/
function toString(value) {
+ console.log('it works!!!');
return value == null ? '' : baseToString(value);
}

第四步:修改 package.json 文件


"scripts": {
+ "postinstall": "patch-package"
}

最后重装一下依赖,测试最终效果:


rm -rf node_modules
yarn
node ./src/app.js

// it works!!!
// 123

可以看到,即便重装依赖,我们对 node_modules 下代码的修改还是被 patch-package 还原并最终生效。


至此我们便完成一次临时打补丁的操作,不过这并非真正优雅的长久之计,长期看还是需要彻底修复第三方包缺陷并逐步移除项目中的 .patch 文件。


作者:王力国
链接:https://juejin.cn/post/7022252841116893215

收起阅读 »

封装一个底部导航

前言 在我们日常项目开发中,我们在做移动端的时候会涉及到地步导航功能,所以封装了这个底部导航组件。 底部导航 BottomNav组件属性 1. value选中值(即选中BottomNavPane的name值)值为字符串类型非必填默认为第一个BottomNavP...
继续阅读 »

前言


在我们日常项目开发中,我们在做移动端的时候会涉及到地步导航功能,所以封装了这个底部导航组件。

底部导航


BottomNav组件属性


1. value
选中值(即选中BottomNavPane的name值)
值为字符串类型
非必填默认为第一个BottomNavPane的name

2. lazy
未显示的内容面板是否延迟渲染
值为布尔类型
默认为false

样式要求
组件外面需要包裹可以相对定位的元素,增加样式:position: relative

BottomNavPane组件属性


1. name
英文名称
值为字符串类型
必填

2. icon
导航图标名称
值为字符串类型
值需要与src/assets/icon目录下svg文件的名称一致(name值不含“.svg”后缀)
必填

3. label
导航图标下面显示的文字
值为字符串类型
必填

4. scroll
是否有滚动条
值为布尔类型
默认值为:true

示例


<template>
<div class="bottom-nav-wrap">
<BottomNav v-model="curNav" :lazy="true">
<BottomNavPane name="home" label="首页" icon="home">
<h1>首页内容</h1>
</BottomNavPane>
<BottomNavPane name="oa" label="办公" icon="logo">
<h1>办公内容</h1>
</BottomNavPane>
<BottomNavPane name="page2" label="我的" icon="user">
<h1>个人中心</h1>
</BottomNavPane>
</BottomNav>
</div>
</template>

<script>
import { BottomNav, BottomNavPane } from '@/components/m/bottomNav'

export default {
name: 'BottomNavDemo',
components: {
BottomNav,
BottomNavPane
},
data () {
return {
curNav: ''
}
}
}
</script>

<style lang="scss" scoped>
.bottom-nav-wrap {
position: absolute;
top: $app-title-bar-height;
bottom: 0;
left: 0;
right: 0;
}
</style>

BottomNav.vue


<template>
<div class="bottom-nav">
<div class="nav-pane-wrap">
<slot></slot>
</div>
<div class="nav-list">
<div class="nav-item"
v-for="info in navInfos"
:key="info.name"
:class="{active: info.name === curValue}"
@click="handleClickNav(info.name)">
<Icon class="nav-icon" :name="info.icon"></Icon>
<span class="nav-label">{{info.label}}</span>
</div>
</div>
</div>
</template>
<script>
import { generateUUID } from '@/assets/js/utils.js'
export default {
name: 'BottomNav',
props: {
// 选中导航值(导航的英文名)
value: String,
// 未显示的内容面板是否延迟渲染
lazy: {
type: Boolean,
default: false
}
},
data () {
return {
// 组件实例的唯一ID
id: generateUUID(),
// 当前选中的导航值(导航的英文名)
curValue: this.value,
// 导航信息数组
navInfos: [],
// 导航面板vue实例数组
panes: []
}
},
watch: {
value (val) {
this.curValue = val
},
curValue (val) {
this.$eventBus.$emit('CHANGE_NAV' + this.id, val)
this.$emit('cahnge', val)
}
},
mounted () {
this.calcPaneInstances()
},
beforeDestroy () {
this.$eventBus.$off('CHANGE_NAV' + this.id)
},
methods: {
// 计算导航面板实例信息
calcPaneInstances () {
if (this.$slots.default) {
const paneSlots = this.$slots.default.filter(vnode => vnode.tag &&
vnode.componentOptions && vnode.componentOptions.Ctor.options.name === 'BottomNavPane')
const panes = paneSlots.map(({ componentInstance }) => componentInstance)
const navInfos = paneSlots.map(({ componentInstance }) => {
// console.log(componentInstance.name, componentInstance)
return {
name: componentInstance.name,
label: componentInstance.label,
icon: componentInstance.icon
}
})
this.navInfos = navInfos
this.panes = panes
if (!this.curValue) {
if (navInfos.length > 0) {
this.curValue = navInfos[0].name
}
} else {
this.$eventBus.$emit('CHANGE_NAV' + this.id, this.curValue)
}
}
},
// 导航点击事件处理方法
handleClickNav (val) {
this.curValue = val
}
}
}
</script>
<style lang="scss" scoped>
.bottom-nav {
display: flex;
flex-direction: column;
height: 100%;
.nav-pane-wrap {
flex: 1;
}
.nav-list {
flex: none;
display: flex;
height: 90px;
background-color: #FFF;
align-items: center;
border-top: 1px solid $base-border-color;
.nav-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
line-height: 1;
text-align: center;
color: #666;
.nav-icon {
font-size: 40px;/*yes*/
}
.nav-label {
margin-top: 6px;
font-size: 24px;/*yes*/
}
&.active {
position: relative;
color: $base-color;
}
}
}
}
</style>

BottomNavPane.vue


<template>
<div v-if="canInit" class="bottom-nav-pane" v-show="show">
<Scroll v-if="scroll">
<slot></slot>
</Scroll>
<slot v-else></slot>
</div>
</template>
<script>
import Scroll from '@/components/base/scroll'

export default {
name: 'BottomNavPane',
components: {
Scroll
},
props: {
// 页签英文名称
name: {
type: String,
required: true
},
// 页签显示的标签
label: {
type: String,
required: true
},
// 图标名称
icon: {
type: String,
required: true
},
// 是否有滚动条
scroll: {
type: Boolean,
default: true
}
},
data () {
return {
// 是否显示
show: false,
// 是否已经显示过
hasShowed: false
}
},
computed: {
canInit () {
return (!this.$parent.lazy) || (this.$parent.lazy && this.hasShowed)
}
},
created () {
this.$eventBus.$on('CHANGE_NAV' + this.$parent.id, val => {
if (val === this.name) {
this.show = true
this.hasShowed = true
} else {
this.show = false
}
})
}
}
</script>
<style lang="scss" scoped>
.bottom-nav-pane {
height: 100%;
position: relative;
}
</style>

/**
* 底部图标导航组件
*/
import BaseBottomNav from './BottomNav.vue'
import BaseBottomNavPane from './BottomNavPane.vue'
export const BottomNav = BaseBottomNav
export const BottomNavPane = BaseBottomNavPane


「欢迎在评论区讨论」



收起阅读 »

如何优雅的集成Google pay到你的项目中

官方集成文档 官方集成文档 官方集成文档第一步:javadependencies { def billing_version = "3.0.0" implementation 'com.android.billingcli...
继续阅读 »

官方集成文档 官方集成文档 官方集成文档

第一步:

java

dependencies {
def billing_version = "3.0.0"

implementation 'com.android.billingclient:billing:$billing_version'
}

kotlin

dependencies {
def billing_version = "3.0.0"

implementation 'com.android.billingclient:billing-ktx:$billing_version'
}

第二部:

private PurchasesUpdatedListener purchaseUpdateListener = new PurchasesUpdatedListener() {
@Override
void onPurchasesUpdated(BillingResult billingResult, List<Purchase> purchases) {
// To be implemented in a later section.
}
};

private BillingClient billingClient = BillingClient.newBuilder(activity)
.setListener(purchasesUpdatedListener)
.enablePendingPurchases()
.build();

第三部:

billingClient.startConnection(new BillingClientStateListener() {
@Override
public void onBillingSetupFinished(BillingResult billingResult) {
if (billingResult.getResponseCode() == BillingResponseCode.OK) {
// The BillingClient is ready. You can query purchases here.
//链接成功
}
}
@Override
public void onBillingServiceDisconnected() {
// Try to restart the connection on the next request to
// Google Play by calling the startConnection() method.
// 链接失败触发,触发重连机制
}
});

第四部: 请求自己服务器,拿到对应的商品列表,这里拿到的商品列表要和Google后台配置的商品列表ID一致。

List<String> skuList = new ArrayList<> ();
skuList.add("premium_upgrade");
SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
params.setSkusList(skuList).setType(SkuType.INAPP);
billingClient.querySkuDetailsAsync(params.build(),
new SkuDetailsResponseListener() {
@Override
public void onSkuDetailsResponse(BillingResult billingResult,
List<SkuDetails> skuDetailsList) {
// Process the result.
}
});

第五步: 调起Google 支付界面

// An activity reference from which the billing flow will be launched.
Activity activity = ...;

// Retrieve a value for "skuDetails" by calling querySkuDetailsAsync().
BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()
.setSkuDetails(skuDetails)
.build();
int responseCode = billingClient.launchBillingFlow(activity, billingFlowParams).getResponseCode();

// Handle the result.

在这里插入图片描述在这里插入图片描述

此处说明你的支付已经完成,回调到你最开始初始化的onPurchasesUpdated方法里边。

if (billingResult.getResponseCode() == BillingResponseCode.OK
&& purchases != null) {
for (Purchase purchase : purchases) {
handlePurchase(purchase);
}
} else if (billingResult.getResponseCode() == BillingResponseCode.USER_CANCELED) {
// Handle an error caused by a user cancelling the purchase flow.
} else {
// Handle any other error codes.
}

第六步: Google 支付分为两个部分,购买和验证,比如我们的产品是虚拟货币,是一次性消耗产品,使用下边方法去验证消耗。 最好的处理方法是请求后台服务器,让后台做一个验证,然后我们再去验证消费(保证安全性)

void handlePurchase(Purchase purchase) {
// Purchase retrieved from BillingClient#queryPurchases or your PurchasesUpdatedListener.
Purchase purchase = ...;

// Verify the purchase.
// Ensure entitlement was not already granted for this purchaseToken.
// Grant entitlement to the user.

ConsumeParams consumeParams =
ConsumeParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.build();

ConsumeResponseListener listener = new ConsumeResponseListener() {
@Override
public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
if (billingResult.getResponseCode() == BillingResponseCode.OK) {
// Handle the success of the consume operation.
}
}
};

billingClient.consumeAsync(consumeParams, listener);
}

因为网络原因可能会出现,掉单的问题,所以就会出现补单的逻辑。

调起支付之前

BillingResult billingResult = billingClient.launchBillingFlow(this, billingFlowParams);
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED) {
queryHistory();
}
/**
* 查询历史记录,有没校验的开始支付验证流程
*/
public void queryHistory() {
if (billingClient == null) {
return;
}
//google消费失败的补单
List<Purchase> purchases = billingClient.queryPurchases(BillingClient.SkuType.INAPP).getPurchasesList();
if (purchases != null && !purchases.isEmpty()) {
for (Purchase purchase : purchases) {
handlePurchase(purchase);
return;
}
}
}

收起阅读 »

android 如何优雅的集成 Razorpay

请在您的应用build.gradle文件中添加以下依赖项:repositories { mavenCentral() } dependencies { implementation 'com.razorpay:checkout...
继续阅读 »
  1. 请在您的应用build.gradle文件中添加以下依赖项:
repositories {   
mavenCentral()
}
dependencies {
implementation 'com.razorpay:checkout:1.5.16'
}

在这里插入图片描述

  1. Checkout并将付款详细信息和选项作为传递JSONObject。确保您添加了order_id在步骤1中生成的(一般是后台生成)
 public void startPayment() {
/*
You need to pass current activity in order to let Razorpay create CheckoutActivity
*/
final Activity activity = this;
final Checkout co = new Checkout();

try {
JSONObject options = new JSONObject();
options.put("name", "Razorpay Corp");
options.put("description", "Demoing Charges");
//You can omit the image option to fetch the image from dashboard
options.put("image", "https://s3.amazonaws.com/rzp-mobile/images/rzp.png");
options.put("order_id", "order_DBJOWzybf0sJbb");//这一部很重要,是后台调用Razorpay的接口生成的,否则支付成功的状态不对

options.put("currency", "INR");
options.put("amount", "100");
options.put("payment_capture", "1");

JSONObject preFill = new JSONObject();
preFill.put("email", "test@razorpay.com");
preFill.put("contact", "9876543210");

options.put("prefill", preFill);

co.open(activity, options);
} catch (Exception e) {
Toast.makeText(activity, "Error in payment: " + e.getMessage(), Toast.LENGTH_SHORT)
.show();
e.printStackTrace();
}
}
  1. 回调处理支付状态,处理成功和错误事件
  public class PaymentActivity extends Activity implements PaymentResultListener{
@Override
public void onPaymentSuccess(String s, PaymentData paymentData) {
}

@Override
public void onPaymentError(int i, String s, PaymentData paymentData) {

}
}
  1. 混淆
-keepclassmembers class * {
@android.webkit.JavascriptInterface <methods>;
}

-keepattributes JavascriptInterface
-keepattributes *Annotation*

-dontwarn com.razorpay.**
-keep class com.razorpay.** {*;}

-optimizations !method/inlining/*

-keepclasseswithmembers class * {
public void onPayment*(...);
}

收起阅读 »

Android-关于设备唯一ID的奇技淫巧

前言最近在二开项目国际版客户的功能,我们项目中默认是有一个游客登录的,一般大家都是取Android设备的唯一ID上传服务器,然后服务器给你分配一个用户信息.但是Google在高版本对于设备唯一Id的获取简直限制到了极点.以前我都是直接获取IMEI来作为设备的唯...
继续阅读 »

前言

最近在二开项目国际版客户的功能,我们项目中默认是有一个游客登录的,一般大家都是取Android设备的唯一ID上传服务器,然后服务器给你分配一个用户信息.但是Google在高版本对于设备唯一Id的获取简直限制到了极点.

以前我都是直接获取IMEI来作为设备的唯一标识

var imei: String = ""
val tm: TelephonyManager =
context.getSystemService(Service.TELEPHONY_SERVICE) as TelephonyManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
imei = tm.imei
} else {
imei = tm.deviceId
}
Log.e("TAG","$imei")

imei和deviceId都有一个重载函数,主要是区别双卡的一个情况

image.png

Android6.0以后我们加一个动态权限即可,但是用户只要拒绝就没办法获取了,不过一般来说我们会有个弹框来引导用户同意

<uses-permission android:name="android.permission.READ_PHONE_STATE"/>

Android 10.0 谷歌再一次收紧权限

image.png

<uses-permission android:name="android.permission.READ_PRIVILEGED_PHONE_STATE" />
如果你把他放到AndroidManifest会报错

image.png

官方也说了,你要是弟弟(9.0 以下)我给你报null,你要是10.0 还敢用我就直接抛异常. 后面在stackoverflow上面找到了一个办法

public class DeviceUuidFactory {
protected static final String PREFS_FILE = "device_id.xml";
protected static final String PREFS_DEVICE_ID = "device_id";
protected static UUID uuid;

public DeviceUuidFactory(Context context) {
if( uuid ==null ) {
synchronized (DeviceUuidFactory.class) {
if( uuid == null) {
final SharedPreferences prefs = context.getSharedPreferences( PREFS_FILE, 0);
final String id = prefs.getString(PREFS_DEVICE_ID, null );
if (id != null) {
// Use the ids previously computed and stored in the prefs file
uuid = UUID.fromString(id);
} else {
final String androidId = Secure.getString(context.getContentResolver(), Secure.ANDROID_ID);
// Use the Android ID unless it's broken, in which case fallback on deviceId,
// unless it's not available, then fallback on a random number which we store
// to a prefs file
try {
if () {
uuid = UUID.nameUUIDFromBytes(androidId.getBytes("utf8"));
} else {
@SuppressLint("MissingPermission") final String deviceId = ((TelephonyManager) context.getSystemService( Context.TELEPHONY_SERVICE )).getDeviceId();
uuid = deviceId!=null ? UUID.nameUUIDFromBytes(deviceId.getBytes("utf8")) : UUID.randomUUID();
}
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
// Write the value out to the prefs file
prefs.edit().putString(PREFS_DEVICE_ID, uuid.toString() ).commit();
}
}
}
}
}
/**
* Returns a unique UUID for the current android device. As with all UUIDs, this unique ID is "very highly likely"
* to be unique across all Android devices. Much more so than ANDROID_ID is.
*
* The UUID is generated by using ANDROID_ID as the base key if appropriate, falling back on
* TelephonyManager.getDeviceID() if ANDROID_ID is known to be incorrect, and finally falling back
* on a random UUID that's persisted to SharedPreferences if getDeviceID() does not return a
* usable value.
*
* In some rare circumstances, this ID may change. In particular, if the device is factory reset a new device ID
* may be generated. In addition, if a user upgrades their phone from certain buggy implementations of Android 2.2
* to a newer, non-buggy version of Android, the device ID may change. Or, if a user uninstalls your app on
* a device that has neither a proper Android ID nor a Device ID, this ID may change on reinstallation.
*
* Note that if the code falls back on using TelephonyManager.getDeviceId(), the resulting ID will NOT
* change after a factory reset. Something to be aware of.
*
* Works around a bug in Android 2.2 for many devices when using ANDROID_ID directly.
*

*
* @return a UUID that may be used to uniquely identify your device for most purposes.
*/
public String getDeviceUuid() {
return uuid.toString();
}
}

这个类的意思是,首先他会去SharedPreferences查询有没有,没有的话再去查询ANDROID_ID,后面判断了是否是9774d56d682e549c,因为有的厂商手机好多ANDROID_ID都是这个,所以判断一下,防止好几万个人用一个账号,不然那就笑嘻嘻了,后面如果真等于9774d56d682e549c了,就通过下面的

@SuppressLint("MissingPermission") final String deviceId = ((TelephonyManager) context.getSystemService( Context.TELEPHONY_SERVICE )).getDeviceId();

来获取DeviceId,但是这个AndroidId虽然可以是获取了,但是会受限于签名文件,如果在相同设备上运行但是应用签名不一样,获取到的ANDROID_ID就会不一样,比如谷歌商店会二次签名apk,他获取的id可能就是159951,后面我们要测试时,上传到内部测试的包好像会再次签名,这次获取的可能是951159,然后我们用android提供的签名文件可能就是147258,我们自己新建一个签名文件就可能是258369,总之这个ANDROID_ID会受制于签名文件

反正最后我们国际版用到了Mob的推送服务,推送中有一个只推送单个设备,然后我们就设想,直接用Mob的唯一设备Id和我们服务器绑定如何,后面一经测试,效果很好,直接跳过大堆测试和寻找时间

//阿里云唯一设备id
val deviceId = PushServiceFactory.getCloudPushService().deviceId

//Mob
CloudPushService pushService = PushServiceFactory.getCloudPushService();
pushService.register(applicationContext, new CommonCallback() {
@Override
public void onSuccess(String response) {
Log.e("TAG", "onSuccess: "+response);
}

@Override
public void onFailed(String errorCode, String errorMessage) {
}
});

//友盟唯一设备ID
val pushAgent = PushAgent.getInstance(context)
pushAgent.register(object : UPushRegisterCallback {
override fun onSuccess(deviceToken: String) {
//注册成功会返回deviceToken deviceToken是推送消息的唯一标志
Log.i(TAG, "注册成功:deviceToken:--> $deviceToken")
}

override fun onFailure(errCode: String, errDesc: String) {
Log.e(TAG, "注册失败:--> code:$errCode, desc:$errDesc")
}
})

这是常用的第三方服务获取唯一设备ID的方法,其实有的人可能用的跟我不一样,基本上文档里面都有,真找不到可以去问问客服

终于解决一个让人头疼的问题了,下班,回家

收起阅读 »

一条SQL查询语句是如何执行的

sql
背景我们执行一条查询语句时,对客户端是一个很简单的过程,但对服务端(MySQL)内部却涉及到很复杂的组件和逻辑,当出现一些比较复杂的SQL问题时,如果不理解其内部执行的原理,将会很难去定位和解决问题正文先聊聊MySQL的逻辑架构大体来说,MySQL可以分为 S...
继续阅读 »

背景

我们执行一条查询语句时,对客户端是一个很简单的过程,但对服务端(MySQL)内部却涉及到很复杂的组件和逻辑,当出现一些比较复杂的SQL问题时,如果不理解其内部执行的原理,将会很难去定位和解决问题

正文

先聊聊MySQL的逻辑架构

image.png

大体来说,MySQL可以分为 Server层和存储引擎层两部分

Server层

  • 包括连接器、查询缓存、分析器、优化器、执行器
  • 实现了MySQL 的大多数核心服务功能,所有的包括查询解析、分析、优化、缓存以及所有的内置函数,所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图

存储引擎层

  • 负责数据的存储和提取。其架构模式是插件式的,支持 InnoDB、MyISAM、Memory 等多个存储引擎。现在最常用的存储引擎是 InnoDB,它从 MySQL 5.5.5 版本开始成为了默认存储引擎。也就是说,你执行 create table 建表的时候,如果不指定引擎类型,默认使用的就是 InnoDB。不过,你也可以通过指定存储引擎的类型来选择别的引擎,比如在 create table 语句中使用 engine=memory, 来指定使用内存引擎创建表。
  • 不同存储引擎的表数据存取方式不同,支持的功能也不同,不同的存储引擎共用一个 Server 层
  • Server 层通过存储引擎API来与它们交互,这些接口屏蔽了不同存储引擎之间的差异,使得这些差异对上层的查询尽可能的透明。这些API包含几十个底层函数,用于执行诸如"开始一个事务"或者"根据主键提取一行记录"等操作,存储引擎不能解析SQL,互相之间也不能通信。只是简单地响应上层服务器的请求

SQL查询语句的执行流程

step1:使用连接器与客户端建立连接

首先客户端会先连接到指定数据库上,这时候接待的就是连接器。连接器负责跟客户端建立连接、获取权限、维持和管理连接

我们在Linux上会通过以下方式与MySQL建立连接,连接命令中的"mysql"是客户端工具

mysql -h$ip -P$port -u$user -p

在完成经典的TCP 握手后,连接器就要开始认证你的身份,这个时候用的就是你输入的用户名和密码:

  • 如果用户名或密码不对,你就会收到一个"Access denied for user"的错误,然后客户端程序结束执行
  • 如果用户名密码认证通过,连接器会到权限表里面查出你拥有的权限。之后这个连接里面的权限判断逻辑,都将依赖于此时读到的权限。这就意味着一个用户成功建立连接后,即使你用管理员账号对这个用户的权限做了修改,也不会影响已经存在连接的权限。修改完成后,只有再新建的连接才会使用新的权限设置

连接完成后,如果你没有后续的动作,这个连接就处于空闲状态,可以输入show processlist 命令看到全部的连接,Command 列显示为“Sleep”的这一行,就表示现在系统里面有一个空闲连接

企业微信截图_16348234821060.png

客户端如果太长时间没动静,连接器就会自动将它断开。这个时间是由参数 wait_timeout 控制的,默认值是 8 小时。如果在连接被断开之后,客户端再次发送请求的话,就会收到一个错误提醒: Lost connection to MySQL server during query,这时候如果你要继续,就需要重连,然后再执行请求了

数据库的连接分为有长连接和短连接:

  • 长连接:指连接成功后,如果客户端持续有请求,则一直使用同一个连接
  • 短连接:指每次执行完很少的几次查询就断开连接,下次查询再重新建立一个

建立连接的过程通常是比较复杂的,因此建议在使用中要尽量减少建立连接的动作,也就是尽量使用长连接

但是全部使用长连接后,你可能会发现,有些时候 MySQL 占用内存涨得特别快,这是因为 MySQL 在执行过程中临时使用的内存是管理在连接对象里面的。这些资源会在连接断开的时候才释放。所以如果长连接累积下来,可能导致内存占用太大,被系统强行杀掉(OOM),从现象看就是 MySQL 异常重启了

怎么解决这个问题呢?你可以考虑以下两种方案:

  • 定期断开长连接。使用一段时间,或者程序里面判断执行过一个占用内存的大查询后,断开连接,之后要查询再重连
  • 如果你用的是 MySQL 5.7 或更新版本,可以在每次执行一个比较大的操作后,通过执行 mysql_reset_connection 来重新初始化连接资源。这个过程不需要重连和重新做权限验证,但是会将连接恢复到刚刚创建完时的状态

step2:查询缓存,有就直接返回查询结果

连接建立完成后,你就可以执行 select 语句了。执行逻辑就会来到第二步:查询缓存

MySQL 拿到一个查询请求后,会先到查询缓存看看,之前是不是执行过这条语句。之前执行过的语句及其结果可能会以 key-value 对的形式,被直接缓存在内存中。key 是查询的语句,value 是查询的结果。如果你的查询能够直接在这个缓存中找到 key,那么这个 value 就会被直接返回给客户端

如果语句不在查询缓存中,就会继续后面的执行阶段。执行完成后,执行结果会被存入查询缓存中。你可以看到,如果查询命中缓存,MySQL 不需要执行后面的复杂操作,就可以直接返回结果,这个效率会很高

但是大多数情况下我会建议你不要使用查询缓存,为什么呢?因为查询缓存往往弊大于利。查询缓存的失效非常频繁,只要有对一个表的更新,这个表上所有的查询缓存都会被清空。因此很可能你费劲地把结果存起来,还没使用呢,就被一个更新全清空了。对于更新压力大的数据库来说,查询缓存的命中率会非常低。除非你的业务就是有一张静态表,很长时间才会更新一次。比如,一个系统配置表,那这张表上的查询才适合使用查询缓存

好在 MySQL 也提供了这种“按需使用”的方式。你可以将参数 query_cache_type 设置成 DEMAND,这样对于默认的 SQL 语句都不使用查询缓存。而对于你确定要使用查询缓存的语句,可以用 SQL_CACHE 显式指定,像下面这个语句一样:

select SQL_CACHE * from T where ID=10;

注意:MySQL 8.0 版本直接将查询缓存的整块功能删掉了

step3:使用分析器解析你的SQL,知道你要做什么

分析器如果没有命中查询缓存,就要开始真正执行语句了。MySQL首先需要知道你要做什么,因此需要对 SQL 语句做解析

分析器先会做“词法分析”:你输入的是由多个字符串和空格组成的一条 SQL 语句,MySQL 需要识别出里面的字符串分别是什么,代表什么。MySQL 从你输入的"select"这个关键字识别出来,这是一个查询语句。它也要把字符串“T”识别成“表名 T”,把字符串“ID”识别成“列 ID”

做完了这些识别以后,就要做“语法分析”:根据词法分析的结果,语法分析器会根据语法规则,判断你输入的这个 SQL 语句是否满足 MySQL 语法。如果你的语句不对,就会收到“You have an error in your SQL syntax”的错误提醒,比如下面这个语句 select 少打了开头的字母“s”

mysql> elect * from t where ID=1;

ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'elect * from t where ID=1' at line 1,

一般语法错误会提示第一个出现错误的位置,所以你要关注的是紧接“use near”的内容

step4:使用优化器确定语句的执行方案

经过了分析器,MySQL 就知道你要做什么了。在开始执行之前,还要先经过优化器的处理

优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序

比如执行下面这样的语句,这个语句是执行两个表的 join:

select * from t1 join t2 using(ID) where t1.c=10 and t2.d=20;
  • 既可以先从表 t1 里面取出 c=10 的记录的 ID 值,再根据 ID 值关联到表 t2,再判断 t2 里面 d 的值是否等于 20。
  • 也可以先从表 t2 里面取出 d=20 的记录的 ID 值,再根据 ID 值关联到 t1,再判断 t1 里面 c 的值是否等于 10。

这两种执行方法的逻辑结果是一样的,但是执行的效率会有不同,而优化器的作用就是决定选择使用哪一个方案。优化器阶段完成后,这个语句的执行方案就确定下来了

step5:使用执行器执行语句

MySQL 通过分析器知道了你要做什么,通过优化器知道了该怎么做,于是就进入了执行器阶段,开始执行语句

开始执行的时候,要先判断你对这个表 T 有没有执行查询的权限

  • 如果没有,就会返回没有权限的错误,如下所示 (在工程实现上,如果命中查询缓存,会在查询缓存返回结果的时候,做权限验证。查询也会在优化器之前调用 precheck 验证权限)。
mysql> elect * from t where ID=1; 

ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'elect * from t where ID=1' at line 1
  • 如果有权限,就打开表继续执行。打开表的时候,执行器就会根据表的引擎定义,去使用这个引擎提供的接口

比如我们这个例子中的表 T 中,ID 字段没有索引,那么执行器的执行流程是这样的:

  1. 调用 InnoDB 引擎接口取这个表的第一行,判断 ID 值是不是 10,如果不是则跳过,如果是则将这行存在结果集中;
  2. 调用引擎接口取“下一行”,重复相同的判断逻辑,直到取到这个表的最后一行。
  3. 执行器将上述遍历过程中所有满足条件的行组成的记录集作为结果集返回给客户端。

对于有索引的表,执行的逻辑也差不多:

  1. 第一次调用的是“取满足条件的第一行”这个接口,
  2. 之后循环取“满足条件的下一行”这个接口,这些接口都是引擎中已经定义好的。

你会在数据库的慢查询日志中看到一个 rows_examined 的字段,表示这个语句执行过程中扫描了多少行。这个值就是在执行器每次调用引擎获取数据行的时候累加的。但在有些场景下,执行器调用一次,在引擎内部则扫描了多行,因此引擎扫描行数跟 rows_examined 并不是完全相同的

总结

  • MySQL的逻辑架构包含两部分,server层存储引擎层

  • SQL查询语句的执行依赖于这些核心组件:先通过连接器与客户端进行连接,随后查询是否可以应用缓存,可以直接返回结果,不可以则使用解析器分析SQL,然后利用优化器确定执行方案,最终利用执行器存储引擎执行SQL获取结果


收起阅读 »

熬夜再战Android之修炼Kotlin-【findView】篇

前提 前面我们学了Kotlin语言,趁热打铁我们试试Kotlin在Android中的应用。 如果是新手,请先学完Android基础。 推荐先看小空之前写的熬夜Android系列,再来尝试。 👉实践过程 😜方式一 使用扩展,如果你第一次创建项目的时候选择的是Ko...
继续阅读 »

前提


前面我们学了Kotlin语言,趁热打铁我们试试Kotlin在Android中的应用。


如果是新手,请先学完Android基础。


推荐先看小空之前写的熬夜Android系列,再来尝试。


👉实践过程


😜方式一


使用扩展,如果你第一次创建项目的时候选择的是Kotlin语言,则默认带有该插件,如果选择的默认是Java语言,则需要手动添加。


该方式的优点就是对编程人员来说可以直接拿到View的id,不需要定义变量和findViewById。


在项目的build文件开头添加【app->build.gradle】


apply plugin: 'kotlin-android-extensions'


image.png
之后在Activity中添加import,固定格式的。


import kotlinx.android.synthetic.main.修改为你的布局名称.*


【*】代表的是该布局下的所有控件,如果只需要指定控件,将【*】改为控件名即可,如下示例


import kotlinx.android.synthetic.main.activity_main.*


import kotlinx.android.synthetic.main.activity_main.mytextview


之后就可以直接在代码中使用控件的id来进行响应操作了。


<TextView
        android:id="@+id/myText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="8dp"
        android:text="芝麻粒儿和空名先生"
        android:textStyle="bold"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

但是方便的同时,问题的隐患也存在着,在底层仍然回归原始使用的是findViewById,所以会对性能有影响


😜方式二


使用findViewById,这个仍然有两种方式,方式一是【lateinit】关键字,但是存在坑,详情看这(在 Kotlin 代码中慎用 lateinit 属性zhuanlan.zhihu.com/p/31297995
推荐方式二使用【lazy】,如下:



private val myText: TextView by lazy { findViewById<TextView>(R.id.myText) }
private val myImg: ImageView by lazy { findViewById<ImageView>(R.id.myImg) }
private val imageView: ImageView by lazy { findViewById<ImageView>(R.id.imageView) }
private val myBtn: Button by lazy { findViewById<Button>(R.id.myBtn) }

上面是在Activity中的使用,而在Fragment中又怎么用呢?


class LoginFragment : Fragment() {
  private var myText: TextView? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        //设置布局
        return inflater.inflate(R.layout.login_fragment, container, false)
    }
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
   myText = view.findViewById(R.id.myText)
        myText?.setText("动态修改文本")
    }
}

除了上面的写法,Android官方给了我们更好的解决方案:


class LoginFragment : Fragment() {
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        //设置布局
        return inflater.inflate(R.layout.login_fragment, container, false)
    }

    private lateinit var myText: TextView
    private lateinit var myImg: ImageView
    private lateinit var imageView: ImageView
    private lateinit var myBtn: Button

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        //查找view
        myText = view.findViewById(R.id.myText)
        myImg = view.findViewById(R.id.myImg)
        imageView = view.findViewById(R.id.imageView)
        myBtn = view.findViewById(R.id.myBtn)
    }
}

还有另外方式就是使用自动化插件,在【File-Setting-Plugins】市场搜索关键字【findview】,看最新的几个,挑选自己用的顺手的使用即可。


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

翻车了,字节一道 Fragment面试题

一道面试题 前段时间面试,面试官先问了一下fragment的生命周期,我一看这简单呀,直接按照下图回答 面试官点点头,然后问,如果Activity里面有一个fragment,那么启动他们时,他们的生命周期加载顺序是什么? 所以今天,我们好好了解了解这个用得...
继续阅读 »

一道面试题


前段时间面试,面试官先问了一下fragment的生命周期,我一看这简单呀,直接按照下图回答


img


面试官点点头,然后问,如果Activity里面有一个fragment,那么启动他们时,他们的生命周期加载顺序是什么?


在这里插入图片描述


所以今天,我们好好了解了解这个用得非常多,但是对底层不是很理解的fragment吧


首先回答面试官的问题,Fragment 的 start与activity 的start 的调用时机



调用顺序:


D/MainActivity: MainActivity:


D/MainActivity: onCreate: start


D/MainFragment: onAttach:


D/MainFragment: onCreate:




D/MainActivity: onCreate: end


D/MainFragment: onCreateView:


D/MainFragment: onViewCreated:


D/MainFragment: onActivityCreated:


D/MainFragment: onViewStateRestored:


D/MainFragment: onCreateAnimation:


D/MainFragment: onCreateAnimator:


D/MainFragment: onStart:




D/MainActivity: onStart:


D/MainActivity: onResume:


D/MainFragment: onResume:



可以看到Activity 在oncreate开始时,Fragment紧接着attach,create,然后activity执行完毕onCreate方法


此后都是Fragment在执行,直到onStart方法结束


然后轮到Activity,执行onStart onResume


也就是,Activity 创建的时候,Fragment一同创建,同时Fragment优先在后台先展示好,最后Activity带着Fragment一起展示到前台。


是什么?


Fragment中文翻译为”碎片“,在手机中,每一个Activity作为一个页面,有时候太大了,尤其是在平板的横屏下,我们希望左半边是一根独立模块,右半边是一个独立模块,比如一个新闻app,左边是标题栏,右边是显示内容


此时就非常适合Fragment


Fragment是内嵌入Activity中的,可以在onCreateView中加载自定义的布局,使用LayoutInflater,然后Activity持有FragmentManager对Fragment进行控制,下图是他的代码框架


img


我们的Activity一般是用AppCompatActivity,而AppCompatActivity继承了FragmentActivity


public class AppCompatActivity extends FragmentActivity implements AppCompatCallback,
TaskStackBuilder.SupportParentable, ActionBarDrawerToggle.DelegateProvider {

也就是说Activity之所支持fragment,是因为有FragmentActivity,他内部有一个FragmentController,这个controller持有一个FragmentManager,真正做事的就是这个FragmentManager的实现类FragmentManagerImpl


整体架构


回到我们刚才的面试题,关于生命周期绝对是重中之重,但是实际上,生命周期本质只是被其他地方的方法被动调用而已,关键是Fragment自己的状态变化了,才会回调生命周期方法,所以我们来看看fragment的状态转移


static final int INITIALIZING = 0;     初始状态,Fragment 未创建
static final int CREATED = 1; 已创建状态,Fragment 视图未创建
static final int ACTIVITY_CREATED = 2; 已视图创建状态,Fragment 不可见
static final int STARTED = 3; 可见状态,Fragment 不处于前台
static final int RESUMED = 4; 前台状态,可接受用户交互

fragment有五个状态,


调用过程如下


img


Fragment的状态转移过程主要受到宿主,事务的影响,宿主一般就是Activity,在我们刚刚的题目中,看到了Activity与Fragment的生命周期交替执行,本质上就是,Activity执行完后通知了Fragment进行状态转移,而Fragment执行了状态转移后对应的回调了生命周期方法


下图可以更加清晰


img


宿主改变Fragment状态


那么我们不禁要问,Activity如何改变Fragment的状态?


我们知道Activity继承于FragmentActivity,最终是通过持有的FragmentManager来控制Fragment,我们去看看


FragmentActivity
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
mFragments.attachHost(null /*parent*/);

super.onCreate(savedInstanceState);
...
mFragments.dispatchCreate();
}

可以看到,onCreate方法中执行了mFragments.dispatchCreate();,看起来像是通知Fragment的onCreate执行,这也印证了我们开始时的周期回调顺序


D/MainActivity: MainActivity: 
D/MainActivity: onCreate: start // 进入onCreate
D/MainFragment: onAttach: // 执行mFragments.dispatchCreate();
D/MainFragment: onCreate:
D/MainActivity: onCreate: end // 退出onCreate

类似的FragmentActivity在每一个生命周期方法中都做了相同的事情


@Override
protected void onDestroy() {
super.onDestroy();

if (mViewModelStore != null && !isChangingConfigurations()) {
mViewModelStore.clear();
}

mFragments.dispatchDestroy();
}

我们进入dispatchCreate看看,


Runnable mExecCommit = new Runnable() {
@Override
public void run() {
execPendingActions();
}

//内部修改了两个状态
public void dispatchCreate() {
mStateSaved = false;
mStopped = false;
dispatchStateChange(Fragment.CREATED);

private void dispatchStateChange(int nextState) {
try {
mExecutingActions = true;
moveToState(nextState, false);// 转移到nextState
} finally {
mExecutingActions = false;
}
execPendingActions();
}
//一路下来会执行到
void moveToState(Fragment f, int newState, int transit, int transitionStyle,
boolean keepActive) {
// Fragments that are not currently added will sit in the onCreate() state.
if ((!f.mAdded || f.mDetached) && newState > Fragment.CREATED) {
newState = Fragment.CREATED;
}
if (f.mRemoving && newState > f.mState) {
if (f.mState == Fragment.INITIALIZING && f.isInBackStack()) {
// Allow the fragment to be created so that it can be saved later.
newState = Fragment.CREATED;
} else {
// While removing a fragment, we can't change it to a higher state.
newState = f.mState;
}
}
...
}

可以看到上面的代码,最终执行到 moveToState,通过判断Fragment当前的状态,同时newState > f.mState,避免状态回退,然后进行状态转移


状态转移完成后就会触发对应的生命周期回调方法


事务管理


如果Fragment只能随着Activity的生命周期变化而变化,那就太不灵活了,所以Android给我们提供了一个独立的操作方案,事务


同样由FragManager管理,具体由FragmentTransaction执行,主要是添加删除替换Fragment等,执行操作后,需要提交来保证生效


FragmentManager fragmentManager = ...
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.setReorderingAllowed(true);

transaction.replace(R.id.fragment_container, ExampleFragment.class, null); // 替换Fragment

transaction.commit();// 这里的commit是提交的一种方法

Android给我们的几种提交方式


image-20211020143006614


FragmentTransaction是个挂名抽象类,真正的实现在BackStackState回退栈中,我们看下commit


@Override
public int commit() {
return commitInternal(false);
}
int commitInternal(boolean allowStateLoss) {
if (mCommitted) throw new IllegalStateException("commit already called");
...
mCommitted = true;
if (mAddToBackStack) {
mIndex = mManager.allocBackStackIndex(this);//1
} else {
mIndex = -1;
}
// 入队操作
mManager.enqueueAction(this, allowStateLoss);//2
return mIndex;
}

可以看到,commit的本质就是将事务提交到队列中,这里出现了两个数组,注释1处


ArrayList<BackStackRecord> mBackStackIndices;
ArrayList<Integer> mAvailBackStackIndices;
public int allocBackStackIndex(BackStackRecord bse) {
synchronized (this) {
if (mAvailBackStackIndices == null || mAvailBackStackIndices.size() <= 0) {
if (mBackStackIndices == null) {
mBackStackIndices = new ArrayList<BackStackRecord>();
}
int index = mBackStackIndices.size();
mBackStackIndices.add(bse);
return ind
} else {
int index = mAvailBackStackIndices.remove(mAvailBackStackIndices.size()-1);
mBackStackIndices.set(index, bse);
return index;
}
}
}

mBackStackIndices数组,每个元素是一个回退栈,用来记录索引。比如说,当有五个BackStackState时,移除掉1,3两个,就是在mBackStackIndices将对应元素置为null,然后mAvailBackStackIndices会添加这两个回退栈,记录被移除的回退栈


当下次commit时,就判定mAvailBackStackIndices中的索引,对应的BackStackState一定是null,直接写到这个索引即可


而一组操作都commit到同一个队列里面,所以要么全部完成,要么全部不做,可以保证原子性


注释二处是一个入队操作


public void enqueueAction(OpGenerator action, boolean allowStateLoss
synchronized (this) {
...
mPendingActions.add(action);
scheduleCommit(); // 真正的提交
}
}
public void scheduleCommit() {
synchronized (this) {
boolean postponeReady =
mPostponedTransactions != null && !mPostponedTransactions.isEmpty();
boolean pendingReady = mPendingActions != null && mPendingActions.size() == 1;
if (postponeReady || pendingReady) {
mHost.getHandler().removeCallbacks(mExecCommit);
mHost.getHandler().post(mExecCommit); // 发送请求
}
}

这里最后 mHost.getHandler()是拿到了宿主Activity的handler,使得可以在主线程执行,mExecCommit本身是一个线程


我们继续看下这个mExecCommit


Runnable mExecCommit = new Runnable() {
@Override
public void run() {
execPendingActions();
}
};
public boolean execPendingActions() {
ensureExecReady(true);
...
doPendingDeferredStart();
burpActive();
return didSomething;
}
void doPendingDeferredStart() {
if (mHavePendingDeferredStart) {
mHavePendingDeferredStart = false;
startPendingDeferredFragments();
}
}
void startPendingDeferredFragments() {
if (mActive == null) return;
for (int i=0; i<mActive.size(); i++) {
Fragment f = mActive.valueAt(i);
if (f != null) {
performPendingDeferredStart(f);
}
}
}
public void performPendingDeferredStart(Fragment f) {
if (f.mDeferStart) {
f.mDeferStart = false;
moveToState(f, mCurState, 0, 0, false); // 最终到了MoveToState
}
}

还记得我们在宿主改变Fragment状态,里面的最终路径吗?是的,就是这个moveToState,无论是宿主改变Fragment状态,还是事务来改变,最终都会执行到moveToState,然后call对应的生命周期方法来执行,这也是为什么我们要将状态转移作为学习主线,而不是生命周期。


除了commit,可以看到FragmentTransaction有众多对Fragment进行增删改查的方法


image-20211020143442569


都是由BackStackState来执行,最后都会执行到moveToState中


具体是如何改变的,有很多细节,这里不再赘述。


小结


本节我们讲了Fragment在android系统中的状态,那就是通过自身状态转移来回调对应生命周期方法,这块是自动实现的,我们开发时不太需要关注状态转移,只要知道什么时候执行某个生命周期方法,然后再在对应方法中写业务逻辑即可


有两个方法可以让Fragment状态转移,



  • 宿主Activity生命周期内自动修改Fragment状态,回调Fragment的生命周期方法

  • 通过手动提交事务,修改Fragment状态,回调Fragment的生命周期方法

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

Android模块化设计之组件开发规范

最近一直在做基础建设方面的工作,面对这三十多个完全没有规范可言的组件,气的我直接打了一套闪电五连鞭,但打工还得继续,于是想对这些组件建立一套规范,来降低够用、使用、维护以及扯皮成本,本想在网上白嫖一套,可找到的都是一些基础的代码规范,用处不大,于是乎根据自己的...
继续阅读 »

最近一直在做基础建设方面的工作,面对这三十多个完全没有规范可言的组件,气的我直接打了一套闪电五连鞭,但打工还得继续,于是想对这些组件建立一套规范,来降低够用、使用、维护以及扯皮成本,本想在网上白嫖一套,可找到的都是一些基础的代码规范,用处不大,于是乎根据自己的工作经验,总结出一套规范/约定出来,希望能抛砖引玉,各位大佬多多指点和补充~

规范本身就是开发人员之间的约定,没有最权威的,只有最适合的。

版本规范

为保证组件在各个项目中的兼容性问题,约定组件开发版本如下:

AndroidSdk版本

minSdkVersion:21

targetSdkVersion:28

语言环境

开发语言:Kotlin

Kotlin版本:1.4.x

JDK版本:1.8

其他建议版本

AndroidStuido:4.2

gradle tools:4.1.x

gradle:6.7.x

组件命名规范

根据组件的功能不同,约定组件分为三个类型:

基础组件

为项目提供与业务无关基础支持的组件库,如提供MVVM架构的lib_basic,提供依赖注入的lib_basic_koin,这类组件统一命名方式为lib_basic_xxx。这些基础组件也可以被工具组件和业务组件依赖,而不仅仅是只被项目依赖。

工具组件

对项目中常用的业务无关功能封装的组件库,如Dialog弹窗,相册选择等,这类组件统一命名方式为lib_util_xxx

业务组件

在某个项目的需求中出现的在其他项目中可能也会用到的功能的封装,比如A业务员版中扫描拍照在A商业版中也有用到,于是单独封装为一个业务组件进行管理,这类组件统一命名方式为lib_tool_xxx

GroupId

所有组件统一GroupId为com.company.android,方便在Maven仓库中进行索引和管理。

版本号

对于迭代的版本号,不做强制性的要求,只需合理进行升级即可,但以下情况需特殊注意:

1、对于上传的开发版本,需要在版本中进行体现,如1.1.0-dev02,与正式版本做区分,在项目中验证无风险后,合并到主迭代版本中。

2、对于紧急修复A项目中的问题而临时发版的,需要在版本中进行体现,如1.1.0-hotfix-A,此次修改在其他使用的项目中验证无风险后,合并到主迭代版本中。

开发原则

开闭原则

在组件升级时,应对新增开放,对修改关闭,即做加法不做减法,目的是为了保证对项目中调用老版本API的兼容问题。对于不再建议使用的API,应使用 @Deprecated注解进行标注,并新增建议使用的API进行代替,而不是直接删除旧API。在完成多个稳定版本的迭代之后,可以所有组件使用者讨论删除旧版API的事宜。

向下兼容原则

所有组件的版本迭代必须向下兼容,即1.2.0版本须兼容1.1.0,在使用者升级版本之后,无需修改业务层代码。

如果因组件前期设计不合理导致升级必须修改业务代码时,组件开发者需要与所有组件使用者商讨技术实现方案,看能否以最小的改动完成组件的升级,并在组件功能开发完毕后,以书面形式告知使用者并提供完整的升级文档

三方隔离原则

我们在封装组件的时候,难免会使用到第三方依赖,为了避免更换三方依赖或依赖升级造成的影响,需要对三方依赖库进行二次封装,避免直接使用。比如我们要使用glide框架加载图片,可以创建一个代理类对glide的功能进行代理,在业务代码中使用代理类进行操作而不是直接调用glide,这样我们将来替换glide框架时,能最小的改动业务代码。

当然,并不是所有的三方库都通过代理模式进行隔离,比如retrofit、RxJava等,毕竟我们的隔离原则是为了以后更简单的迭代而不是自寻烦恼。

最少依赖原则

为了减少项目中依赖的类库,在组件封装中遇到如下情况,应对组件进行拆分工作:

现封装一图片加载类库lib_util_imageloader,对GlidePicasso进行了二次封装,由于两个类库提供了类似的功能,在项目中只需要使用一套就能满足业务需求,没有必要把两个图片加载框架都进行依赖,所以应该对lib_util_imageloader进行拆分为如下结构:

lib_util_imageloader_core:图片加载核心库,把加载图片的方法抽象为接口,面向项目,不做具体的实现。

lib_util_imageloader_glide:对glide进行二次封装,实现core中的接口。

lib_util_imageloader_picasso:对picasso进行二次封装,实现core中的接口。

在项目使用图片加载库时,除了必须要依赖的core外,只需要从glide和picasso中挑选一个即可,这样就不会把用不到的类库也打包到项目中去了。

这样的做法还有一个好处,就是容易拓展。如果我想要使用Fresco,只需要再新增一个lib_util_imageloader_fresco,并实现core的接口即可,在切换组件时,只需要改变gradle文件中的依赖,无需变更业务代码,因为业务代码都是基于core组件的。

最少可见原则

组件应该尽可能少的对外暴露类、接口、方法等。可以通过外观模式对使用者统一提供API,降低使用者的理解难度。

支持开发模式

组件需要预留开发者模式,可以让使用者自行选择开启关闭。

打开开发者模式:组件运行的关键节点需进行日志输出,方便使用者进行调试,可以在运行时抛出异常。

关闭开发者模式:组件不再对外输出Error级别以下的日志,禁止在运行时抛出异常、ANR,如发生异常需要在组件内捕获并通过错误回调或打印Error级别日志等方式告知使用者。

可拓展性

部分组件(视具体情况而定,多数为对三方类库的封装组件)应该有一定的拓展性,应支持使用者自定义实现覆盖默认实现。

比如Dialog类库有默认弹窗样式,需要支持使用者自定义弹窗样式而不是只能在默认样式中进行选择,图片加载框架也是相同的逻辑。

开发规范

包名规范

为了避免无意中导致的包名冲突问题,约定组件的包名为4级,除去前两级的com.company为固定写法以外,后两级可根据具体的组件功能进行命名,并要求有良好的可读性。

资源规范

组件中定义资源文件时,要以组件名称为前缀,避免资源冲突导致的打包问题,需要在gradle文件的android节点下新增如下代码强制进行资源名称前缀检查:

android{
 resourcePrefix = "${your_component_name}_"
}
复制代码

代码规范

应当遵循Java开发代码规范,这里不再赘述。

可见域规范

Kotlin中新增关键字internal,可用于修饰类名、方法名和成员变量名,限制所修饰的对象模块内可见,对于无需对外暴露的类、方法和变量,应当降低其可见域。

内联函数

Kotlin中新增关键字inline修饰方法,可以减少方法栈的进栈方法数。在封装组件时善用inline以提高组件的运行效率。

内存泄漏

所有组件在发布之间,必须进行内存泄漏检测,禁止存在内存泄漏的组件上线,应在开发阶段修复所有泄漏问题。

混淆

组件开发者需要确认自己的组件在打包混淆后是否可以正常工作,如有用到运行时注解、反射、Json转换等功能,需要在接入文档中声明避免混淆的规则。

文档

组件的接入、使用、升级和注意事项等需要在开发文档中有明确的体现,没有接入文档或者文档不完善一律不许通过验收。

Demo

组件最好有配套的演示工程,用来最直观的体现出组件所提供的功能,也方便使用者进行参考。

最后

目前只能想起这么多,以后有新增的会继续补充。

规范好定制,落实起来却难,路漫漫其修远兮,加油吧,打工人!


作者:王远道
链接:https://juejin.cn/post/7021434072652054565
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

消失性进度条

效果&使用 图例分别为: 修改读条起点为y轴正方向 消失性读条 正常读条 使用: 1 在xml中添加控件 <com.lloydfinch.ProgressTrackBar android:id="@+id/progress_tr...
继续阅读 »

效果&使用


效果


图例分别为:



  • 修改读条起点为y轴正方向

  • 消失性读条

  • 正常读条


使用:



  • 1 在xml中添加控件


<com.lloydfinch.ProgressTrackBar
android:id="@+id/progress_track_bar"
android:layout_width="62dp"
android:layout_height="62dp"
app:p_second_color="#E91E63"
app:p_width="3dp" />

<com.lloydfinch.ProgressTrackBar
android:id="@+id/progress_track_bar2"
android:layout_width="62dp"
android:layout_height="62dp"
app:p_first_color="#18B612"
app:p_second_color="#00000000"
app:p_width="3dp" />

<com.lloydfinch.ProgressTrackBar
android:id="@+id/progress_track_bar3"
android:layout_width="62dp"
android:layout_height="62dp"
app:p_first_color="#ffd864"
app:p_second_color="#1C3F7C"
app:p_width="3dp" />


  • 2 在代码中启动倒计时


val trackBar = findViewById<ProgressTrackBar>(R.id.progress_track_bar)
trackBar.setStartAngle(-90F) // 从-90度开始读条
trackBar.setOnProgressListener { // 进度回调
Log.d("ProgressTrackBar", "progress is $it")
}
trackBar.startTask(0) { // 开始计时,传入读条结束的回调
Log.d("ProgressTrackBar", "progress run finish")
}

// 从0开始计时
findViewById<ProgressTrackBar>(R.id.progress_track_bar2).startTask(0)

// 从20开始计时
findViewById<ProgressTrackBar>(R.id.progress_track_bar3).startTask(20)

思路&编码


核心思路就一个: 画原环。我们要画两个圆环,一个下层的完整圆环作为底色,一个上层的圆弧作为进度。重点就是计算圆弧弧度的问题了。


假设当前进度是current,最大进度是max,那么当前圆弧进度就是:(current/max)*360,然后我们直接调用:


// oval: 放置圆弧的矩形
// startAngle: 开始绘制的起点角度,方向是顺时针计算的。0就x正半轴,90就是y轴负半轴
// sweepAngle: 要绘制的圆弧的弧度,就是上述: (current/max)x360
// false: 表示不连接到圆心,表示绘制一个圆弧
canvas.drawArc(oval, startAngle, sweepAngle, false, mPaint);

就能绘制出对应的圆弧。


所以,我们这样:


// 绘制下层: 圆形
mPaint.setColor(firstLayerColor);
canvas.drawCircle(x, y, radius, mPaint);

// 绘制上层: 圆弧
mPaint.setColor(secondLayerColor);
float sweepAngle = (currentProgress / maxProgress) * 360;
canvas.drawArc(oval, startAngle, sweepAngle, false, mPaint);

我们先用下层颜色绘制一个圆形,然后用上层颜色绘制个圆弧,然后不断触发重绘,就能得到想要的效果。


但是,如果我们想要的是: 随着进度变大,圆弧越来越短呢?比如示例图的第二个效果。说白了就是让上层随着时间流逝而变小,直到消失,怎么实现呢?


其实,说白了就是时间越长,弧度越小,我们做减法即可,我们用(max-current)来作为已读进度,这样随着时间流逝,进度就越来越小。


有人说,这样不对啊,这样(max-current)不就越读越小了吗,这样画出来的弧度就越来越短了,最后完全漏出了底层,给人的感觉是倒着读的。没错,所以,我们只绘制一层,我们用下层颜色来绘制圆弧!这样,随着时间流逝,弧度越来越小,因为圆弧是用下层颜色绘制的,所以视觉上就是: 下层越来越少。给人的感觉就是: 上层越来越大以至于盖住了下层。


逻辑如下:


// 用下层颜色 绘制 剩下的弧度
mPaint.setColor(firstLayerColor);
float leaveAngle = ((maxProgress - currentProgress) / maxProgress) * 360;
canvas.drawArc(oval, startAngle, leaveAngle, false, mPaint);

可以看到,这里只绘制一层,随着时间流逝,圆弧越来越短,给人的感觉就是: 圆弧消失。就达到了示例图中 第二个圆弧的效果。


整体代码如下:


public class ProgressTrackBar extends View {


private static final int DEFAULT_FIRST_COLOR = Color.WHITE;
private static final int DEFAULT_SECOND_COLOR = Color.parseColor("#FFA12F");

private static final int PROGRESS_WIDTH = 6;
private static final float MAX_PROGRESS = 360F;
private static final int DEFAULT_SPEED = 1;

private Paint mPaint;
private float startAngle = 0;
private int firstLayerColor = DEFAULT_FIRST_COLOR;
private int secondLayerColor = DEFAULT_SECOND_COLOR;
private final RectF oval = new RectF(); // 圆形轨迹
private float maxProgress = MAX_PROGRESS; // 最大进度:ms
private float currentProgress = 0F; // 当前进度:ms
private int speed = DEFAULT_SPEED; // 速度(多长时间更新一次UI):ms
private int progressWidth = PROGRESS_WIDTH; // 进度条宽度

private OnProgressFinished onProgressFinished;

private Handler taskHandler;
private OnProgress runnable; //进度回调

// 顶层颜色是否是透明
private boolean isSecondColorTransparent = false;

public ProgressTrackBar(Context context) {
super(context);
init();
}

public ProgressTrackBar(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);

TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.ProgressTrackBar);
firstLayerColor = typedArray.getColor(R.styleable.ProgressTrackBar_p_first_color, DEFAULT_FIRST_COLOR);
secondLayerColor = typedArray.getColor(R.styleable.ProgressTrackBar_p_second_color, DEFAULT_SECOND_COLOR);
startAngle = typedArray.getFloat(R.styleable.ProgressTrackBar_p_start, 0F);
progressWidth = typedArray.getDimensionPixelSize(R.styleable.ProgressTrackBar_p_width, PROGRESS_WIDTH);
maxProgress = typedArray.getDimension(R.styleable.ProgressTrackBar_p_max_progress, MAX_PROGRESS);

typedArray.recycle();

init();
}

public ProgressTrackBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}

private void init() {
refresh();
mPaint = new Paint();
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setAntiAlias(true);
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPaint.setStrokeWidth(progressWidth);
}

public void setFirstLayerColor(int firstLayerColor) {
this.firstLayerColor = firstLayerColor;
}

public void setSecondLayerColor(int secondLayerColor) {
this.secondLayerColor = secondLayerColor;
refresh();
}

public void setMaxProgress(float maxProgress) {
this.maxProgress = maxProgress;
}

public void setSpeed(int speed) {
this.speed = speed;
}

public void setStartAngle(float startAngle) {
this.startAngle = startAngle;
}

public void setProgressWidth(int progressWidth) {
this.progressWidth = progressWidth;
}

public void setOnProgressListener(OnProgress runnable) {
this.runnable = runnable;
}

public void setOnProgressFinished(OnProgressFinished onProgressFinished) {
this.onProgressFinished = onProgressFinished;
}

private void initTask() {
taskHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
if (currentProgress < maxProgress) {
currentProgress += speed;
postInvalidate();
if (runnable != null) {
runnable.onProgress(currentProgress);
}
taskHandler.sendEmptyMessageDelayed(0, speed);
} else {
stopTask();
}
}
};
}

private void refresh() {
isSecondColorTransparent = (secondLayerColor == Color.parseColor("#00000000"));
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int x = getWidth() >> 1;
int y = getHeight() >> 1;
int center = Math.min(x, y);
int radius = center - progressWidth;

int left = x - radius;
int top = y - radius;
int right = x + radius;
int bottom = y + radius;
oval.set(left, top, right, bottom);

// 这里需要处理一下上层是透明的情况
if (isSecondColorTransparent) {
// 用下层颜色 绘制 剩下的弧度
mPaint.setColor(firstLayerColor);
float leaveAngle = ((maxProgress - currentProgress) / maxProgress) * 360;
canvas.drawArc(oval, startAngle, leaveAngle, false, mPaint);
} else {
// 绘制下层
mPaint.setColor(firstLayerColor);
canvas.drawCircle(x, y, radius, mPaint);

// 绘制上层
mPaint.setColor(secondLayerColor);
float sweepAngle = (currentProgress / maxProgress) * 360;
canvas.drawArc(oval, startAngle, sweepAngle, false, mPaint);
}
}

public void startTask(int progress) {
currentProgress = progress;
initTask();
taskHandler.sendEmptyMessage(0);
}

public void startTask(int progress, OnProgressFinished onProgressFinished) {
this.onProgressFinished = onProgressFinished;
currentProgress = progress;
initTask();
taskHandler.sendEmptyMessage(0);
}

public void stopTask() {
if (onProgressFinished != null) {
onProgressFinished.onFinished();
}
if (taskHandler != null) {
taskHandler.removeCallbacksAndMessages(null);
}
}

@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
stopTask();
}

public interface OnProgressFinished {
void onFinished();
}

public interface OnProgress {
void onProgress(float progress);
}
}

总结


核心思路就一个: 如果上层要用透明盖住下层,这是不可能的,所以不如用上层的相对值去绘制下层


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

你知道为何跨域中会发送 options 请求?

同源策略 同源策略是一个重要的安全策略,它用于限制一个 origin 的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。 简单说,当我们访问一个网站时,浏览器会对源地址的不同部分(协议://域名:端口)做检查...
继续阅读 »

同源策略



同源策略是一个重要的安全策略,它用于限制一个 origin 的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。



简单说,当我们访问一个网站时,浏览器会对源地址的不同部分(协议://域名:端口)做检查。比如防止利用它源的存储信息(Cookies...)做不安全的用途。


跨域 CORS


但凡被浏览器识别为不同源,浏览器都会认为是跨域,默认是不允许的。


比如:试图在 http://127.0.0.1:4000 中,请求 http://127.0.0.1:3000 的资源会出现如下错误:


跨域错误


这也是前端 100% 在接口调试中会遇到的问题。


同源和跨域的判断规则


简单请求和复杂请求



相信都会在浏览器的 Network 中看到两个同样地址的请求,有没有想过这是为什么呢?这是因为在请求中,会分为 简单请求复杂请求


简单请求:满足如下条件的,将不会触发跨域检查:



  • 请求方法为:GETPOSTHEAD

  • 请求头:AcceptAccept-LanguageContent-LanguageContent-Type


其中 Content-Type 限定为 :text/plain、multipart/form-data、application/x-www-form-urlencoded


我们可以更改同源规则,看下如下示例:



http://127.0.0.1:4000/ 下,请求 http://127.0.0.1:3000 不同端口的地址



简单请求


域名不同,这已经跨域了。但由于请求方法为 GET,符合 简单请求,请求将正常工作。


复杂请求:不满足简单请求的都为复杂请求。在发送请求前,会使用 options 方法发起一个 预检请求(Preflight) 到服务器,以获知服务器是否允许该实际请求。


模拟一个跨域请求:


// 端口不同,content-type 也非限定值
axios.post(
'http://127.0.0.1:3000/test/cors',
{},
{
headers: {
'content-type': 'application/json',
},
}
);

能看到在请求之前浏览器会事先发起一个 Preflight 预检请求


Preflight


这个 预检请求 的请求方法为 options,同时会包含 Access-Control-xxx 的请求头:


options请求信息


当然,此时服务端没有做跨域处理(示例使用 express 起的服务,预检请求默认响应 200),就会出现浏览器 CORS 的错误警告。


跨域错误


如何解决跨域


对于跨域,前端再熟悉不过,百度搜索能找到一堆解决方法,关键词不是 JSONP,或者添加些 Access-Control-XXX 响应头。


本篇将详细说下后一种方式,姑且称为:服务端解决方案。


为 options 添加响应头


express 举例,首先对 OPTIONS 方法的请求添加这些响应头,它将根据告诉浏览器根据这些属性进行跨域限制:


app.use(function (req, res, next) {
if (req.method == 'OPTIONS') {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, PUT, POST, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'content-type');
res.status(200).end();
}
});

如果你不对 预检接口 做正确的设置,那么后续一切都是徒劳。


打个比方:如果 Access-Control-Allow-Methods 只设置了 POST,如果客户端请求方法为 PUT,那么最终会出现跨域异常,并会指出 PUT 没有在预检请求中的 Access-Control-Allow-Methods 出现:


跨域方法错误
所以,以后读懂跨域异常对于正确的添加服务端响应信息非常重要。另外:GET、POST、HEAD 属于简单请求的方法,所以即使不在 Access-Control-Allow-Methods 定义也不碍事(如果不对请指出)


正式的跨域请求


随后对我们代码发出的请求额外添加跨域响应头(这需要和前面的预检接口一致)


if (req.method == 'OPTIONS') {
//...
} else {
// http://127.0.0.1:3000/test/cors
res.setHeader('Access-Control-Allow-Origin', '*');
next();
}

最后能看到我们等请求正常请求到了:


跨域请求


对于跨域请求头的说明


上例出现了我们经常见到的三个:Access-Control-Allow-OriginAccess-Control-Allow-MethodsAccess-Control-Allow-Headers


参考 cors 库,另外还有其他用于预检请求的响应头:



下面将对上面这些头做个说明。


Access-Control-Allow-Origin


预检请求正常请求 告知浏览器被允许的源。支持通配符“*”,但不支持以逗号“,”分割的多源填写方式。


如果尝试些多个域名,则会出现如下错误:



Response to preflight request doesn't pass access control check: The 'Access-Control-Allow-Origin' header contains multiple values 'aaa,bbb', but only one is allowed.



多源错误


另外,也不建议 Access-Control-Allow-Origin 以通配符方式定义,这样会增加安全隐患,最好以请求方的 origin 来赋值。


const origin = req.headers.origin;
res.setHeader('Access-Control-Allow-Origin', origin || '*');
// 因为会随着客户端请求的 Origin 变化,所以标识 Vary,让浏览器不要缓存
res.setHeader('Vary', 'Origin');

Access-Control-Allow-Methods


被允许的 Http 方法,按照需要填写,支持多个,例如: GET , HEAD , PUT , PATCH , POST , DELETE


由于判断 简单请求 之一的 HTTP 方法默认为 GETPOSTHEAD ,所以这些即使不在 Access-Control-Allow-Methods 约定,浏览器也是支持的。


比如:如果服务端定义 PUT 方法,而客户端发送的方法为 DELETE,则会出现如下错误:


res.setHeader('Access-Control-Allow-Methods', 'PUT');


Method DELETE is not allowed by Access-Control-Allow-Methods in preflight response.



方法错误


Access-Control-Allow-Headers


预检接口 告知客户端允许的请求头。


简单请求 约定的请求头默认支持: AcceptAccept-LanguageContent-LanguageContent-Typetext/plain、multipart/form-data、application/x-www-form-urlencoded


如果客户端的请求头不在定义范围内,则会报错:



Request header field abc is not allowed by Access-Control-Allow-Headers in preflight response.



请求头错误


需要将此头调整为:


res.setHeader('Access-Control-Allow-Headers', 'content-type, abc');

Access-Control-Max-Age


定义 预检接口 告知客户端允许的请求头可以缓存多久。


默认时间规则:



  • 在 Firefox 中,上限是 24 小时 (即 86400 秒)。

  • 在 Chromium v76 之前, 上限是 10 分钟(即 600 秒)。

  • 从 Chromium v76 开始,上限是 2 小时(即 7200 秒)。

  • Chromium 同时规定了一个默认值 5 秒。

  • 如果值为 -1,表示禁用缓存,则每次请求前都需要使用 OPTIONS 预检请求。


比如设置为 5 秒后,客户端在第一次会发送 预检接口 后,5 秒内将不再发送 预检接口


res.setHeader('Access-Control-Max-Age', '5');

缓存示例


Access-Control-Allow-Credentials


跨域的请求,默认浏览器不会将当前地址的 Cookies 信息传给服务器,以确保信息的安全性。如果有需要,服务端需要设置 Access-Control-Allow-Credentials 响应头,另外客户端也需要开启 withCredentials 配置。


// 客户端请求
axios.post(
'http://127.0.0.1:3000/test/cors',
{},
{
headers: {
'content-type': 'application/json',
abc: '123',
},
withCredentials: true,
}
);

// 所有请求
res.setHeader('Access-Control-Allow-Credentials', 'true');

需要注意的是,Access-Control-Allow-Origin 不能设置通配符“*”方式,会出现如下错误:


不支持通配符
这个 Access-Control-Allow-Origin 必须是当前页面源的地址。


Access-Control-Expose-Headers


Access-Control-Allow-Credentials 类似,如果服务端有自定义设置的请求头,跨域的客户端请求在响应信息中是接收不到该请求头的。


// 服务端
res.setHeader('def', '123');

axios
.post(
'http://127.0.0.1:3000/test/cors',
{},
{
headers: {
'content-type': 'application/json',
abc: '123',
},
withCredentials: true,
}
)
.then((data) => {
console.log(data.headers.def); //undefined
});

需要在服务端设置 Access-Control-Expose-Headers 响应头,并标记哪些头是客户端能获取到的:


res.setHeader('Access-Control-Expose-Headers', 'def');
res.setHeader('def', '123');

Access-Control-Request-Headers


我试了半天没找到 Access-Control-Request-Headers 的使用示例,其实它是根据当前请求的头拼接得到的。


如果客户端的请求头为:


{
"content-type": "application/json",
"abc": "123",
"xyz": "123",
},

那么浏览器最后会在 预检接口 添加一个 Access-Control-Request-Headers 的头,其值为:abc,content-type,xyz。然后服务端再根据 Access-Control-Allow-Headers 告诉浏览器服务端的请求头支持说明,最后浏览器判断是否会有跨域错误。


另外,对于服务端也需要针对 Access-Control-Request-HeadersVary 处理:


res.setHeader('Vary', 'Origin' + ', ' + req.headers['access-control-request-headers']);

如此,对于跨域及其怎么处理头信息会有个基本的概念。希望在遇到类似问题能有章法的解决,而非胡乱尝试。


作者:Eminoda
链接:https://juejin.cn/post/7021077647417409550

收起阅读 »

移动端常见问题汇总,拿来吧你!

1px适配方案 某些时候,设计人员希望 1px在手机显示的就是1px,这也是....闲的,但是我们也要满足他们的需求, 这时候我们可以利用缩放来达到目的 .border_1px:before{    content: '';  ...
继续阅读 »

1px适配方案


某些时候,设计人员希望 1px在手机显示的就是1px,这也是....闲的,但是我们也要满足他们的需求,


这时候我们可以利用缩放来达到目的


.border_1px:before{
   content: '';
   position: absolute;
   top: 0;
   height: 1px;
   width: 100%;
   background-color: #000;
   transform-origin: 0% 0%;
}
@media only screen and (-webkit-min-device-pixel-ratio:2){
   .border_1px:before{
       transform: scaleY(0.5);
  }
}
@media only screen and (-webkit-min-device-pixel-ratio:3){
   .border_1px:before{
       transform: scaleY(0.33);
  }
}


设置一个专门的class来处理1px的问题,利用伪类给其添加



  • -webkit-min-device-pixel-ratio 获取像素比

  • transform: scaleY(0.5) 垂直方向缩放,后面的数字是倍数


图片模糊问题


.avatar{
   background-image: url(conardLi_1x.png);
}
@media only screen and (-webkit-min-device-pixel-ratio:2){
   .avatar{
       background-image: url(conardLi_2x.png);
  }
}
@media only screen and (-webkit-min-device-pixel-ratio:3){
   .avatar{
       background-image: url(conardLi_3x.png);
  }
}

根据不一样的像素比,准备不一样的图片,正常来说是1px图片像素 对应1px物理像素,图片的显示就不会模糊啦,但是这样的情况不多,不是非常重要,特殊需求的图,我们不这么做。


滚动穿透问题


wt1.gif


移动端的网站,我们是经常会有一些弹出框出现的,这样的弹出框,在上面滑动,会导致我们后面的整个页面发生移动,这个问题怎么解决呢??


body{
   position:fixed;
   width:100%;
}

给body添加position:fixed就可以使滚动条失效,这里弹框的显示和隐藏,我们利用JS进行控制,而且添加上position:fixed的一瞬间,可以看到页面一下回到0,0的位置,因为fixed是根据可视区定位的。


键盘唤起


main{
   padding: 2rem 0;
   /* height: 2000px; */
   position: absolute;
   top: 60px;
   bottom: 60px;
   overflow-y: scroll;
   width: 100%;
   -webkit-overflow-scrolling: touch;
}

当底部根据页面进行fixed定位的时候,键盘弹出一瞬间,fixed会失效,变成类似absoult,让main的内容无滚动,就不会连带fixed一起动了


并且为了保证如丝般顺滑:



  • -webkit-overflow-scrolling: touch;


移动端的神奇操作


IOS下的一些设置 和 安卓下的一些设置


添加到主屏幕后的标题


<meta name="apple-mobile-web-app-title" content="标题"> 

image.png


添加到主屏后的APP图标


<link href="short_cut_114x114.png" rel="apple-touch-icon-precomposed">


  • 一般我们只需要提供一个114*114的图标即可


启用webApp全屏模式


<meta name="apple-mobile-web-app-capable" content="yes" /> 
<meta name="apple-touch-fullscreen" content="yes" />



  • apple-mobile-web-app-capable


    删除默认的苹果工具栏和菜单栏,默认为no




  • apple-touch-fullscreen


    全屏显示




移动端手机号码识别


<meta name="format-detection" content="telephone=no" />


  • safari会对一些可能是手机号码的数字,进行识别,我们可以利用上面的方式,禁止识别


手动开启拨打电话功能


<a href="tel:13300000000">13300000000</a>


  • 在手机上点击这个链接,可以直接拨打电话


手动开启短信功能


<a href="sms:13300000000">13300000000</a>


  • 在手机上点击这个链接,可以跳转去短信页面,给该手机号发送消息


移动端邮箱识别


<meta name="format-detection" content="email=no" /> 

手动开启邮箱发送功能


<a href="mailto:854121000@qq.com">发送邮件</a>


  • 调用邮箱发送功能


优先启用最新版本IE和chrome


<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" /> 

移动端默认样式




  • 移动端默认字体



    1. 数字 和 英文字体 可使用Helvetica字体,IOS 和 Android都有这个字体

    2. 手机系统都有自己默认的字体,直接使用默认的


    body{
       font-family:Helvetica;
    }



  • 字体大小


    如果只是适配手机,可以使用px




  • IOS系统中,链接、按钮等点击会有灰色遮罩


    a,button,input,textarea{-webkit-tap-highlight-color: rgba(0,0,0,0)}



  • 去除圆角


    button,input{
       -webkit-appearance:none;
       border-radius: 0;
    }



  • 禁止文本缩放


    html{
        -webkit-text-size-adjust: 100%;
    }
收起阅读 »

你真的了解border-radius吗?

水平半径和垂直半径 现在很多人都不知道我们平常使用的圆角值是一种缩写,例如我们平常写的top圆角10px就是一种缩写: border-top-left-radius:10px; 等同于 border-top-left-radius:10px 10px; 其中...
继续阅读 »

水平半径和垂直半径


现在很多人都不知道我们平常使用的圆角值是一种缩写,例如我们平常写的top圆角10px就是一种缩写:


border-top-left-radius:10px; 等同于 border-top-left-radius:10px 10px; 

其中,第一个值表示水平半径,第二个值表示圆角垂直半径;


例如:


    <style>
.talk-dialog {
position: relative;
background: deepskyblue;
width: 100px;
height: 100px;
margin: 0 auto;
border-top-left-radius: 30px 80px;
}
</style>

 <div class="talk-dialog"></div>

image.png


那么border-radius的写法应该怎么去写呢??它的水平半径和垂直半径是通过 斜杠 区分。 例如:


border-radius: 30px / 40px;


表示四个角的圆角水平半径都是30px,垂直半径是40px;


border-radius斜杠前后都支持1-4个值,以下多个值得写法为:


border-radius:10px 20px / 5% 20% 3% 10%;(左上+右下,右上+左下, / 左上,右上,右下,左下)


重叠问题


难道你认为这就完了,border-radius你彻底搞懂了??其实不然!


我们来看看下面一个列子:


<!DOCTYPE html>
<html lang="en">
<head>

<style>
.talk-dialog {
position: relative;
background: red;
width: 100px;//重点关注
height: 100px;//重点关注
border-radius: 30px / 80px; //重点关注
margin: 50px auto;
}

.talk-dialog1 {
position: relative;
background: deepskyblue;
width: 100px;//重点关注
height: 100px;//重点关注
border-top-left-radius: 30px 80px; //重点关注
margin: 10px auto;
}
</style>

</head>

<body>
<div class="talk-dialog"></div>
<div class="talk-dialog1"></div>
</body>
</html>

我们的容器大小宽为100px,高为100px, 问大家一个问题!


border-radius: 30px / 80px; 与 border-top-left-radius: 30px 80px; 两个不同的容器的 top-left的圆角大小一样吗???


image.png


大家或许这样看不出来,我们修改为绝对布局,两个元素重叠在一起看看是否左上角可以完美重叠?


image.png


答案揭晓: 圆角效果是不一样的,因为我们容器的垂直高度为100px,我们border-radius:30px / 80px设置以后,我们元素的高度不足以放下两个半轴为80px(80+80=160)的椭圆,如果这种场景不做约束,曲线就会发生一定的重叠,因此css 规范对圆角曲线重叠问题做了额外的渲染设定,具体算法如下:


f=min(L宽度/S宽度,L高度/S高度),L为容器宽高,S为半径之和,


这里计算我们的例子:f=min(100/60,100/160)=0.625 , f的值小于1,则所有的圆角值都要乘以f


因此:border-radius: 30px / 80px;


左上角值等同于:


border-top-left-radius:18.75px 50px;


细节



  • border-radius 不支持负值

  • 圆角以外的区域不可点击

  • border-radius没有继承性,因此父元素设置了border-radius,子元素依旧是直角效果,要想达到圆角效果,需要加overflow:hidden。(重要,工作中常用)

  • border-radius 也支持transition过渡效果


高级用法案例:


image.png


代码:


<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="utf-8" />
<title></title>
<link rel="icon" href="data:;base64,=" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no" />
<meta name=" theme-color" content="#000000" />
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
<style>
.radius {
width: 150px;
height: 150px;
border-radius: 70% 30% 30% 70% / 60% 40% 60% 40%;
object-fit: cover;
object-position: right;
}

.demo {
position: relative;
width: 150px;
height: 150px;
margin: 10px auto;
}

.radius-1 {
width: 150px;
height: 150px;
object-fit: cover;
object-position: right;
background: deepskyblue;
color: #fff;
font-size: 40px;
text-align: center;
line-height: 120px;
border-bottom-right-radius: 100%;
}

.talk {
padding: 10px;
background: deepskyblue;
border-radius: .5em;
color: #fff;
position: relative;
z-index: 0;
}

.talk::before {
content: "";
position: absolute;
width: 15px;
height: 10px;
color: deepskyblue;
border-top: 10px solid;
border-top-left-radius: 80%;
left: 0;
bottom: 0;
margin-left: -12px;
-ms-transform: skewX(-30deg) scaleY(1.3);
transform: skewX(-30deg) scaleY(1.3);
z-index: -1;
}
</style>

</head>

<body>
<div class="demo demo1">
<img class="radius" src="./1.jpg" />
</div>
<div class="demo demo2">
<div class="radius-1">1</div>
</div>
<div class="demo demo3">
<div class="talk">border-radius圆角效果实现。</div>
</div>
</body>

</html>

结语:


欢迎大家多提宝贵意见,一赞一回,如果本文让你get 到知识,请不要吝啬你的star!



收起阅读 »

写给vue转react的同志们(5)

写给vue转react的同志们(4)我们知道 React 中使用高阶组件(下面简称HOC)来复用一些组件的逻辑。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。具体而言,高阶组件是参数为组件,返回值为新组件...
继续阅读 »

写给vue转react的同志们(4)
我们知道 React 中使用高阶组件(下面简称HOC)来复用一些组件的逻辑。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。具体而言,高阶组件是参数为组件,返回值为新组件的函数。


组件是将 props 转换为 UI,而高阶组件是将组件转换为另一个组件。


const EnhancedComponent = higherOrderComponent(WrappedComponent);

上面出自 React 官方文档。


那在 Vue 中 复用组件逻辑实际上比较简单,利用 Mixins 混入复用组件逻辑,当 Mixins 中的逻辑过多时(比如方法和属性),在项目当中使用时追述源代码会比较麻烦,因为他在混入后没有明确告诉你哪个方法是被复用的。


//mixins.js(Vue 2 举例)
export defalut {
data() {
return {
text: 'hello'
}
}
}
// a.vue
import mixins from './mixins.js'
export defalut {
mixins: [mixins]
computed: {
acitveText() {
return `来自mixins的数据:${this.text}`
}
}
}
复制代码

可以看到除了在开头引入并挂载混入,并没有看到this.text是从哪里来的,混入虽然好用,但当逻辑复杂时,其阅读起来是有一定困难的。


那你想在 Vue 中强行使用像 React 那样的高阶组件呢?那当然可以。只是 Vue 官方不怎么推崇 HOC,且 Mixins 本身可以实现 HOC 相关功能。


简单举个例子:


// hoc.js
import Vue from 'Vue'

export default const HOC = (component, text) => {
return Vue.component('HOC', {
render(createElement) {
return createElement(component, {
on: { ...this.$listeners },
props: {
text: this.text
}
})
},
data() {
return {
text: text,
hocText: 'HOC'
}
},
mounted() {
// do something ...
console.log(this.text)
console.log(this.hocText)
}
})
}

使用高阶组件:


// user.vue
<template>
<userInfo/>
</template>

<script>
import HOC from './hoc.js'
// 引入某个组件
import xxx from './xxx'

const userInfo = HOC(xxx, 'hello')

export default {
name: 'user',
components: {
userInfo
}
}
</script>



是不是相比 Mixins 更加复杂一点了?在 Vue 中使用高阶组件所带来的收益相对于 Mixins 并没有质的变化。不过话又说回来,起初 React 也是使用 Mixins 来完成代码复用的,比如为了避免组件的非必要的重复渲染可以在组件中混入 PureRenderMixin


const PureRenderMixin = require('react-addons-pure-render-mixin')
const component = React.createClass({
mixins: [PureRenderMixin]
})


后来 React 使用shallowCompare 来 替代 PureRenderMixin


const shallowCompare = require('react-addons-shallow-compare')
const component = React.createClass({
shouldComponentUpdate: (nextProps, nextState) => {
return shallowCompare(nextProps, nextState)
}
})


这需要你自己在组件中实现 shouldComponentUpdate 方法,只不过这个方法具体的工作由 shallowCompare 帮你完成,我们只需调用即可。


再后来 React 为了避免总是要重复调用这段代码,React.PureComponent 应运而生,总之 React 在慢慢将 Mixins 脱离开来,这对他们的生态系统并不是特别的契合。当然每种方案都各有千秋,只是是否适合自己的框架。


那我们回归 HOC,在 React 中如何封装 HOC 呢?


实际上我在往期篇幅有提到过:
点击传送


但是我还是简单举个例子:


封装 HOC:


// hoc.js
export default const HOC = (WrappedComponent) => {
return Class newComponent extends WrappedComponent {
constructor(props) {
super(props)
// do something ...
this.state = {
text: 'hello'
}
}
componentDidMount() {
super.componentDidMount()
// do something ...
console.log('this.state.text')
}
render() {
// init render
return super.render()
}
}
}



使用 HOC:


// user.js
import HOC from './hoc.js'
class user extends React.Component {
// do something ...
}
export defalut HOC(user)


装饰器写法更为简洁:


import HOC from './hoc.js'
@HOC
class user extends React.Component {
// do something ...
}
export defalut user


可以看到无论 Vue 还是 React 亦或是 HOC 或 Mixins 他们都是为了解决组件逻辑复用应运而生的,具体使用哪一种方案还要看你的项目契合度等其他因素。


技术本身并无好坏,只是会随着时间推移被其他更适合的方案取代,技术迭代也是必然的,相信作为一个优秀的程序员也不会去讨论一个技术的好或坏,只有适合与否。


作者:饼干_
链接:https://juejin.cn/post/7020215941422137381

收起阅读 »

Android学习指南 — Android进阶篇

ARTART 代表 Android Runtime,其处理应用程序执行的方式完全不同于 Dalvik,Dalvik 是依靠一个 Just-In-Time (JIT) 编译器去解释字节码。开发者编译后的应用代码需要通过一个解释器在用户的设备上运行,这一机制并不高...
继续阅读 »

ART

ART 代表 Android Runtime,其处理应用程序执行的方式完全不同于 Dalvik,Dalvik 是依靠一个 Just-In-Time (JIT) 编译器去解释字节码。开发者编译后的应用代码需要通过一个解释器在用户的设备上运行,这一机制并不高效,但让应用能更容易在不同硬件和架构上运 行。ART 则完全改变了这套做法,在应用安装时就预编译字节码到机器语言,这一机制叫 Ahead-Of-Time (AOT)编译。在移除解释代码这一过程后,应用程序执行将更有效率,启动更快。

ART 功能

预先 (AOT) 编译

ART 引入了预先编译机制,可提高应用的性能。ART 还具有比 Dalvik 更严格的安装时验证。在安装时,ART 使用设备自带的 dex2oat 工具来编译应用。该实用工具接受 DEX 文件作为输入,并为目标设备生成经过编译的应用可执行文件。该工具应能够顺利编译所有有效的 DEX 文件。

垃圾回收优化

垃圾回收 (GC) 可能有损于应用性能,从而导致显示不稳定、界面响应速度缓慢以及其他问题。ART 通过以下几种方式对垃圾回收做了优化:

  • 只有一次(而非两次)GC 暂停
  • 在 GC 保持暂停状态期间并行处理
  • 在清理最近分配的短时对象这种特殊情况中,回收器的总 GC 时间更短
  • 优化了垃圾回收的工效,能够更加及时地进行并行垃圾回收,这使得 GC_FOR_ALLOC 事件在典型用例中极为罕见
  • 压缩 GC 以减少后台内存使用和碎片

开发和调试方面的优化

  • 支持采样分析器

一直以来,开发者都使用 Traceview 工具(用于跟踪应用执行情况)作为分析器。虽然 Traceview 可提供有用的信息,但每次方法调用产生的开销会导致 Dalvik 分析结果出现偏差,而且使用该工具明显会影响运行时性能

ART 添加了对没有这些限制的专用采样分析器的支持,因而可更准确地了解应用执行情况,而不会明显减慢速度。KitKat 版本为 Dalvik 的 Traceview 添加了采样支持。

  • 支持更多调试功能

ART 支持许多新的调试选项,特别是与监控和垃圾回收相关的功能。例如,查看堆栈跟踪中保留了哪些锁,然后跳转到持有锁的线程;询问指定类的当前活动的实例数、请求查看实例,以及查看使对象保持有效状态的参考;过滤特定实例的事件(如断点)等。

  • 优化了异常和崩溃报告中的诊断详细信息

当发生运行时异常时,ART 会为您提供尽可能多的上下文和详细信息。ART 会提供 java.lang.ClassCastExceptionjava.lang.ClassNotFoundException 和 java.lang.NullPointerException 的更多异常详细信息(较高版本的 Dalvik 会提供 java.lang.ArrayIndexOutOfBoundsException 和 java.lang.ArrayStoreException 的更多异常详细信息,这些信息现在包括数组大小和越界偏移量;ART 也提供这类信息)。

ART GC

ART 有多个不同的 GC 方案,这些方案包括运行不同垃圾回收器。默认方案是 CMS(并发标记清除)方案,主要使用粘性 CMS 和部分 CMS。粘性 CMS 是 ART 的不移动分代垃圾回收器。它仅扫描堆中自上次 GC 后修改的部分,并且只能回收自上次 GC 后分配的对象。除 CMS 方案外,当应用将进程状态更改为察觉不到卡顿的进程状态(例如,后台或缓存)时,ART 将执行堆压缩。

除了新的垃圾回收器之外,ART 还引入了一种基于位图的新内存分配程序,称为 RosAlloc(插槽运行分配器)。此新分配器具有分片锁,当分配规模较小时可添加线程的本地缓冲区,因而性能优于 DlMalloc。

与 Dalvik 相比,ART CMS 垃圾回收计划在很多方面都有一定的改善:

  • 与 Dalvik 相比,暂停次数从 2 次减少到 1 次。Dalvik 的第一次暂停主要是为了进行根标记,即在 ART 中进行并发标记,让线程标记自己的根,然后马上恢复运行。
  • 与 Dalvik 类似,ART GC 在清除过程开始之前也会暂停 1 次。两者在这方面的主要差异在于:在此暂停期间,某些 Dalvik 环节在 ART 中并发进行。这些环节包括 java.lang.ref.Reference 处理、系统弱清除(例如,jni 弱全局等)、重新标记非线程根和卡片预清理。在 ART 暂停期间仍进行的阶段包括扫描脏卡片以及重新标记线程根,这些操作有助于缩短暂停时间。
  • 相对于 Dalvik,ART GC 改进的最后一个方面是粘性 CMS 回收器增加了 GC 吞吐量。不同于普通的分代 GC,粘性 CMS 不移动。系统会将年轻对象保存在一个分配堆栈(基本上是 java.lang.Object 数组)中,而非为其设置一个专属区域。这样可以避免移动所需的对象以维持低暂停次数,但缺点是容易在堆栈中加入大量复杂对象图像而使堆栈变长。

ART GC 与 Dalvik 的另一个主要区别在于 ART GC 引入了移动垃圾回收器。使用移动 GC 的目的在于通过堆压缩来减少后台应用使用的内存。目前,触发堆压缩的事件是 ActivityManager 进程状态的改变。当应用转到后台运行时,它会通知 ART 已进入不再“感知”卡顿的进程状态。此时 ART 会进行一些操作(例如,压缩和监视器压缩),从而导致应用线程长时间暂停。目前正在使用的两个移动 GC 是同构空间压缩和半空间压缩。

  • 半空间压缩将对象在两个紧密排列的碰撞指针空间之间进行移动。这种移动 GC 适用于小内存设备,因为它可以比同构空间压缩稍微多节省一点内存。额外节省出的空间主要来自紧密排列的对象,这样可以避免 RosAlloc/DlMalloc 分配器占用开销。由于 CMS 仍在前台使用,且不能从碰撞指针空间中进行收集,因此当应用在前台使用时,半空间还要再进行一次转换。这种情况并不理想,因为它可能引起较长时间的暂停。
  • 同构空间压缩通过将对象从一个 RosAlloc 空间复制到另一个 RosAlloc 空间来实现。这有助于通过减少堆碎片来减少内存使用量。这是目前非低内存设备的默认压缩模式。相比半空间压缩,同构空间压缩的主要优势在于应用从后台切换到前台时无需进行堆转换。

Hook

基本流程

1、根据需求确定 要 hook 的对象
2、寻找要hook的对象的持有者,拿到要 hook 的对象
3、定义“要 hook 的对象”的代理类,并且创建该类的对象
4、使用上一步创建出来的对象,替换掉要 hook 的对象

使用示例

/**
* hook的核心代码
* 这个方法的唯一目的:用自己的点击事件,替换掉 View 原来的点击事件
*
* @param view hook的范围仅限于这个view
*/
@SuppressLint({"DiscouragedPrivateApi", "PrivateApi"})
public static void hook(Context context, final View view) {//
try {
// 反射执行View类的getListenerInfo()方法,拿到v的mListenerInfo对象,这个对象就是点击事件的持有者
Method method = View.class.getDeclaredMethod("getListenerInfo");
method.setAccessible(true);//由于getListenerInfo()方法并不是public的,所以要加这个代码来保证访问权限
Object mListenerInfo = method.invoke(view);//这里拿到的就是mListenerInfo对象,也就是点击事件的持有者

// 要从这里面拿到当前的点击事件对象
Class<?> listenerInfoClz = Class.forName("android.view.View$ListenerInfo");// 这是内部类的表示方法
Field field = listenerInfoClz.getDeclaredField("mOnClickListener");
final View.OnClickListener onClickListenerInstance = (View.OnClickListener) field.get(mListenerInfo);//取得真实的mOnClickListener对象

// 2. 创建我们自己的点击事件代理类
// 方式1:自己创建代理类
// ProxyOnClickListener proxyOnClickListener = new ProxyOnClickListener(onClickListenerInstance);
// 方式2:由于View.OnClickListener是一个接口,所以可以直接用动态代理模式
// Proxy.newProxyInstance的3个参数依次分别是:
// 本地的类加载器;
// 代理类的对象所继承的接口(用Class数组表示,支持多个接口)
// 代理类的实际逻辑,封装在new出来的InvocationHandler内
Object proxyOnClickListener = Proxy.newProxyInstance(context.getClass().getClassLoader(), new Class[]{View.OnClickListener.class}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Log.d("HookSetOnClickListener", "点击事件被hook到了");//加入自己的逻辑
return method.invoke(onClickListenerInstance, args);//执行被代理的对象的逻辑
}
});
// 3. 用我们自己的点击事件代理类,设置到"持有者"中
field.set(mListenerInfo, proxyOnClickListener);
} catch (Exception e) {
e.printStackTrace();
}
}

// 自定义代理类
static class ProxyOnClickListener implements View.OnClickListener {
View.OnClickListener oriLis;

public ProxyOnClickListener(View.OnClickListener oriLis) {
this.oriLis = oriLis;
}

@Override
public void onClick(View v) {
Log.d("HookSetOnClickListener", "点击事件被hook到了");
if (oriLis != null) {
oriLis.onClick(v);
}
}
}

Proguard

Proguard 具有以下三个功能:

  • 压缩(Shrink): 检测和删除没有使用的类,字段,方法和特性
  • 优化(Optimize) : 分析和优化Java字节码
  • 混淆(Obfuscate): 使用简短的无意义的名称,对类,字段和方法进行重命名

规则

  • 关键字
关键字描述
keep保留类和类中的成员,防止被混淆或移除
keepnames保留类和类中的成员,防止被混淆,成员没有被引用会被移除
keepclassmembers只保留类中的成员,防止被混淆或移除
keepclassmembernames只保留类中的成员,防止被混淆,成员没有引用会被移除
keepclasseswithmembers保留类和类中的成员,防止被混淆或移除,保留指明的成员
keepclasseswithmembernames保留类和类中的成员,防止被混淆,保留指明的成员,成员没有引用会被移除
  • 通配符
通配符描述
匹配类中的所有字段
匹配类中所有的方法
匹配类中所有的构造函数
*匹配任意长度字符,不包含包名分隔符(.)
**匹配任意长度字符,包含包名分隔符(.)
***匹配任意参数类型
  • 指定混淆时可使用字典
-applymapping filename 指定重用一个已经写好了的map文件作为新旧元素名的映射。
-obfuscationdictionary filename 指定一个文本文件用来生成混淆后的名字。
-classobfuscationdictionary filename 指定一个混淆类名的字典
-packageobfuscationdictionary filename 指定一个混淆包名的字典
-overloadaggressively 混淆的时候大量使用重载,多个方法名使用同一个混淆名(慎用)

公共模板

#############################################
#
# 对于一些基本指令的添加
#
#############################################
# 代码混淆压缩比,在 0~7 之间,默认为 5,一般不做修改
-optimizationpasses 5

# 混合时不使用大小写混合,混合后的类名为小写
-dontusemixedcaseclassnames

# 指定不去忽略非公共库的类
-dontskipnonpubliclibraryclasses

# 这句话能够使我们的项目混淆后产生映射文件
# 包含有类名->混淆后类名的映射关系
-verbose

# 指定不去忽略非公共库的类成员
-dontskipnonpubliclibraryclassmembers

# 不做预校验,preverify 是 proguard 的四个步骤之一,Android 不需要 preverify,去掉这一步能够加快混淆速度。
-dontpreverify

# 保留 Annotation 不混淆
-keepattributes *Annotation*,InnerClasses

# 避免混淆泛型
-keepattributes Signature

# 抛出异常时保留代码行号
-keepattributes SourceFile,LineNumberTable

# 指定混淆是采用的算法,后面的参数是一个过滤器
# 这个过滤器是谷歌推荐的算法,一般不做更改
-optimizations !code/simplification/cast,!field/*,!class/merging/*


#############################################
#
# Android开发中一些需要保留的公共部分
#
#############################################

# 保留我们使用的四大组件,自定义的 Application 等等这些类不被混淆
# 因为这些子类都有可能被外部调用
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Appliction
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider
-keep public class * extends android.app.backup.BackupAgentHelper
-keep public class * extends android.preference.Preference
-keep public class * extends android.view.View
-keep public class com.android.vending.licensing.ILicensingService


# 保留 support 下的所有类及其内部类
-keep class android.support.** { *; }

# 保留继承的
-keep public class * extends android.support.v4.**
-keep public class * extends android.support.v7.**
-keep public class * extends android.support.annotation.**

# 保留 R 下面的资源
-keep class **.R$* { *; }

# 保留本地 native 方法不被混淆
-keepclasseswithmembernames class * {
native <methods>;
}

# 保留在 Activity 中的方法参数是view的方法,
# 这样以来我们在 layout 中写的 onClick 就不会被影响
-keepclassmembers class * extends android.app.Activity {
public void *(android.view.View);
}

# 保留枚举类不被混淆
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}

# 保留我们自定义控件(继承自 View)不被混淆
-keep public class * extends android.view.View {
*** get*();
void set*(***);
public <init>(android.content.Context);
public <init>(android.content.Context, android.util.AttributeSet);
public <init>(android.content.Context, android.util.AttributeSet, int);
}

# 保留 Parcelable 序列化类不被混淆
-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}

# 保留 Serializable 序列化的类不被混淆
-keepnames class * implements java.io.Serializable
-keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID;
private static final java.io.ObjectStreamField[] serialPersistentFields;
!static !transient <fields>;
!private <fields>;
!private <methods>;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}

# 对于带有回调函数的 onXXEvent、**On*Listener 的,不能被混淆
-keepclassmembers class * {
void *(**On*Event);
void *(**On*Listener);
}

# webView 处理,项目中没有使用到 webView 忽略即可
-keepclassmembers class fqcn.of.javascript.interface.for.webview {
public *;
}
-keepclassmembers class * extends android.webkit.webViewClient {
public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap);
public boolean *(android.webkit.WebView, java.lang.String);
}
-keepclassmembers class * extends android.webkit.webViewClient {
public void *(android.webkit.webView, java.lang.String);
}

# js
-keepattributes JavascriptInterface
-keep class android.webkit.JavascriptInterface { *; }
-keepclassmembers class * {
@android.webkit.JavascriptInterface <methods>;
}

# @Keep
-keep,allowobfuscation @interface android.support.annotation.Keep
-keep @android.support.annotation.Keep class *
-keepclassmembers class * {
@android.support.annotation.Keep *;
}

常用的自定义混淆规则

# 通配符*,匹配任意长度字符,但不含包名分隔符(.)
# 通配符**,匹配任意长度字符,并且包含包名分隔符(.)

# 不混淆某个类
-keep public class com.jasonwu.demo.Test { *; }

# 不混淆某个包所有的类
-keep class com.jasonwu.demo.test.** { *; }

# 不混淆某个类的子类
-keep public class * com.jasonwu.demo.Test { *; }

# 不混淆所有类名中包含了 ``model`` 的类及其成员
-keep public class **.*model*.** {*;}

# 不混淆某个接口的实现
-keep class * implements com.jasonwu.demo.TestInterface { *; }

# 不混淆某个类的构造方法
-keepclassmembers class com.jasonwu.demo.Test {
public <init>();
}

# 不混淆某个类的特定的方法
-keepclassmembers class com.jasonwu.demo.Test {
public void test(java.lang.String);
}

aar中增加独立的混淆配置

build.gralde

android {
···
defaultConfig {
···
consumerProguardFile 'proguard-rules.pro'
}
···
}

检查混淆和追踪异常

开启 Proguard 功能,则每次构建时 ProGuard 都会输出下列文件:

  • dump.txt
    说明 APK 中所有类文件的内部结构。
  • mapping.txt
    提供原始与混淆过的类、方法和字段名称之间的转换。
  • seeds.txt
    列出未进行混淆的类和成员。
  • usage.txt
    列出从 APK 移除的代码。

这些文件保存在 /build/outputs/mapping/release/ 中。我们可以查看 seeds.txt 里面是否是我们需要保留的,以及 usage.txt 里查看是否有误删除的代码。 mapping.txt 文件很重要,由于我们的部分代码是经过重命名的,如果该部分出现 bug,对应的异常堆栈信息里的类或成员也是经过重命名的,难以定位问题。我们可以用 retrace 脚本(在 Windows 上为 retrace.bat;在 Mac/Linux 上为 retrace.sh)。它位于 /tools/proguard/ 目录中。该脚本利用 mapping.txt 文件和你的异常堆栈文件生成没有经过混淆的异常堆栈文件,这样就可以看清是哪里出问题了。使用 retrace 工具的语法如下:

retrace.bat|retrace.sh [-verbose] mapping.txt [<stacktrace_file>]

架构

MVC

在 Android 中,三者的关系如下:

由于在 Android 中 xml 布局的功能性太弱,所以 Activity 承担了绝大部分的工作,所以在 Android 中 mvc 更像:

总结:

  • 具有一定的分层,model 解耦,controller 和 view 并没有解耦
  • controller 和 view 在 Android 中无法做到彻底分离,Controller 变得臃肿不堪
  • 易于理解、开发速度快、可维护性高

MVP

通过引入接口 BaseView,让相应的视图组件如 Activity,Fragment去实现 BaseView,把业务逻辑放在 presenter 层中,弱化 Model 只有跟 view 相关的操作都由 View 层去完成。

总结:

  • 彻底解决了 MVC 中 View 和 Controller 傻傻分不清楚的问题
  • 但是随着业务逻辑的增加,一个页面可能会非常复杂,UI 的改变是非常多,会有非常多的 case,这样就会造成 View 的接口会很庞大
  • 更容易单元测试

MVVM

在 MVP 中 View 和 Presenter 要相互持有,方便调用对方,而在 MVP 中 View 和 ViewModel 通过 Binding 进行关联,他们之前的关联处理通过 DataBinding 完成。

总结:

  • 很好的解决了 MVC 和 MVP 的问题
  • 视图状态较多,ViewModel 的构建和维护的成本都会比较高
  • 但是由于数据和视图的双向绑定,导致出现问题时不太好定位来源

Jetpack

架构

CMake 构建 NDK 项目

CMake 是一个开源的跨平台工具系列,旨在构建,测试和打包软件,从 Android Studio 2.2 开始,Android Sudio 默认地使用 CMake 与 Gradle 搭配使用来构建原生库。

启动方式只需要在 app/build.gradle 中添加相关:

android {
···
defaultConfig {
···
externalNativeBuild {
cmake {
cppFlags ""
}
}

ndk {
abiFilters 'arm64-v8a', 'armeabi-v7a'
}
}
···
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
}

然后在对应目录新建一个 CMakeLists.txt 文件:

# 定义了所需 CMake 的最低版本
cmake_minimum_required(VERSION 3.4.1)

# add_library() 命令用来添加库
# native-lib 对应着生成的库的名字
# SHARED 代表为分享库
# src/main/cpp/native-lib.cpp 则是指明了源文件的路径。
add_library( # Sets the name of the library.
native-lib

# Sets the library as a shared library.
SHARED

# Provides a relative path to your source file(s).
src/main/cpp/native-lib.cpp)

# find_library 命令添加到 CMake 构建脚本中以定位 NDK 库,并将其路径存储为一个变量。
# 可以使用此变量在构建脚本的其他部分引用 NDK 库
find_library( # Sets the name of the path variable.
log-lib

# Specifies the name of the NDK library that
# you want CMake to locate.
log)

# 预构建的 NDK 库已经存在于 Android 平台上,因此,无需再构建或将其打包到 APK 中。
# 由于 NDK 库已经是 CMake 搜索路径的一部分,只需要向 CMake 提供希望使用的库的名称,并将其关联到自己的原生库中

# 要将预构建库关联到自己的原生库
target_link_libraries( # Specifies the target library.
native-lib

# Links the target library to the log library
# included in the NDK.
${log-lib})
···

常用的 Android NDK 原生 API

支持 NDK 的 API 级别关键原生 API包括
3Java 原生接口#include <jni.h>
3Android 日志记录 API#include <android/log.h>
5OpenGL ES 2.0#include <GLES2/gl2.h> #include <GLES2/gl2ext.h>
8Android 位图 API#include <android/bitmap.h>
9OpenSL ES#include <SLES/OpenSLES.h> #include <SLES/OpenSLES_Platform.h> #include <SLES/OpenSLES_Android.h> #include <SLES/OpenSLES_AndroidConfiguration.h>
9原生应用 API#include <android/rect.h> #include <android/window.h> #include<android/native_activity.h> ···
18OpenGL ES 3.0#include <GLES3/gl3.h> #include <GLES3/gl3ext.h>
21原生媒体 API#include <media/NdkMediaCodec.h> #include <media/NdkMediaCrypto.h> ···
24原生相机 API#include <camera/NdkCameraCaptureSession.h> #include <camera/NdkCameraDevice.h> ···
···

类加载器

双亲委托模式

某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子 ClassLoader 再加载一次。如果不使用这种委托模式,那我们就可以随时使用自定义的类来动态替代一些核心的类,存在非常大的安全隐患。

DexPathList

DexClassLoader 重载了 findClass 方法,在加载类时会调用其内部的 DexPathList 去加载。DexPathList 是在构造 DexClassLoader 时生成的,其内部包含了 DexFile。

DexPathList.java
public Class findClass(String name) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext);
if (clazz != null) {
return clazz;
}
}
}
return null;
}
收起阅读 »

Android自定义控件六边形

Android自定义六边形控件一.效果图原文地址: https://blog.csdn.net/oMengHui/article/details/45540645二.核心算法平面内一个坐标点是否在多边形内判断,使用射线法判断。从目标点出发引一条射线,...
继续阅读 »



Android自定义六边形控件

一.效果图

原文地址: https://blog.csdn.net/oMengHui/article/details/45540645

20150506195536825.gif

二.核心算法
平面内一个坐标点是否在多边形内判断,使用射线法判断。从目标点出发引一条射线,看这条射线和多边形所有边的交点数目。如果是奇数个交点,则说明点在多边形内部;如果是偶数个交点,则说明在外部。

20150506195740393.jpeg

算法图解:

20150506195748111.jpeg

参考代码:

int pnpoly(int nvert, float *vertx, float *verty, float testx, float testy)
{
int i, j, c = 0;
for (i = 0, j = nvert-1; i < nvert; j = i++)
{
if ( ((verty[i]>testy) != (verty[j]>testy)) &&
(testx < (vertx[j]-vertx[i]) * (testy-verty[i]) / (verty[j]-verty[i]) + vertx[i]) )
c = !c;
}
return c;
}
复制代码

更多参考信息

三.知识点
1.控件属性自定义和使用 在values->attrs->declare-styleable中定义属性;在布局中引入(格式xmls:sec=”schemas.android.com/apk/res/程序包…;

2.Paint画笔使用 class继承View后重写onDraw方法,Paint paint=new Paint().setStyle(Style.FILL); canvas.drawText(“Hello”,x,y,paint);

3.Path路径使用 Path path=new Path(); path.moveTo(x1,y1); path.lineTo(x2,y2); path.close(); canvas.drawPath(path,paint);

4.图片缩放平铺居中 六边形视图显示为正方形,如属性设置图片宽高不相等直接使用图片会被拉伸变形。通过逻辑处理以图片宽高较小值居中裁剪图片。

 /**
* 按宽/高缩放图片到指定大小并进行裁剪得到中间部分图片
*
* @param bitmap 源bitmap
* @param w 缩放后指定的宽度
* @param h 缩放后指定的高度
* @return 缩放后的中间部分图片
*/
public static Bitmap zoomBitmap(Bitmap bitmap, int w, int h) {
int width = bitmap.getWidth();
int height = bitmap.getHeight();
float scaleWidht, scaleHeight, x, y;
Bitmap newbmp;
Matrix matrix = new Matrix();
if (width > height) {
scaleWidht = ((float) h / height);
scaleHeight = ((float) h / height);
x = (width - w * height / h) / 2;// 获取bitmap源文件中x做表需要偏移的像数大小
y = 0;
} else if (width < height) {
scaleWidht = ((float) w / width);
scaleHeight = ((float) w / width);
x = 0;
y = (height - h * width / w) / 2;// 获取bitmap源文件中y做表需要偏移的像数大小
} else {
scaleWidht = ((float) w / width);
scaleHeight = ((float) w / width);
x = 0;
y = 0;
}
matrix.postScale(scaleWidht, scaleHeight);
try {
newbmp = Bitmap.createBitmap(bitmap, (int) x, (int) y,
(int) (width - x), (int) (height - y), matrix, true);// createBitmap()方法中定义的参数x+width要小于或等于bitmap.getWidth(),y+height要小于或等于bitmap.getHeight()
} catch (Exception e) {
e.printStackTrace();
return null;
}
return newbmp;
}
复制代码

5.动画(ScaleAnimation)

Animation scaleAnimation = new ScaleAnimation(start, end, start, end,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF,
0.5f);
scaleAnimation.setDuration(30);
scaleAnimation.setFillAfter(true);
this.startAnimation(endAnimation);
复制代码

6.监听实现

HexagonView.java ->
public interface OnHexagonViewClickListener {
public void onClick(View view);
}
public void setOnHexagonClickListener(OnHexagonViewClickListener listener) {
this.listener = listener;
}
OnHexagonViewClickListener hexagonListener=new OnHexagonViewClickListener();//实例化
/**
*系统onTouchEvent
*/
public boolean onTouchEvent(MotionEvent event){
if(!isOn){//未点中六边形
break;
}
switch(event.getAction()){
case MotionEvent.ACTION_UP:
if(hexagonListener!=null){
hexagonListener.click(this);
}
break;
}
}

MainActivity->
public class MainActivity extends Activity implements HexagonView.OnHexagonViewClickListener{
HexagonView hexagonViewHello;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
hexagonViewHello=(HexagonView)this.findViewById(R.id.hexagonviewhello);
hexagonViewHello.setOnHexagonClickListener(this);
}
/**
* 事件监听
*/
public void onClick(View view){
Log.d(TAG,"onClick()");
switch (view.getId()){
case R.id.hexagonviewhello:
Toast.makeText(this,"Hello",Toast.LENGTH_SHORT).show();
break;
}
}

}
收起阅读 »

Kotlin协程的取消和异常传播机制

1.协程核心概念回顾结构化并发(Structured Concurrency)作用域(CoroutineScope /SupervisorScope)作业(Job/SupervisorJob)开启协程(launch/async)2.协程的取消2.1 协程的取消...
继续阅读 »

1.协程核心概念回顾

结构化并发(Structured Concurrency)

作用域(CoroutineScope /SupervisorScope)

作业(Job/SupervisorJob)

开启协程(launch/async)

2.协程的取消

2.1 协程的取消操作

Job生命周期

  • 作用域或作业的取消

示例代码

   suspend fun c01_cancle() {
val scope = CoroutineScope(Job())
val job1 = scope.launch { }
val job2 = scope.launch { }
//取消作业
job1.cancel()
job2.cancel()
//取消作用域
scope.cancel()

}

注意:不能在已取消的作用域中再开启协程

2.2确保协程可以被取消

  • 协程的取消只是标记了协程的取消状态,并未真正取消协程

示例代码:

  val job = launch(Dispatchers.Default) {
var i = 0
while (i < 5) {
println("Hello ${i++}")
Thread.sleep(100)
}
}
delay(200)
println("Cancel!")
job.cancel()

打印结果://未真正取消,直接检查

Hello 0
Hello 1
Hello 2
Cancel!
Hello 3
Hello 4

  • 可以用 isActive ensureActive() yield来在关键位置做检查,确保协程可以正常关闭
   val job = launch(Dispatchers.Default) {
var i = 0
while (i < 5 && isActive) {//方法1
ensureActive()//方法2
yield()//方法3
println("Hello ${i++}")
Thread.sleep(100)
}
}
delay(200)
println("Cancel!")
job.cancel()

2.3 协程取消后的资源关闭

  • try/finally可以关闭资源
 launch {
try {
openIo()//开启文件io
delay(100)
throw ArithmeticException()
} finally {
println("协程结束")
closeIo()//关闭文件io
}
}
  • 注意:finally中不能调用挂起函数(如果一定要调用,需要用withContext(NonCancellable),不推荐使用)
   launch {
try {
work()
} finally {
//withContext(NonCancellable)可以执行,不然不会再被执行
withContext(NonCancellable) {
delay(1000L) // 挂起方法
println("Cleanup done!")
}
}
}

2.4 CancellationException 会被忽略

  val job = launch {
try {
delay(Long.MAX_VALUE)
} catch (e: Exception) {
println("捕获到一个异常$e")
//打印:捕获到一个异常java.util.concurrent.CancellationException: 我是一个取消异常
}
}
yield()
job.cancel(CancellationException("我是一个取消异常"))
job.join()

3.协程的异常传播机制

3.1 捕捉协程异常

3.1.1 try/catch

  • try/catch业务代码
 launch {
try {
throw ArithmeticException("计算错误")
} catch (e: Exception) {
println("捕获到一个异常$e")
}
}
//打印:捕获到一个异常java.lang.ArithmeticException: 计算错误
  • try/catch协程
  try {
launch {
throw ArithmeticException("计算错误")
}
} catch (e: Exception) {
println("捕获到一个异常$e")
}

//无法捕捉到 error日志
Exception in thread "main" java.lang.ArithmeticException: 计算错误
at com.jinbo.kotlin.coroutine.C05_Exception$testDemo$2$1.invokeSuspend(C05_Exception.kt:65)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:56)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:274)
at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:84)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)
at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
at com.jinbo.kotlin.coroutine.C05_Exception.main(C05_Exception.kt:17)
  • 无法通过外部try-catch语句来捕获协程异常

3.1.2 CoroutineExceptionHandler 捕捉异常

  supervisorScope {
val exceptionHandler = CoroutineExceptionHandler { _, e ->
println("捕获到一个异常$e")
}
launch(exceptionHandler) {
throw ArithmeticException("计算错误")
}
}
//捕获到一个异常java.lang.ArithmeticException: 计算错误

3.1.3 runCatching 捕捉异常

  val catching = kotlin.runCatching {
"hello"
throw ArithmeticException("我是一个异常")
}
if (catching.isSuccess) {
println("正常结果是${catching.getOrNull()}")
} else {
println("失败了,原因是:${catching.exceptionOrNull()}")
}

这时,就要介绍协程的异常传播机制

3.2 协同作用域的传播机制

3.2.1 特性

  • 双向传播,取消子协程,取消自己,向父协程传播

[协同作用域传播特性] 示意图

  coroutineScope {
launch {
launch {
//子协程的异常,会向上传播
throw ArithmeticException() }
}
launch {
launch { }
}
}

3.2.2 子协程无法捕获自己的异常,只有父协程才可以


val scope = CoroutineScope(Job())
//父协程(根协程)才可以捕获异常
scope.launch(exceptionHandler) {
launch {
throw ArithmeticException("我是一个子异常")
}
//这时不会捕获到,会向上传播
// launch(exceptionHandler) {
// throw ArithmeticException("我是另外一个子异常")
// }
}

3.2.3 当父协程的所有子协程都结束后,原始的异常才会被父协程处理

val handler = CoroutineExceptionHandler { _, exception ->
println("捕捉到异常: $exception")
}
val job = GlobalScope.launch(handler) {
launch { // 第一个子协程
try {
delay(Long.MAX_VALUE)
} finally {
withContext(NonCancellable) {
println("第一个子协程还在运行,所以暂时不会处理异常")
delay(100)
println("现在子协程处理完成了")
}
}
}
launch { // 第二个子协程
delay(10)
println("第二个子协程出异常了")
throw ArithmeticException()
}
}
job.join()

//打印结棍:

第二个子协程出异常了
第一个子协程还在运行,所以暂时不会处理异常
现在子协程处理完成了
捕捉到异常: java.lang.ArithmeticException

3.2.4 异常聚合

第 1 个发生的异常会被优先y处理,在此之后发生的所有其他异常会被添加到最先发生的异常上, 作为被压制(suppressed)的异常

  val handler = CoroutineExceptionHandler { _, exception ->
println("捕捉到异常: $exception ${exception.suppressed.contentToString()}")
}
val job = GlobalScope.launch(handler) {
launch {
delay(100)
throw IOException() // 第一个异常
}
launch {
try {
delay(Long.MAX_VALUE) // 当另一个同级的协程因 IOException 失败时,它将被取消
} finally {
throw ArithmeticException() // 同时抛出第二个异常
}
}

delay(Long.MAX_VALUE)
}
job.join()

输出:
捕捉到异常: java.io.IOException [java.lang.ArithmeticException]

3.2.5 launch 和 async异常处理

  • launch 直接抛出异常,无等待
  launch {
throw ArithmeticException("launch异常")
}

//打印
Exception in thread "main" java.lang.ArithmeticException: launch异常
  • async预期会在用户调用await()时,再反馈异常

直接在根协程(GlobalScope) 或 supervisor子协程时,async会在await()时抛出异常

  supervisorScope {
val deferred = async {
throw ArithmeticException("异常")
}
}
//打印结果:空
  • 在await()时才抛出异常
  supervisorScope {
val deferred = async {
throw ArithmeticException("异常")
}
try {
deferred.await()
} catch (e: Exception) {
println("捕获到一个异常$e")
}
}
//打印结果:
捕获到一个异常java.lang.ArithmeticException: 异常
  • tips: 如果不是直接在根协程(GlobalScope) 或 supervisor子协程时,async 和 launch表现一致,直接抛出异常,不会在await()时,再抛出异常
  supervisorScope {
launch {
val deferred = async {
throw ArithmeticException("异常")
}
}
}

3.2.6 coroutineScope外部可以用try-catch捕获(supervisor不可以)

 try {
coroutineScope {
launch {
throw ArithmeticException("异常")
}
}
} catch (e: Exception) {
println("捕捉到异常:$e")
}

//打印结果:
捕捉到异常:java.lang.ArithmeticException: 异常

3.3 监督作用域的传播机制

3.3.1 特性 单向向下传播

  • 监督作用域的传播机制 (独立决策的权利?)

示意图

- supervisor的示例代码

3.3.2 子协程可以单独设置CoroutineExceptionHandler

 supervisorScope {
launch(exceptionHandler) {
throw ArithmeticException("异常出现了")
}
}

打印结果:
发现了异常java.lang.ArithmeticException: 异常出现了

3.3.3 监督作业只对它直接的子协程有用

  supervisorScope {
//监督作业只对它直接的子协程有用
launch(exceptionHandler) {
throw ArithmeticException("异常出现了")
}
}
-无效示例代码
supervisorScope {
launch {
//监督作业的子子协程无法独立处理异常,向上抛异常
launch(exceptionHandler) {
throw ArithmeticException("异常出现了")
}
}
}

//打印结果:
Exception in thread "main" java.lang.ArithmeticException: 异常出现了
at com.jinbo.kotlin.coroutine.C05_Exception$testDemo$2$1$1$1.invokeSuspend(C05_Exception.kt:1039)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:56)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:274)
at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:84)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)
at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
at com.jinbo.kotlin.coroutine.C05_Exception.main(C05_Exception.kt:17)

3.4 正确使用coroutineExceptionHandler

3.4.1 根协程(GlobalScope)//TODO 确认

  GlobalScope.launch(exceptionHandler) {  }

3.4.2 supervisorScope 直接子级

 supervisorScope {
launch(exceptionHandler) {
throw ArithmeticException("异常出现了")
}
}

3.4.3 手动创建的Scope(Job()/SupervisorJob())

 val scope = CoroutineScope(Job())
scope.launch(exceptionHandler) {
throw ArithmeticException("异常")
}

4 思考

4.1 android 的协同

  • viewmodelScope lifecycleScope

收起阅读 »

DiffUtil 让 RecyclerView 更好用

DiffUtil 让 RecyclerView 更好用前几天在写局部刷新RecyclerView时,评论区有掘友提到了DiffUtil,说实话,确实没有在项目中用到过,查了资料,DiffUtil帮我们做了很多刷新很多工作,真香。DiffUtil是什么DiffU...
继续阅读 »

DiffUtil 让 RecyclerView 更好用

前几天在写局部刷新RecyclerView时,评论区有掘友提到了DiffUtil,说实话,确实没有在项目中用到过,查了资料,DiffUtil帮我们做了很多刷新很多工作,真香。

DiffUtil是什么

DiffUtil 是来自recycleview-v7下的工具类,Diff 直接翻译过来是 差异、对比,所以这个工具类主要帮助我们对比两个数据集,寻找出最小的变化量。那么它和RecyclerView有什么关系呢,实际上我们只要把新旧数据集给到DiffUtil,那么它就会自动帮我们对比数据,并且刷新适配器,而不用我们判断,是增加了删除了等等,DiffUtil对比之后自动帮我们搞定,这就是它非常好用的地方了

常规的适配器

我们先用RecyclerView写一个常规的列表。
它拥有刷新item和item局部刷新的功能。
代码如下:

MainActivity: 主界面

public class MainActivity extends AppCompatActivity {

private RecyclerView recyclerView;

private List<PersonInfo> mDatas;
private PersonAdapter personAdapter;

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

recyclerView = findViewById(R.id.recyclerView);
initData();
recyclerView.setLayoutManager(new LinearLayoutManager(this));
personAdapter = new PersonAdapter(this, mDatas);
recyclerView.setAdapter(personAdapter);
}

private void initData() {
mDatas = new ArrayList<>();
mDatas.add(new PersonInfo(1, "姓名1"));
mDatas.add(new PersonInfo(2, "姓名2"));
mDatas.add(new PersonInfo(3, "姓名3"));
mDatas.add(new PersonInfo(4, "姓名4"));
mDatas.add(new PersonInfo(5, "姓名5"));
mDatas.add(new PersonInfo(6, "姓名6"));
mDatas.add(new PersonInfo(7, "姓名7"));
mDatas.add(new PersonInfo(8, "姓名8"));
mDatas.add(new PersonInfo(9, "姓名9"));
mDatas.add(new PersonInfo(10, "姓名10"));
mDatas.add(new PersonInfo(11, "姓名11"));
}

public void ADD(View view) {
int position = mDatas.size();
List<PersonInfo> tempData = new ArrayList<>();

tempData.add(new PersonInfo(12, "姓名12"));
tempData.add(new PersonInfo(13, "姓名13"));
tempData.add(new PersonInfo(14, "姓名114"));

mDatas.addAll(tempData);
personAdapter.notifyItemRangeInserted(position, tempData.size());
}

public void DELETE(View view) {
mDatas.remove(1);
personAdapter.notifyItemRemoved(1);
}

public void UPDATE(View view) {
mDatas.get(1).setName("姓名:我被更新了");
personAdapter.notifyItemChanged(1);
}

public void UPDATE2(View view) {
mDatas.get(1).setName("姓名:我被更新了");

Bundle payload = new Bundle();
payload.putString("KEY_NAME", mDatas.get(1).getName());
personAdapter.notifyItemChanged(1, payload);
}
}
复制代码

PersonAdapter: 适配器

public class PersonAdapter extends RecyclerView.Adapter<PersonAdapter.DiffVH> {
private List<PersonInfo> mDatas;
private LayoutInflater mInflater;

public PersonAdapter(Context context, List<PersonInfo> mDatas) {
this.mDatas = mDatas;
mInflater = LayoutInflater.from(context);
}

public void setDatas(List<PersonInfo> mDatas) {
this.mDatas = mDatas;
}

@Override
public DiffVH onCreateViewHolder(ViewGroup parent, int viewType) {
return new DiffVH(mInflater.inflate(R.layout.item_person, parent, false));
}

@Override
public void onBindViewHolder(final DiffVH holder, final int position) {
PersonInfo personInfo = mDatas.get(position);
holder.tv_index.setText(String.valueOf(personInfo.getIndex()));
holder.tv_name.setText(String.valueOf(personInfo.getName()));
}

@Override
public void onBindViewHolder(DiffVH holder, int position, List<Object> payloads) {
if (payloads.isEmpty()) {
onBindViewHolder(holder, position);
} else {
Bundle payload = (Bundle) payloads.get(0);
PersonInfo bean = mDatas.get(position);
for (String key : payload.keySet()) {
switch (key) {
case "KEY_INDEX":
holder.tv_index.setText(String.valueOf(bean.getIndex()));
break;
case "KEY_NAME":
holder.tv_name.setText(String.valueOf(bean.getName()));
break;
default:
break;
}
}
}
}

@Override
public int getItemCount() {
return mDatas != null ? mDatas.size() : 0;
}

class DiffVH extends RecyclerView.ViewHolder {
TextView tv_index;
TextView tv_name;

public DiffVH(View view) {
super(view);
tv_index = view.findViewById(R.id.tv_index);
tv_name = view.findViewById(R.id.tv_name);
}
}
}

复制代码

PersonInfo: 实体类

public class PersonInfo {
private int index;
private String name;

public PersonInfo(int index, String name) {
this.index = index;
this.name = name;
}

public int getIndex() {
return index;
}

public void setIndex(int index) {
this.index = index;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}
复制代码

activity_main

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

<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">

<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="增加"
android:onClick="ADD"/>

<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="删除"
android:onClick="DELETE"/>

<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="修改"
android:onClick="UPDATE"/>

<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="局部更新"
android:onClick="UPDATE2"/>

</LinearLayout>


<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />


</LinearLayout>

复制代码

item_person

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

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:background="@color/purple_200"
android:orientation="horizontal"
android:padding="5dp">

<TextView
android:id="@+id/tv_index"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true" />

<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toEndOf="@+id/tv_index" />

</RelativeLayout>


</LinearLayout>
复制代码

引入DiffUtil

我们创建一个DiffUtil类,在需要更新的时候,用DiffUtil中的方法去代替原本的刷新方法。

用新增举例,这样就能达到更新的目的

public void ADD(View view) {
List<PersonInfo> newData = new ArrayList<>();
newData.addAll(mDatas);
newData.add(new PersonInfo(12, "姓名12"));
newData.add(new PersonInfo(13, "姓名13"));
newData.add(new PersonInfo(14, "姓名114"));

DiffUtil.calculateDiff(new DiffUtilCallBack(newData,mDatas), true).dispatchUpdatesTo(personAdapter);
mDatas = newData;
personAdapter.setDatas(mDatas);
}

复制代码

DiffUtilCallBack

public class DiffUtilCallBack extends DiffUtil.Callback {
private List<PersonInfo> newlist;
private List<PersonInfo> oldlist;

public DiffUtilCallBack(List<PersonInfo> newlist, List<PersonInfo> oldlist) {
this.newlist = newlist;
this.oldlist = oldlist;
}

@Override
public int getOldListSize() {
return oldlist.size();
}

@Override
public int getNewListSize() {
return newlist.size();
}

@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
//判断是否是同一个item,可以在这里处理 判断是否是相同item的逻辑,比如id之类的
return newlist.get(newItemPosition).getIndex() == oldlist.get(oldItemPosition).getIndex();
}

@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
//判断数据是否发生改变,这个 方法会在上面的方法返回true时调用, 因为虽然item是同一个,但有可能item的数据发生了改变
return newlist.get(newItemPosition).getName().equals(oldlist.get(oldItemPosition).getName());
}
}
复制代码

万能适配器中的DiffUtil

配合RecyclerView,我一直在使用万能适配器(BaseRecyclerViewAdapterHelper),如果你也习惯了使用万能适配器,在它的3.0方法中引入了对DiffUtil的支持。

直接看官方文档吧。

代码下载:https://github.com/CymChad/BaseRecyclerViewAdapterHelper/archive/refs/heads/master.zip

收起阅读 »

iOS 上的 WebSocket 框架 Starscream

iOS
Starscream实现Websocket通讯1.Starscream 简介2.Starscream 使用2.1 Starscream基本使用2.2 Starscream高阶使用2.2.1 判断是否连接2.2.2 自定义头文件2.2.3 自定义HTTP方法2....
继续阅读 »

Starscream实现Websocket通讯
1.Starscream 简介
2.Starscream 使用
2.1 Starscream基本使用
2.2 Starscream高阶使用
2.2.1 判断是否连接
2.2.2 自定义头文件
2.2.3 自定义HTTP方法
2.2.4 协议
2.2.5 自签名 SSL
2.2.5.1 SSL引脚
2.2.5.2 SSL密码套件
2.2.6 压缩扩展
2.2.7 自定义队列
2.2.8 高级代理
3.Starscream 使用Demo
1.Starscream 简介

Starscream的特征:

Conforms to all of the base Autobahn test suite.
Nonblocking. Everything happens in the background, thanks to GCD.
TLS/WSS support.
Compression Extensions support (RFC 7692)
Simple concise codebase at just a few hundred LOC.
什么是websocket:
WebSocket protocol 是HTML5一种新的协议。它实现了浏览器与服务器全双工通信(full-duplex)。
在 WebSocket API,浏览器和服务器只需要要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。

HTTP 第一次出现是 1991 年,它设计为一种请求/响应式的通讯机制。Web 浏览器用这种机制工作良好,用户请求 web 页,服务器返回内容。但某些时候,需要有新数据时不经过用户请求就通知用户——也就是,服务器推。
HTTP 协议无法很好地解决推模型。在 websocket 出现前,web 服务通过一系列浏览器刷新机制来实现推模型,但效率无法让人满意。
webSocket 实现了服务端推机制。新的 web 浏览器全都支持 WebSocket,这使得它的使用超级简单。通过 WebSocket 能够打开持久连接,大部分网络都能轻松处理 WebSocket 连接。
WebSocket 通常应用在某些数据经常性或频繁改变的场景。例如 Facebook 中的 web 通知、Slack 中的实时聊天、交易系统中的变化的股票价格
socket通讯过程:


集成Websocket:

开发中推荐使用Starscream框架。通过pod 方式导入:

pod 'Starscream'
1
Starscream 使用swift版本为4.2
2.Starscream 使用
2.1 Starscream基本使用
import UIKit
import Starscream
@objc public protocol DSWebSocketDelegate: NSObjectProtocol{
/**websocket 连接成功*/
optional func websocketDidConnect(sock: DSWebSocket)
/**websocket 连接失败*/
optional func websocketDidDisconnect(socket: DSWebSocket, error: NSError?)
/**websocket 接受文字信息*/
func websocketDidReceiveMessage(socket: DSWebSocket, text: String)
/ **websocket 接受二进制信息*/
optional func websocketDidReceiveData(socket: DSWebSocket, data: NSData)
}
public class DSWebSocket: NSObject,WebSocketDelegate {
var socket:WebSocket!
weak var webSocketDelegate: DSWebSocketDelegate?
//单例
class func sharedInstance() -> DSWebSocket
{
return manger
}
static let manger: DSWebSocket = {
return DSWebSocket()
}()

//MARK:- 链接服务器
func connectSever(){
socket = WebSocket(url: NSURL(string: 你的URL网址如:ws://192.168.3.209:8080/shop))
socket.delegate = self
socket.connect()
}

//发送文字消息
func sendBrandStr(brandID:String){
socket.writeString(brandID))
}
//MARK:- 关闭消息
func disconnect(){
socket.disconnect()
}

//MARK: - WebSocketDelegate
//客户端连接到服务器时,websocketDidConnect将被调用。
public func websocketDidConnect(socket: WebSocket){
debugPrint("连接成功了: \(error?.localizedDescription)")
webSocketDelegate?.websocketDidConnect!(self)
}
//客户端与服务器断开连接后,将立即调用 websocketDidDisconnect。
public func websocketDidDisconnect(socket: WebSocket, error: NSError?){
debugPrint("连接失败了: \(error?.localizedDescription)")
webSocketDelegate?.websocketDidDisconnect!(self, error: error)
}
//当客户端从连接获取一个文本框时,调用 websocketDidReceiveMessage。
//注:一般返回的都是字符串
public func websocketDidReceiveMessage(socket: WebSocket, text: String){
debugPrint("接受到消息了: \(error?.localizedDescription)")
webSocketDelegate?.websocketDidReceiveMessage!(self, text: text)
}
public func websocketDidReceiveData(socket: WebSocket, data: NSData){
debugPrint("data数据")
webSocketDelegate?.websocketDidReceiveData!(self, data: data)
}
}

编写一个pong框架
writePong方法与writePing相同,但发送一个pong控制帧。
socket.write(pong: Data()) //example on how to write a pong control frame over the socket!
1
Starscream会自动响应传入的ping 控制帧,这样你就不需要手动发送 pong。

但是,如果出于某些原因需要控制这个 prosses,你可以通过禁用 respondToPingWithPong 来关闭自动 ping 响应。

socket.respondToPingWithPong=false//Do not automaticaly respond to incoming pings with pongs.
1
当客户端从连接获得一个pong响应时,调用 websocketDidReceivePong。 你需要实现WebSocketPongDelegate协议并设置一个额外的委托,例如: socket.pongDelegate = self

funcwebsocketDidReceivePong(socket: WebSocketClient, data: Data?) {
print("Got pong! Maybe some data: (data?.count)")
}

2.2 Starscream高阶使用
2.2.1 判断是否连接
if socket.isConnected {
// do cool stuff.
}

2.2.2 自定义头文件
你可以使用自己自定义的web socket标头覆盖默认的web socket标头,如下所示:
var request = URLRequest(url: URL(string: "ws://localhost:8080/")!)
request.timeoutInterval = 5
request.setValue("someother protocols", forHTTPHeaderField: "Sec-WebSocket-Protocol")
request.setValue("14", forHTTPHeaderField: "Sec-WebSocket-Version")
request.setValue("Everything is Awesome!", forHTTPHeaderField: "My-Awesome-Header")
let socket = WebSocket(request: request)

2.2.3 自定义HTTP方法
你的服务器在连接到 web socket时可能会使用不同的HTTP方法:
var request = URLRequest(url: URL(string: "ws://localhost:8080/")!)
request.httpMethod = "POST"
request.timeoutInterval = 5
let socket = WebSocket(request: request)

2.2.4 协议
如果需要指定协议,简单地将它的添加到 init:
//chat and superchat are the example protocols here
socket = WebSocket(url: URL(string: "ws://localhost:8080/")!, protocols: ["chat","superchat"])
socket.delegate = self
socket.connect()

2.2.5 自签名 SSL
socket = WebSocket(url: URL(string: "ws://localhost:8080/")!, protocols: ["chat","superchat"])

//set this if you want to ignore SSL cert validation, so a self signed SSL certificate can be used.
socket.disableSSLCertValidation = true

2.2.5.1 SSL引脚
Starscream还支持SSL固定。
socket = WebSocket(url: URL(string: "ws://localhost:8080/")!, protocols: ["chat","superchat"])
let data = ... //load your certificate from disk
socket.security = SSLSecurity(certs: [SSLCert(data: data)], usePublicKeys: true)
//socket.security = SSLSecurity() //uses the .cer files in your app's bundle

你可以加载证书的Data 小区,否则你可以使用 SecKeyRef,如果你想要使用 public 键。 usePublicKeys bool是使用证书进行验证还是使用 public 键。 如果选择 usePublicKeys,将自动从证书中提取 public 密钥。

2.2.5.2 SSL密码套件
要使用SSL加密连接,你需要告诉小红你的服务器支持的密码套件。
socket = WebSocket(url: URL(string: "wss://localhost:8080/")!, protocols: ["chat","superchat"])

// Set enabled cipher suites to AES 256 and AES 128
socket.enabledSSLCipherSuites = [TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256]

如果你不知道服务器支持哪些密码套件可以查看:SSL Labs

2.2.6 压缩扩展
Starscream支持压缩扩展( RFC 7692 )。 默认情况下,压缩是启用的,但是只有当服务器支持压缩时才会使用压缩。 你可以通过 .enableCompression 属性启用或者禁用压缩:
socket = WebSocket(url: URL(string: "ws://localhost:8080/")!)
socket.enableCompression = false

如果应用程序正在传输已经压缩。随机或者其他uncompressable数据,则应禁用压缩。
2.2.7 自定义队列
调用委托方法时可以指定自定义队列。 默认使用 DispatchQueue.main,因此使所有委托方法调用都在主线程上运行。 重要的是要注意,所有 web socket处理都是在后台线程上完成的,只有修改队列时才更改委托方法。 实际的处理总是在后台线程上,不会暂停你的应用程序。
socket = WebSocket(url: URL(string: "ws://localhost:8080/")!, protocols: ["chat","superchat"])
//create a custom queue
socket.callbackQueue = DispatchQueue(label: "com.vluxe.starscream.myapp")

2.2.8 高级代理
socket.advancedDelegate = self

websocketDidReceiveMessage
func websocketDidReceiveMessage(socket: WebSocketClient, text: String, response: WebSocket.WSResponse) {
print("got some text: \(text)")
print("First frame for this message arrived on \(response.firstFrame)")
}

websocketDidReceiveData
func websocketDidReceiveData(socket: WebSocketClient, data: Date, response: WebSocket.WSResponse) {
print("got some data it long: \(data.count)")
print("A total of \(response.frameCount) frames were used to send this data")
}

websocketHttpUpgrade
当发送HTTP升级请求后,会返回下面回调

func websocketHttpUpgrade(socket: WebSocketClient, request: CFHTTPMessage) {
print("the http request was sent we can check the raw http if we need to")
}

func websocketHttpUpgrade(socket: WebSocketClient, response: CFHTTPMessage) {
print("the http response has returned.")
————————————————

原文链接:https://blog.csdn.net/kyl282889543/article/details/100655005

收起阅读 »

iOS 15-适配要点

iOS
增加UISheetPresentationController,通过它可以控制 Modal 出来的 UIViewController 的显示大小,且可以通过拖拽手势在不同大小之间进行切换。只需要在跳转的目标 UIViewController 做如下处理:if ...
继续阅读 »

  1. 增加UISheetPresentationController,通过它可以控制 Modal 出来的 UIViewController 的显示大小,且可以通过拖拽手势在不同大小之间进行切换。只需要在跳转的目标 UIViewController 做如下处理:

    if let presentationController = presentationController as? UISheetPresentationController {
    // 显示时支持的尺寸
    presentationController.detents = [.medium(), .large()]
    // 显示一个指示器表示可以拖拽调整大小
    presentationController.prefersGrabberVisible = true
    }
  2. UIButton支持更多配置。UIButton.Configuration是一个新的结构体,它指定按钮及其内容的外观和行为。它有许多与按钮外观和内容相关的属性,如cornerStyle、baseForegroundColor、baseBackgroundColor、buttonSize、title、image、subtitle、titlePadding、imagePadding、contentInsets、imagePlacement等。

    // Plain
    let plain = UIButton(configuration: .plain(), primaryAction: nil)
    plain.setTitle("Plain", for: .normal)
    // Gray
    let gray = UIButton(configuration: .gray(), primaryAction: nil)
    gray.setTitle("Gray", for: .normal)
    // Tinted
    let tinted = UIButton(configuration: .tinted(), primaryAction: nil)
    tinted.setTitle("Tinted", for: .normal)
    // Filled
    let filled = UIButton(configuration: .filled(), primaryAction: nil)
    filled.setTitle("Filled", for: .normal)

    Snipaste_2021-07-11_15-26-55.png16259886795922.png

  3. 推出CLLocationButton用于一次性定位授权,该内容内置于CoreLocationUI模块,但如果需要获取定位的详细信息仍然需要借助于CoreLocation

    let locationButton = CLLocationButton()
    // 文字
    locationButton.label = .currentLocation
    locationButton.fontSize = 20
    // 图标
    locationButton.icon = .arrowFilled
    // 圆角
    locationButton.cornerRadius = 10
    // tint
    locationButton.tintColor = UIColor.systemPink
    // 背景色
    locationButton.backgroundColor = UIColor.systemGreen
    // 点击事件,应该在在其中发起定位请求
    locationButton.addTarget(self, action: #selector(getCurrentLocation), for: .touchUpInside)
  4. URLSession 推出支持 async/await 的 API,包括获取数据、上传与下载。

    // 加载数据
    let (data, response) = try await URLSession.shared.data(from: url)
    // 下载
    let (localURL, _) = try await session.download(from: url)
    // 上传
    let (_, response) = try await session.upload(for: request, from: data)

  5. 系统图片支持多个层,支持多种渲染模式。

    // hierarchicalColor:多层渲染,透明度不同
    let config = UIImage.SymbolConfiguration(hierarchicalColor: .systemRed)
    let image = UIImage(systemName: "square.stack.3d.down.right.fill", withConfiguration: config)
    // paletteColors:多层渲染,设置不同风格
    let config2 = UIImage.SymbolConfiguration(paletteColors: [.systemRed, .systemGreen, .systemBlue])
    let image2 = UIImage(systemName: "person.3.sequence.fill", withConfiguration: config2)
  6. UINavigationBar、UIToolbar 和 UITabBar 设置颜色,需要使用 UIBarAppearance APIs。

    // UINavigationBar
    let navigationBarAppearance = UINavigationBarAppearance()
    navigationBarAppearance.backgroundColor = .red
    navigationController?.navigationBar.scrollEdgeAppearance = navigationBarAppearance
    navigationController?.navigationBar.standardAppearance = navigationBarAppearance
    // UIToolbar
    let toolBarAppearance = UIToolbarAppearance()
    toolBarAppearance.backgroundColor = .blue
    navigationController?.toolbar.scrollEdgeAppearance = toolBarAppearance
    navigationController?.toolbar.standardAppearance = toolBarAppearance
    // UITabBar
    let tabBarAppearance = UITabBarAppearance()
    toolBarAppearance.backgroundColor = .purple
    tabBarController?.tabBar.scrollEdgeAppearance = tabBarAppearance
    tabBarController?.tabBar.standardAppearance = tabBarAppearance
  7. UITableView 新增了属性 sectionHeaderTopPadding,会给每一个section 的 header 增加一个默认高度。

    tableView.sectionHeaderTopPadding = 0
  8. UIImage 新增了几个调整尺寸的方法。

    // preparingThumbnail
    UIImage(named: "sv.png")?.preparingThumbnail(of: CGSize(width: 200, height: 100))
    // prepareThumbnail,闭包中直接获取调整后的UIImage
    UIImage(named: "sv.png")?.prepareThumbnail(of: CGSize(width: 200, height: 100)) { image in
    // 需要回到主线程更新UI
    }
    // byPreparingThumbnail
    await UIImage(named: "sv.png")?.byPreparingThumbnail(ofSize: CGSize(width: 100, height: 100))

文中代码已在 Xcode 13 Beta3 中测试通过, 案例源代码下载地址

收起阅读 »

iOS Runtime (四)Runtime的消息机制

iOS
引言 iOS的消息转发机制,在我们开发中有时候忘记实现某个声明的方法,从而在运行过程中调用该方法出现崩溃, 当然这类问题是可以解决的,在当前对象或者父类对象中添加对象的方法实现,再重新运行,调用该方法就能解决这个问题,又或者在我们运行的时候动态的去添加接收者中...
继续阅读 »

引言


iOS的消息转发机制,在我们开发中有时候忘记实现某个声明的方法,从而在运行过程中调用该方法出现崩溃,


当然这类问题是可以解决的,在当前对象或者父类对象中添加对象的方法实现,再重新运行,调用该方法就能解决这个问题,又或者在我们运行的时候动态的去添加接收者中未知方法实现,这就是这篇重点要学习的内容。


错误异常实例


创建Game对象代码

#import <Foundation/Foundation.h>
@interface Game : NSObject
- (void)Play;
- (void)DoThings:(NSString *)Str Num:(NSInteger)num;
@end

#import "Game.h"
@implementation Game
- (void)Play{
NSLog(@"the game is play");
}
@end

调用该对象中没有实现的方法 DoThings: Num:


Game *game = [[Game alloc]init];
[game DoThings:@"wodenidetade" Num:10];

当我们运行的时候会报当调用的对象的方法不存在,即便是消息转发过后还是不存在的时候,就会抛出这个异常


解决方案


第一种方式

遇到这种情况通常第一个想法就是在该对象或继承树中的实现文件中添加该方法并实现,这种形式,就是你必须要去实现方法,需要开发者主动去写代码。


第二种方式

消息转发在运行时:


1.动态方法解析

+(BOOL)resolveInstanceMethod:(SEL)sel 实例方法解析
+(BOOL)resolveClassMethod:(SEL)sel 类方法解析

当运用消息转发运行时,根据调用的方法类型调用这两个方法其中一个,返回值BOOL类型,告诉系统该消息是否被处理,YES处理 NO 未处理



  • resolveInstanceMethod实例方法调用

  • resolveClassMethod类方法调用


这样的作用是: 当接受者接受到的消息方法并没有找到的情况下,系统会调用该函数,给予这个对象一次动态添加该消息方法实现的机会,如果该对象动态的添加了这个方法的实现,就返回YES,告诉系统这个消息我已经处理完毕。再次运行该方法。


注意:

当这个对象在实现了resolveInstanceMethod,resolveClassMethod两个方法,并没有对该对象消息进行处理,那么该方法会被调用两次:


一次是没有找到该方法需要对象解析处理;第二次是告诉系统我处理完成需要再次调用该方法但实际上并没有处理完成,所以会调用第二次该方法崩溃


2.后备接收者对象

-(id)forwardingTargetForSelector:(SEL)aSelector

在消息转发第一次方法解析中没有处理方法,并告诉系统本对象无法处理,需另寻办法,那么系统会给予另外一个办法,就是让别的对象B来处理该问题,如果对象B能够处理该消息,那么该消息转发结束。


将未知SEL作为参数传入,寻找另外对象处理,如果可以处理,返回该对象


3.以其他形式实现该消息方法

-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
-(void)forwardInvocation:(NSInvocation *)anInvocation

当我们在前面两个步骤都没有处理该未知SEL时,就会到第三个步骤,上述两个方法是最后寻找IML的机会



  • 将未知SEL作为参数传入methodSignatureForSelector,在该方法中处理该消息,一旦能够处理,返回方法签名(自由的修改方法签名,apple签名),让后续forwardInvocation来进行处理



  • forwardInvocation中我们可以做很多的操作,这个方法比forwardingTargetForSelector更灵活



    • 也可以做到像forwardingTargetForSelector一样的结果,不同的是一个是让别的对象去处理,后者是直接切换调用目标,也就是该方法的Target

    • 我们也可以修改该方法的SEL,就是重新替换一个新的SEL

    • ....




4.直到最后未处理,抛出异常

-(void)doesNotRecognizeSelector:(SEL)aSelector

作为找不到函数实现的最后一步,NSObject实现这个函数只有一个功能,就是抛出异常。


虽然理论上可以重载这个函数实现保证不抛出异常(不调用super实现),但是苹果文档着重提出“一定不能让这个函数就这么结束掉,必须抛出异常”。


流程图


代码实现

/*
* 第一步 实例方法专用 方法解析
**/
+ (BOOL)resolveInstanceMethod:(SEL)sel{

NSLog(@"%@",NSStringFromSelector(sel));

if(sel == @selector(DoThings:Num:)){
class_addMethod([self class], sel, (IMP)MyMethodIMP, "v@:");
return YES;
}

return [super resolveInstanceMethod:sel];
}

/*
* 第二步 如果第一步未处理,那么让别的对象去处理这个方法
**/
-(id)forwardingTargetForSelector:(SEL)aSelector{
if([NSStringFromSelector(aSelector) isEqualToString:@"DoThings:Num:"]){
return [[Tools alloc]init];
}
return [super forwardingTargetForSelector:aSelector];
}

/*
* 第三步 如果前两步未处理,这是最后处理的机会将目标函数以其他形式执行
**/
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSString *SelStr = NSStringFromSelector(aSelector);
if([SelStr isEqualToString:@"DoThings:Num:"]){
[NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}

-(void)forwardInvocation:(NSInvocation *)anInvocation{
//改变消息接受者对象
[anInvocation invokeWithTarget:[[Tools alloc]init]];

//改变消息的SEL
anInvocation.selector = @selector(flyGame);
[anInvocation invokeWithTarget:self];
}

- (void)flyGame{
NSLog(@"我要飞翔追逐梦想!");
}

/*
* 作为找不到函数实现的最后一步,NSObject实现这个函数只有一个功能,就是抛出异常。
* 虽然理论上可以重载这个函数实现保证不抛出异常(不调用super实现),但是苹果文档着重提出“一定不能让这个函数就这么结束掉,必须抛出异常”。
*
***/
- (void)doesNotRecognizeSelector:(SEL)aSelector{

}


作者:响彻天堂
链接:https://www.jianshu.com/p/019bce1e6253

上一篇链接:https://www.imgeek.org/article/825358865

收起阅读 »

Java正则表达式语法大全

在我们日常开发项目中经常用到正则表达式/比如邮箱/电话手机号/域名/ip等)都会经常用到其实一个字符串就是一个简单的正则表达式,例如 Hello World 正则表达式匹配 "Hello World" 字符串。.(点号)也是一个正则表达式,...
继续阅读 »

在我们日常开发项目中经常用到正则表达式/比如邮箱/电话手机号/域名/ip等)都会经常用到

其实一个字符串就是一个简单的正则表达式,例如 Hello World 正则表达式匹配 "Hello World" 字符串。

.(点号)也是一个正则表达式,它匹配任何一个字符如:"a" 或 "1"。

下表列出了一些正则表达式的实例及描述:

正则表达式描述
this is text匹配字符串 "this is text"
this\s+is\s+text注意字符串中的 \s+ 。匹配单词 "this" 后面的 \s+  可以匹配多个空格,之后匹配 is 字符串,再之后 \s+  匹配多个空格然后再跟上 text 字符串。可以匹配这个实例:this is text
^\d+(.\d+)?^ 定义了以什么开始\d+ 匹配一个或多个数字? 设置括号内的选项是可选的. 匹配 "."可以匹配的实例:"5", "1.5" 和 "2.21"。

java 正则表达式和 Perl 的是最为相似的。

java.util.regex 包主要包括以下三个类:

  • Pattern 类:

    pattern 对象是一个正则表达式的编译表示。Pattern 类没有公共构造方法。要创建一个 Pattern 对象,你必须首先调用其公共静态编译方法,它返回一个 Pattern 对象。该方法接受一个正则表达式作为它的第一个参数。

  • Matcher 类:

    Matcher 对象是对输入字符串进行解释和匹配操作的引擎。与Pattern 类一样,Matcher 也没有公共构造方法。你需要调用 Pattern 对象的 matcher 方法来获得一个 Matcher 对象。

  • PatternSyntaxException:

    PatternSyntaxException 是一个非强制异常类,它表示一个正则表达式模式中的语法错误。 以下实例中使用了正则表达式  .runoob.  用于查找字符串中是否包了 runoob 子串:

import java.util.regex.*;
class RegexExample1{
public static void main(String[] args){
String content = "I am noob " + "from runoob.com.";
String pattern = ".*runoob.*";
boolean isMatch = Pattern.matches(pattern, content);
System.out.println("字符串中是否包含了 'runoob' 子字符串? " + isMatch);
}
}

最终打印字符串中是否包含了 'runoob' 子字符串? true

正则表达式语法大全

在其他语言中,\ 表示:我想要在正则表达式中插入一个普通的(字面上的)反斜杠,请不要给它任何特殊的意义。

在 Java 中,\ 表示:我要插入一个正则表达式的反斜线,所以其后的字符具有特殊的意义。 所以,在其他的语言中(如 Perl),一个反斜杠 \ 就足以具有转义的作用,而在 Java 中正则表达式中则需要有两个反斜杠才能被解析为其他语言中的转义作用。也可以简单的理解在 Java 的正则表达式中,两个 \ 代表其他语言中的一个 \,这也就是为什么表示一位数字的正则表达式是 \d,而表示一个普通的反斜杠是 \。

System.out.print("\");    // 输出为 \
System.out.print("\\"); // 输出为 \
字符说明
\将下一字符标记为特殊字符、文本、反向引用或八进制转义符。例如, n匹配字符 n。\n 匹配换行符。序列 \\ 匹配 \ ,\( 匹配 (。
^匹配输入字符串开始的位置。如果设置了 RegExp 对象的 Multiline 属性,^ 还会与"\n"或"\r"之后的位置匹配。
$匹配输入字符串结尾的位置。如果设置了 RegExp 对象的 Multiline 属性,$ 还会与"\n"或"\r"之前的位置匹配。
*零次或多次匹配前面的字符或子表达式。例如,zo* 匹配"z"和"zoo"。* 等效于 {0,}。
+一次或多次匹配前面的字符或子表达式。例如,"zo+"与"zo"和"zoo"匹配,但与"z"不匹配。+ 等效于 {1,}。
?零次或一次匹配前面的字符或子表达式。例如,"do(es)?"匹配"do"或"does"中的"do"。? 等效于 {0,1}。
{n}n 是非负整数。正好匹配 n 次。例如,"o{2}"与"Bob"中的"o"不匹配,但与"food"中的两个"o"匹配。
{n,}n 是非负整数。至少匹配 n 次。例如,"o{2,}"不匹配"Bob"中的"o",而匹配"foooood"中的所有 o。"o{1,}"等效于"o+"。"o{0,}"等效于"o*"。
{n,m}m 和 n 是非负整数,其中 n <= m。匹配至少 n 次,至多 m 次。例如,"o{1,3}"匹配"fooooood"中的头三个 o。'o{0,1}' 等效于 'o?'。注意:您不能将空格插入逗号和数字之间。
?当此字符紧随任何其他限定符(*、+、?、{n}、{n,}、{n,m})之后时,匹配模式是"非贪心的"。"非贪心的"模式匹配搜索到的、尽可能短的字符串,而默认的"贪心的"模式匹配搜索到的、尽可能长的字符串。例如,在字符串"oooo"中,"o+?"只匹配单个"o",而"o+"匹配所有"o"。
.匹配除"\r\n"之外的任何单个字符。若要匹配包括"\r\n"在内的任意字符,请使用诸如"[\s\S]"之类的模式。
(pattern)匹配 pattern 并捕获该匹配的子表达式。可以使用  0…9 属性从结果"匹配"集合中检索捕获的匹配。若要匹配括号字符 ( ),请使用"("或者")"。
(?:pattern)匹配 pattern 但不捕获该匹配的子表达式,即它是一个非捕获匹配,不存储供以后使用的匹配。这对于用"or"字符 (
(?=pattern)执行正向预测先行搜索的子表达式,该表达式匹配处于匹配 pattern 的字符串的起始点的字符串。它是一个非捕获匹配,即不能捕获供以后使用的匹配。例如,'Windows (?=95
(?!pattern)执行反向预测先行搜索的子表达式,该表达式匹配不处于匹配 pattern 的字符串的起始点的搜索字符串。它是一个非捕获匹配,即不能捕获供以后使用的匹配。例如,'Windows (?!95
xy
[xyz]字符集。匹配包含的任一字符。例如,"[abc]"匹配"plain"中的"a"。
[^xyz]反向字符集。匹配未包含的任何字符。例如,"[^abc]"匹配"plain"中"p","l","i","n"。
[a-z]字符范围。匹配指定范围内的任何字符。例如,"[a-z]"匹配"a"到"z"范围内的任何小写字母。
[^a-z]反向范围字符。匹配不在指定的范围内的任何字符。例如,"[^a-z]"匹配任何不在"a"到"z"范围内的任何字符。
\b匹配一个字边界,即字与空格间的位置。例如,"er\b"匹配"never"中的"er",但不匹配"verb"中的"er"。
\B非字边界匹配。"er\B"匹配"verb"中的"er",但不匹配"never"中的"er"。
\cx匹配 x 指示的控制字符。例如,\cM 匹配 Control-M 或回车符。x 的值必须在 A-Z 或 a-z 之间。如果不是这样,则假定 c 就是"c"字符本身。
\d数字字符匹配。等效于 [0-9]。
\D非数字字符匹配。等效于 [^0-9]。
\f换页符匹配。等效于 \x0c 和 \cL。
\n换行符匹配。等效于 \x0a 和 \cJ。
\r匹配一个回车符。等效于 \x0d 和 \cM。
\s匹配任何空白字符,包括空格、制表符、换页符等。与 [ \f\n\r\t\v] 等效。
\S匹配任何非空白字符。与 [^ \f\n\r\t\v] 等效。
\t制表符匹配。与 \x09 和 \cI 等效。
\v垂直制表符匹配。与 \x0b 和 \cK 等效。
\w匹配任何字类字符,包括下划线。与"[A-Za-z0-9_]"等效。
\W与任何非单词字符匹配。与"[^A-Za-z0-9_]"等效。
\xn匹配 n,此处的 n 是一个十六进制转义码。十六进制转义码必须正好是两位数长。例如,"\x41"匹配"A"。"\x041"与"\x04"&"1"等效。允许在正则表达式中使用 ASCII 代码。
*num*匹配 num,此处的 num 是一个正整数。到捕获匹配的反向引用。例如,"(.)\1"匹配两个连续的相同字符。
*n*标识一个八进制转义码或反向引用。如果 *n* 前面至少有 n 个捕获子表达式,那么 n 是反向引用。否则,如果 n 是八进制数 (0-7),那么 n 是八进制转义码。
*nm*标识一个八进制转义码或反向引用。如果 *nm* 前面至少有 nm 个捕获子表达式,那么 nm 是反向引用。如果 *nm* 前面至少有 n 个捕获,则 n 是反向引用,后面跟有字符 m。如果两种前面的情况都不存在,则 *nm* 匹配八进制值 nm,其中 n 和 m 是八进制数字 (0-7)。
\nml当 n 是八进制数 (0-3),m 和 l 是八进制数 (0-7) 时,匹配八进制转义码 nml。
\un匹配 n,其中 n 是以四位十六进制数表示的 Unicode 字符。例如,\u00A9 匹配版权符号 (©)。

注意:根据 Java Language Specification 的要求,Java 源代码的字符串中的反斜线被解释为 Unicode 转义或其他字符转义。因此必须在字符串字面值中使用两个反斜线,表示正则表达式受到保护,不被 Java 字节码编译器解释。例如,当解释为正则表达式时,字符串字面值 "\b" 与单个退格字符匹配,而 "\b" 与单词边界匹配。字符串字面值 "(hello)" 是非法的,将导致编译时错误;要与字符串 (hello) 匹配,必须使用字符串字面值 "\(hello\)"。


作者:java李杨勇
链接:https://juejin.cn/post/7020303920966664222
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

熬夜再战Android之修炼Kotlin-【Get和Set】、【继承】、【抽象类/嵌套类/内部类】篇

前提 当前环境 2021年10月8日最新下载2020.3.1 Patch 2 版本 👉实践过程 😜Get和Set 其实Kotlin声明实体类之后,里面的变量默认就带有set和get的属性功能了。除非想要特殊业务内容。 比如set需要结合项目进行其他业务处理,g...
继续阅读 »

前提


当前环境


2021年10月8日最新下载2020.3.1 Patch 2 版本


👉实践过程


😜Get和Set


其实Kotlin声明实体类之后,里面的变量默认就带有set和get的属性功能了。除非想要特殊业务内容。


比如set需要结合项目进行其他业务处理,get也是同样的道理。


【filed】是系统内置的一个关键字,算是中间变量


除了这些


var name: String? = null
        set(value) { //value随意起名
            field = value  //这个field是系统内置的 用在get
        }
        get() {
            return field + "这是返回"
        }
var urlJUEJIIN: String? = null
        get() =field+"这是只有get"
var urlCSDN: String? = null
var urlList: List<String>? = null

😜继承


在Java中可以说所有的类都继承自Object,而在Kotlin中可以说所有的是继承自Any类。


在Java中继承使用关键字【extends】,而在Kotlin中使用【:】(英文冒号)


除此之外,不管是方法重写还是属性变量重写,前面都加上【override】关键字,这一点和Java一样


class EntityTwo : Entity {
    constructor() {

    }

    constructor(name: String) : this(name, 0) {

    }

    //不同参数的次要构造函数
    constructor(name: String, age: Int) : super(name, age) {
        Log.e("TAG,", "执行了子类构造器$name===$age")
    }
}

😜接口


这点也和Java类似,使用【interface】定义,使用上也没差距


修饰类的关键字有



  • abstract    // 说明该类为抽象类 

  • final       // 说明该类为类不可继承,默认属性

  • enum        // 说明该类为枚举类

  • open        // 说明该类为类可继承,类默认是final的

  • annotation  // 说明该类为注解类


访问权限的修饰符有:



  • private    // 访问权限-仅在同一个文件中可见

  • protected  // 访问权限-同一个文件中或子类可见

  • public     // 访问权限-所有调用的地方都可见

  • internal   // 访问权限-同一个模块中可见


经过学习和试验验证,小空决定还是用Java的实体类吧,反正他们有互操作性。


😜抽象类/嵌套类/内部类


小空带大家直接用实例来看明白


abstract class EntityThree {
    abstract fun methonOne()
}

//嵌套类实例
class One {                  // 这是外部类
    private val age: Int = 1
    class Two {             // 这是在类里面的类,叫做嵌套类
        fun hello() {

        }

        fun hi() = 3
    }
}

//内部类使用关键字inner
class Three {
    inner class Four { //这个Four类是内部类
        fun hello() {

        }
        fun hi() = 3
    }
}
//这是引用示例
var one = Three().Four().hello()

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

使用BlackHook(黑钩) 可以Hook一切java或者kotlin方法

前言 之前做内存优化的时候,为了实现对线程的使用监控,借助了一个第三方的hook框架(epic),这个框架可以hook一切java方法,使用也简单,但是最大的问题是它有较严重的兼容性问题,部分机型会出现闪退的现象,这就导致它不能被带到线上使用,只能在线下使用,...
继续阅读 »

前言


之前做内存优化的时候,为了实现对线程的使用监控,借助了一个第三方的hook框架(epic),这个框架可以hook一切java方法,使用也简单,但是最大的问题是它有较严重的兼容性问题,部分机型会出现闪退的现象,这就导致它不能被带到线上使用,只能在线下使用,为了实现在线上监控线程的使用,于是我便开发了BlackHook插件,也可以hook一切java方法,而且很稳定,没有兼容性问题,真是十足的黑科技


简介


BlackHook 是一个实现编译时插桩的gradle插件,基于ASM+Tranfrom实现,理论上可以hook任意一个java方法或者kotlin方法,只要代码对应的字节码可以在编译阶段被Tranfrom扫描到,就可以使用ASM在代码对应的字节码处插入特定字节码,从而hook该方法


优点



  1. 用DSL(领域特定语言)使用该插件,使用简单,配置灵活,而且插入的字节码可以使用
    ASM Bytecode Viewer Support Kotlin 插件自动生成,上手难度低

  2. 理论上可以hook任意一个java方法,只要代码对应的字节码可以在编译阶段被Tranfrom扫描到

  3. 基于ASM+Tranfrom实现,在编译阶段直接修改字节码,效率高,没有兼容性问题


使用


在app下面的build.gradle文件添加如下代码


apply plugin: 'com.blackHook'

/**
* 返回hook线程构造函数的字节码,Hook 线程的构造函数,让每次在调用Thread的构造函数的时候就会调用
* ThreadCheck类的 printThread方法,从而在控制台打印线程的构造函数的调用堆栈,这些代码可以借助
* ASM Bytecode Viewer Support Kotlin生成,MethodVisitor是ASM提供的一个类,用于修改字节码
*/
void createHookThreadByteCode(MethodVisitor mv, String className) {
mv.visitTypeInsn(Opcodes.NEW, "com/quwan/tt/asmdemoapp/ThreadCheck")
mv.visitInsn(Opcodes.DUP)
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "com/quwan/tt/asmdemoapp/ThreadCheck", "<init>", "()V", false)
mv.visitLdcInsn(className)
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "com/quwan/tt/asmdemoapp/ThreadCheck", "printThread", "(Ljava/lang/String;)V", false)
}

/**
* 返回需要被hook的方法,需要被hook的方法是Thread的构造函数
*/
List<HookMethod> getHookMethods() {
List<HookMethod> hookMethodList = new ArrayList<>()
hookMethodList.add(new HookMethod("java/lang/Thread", "<init>", "()V", { MethodVisitor mv -> createHookThreadByteCode(mv, "java/lang/Thread") }))
return hookMethodList
}

blackHook {
//表示要处理的数据类型是什么,CLASSES 表示要处理编译后的字节码(可能是 jar 包也可能是目录),RESOURCES 表示要处理的是标准的 java 资源
inputTypes BlackHook.CONTENT_CLASS
//表示Transform 的作用域,这里设置的SCOPE_FULL_PROJECT代表作用域是全工程
scopes BlackHook.SCOPE_FULL_PROJECT
//表示是否支持增量编译,false不支持
isIncremental false
//表示hook的方法
hookMethodList = getHookMethods()
}

以上的代码其实是hook的Thread的构造函数,将ThreadCheck的printThread方法hook到了Thread的构造函数中,每次调用线程的构造函数的时候就会调用ThreadCheck的printThread方法,这个方法会打印出Thread的构造函数的调用堆栈,从而可以在控制台知道哪个页面的哪行代码实例化了Thread,ThreadCheck的代码如下


class ThreadCheck {

var isCanAppendLog = false
private val tag = "====>ThreadCheck"

fun printThread(name : String){

println("====>printThread:${name}")

val es = Thread.currentThread().stackTrace

val normalInfo = StringBuilder(" \nThreadTrace:")
.append("\nthreadName:${name}")
.append("\n====================================threadTraceStart=======================================")

for (e in es) {

if (e.className == "dalvik.system.VMStack" && e.methodName == "getThreadStackTrace") {
isCanAppendLog = false
}

if (e.className.contains("ThreadCheck") && e.methodName == "printThread") {
isCanAppendLog = true
} else {
if (isCanAppendLog) {
normalInfo.append("\n${e.className}(lineNumber:${e.lineNumber})")
}
}
}
normalInfo.append("\n=====================================threadTraceEnd=======================================")

Log.i(tag, normalInfo.toString())
}

}

上面的代码获取了调用堆栈,并且打印到控制台


实现原理


首先它是一个gradle 的自定义Plugin,其次它是通过在编译阶段修改字节码实现Hook,在编译阶段通过Tranfrom扫描所有的字节码,然后根据在使用插件的时候设置的需要被Hook的方法,插入需要被插入的字节码,
需要被插入的字节码也是在使用的时候设置的,例如下面的代码


/**
* 返回hook线程构造函数的字节码,Hook 线程的构造函数,让每次在调用Thread的构造函数的时候就会调用
* ThreadCheck的 printThread方法,从而在控制台打印线程的构造函数的调用堆栈,这些代码可以借助
* ASM Bytecode Viewer Support Kotlin生成,MethodVisitor是ASM提供的一个类,用于修改字节码
*/
void createHookThreadByteCode(MethodVisitor mv, String className) {
mv.visitTypeInsn(Opcodes.NEW, "com/quwan/tt/asmdemoapp/ThreadCheck")
mv.visitInsn(Opcodes.DUP)
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "com/quwan/tt/asmdemoapp/ThreadCheck", "<init>", "()V", false)
mv.visitLdcInsn(className)
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "com/quwan/tt/asmdemoapp/ThreadCheck", "printThread", "(Ljava/lang/String;)V", false)
}

准备过程


实现这个gradle插件需要我们有足够的预备知识,如下:



实现过程


1.自定义gradle plugin


因为这是一个gradle插件,所以需要我们自定义一个gradle的plugin


1. 新建一个模块


在工程中新建一个模块,命名为"buildSrc",注意,一定要命名为buildSrc,否则在工程中必须要将代码发布到本地或者远程maven仓库中才能正常使用,这样调试不方便,如下所示:


image.png


2. 然后配置gradle脚本,代码如下所示:


plugins {
id 'java-library'
id 'maven'
id 'groovy'
}

java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

dependencies {
implementation gradleApi()//gradle sdk
implementation localGroovy()
implementation "com.android.tools.build:gradle:3.4.1"
implementation 'org.ow2.asm:asm:9.1'
implementation 'org.ow2.asm:asm-commons:9.1'
}

3. 实现Plugin类


新建groovy文件夹,新建BlackHookPlugin类,继承Transform类,实现Plugin接口


image.png


BlackHookPlugin代码如下所示:


package com.blackHook.plugin

class BlackHookPlugin extends Transform implements Plugin<Project> {

....此处省略了很多代码

@Override
void apply(Project target) {
println("注册了")
project = target
target.extensions.getByType(BaseExtension).registerTransform(this)
target.extensions.create("blackHook", BlackHook.class)
}

....此处省略了很多代码
}

新建resources文件夹,新建com.blackHook.properties文件,如下所示


image.png


com.blackHook.properties文件的代码如下:


implementation-class=com.blackHook.plugin.BlackHookPlugin

implementation-class的值即是BlackHookPlugin的完整路径,另外,com.blackHook.properties文件的文件名既是使用插件的时候的插件名,如下代码:


apply plugin: 'com.blackHook'

2. 实现BlackHook扩展类


新建BlackHook类,代码如下


public class BlackHook {

Closure methodHooker;

List<HookMethod> hookMethodList = new ArrayList<>();

public static final String CONTENT_CLASS = "CONTENT_CLASS";
public static final String CONTENT_JARS = "CONTENT_JARS";
public static final String CONTENT_RESOURCES = "CONTENT_RESOURCES";

public static final String SCOPE_FULL_PROJECT = "SCOPE_FULL_PROJECT";
public static final String PROJECT_ONLY = "PROJECT_ONLY";

String inputTypes = CONTENT_CLASS;

String scopes = SCOPE_FULL_PROJECT;

boolean isNeedLog = false;

boolean isIncremental = false;

public Closure getMethodHooker() {
return methodHooker;
}

public void setMethodHooker(Closure methodHooker) {
this.methodHooker = methodHooker;
}

public List<HookMethod> getHookMethodList() {
return hookMethodList;
}

public void setHookMethodList(List<HookMethod> hookMethodList) {
this.hookMethodList = hookMethodList;
}

public String getInputTypes() {
return inputTypes;
}

public void setInputTypes(String inputTypes) {
this.inputTypes = inputTypes;
}

public String getScopes() {
return scopes;
}

public void setScopes(String scopes) {
this.scopes = scopes;
}

public boolean getIsIncremental() {
return isIncremental;
}

public void setIsIncremental(boolean incremental) {
isIncremental = incremental;
}

public boolean getIsNeedLog() {
return isNeedLog;
}

public void setIsNeedLog(boolean needLog) {
isNeedLog = needLog;
}
}

这个类用于接收开发人员使用插件的时候设置的参数和需要被Hook的方法以及参与Hook的字节码,我们在使用blackHook插件的时候可以使用DSL的方式来使用,如下代码所示:


blackHook {
//表示要处理的数据类型是什么,CLASSES 表示要处理编译后的字节码(可能是 jar 包也可能是目录), RESOURCES 表示要处理的是标准的 java 资源
inputTypes BlackHook.CONTENT_CLASS
//表示Transform 的作用域,这里设置的SCOPE_FULL_PROJECT代表作用域是全工程
scopes BlackHook.SCOPE_FULL_PROJECT
//表示是否支持增量编译,false不支持
isIncremental false
//表示hook的方法
hookMethodList = getHookMethods()
}

之所以可以这么做是因为我们在BlackHookPlugin将BlackHook类添加到了target.extensions(扩展属性)中,
如下代码:


class BlackHookPlugin extends Transform implements Plugin<Project> {
@Override
void apply(Project target) {
target.extensions.create("blackHook", BlackHook.class)
}
}

3.开始实现扫描


需要在BlackHookPlugin的transform()方法中扫描全局代码,代码如下:


  @Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
Collection<TransformInput> inputs = transformInvocation.inputs
TransformOutputProvider outputProvider = transformInvocation.outputProvider
if (outputProvider != null) {
outputProvider.deleteAll()
}
if (blackHook == null) {
blackHook = new BlackHook()
blackHook.methodHooker = project.extensions.blackHook.methodHooker
blackHook.isNeedLog = project.extensions.blackHook.isNeedLog
for (int i = 0; i < project.extensions.blackHook.hookMethodList.size(); i++) {
HookMethod hookMethod = new HookMethod()
hookMethod.className = project.extensions.blackHook.hookMethodList.get(i).className
hookMethod.methodName = project.extensions.blackHook.hookMethodList.get(i).methodName
hookMethod.descriptor = project.extensions.blackHook.hookMethodList.get(i).descriptor
hookMethod.createBytecode = project.extensions.blackHook.hookMethodList.get(i).createBytecode
blackHook.hookMethodList.add(hookMethod)
}
}
inputs.each { input ->
input.directoryInputs.each { directoryInput ->
handleDirectoryInput(directoryInput, outputProvider)
}
//遍历jarInputs
input.jarInputs.each { JarInput jarInput ->
//处理jarInputs
handleJarInputs(jarInput, outputProvider)
}
}
super.transform(transformInvocation)
}

void handleDirectoryInput(DirectoryInput directoryInput, TransformOutputProvider outputProvider) {
if (directoryInput.file.isDirectory()) {
directoryInput.file.eachFileRecurse { file ->
String name = file.name
if (name.endsWith(".class") && !name.startsWith("R$drawable")
&& !"R.class".equals(name) && !"BuildConfig.class".equals(name)) {
ClassReader classReader = new ClassReader(file.bytes)
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
ClassVisitor classVisitor = new AllClassVisitor(classWriter, blackHook)
classReader.accept(classVisitor, EXPAND_FRAMES)
byte[] code = classWriter.toByteArray()
FileOutputStream fos = new FileOutputStream(
file.parentFile.absolutePath + File.separator + name)
fos.write(code)
fos.close()
}
}
}

//处理完输入文件之后,要把输出给下一个任务
def dest = outputProvider.getContentLocation(directoryInput.name,
directoryInput.contentTypes, directoryInput.scopes,
Format.DIRECTORY)
FileUtils.copyDirectory(directoryInput.file, dest)
}

void handleJarInputs(JarInput jarInput, TransformOutputProvider outputProvider) {
if (jarInput.file.getAbsolutePath().endsWith(".jar")) {
//重名名输出文件,因为可能同名,会覆盖
def jarName = jarInput.name

def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
if (jarName.endsWith(".jar")) {
jarName = jarName.substring(0, jarName.length() - 4)
}
JarFile jarFile = new JarFile(jarInput.file)
Enumeration enumeration = jarFile.entries()
File tmpFile = new File(jarInput.file.getParent() + File.separator + "classes_temp.jar")
//避免上次的缓存被重复插入
if (tmpFile.exists()) {
tmpFile.delete()
}
JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(tmpFile))
//用于保存
while (enumeration.hasMoreElements()) {
JarEntry jarEntry = (JarEntry) enumeration.nextElement()
String entryName = jarEntry.getName()
ZipEntry zipEntry = new ZipEntry(entryName)
InputStream inputStream = jarFile.getInputStream(jarEntry)
//插桩class
if (entryName.endsWith(".class") && !entryName.startsWith("R$")
&& !"R.class".equals(entryName) && !"BuildConfig.class".equals(entryName)) {
//class文件处理
jarOutputStream.putNextEntry(zipEntry)
ClassReader classReader = new ClassReader(IOUtils.toByteArray(inputStream))
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
ClassVisitor cv = new AllClassVisitor(classWriter, blackHook)
classReader.accept(cv, EXPAND_FRAMES)
byte[] code = classWriter.toByteArray()
jarOutputStream.write(code)
} else {
jarOutputStream.putNextEntry(zipEntry)
jarOutputStream.write(IOUtils.toByteArray(inputStream))
}
jarOutputStream.closeEntry()
}
//结束
jarOutputStream.close()
jarFile.close()
def dest = outputProvider.getContentLocation(jarName + md5Name,
jarInput.contentTypes, jarInput.scopes, Format.JAR)
FileUtils.copyFile(tmpFile, dest)
tmpFile.delete()
}
}

扫描的过程中会将扫描到的所有类的信息(包含类名,父类名,方法名等)交给AllClassVisitor类,AllClassVisitor类代码如下所示:


public class AllClassVisitor extends ClassVisitor {
private String className;
private BlackHook blackHook;
private String superClassName;

public AllClassVisitor(ClassVisitor classVisitor, BlackHook blackHook) {
super(ASM6, classVisitor);
this.blackHook = blackHook;
}

@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
className = name;
superClassName = superName;
}

// 扫描到每个类中的方法的时候会回调到这个方法
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);
// 新建AllMethodVisitor类,将扫描到类和方法的信息以及BlackHook类存储的参数交给 AllMethodVisitor对象,由AllMethodVisitor来判断是否需要Hook指定的方法
return new AllMethodVisitor(blackHook, mv, access, name, descriptor, className, superClassName);
}

然后在AllClassVisitor类中会将将扫描到的类和方法的信息以及BlackHook扩展类存储的参数交给AllMethodVisitor对象,由AllMethodVisitor来判断是否需要Hook指定的方法,AllMethodVisitor代码如下:


class AllMethodVisitor extends AdviceAdapter {
private final String methodName;
private final String className;
private BlackHook blackHook;
private String superClassName;

protected AllMethodVisitor(BlackHook blackHook, org.objectweb.asm.MethodVisitor methodVisitor, int access, String name, String descriptor, String className, String superClassName) {
super(ASM5, methodVisitor, access, name, descriptor);
this.blackHook = blackHook;
this.methodName = name;
this.className = className;
this.superClassName = superClassName;
}

@Override
protected void onMethodEnter() {
super.onMethodEnter();
}

@Override
public void visitMethodInsn(int opcode, String owner, String methodName, String descriptor, boolean isInterface) {
super.visitMethodInsn(opcode, owner, methodName, descriptor, isInterface);
if (blackHook.isNeedLog) {
System.out.println("====>methodInfo:" + "className:" + owner + ",methodName:" + methodName + ",descriptor:" + descriptor);
}
if (blackHook != null && blackHook.hookMethodList != null && blackHook.hookMethodList.size() > 0) {
for (int i = 0; i < blackHook.hookMethodList.size(); i++) {
HookMethod hookMethod = blackHook.hookMethodList.get(i);
//这里根据开发人员设置的需要hook的方法以及扫描到的方法来判断是否需要hook
if ((owner.equals(hookMethod.className) || superClassName.equals(hookMethod.className) || className.equals(hookMethod.className)) && methodName.equals(hookMethod.methodName) && descriptor.equals(hookMethod.descriptor)) {
hookMethod.createBytecode.call(mv);
break;
}
}
}
}
}

在这个类中根据开发人员调用插件的时候设置的需要hook的方法以及扫描到的方法来判断是否需要hook


4.源码


github.com/18824863285…


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

扒一扒Android的.9图

前言相信大家对.9图都不陌生,我们在开发当中当有控件的背景需要对内容的大小做自适应的时候,可能就需要用到.9图。如下图所示,就是一张.9图。官方是这么定义的:NinePatchDrawable 图形是一种可拉伸的位图,可用作视图的背景。Android...
继续阅读 »

前言

相信大家对.9图都不陌生,我们在开发当中当有控件的背景需要对内容的大小做自适应的时候,可能就需要用到.9图。如下图所示,就是一张.9图。官方是这么定义的:

NinePatchDrawable 图形是一种可拉伸的位图,可用作视图的背景。Android 会自动调整图形的大小以适应视图的内容。NinePatch 图形是标准 PNG 图片,包含一个额外的 1 像素边框。必须使用 9.png 扩展名将其保存在项目的 res/drawable/ 目录下。

ninepatch_raw.png

那么有人可能会说,这有什么好讲的,从做Andorid开始,我就一直用到现在了。但是,往往越简单的东西,我们越容易忽略它。下面我们就带着这几个问题,一步步来看:

  1. Android是怎么识别一张.9图的?
  2. .9图片一定要放在res/drawable目录下吗,Android是怎么处理它的,为什么在手机上显示出来这个黑色边线却不见了?
  3. 一定要用.9图才能达到自适应的效果吗,普通图片行不行?

PNG

定义

从官方介绍可以得知,.9图是一张标准的PNG图片,只不过是加了一些额外的像素而已,那么首先我们得了解一下什么是PNG。

便携式网络图形(英语:Portable Network Graphics,PNG)是一种支持无损压缩的位图图形格式,支持索引、灰度、RGB三种颜色方案以及Alpha通道等特性。PNG的开发目标是改善并取代GIF作为适合网络传输的格式而不需专利许可,所以被广泛应用于互联网及其他方面上。

文件结构

文件跟协议一样,都是用数据来呈现的。那么既然协议有协议头来标识是什么协议,文件也一样。PNG的文件标识(file signature)是由8个字节组成(89 50 4E 47 0D 0A 1A 0A, 十六进制),系统就是根据这8个自己来识别出PNG文件。

在文件头之后,紧跟着的是数据块。PNG的数据块分为两类,一类是PNG文件必须包含、读写软件也必须要支持的关键块(critical chunk);另一种叫做辅助块(ancillary chunks),PNG允许软件忽略它不认识的附加块。这种基于数据块的设计,允许PNG格式在扩展时仍能保持与旧版本兼容。

数据块的格式:

名称字节数说明
Length4字节指定数据块中数据域的长度,其长度不超过(2^{31}-1)字节
Chunk Type Code(数据块类型码)4字节数据块类型码由ASCII字母(A-Z和a-z)组成
Chunk Data(数据块实际内容)实际内容长度存储按照Chunk Type Code指定的数据
CRC(循环冗余检测)4字节存储用来检测是否有错误的循环冗余码

关键块中有4个标准的数据块:

  • 文件头数据块IHDR(header chunk):包含有图像基本信息,作为第一个数据块出现并只出现一次。
  • 调色板数据块PLTE(palette chunk):必须放在图像数据块之前。
  • 图像数据块IDAT(image data chunk):存储实际图像数据。PNG数据允许包含多个连续的图像数据块。
  • 图像结束数据IEND(image trailer chunk):放在文件尾部,表示PNG数据流结束。

当然关于PNG的信息不止这些,有兴趣了解更多的话,可以去阅读RFC 2083,这里不做过多赘述。

所以不难猜出,.9图是在PNG的辅助块加了自己可以识别的数据块,然后显示的时候对图片做特殊的处理

Android是怎么加载一张.9图的

在Android中,一张图片对应的是一个Bitmap,我们可以看看从怎么从文件读取一张Bitmap入手

//BitmapFactory.java

public static Bitmap decodeFile(String pathName) {
   return decodeFile(pathName, null);
}

private static native Bitmap nativeDecodeStream(InputStream is, byte[] storage,
           Rect padding, Options opts, long inBitmapHandle, long colorSpaceHandle);

我们根据一个文件路径读取一张图片的话,需要调用BitmapFactorydecodeFile方法,这里我省略了一些过程,但最终都会调用到nativeDecodeStream这个方法,它是一个native方法,接着看C++那边是怎么实现的

//BitmapFactory.cpp

static jobject nativeDecodeStream(JNIEnv* env, jobject clazz, jobject is, jbyteArray storage,
       jobject padding, jobject options, jlong inBitmapHandle, jlong colorSpaceHandle) {
...

   if (stream.get()) {
      ...
       bitmap = doDecode(env, std::move(bufferedStream), padding, options, inBitmapHandle,
                         colorSpaceHandle);
  }
   return bitmap;
}

static jobject doDecode(JNIEnv* env, std::unique_ptr<SkStreamRewindable> stream,
                       jobject padding, jobject options, jlong inBitmapHandle,
                       jlong colorSpaceHandle) {
...
   NinePatchPeeker peeker;
   std::unique_ptr<SkAndroidCodec> codec;
  {
      ...
       std::unique_ptr<SkCodec> c = SkCodec::MakeFromStream(std::move(stream), &result, &peeker);
      ...
  }
 
...
jbyteArray ninePatchChunk = NULL;
   if (peeker.mPatch != NULL) {
       size_t ninePatchArraySize = peeker.mPatch->serializedSize();
       ninePatchChunk = env->NewByteArray(ninePatchArraySize);
       jbyte* array = (jbyte*) env->GetPrimitiveArrayCritical(ninePatchChunk, NULL);
       memcpy(array, peeker.mPatch, peeker.mPatchSize);
       env->ReleasePrimitiveArrayCritical(ninePatchChunk, array, 0);
  }
 
// now create the java bitmap
   return bitmap::createBitmap(env, defaultAllocator.getStorageObjAndReset(),
           bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1);
}

doDecode方法很长,这里只提取关键部分。我们看到了关键的NinePatchPeeker,然后把它的指针传给MakeFromStream这个方法。接着copy出NinePatchPeekermPatchBitmap作为构造参数。我们接着往下看:

// SkCodec.cpp
// 刚才NinePatchPeeker传给了这个方法的第三个参数,NinePatchPeeker实际上是实现了SkPngChunkReader
std::unique_ptr<SkCodec> SkCodec::MakeFromStream(
       std::unique_ptr<SkStream> stream, Result* outResult,
       SkPngChunkReader* chunkReader, SelectionPolicy selectionPolicy) {
 
  ...
#ifdef SK_HAS_PNG_LIBRARY
   if (SkPngCodec::IsPng(buffer, bytesRead)) {
       return SkPngCodec::MakeFromStream(std::move(stream), outResult, chunkReader);
  } else
#endif
  ...
}

// SkPngCodec.cpp
std::unique_ptr<SkCodec> SkPngCodec::MakeFromStream(std::unique_ptr<SkStream> stream,
                                                   Result* result, SkPngChunkReader* chunkReader) {
   SkCodec* outCodec = nullptr;
   *result = read_header(stream.get(), chunkReader, &outCodec, nullptr, nullptr);
   if (kSuccess == *result) {
       // Codec has taken ownership of the stream.
       SkASSERT(outCodec);
       stream.release();
  }
   return std::unique_ptr<SkCodec>(outCodec);
}

static SkCodec::Result read_header(SkStream* stream, SkPngChunkReader* chunkReader,
                                  SkCodec** outCodec,
                                  png_structp* png_ptrp, png_infop* info_ptrp) {
...
#ifdef PNG_READ_UNKNOWN_CHUNKS_SUPPORTED
   // Hookup our chunkReader so we can see any user-chunks the caller may be interested in.
   // This needs to be installed before we read the png header. Android may store ninepatch
   // chunks in the header.
   if (chunkReader) {
       png_set_keep_unknown_chunks(png_ptr, PNG_HANDLE_CHUNK_ALWAYS, (png_byte*)"", 0);
       png_set_read_user_chunk_fn(png_ptr, (png_voidp) chunkReader, sk_read_user_chunk);
  }
#endif
...
}

这里重点看下png_set_read_user_chunk_fn这个方法,传了chunkReadersk_read_user_chunk方法进去

#ifdef PNG_READ_USER_CHUNKS_SUPPORTED
void PNGAPI
png_set_read_user_chunk_fn(png_structrp png_ptr, png_voidp user_chunk_ptr,
   png_user_chunk_ptr read_user_chunk_fn) {
  ...
  png_ptr->read_user_chunk_fn = read_user_chunk_fn;
  png_ptr->user_chunk_ptr = user_chunk_ptr;
}
#endif

这个方法主要是对png_ptr的两个变量进行赋值,png_ptr是一个PNG结构体的指针。之后read_user_chunk_fn这个方法会在pngrutil.c中被调用

// pngrutil.c
void png_handle_unknown(png_structrp png_ptr, png_inforp info_ptr,
   png_uint_32 length, int keep) {
...
# ifdef PNG_READ_USER_CHUNKS_SUPPORTED
  if (png_ptr->read_user_chunk_fn != NULL) {
     if (png_cache_unknown_chunk(png_ptr, length) != 0) {
        /* Callback to user unknown chunk handler */
        int ret = (*(png_ptr->read_user_chunk_fn))(png_ptr,
            &png_ptr->unknown_chunk);
    }
  }
...
}

这里看方法名就知道是libpng这个库在读取未知的数据块,调用了read_user_chunk_fn方法读取用户自己定义的数据块。而read_user_chunk_fn就是上面的sk_read_user_chunk

// SkPngCodec.cpp
#ifdef PNG_READ_UNKNOWN_CHUNKS_SUPPORTED
static int sk_read_user_chunk(png_structp png_ptr, png_unknown_chunkp chunk) {
   SkPngChunkReader* chunkReader = (SkPngChunkReader*)png_get_user_chunk_ptr(png_ptr);
   // readChunk() returning true means continue decoding
   return chunkReader->readChunk((const char*)chunk->name, chunk->data, chunk->size) ? 1 : -1;
}
#endif

// pngget.c
#ifdef PNG_USER_CHUNKS_SUPPORTED
png_voidp PNGAPI
png_get_user_chunk_ptr(png_const_structrp png_ptr) {
  return (png_ptr ? png_ptr->user_chunk_ptr : NULL);
}
#endif

拿到一个SkPngChunkReader,而它的具体实现上面有说到,就是NinePatchPeeker

// NinePatchPeeker.cpp
bool NinePatchPeeker::readChunk(const char tag[], const void* data, size_t length) {
   if (!strcmp("npTc", tag) && length >= sizeof(Res_png_9patch)) {
       Res_png_9patch* patch = (Res_png_9patch*) data;
       size_t patchSize = patch->serializedSize();
       if (length != patchSize) {
           return false;
      }
       // You have to copy the data because it is owned by the png reader
       Res_png_9patch* patchNew = (Res_png_9patch*) malloc(patchSize);
       memcpy(patchNew, patch, patchSize);
       Res_png_9patch::deserialize(patchNew);
       patchNew->fileToDevice();
       free(mPatch);
       mPatch = patchNew;
       mPatchSize = patchSize;
  } else if (!strcmp("npLb", tag) && length == sizeof(int32_t) * 4) {
       mHasInsets = true;
       memcpy(&mOpticalInsets, data, sizeof(int32_t) * 4);
  } else if (!strcmp("npOl", tag) && length == 24) { // 4 int32_ts, 1 float, 1 int32_t sized byte
       mHasInsets = true;
       memcpy(&mOutlineInsets, data, sizeof(int32_t) * 4);
       mOutlineRadius = ((const float*)data)[4];
       mOutlineAlpha = ((const int32_t*)data)[5] & 0xff;
  }
   return true;
}

找了这么久,我们的目的地终于找到了。可以看到.9图对应的数据块有三个:npTcnpLbnpOl,负责图片图片拉伸的是npTc这个数据块。它在这里用一个Res_png_9patch的结构体封装,我们可以从这个结构体的注释就可以知道很多事情了,懒得看注释的话可以跳过,直接看我下面的解释:

/**
* This chunk specifies how to split an image into segments for
* scaling.
*
* There are J horizontal and K vertical segments. These segments divide
* the image into J*K regions as follows (where J=4 and K=3):
*
*     F0   S0   F1     S1
*   +-----+----+------+-------+
* S2| 0 | 1 | 2   |   3   |
*   +-----+----+------+-------+
*   |     |   |     |       |
*   |     |   |     |       |
* F2| 4 | 5 | 6   |   7   |
*   |     |   |     |       |
*   |     |   |     |       |
*   +-----+----+------+-------+
* S3| 8 | 9 | 10 |   11 |
*   +-----+----+------+-------+
*
* Each horizontal and vertical segment is considered to by either
* stretchable (marked by the Sx labels) or fixed (marked by the Fy
* labels), in the horizontal or vertical axis, respectively. In the
* above example, the first is horizontal segment (F0) is fixed, the
* next is stretchable and then they continue to alternate. Note that
* the segment list for each axis can begin or end with a stretchable
* or fixed segment.
*
* ...
*
* The colors array contains hints for each of the regions. They are
* ordered according left-to-right and top-to-bottom as indicated above.
* For each segment that is a solid color the array entry will contain
* that color value; otherwise it will contain NO_COLOR. Segments that
* are completely transparent will always have the value TRANSPARENT_COLOR.
*
* The PNG chunk type is "npTc".
*/
struct alignas(uintptr_t) Res_png_9patch
{
int8_t wasDeserialized;
   uint8_t numXDivs, numYDivs, numColors;

   uint32_t xDivsOffset, yDivsOffset, colorsOffset;

// .9图右边和下边黑线描述的方位
   int32_t paddingLeft, paddingRight, paddingTop, paddingBottom;

   enum {
       // The 9 patch segment is not a solid color.
       NO_COLOR = 0x00000001,

       // The 9 patch segment is completely transparent.
       TRANSPARENT_COLOR = 0x00000000
  };

...
     
   inline int32_t* getXDivs() const {
       return reinterpret_cast<int32_t*>(reinterpret_cast<uintptr_t>(this) + xDivsOffset);
  }
   inline int32_t* getYDivs() const {
       return reinterpret_cast<int32_t*>(reinterpret_cast<uintptr_t>(this) + yDivsOffset);
  }
   inline uint32_t* getColors() const {
       return reinterpret_cast<uint32_t*>(reinterpret_cast<uintptr_t>(this) + colorsOffset);
  }
}

注释告诉我们几个信息:

  • 一张图片被分为几个区块,支持拉伸的区块坐标分别存储在xDivs和yDivs两个数组。

  • S开头的表示可以拉伸(其实就是做.9图时,旁边1像素的黑线标记的范围),F表示不能拉伸。

    按照注释中的例子,图片被分为12块,例如S0,它表示编号为1、5、9在横轴方向 上是可以拉伸的,S1则表示标号3、7、11是支持拉伸的。所以xDivs和yDivs存储的数据长下面这样:

    xDivs = [S0.start, S0.end, S1.start, S1.end]

    yDivs = [S2.start, S2.end, S3.start, S3.end]

  • colors 描述了各个区块的颜色,按照从左到右从上到下表示。通常情况下,赋值为源码中定义的NO_COLOR = 0x00000001就行了

    colors = [c1, c2, c3, .... c11]

  • 横向(或者纵向)有多个拉伸块的时候,他们的拉伸长度是按照他们标识的范围比例来算的。加入S0是1像素,S1是3像素,则他们拉伸长度按照1:3去拉伸

数据结构

那么,从Res_png_9patch的序列化方法,我们可以推断出这个chunk的数据结构

void Res_png_9patch::serialize(const Res_png_9patch& patch, const int32_t* xDivs,
                              const int32_t* yDivs, const uint32_t* colors, void* outData) {
   uint8_t* data = (uint8_t*) outData;
   memcpy(data, &patch.wasDeserialized, 4);     // copy wasDeserialized, numXDivs, numYDivs, numColors
   memcpy(data + 12, &patch.paddingLeft, 16);   // copy paddingXXXX
   data += 32;

   memcpy(data, xDivs, patch.numXDivs * sizeof(int32_t));
   data +=  patch.numXDivs * sizeof(int32_t);
   memcpy(data, yDivs, patch.numYDivs * sizeof(int32_t));
   data +=  patch.numYDivs * sizeof(int32_t);
   memcpy(data, colors, patch.numColors * sizeof(uint32_t));
}
名称字节长度说明
wasDeserialized1这个值为-1的话表示这个区块不是.9图
numXDivs1xDivs 数组长度
numYDivs1yDivs 数组长度
numColors1colors 数组长度
--4无意义
--4无意义
paddingLeft4横向内容区域的左边
paddingRight4横向内容区域的右边
paddingTop4纵向内容区域的顶部
paddingBottom4纵向内容区域的底部
--无意义
xDivsnumXDivs * 4横向拉伸区域(图片上方黑线)
yDivsnumYDivs * 4纵向拉伸区域(图片左边黑线)
colorsnumColors * 4各个区块颜色

小结

那么,到这里Android把一个.9图加载成Bitmap给理清楚了。先通过读取PNG到header信息,发现有npTc数据块到时候,把它到chunk数据读取出来,用来做Bitmap的构造参数。接下来我们看看绘制

绘制

.9图是用NinePatchDrawable做绘制的,使用方式是这样的:

val bitmap = BitmapFactory.decodeFile(absolutePath)
// 检查bitmap的ninePatchChunk是不是属于.9图的格式,其实就是判断这个chunk的wasDeserialized(第一个字节)是不是等于-1,
val isNinePatch = NinePatch.isNinePatchChunk(bitmap.ninePatchChunk)
if (isNinePatch) {
// 用bitmap以及bitmap.ninePatchChunk构造NinePatchDrawable
val background = NinePatchDrawable(context.resources, bitmap, bitmap.ninePatchChunk, Rect(), null)
imageView.background = background
}

NinePatchDrawable的绘制方法里,又会调用到native方法,由于篇幅原因,这里简单的列下调用栈,大家感兴趣的话可以去看源码:

NinePatchDrawable.java -> draw()
NinePatch.java -> draw()
Canvas.java -> drawPatch()
BaseCanvas.java -> drawPatch()
-> nDrawNinePatch() // 这里是一个native方法,从这里开始就都是native逻辑了

SkiaCanvas.cpp -> drawNinePatch() // Canvas所有的native方法都对应的native层的SkiaCanvas。这里会根据xDivs和yDivs的数据把图片分为N个格子
SkCanvas.cpp -> drawImageLattice()
SkDevice.cpp -> onDrawImageLattice() // 这里循环绘制每个格子
-> drawImageRect()
SkBitmapDevice.cpp -> drawBitmapRect() // 这里给Paint设置了BitmapShader去绘制图片,模式用的是CLAMP(拉伸模式)

到这里,从加载到绘制的过程都已经讲完了,但是还漏了一块,那就是编译。

编译

大家有没有疑问,.9图header里面,npTc这个数据块哪里来?官方介绍为什么叫我们要保存到res/drawable/里面?

其实在编译的时候,aapt会对res/drawable/的图片进行编译,发现是.9图,就把图片四周的黑色像素提取出来,整理成npTc数据块,放到PNG的header里面。

我们可以用Vim打开一张未编译的.9图看看

1.png

这里我们可以看到一些基本的数据块,例如IHDR以及IEND。接着我们用aapt编译一下这张.9图,具体命令如下:

./aapt s -i xxx_in.png -o xxx_out.png

2.png 用Vim打开之后,可以看到多了很多信息,也可以看到.9图对应的npTc数据块,打开图片也可以发现那些四周的黑线不见了。

最后

回答一下文章开头的几个问题:

.9图片一定要放在res/drawable目录下吗

这个不一定,如果你需要从assets目录、sdcard、或者网络读取.9图,也可以实现。只不过需要手动用aapt对图片做处理

一定要用.9图才能达到自适应的效果吗,普通图片行不行

通过了解.9图的原理之后,答案是肯定行的。我们可以自己手动构造ninePatchChunk, 然后传给NinePatchDrawable就可以了,这里就不写代码演示了。


收起阅读 »

在Android中使用Netty进行通讯,附带服务端代码

NettyNetty 是一个利用 Java 的高级网络的能力,隐藏其背后的复杂性而提供一个易于使用的 API 的客户端/服务器框架。 Netty 是一个广泛使用的 Java 网络编程框架(Netty 在 2011 年获得了Duke's Choice Award...
继续阅读 »

Netty

Netty 是一个利用 Java 的高级网络的能力,隐藏其背后的复杂性而提供一个易于使用的 API 的客户端/服务器框架。 Netty 是一个广泛使用的 Java 网络编程框架(Netty 在 2011 年获得了Duke's Choice Award,见http://www.java.net/dukeschoice… Facebook 和 Instagram 以及流行 开源项目如 Infinispan, HornetQ, Vert.x, Apache Cassandra 和 Elasticsearch 等,都利用其强大的对于网络抽象的核心代码。

依赖引入

由于使用最新版本的话,发现有个类找不到,后面查了下是因为jdk版本,在android中的话,太高的jdk版本肯定不支持,所以我找了19年的发行版,测试ok。

implementation 'io.netty:netty-all:4.1.42.Final'

服务端代码实现

NettyServer

@Slf4j
public class NettyServer {

public void start(InetSocketAddress socketAddress) {
//new 一个主线程组
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
//new 一个工作线程组
EventLoopGroup workGroup = new NioEventLoopGroup(200);
ServerBootstrap bootstrap = new ServerBootstrap()
.group(bossGroup, workGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ServerChannelInitializer())
.localAddress(socketAddress)
//设置队列大小
.option(ChannelOption.SO_BACKLOG, 1024)
// 两小时内没有数据的通信时,TCP会自动发送一个活动探测数据报文
.childOption(ChannelOption.SO_KEEPALIVE, true);
//绑定端口,开始接收进来的连接
try {
ChannelFuture future = bootstrap.bind(socketAddress).sync();
log.info("服务器启动开始监听端口: {}", socketAddress.getPort());
// future.channel().writeAndFlush("你好啊");
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//关闭主线程组
bossGroup.shutdownGracefully();
//关闭工作线程组
workGroup.shutdownGracefully();
}
}
}

ServerChannelInitializer

public class ServerChannelInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//添加编解码
socketChannel.pipeline().addLast("decoder", new StringDecoder(CharsetUtil.UTF_8));
socketChannel.pipeline().addLast("encoder", new StringEncoder(CharsetUtil.UTF_8));
socketChannel.pipeline().addLast(new NettyServerHandler());
}
}

NettyServerHandler

@Slf4j
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
/**
* 客户端连接会触发
*/

@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
log.info("Channel active......");
}

@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
log.info("Channel Inactive......");
}

/**
* 客户端发消息会触发
*/

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//----------- 只改了这里 -----------
log.info("服务器收到消息1111: {}", msg.toString());

ctx.write("{\"data\":{\"taskData\":{\"collectionRule\":{\"id\":1,\"name\":\"IP主播直播室互动用户\",\"rule\":\"[{\\\"label\\\":\\\"抖音号\\\",\\\"key\\\":\\\"dyId\\\",\\\"type\\\":\\\"string\\\"},{\\\"key\\\":\\\"count\\\",\\\"label\\\":\\\"数量\\\",\\\"type\\\":\\\"string\\\"}]\",\"ruleType\":\"collect\",\"source\":\"collectLiveAudience\"},\"description\":\"粉丝列表-付鹏的财经世界3\",\"ruleId\":\"1\",\"ruleParam\":\"{\\\"dyId\\\":\\\"ghsys\\\",\\\"count\\\":\\\"140000\\\"}\"},\"taskId\":\"64\",\"taskType\":\"collection\"},\"devicesId\":\"5011bbdcd5006a93\",\"type\":\"task\"}");
ctx.flush();
}

/**
* 发生异常触发
*/

@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}

启动服务

@SpringBootApplication
public class ServerApplication {

public static void main(String[] args) {
SpringApplication.run(ServerApplication.class, args);
//启动服务端
NettyServer nettyServer = new NettyServer();
nettyServer.start(new InetSocketAddress("192.18.52.95", 8091));
}
}

客户端代码实现

其实netty的使用客户端和服务器端整体上是差不多的,所以这里只列出来核心代码。

初始化操作

abstract class McnNettyTask : Runnable {

private var socketChannel: SocketChannel? = null
private var isConnected = false

override fun run() {
createConnection()
}

private fun createConnection() {
val nioEventLoopGroup = NioEventLoopGroup()
val bootstrap = Bootstrap()
bootstrap
.group(nioEventLoopGroup)
.option(ChannelOption.TCP_NODELAY, true) //无阻塞
.channel(NioSocketChannel::class.java)
.option(ChannelOption.SO_KEEPALIVE, true) //长连接
.option(ChannelOption.SO_TIMEOUT, 30_000) //收发超时
.handler(McnClientInitializer(object : McnClientListener {
override fun disConnected() {
isConnected = false
}

override fun connected() {
isConnected = true
}
}, object : McnEventListener {
override fun onReceiverMessage(messageRequest: MessageRequest) {
dispatchMessage(messageRequest)
}
}))
try {
val channelFuture = bootstrap.connect(McnNettyConfig.ip, McnNettyConfig.port)
.addListener(object : ChannelFutureListener {
override fun operationComplete(future: ChannelFuture) {
if (future.isSuccess) {
socketChannel = future.channel() as SocketChannel;
isConnected = true
CommonConsole.log("netty connect success (ip: ${McnNettyConfig.ip}, port: ${McnNettyConfig.port})")

sendMsg(MessageRequest.createDevicesState(0))
} else {
CommonConsole.log("netty connect failure (ip: ${McnNettyConfig.ip}, port: ${McnNettyConfig.port})")
isConnected = false
future.channel().close()
nioEventLoopGroup.shutdownGracefully()
}
}
}).sync()//阻塞,直到连接完成
channelFuture.channel().closeFuture().sync()
} catch (ex: Exception) {
ex.printStackTrace()
} finally {
//释放所有资源和创建的线程
nioEventLoopGroup.shutdownGracefully()
}
}

fun isConnected(): Boolean {
return isConnected
}

fun disConnected() {
socketChannel?.close()
}

abstract fun dispatchMessage(messageRequest: MessageRequest)

fun sendMsg(msg: String, nettyMessageListener: McnMessageListener? = null) {
if (!isConnected()) {
nettyMessageListener?.sendFailure()
return
}
socketChannel?.run {
writeAndFlush(msg + "###").addListener { future ->
if (future.isSuccess) {
//消息发送成功
CommonConsole.log("netty send message success (message: $msg")
nettyMessageListener?.sendSuccess()
} else {
//消息发送失败
CommonConsole.log("netty send message failure (message: $msg")
nettyMessageListener?.sendFailure()
}
}
}
}
}

加载handler和Initializer

class McnClientInitializer(
private val nettyClientListener: McnClientListener,
private val nettyEventListener: McnEventListener
) :
ChannelInitializer<SocketChannel>() {

override fun initChannel(socketChannel: SocketChannel) {
val pipeline = socketChannel.pipeline()
// pipeline.addLast("decoder", McnStringDecoder())
// pipeline.addLast("encoder", McnStringEncoder())
// pipeline.addLast(LineBasedFrameDecoder(1024))
pipeline.addLast("decoder", StringDecoder())
// pipeline.addLast("encoder", StringEncoder())
pipeline.addLast(DelimiterBasedFrameEncoder("###"))
pipeline.addLast(McnClientHandler(nettyClientListener, nettyEventListener))
}
}

核心数据接收处理handler

class McnClientHandler(
private val nettyClientListener: McnClientListener,
private val nettyEventListener: McnEventListener
) :
SimpleChannelInboundHandler<String>() {

override fun channelActive(ctx: ChannelHandlerContext?) {
super.channelActive(ctx)
CommonConsole.log("Netty channelActive.........")
nettyClientListener.connected()
}

override fun channelInactive(ctx: ChannelHandlerContext?) {
super.channelInactive(ctx)
nettyClientListener.disConnected()
}

override fun channelReadComplete(ctx: ChannelHandlerContext?) {
super.channelReadComplete(ctx)
CommonConsole.log("Netty channelReadComplete.........")
}

override fun exceptionCaught(ctx: ChannelHandlerContext?, cause: Throwable?) {
super.exceptionCaught(ctx, cause)
CommonConsole.log("Netty exceptionCaught.........${cause?.message}")
cause?.printStackTrace()
ctx?.close()
}

override fun channelRead0(ctx: ChannelHandlerContext?, msg: String?) {
CommonConsole.log("Netty channelRead.........${msg}")
msg?.run {
try {
val messageRequest =
Gson().fromJson<MessageRequest>(msg, MessageRequest::class.java)
nettyEventListener.onReceiverMessage(messageRequest)
} catch (ex: Exception) {
ex.printStackTrace()
}
ReferenceCountUtil.release(msg)
}
}
}

处理数据粘包 & 数据分包

如果使用netty,你肯定会碰到数据粘包和数据分包的问题的。所谓数据粘包就是当数据量比较小的情况下,相近时间内的多个发送数据会被作为一个数据包接收解析。而数据分包就是会将一个比较大的数据包分成为很多个小的数据包。不管是数据的分包还是粘包,都会导致我们使用的时候不能简单使用数据,所以我们要对粘包和分包数据做处理,让每次发送的数据都是独立且完整的。

对于数据编码,使用自定义的解析器处理,相当于是对数据使用特定字符串做拼接和反取操作。

服务端:

ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024,
Unpooled.wrappedBuffer(delimiter.getBytes())));
// 将分隔之后的字节数据转换为字符串数据
ch.pipeline().addLast(new StringDecoder());
// 这是我们自定义的一个编码器,主要作用是在返回的响应数据最后添加分隔符
ch.pipeline().addLast(new DelimiterBasedFrameEncoder("###"));
// 最终处理数据并且返回响应的handler
ch.pipeline().addLast(new EchoServerHandler());

客户端:

/ 对服务端返回的消息通过_$进行分隔,并且每次查找的最大大小为1024字节
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024,
Unpooled.wrappedBuffer(delimiter.getBytes())));
// 将分隔之后的字节数据转换为字符串
ch.pipeline().addLast(new StringDecoder());
// 对客户端发送的数据进行编码,这里主要是在客户端发送的数据最后添加分隔符
ch.pipeline().addLast(new DelimiterBasedFrameEncoder("###"));
// 客户端发送数据给服务端,并且处理从服务端响应的数据
ch.pipeline().addLast(new EchoClientHandler());

DelimiterBasedFrameEncoder

public class DelimiterBasedFrameEncoder extends MessageToByteEncoder<String> {

private String delimiter;

public DelimiterBasedFrameEncoder(String delimiter) {
this.delimiter = delimiter;
}

@Override
protected void encode(ChannelHandlerContext ctx, String msg, ByteBuf out)
throws Exception {
// 在响应的数据后面添加分隔符
ctx.writeAndFlush(Unpooled.wrappedBuffer((msg + delimiter).getBytes()));
}
}

如果是

对客户端 & 服务端通讯加入[暗号]

在客户端和服务端数据通讯的时候,为了确保数据的完整性和安全性,通常会加入一段暗号,作为安全校验。其实两端真实的通信结构就变成了如下:

完整数据 = 暗号字节 + 真实数据内容

客户端和服务端在获取到数据之后,按照约定将暗号数据移除之后,剩下的就是正式的数据。

对于暗号的处理,可以通过MessageToMessageEncoder来实现,通过对获取到的通讯字节码编解码来对数据处理。

McnStringEncoder

/**
* copy自 StringEncoder源码,进行修改,增加了业务处理暗号
*/

class McnStringEncoder : MessageToMessageEncoder<CharSequence> {

var charset: Charset? = null

constructor(charset: Charset?) {
if (charset == null) {
throw NullPointerException("charset")
} else {
this.charset = charset
}
}

constructor() : this(Charset.defaultCharset())

override fun encode(ctx: ChannelHandlerContext?, msg: CharSequence?, out: MutableList<Any>?) {
if (msg?.isNotEmpty() == true) {
out?.add(
ByteBufUtil.encodeString(
ctx!!.alloc(),
CharBuffer.wrap(McnNettyConfig.private_key + msg),
charset
)
)
}
}
}

McnStringDecoder

/**
* copy自 StringDecoder源码,进行修改,增加了业务处理暗号
*/

class McnStringDecoder : MessageToMessageDecoder<ByteBuf> {
var charset: Charset? = null

constructor(charset: Charset?) {
if (charset == null) {
throw NullPointerException("charset")
} else {
this.charset = charset
}
}

constructor() : this(Charset.defaultCharset())

override fun decode(ctx: ChannelHandlerContext?, msg: ByteBuf?, out: MutableList<Any>?) {
msg?.run {
Log.e("info", "decoder结果====>${msg.toString(charset)}")
//校验报文长度是否合法
if (msg.readableBytes() <= McnNettyConfig.keyLength) {
out?.add(ErrorData.creator(ErrorData.LENGTH_ERROR, "报文长度校验失败"))
return
}
val privateKey = this.readBytes(McnNettyConfig.keyLength)
//校验报文暗号是否匹配
if (privateKey.toString(charset) != McnNettyConfig.private_key) {
out?.add(ErrorData.creator(ErrorData.PRIVATE_KEY_ERROR, "报文暗号校验失败"))
return
}
//获取真实报文内容
out?.add(this.toString(charset))
}
}

data class ErrorData(
var errorCode: Int,
var errorMsg: String
) {

companion object {

//长度异常
const val LENGTH_ERROR = -10001

//报文校验失败
const val PRIVATE_KEY_ERROR = -10002

@JvmStatic
fun creator(errorCode: Int, message: String): String {
val errorData = ErrorData(errorCode, message)
return JSON.toJSONString(errorData)
}
}
}
}

最后的使用就很简单了。只需要将我们的处理加到处理链就行了。

val pipeline = socketChannel.pipeline()
pipeline.addLast("decoder", McnStringDecoder())
pipeline.addLast("encoder", McnStringEncoder())

收起阅读 »

再谈协程之第三者Flow基础档案

该来的还是来了,LiveData提供了响应式编程的基础,搭建了一套数据观察者的使用框架,但是,它相当于RxJava这类的异步框架来说,有点略显单薄了,这也是经常被人诟病的问题,因此,Flow这个小三就顺应而生了。Flow作为一套异步数据流框架,几乎可以约等于R...
继续阅读 »

该来的还是来了,LiveData提供了响应式编程的基础,搭建了一套数据观察者的使用框架,但是,它相当于RxJava这类的异步框架来说,有点略显单薄了,这也是经常被人诟病的问题,因此,Flow这个小三就顺应而生了。

Flow作为一套异步数据流框架,几乎可以约等于RxJava,但借助Kotlin语法糖和协程,以及Kotlin的DSL语法,可以让Flow的写法变得异常简洁,让你直面人性最善良的地方,一切的黑暗和丑陋,都被编译器消化了。而且,Flow作为LiveData的进化版本,可以很好的和JetPack结合起来,作为全家桶的一员,为统一架构添砖加瓦。

要理解FLow,首先需要了解Flow的各种操作符和基础功能,如果不理解这些,那么很难将Flow灵活运用,所以,本节主要来梳理Flow的基础。

Flow前言

首先,我们来看一个新的概念——冷流和热流,如果你看网上的Flow相关的文章,十有八九都会提到这个很冷门的名词。

Flow是早上冷的,到Channel才热起来。

一个异步数据流,通常包含三部分:

  • 上游
  • 操作符
  • 下游

所谓冷流,即下游无消费行为时,上游不会产生数据,只有下游开始消费,上游才从开始产生数据。

而所谓热流,即无论下游是否有消费行为,上游都会自己产生数据。

Flow操作符

Flow和RxJava一样,用各种操作符撑起了异步数据流框架的半边天。Flow默认为冷流,即下游有消费时,才执行生产操作。

所以,操作符也被分为两类——中间操作符和末端操作符,中间操作符不会产生消费行为,返回依然为Flow,而末端操作符,会产生消费行为,即触发流的生产。

Flow的创建

仅仅创建Flow,是不会执行Flow中的任何代码的,但我们首先,还是要看下如何创建Flow。

  • flow

通过flow{}构造器,可以快速创建Flow,在flow中,可以使用emit来生产数据(或者emitAll生产批量数据),示例如下。

flow {
for (i in 0..3) {
emit(i.toString())
}
}
  • flowOf

与listOf类似,Flow可以通过flowOf来产生有限的已知数据。

flowOf(1, 2, 3)
  • asFlow

asFlow用于将List转换为Flow。

listOf(1,2,3).asFlow()
  • emptyFlow

如题,创建一个空流。

末端操作符

末端操作符在调用之后,创建Flow的代码才会执行,这点和Sequence非常类似。

  • collect

collect是最常用的末端操作符,示例如下。

末端操作符都是suspend函数,所以需要运行在协程作用域中。

MainScope().launch {
val time = measureTimeMillis {
flow {
for (i in 0..3) {
Log.d("xys", "emit value---$i")
emit(i.toString())
}
}.collect {
Log.d("xys", "Result---$it")
}
}
Log.d("xys", "Time---$time")
}
  • collectIndexed

带下标的collect,下标是Flow中的emit顺序。

MainScope().launch {
val time = measureTimeMillis {
flow {
for (i in 0..3) {
Log.d("xys", "emit value---$i")
emit(i.toString())
}
}.collectIndexed { index, value ->
Log.d("xys", "Result in $index --- $value")
}
}
Log.d("xys", "Time---$time")
}
  • collectLatest

collectLatest用于在collect中取消未来得及处理的数据,只保留当前最新的生产数据。

flowOf(1, 2, 3).collectLatest {
delay(1)
Log.d("xys", "Result---$it")
}
  • toCollection、toSet、toList

这些操作符用于将Flow转换为Collection、Set和List。

  • launchIn

在指定的协程作用域中直接执行Flow。

flow {
for (i in 0..3) {
Log.d("xys", "emit value---$i")
emit(i.toString())
}
}.launchIn(MainScope())
  • last、lastOrNull、first、firstOrNull

返回Flow的最后一个值(第一个值),区别是last为空的话,last会抛出异常,而lastOrNull可空。

flow {
for (i in 0..3) {
emit(i.toString())
}
}.last()

状态操作符

状态操作符不做任何修改,只是在合适的节点返回状态。

  • onStart:在上游生产数据前调用
  • onCompletion:在流完成或者取消时调用
  • onEach:在上游每次emit前调用
  • onEmpty:流中未产生任何数据时调用
  • catch:对上游中的异常进行捕获
  • retry、retryWhen:在发生异常时进行重试,retryWhen中可以拿到异常和当前重试的次数
MainScope().launch {
Log.d("xys", "Coroutine in ${Thread.currentThread().name}")
val time = measureTimeMillis {
flow {
for (i in 0..3) {
emit(i.toString())
}
throw Exception("Test")
}.retryWhen { _, retryCount ->
retryCount <= 3
}.onStart {
Log.d("xys", "Start Flow in ${Thread.currentThread().name}")
}.onEach {
Log.d("xys", "emit value---$it")
}.onCompletion {
Log.d("xys", "Flow Complete")
}.catch { error ->
Log.d("xys", "Flow Error $error")
}.collect {
Log.d("xys", "Result---$it")
}
}
Log.d("xys", "Time---$time")
}

另外,onCompletion也可以监听异常,代码如下所示。

.onCompletion { exception ->
Log.d("xys", "Result---$exception")
}

Transform操作符

与RxJava一样,在数据流中,我们可以利用操作符对数据进行各种变换,以满足操作流的不同需求。

  • map、mapLatest、mapNotNull

map操作符将Flow的输入通过block转换为新的输出。

flow {
for (i in 0..3) {
emit(i)
}
}.map {
it * it
}
  • transform、transformLatest

transform操作符与map操作符有点一样,但又不完全一样,map是一对一的变换,而transform则可以完全控制流的数据,进行过滤、 重组等等操作都可以。

flow {
for (i in 0..3) {
emit(i)
}
}.transform { value ->
if (value == 1) {
emit("!!!$value!!!")
}
}.collect {
Log.d("xys", "Result---$it")
}
  • transformWhile

transformWhile的返回值是一个bool类型,用来控制流的截断,如果返回true,则流继续执行,如果false,则流截断。

flow {
for (i in 0..3) {
emit(i)
}
}.transformWhile { value ->
emit(value)
value == 1
}.collect {
Log.d("xys", "Result---$it")
}

过滤操作符

如题,过滤操作符用于过滤流中的数据。

  • filter、filterInstance、filterNot、filterNotNull

过滤操作符可以按条件、类型或者对过滤取反、取非空等条件进行操作。

flow {
for (i in 0..3) {
emit(i)
}
}.filter { value ->
value == 1
}.collect {
Log.d("xys", "Result---$it")
}
  • drop、dropWhile、take、takeWhile

这类操作符可以丢弃前n个数据,或者是只拿前n个数据。带while后缀的,则表示按条件进行判断。

  • debounce

debounce操作符用于防抖,指定时间内的值只接收最新的一个。

  • sample

sample操作符与debounce操作符有点像,但是却限制了一个周期性时间,sample操作符获取的是一个周期内的最新的数据,可以理解为debounce操作符增加了周期的限制。

  • distinctUntilChangedBy

去重操作符,可以按照指定类型的参数进行去重。

组合操作符

组合操作符用于将多个Flow的数据进行组合。

  • combine、combineTransform

combine操作符可以连接两个不同的Flow。

val flow1 = flowOf(1, 2).onEach { delay(10) }
val flow2 = flowOf("a", "b", "c").onEach { delay(20) }
flow1.combine(flow2) { i, s -> i.toString() + s }.collect {
Log.d("xys", "Flow combine: $it")
}

输出为:

D/xys: Flow combine: 1a
D/xys: Flow combine: 2a
D/xys: Flow combine: 2b
D/xys: Flow combine: 2c

可以发现,当两个Flow数量不同时,始终由Flow1开始,用其最新的元素,与Flow2的最新的元素进行组合,形成新的元素。

  • merge

merge操作符用于将多个流合并。

val flow1 = flowOf(1, 2).onEach { delay(10) }
val flow2 = flowOf("a", "b", "c").onEach { delay(20) }
listOf(flow1, flow2).merge().collect {
Log.d("xys", "Flow merge: $it")
}

输出为:

D/xys: Flow merge: 1
D/xys: Flow merge: 2
D/xys: Flow merge: a
D/xys: Flow merge: b
D/xys: Flow merge: c

merge的输出结果是按照时间顺序,将多个流依次发射出来。

  • zip

zip操作符会分别从两个流中取值,当一个流中的数据取完,zip过程就完成了。

val flow1 = flowOf(1, 2).onEach { delay(10) }
val flow2 = flowOf("a", "b", "c").onEach { delay(20) }
flow1.zip(flow2) { i, s -> i.toString() + s }.collect {
Log.d("xys", "Flow zip: $it")
}

输出为:

D/xys: Flow zip: 1a
D/xys: Flow zip: 2b

线程切换

在Flow中,可以简单的使用flowOn来指定线程的切换,flowOn会对上游,以及flowOn之前的所有操作符生效。

flow {
for (i in 0..3) {
Log.d("xys", "Emit Flow in ${Thread.currentThread().name}")
emit(i)
}
}.map {
Log.d("xys", "Map Flow in ${Thread.currentThread().name}")
it * it
}.flowOn(Dispatchers.IO).collect {
Log.d("xys", "Collect Flow in ${Thread.currentThread().name}")
Log.d("xys", "Result---$it")
}

这种情况下,flow和map的操作都将在子线程中执行。

而如果是这样:

flow {
for (i in 0..3) {
Log.d("xys", "Emit Flow in ${Thread.currentThread().name}")
emit(i)
}
}.flowOn(Dispatchers.IO).map {
Log.d("xys", "Map Flow in ${Thread.currentThread().name}")
it * it
}.collect {
Log.d("xys", "Collect Flow in ${Thread.currentThread().name}")
Log.d("xys", "Result---$it")
}

这样map就会执行在主线程了。

同时,你也可以多次调用flowOn来不断的切换线程,让前面的操作符执行在不同的线程中。

取消Flow

Flow也是可以被取消的,最常用的方式就是通过withTimeoutOrNull来取消,代码如下所示。

MainScope().launch {
withTimeoutOrNull(2500) {
flow {
for (i in 1..5) {
delay(1000)
emit(i)
}
}.collect {
Log.d("xys", "Flow: $it")
}
}
}

这样当输出1、2之后,Flow就被取消了。

Flow的取消,实际上就是依赖于协程的取消。

Flow的同步非阻塞模型

首先,我们要理解下,什么叫同步非阻塞,默认场景下,Flow在没有切换线程的时候,运行在协程作用域指定的线程,这就是同步,那么非阻塞又是什么呢?我们知道emit和collect都是suspend函数,所谓suspend函数,就是会挂起,将CPU资源让出去,这就是非阻塞,因为suspend了就可以让一让,让给谁呢?让给其它需要执行的函数,执行完毕后,再把资源还给我。

所以,我们来看下面这个例子。

flow {
for (i in 0..3) {
emit(i)
}
}.onStart {
Log.d("xys", "Start Flow in ${Thread.currentThread().name}")
}.onEach {
Log.d("xys", "emit value---$it")
}.collect {
Log.d("xys", "Result---$it")
}

输出为:

D/xys: Start Flow in main
D/xys: emit value---0
D/xys: Result---0
D/xys: emit value---1
D/xys: Result---1
D/xys: emit value---2
D/xys: Result---2
D/xys: emit value---3
D/xys: Result---3

可以发现,emit一个,collect拿一个,这就是同步非阻塞,互相谦让,这样谁都可以执行,看上去flow中的代码和collect中的代码,就是同步执行的。

异步非阻塞模型

假如我们给Flow增加一个线程切换,让Flow执行在子线程,同样是上面的代码,我们再来看下执行情况。

flow {
for (i in 0..3) {
emit(i)
}
}.onStart {
Log.d("xys", "Start Flow in ${Thread.currentThread().name}")
}.onEach {
Log.d("xys", "emit value---$it")
}.flowOn(Dispatchers.IO).collect {
Log.d("xys", "Collect Flow in ${Thread.currentThread().name}")
Log.d("xys", "Result---$it")
}

输出为:

D/xys: Start Flow in DefaultDispatcher-worker-1
D/xys: emit value---0
D/xys: emit value---1
D/xys: emit value---2
D/xys: emit value---3
D/xys: Collect Flow in main
D/xys: Result---0
D/xys: Collect Flow in main
D/xys: Result---1
D/xys: Collect Flow in main
D/xys: Result---2
D/xys: Collect Flow in main
D/xys: Result---3

这个时候,Flow就变成了异步非阻塞模型,异步呢,就更好理解了,因为在不同线程,而此时的非阻塞,就没什么意义了,由于flow代码先执行,而这里的代码由于没有delay,所以是同步执行的,执行的同时,collect在主线程进行监听。

除了使用flowOn来切换线程,使用channelFlow也可以实现异步非阻塞模型。


收起阅读 »

Hilt 扩展 | MAD Skills

案例: WorkManager 扩展Hilt 扩展是一个生成代码的库,常通过注解处理器实现。生成的代码作为构成 Hilt 依赖项注入关系图的模块或入口点。Jetpack 中 WorkManager 的集成库就是一个扩展的例子。WorkManager ...
继续阅读 »

案例: WorkManager 扩展

Hilt 扩展是一个生成代码的库,常通过注解处理器实现。生成的代码作为构成 Hilt 依赖项注入关系图的模块或入口点。

Jetpack 中 WorkManager 的集成库就是一个扩展的例子。WorkManager 扩展帮助我们减少向 worker 提供依赖项时所需的模板代码及配置。该库由两部分组成,分别为 androidx.hilt:hilt-work 和 androidx.hilt:hilt-compiler。第一部分包含 HiltWorker 注解以及一些运行时的辅助类,第二部分是一个注解处理器,根据第一部分中注解提供的信息生成模块。

扩展的使用非常简单,仅需在您的 worker 上添加 @HiltWorker 注解:

@HiltWorker
public class ExampleWorker extends Worker {
// ...
}

扩展编译器会生成一个添加了 @Module 注解的类:

@Generated("androidx.hilt.AndroidXHiltProcessor")
@Module
@InstallIn(SingletonComponent.class)
@OriginatingElement(
topLevelClass = ExampleWorker.class
)
public interface ExampleWorker_HiltModule {
@Binds
@IntoMap
@StringKey("my.app.ExmapleWorker")
WorkerAssistedFactory<? extends ListenableWorker> bind(
ExampleWorker_AssistedFactory factory);
}

该模块为 worker 定义了一个可以访问 HiltWorkerFactory 的绑定。然后,配置 WorkerManager 使用该 factory,从而使 worker 的依赖项注入可用。

Hilt 聚合

启用扩展的一个关键机制是 Hilt 能够从类路径中发现模块和入口点。这被称为聚合,因为模块和入口点被聚合到带有 @HiltAndroidApp 注解的 Application 中。

由于 Hilt 具有聚合能力,任何通过添加 @InstallIn 注解生成 @Module 及 @EntryPoint 的工具都可以被 Hilt 发现,并在编译期成为 Hilt DI 图中的一部分。这使得扩展可以轻松地以插件形式集成到 Hilt,无需开发者处理任何额外工作。

注解处理器

生成代码的常规途径是使用注解处理器。源文件转换为 class 文件之前,注解处理器会在编译器中运行。当资源带有处理器所声明的已支持的注解时,处理器会进行处理。处理器可以生成进一步需要被处理的方法,因此编译器会不断循环运行注解处理器,直到没有新的内容产生。一旦所有的环节都完成,编译器才会将源文件转换为 class 文件。

△ 注解处理示意图

△ 注解处理示意图

由于循环机制,处理器可以相互作用。这非常重要,因为这使得 Hilt 的注解处理器可以处理由其他处理器生成的 @Module 或 @EntryPoint 类。这也意味着您的扩展也可以建立在其他人编写的扩展之上!

WorkManager extension processor 根据带有 @HiltWorker 注解的类生成代码,同时验证注解用法并使用 JavaPoet 等库生成代码。

Hilt 扩展注解

Hilt API 中有两个重要的注解: @GeneratesRootInput 和 @OriginatingElement。扩展应该使用这些注解才能与 Hilt 正确集成。

扩展应该使用 @GeneratesRootInput 来启用代码生成的注解。这让 Hilt 注解处理器知道它应该在生成组件之前完成扩展注解处理器的工作。例如,@HiltWorker 注解本身是被 @GeneratesRootInput 注解修饰的:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
@GeneratesRootInput
public @interface HiltWorker {
}

所生成的带有 @Module、@EntryPoint 以及 @InstallIn 注解的类都需要添加 @OriginatingElement 注解,该注解的输入参数是触发模块或入口点生成的顶层类。这就是 Hilt 判断生成的模块和入口点是否在本地测试的依据。例如,在 Hilt 测试中定义了一个添加 @HiltWorker 注解的内部类,模块的初始元素就是测试值。

测试案例如下:

@HiltAndroidTest
class SampleTest {
@HiltWorker
class TestWorker extends Worker {
// …
}
}

生成的模块包含 @OriginatingElement 注解:

@Module
@InstallIn(SingletonComponent.class)
@OriginatingElement(
topLevelClass = SampleTest.class
)
public interface SampleTest_TestWorker__HiltModule {
// …
}

心得

Hilt 扩展支持多种可能性,以下是创建扩展的一些心得:

项目中的通用模式

如果您的项目中有创建模块或入口点的通用模式,那么它们很大概率可以通过使用 Hilt 扩展实现自动化。举个例子,如果每一个实现特定接口的类都必须创建一个具有多绑定的模块,那么可以创建一个扩展,只需在实现类上添加注解即可生成多重绑定模块。

支持非标准成员注入

对于那些 Framework 中已经支持带有实例化能力的成员注入类型,我们需要创建一个 @EntryPoint。如果有多种类型需要被成员注入,那么自动创建入口点的扩展会很有用。例如,需要通过 ServiceLoader 发现服务实现的库负责实例化发现的服务。为了将依赖项注入到服务实现中,必须创建一个 @EntryPoint。通过使用 Hilt 扩展,可以使用在实现类上添加注解完成自动生成入口点。扩展可以进一步生成代码以使用入口点,例如由服务实现扩展的基类。这类似于 @AndroidEntryPoint 为 Activity 创建 @EntryPoint,并创建使用生成的入口点在 Activity 中执行成员注入的基类。

镜像绑定

有时需要使用不同的限定符来镜像或重新声明绑定。当存在自定义组件时,这可能更常见。为了避免丢失重新声明的绑定,可以创建 Hilt 扩展以自动生成其他镜像绑定的模块。例如,考虑包含不同依赖项实现的应用中 "付费" 和 "免费" 订阅的情况。然后,每一层都有两个不同的自定义组件,这样您就可以确定依赖关系的作用域。当添加一个通用的未限定作用域的绑定时,定义绑定的模块可以在其 @InstallIn 中包含两个组件,也可以加载在父组件中,通常是单例组件。但是当绑定被限定作用域时,模块必须被复制,因为需要不同的限定符。实现一个扩展就可以生成两个模块,可以避免样板代码并确保不会遗漏通用绑定。

总结

Hilt 的扩展可以进一步增强代码库中的依赖项注入能力,因为它们可以实现与 Hilt 尚不支持的其他库集成。总而言之,扩展通常由两部分组成,包含扩展注解的运行时部分,以及生成 @Module 或 @EntryPoint 的代码生成器 (通常是注解处理器)。扩展的运行时部分可能有额外的辅助类,这些辅助类使用声明在生成的模块或入口点中绑定。代码生成器还可能生成与扩展相关的附加代码,它们无需专门生成模块和入口点。

扩展必须使用两个注解才能与 Hilt 正确交互:

  • @GeneratesRootInput 添加在扩展注解上。
  • @OriginatingElement 由扩展添加在生成的模块或入口点上。

最后,您可以查看 hilt-install-binding 项目,这是一个简单扩展的示例,它展示了本文中提到的概念。

以上便是 MAD Skills 系列关于 Hilt 的全部内容,如需观看视频全集,请移步到 Hilt - MAD Skills 播放列表。感谢阅读本文!

欢迎您 点击这里 向我们提交反馈,或分享您喜欢的内容、发现的问题。您的反馈对我们非常重要,感谢您的支持!

收起阅读 »

面试官:Java从编译到执行,发生了什么?

面试官:今天从基础先问起吧,你是怎么理解Java是一门「跨平台」的语言,也就是「一次编译,到处运行的」?候选者:很好理解啊,因为我们有JVM。候选者:Java源代码会被编译为class文件,class文件是运行在JVM之上的。候选者:当我们日常开发安装JDK的...
继续阅读 »

面试官:今天从基础先问起吧,你是怎么理解Java是一门「跨平台」的语言,也就是「一次编译,到处运行的」?

候选者:很好理解啊,因为我们有JVM。

候选者:Java源代码会被编译为class文件,class文件是运行在JVM之上的。

候选者:当我们日常开发安装JDK的时候,可以发现JDK是分「不同的操作系统」,JDK里是包含JVM的,所以Java依赖着JVM实现了『跨平台』

候选者:JVM是面向操作系统的,它负责把Class字节码解释成系统所能识别的指令并执行,同时也负责程序运行时内存的管理。

面试官那要不你来聊聊从源码文件(.java)到代码执行的过程呗?

候选者:嗯,没问题的

候选者:简单总结的话,我认为就4个步骤:编译->加载->解释->执行

候选者:编译:将源码文件编译成JVM可以解释的class文件。

候选者:编译过程会对源代码程序做 「语法分析」「语义分析」「注解处理」等等处理,最后才生成字节码文件。

候选者:比如对泛型的擦除和我们经常用的Lombok就是在编译阶段干的。

候选者:加载:将编译后的class文件加载到JVM中。

候选者:在加载阶段又可以细化几个步骤:装载->连接->初始化

候选者:下面我对这些步骤又细说下哈。

候选者:【装载时机】为了节省内存的开销,并不会一次性把所有的类都装载至JVM,而是等到「有需要」的时候才进行装载(比如new和反射等等)

候选者:【装载发生】class文件是通过「类加载器」装载到jvm中的,为了防止内存中出现多份同样的字节码,使用了双亲委派机制(它不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上)

候选者:【装载规则】JDK 中的本地方法类一般由根加载器(Bootstrp loader)装载,JDK 中内部实现的扩展类一般由扩展加载器(ExtClassLoader )实现装载,而程序中的类文件则由系统加载器(AppClassLoader )实现装载。

候选者:装载这个阶段它做的事情可以总结为:查找并加载类的二进制数据,在JVM「堆」中创建一个java.lang.Class类的对象,并将类相关的信息存储在JVM「方法区」中

面试官:嗯…

候选者:通过「装载」这个步骤后,现在已经把class文件装载到JVM中了,并创建出对应的Class对象以及类信息存储至方法区了。

候选者:「连接」这个阶段它做的事情可以总结为:对class的信息进行验证、为「类变量」分配内存空间并对其赋默认值。

候选者:连接又可以细化为几个步骤:验证->准备->解析

候选者:1. 验证:验证类是否符合 Java 规范和 JVM 规范

候选者:2. 准备:为类的静态变量分配内存,初始化为系统的初始值

候选者:3. 解析:将符号引用转为直接引用的过程

面试官:嗯…

候选者:通过「连接」这个步骤后,现在已经对class信息做校验并分配了内存空间和默认值了。

候选者:接下来就是「初始化」阶段了,这个阶段可以总结为:为类的静态变量赋予正确的初始值。

候选者:过程大概就是收集class的静态变量、静态代码块、静态方法至()方法,随后从上往下开始执行。

候选者:如果「实例化对象」则会调用方法对实例变量进行初始化,并执行对应的构造方法内的代码。

候选者:扯了这么多,现在其实才完成至(编译->加载->解释->执行)中的加载阶段,下面就来说下【解释阶段】做了什么

候选者:初始化完成之后,当我们尝试执行一个类的方法时,会找到对应方法的字节码的信息,然后解释器会把字节码信息解释成系统能识别的指令码。

候选者:「解释」这个阶段它做的事情可以总结为:把字节码转换为操作系统识别的指令

候选者:在解释阶段会有两种方式把字节码信息解释成机器指令码,一个是字节码解释器、一个是即时编译器(JIT)。

候选者:JVM会对「热点代码」做编译,非热点代码直接进行解释。当JVM发现某个方法或代码块的运行特别频繁的时候,就有可能把这部分代码认定为「热点代码」

候选者:使用「热点探测」来检测是否为热点代码。「热点探测」一般有两种方式,计数器和抽样。HotSpot使用的是「计数器」的方式进行探测,为每个方法准备了两类计数器:方法调用计数器和回边计数器

候选者:这两个计数器都有一个确定的阈值,当计数器超过阈值溢出了,就会触发JIT编译。

候选者:即时编译器把热点方法的指令码保存起来,下次执行的时候就无需重复的进行解释,直接执行缓存的机器语言

面试官:嗯…

候选者:解释阶段结束后,最后就到了执行阶段。

候选者:「执行」这个阶段它做的事情可以总结为:操作系统把解释器解析出来的指令码,调用系统的硬件执行最终的程序指令。

候选者:上面就是我对从源码文件(.java)到代码执行的过程的理解了。

面试官:嗯…我还想问下你刚才提到的双亲委派模型…

候选者:下次一定!

本文总结:

  • Java跨平台因为有JVM屏蔽了底层操作系统

  • Java源码到执行的过程,从JVM的角度看可以总结为四个步骤:编译->加载->解释->执行

    • 「编译」经过 语法分析、语义分析、注解处理 最后才生成会class文件
    • 「加载」又可以细分步骤为:装载->连接->初始化。装载则把class文件装载至JVM,连接则校验class信息、分配内存空间及赋默认值,初始化则为变量赋值为正确的初始值。连接里又可以细化为:验证、准备、解析
    • 「解释」则是把字节码转换成操作系统可识别的执行指令,在JVM中会有字节码解释器和即时编译器。在解释时会对代码进行分析,查看是否为「热点代码」,如果为「热点代码」则触发JIT编译,下次执行时就无需重复进行解释,提高解释速度
    • 「执行」调用系统的硬件执行最终的程序指令


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

收起阅读 »

面试官:双亲委派模型你了解吗?

面试官:要不你今天来详细讲讲双亲委派机制? 候选者:嗯,好的。 候选者:上次提到了:class文件是通过「类加载器」装载至JVM中的 候选者:为了防止内存中存在多份同样的字节码,使用了双亲委派机制(它不会自己去尝试加载类,而是把请求委托给父加载器去完成,依次向...
继续阅读 »

面试官要不你今天来详细讲讲双亲委派机制?


候选者:嗯,好的。


候选者:上次提到了:class文件是通过「类加载器」装载至JVM中的


候选者:为了防止内存中存在多份同样的字节码,使用了双亲委派机制(它不会自己去尝试加载类,而是把请求委托给父加载器去完成,依次向上)


候选者:JDK 中的本地方法类一般由根加载器(Bootstrp loader)装载,JDK 中内部实现的扩展类一般由扩展加载器(ExtClassLoader )实现装载,而程序中的类文件则由系统加载器(AppClassLoader )实现装载。



候选者:这应该很好理解吧?


面试官:雀食(确实)!


面试官顺着话题,我想问问,打破双亲委派机制是什么意思?


候选者:很好理解啊,意思就是:只要我加载类的时候,不是从APPClassLoader->Ext ClassLoader->BootStrap ClassLoader 这个顺序找,那就算是打破了啊


候选者:因为加载class核心的方法在LoaderClass类的loadClass方法上(双亲委派机制的核心实现)


候选者:那只要我自定义个ClassLoader,重写loadClass方法(不依照往上开始寻找类加载器),那就算是打破双亲委派机制了。


面试官:这么简单?


候选者:嗯,就是这么简单


面试官那你知道有哪个场景破坏了双亲委派机制吗?


候选者:最明显的就Tomcat啊


面试官:详细说说?


候选者:在初学时部署项目,我们是把war包放到tomcat的webapp下,这意味着一个tomcat可以运行多个Web应用程序(:


候选者:是吧?


面试官:嗯..


候选者:那假设我现在有两个Web应用程序,它们都有一个类,叫做User,并且它们的类全限定名都一样,比如都是com.yyy.User。但是他们的具体实现是不一样的


候选者:那么Tomcat是如何保证它们是不会冲突的呢?


候选者:答案就是,Tomcat给每个 Web 应用创建一个类加载器实例(WebAppClassLoader),该加载器重写了loadClass方法,优先加载当前应用目录下的类,如果当前找不到了,才一层一层往上找(:


候选者:那这样就做到了Web应用层级的隔离



面试官嗯,那你还知道Tomcat还有别的类加载器吗?


候选者:嗯,知道的


候选者:并不是Web应用程序下的所有依赖都需要隔离的,比如Redis就可以Web应用程序之间共享(如果有需要的话),因为如果版本相同,没必要每个Web应用程序都独自加载一份啊。


候选者:做法也很简单,Tomcat就在WebAppClassLoader上加了个父类加载器(SharedClassLoader),如果WebAppClassLoader自身没有加载到某个类,那就委托SharedClassLoader去加载。


候选者:(无非就是把需要应用程序之间需要共享的类放到一个共享目录下嘛)


面试官:嗯..


候选者:为了隔绝Web应用程序与Tomcat本身的类,又有类加载器(CatalinaClassLoader)来装载Tomcat本身的依赖


候选者:如果Tomcat本身的依赖和Web应用还需要共享,那么还有类加载器(CommonClassLoader)来装载进而达到共享


候选者:各个类加载器的加载目录可以到tomcat的catalina.properties配置文件上查看


候选者:我稍微画下Tomcat的类加载结构图吧,不然有点抽象



面试官:嗯,还可以,我听懂了,有点意思。


面试官顺便,我想问下,JDBC你不是知道吗,听说它也是破坏了双亲委派模型的,你怎么理解的。


候选者:Eumm,这个有没有破坏,见仁见智吧。


候选者:JDBC定义了接口,具体实现类由各个厂商进行实现嘛(比如MySQL)


候选者:类加载有个规则:如果一个类由类加载器A加载,那么这个类的依赖类也是由「相同的类加载器」加载。


候选者:我们用JDBC的时候,是使用DriverManager进而获取Connection,DriverManager在java.sql包下,显然是由BootStrap类加载器进行装载


候选者:当我们使用DriverManager.getConnection()时,得到的一定是厂商实现的类。


候选者:但BootStrap ClassLoader会能加载到各个厂商实现的类吗?


候选者:显然不可以啊,这些实现类又没在java包中,怎么可能加载得到呢


面试官:嗯..


候选者:DriverManager的解决方案就是,在DriverManager初始化的时候,得到「线程上下文加载器」


候选者:去获取Connection的时候,是使用「线程上下文加载器」去加载Connection的,而这里的线程上下文加载器实际上还是App ClassLoader


候选者:所以在获取Connection的时候,还是先找Ext ClassLoader和BootStrap ClassLoader,只不过这俩加载器肯定是加载不到的,最终会由App ClassLoader进行加载



面试官:嗯..


候选者:那这种情况,有的人觉得破坏了双亲委派机制,因为本来明明应该是由BootStrap ClassLoader进行加载的,结果你来了一手「线程上下文加载器」,改掉了「类加载器」


候选者:有的人觉得没破坏双亲委派机制,只是改成由「线程上下文加载器」进行类加载,但还是遵守着:「依次往上找父类加载器进行加载,都找不到时才由自身加载」。认为”原则”上是没变的。


面试官:那我了解了


本文总结




  • 前置知识: JDK中默认类加载器有三个:AppClassLoader、Ext ClassLoader、BootStrap ClassLoader。AppClassLoader的父加载器为Ext ClassLoader、Ext ClassLoader的父加载器为BootStrap ClassLoader。这里的父子关系并不是通过继承实现的,而是组合。




  • 什么是双亲委派机制: 加载器在加载过程中,先把类交由父类加载器进行加载,父类加载器没找到才由自身加载。




  • 双亲委派机制目的: 为了防止内存中存在多份同样的字节码(安全)




  • 类加载规则: 如果一个类由类加载器A加载,那么这个类的依赖类也是由「相同的类加载器」加载。




  • 如何打破双亲委派机制: 自定义ClassLoader,重写loadClass方法(只要不依次往上交给父加载器进行加载,就算是打破双亲委派机制)




  • 打破双亲委派机制案例: Tomcat



    • 为了Web应用程序类之间隔离,为每个应用程序创建WebAppClassLoader类加载器

    • 为了Web应用程序类之间共享,把ShareClassLoader作为WebAppClassLoader的父类加载器,如果WebAppClassLoader加载器找不到,则尝试用ShareClassLoader进行加载

    • 为了Tomcat本身与Web应用程序类隔离,用CatalinaClassLoader类加载器进行隔离,CatalinaClassLoader加载Tomcat本身的类

    • 为了Tomcat与Web应用程序类共享,用CommonClassLoader作为CatalinaClassLoader和ShareClassLoader的父类加载器

    • ShareClassLoader、CatalinaClassLoader、CommonClassLoader的目录可以在Tomcat的catalina.properties进行配置




  • 线程上下文加载器: 由于类加载的规则,很可能导致父加载器加载时依赖子加载器的类,导致无法加载成功(BootStrap ClassLoader无法加载第三方库的类),所以存在「线程上下文加载器」来进行加载。


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

Flutter 快速开发框架

项目简介 此框架旨在将常规的Flutter项目中使用到的通用(与业务无关)的功能从剥离出来,构成Flutter开发项目的框架,在开发新的Flutter项目时,可以直接引用本项目 import 'package:framework/framework.dart'...
继续阅读 »

项目简介


此框架旨在将常规的Flutter项目中使用到的通用(与业务无关)的功能从剥离出来,构成Flutter开发项目的框架,在开发新的Flutter项目时,可以直接引用本项目 import 'package:framework/framework.dart'来使用框架中相关的功能,提升开发效率。


Github项目地址:可以看我主页找我拿


此框架目前包含以下功能模块:接口请求API模块、消息提示模块、路由模块、统一错误处理、日志模块、屏幕适配测试、自定义UI组件库、本地存储模块构成


框架使用说明


引用


import 'package:framework/framework.dart';

使用


参考 Example 中使用的例子

框架首页



网络HTTP模块



消息提示模块



路由模块



统一错误处理介绍



日志模块



屏幕适配测试



自定义UI组件库



本地存储模块



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

都 2021 年了,还有人在研究 Handler?

我们经常使用和提及 Android 中特有的线程间通信方式即 Handler 机制,缘于该机制特别好用、极为重要! 初尝 Handler 机制的时候,原以为 Handler 类发挥了很大的作用。当你深入了解它的原理之后,会发现 Handler 只是该机制的调用...
继续阅读 »

我们经常使用和提及 Android 中特有的线程间通信方式即 Handler 机制,缘于该机制特别好用、极为重要!


初尝 Handler 机制的时候,原以为 Handler 类发挥了很大的作用。当你深入了解它的原理之后,会发现 Handler 只是该机制的调用入口和回调而已,最重要的东西是 LooperMessagQueue,以及不断流转的 Message


本次针对 Handler 机制常被提及和容易困扰的 20 个问题进行整理和回答,供大家解惑和回顾~


问题前瞻:



  1. 简述下 Handler 机制的总体原理?

  2. Looper 存在哪?如何可以保证线程独有?

  3. 如何理解 ThreadLocal 的作用?

  4. 主线程 Main Looper 和一般 Looper 的异同?

  5. Handler 或者说 Looper 如何切换线程?

  6. Looper 的 loop() 死循环为什么不卡死?

  7. Looper 的等待是如何能够准确唤醒的?

  8. Message 如何获取?为什么这么设计?

  9. MessageQueue 如何管理 Message?

  10. 理解 Message 和 MessageQueue 的异同?

  11. Message 的执行时刻如何管理?

  12. Handler、Mesage 和 Runnable 的关系如何理解?

  13. IdleHandler 空闲 Message 了解过吗?有什么用?

  14. 异步 Message 或同步屏障了解过吗?怎么用?什么原理?

  15. Looper 和 MessageQueue、Message 及 Handler 的关系?

  16. Native 侧的 NativeMessageQueue 和 Looper 的作用是?

  17. Native 侧如何使用 Looper?

  18. Handler 为什么可能导致内存泄露?如何避免?

  19. Handler 在系统当中的应用

  20. Android 为什么不允许并发访问 UI?


1. 简述下 Handler 机制的总体原理?




  1. Looper 准备和开启轮循:



    • Looper#prepare() 初始化线程独有的 Looper 以及 MessageQueue

    • Looper#loop() 开启死循环读取 MessageQueue 中下一个满足执行时间的 Message

      • 尚无 Message 的话,调用 Native 侧的 pollOnce() 进入无限等待

      • 存在 Message,但执行时间 when 尚未满足的话,调用 pollOnce() 时传入剩余时长参数进入有限等待






  2. Message 发送、入队和出队:



    • Native 侧如果处于无限等待的话:任意线程向 Handler 发送 MessageRunnable 后,Message 将按照 when 条件的先后,被插入 Handler 持有的 Looper 实例所对应的 MessageQueue 中适当的位置。 MessageQueue 发现有合适的 Message 插入后将调用 Native 侧的 wake() 唤醒无限等待的线程。这将促使 MessageQueue 的读取继续进入下一次循环,此刻 Queue 中已有满足条件的 Message 则出队返回给 Looper

    • Native 侧如果处于有限等待的话:在等待指定时长后 epoll_wait 将返回。线程继续读取 MessageQueue,此刻因为时长条件将满足将其出队




  3. Looper 处理 Message 的实现:


    Looper 得到 Message 后回调 Message 的 callback 属性即 Runnable,或依据 target 属性即 Handler,去执行 Handler 的回调。



    • 存在 mCallback 属性的话回调 Handler$Callback

    • 反之,回调 handleMessage()




2. Looper 存在哪?如何可以保证线程独有?



  • Looper 实例被管理在静态属性 sThreadLocal

  • ThreadLocal 内部通过 ThreadLocalMap 持有 Looper,key 为 ThreadLocal 实例本身,value 即为 Looper 实例

  • 每个 Thread 都有一个自己的 ThreadLocalMap,这样可以保证每个线程对应一个独立的 Looper 实例,进而保证 myLooper() 可以获得线程独有的 Looper


彩蛋:一个 App 拥有几个 Looper 实例?几个 ThreadLocal 实例?几个 MessageQueue 实例?几个 Message 实例?几个 Handler 实例



  • 一个线程只有一个 Looper 实例

  • 一个 Looper 实例只对应着一个 MessageQueue 实例

  • 一个 MessageQueue 实例可对应多个 Message 实例,其从 Message 静态池里获取,存在 50 的上限

  • 一个线程可以拥有多个 Handler 实例,其Handler 只是发送和执行任务逻辑的入口和出口

  • ThreadLocal 实例是静态的,整个进程共用一个实例。每个 Looper 存放的 ThreadLocalMap 均弱引用它作为 key


3. 如何理解 ThreadLocal 的作用?



  • 首先要明确并非不是用来切换线程的,只是为了让每个线程方便程获取自己的 Looper 实例,见 Looper#myLooper()

    • 后续可供 Handler 初始化时指定其所属的 Looper 线程

    • 也可用来线程判断自己是否是主线程




4. 主线程 Main Looper 和一般 Looper 的异同?




  • 区别:



    1. Main Looper 不可 quit


    主线程需要不断读取系统消息和用书输入,是进程的入口,只可被系统直接终止。进而其 Looper 在创建的时候设置了不可 quit 的标志,而其他线程的 Looper 则可以也必须手动 quit



    1. Main Looper 实例还被静态缓存


    为了便于每个线程获得主线程 Looper 实例,见 Looper#getMainLooper(),Main Looper 实例还作为 sMainLooper 属性缓存到了 Looper 类中。




  • 相同点:



    1. 都是通过 Looper#prepare() 间接调用 Looper 构造函数创建的实例

    2. 都被静态实例 ThreadLocal 管理,方便每个线程获取自己的 Looper 实例




彩蛋:主线程为什么不用初始化 Looper?


App 的入口并非 MainActivity,也不是 Application,而是 ActivityThread。


其为了 Application、ContentProvider、Activity 等组件的运行,必须事先启动不停接受输入的 Looper 机制,所以在 main() 执行的最后将调用 prepareMainLooper() 创建 Looper 并调用 loop() 轮循。


不需要我们调用,也不可能有我们调用。


可以说如果主线程没有创建 Looper 的话,我们的组件也不可能运行得到!


5. Handler 或者说 Looper 如何切换线程?




  1. Handler 创建的时候指定了其所属线程的 Looper,进而持有了 Looper 独有的 MessageQueue




  2. Looper#loop() 会持续读取 MessageQueue 中合适的 Message,没有 Message 的时候进入等待




  3. 当向 Handler 发送 Message 或 Runnable 后,会向持有的 MessageQueue 中插入 Message




  4. Message 抵达并满足条件后会唤醒 MessageQueue 所属的线程,并将 Message 返回给 Looper




  5. Looper 接着回调 Message 所指向的 Handler Callback 或 Runnable,达到线程切换的目的




简言之,向 Handler 发送 Message 其实是向 Handler 所属线程的独有 MessageQueue 插入 Message。而线程独有的 Looper 又会持续读取该 MessageQueue。所以向其他线程的 Handler 发送完 Message,该线程的 Looper 将自动响应。


6. Looper 的 loop() 死循环为什么不卡死?


为了让主线程持续处理用户的输入,loop() 是死循环,持续调用 MessageQueue#next() 读取合适的 Message。


但当没有 Message 的时候,会调用 pollOnce() 并通过 Linux 的 epoll 机制进入等待并释放资源。同时 eventFd 会监听 Message 抵达的写入事件并进行唤醒。


这样可以空闲时释放资源、不卡死线程,同时能持续接收输入的目的


彩蛋1:loop() 后的处理为什么不可执行


因为 loop() 是死循环,直到 quit 前后面的处理无法得到执行,所以避免将处理放在 loop() 的后面。


**彩蛋2:Looper 等待的时候线程到底是什么状态? **


调用 Linux 的 epoll 机制进入等待,事实上 Java 侧打印该线程的状态,你会发现线程处于 Runnable 状态,只不过 CPU 资源被暂时释放。


7. Looper 的等待是如何能够准确唤醒的?


读取合适 Message 的 MessageQueue#next() 会因为 Message 尚无或执行条件尚未满足进行两种等的等待:




  • 无限等待


    尚无 Message(队列中没有 Message 或建立了同步屏障但尚无异步 Message)的时候,调用 Natvie 侧的 pollOnce() 会传入参数 -1


    Linux 执行 epoll_wait() 将进入无限等待,其等待合适的 Message 插入后调用 Native 侧的 wake() 向唤醒 fd 写入事件触发唤醒 MessageQueue 读取的下一次循环




  • 有限等待


    有限等待的场合将下一个 Message 剩余时长作为参数交给 epoll_wait(),epoll 将等待一段时间之后自动返回,接着回到 MessageQueue 读取的下一次循环




8. Message 如何获取?为什么这么设计?




  • 享元设计模式:通过 Message 的静态方法 obatin() 获取,因为该方法不是无脑地 new,而是从单链表池子里获取实例,并在 recycle() 后将其放回池子




  • 好处在于复用 Message 实例,满足频繁使用 Message 的场景,更加高效




  • 当然,缓存池存在上限 50,因为没必要无限制地缓存,这本身也是一种浪费




  • 需要留意缓存池是静态的,也就是整个进程共用一个缓存池




9. MessageQueue 如何管理 Message?



  • MessageQueue 通过单链表管理 Message,不同于进程共用的 Message Pool,其是线程独有的

  • 通过 Message 的执行时刻 when 对 Message 进行排队和出队

  • MessageQueue 除了管理 Message,还要管理空闲 Handler 和 同步屏障


10. 理解 Message 和 MessageQueue 的异同?




  • 相同点:都是通过单链表来管理 Message 实例;




    • Message 通过 obtain() 和 recycle() 向单链表获取插入节点




    • MessageQueue 通过 enqueueMessage() 和 next() 向单链表获取和插入节点






  • 区别:



    • Message 单链表是静态的,供进程使用的缓存池




  • MessageQueue 单链表非静态,只供 Looper 线程使用




11. Message 的执行时刻如何管理?



  • 发送的 Message 都是按照执行时刻 when 属性的先后管理在 MessageQueue 里

    • 延时 Message 的 when 等于调用的当前时刻delay 之和

    • 非延时 Message 的 when 等于当前时刻(delay 为 0

    • 插队 Message 的 when 固定为 0,便于插入队列的 head



  • 之后 MessageQueue 会根据读取的时刻和 when 进行比较

    • 将 when 已抵达的出队,

    • 尚未抵达的计算出当前时刻和目标 when 的插值,交由 Native 等待对应的时长,时间到了自动唤醒继续进行 Message 的读取




事实上,无论上述哪种 Message 都不能保证在其对应的 when 时刻执行,往往都会延迟一些!因为必须等当前执行的 Message 处理完了才有机会读取队列的下一个 Message。


比如发送了非延时 Message,when 即为发送的时刻,可它们不会立即执行。都要等主线程现有的任务(Message)走完才能有机会出队,而当这些任务执行完 when 的时刻已经过了。假使队列的前面还有其他 Message 的话,延迟会更加明显!


彩蛋:. onCreate() 里向 Handler 发送大量 Message 会导致主线程卡顿吗?


不会,发送的大量 Message 并非立即执行,只是先放到队列当中而已。


onCreate() 以及之后同步调用的 onStart() 和 onResume() 处理,本质上也是 Message。等这个 Message 执行完之后,才会进行读取 Message 的下一次循环,这时候才能回调 onCreate 里发送的 Message。


需要说明的是,如果发送的是 FrontOfQueue 将 Message 插入队首也不会立即先执行,因为 onStart 和 onResume 是 onCreate 之后同步调用的,本质上是同一个 Message 的作业周期


12. Handler、Mesage 和 Runnable 的关系如何理解?



  • 作为使用 Handler 机制的入口,Handler 是发送 Message 或 Runnable 的起点

  • 发送的 Runnable 本质上也是 Message,只不过作为 callback 属性被持有

  • Handler 作为 target 属性被持有在 Mesage 中,在 Message 执行条件满足的时候供 Looper 回调


事实上,Handler 只是供 App 使用 Handler 机制的 API,实质来说,Message 是更为重要的载体。


13. IdleHandler 空闲 Message 了解过吗?有什么用?




  • 适用于期望空闲时候执行,但不影响主线程操作的任务




  • 系统应用:



    1. Activity destroy 回调就放在了 IdleHandler

    2. ActivityThreadGCHandler 使用了 IdleHandler,在空闲的时候执行 GC 操作




  • App 应用:



    1. 发送一个返回 true 的 IdleHandler,在里面让某个 View 不停闪烁,这样当用户发呆时就可以诱导用户点击这个 View

    2. 将某部分初始化放在 IdleHandler 里不影响 Activity 的启动




  • 彩蛋问题:



    1. add/remove IdleHandler 的方法,是否需要成对使用?


    不需要,回调返回 false 也可以移除




    1. mIdleHanders 一直不为空时,为什么不会进入死循环?




    执行过 IdleHandler 之后会将计数重置为 0,确保下一次循环不重复执行



    1. 是否可以将一些不重要的启动服务,搬移到 IdleHandler 中去处理?


    最好不要,回调时机不太可控,需要搭配 remove 谨慎使用



    1. IdleHandle 的 queueIdle() 运行在那个线程?


    取决于 IdleHandler add 到的 MessageQueue 所处的线程




14. 异步 Message 或同步屏障了解过吗?怎么用?什么原理?




  • 异步 Message:设置了 isAsync 属性的 Message 实例



    • 可以用异步 Handler 发送

    • 也可以调用 Message#setAsynchronous() 直接设置为异步 Message




  • 同步屏障:在 MessageQueue 的某个位置放一个 target 属性为 null 的 Message,确保此后的非异步 Message 无法执行,只能执行异步 Message




  • 原理:当 MessageQueue 轮循 Message 时候发现建立了同步屏障的时候,会去跳过其他 Message,读取下个 async 的 Message 并执行,屏障移除之前同步 Message 都会被阻塞




  • 应用:比如屏幕刷新 Choreographer 就使用到了同步屏障,确保屏幕刷新事件不会因为队列负荷影响屏幕及时刷新。




  • 注意:同步屏障的添加或移除 API 并未对外公开,App 需要使用的话需要依赖反射机制




15. Looper 和 MessageQueue、Message 及 Handler 的关系?



  • Message 是承载任务的载体,在 Handler 机制中贯穿始终

  • Handler 则是对外公开的 API,负责发送 Message 和处理任务的回调,是 Message 的生产者

  • MessagQueue 负责管理待处理 Message 的入队和出队,是 Message 的容器

  • Looper 负责轮循 MessageQueue,保持线程持续运行任务,是 Message 的消费者


彩蛋:如何保证 MessageQueue 并发访问安全?


任何线程都可以通过 Handler 生产 Message 并放入 MessageQueue 中,可 Queue 所属的 Looper 在持续地读取并尝试消费 Message。如何保证两者不产生死锁?


Looper 在消费 Message 之前要先拿到 MessageQueue 的锁,**只不过没有 Message 或 Message 尚未满足条件的进行等待前会事先释放锁,**具体在于 nativePollOnce() 的调用在 synchronized 方法块的外侧。


Message 入队前也需先拿到 MessageQueue 的锁,而这时 Looper 线程正在等待且不持有锁,可以确保 Message 的成功入队。入队后执行唤醒后释放锁,Native 收到 event 写入后恢复 MessagQueue 的读取并可以拿到锁,成功出队。


这样一种在没有 Message 可以消费时执行等待同时不占着锁的机制,避免了生产和消费的死锁。


16. Native 侧的 NativeMessageQueue 和 Looper 的作用是?




  • NativeMessageQueue 负责连接 Java 侧的 MessageQueue,进行后续的 waitwake,后续将创建 wake 的FD,并通过 epoll 机制等待或唤醒。但并不参与管理 Java 的 Message




  • Native 侧也需要 Looper 机制,等待和唤醒的需求是同样的,所以将这部分实现都封装到了 JNI 的NativeMessageQueue 和 Native 的 Looper 中,供 Java 和 Native 一起使用




17. Native 侧如何使用 Looper?




  • Looper Native 部分承担了 Java 侧 Looper 的等待和唤醒,除此之外其还提供了 Message、MessageHandlerWeakMessageHandlerLooperCallbackSimpleLooperCallback 等 API




  • 这些部分可供 Looper 被 Native 侧直接调用,比如 InputFlinger 广泛使用了 Looper




  • 主要方法是调用 Looper 构造函数或 prepare 创建 Looper,然后通过 poll 开始轮询,接着 sendMessageaddEventFd,等待 Looper 的唤醒。使用过程和 Java 的调用思路类似




18. Handler 为什么可能导致内存泄露?如何避免?



  • 持有 Activity 实例的内名内部类或内部类的生命周期应当和 Activity 保持一致,否则产生内存泄露的风险。

  • 如果 Handler 使用不当,将造成不一致,表现为:匿名内部类或内部类写法的 Handler、Handler$Callback、Runnable,或者Activity 结束时仍有活跃的 Thread 线程或 Looper 子线程

  • 具体在于:异步任务仍然活跃或通过发送的 Message 尚未处理完毕,将使得内部类实例的生命周期被错误地延长。造成本该回收的 Activity 实例被别的 ThreadMain Looper 占据而无法及时回收(活跃的 Thread 或 静态属性 sMainLooper 是 GC Root 对象)

  • 建议的做法:

    • 无论是 Handler、Handler$Callback 还是 Runnable,尽量采用静态内部类 + 弱引用的写法,确保尽管发生不当引用的时候也可以因为弱引用能清楚持有关系

    • 另外在 Activity 销毁的时候及时地终止 Thread、停止子线程的 Looper 或清空 Message,确保彻底切断 Activity 经由 Message 抵达 GC Root 的引用源头(Message 清空后会其与 Handler 的引用关系,Thread 的终止将结束其 GC Root 的源头)




注意:静态的 sThreadLocal 实例不持有存放 Looper 实例的 ThreadLocalMap,而是由 Thread 持有。从这个角度上来讲,Looper 会被活跃的 GC Root Thread 持有,进而也可能导致内存泄露。


彩蛋:网传的 Handler$Callback 方案能否解决内存泄露?


不能。


Callback 采用内部类或匿名内部类写法的话,默认持有 Activity 的引用,而 Callback 被 Handler 持有。这最终将导致 Message -> Handler -> Callback -> Activity 的链条仍然存在。


19. Handler 在系统当中的应用


特别广泛,比如:



  • Activity 生命周期的管理

  • 屏幕刷新

  • HandlerThread、IntentService

  • AsyncTask 等。


主要利用 Handler 的切换线程、主线程异步 Message 的重要特性。注意:Binder 线程非主线程,但很多操作比如生命周期的管理都要回到主线程,所以很多 Binder 调用过来后都要通过 Handler 切换回主线程执行后续任务,比如 ActviityThread$H 就是 extends Handler。


20. Android 为什么不允许并发访问 UI?


Android 中 UI 非线程安全,并发访问的话会造成数据和显示错乱。


但此限制的检查始于ViewRootImpl#checkThread(),其会在刷新等多个访问 UI 的时机被调用,去检查当前线程,非主线程的话抛出异常。


而 ViewRootImpl 的创建在 onResume() 之后,也就是说如果在 onResume() 执行前启动线程访问 UI 的话是不会报错的,这点需要留意!


彩蛋:onCreate() 里子线程更新 UI 有问题吗?为什么?


不会。


因为异常的检测处理在 ViewRootImpl 中,该实例的创建和检测在 onResume() 之后进行。


结语


能力和精力有限,如果出现遗漏、错误或细节不明的地方,欢迎不吝赐教。


让我们共同维护这些个问题,彻底吃透 Handler 机制!


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

【知识点】OkHttp 原理 8 连问

前言 OkHttp可以说是Android开发中最常见的网络请求框架,OkHttp使用方便,扩展性强,功能强大,OKHttp源码与原理也是面试中的常客 但是OKHttp的源码内容比较多,想要学习它的源码往往千头万绪,一时抓不住重点. 本文从几个问题出发梳理OKH...
继续阅读 »

前言


OkHttp可以说是Android开发中最常见的网络请求框架,OkHttp使用方便,扩展性强,功能强大,OKHttp源码与原理也是面试中的常客

但是OKHttp的源码内容比较多,想要学习它的源码往往千头万绪,一时抓不住重点.

本文从几个问题出发梳理OKHttp相关知识点,以便快速构建OKHttp知识体系,如果对你有用,欢迎点赞~


本文主要包括以下内容



  1. OKHttp请求的整体流程是怎样的?

  2. OKHttp分发器是怎样工作的?

  3. OKHttp拦截器是如何工作的?

  4. 应用拦截器和网络拦截器有什么区别?

  5. OKHttp如何复用TCP连接?

  6. OKHttp空闲连接如何清除?

  7. OKHttp有哪些优点?

  8. OKHttp框架中用到了哪些设计模式?


1. OKHttp请求整体流程介绍


首先来看一个最简单的Http请求是如何发送的。


   val okHttpClient = OkHttpClient()
val request: Request = Request.Builder()
.url("https://www.google.com/")
.build()

okHttpClient.newCall(request).enqueue(object :Callback{
override fun onFailure(call: Call, e: IOException) {
}

override fun onResponse(call: Call, response: Response) {
}
})

这段代码看起来比较简单,OkHttp请求过程中最少只需要接触OkHttpClientRequestCallResponse,但是框架内部会进行大量的逻辑处理。

所有网络请求的逻辑大部分集中在拦截器中,但是在进入拦截器之前还需要依靠分发器来调配请求任务。

关于分发器与拦截器,我们在这里先简单介绍下,后续会有更加详细的讲解



  • 分发器:内部维护队列与线程池,完成请求调配;

  • 拦截器:五大默认拦截器完成整个请求过程。




整个网络请求过程大致如上所示



  1. 通过建造者模式构建OKHttpClientRequest

  2. OKHttpClient通过newCall发起一个新的请求

  3. 通过分发器维护请求队列与线程池,完成请求调配

  4. 通过五大默认拦截器完成请求重试,缓存处理,建立连接等一系列操作

  5. 得到网络请求结果


2. OKHttp分发器是怎样工作的?


分发器的主要作用是维护请求队列与线程池,比如我们有100个异步请求,肯定不能把它们同时请求,而是应该把它们排队分个类,分为正在请求中的列表和正在等待的列表,
等请求完成后,即可从等待中的列表中取出等待的请求,从而完成所有的请求


而这里同步请求各异步请求又略有不同


同步请求


synchronized void executed(RealCall call) {
runningSyncCalls.add(call);
}

因为同步请求不需要线程池,也不存在任何限制。所以分发器仅做一下记录。后续按照加入队列的顺序同步请求即可


异步请求


synchronized void enqueue(AsyncCall call) {
//请求数最大不超过64,同一Host请求不能超过5个
if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
runningAsyncCalls.add(call);
executorService().execute(call);
} else {
readyAsyncCalls.add(call);
}
}

当正在执行的任务未超过最大限制64,同时同一Host的请求不超过5个,则会添加到正在执行队列,同时提交给线程池。否则先加入等待队列。

每个任务完成后,都会调用分发器的finished方法,这里面会取出等待队列中的任务继续执行


3. OKHttp拦截器是怎样工作的?


经过上面分发器的任务分发,下面就要利用拦截器开始一系列配置了


# RealCall
override fun execute(): Response {
try {
client.dispatcher.executed(this)
return getResponseWithInterceptorChain()
} finally {
client.dispatcher.finished(this)
}
}

我们再来看下RealCallexecute方法,可以看出,最后返回了getResponseWithInterceptorChain,责任链的构建与处理其实就是在这个方法里面


internal fun getResponseWithInterceptorChain(): Response {
// Build a full stack of interceptors.
val interceptors = mutableListOf<Interceptor>()
interceptors += client.interceptors
interceptors += RetryAndFollowUpInterceptor(client)
interceptors += BridgeInterceptor(client.cookieJar)
interceptors += CacheInterceptor(client.cache)
interceptors += ConnectInterceptor
if (!forWebSocket) {
interceptors += client.networkInterceptors
}
interceptors += CallServerInterceptor(forWebSocket)

val chain = RealInterceptorChain(
call = this,interceptors = interceptors,index = 0
)
val response = chain.proceed(originalRequest)
}

如上所示,构建了一个OkHttp拦截器的责任链

责任链,顾名思义,就是用来处理相关事务责任的一条执行链,执行链上有多个节点,每个节点都有机会(条件匹配)处理请求事务,如果某个节点处理完了就可以根据实际业务需求传递给下一个节点继续处理或者返回处理完毕。

如上所示责任链添加的顺序及作用如下表所示:







































拦截器作用
应用拦截器拿到的是原始请求,可以添加一些自定义header、通用参数、参数加密、网关接入等等。
RetryAndFollowUpInterceptor处理错误重试和重定向
BridgeInterceptor应用层和网络层的桥接拦截器,主要工作是为请求添加cookie、添加固定的header,比如Host、Content-Length、Content-Type、User-Agent等等,然后保存响应结果的cookie,如果响应使用gzip压缩过,则还需要进行解压。
CacheInterceptor缓存拦截器,如果命中缓存则不会发起网络请求。
ConnectInterceptor连接拦截器,内部会维护一个连接池,负责连接复用、创建连接(三次握手等等)、释放连接以及创建连接上的socket流。
networkInterceptors(网络拦截器)用户自定义拦截器,通常用于监控网络层的数据传输。
CallServerInterceptor请求拦截器,在前置准备工作完成后,真正发起了网络请求。

我们的网络请求就是这样经过责任链一级一级的递推下去,最终会执行到CallServerInterceptorintercept方法,此方法会将网络响应的结果封装成一个Response对象并return。之后沿着责任链一级一级的回溯,最终就回到getResponseWithInterceptorChain方法的返回,如下图所示:


4. 应用拦截器和网络拦截器有什么区别?


从整个责任链路来看,应用拦截器是最先执行的拦截器,也就是用户自己设置request属性后的原始请求,而网络拦截器位于ConnectInterceptorCallServerInterceptor之间,此时网络链路已经准备好,只等待发送请求数据。它们主要有以下区别



  1. 首先,应用拦截器在RetryAndFollowUpInterceptorCacheInterceptor之前,所以一旦发生错误重试或者网络重定向,网络拦截器可能执行多次,因为相当于进行了二次请求,但是应用拦截器永远只会触发一次。另外如果在CacheInterceptor中命中了缓存就不需要走网络请求了,因此会存在短路网络拦截器的情况。

  2. 其次,除了CallServerInterceptor之外,每个拦截器都应该至少调用一次realChain.proceed方法。实际上在应用拦截器这层可以多次调用proceed方法(本地异常重试)或者不调用proceed方法(中断),但是网络拦截器这层连接已经准备好,可且仅可调用一次proceed方法。

  3. 最后,从使用场景看,应用拦截器因为只会调用一次,通常用于统计客户端的网络请求发起情况;而网络拦截器一次调用代表了一定会发起一次网络通信,因此通常可用于统计网络链路上传输的数据。


5. OKHttp如何复用TCP连接?


ConnectInterceptor的主要工作就是负责建立TCP连接,建立TCP连接需要经历三次握手四次挥手等操作,如果每个HTTP请求都要新建一个TCP消耗资源比较多

Http1.1已经支持keep-alive,即多个Http请求复用一个TCP连接,OKHttp也做了相应的优化,下面我们来看下OKHttp是怎么复用TCP连接的


ConnectInterceptor中查找连接的代码会最终会调用到ExchangeFinder.findConnection方法,具体如下:


# ExchangeFinder
//为承载新的数据流 寻找 连接。寻找顺序是 已分配的连接、连接池、新建连接
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
synchronized (connectionPool) {
// 1.尝试使用 已给数据流分配的连接.(例如重定向请求时,可以复用上次请求的连接)
releasedConnection = transmitter.connection;
result = transmitter.connection;

if (result == null) {
// 2. 没有已分配的可用连接,就尝试从连接池获取。(连接池稍后详细讲解)
if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, null, false)) {
result = transmitter.connection;
}
}
}

synchronized (connectionPool) {
if (newRouteSelection) {
//3. 现在有了IP地址,再次尝试从连接池获取。可能会因为连接合并而匹配。(这里传入了routes,上面的传的null)
routes = routeSelection.getAll();
if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, false)) {
foundPooledConnection = true;
result = transmitter.connection;
}
}

// 4.第二次没成功,就把新建的连接,进行TCP + TLS 握手,与服务端建立连接. 是阻塞操作
result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
connectionRetryEnabled, call, eventListener);

synchronized (connectionPool) {
// 5. 最后一次尝试从连接池获取,注意最后一个参数为true,即要求 多路复用(http2.0)
//意思是,如果本次是http2.0,那么为了保证 多路复用性,(因为上面的握手操作不是线程安全)会再次确认连接池中此时是否已有同样连接
if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, true)) {
// 如果获取到,就关闭我们创建里的连接,返回获取的连接
result = transmitter.connection;
} else {
//最后一次尝试也没有的话,就把刚刚新建的连接存入连接池
connectionPool.put(result);
}
}

return result;
}

上面精简了部分代码,可以看出,连接拦截器使用了5种方法查找连接



  1. 首先会尝试使用 已给请求分配的连接。(已分配连接的情况例如重定向时的再次请求,说明上次已经有了连接)

  2. 若没有 已分配的可用连接,就尝试从连接池中 匹配获取。因为此时没有路由信息,所以匹配条件:address一致——hostport、代理等一致,且匹配的连接可以接受新的请求。

  3. 若从连接池没有获取到,则传入routes再次尝试获取,这主要是针对Http2.0的一个操作,Http2.0可以复用square.comsquare.ca的连接

  4. 若第二次也没有获取到,就创建RealConnection实例,进行TCP + TLS握手,与服务端建立连接。

  5. 此时为了确保Http2.0连接的多路复用性,会第三次从连接池匹配。因为新建立的连接的握手过程是非线程安全的,所以此时可能连接池新存入了相同的连接。

  6. 第三次若匹配到,就使用已有连接,释放刚刚新建的连接;若未匹配到,则把新连接存入连接池并返回。


以上就是连接拦截器尝试复用连接的操作,流程图如下:


6. OKHttp空闲连接如何清除?


上面说到我们会建立一个TCP连接池,但如果没有任务了,空闲的连接也应该及时清除,OKHttp是如何做到的呢?


  # RealConnectionPool
private val cleanupQueue: TaskQueue = taskRunner.newQueue()
private val cleanupTask = object : Task("$okHttpName ConnectionPool") {
override fun runOnce(): Long = cleanup(System.nanoTime())
}

long cleanup(long now) {
int inUseConnectionCount = 0;//正在使用的连接数
int idleConnectionCount = 0;//空闲连接数
RealConnection longestIdleConnection = null;//空闲时间最长的连接
long longestIdleDurationNs = Long.MIN_VALUE;//最长的空闲时间

//遍历连接:找到待清理的连接, 找到下一次要清理的时间(还未到最大空闲时间)
synchronized (this) {
for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
RealConnection connection = i.next();

//若连接正在使用,continue,正在使用连接数+1
if (pruneAndGetAllocationCount(connection, now) > 0) {
inUseConnectionCount++;
continue;
}
//空闲连接数+1
idleConnectionCount++;

// 赋值最长的空闲时间和对应连接
long idleDurationNs = now - connection.idleAtNanos;
if (idleDurationNs > longestIdleDurationNs) {
longestIdleDurationNs = idleDurationNs;
longestIdleConnection = connection;
}
}
//若最长的空闲时间大于5分钟 或 空闲数 大于5,就移除并关闭这个连接
if (longestIdleDurationNs >= this.keepAliveDurationNs
|| idleConnectionCount > this.maxIdleConnections) {
connections.remove(longestIdleConnection);
} else if (idleConnectionCount > 0) {
// else,就返回 还剩多久到达5分钟,然后wait这个时间再来清理
return keepAliveDurationNs - longestIdleDurationNs;
} else if (inUseConnectionCount > 0) {
//连接没有空闲的,就5分钟后再尝试清理.
return keepAliveDurationNs;
} else {
// 没有连接,不清理
cleanupRunning = false;
return -1;
}
}
//关闭移除的连接
closeQuietly(longestIdleConnection.socket());

//关闭移除后 立刻 进行下一次的 尝试清理
return 0;
}

思路还是很清晰的:



  1. 在将连接加入连接池时就会启动定时任务

  2. 有空闲连接的话,如果最长的空闲时间大于5分钟 或 空闲数 大于5,就移除关闭这个最长空闲连接;如果 空闲数 不大于5 且 最长的空闲时间不大于5分钟,就返回到5分钟的剩余时间,然后等待这个时间再来清理。

  3. 没有空闲连接就等5分钟后再尝试清理。

  4. 没有连接不清理。


流程如下图所示:


7. OKHttp有哪些优点?



  1. 使用简单,在设计时使用了外观模式,将整个系统的复杂性给隐藏起来,将子系统接口通过一个客户端OkHttpClient统一暴露出来。

  2. 扩展性强,可以通过自定义应用拦截器与网络拦截器,完成用户各种自定义的需求

  3. 功能强大,支持SpdyHttp1.XHttp2、以及WebSocket等多种协议

  4. 通过连接池复用底层TCP(Socket),减少请求延时

  5. 无缝的支持GZIP减少数据流量

  6. 支持数据缓存,减少重复的网络请求

  7. 支持请求失败自动重试主机的其他ip,自动重定向


8. OKHttp框架中用到了哪些设计模式?



  1. 构建者模式:OkHttpClientRequest的构建都用到了构建者模式

  2. 外观模式: OkHttp使用了外观模式,将整个系统的复杂性给隐藏起来,将子系统接口通过一个客户端OkHttpClient统一暴露出来。

  3. 责任链模式: OKHttp的核心就是责任链模式,通过5个默认拦截器构成的责任链完成请求的配置

  4. 享元模式: 享元模式的核心即池中复用,OKHttp复用TCP连接时用到了连接池,同时在异步请求中也用到了线程池


总结


本文主要梳理了OKHttp原理相关知识点,并回答了以下问题:



  1. OKHttp请求的整体流程是怎样的?

  2. OKHttp分发器是怎样工作的?

  3. OKHttp拦截器是如何工作的?

  4. 应用拦截器和网络拦截器有什么区别?

  5. OKHttp如何复用TCP连接?

  6. OKHttp空闲连接如何清除?

  7. OKHttp有哪些优点?

  8. OKHttp框架中用到了哪些设计模式?

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

写给vue转react的同志们(4)

下一篇应各位老爷要求,这篇文章开始拥抱hooks,本文将从vue3与react 17.x(hooks)对比来感受两大框架的同工异曲之处。 今天的主题:vue3与react 定义与修改数据vue3与react 计算属性vue3与react 实现监听 vue3与r...
继续阅读 »

下一篇

应各位老爷要求,这篇文章开始拥抱hooks,本文将从vue3react 17.x(hooks)对比来感受两大框架的同工异曲之处。


今天的主题:

vue3与react 定义与修改数据

vue3与react 计算属性

vue3与react 实现监听


vue3与react hooks 定义与修改数据


实际上两者都是偏hooks的写法,这样的高灵活性的组合,相信大部分人还是觉得香的,无论是以前的vue options或是react class的写法都是比较臃肿且复用性较差的(相较于hooks)。下面举个例子对比一下。


vue3





react


import { useState } from 'react';
function App() {
const [todos, setTodos] = useState({
age: 25,
sex: 'man'
})
const setObj = () => {
setTodos({
...todos,
age: todos.age + 1
})
}
return (

{todos.age}


{todos.sex}



);
}



通过比较上述代码可以看到vue3react hooks基本写法是差不多的,只是vue提倡template写法,react提倡jsx写法,模板的写法并不影响你js逻辑的使用,所以不论框架再怎么变化,js也是我们前端的铁饭碗,请各位务必掌握好!


vue3与react 计算属性


计算属性这一块是为了不让我们在模板处写上太过复杂的运算,这是计算属性存在的意义。vue3中提供了computed方法,react hook提供了useMemo让我们实现计算属性(没有类写法中可以使用get来实现计算属性具体可看往期文章)


vue3






react


import { useMemo, useState } from 'react'
function App() {
const [obj, setObj] = useState({
age: 25,
sex: 'man'
})
const people = useMemo(() => {
return `this people age is ${obj.age} and sex is ${obj.sex}`
}, [obj])
return (

age: {obj.age}


sex: {obj.sex}


info: {people}



)
}


可以看到对比两大框架的计算属性,除了模板书写略有不同其他基本神似,都是hooks写法,通过框架内部暴露的某个方法去实现某个操作,这样一来追述和定位错误时也更加方便,hooks大概率就是现代框架的趋势,它不仅让开发者的代码可以更加灵活的组合复用,数据和方法来源也更加容易定位清晰。


vue3与react 实现监听


vue3watch被暴露成一个方法通过传入对应监听的参数以及回调函数实现,react中也有类似的功能useEffect,实际上他和componentDidMountcomponentDidUpdate 和 componentWillUnmount 具有相同的用途,只不过被合并成了一个 API。看例子:


vue3



import { ref, watch } from 'vue'
export default {
setup() {
const count = ref(0)
watch(count,
(val) => {
console.log(val)
},
{ immediate: true, deep: true }
)
function setCount() {
count.value ++
}
return {
count,
setCount
}
}

}

react


import { useState, useEffect } from 'react'
function App() {
const [count, setCount] = useState(0)
const setCount = () => {
setCount(count + 1)
}
useEffect(() => {
console.log(count)
})
return (

count: {count}



)
}

可以看到,vue3整体趋势是往hooks靠,不难看出来未来不论哪种框架大概率最终都会往hooks靠,react hooks无疑是给了我们巨大的启发,函数式编程会越来越普及,从远古时期的传统三大金刚html、css、script就能产出一个页面到现在的组件化,一个js即可是一个页面。


总结


函数式编程是趋势,但其实有很多老项目都是基于vue2.xoptions写法或者react class的写法还是居多,把这些项目迁移迭代到最新才是头疼的事情,当然选择适合现有项目的技术体系才是最重要的。


作者:饼干_
链接:https://juejin.cn/post/6991765115150270478

收起阅读 »

写给vue转react的同志们(3)

下一篇我们都知道vue上手比较容易是因为他的三标签写法以及对指令的封装,他更像一个做好的包子你直接吃。 相比react他的纯js写法,相对来说自由度更高,这也意味着很多东西你需要自己手动封装,所以对新手没那么友好,所以他更像面粉,但可以制作更多花样的食物。 今...
继续阅读 »

下一篇

我们都知道vue上手比较容易是因为他的三标签写法以及对指令的封装,他更像一个做好的包子你直接吃。


相比react他的纯js写法,相对来说自由度更高,这也意味着很多东西你需要自己手动封装,所以对新手没那么友好,所以他更像面粉,但可以制作更多花样的食物。


今天的主题

react 计算属性

react ref


react 计算属性


我们知道vue中有提供computed让我们来实现计算属性,只要依赖改变就会发生变化,那么react中是没有提供的,这里我们需要自己手动实现计算属性。简单举例一下:


vue 计算属性






react 计算属性(类写法)


class App extends React.Component {
constructor(props) {
super(props)
this.state = {
msg: 'hello react'
}
}
get react_computed() {
return this.state.msg
}
componentDidMount() {
setTimeout(() => {
this.setState({
msg: 'hello react change'
})
}, 2000)
}
render() {
return (

{ this.react_computed }

)
}

}


可以看到react中我们手动定义了get来让他获取msg的值,从而实现了计算属性,实际上vue中的computed也是基于get和set实现的,get中收集依赖,在set中派发更新。


react ref


vue中的ref使用起来也是非常简单在对应组件上标记即可获取组件的引用,那么react中呢?
react中当然也可以像vue一样使用,但官方并不推荐字符串的形式来使用ref,并且在react16.x后的版本移除了。


看一段大佬描述:



  • 它要求 React 跟踪当前呈现的组件(因为它无法猜测this)。这让 React 变慢了一点。

  • 它不像大多数人所期望的那样使用“渲染回调”模式(例如),因为 ref 会因为DataGrid上述原因而被放置。

  • 它不是可组合的,即如果一个库在传递的子组件上放置了一个引用,用户不能在它上面放置另一个引用。回调引用是完全可组合的。


举例:


vue ref






react ref


class App extends React.Component {
myRef = React.createRef()
constructor(props) {
super(props)
}
render() {
return (

// 正常使用

// 回调使用(可组合)
this['' + index]} />
// 调用api(react16.x)


)
}

}

vue中的ref我们不必多言,看看react的,官方更推荐第三种用法(react16.x),第二种用法在更新过程中会被执行两次,通过在外部定义箭头函数使用即可,但是大多情况都是无关紧要。第一种用法在react16.x后的版本被废弃了。


总结


都到这篇了,相信你转型react上手业务基本没问题了,后续将慢慢深入两大框架的对比,重点叙述react,vue辅之。


我是饼干,让我们一起成长。最后别忘记点赞关注收藏三连击🌟


作者:饼干_
链接:https://juejin.cn/post/6979061382415122462

收起阅读 »

写给vue转react的同志们(2)

下一篇react中想实现类似vue中的插槽 首先,我个人感觉jsx的写法比模板写法要灵活些,虽然没有像vue那样有指令,这就是为啥vue会上手简单点,因为他就像教科书一样教你怎么使用,而react纯靠你手写表达式来实现。 如果你想实现类似插槽的功能,其实大部分...
继续阅读 »

下一篇

react中想实现类似vue中的插槽


首先,我个人感觉jsx的写法比模板写法要灵活些,虽然没有像vue那样有指令,这就是为啥vue会上手简单点,因为他就像教科书一样教你怎么使用,而react纯靠你手写表达式来实现。


如果你想实现类似插槽的功能,其实大部分UI框架也可以是你自己定义的组件,例如ant desgin的组件,他的某些属性是可以传jsx来实现类似插槽的功能的,比如:


import React from 'react'
import { Popover } from 'antd'

class App extends React.Component {
constructor(props) {
super(props)
this.state = {
content: (

你好,这里是react插槽



)
}
}
render() {
const { content } = this.state
return (


悬浮


)
}
}

上面这样就可以实现类似插槽的功能,这点上确实是比vue灵活些,不需要在结构里在加入特定的插槽占位。
如果是vue的话就可能是这样:




大家可以自己写写demo去体会一下。


单向数据流与双向绑定


我们知道vue中通过发布订阅模式实现了响应式,把inputchange封装成v-model实现双向绑定,react则没有,需要你自己通过this.setState去实现,这点上我还是比较喜欢v-model能省不少事。


虽说单向数据流更清晰,但实际大部分人在开发中出现bug依旧要逐个去寻找某个属性值在哪些地方使用过,尤其是当表单项很多且校验多的时候,代码会比vue多不少,所以大家自行衡量,挑取合适自己或团队的技术栈才是最关键的,不要盲目追求所谓的新技术。


举个例子(简写):


react


import React from 'react'
import { Form, Input, Button } from 'antd'

const FormItem = Form.Item

class App extends React.Component {
constructor(props) {
super(props)
}

onChange(key, e) {
this.setState({
[key] : e
})
}

onClick = () => {
console.log('拿到了:',this.state.username)
}

render() {
return (





vue(简写)





其实乍一看也差不了多少,vue这种options的写法其实会比较清晰一点,react则需要你自己去划分功能区域。


css污染


vue中可以使用scoped来防止样式污染,react没有,需要用到.module.css,原理都是一样的,通过给类名添加一个唯一hash值来标识。


举个例子:


react(简写):


xxx.module.css

.xxx {
background-color: red;
}

xxx.css

.xxx {
background-color: blue;
}

xxx.jsx
import React from 'react'
import style form './xxx.module.css'
import './xxx.css'
class App extends React.Component {
render(){
return (


blue


red


)
}
}


vue


xxx.css
.xxx {
background-color: red;
}

xxx.vue

export default {
methods: {
click() {
this.$router.push('yyy')
}
}
}



yyy.vue

export default {
methods: {
click() {
this.$router.push('xxx')
}
}
}




上面只是简单举个例子,页面之间的样式污染主要是因为css默认是全局生效的,所以无论scoped也好或是module.css也好,都会解析成ast语法树,通过添加hash值生成唯一样式。


总结


无论vue也好,react也好不变的都是js,把js基础打牢才是硬道理。


作者:饼干_
链接:https://juejin.cn/post/6972099403213438984

收起阅读 »

写给vue转react的同志们(1)

学习一个框架最好的办法就是从业务做起。首先我们要弄清做业务需要什么知识点去支持 今天的主题:react 是怎么样传输数据的react 怎么封装组件react 的生命周期 实际上vue熟练的同学们,我觉得转react还是比较好上手的,就是要适应他的纯js的写法以...
继续阅读 »

学习一个框架最好的办法就是从业务做起。首先我们要弄清做业务需要什么知识点去支持


今天的主题:

react 是怎么样传输数据的

react 怎么封装组件

react 的生命周期


实际上vue熟练的同学们,我觉得转react还是比较好上手的,就是要适应他的纯js的写法以及jsx等,个人认为还是比较好接受的,其实基本上都一样,只要弄清楚数据怎么传输怎么处理,那剩下的jsx大家都会写了吧。


react 组件通讯


这里我们来跟vue对比一下。比如
在vue中父子组件传值(简写):


// 父组件
data: {
testText:'这是父值'
}
methods:{
receive(val) {
console.log('这是子值:', val)
}
}
<child :testText="testText" @childCallBack="receive"/>
// 子组件
props: {
testText: {
type: String,
default: ''
}
}
methods:{
handleOn(){
this.$emit('childCallBack', '我是子组件')
}
}
<template>
<div @click="handleOn">{{testText}}</div>
</template>

在react中父子组件传值:


// 父组件
export default class Father extends React.Component {
constructor(props) {
super(props)
this.state = {
testText: '这是父值'
}
receive = (val) => {
console.log('这是子值:', val)
}
render(){
return(
<div>
<Son childCallBack={this.receive} testText={testText}/>
</div>
)
}
}
}
// 子组件
export default class Son extends React.Component {
constructor(props) {
super(props)
}
render() {
const { testText } = this.props
return (
<div>
父组件传过来的testText:{testText}
<div onClick={this.receiveFather}>
点我从子传父
</div>
</div>
)
}
receiveFather = () => {
this.props.childCallBack('我是子组件')
}
}

可以看到react 和 vue 其实相差不大,都是通过props去进行父传子的通讯,然后通过一个事件把子组件的数据传给父组件。聪明的同学肯定注意到react里我用了箭头函数赋给了一个变量了。如果不这样写,this的指向是不确定的,也可以在标签上这样写this.receiveFather.bind(this),不过这样写的坏处就是对性能有影响,可以在constructor中一次绑定即可。但还是推荐箭头函数的写法。(封装组件其实跟这个八九不离十了,就不再叙述)


react 单向数据流


我们都知道vue里直接v-model 然后通过this.属性名就可以访问和修改属性了,这是vue劫持了get和set做了依赖收集和派发更新,但是react里没有这种东西,你不能直接通过this.state.属性名去修改值,需要通过this.setState({"属性名":"属性值"}, callback(回调函数)),你在同一地方修改属性是没办法立刻拿到修改后的属性值,需要通过setState的回调拿到。我还是比较喜欢vue的双向绑定(手动狗头)。


react 的生命周期


我们都知道vue的生命周期(create、mounted、update、destory),其实react也差不多,他们都是要把某个html的div替换并挂载渲染的。
列举比较常用的:


constructor()
constructor()中完成了React数据的初始化,它接受两个参数:props和context,当想在函数内部使用这两个参数时,需使用super()传入这两个参数。这个就当于定义初始数据,熟悉vue的同学你可以把他当成诸如data、methods等。
注意:只要使用了constructor()就必须写super(),否则会导致this指向错误。


componentWillMount()
componentWillMount()一般用的比较少,它更多的是在服务端渲染时使用。它代表的过程是组件已经经历了constructor()初始化数据后,但是还未渲染DOM时。这个相当于vue的created啦,vue中可以通过在这个阶段用$nextTick去操作dom(不建议),不知道react有没有类似的api呢?


componentDidMount()
组件第一次渲染完成,此时dom节点已经生成,可以在这里调用ajax请求,返回数据setState后组件会重新渲染,这个就相当于vue的mounted阶段啦。


componentWillUnmount ()
在此处完成组件的卸载和数据的销毁。这个相当于vue中的beforeDestory啦,clear你在组件中所有的setTimeout,setInterval,移除所有组建中的监听 removeEventListener。


componentWillUpdate (nextProps,nextState)
组件进入重新渲染的流程,这里可以拿到改变后的数据值(相当于vue中updated)。


componentDidUpdate(prevProps,prevState)
组件更新完毕后,react只会在第一次初始化成功会进入componentDidmount,之后每次重新渲染后都会进入这个生命周期,这里可以拿到prevProps和prevState,即更新前的props和state,(相当于vue中的beforeupdated)。


render()
render函数会插入jsx生成的dom结构,react会生成一份虚拟dom树,在每一次组件更新时,在此react会通过其diff算法比较更新前后的新旧DOM树,比较以后,找到最小的有差异的DOM节点,并重新渲染。这里就是你写页面的地方。


总结


小细节

react 中使用组件第一个字母需大写

react 万物皆可 props

mobx 很香🐶

react中没有指令(如v-if、v-for等)需自己写三目运算符或so on~



总结一下,从vue转react还是比较好上手的(react中还有函数式写法我没有说,感兴趣可以看看),个人认为弄清楚数据通讯以及生命周期对应的钩子使用场景等,其实基本就差不多啦。但是还有很多细节需要同学们在实际业务中去发现和解决。react只是框架,大家js基础还是要打好的。祝各位工作顺利,准时发薪。🐶

收起阅读 »