注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

Android判断Activity是否在AndroidManifest.xml里面注册(源码分析)

Android判断Activity是否在AndroidManifest.xml里面注册(源码分析) 这个问题相信大家在实际的开发中,都遇到过这个问题,答案就不用说了,在AndroidManifest.xml中添加Activity的注册,毕竟Activity...
继续阅读 »


Android判断Activity是否在AndroidManifest.xml里面注册(源码分析)


在这里插入图片描述
这个问题相信大家在实际的开发中,都遇到过这个问题,答案就不用说了,在AndroidManifest.xml中添加Activity的注册,毕竟Activity属于四大组件之一,使用的时候,需要要在清单文件中注册。


<activity android:name=".TargetActivity"></activity>

但是这个出现这个问题的根源在哪里?下面我们就进入源码仔细看看。


这里就不一步一步进入源码,直接分析关键代码:


public ActivityResult execStartActivity( Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode, Bundle options) { ...
try {
intent.migrateExtraStreamToClipData();
intent.prepareToLeaveProcess(who);
? //1.通过IActivityManager调用我们执行AMS的startActivity方法,并返回执行 结果
int result = ActivityManager.getService() .startActivity(whoThread, who.getBasePackageName(), intent, intent.resolveTypeIfNeeded(who.getContentResolver()), token, target != null ? target.mEmbeddedID : null, requestCode, 0, null, options);
//2. 检查结果
checkStartActivityResult(result, intent);
} catch (RemoteException e) {
throw new RuntimeException("Failure from system", e);
}
return null;
}

通过源码execStartActivity这个方法可以看到主要是在这个检查结果这里面去分析的checkStartActivityResult(result, intent);


public static void checkStartActivityResult(int res, Object intent) {
if (!ActivityManager.isStartResultFatalError(res)) {
return;
}
switch (res) {
case ActivityManager.START_INTENT_NOT_RESOLVED:
case ActivityManager.START_CLASS_NOT_FOUND:
//3. 这里我们找到了报错的地方,原来是res结果为 START_INTENT_NOT_RESOLVED,
// START_CLASS_NOT_FOUND就会报这个错误
if (intent instanceof Intent && ((Intent) intent).getComponent() != null)
throw new ActivityNotFoundException("Unable to find explicit activity class " + ((Intent) intent).getComponent().toShortString() + "; have you declared this activity in your AndroidManifest.xml?");
throw new ActivityNotFoundException("No Activity found to handle " + intent);
case ActivityManager.START_PERMISSION_DENIED:
throw new SecurityException("Not allowed to start activity " + intent);
case ActivityManager.START_FORWARD_AND_REQUEST_CONFLICT:
throw new AndroidRuntimeException("FORWARD_RESULT_FLAG used while also requesting a result");
case ActivityManager.START_NOT_ACTIVITY:
throw new IllegalArgumentException("PendingIntent is not an activity");
case ActivityManager.START_NOT_VOICE_COMPATIBLE:
throw new SecurityException("Starting under voice control not allowed for: " + intent);
case ActivityManager.START_VOICE_NOT_ACTIVE_SESSION:
throw new IllegalStateException("Session calling startVoiceActivity does not match active session");
case ActivityManager.START_VOICE_HIDDEN_SESSION:
throw new IllegalStateException("Cannot start voice activity on a hidden session");
case ActivityManager.START_ASSISTANT_NOT_ACTIVE_SESSION:
throw new IllegalStateException("Session calling startAssistantActivity does not match active session");
case ActivityManager.START_ASSISTANT_HIDDEN_SESSION:
throw new IllegalStateException("Cannot start assistant activity on a hidden session");
case ActivityManager.START_CANCELED:
throw new AndroidRuntimeException("Activity could not be started for " + intent);
default:
throw new AndroidRuntimeException("Unknown error code " + res + " when starting " + intent);
}
}

可以看到当结果为


case ActivityManager.START_INTENT_NOT_RESOLVED:


case ActivityManager.START_CLASS_NOT_FOUND:


?就会报
throw new ActivityNotFoundException("Unable to find explicit activity class " + ((Intent) intent).getComponent().toShortString() + “; have you declared this activity in your AndroidManifest.xml?”);


这下我们就知道了如果没在清单文件中添加这个注册,报错的位置。


AMS是如何判断activity没有注册的,首先我们得明白startActivity执行的主流程


这个篇幅太多了,可以自己去源码跟一下,这里不作介绍,


我们这里分析主要流程代码


找到在ASR.startActivity (ActivityStarter)中返回了


START_INTENT_NOT_RESOLVED,START_CLASS_NOT_FOUND


private int startActivity(IApplicationThread caller, Intent intent, Intent ephemeralIntent, String resolvedType, ActivityInfo aInfo, ResolveInfo rInfo, IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor, IBinder resultTo, String resultWho, int requestCode, int callingPid, int callingUid, String callingPackage, int realCallingPid, int realCallingUid, int startFlags, SafeActivityOptions options, boolean ignoreTargetSecurity, boolean componentSpecified, ActivityRecord[] outActivity, TaskRecord inTask, boolean allowPendingRemoteAnimationRegistryLookup) {
int err = ActivityManager.START_SUCCESS;
...
//接下来开始做一些校验判断
if (err == ActivityManager.START_SUCCESS && intent.getComponent() == null) {
// We couldn't find a class that can handle the given Intent.
// That's the end of that! err = ActivityManager.START_INTENT_NOT_RESOLVED;
// 从Intent中无法找 到相应的Component
}
if (err == ActivityManager.START_SUCCESS && aInfo == null) {
// We couldn't find the specific class specified in the Intent.
// Also the end of the line.
err = ActivityManager.START_CLASS_NOT_FOUND;
// 从Intent中无法找到相 应的ActivityInfo
}
...
if (err != START_SUCCESS) {
//不能成功启动了,返回err
if (resultRecord != null) {
resultStack.sendActivityResultLocked(-1, resultRecord, resultWho, requestCode, RESULT_CANCELED, null);
}
SafeActivityOptions.abort(options);
return err;
}
//创建出我们的目标ActivityRecord对象,存到传入数组0索引上
ActivityRecord r = new ActivityRecord(mService, callerApp, callingPid, callingUid, callingPackage, intent, resolvedType, aInfo, mService.getGlobalConfiguration(), resultRecord, resultWho, requestCode, componentSpecified, voiceSession != null, mSupervisor, checkedOptions, sourceRecord);
...
return startActivity(r, sourceRecord, voiceSession, voiceInteractor, startFlags, true /* doResume */, checkedOptions, inTask, outActivity);
}

但是 intent.getComponent(),aInfo又是从哪儿获取的呢,我们回溯到


startActivityMayWait.


看下上面的aInfo哪来的.


ActivityInfo resolveActivity(Intent intent, ResolveInfo rInfo, int startFlags, ProfilerInfo profilerInfo) {
? final ActivityInfo aInfo = rInfo != null ? rInfo.activityInfo : null;
if (aInfo != null) {
// Store the found target back into the intent, because now that
// we have it we never want to do this again. For example, if the
// user navigates back to this point in the history, we should
// always restart the exact same activity.
intent.setComponent(new ComponentName(aInfo.applicationInfo.packageName, aInfo.name));
// Don't debug things in the system process ...
}
return aInfo;
}

发现是从rInfo来的


ResolveInfo resolveIntent(Intent intent, String resolvedType, int userId, int flags, int filterCallingUid) {
synchronized (mService) {
? try {...final long token = Binder.clearCallingIdentity();
try {
return mService.getPackageManagerInternalLocked().resolveIntent(intent, resolvedType, modifiedFlags, userId, true, filterCallingUid);
} finally {
Binder.restoreCallingIdentity(token);
} ...
}
}
}

rInfo的获取


PackageManagerInternal getPackageManagerInternalLocked() {
if (mPackageManagerInt == null) {
? mPackageManagerInt = LocalServices.getService(PackageManagerInternal.class);
}
return mPackageManagerInt;
}

具体实现类是PackageManagerService


?@Override
public ResolveInfo resolveIntent(Intent intent, String resolvedType, int flags, int userId) {
return resolveIntentInternal(intent, resolvedType, flags, userId, false, Binder.getCallingUid());
}

看resolveIntentInternal


private ResolveInfo resolveIntentInternal(Intent intent, String resolvedType,int flags, int userId, boolean resolveForStart, int filterCallingUid) {
try {...
//获取ResolveInfo列表
final List<ResolveInfo> query = queryIntentActivitiesInternal(intent, resolvedType, flags, filterCallingUid, userId, resolveForStart, true /*allowDynamicSplits*/);
? Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
//找出最好的返回
final ResolveInfo bestChoice = chooseBestActivity(intent, resolvedType, flags, query, userId);
return bestChoice;
} finally {
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
}
}

看 queryIntentActivitiesInternal


private @NonNull List<ResolveInfo> queryIntentActivitiesInternal(Intent intent, String resolvedType, int flags, int filterCallingUid, int userId, boolean resolveForStart, boolean allowDynamicSplits) {
? ...
if (comp != null) {
final List<ResolveInfo> list = new ArrayList<ResolveInfo>(1);
final ActivityInfo ai = getActivityInfo(comp, flags, userId);
if (ai != null) {
...
}
}

原来是从getActivityInfo获取的


	@Override
public ActivityInfo getActivityInfo(ComponentName component, int flags, int userId) {
return getActivityInfoInternal(component, flags, Binder.getCallingUid(), userId);
? }

getActivityInfoInternal方法


private ActivityInfo getActivityInfoInternal(ComponentName component, int flags, int filterCallingUid, int userId) {
if (!sUserManager.exists(userId)) return null;
flags = updateFlagsForComponent(flags, userId, component);
if (!isRecentsAccessingChildProfiles(Binder.getCallingUid(), userId)) {
mPermissionManager.enforceCrossUserPermission(Binder.getCallingUid(), userId, false /* requireFullPermission */, false /* checkShell */, "get activity info");
}
synchronized (mPackages) {
//关键点
PackageParser.Activity a = mActivities.mActivities.get(component);
if (DEBUG_PACKAGE_INFO) Log.v(TAG, "getActivityInfo " + component + ": " + a);
? if (a != null && mSettings.isEnabledAndMatchLPr(a.info, flags, userId)) {
PackageSetting ps = mSettings.mPackages.get(component.getPackageName());
if (ps == null) return null;
if (filterAppAccessLPr(ps, filterCallingUid, component, TYPE_ACTIVITY, userId)) {
return null;
}
//关键点
return PackageParser.generateActivityInfo(a, flags, ps.readUserState(userId), userId);
}
if (mResolveComponentName.equals(component)) {
return PackageParser.generateActivityInfo(mResolveActivity, flags, new PackageUserState(), userId);
}
}
return null;
}

分析到这里,大家应该知道怎么回事了吧,其实就是解析了AndroidManifest.xml里面的信息,具体怎么解析,等有空了分析。


————————————————
版权声明:本文为CSDN博主「拖鞋王子猪」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/sinat_26397681/article/details/117904722



收起阅读 »

美团面试题:JVM的年轻代是怎么设计的?

1、JVM中的堆,一般分为三个部分,新生代、老年代和永久代。这个是你第一天学JVM就知道的。但你可以先想想,为什么需要把堆分代?不分代不能完成他所做的事情么? 2、是这样,如果没有分代,那我们所有的对象都在一块,GC 的时候就要先找到哪些对象没用,怎么找呢...
继续阅读 »

1、JVM中的堆,一般分为三个部分,新生代、老年代和永久代。这个是你第一天学JVM就知道的。但你可以先想想,为什么需要把堆分代?不分代不能完成他所做的事情么?


2、是这样,如果没有分代,那我们所有的对象都在一块,GC 的时候就要先找到哪些对象没用,怎么找呢?没分代就得对堆的所有区域进行扫描。但你知道,很多Java对象都是朝生夕死的,如果分代的话,我们可以把新创建的对象放到某一地方,GC的时候就可以迅速回收这块存“朝生夕死”对象的区域。


3、所以,一句话总结,分代的唯一理由就是优化 GC 性能。你这么记,就容易把知识串起来了。


4、HotSpot JVM 把年轻代分为了三部分:1 个 Eden 区和 2 个 Survivor 区(分别叫from 和 to),他们的默认比例为 8:1。一般情况下,新创建的对象都会被分配到 Eden区,这些对象经过第一次 Minor GC 后,如果仍然存活,将会被移到 Survivor 区。对象在 Survivor 区中每熬过一次 Minor GC,年龄就会增加 1 岁,当它的年龄增加到一定程度时,就会被移动到年老代中。这是一个对象的生存路径。


5、因为年轻代中的对象基本都是朝生夕死的( 80% 以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。


6、在 GC 开始的时候,对象只会存在于 Eden 区和名为“ From ”的 Survivor 区,Survivor 区“To”是空的。紧接着进行 GC,Eden 区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。


7、年龄达到一定值(可以通过-XX: MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次 GC 后,Eden 区和From 区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次 GC 前的“From”,新的“From”就是上次 GC 前的“To”。不管怎样,都会保证名为 To 的 Survivor 区域是空的。



8、Minor GC 会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。注意,我说的是 Minor GC,不是 Full GC,这俩的关系你要缕清楚。


9、好记吗?不好记,再给你做个类比。我叫小强,是一个普通的 Java 对象,我出生在Eden 区,在 Eden 区我还看到和我长的很像的小兄弟,我们在 Eden 区中玩了挺长时间。有一天 Eden 区中的人实在是太多了,我就被迫去了 Survivor 区的“From”区,自从去了 Survivor 区,我就开始漂了,有时候在 Survivor 的“From”区,有时候在 Survivor 的“To”区,居无定所。直到我 18 岁的时候,爸爸说我成人了,该去社会上闯闯了。于是我就去了年老代那边,年老代里,人很多,并且年龄都挺大的,我在这里也认识了很多人。在年老代里,我生活了20年(每次GC加一岁),然后去世了。


10、年轻代的JVM参数也不多,给你列出来了,你也不用记,用着多了自然就熟悉了。



总而言之,JVM 内存问题排查需要掌握一定的技巧,而这些技巧并不是告诉你,你就会用的,更重要的还是需要在实战中去应用



收起阅读 »

Android:OkHttp的理解和使用

OkHttp的理解和使用 1、什么是OkHttp 1、网络请求发展 历史上Http请求库优缺点 HttpURLConnection—>Apache HTTP Client—>Volley—->okHttp 2、项目开源...
继续阅读 »




OkHttp的理解和使用


在这里插入图片描述


1、什么是OkHttp


1、网络请求发展


历史上Http请求库优缺点



HttpURLConnection—>Apache HTTP Client—>Volley—->okHttp



2、项目开源地址



https://github.com/square/okhttp



3、OkHttp是什么



  • OKhttp是一个网络请求开源项目,Android网络请求轻量级框架,支持文件上传与下载,支持https。


2、OkHttp的作用


OkHttp是一个高效的HTTP库:



  • 支持HTTP/2, HTTP/2通过使用多路复用技术在一个单独的TCP连接上支持并发, 通过在一个连接上一次性发送多个请求来发送或接收数据

  • 如果HTTP/2不可用, 连接池复用技术也可以极大减少延时

  • 支持GZIP, 可以压缩下载体积

  • 响应缓存可以直接避免重复请求

  • 会从很多常用的连接问题中自动恢复

  • 如果您的服务器配置了多个IP地址, 当第一个IP连接失败的时候, OkHttp会自动尝试下一个IP OkHttp还处理了代理服务器问题和SSL握手失败问题


优势



  • 使用 OkHttp无需重写您程序中的网络代码。OkHttp实现了几乎和java.net.HttpURLConnection一样的API。如果您用了 Apache HttpClient,则OkHttp也提供了一个对应的okhttp-apache 模块


3、Okhttp的基本使用


Okhttp的基本使用,从以下五方面讲解:



  • 1.Get请求(同步和异步)

  • 2.POST请求表单(key-value)

  • 3.POST请求提交(JSON/String/文件等)

  • 4.文件下载

  • 5.请求超时设置


加入build.gradle


compile 'com.squareup.okhttp3:okhttp:3.6.0'

3.1、Http请求和响应的组成


http请求
在这里插入图片描述
所以一个类库要完成一个http请求, 需要包含 请求方法, 请求地址, 请求协议, 请求头, 请求体这五部分. 这些都在okhttp3.Request的类中有体现, 这个类正是代表http请求的类. 看下图:


在这里插入图片描述
其中HttpUrl类代表请求地址, String method代表请求方法, Headers代表请求头, RequestBody代表请求体. Object tag这个是用来取消http请求的标志, 这个我们先不管.


http响应


响应组成图:
在这里插入图片描述
可以看到大体由应答首行, 应答头, 应答体构成. 但是应答首行表达的信息过多, HTTP/1.1表示访问协议, 200是响应码, OK是描述状态的消息.


根据单一职责, 我们不应该把这么多内容用一个应答首行来表示. 这样的话, 我们的响应就应该由访问协议, 响应码, 描述信息, 响应头, 响应体来组成.


3.2、OkHttp请求和响应的组成


OkHttp请求


构造一个http请求, 并查看请求具体内容:


final Request request = new Request.Builder().url("https://github.com/").build();

我们看下在内存中, 这个请求是什么样子的, 是否如我们上文所说和请求方法, 请求地址, 请求头, 请求体一一对应.
在这里插入图片描述
OkHttp响应


OkHttp库怎么表示一个响应:
在这里插入图片描述
可以看到Response类里面有Protocol代表请求协议, int code代表响应码, String message代表描述信息, Headers代表响应头, ResponseBody代表响应体. 当然除此之外, 还有Request代表持有的请求, Handshake代表SSL/TLS握手协议验证时的信息, 这些额外信息我们暂时不问.


有了刚才说的OkHttp响应的类组成, 我们看下OkHttp请求后响应在内存中的内容:


final Request request = new Request.Builder().url("https://github.com/").build();
Response response = client.newCall(request).execute();

在这里插入图片描述


3.3、GET请求同步方法


同步GET的意思是一直等待http请求, 直到返回了响应. 在这之间会阻塞进程, 所以通过get不能在Android的主线程中执行, 否则会报错.


对于同步请求在请求时需要开启子线程,请求成功后需要跳转到UI线程修改UI。


public void getDatasync(){
new Thread(new Runnable() {
@Override
public void run() {
try {
OkHttpClient client = new OkHttpClient();//创建OkHttpClient对象
Request request = new Request.Builder()
.url("http://www.baidu.com")//请求接口。如果需要传参拼接到接口后面。
.build();//创建Request 对象
Response response = null;
response = client.newCall(request).execute();//得到Response 对象
if (response.isSuccessful()) {
Log.d("kwwl","response.code()=="+response.code());
Log.d("kwwl","response.message()=="+response.message());
Log.d("kwwl","res=="+response.body().string());
//此时的代码执行在子线程,修改UI的操作请使用handler跳转到UI线程。
}
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}

此时打印结果如下:


response.code()==200
response.message()OK;
res{“code”:200,“message”:success};

OkHttpClient实现了Call.Factory接口, 是Call的工厂类, Call负责发送执行请求和读取响应.
Request代表Http请求, 通过Request.Builder辅助类来构建.


client.newCall(request)通过传入一个http request, 返回一个Call调用. 然后执行execute()方法, 同步获得Response代表Http请求的响应. response.body()是ResponseBody类, 代表响应体


注意事项:


1,Response.code是http响应行中的code,如果访问成功则返回200.这个不是服务器设置的,而是http协议中自带的。res中的code才是服务器设置的。注意二者的区别。


2,response.body().string()本质是输入流的读操作,所以它还是网络请求的一部分,所以这行代码必须放在子线程。


3,response.body().string()只能调用一次,在第一次时有返回值,第二次再调用时将会返回null。原因是:response.body().string()的本质是输入流的读操作,必须有服务器的输出流的写操作时客户端的读操作才能得到数据。而服务器的写操作只执行一次,所以客户端的读操作也只能执行一次,第二次将返回null。


4、响应体的string()方法对于小文档来说十分方便高效. 但是如果响应体太大(超过1MB), 应避免使用 string()方法, 因为它会将把整个文档加载到内存中.


5、对于超过1MB的响应body, 应使用流的方式来处理响应body. 这和我们处理xml文档的逻辑是一致的, 小文件可以载入内存树状解析, 大文件就必须流式解析.


注解:


responseBody.string()获得字符串的表达形式, 或responseBody.bytes()获得字节数组的表达形式, 这两种形式都会把文档加入到内存. 也可以通过responseBody.charStream()和responseBody.byteStream()返回流来处理.


3.4、GET请求异步方法


异步GET是指在另外的工作线程中执行http请求, 请求时不会阻塞当前的线程, 所以可以在Android主线程中使用.


这种方式不用再次开启子线程,但回调方法是执行在子线程中,所以在更新UI时还要跳转到UI线程中。


下面是在一个工作线程中下载文件, 当响应可读时回调Callback接口. 当响应头准备好后, 就会调用Callback接口, 所以读取响应体时可能会阻塞. OkHttp现阶段不提供异步api来接收响应体。


private final OkHttpClient client = new OkHttpClient();

public void run() throws Exception {
Request request = new Request.Builder()
.url("http://publicobject.com/helloworld.txt")
.build();

client.newCall(request).enqueue(new Callback() {
@Override public void onFailure(Request request, Throwable throwable) {
throwable.printStackTrace();
}

@Override public void onResponse(Response response) throws IOException {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

Headers responseHeaders = response.headers();
for (int i = 0; i < responseHeaders.size(); i++) {
System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i));
}

System.out.println(response.body().string());
}
});
}

异步请求的打印结果与注意事项与同步请求时相同。最大的不同点就是异步请求不需要开启子线程,enqueue方法会自动将网络请求部分放入子线程中执行。


注意事项:



  • 1,回调接口的onFailure方法onResponse执行在子线程。

  • 2,response.body().string()方法也必须放在子线程中。当执行这行代码得到结果后,再跳转到UI线程修改UI。


3.5、post请求方法


Post请求也分同步和异步两种方式,同步与异步的区别和get方法类似,所以此时只讲解post异步请求的使用方法。


private void postDataWithParame() {
OkHttpClient client = new OkHttpClient();//创建OkHttpClient对象。
FormBody.Builder formBody = new FormBody.Builder();//创建表单请求体
formBody.add("username","zhangsan");//传递键值对参数
Request request = new Request.Builder()//创建Request 对象。
.url("http://www.baidu.com")
.post(formBody.build())//传递请求体
.build();
client.newCall(request).enqueue(new Callback() {。。。});//回调方法的使用与get异步请求相同,此时略。
}



看完代码我们会发现:post请求中并没有设置请求方式为POST,回忆在get请求中也没有设置请求方式为GET,那么是怎么区分请求方式的呢?重点是Request.Builder类的post方法,在Request.Builder对象创建最初默认是get请求,所以在get请求中不需要设置请求方式,当调用post方法时把请求方式修改为POST。所以此时为POST请求。


3.6、POST请求传递参数的方法总结


3.6.1、Post方式提交String


下面是使用HTTP POST提交请求到服务. 这个例子提交了一个markdown文档到web服务, 以HTML方式渲染markdown. 因为整个请求体都在内存中, 因此避免使用此api提交大文档(大于1MB).


public static final MediaType MEDIA_TYPE_MARKDOWN
= MediaType.parse("text/x-markdown; charset=utf-8");

private final OkHttpClient client = new OkHttpClient();

public void run() throws Exception {
String postBody = ""
+ "Releases\n"
+ "--------\n"
+ "\n"
+ " * _1.0_ May 6, 2013\n"
+ " * _1.1_ June 15, 2013\n"
+ " * _1.2_ August 11, 2013\n";

Request request = new Request.Builder()
.url("https://api.github.com/markdown/raw")
.post(RequestBody.create(MEDIA_TYPE_MARKDOWN, postBody))
.build();

Response response = client.newCall(request).execute();
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

System.out.println(response.body().string());
}

3.6.2、Post方式提交


以流的方式POST提交请求体. 请求体的内容由流写入产生. 这个例子是流直接写入Okio的BufferedSink. 你的程序可能会使用OutputStream, 你可以使用BufferedSink.outputStream()来获取. OkHttp的底层对流和字节的操作都是基于Okio库, Okio库也是Square开发的另一个IO库, 填补I/O和NIO的空缺, 目的是提供简单便于使用的接口来操作IO.


public static final MediaType MEDIA_TYPE_MARKDOWN
= MediaType.parse("text/x-markdown; charset=utf-8");

private final OkHttpClient client = new OkHttpClient();

public void run() throws Exception {
RequestBody requestBody = new RequestBody() {
@Override public MediaType contentType() {
return MEDIA_TYPE_MARKDOWN;
}

@Override public void writeTo(BufferedSink sink) throws IOException {
sink.writeUtf8("Numbers\n");
sink.writeUtf8("-------\n");
for (int i = 2; i <= 997; i++) {
sink.writeUtf8(String.format(" * %s = %s\n", i, factor(i)));
}
}

private String factor(int n) {
for (int i = 2; i < n; i++) {
int x = n / i;
if (x * i == n) return factor(x) + " × " + i;
}
return Integer.toString(n);
}
};

Request request = new Request.Builder()
.url("https://api.github.com/markdown/raw")
.post(requestBody)
.build();

Response response = client.newCall(request).execute();
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

System.out.println(response.body().string());
}

3.6.3、Post方式提交文件


public static final MediaType MEDIA_TYPE_MARKDOWN
= MediaType.parse("text/x-markdown; charset=utf-8");

private final OkHttpClient client = new OkHttpClient();

public void run() throws Exception {
File file = new File("README.md");

Request request = new Request.Builder()
.url("https://api.github.com/markdown/raw")
.post(RequestBody.create(MEDIA_TYPE_MARKDOWN, file))
.build();

Response response = client.newCall(request).execute();
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

System.out.println(response.body().string());
}

3.6.4、Post方式提交表单


使用FormEncodingBuilder来构建和HTML标签相同效果的请求体. 键值对将使用一种HTML兼容形式的URL编码来进行编码.


 private final OkHttpClient client = new OkHttpClient();

public void run() throws Exception {
RequestBody formBody = new FormBody.Builder()
.add("search", "Jurassic Park")
.build();
Request request = new Request.Builder()
.url("https://en.wikipedia.org/w/index.php")
.post(formBody)
.build();

Response response = client.newCall(request).execute();
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

System.out.println(response.body().string());
}

3.7、POST其他用法


3.7.1、提取响应头


典型的HTTP头像是一个Map<String, String> : 每个字段都有一个或没有值. 但是一些头允许多个值, 像Guava的Multimap


例如:


HTTP响应里面提供的Vary响应头, 就是多值的. OkHttp的api试图让这些情况都适用.



  • 当写请求头的时候, 使用header(name, value)可以设置唯一的name、value. 如果已经有值, 旧的将被移除,然后添加新的. 使用addHeader(name, value)可以添加多值(添加, 不移除已有的).

  • 当读取响应头时, 使用header(name)返回最后出现的name、value. 通常情况这也是唯一的name、value.如果没有值, 那么header(name)将返回null. 如果想读取字段对应的所有值,使用headers(name)会返回一个list.


为了获取所有的Header, Headers类支持按index访问.


private final OkHttpClient client = new OkHttpClient();

public void run() throws Exception {
Request request = new Request.Builder()
.url("https://api.github.com/repos/square/okhttp/issues")
.header("User-Agent", "OkHttp Headers.java")
.addHeader("Accept", "application/json; q=0.5")
.addHeader("Accept", "application/vnd.github.v3+json")
.build();

Response response = client.newCall(request).execute();
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

System.out.println("Server: " + response.header("Server"));
System.out.println("Date: " + response.header("Date"));
System.out.println("Vary: " + response.headers("Vary"));
}

3.7.2、使用Gson来解析JSON响应


Gson是一个在JSON和Java对象之间转换非常方便的api库. 这里我们用Gson来解析Github API的JSON响应.


注意: ResponseBody.charStream()使用响应头Content-Type指定的字符集来解析响应体. 默认是UTF-8.


private final OkHttpClient client = new OkHttpClient();
private final Gson gson = new Gson();

public void run() throws Exception {
Request request = new Request.Builder()
.url("https://api.github.com/gists/c2a7c39532239ff261be")
.build();
Response response = client.newCall(request).execute();
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

Gist gist = gson.fromJson(response.body().charStream(), Gist.class);
for (Map.Entry<String, GistFile> entry : gist.files.entrySet()) {
System.out.println(entry.getKey());
System.out.println(entry.getValue().content);
}
}

static class Gist {
Map<String, GistFile> files;
}

static class GistFile {
String content;
}

3.7.3、响应缓存


为了缓存响应, 你需要一个你可以读写的缓存目录, 和缓存大小的限制. 这个缓存目录应该是私有的, 不信任的程序应不能读取缓存内容.


一个缓存目录同时拥有多个缓存访问是错误的. 大多数程序只需要调用一次new OkHttp(), 在第一次调用时配置好缓存, 然后其他地方只需要调用这个实例就可以了. 否则两个缓存示例互相干扰, 破坏响应缓存, 而且有可能会导致程序崩溃.


响应缓存使用HTTP头作为配置. 你可以在请求头中添加Cache-Control: max-stale=3600 , OkHttp缓存会支持. 你的服务通过响应头确定响应缓存多长时间, 例如使用Cache-Control: max-age=9600.


private final OkHttpClient client;

public CacheResponse(File cacheDirectory) throws Exception {
int cacheSize = 10 * 1024 * 1024; // 10 MiB
Cache cache = new Cache(cacheDirectory, cacheSize);

client = new OkHttpClient();
client.setCache(cache);
}

public void run() throws Exception {
Request request = new Request.Builder()
.url("http://publicobject.com/helloworld.txt")
.build();

Response response1 = client.newCall(request).execute();
if (!response1.isSuccessful()) throw new IOException("Unexpected code " + response1);

String response1Body = response1.body().string();
System.out.println("Response 1 response: " + response1);
System.out.println("Response 1 cache response: " + response1.cacheResponse());
System.out.println("Response 1 network response: " + response1.networkResponse());

Response response2 = client.newCall(request).execute();
if (!response2.isSuccessful()) throw new IOException("Unexpected code " + response2);

String response2Body = response2.body().string();
System.out.println("Response 2 response: " + response2);
System.out.println("Response 2 cache response: " + response2.cacheResponse());
System.out.println("Response 2 network response: " + response2.networkResponse());

System.out.println("Response 2 equals Response 1? " + response1Body.equals(response2Body));
}

如果需要阻值response使用缓存, 使用CacheControl.FORCE_NETWORK. 如果需要阻值response使用网络, 使用CacheControl.FORCE_CACHE.


警告
如果你使用FORCE_CACHE, 但是response要求使用网络, OkHttp将会返回一个504 Unsatisfiable Request响应.





收起阅读 »

功能强大的升级库

CheckVersionLib V2版震撼来袭,功能强大,链式编程,调用简单,集成轻松,扩展性强大老规矩先看V2效果,这个版本最大的特点就是使用非常简单,相对于1.+版本 效果 特点 任何地方都可以调用 简单简单简单简单(重要的话我说四遍) 扩...
继续阅读 »

CheckVersionLib


V2版震撼来袭,功能强大,链式编程,调用简单,集成轻松,扩展性强大

老规矩先看V2效果,这个版本最大的特点就是使用非常简单,相对于1.+版本


效果

V2.gif



特点



  • 任何地方都可以调用




  • 简单简单简单简单(重要的话我说四遍)




  • 扩展性强大




  • 所有具有升级功能的app均可使用,耶稣说的




  • 更强大的自定义界面支持




  • 支持强制更新(一行代码)




  • 支持静默下载 (一行代码)




  • 适配到Android Q




导入

allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}


appcompat

  implementation 'com.github.AlexLiuSheng:CheckVersionLib:2.4.1_appcompat'


jitpack && android x

dependencies {
implementation 'com.github.AlexLiuSheng:CheckVersionLib:2.4.1_androidx'
}


使用

和1.+版本一样,两种模式


只使用下载模式


先来个最简单的调用

        AllenVersionChecker
.getInstance()
.downloadOnly(
UIData.create().setDownloadUrl(downloadUrl)
)
.executeMission(context);

UIData:UIData是一个Bundle,用于存放用于UI展示的一些数据,后面自定义界面时候可以拿来用


请求服务器版本+下载


该模式最简单的使用

   AllenVersionChecker
.getInstance()
.requestVersion()
.setRequestUrl(requestUrl)
.request(new RequestVersionListener() {
@Nullable
@Override
public UIData onRequestVersionSuccess(String result) {
//拿到服务器返回的数据,解析,拿到downloadUrl和一些其他的UI数据
...
//如果是最新版本直接return null
return UIData.create().setDownloadUrl(downloadUrl);
}

@Override
public void onRequestVersionFailure(String message) {

}
})
.executeMission(context);

请求版本一些其他的http参数可以设置,如下

 AllenVersionChecker
.getInstance()
.requestVersion()
.setHttpHeaders(httpHeader)
.setRequestMethod(HttpRequestMethod.POSTJSON)
.setRequestParams(httpParam)
.setRequestUrl(requestUrl)
.request(new RequestVersionListener() {
@Nullable
@Override
public UIData onRequestVersionSuccess(String result) {
//拿到服务器返回的数据,解析,拿到downloadUrl和一些其他的UI数据
...
UIData uiData = UIData
.create()
.setDownloadUrl(downloadUrl)
.setTitle(updateTitle)
.setContent(updateContent);
//放一些其他的UI参数,拿到后面自定义界面使用
uiData.getVersionBundle().putString("key", "your value");
return uiData;

}

@Override
public void onRequestVersionFailure(String message) {

}
})
.executeMission(context);


合适的地方关闭任务

为了避免不必要的内存泄漏,需要在合适的地方取消任务

AllenVersionChecker.getInstance().cancelAllMission();

以上就是最基本的使用(库默认会有一套界面),如果还不满足项目需求,下面就可以用这个库来飙车了


一些其他的function设置

解释下,下面的builder叫DownloadBuilder

 DownloadBuilder builder=AllenVersionChecker
.getInstance()
.downloadOnly();


or



DownloadBuilder builder=AllenVersionChecker
.getInstance()
.requestVersion()
.request()

取消任务


 AllenVersionChecker.getInstance().cancelAllMission(this);

静默下载


 builder.setSilentDownload(true); 默认false

设置当前服务器最新的版本号,供库判断是否使用缓存



  • 缓存策略:如果本地有安装包,首先判断与当前运行的程序的versionCode是否不一致,然后判断是否有传入最新的
    versionCode,如果传入的versionCode大于本地的,重新从服务器下载,否则使用缓存

 builder.setNewestVersionCode(int); 默认null

强制更新


设置此listener即代表需要强制更新,会在用户想要取消下载的时候回调
需要你自己关闭所有界面

builder.setForceUpdateListener(() -> {
forceUpdate();
});

update in v2.2.1
动态设置是否强制更新,如果使用本库来请求服务器,可以在回调时动态设置一些参数或者回调

   public UIData onRequestVersionSuccess(DownloadBuilder downloadBuilder,String result) {
downloadBuilder.setForceUpdateListener(() -> {
forceUpdate();
});
Toast.makeText(V2Activity.this, "request successful", Toast.LENGTH_SHORT).show();
return crateUIData();
}

下载忽略本地缓存


如果本地有安装包缓存也会重新下载apk

 builder.setForceRedownload(true); 默认false

是否显示下载对话框


builder.setShowDownloadingDialog(false); 默认true

是否显示通知栏


builder.setShowNotification(false);  默认true

以前台service运行(update in 2.2.2)
推荐以前台服务运行更新,防止在后台时,服务被杀死


builder.setRunOnForegroundService(true); 默认true

自定义通知栏


      builder.setNotificationBuilder(
NotificationBuilder.create()
.setRingtone(true)
.setIcon(R.mipmap.dialog4)
.setTicker("custom_ticker")
.setContentTitle("custom title")
.setContentText(getString(R.string.custom_content_text))
);

是否显示失败对话框


  builder.setShowDownloadFailDialog(false); 默认true

自定义下载路径


  builder.setDownloadAPKPath(address); 默认:/storage/emulated/0/AllenVersionPath/

自定义下载文件名


  builder.setApkName(apkName); 默认:getPackageName()

可以设置下载监听


   builder.setApkDownloadListener(new APKDownloadListener() {
@Override
public void onDownloading(int progress) {

}

@Override
public void onDownloadSuccess(File file) {

}

@Override
public void onDownloadFail() {

}
});

设置取消监听
此回调会监听所有cancel事件


 
builder.setOnCancelListener(() -> {
Toast.makeText(V2Activity.this,"Cancel Hanlde",Toast.LENGTH_SHORT).show();
});

如果想单独监听几种状态下的cancel,可像如下这样设置


  • builder.setDownloadingCancelListener();

  • builder.setDownloadFailedCancelListener();

  • builder.setReadyDownloadCancelListener();


设置确定监听(added after 2.2.2)



  • builder.setReadyDownloadCommitClickListener();

  • builder.setDownloadFailedCommitClickListener();


静默下载+直接安装(不会弹出升级对话框)


    builder.setDirectDownload(true);
builder.setShowNotification(false);
builder.setShowDownloadingDialog(false);
builder.setShowDownloadFailDialog(false);

自定义安装回调


    setCustomDownloadInstallListener(CustomInstallListener customDownloadInstallListener)


自定义界面

自定义界面使用回调方式,开发者需要返回自己定义的Dialog(父类android.app)



  • 所有自定义的界面必须使用listener里面的context实例化




  • 界面展示的数据通过UIData拿




自定义显示更新界面


设置CustomVersionDialogListener



  • 定义此界面必须有一个确定下载的按钮,按钮id必须为@id/versionchecklib_version_dialog_commit




  • 如果有取消按钮(没有忽略本条要求),则按钮id必须为@id/versionchecklib_version_dialog_cancel



eg.

  builder.setCustomVersionDialogListener((context, versionBundle) -> {
BaseDialog baseDialog = new BaseDialog(context, R.style.BaseDialog, R.layout.custom_dialog_one_layout);
//versionBundle 就是UIData,之前开发者传入的,在这里可以拿出UI数据并展示
TextView textView = baseDialog.findViewById(R.id.tv_msg);
textView.setText(versionBundle.getContent());
return baseDialog;
});

自定义下载中对话框界面


设置CustomDownloadingDialogListener


  • 如果此界面要设计取消操作(没有忽略),请务必将id设置为@id/versionchecklib_loading_dialog_cancel

    builder.setCustomDownloadingDialogListener(new CustomDownloadingDialogListener() {
@Override
public Dialog getCustomDownloadingDialog(Context context, int progress, UIData versionBundle) {
BaseDialog baseDialog = new BaseDialog(context, R.style.BaseDialog, R.layout.custom_download_layout);
return baseDialog;
}
//下载中会不断回调updateUI方法
@Override
public void updateUI(Dialog dialog, int progress, UIData versionBundle) {
TextView tvProgress = dialog.findViewById(R.id.tv_progress);
ProgressBar progressBar = dialog.findViewById(R.id.pb);
progressBar.setProgress(progress);
tvProgress.setText(getString(R.string.versionchecklib_progress, progress));
}
});

自定义下载失败对话框


设置CustomDownloadFailedListener



  • 如果有重试按钮请将id设置为@id/versionchecklib_failed_dialog_retry




  • 如果有 确认/取消按钮请将id设置为@id/versionchecklib_failed_dialog_cancel



   builder.setCustomDownloadFailedListener((context, versionBundle) -> {
BaseDialog baseDialog = new BaseDialog(context, R.style.BaseDialog, R.layout.custom_download_failed_dialog);
return baseDialog;
});


update Log


  • 2.2.1

    • 修复内存泄漏问题

    • 使用binder传递参数

    • 一些已知的bug




混淆配置

 -keepattributes *Annotation*
-keepclassmembers class * {
@org.greenrobot.eventbus.Subscribe ;
}
-keep enum org.greenrobot.eventbus.ThreadMode { *; }

# Only required if you use AsyncExecutor
-keepclassmembers class * extends org.greenrobot.eventbus.util.ThrowableFailureEvent {
(java.lang.Throwable);
}

git地址:https://github.com/AlexLiuSheng/CheckVersionLib

下载地址:CheckVersionLib-master.zip

收起阅读 »

优秀优秀,Android图片涂鸦库

DoodleImage doodle for Android. You can undo, zoom, move, add text, textures, etc. Also, a powerful, customizable and extensible d...
继续阅读 »

Doodle


Image doodle for Android. You can undo, zoom, move, add text, textures, etc. Also, a powerful, customizable and extensible doodle framework & multi-function drawing board.

Android图片涂鸦,具有撤消、缩放、移动、添加文字,贴图等功能。还是一个功能强大,可自定义和可扩展的涂鸦框架、多功能画板。

01.gif

01
02
03


Feature 特性



  • Brush and shape 画笔及形状


    The brush can choose hand-painted, mosaic, imitation, eraser, text, texture, and the imitation function is similar to that in PS, copying somewhere in the picture. Shapes can be selected from hand-drawn, arrows, lines, circles, rectangles, and so on. The background color of the brush can be selected as a color, or an image.


    画笔可以选择手绘、马赛克、仿制、橡皮擦、文字、贴图,其中仿制功能跟PS中的类似,复制图片中的某处地方。形状可以选择手绘、箭头、直线、圆、矩形等。画笔的底色可以选择颜色,或者一张图片。




  • Undo/Redo 撤销/重做


    Each step of the doodle operation can be undone or redone.


    每一步的涂鸦操作都可以撤销。




  • Zoom, move, and rotate 放缩、移动及旋转


    In the process of doodle, you can freely zoom, move and rotate the picture with gestures. Also, you can move,rotate and scale the doodle item.


    在涂鸦的过程中,可以自由地通过手势缩放、移动、旋转图片。可对涂鸦移动、旋转、缩放等。




  • Zoomer 放大器


    In order to doodle more finely, an zoomer can be set up during the doodle process.


    为了更细微地涂鸦,涂鸦过程中可以设置出现放大器。




Usage 用法


Gradle

allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}

dependencies {
compile 'com.github.1993hzw:Doodle:5.5.3'
}

There are two ways to use the Doodle library:

这里有两种方式使用Doodle涂鸦库


A. Launch DoodleActivity directly (the layout is like demo images above). If you need to customize more interactions, please use another method (Way B).

使用写好的涂鸦界面,直接启动.启动的页面可参看上面的演示图片。如果需要自定义更多的交互方式,则请使用另一种方式(即B方式)。

DoodleParams params = new DoodleParams(); // 涂鸦参数
params.mImagePath = imagePath; // the file path of image
DoodleActivity.startActivityForResult(MainActivity.this, params, REQ_CODE_DOODLE);

See DoodleParams for more details.

查看DoodleParams获取更多涂鸦参数信息。


B. Recommend, use DoodleView and customize your layout.

推荐的方法:使用DoodleView,便于拓展,灵活性高,自定义自己的交互界面.

/*
Whether or not to optimize drawing, it is suggested to open, which can optimize the drawing speed and performance.
Note: When item is selected for editing after opening, it will be drawn at the top level, and not at the corresponding level until editing is completed.
是否优化绘制,建议开启,可优化绘制速度和性能.
注意:开启后item被选中编辑时时会绘制在最上面一层,直到结束编辑后才绘制在相应层级
*/
boolean optimizeDrawing = true;
DoodleView mDoodleView = new DoodleView(this, bitmap, optimizeDrawing, new IDoodleListener() {
/*
called when save the doodled iamge.
保存涂鸦图像时调用
*/
@Override
public void onSaved(IDoodle doodle, Bitmap bitmap, Runnable callback) {
//do something
}

/*
called when it is ready to doodle because the view has been measured. Now, you can set size, color, pen, shape, etc.
此时view已经测量完成,涂鸦前的准备工作已经完成,在这里可以设置大小、颜色、画笔、形状等。
*/
@Override
public void onReady(IDoodle doodle) {
//do something
}
});

mTouchGestureListener = new DoodleOnTouchGestureListener(mDoodleView, new DoodleOnTouchGestureListener.ISelectionListener() {
/*
called when the item(such as text, texture) is selected/unselected.
item(如文字,贴图)被选中或取消选中时回调
*/
@Override
public void onSelectedItem(IDoodle doodle, IDoodleSelectableItem selectableItem, boolean selected) {
//do something
}

/*
called when you click the view to create a item(such as text, texture).
点击View中的某个点创建可选择的item(如文字,贴图)时回调
*/
@Override
public void onCreateSelectableItem(IDoodle doodle, float x, float y) {
//do something
/*
if (mDoodleView.getPen() == DoodlePen.TEXT) {
IDoodleSelectableItem item = new DoodleText(mDoodleView, "hello", 20 * mDoodleView.getUnitSize(), new DoodleColor(Color.RED), x, y);
mDoodleView.addItem(item);
} else if (mDoodleView.getPen() == DoodlePen.BITMAP) {
IDoodleSelectableItem item = new DoodleBitmap(mDoodleView, bitmap, 80 * mDoodle.getUnitSize(), x, y);
mDoodleView.addItem(item);
}
*/
}
});

// create touch detector, which dectects the gesture of scoll, scale, single tap, etc.
// 创建手势识别器,识别滚动,缩放,点击等手势
IDoodleTouchDetector detector = new DoodleTouchDetector(getApplicationContext(), mTouchGestureListener);
mDoodleView.setDefaultTouchDetector(detector);

// Setting parameters.设置参数
mDoodleView.setPen(DoodlePen.TEXT);
mDoodleView.setShape(DoodleShape.HAND_WRITE);
mDoodleView.setColor(new DoodleColor(Color.RED));

When turning off optimized drawing, you only need to call addItem(IDoodleItem) when you create it. When you start optimizing drawing, the created or selected item needs to call markItemToOptimizeDrawing(IDoodleItem), and you should call notifyItemFinishedDrawing(IDoodleItem) when you finish drawing. So this is generally used in code:

当关闭优化绘制时,只需要在创建时调用addItem(IDoodleItem);而当开启优化绘制时,创建或选中的item需要调用markItemToOptimizeDrawing(IDoodleItem),结束绘制时应调用notifyItemFinishedDrawing(IDoodleItem)。因此在代码中一般这样使用:

// when you are creating a item or selecting a item to edit
if (mDoodle.isOptimizeDrawing()) {
mDoodle.markItemToOptimizeDrawing(item);
} else {
mDoodle.addItem(item);
}

...

// finish creating or editting
if (mDoodle.isOptimizeDrawing()) {
mDoodle.notifyItemFinishedDrawing(item);
}

Then, add the DoodleView to your layout. Now you can start doodling freely.

把DoodleView添加到布局中,然后开始涂鸦。


Demo 实例

Here are other simple examples to teach you how to use the doodle framework.



  1. Mosaic effect
    马赛克效果




  2. Change text's size by scale gesture
    手势缩放文本大小



More...

Now I think you should know that DoodleActivity has used DoodleView. You also can customize your layout like DoodleActivity. See DoodleActivity for more details.

现在你应该知道DoodleActivity就是使用了DoodleView实现涂鸦,你可以参照DoodleActivity是怎么实现涂鸦界面的交互来实现自己的自定义页面。

DoodleView has implemented IDoodle.

DoodleView实现了IDoodle接口。

public interface IDoodle {
...
public float getUnitSize();
public void setDoodleRotation(int degree);
public void setDoodleScale(float scale, float pivotX, float pivotY);
public void setPen(IDoodlePen pen);
public void setShape(IDoodleShape shape);
public void setDoodleTranslation(float transX, float transY);
public void setSize(float paintSize);
public void setColor(IDoodleColor color);
public void addItem(IDoodleItem doodleItem);
public void removeItem(IDoodleItem doodleItem);
public void save();
public void topItem(IDoodleItem item);
public void bottomItem(IDoodleItem item);
public boolean undo(int step);
...
}


Framework diagram 框架图

structure


Doodle Coordinate 涂鸦坐标

coordinate


Extend 拓展

You can create a customized item like DoodlePath, DoodleText, DoodleBitmap which extend DoodleItemBase or implement IDoodleItem.

实现IDoodleItem接口或基础DoodleItemBase,用于创建自定义涂鸦条目item,比如DoodlePath, DoodleText, DoodleBitmap

You can create a customized pen like DoodlePen which implements IDoodlePen.

实现IDoodlePen接口用于创建自定义画笔pen,比如DoodlePen

You can create a customized shape like DoodleShape which implements IDoodleShape.

实现IDoodleShape接口用于创建自定义形状shape,比如DoodleShape

You can create a customized color like DoodleColor which implements IDoodleColor.

实现IDoodleColor接口用于创建自定义颜色color,比如DoodleColor

You can create a customized touch gesture detector like DoodleTouchDetector(GestureListener) which implements IDoodleTouchDetector.

实现IDoodleTouchDetector接口用于创建自定义手势识别器,比如DoodleTouchDetector


git地址:https://github.com/1993hzw/doodle

下载地址:doodle-master.zip

收起阅读 »

Swift - 第三方日历组件CVCalendar使用详解1(配置、基本用法)

CVCalendar 是一款超好用的第三方日历组件,不仅功能强大,而且可以方便地进行样式自定义。同时,CVCalendar 还提供月视图、周视图两种展示模式,我们可以根据需求自由选择使用。一、安装配置1. 从 GitHub 上下载最新的代码:https://g...
继续阅读 »

CVCalendar 是一款超好用的第三方日历组件,不仅功能强大,而且可以方便地进行样式自定义。同时,CVCalendar 还提供月视图、周视图两种展示模式,我们可以根据需求自由选择使用。

一、安装配置

1. 从 GitHub 上下载最新的代码:https://github.com/Mozharovsky/CVCalendar
2. 将下载下来的源码包中 CVCalendar.xcodeproj 拖拽至你的工程中 


3. 工程 -> General -> Embedded Binaries 项,把 iOS 版的 framework 添加进来:CVCalendar.framework


4. 最后,在需要使用 CVCalendar 的地方 import 进来就可以了

import CVCalendar

二、基本用法

1,月视图使用样例 

① 效果图
1. 初始化的时候自动显示当月日历,且“今天”的日期文字是红色的。
2. 顶部导航栏标题显示当前日历的年、月信息,日历左右滑动切换的时候,标题内容也会随之改变。
3. 点击导航栏右侧的“今天”按钮,日历又会跳回到当前日期。
4. 点击日历上的任一日期时间后,该日期背景色会变蓝色(如果是今天则变红色)。同时我们在日期选择响应中,将选择的日期弹出显示。

      

② 样例代码
日历组件分为:CVCalendarMenuView 和 CVCalendarView 两部分。前者是显示星期的菜单栏,后者是日期表格视图。这二者的位置和大小我们可以随意调整设置。
组件提供了许多代理协议让我进行样式调整或功能响应,我们可以选择使用。但其中 CVCalendarViewDelegate, CVCalendarMenuViewDelegate 这两个协议是必须的。

import UIKit
import CVCalendar

class ViewController: UIViewController {
//星期菜单栏
private var menuView: CVCalendarMenuView!

//日历主视图
private var calendarView: CVCalendarView!

var currentCalendar: Calendar!

override func viewDidLoad() {
super.viewDidLoad()

currentCalendar = Calendar.init(identifier: .gregorian)

//初始化的时候导航栏显示当年当月
self.title = CVDate(date: Date(), calendar: currentCalendar).globalDescription

//初始化星期菜单栏
self.menuView = CVCalendarMenuView(frame: CGRect(x:0, y:80, width:300, height:15))

//初始化日历主视图
self.calendarView = CVCalendarView(frame: CGRect(x:0, y:110, width:300,
height:450))

//星期菜单栏代理
self.menuView.menuViewDelegate = self

//日历代理
self.calendarView.calendarDelegate = self

//将菜单视图和日历视图添加到主视图上
self.view.addSubview(menuView)
self.view.addSubview(calendarView)
}

//今天按钮点击
@IBAction func todayButtonTapped(_ sender: AnyObject) {
let today = Date()
self.calendarView.toggleViewWithDate(today)
}

override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()

//更新日历frame
self.menuView.commitMenuViewUpdate()
self.calendarView.commitCalendarViewUpdate()
}

override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}

extension ViewController: CVCalendarViewDelegate,CVCalendarMenuViewDelegate {
//视图模式
func presentationMode() -> CalendarMode {
//使用月视图
return .monthView
}

//每周的第一天
func firstWeekday() -> Weekday {
//从星期一开始
return .monday
}

func presentedDateUpdated(_ date: CVDate) {
//导航栏显示当前日历的年月
self.title = date.globalDescription
}

//每个日期上面是否添加横线(连在一起就形成每行的分隔线)
func topMarker(shouldDisplayOnDayView dayView: CVCalendarDayView) -> Bool {
return true
}

//切换月的时候日历是否自动选择某一天(本月为今天,其它月为第一天)
func shouldAutoSelectDayOnMonthChange() -> Bool {
return false
}

//日期选择响应
func didSelectDayView(_ dayView: CVCalendarDayView, animationDidFinish: Bool) {
//获取日期
let date = dayView.date.convertedDate()!
// 创建一个日期格式器
let dformatter = DateFormatter()
dformatter.dateFormat = "yyyy年MM月dd日"
let message = "当前选择的日期是:\(dformatter.string(from: date))"
//将选择的日期弹出显示
let alertController = UIAlertController(title: "", message: message,
preferredStyle: .alert)
let okAction = UIAlertAction(title: "确定", style: .cancel, handler: nil)
alertController.addAction(okAction)
self.present(alertController, animated: true, completion: nil)
}
}

2,周视图使用样例

同月视图模式相比,周视图日历区域只有一行(每次显示7天日期)。其它方面和月视图相比差别不大,也都是左右滑动切换显示下一周、下一周日期。


import UIKit
import CVCalendar

class ViewController: UIViewController {
//星期菜单栏
private var menuView: CVCalendarMenuView!

//日历主视图
private var calendarView: CVCalendarView!

var currentCalendar: Calendar!

override func viewDidLoad() {
super.viewDidLoad()

currentCalendar = Calendar.init(identifier: .gregorian)

//初始化的时候导航栏显示当年当月
self.title = CVDate(date: Date(), calendar: currentCalendar).globalDescription

//初始化星期菜单栏
self.menuView = CVCalendarMenuView(frame: CGRect(x:0, y:80, width:300, height:15))

//初始化日历主视图
self.calendarView = CVCalendarView(frame: CGRect(x:0, y:110, width:300,
height:50))

//星期菜单栏代理
self.menuView.menuViewDelegate = self

//日历代理
self.calendarView.calendarDelegate = self

//将菜单视图和日历视图添加到主视图上
self.view.addSubview(menuView)
self.view.addSubview(calendarView)
}

//今天按钮点击
@IBAction func todayButtonTapped(_ sender: AnyObject) {
let today = Date()
self.calendarView.toggleViewWithDate(today)
}

override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()

//更新日历frame
self.menuView.commitMenuViewUpdate()
self.calendarView.commitCalendarViewUpdate()
}

override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}

extension ViewController: CVCalendarViewDelegate,CVCalendarMenuViewDelegate {
//视图模式
func presentationMode() -> CalendarMode {
//使用周视图
return .weekView
}

//每周的第一天
func firstWeekday() -> Weekday {
//从星期一开始
return .monday
}

func presentedDateUpdated(_ date: CVDate) {
//导航栏显示当前日历的年月
self.title = date.globalDescription
}

//每个日期上面是否添加横线(连在一起就形成每行的分隔线)
func topMarker(shouldDisplayOnDayView dayView: CVCalendarDayView) -> Bool {
return true
}

//切换周的时候日历是否自动选择某一天(本周为今天,其它周为第一天)
func shouldAutoSelectDayOnWeekChange() -> Bool {
return false
}

//日期选择响应
func didSelectDayView(_ dayView: CVCalendarDayView, animationDidFinish: Bool) {
//获取日期
let date = dayView.date.convertedDate(calendar: currentCalendar)!
// 创建一个日期格式器
let dformatter = DateFormatter()
dformatter.dateFormat = "yyyy年MM月dd日"
let message = "当前选择的日期是:\(dformatter.string(from: date))"
//将选择的日期弹出显示
let alertController = UIAlertController(title: "", message: message,
preferredStyle: .alert)
let okAction = UIAlertAction(title: "确定", style: .cancel, handler: nil)
alertController.addAction(okAction)
self.present(alertController, animated: true, completion: nil)
}
}

转自:https://www.hangge.com/blog/cache/detail_1504.html#

收起阅读 »

LeakCanary原理分析

LeakCanary 是一个很好用的Android内存泄露检测工具,今天从源码角度分析下其检测内存泄露的原理,不同版本 源码 会有一定差异,这里参考的是2.7版本。1. Reference简介Java中的四种引用类型,我们先简单复习下强引用,对象有强引用时不能...
继续阅读 »

LeakCanary 是一个很好用的Android内存泄露检测工具,今天从源码角度分析下其检测内存泄露的原理,不同版本 源码 会有一定差异,这里参考的是2.7版本。

1. Reference简介

Java中的四种引用类型,我们先简单复习下

  • 强引用,对象有强引用时不能被回收
  • 软引用 SoftReference,对象只有软引用时,在内存不足时触发GC会回收该对象
  • 弱引用 WeakReference,对象只有弱引用时,下次GC就会回收该对象
  • 虚引用 PhantomReference,平常很少会用到,源码注释主要用来监听对象清理前的动作,比Java finalization更灵活,PhantomReference需要与 ReferenceQueue 一起配合使用。

Phantom references are most often used for scheduling pre-mortem cleanup actions in a more flexible way than is possible with the Java finalization mechanism.

ReferenceQueue

上面提到PhantomReferenceReferenceQueue配合监听对象被回收,实际上WeakReferenceSoftReference同样可以与ReferenceQueue关联使用,只要构造方法传入ReferenceQueue参数即可。在引用所指的对象被回收后,引用本身将会被加入到ReferenceQueue之中。

2. LeakCanary使用简介

  • 在app的build.gradle中加入依赖
dependencies {
// debugImplementation because LeakCanary should only run in debug builds.
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
}
  • LeakCanary会自动监控Activity、Fragment、Fragment View、RootView、Service的泄露
  • 如果需要监控其它对象的泄露,可以手动添加如下代码
AppWatcher.objectWatcher.watch(myView, "View was detached")

3. LeakCanary源码分析

初始化

LeakCanary新版本使用ContentProvider自动初始化,不需要再手动调用install方法


    android:name="leakcanary.internal.AppWatcherInstaller$MainProcess"
android:authorities="${applicationId}.leakcanary-installer"
android:enabled="@bool/leak_canary_watcher_auto_install"
android:exported="false" />

如果想禁用自动初始化,在app res中加入

false

接下来我们从源码分析下LeakCanary初始化的流程:

internal sealed class AppWatcherInstaller : ContentProvider() {

/**
* [MainProcess] automatically sets up the LeakCanary code that runs in the main app process.
*/
internal class MainProcess : AppWatcherInstaller()

/**
* When using the `leakcanary-android-process` artifact instead of `leakcanary-android`,
* [LeakCanaryProcess] automatically sets up the LeakCanary code
*/
internal class LeakCanaryProcess : AppWatcherInstaller()

override fun onCreate(): Boolean {
val application = context!!.applicationContext as Application
AppWatcher.manualInstall(application)
return true
}

...
}

AppWatcherInstaller继承ContentProvider,onCreate会调用AppWatcher的manualInstall方法,完成自动初始化。

object AppWatcher {

/**
* The [ObjectWatcher] used by AppWatcher to detect retained objects.
* Only set when [isInstalled] is true.
*/
val objectWatcher = ObjectWatcher(
clock = { SystemClock.uptimeMillis() },
checkRetainedExecutor = {
check(isInstalled) {
"AppWatcher not installed"
}
mainHandler.postDelayed(it, retainedDelayMillis)
},
isEnabled = { true }
)

@JvmOverloads
fun manualInstall(
application: Application,
retainedDelayMillis: Long = TimeUnit.SECONDS.toMillis(5),
watchersToInstall: List = appDefaultWatchers(application)
) {
checkMainThread()
if (isInstalled) {
throw IllegalStateException(
"AppWatcher already installed, see exception cause for prior install call", installCause
)
}
check(retainedDelayMillis >= 0) {
"retainedDelayMillis $retainedDelayMillis must be at least 0 ms"
}
installCause = RuntimeException("manualInstall() first called here")
this.retainedDelayMillis = retainedDelayMillis
if (application.isDebuggableBuild) {
LogcatSharkLog.install()
}
// Requires AppWatcher.objectWatcher to be set
LeakCanaryDelegate.loadLeakCanary(application)

watchersToInstall.forEach {
it.install()
}
}

fun appDefaultWatchers(
application: Application,
reachabilityWatcher: ReachabilityWatcher = objectWatcher
): List {
return listOf(
ActivityWatcher(application, reachabilityWatcher),
FragmentAndViewModelWatcher(application, reachabilityWatcher),
RootViewWatcher(reachabilityWatcher),
ServiceWatcher(reachabilityWatcher)
)
}
...
}

manualInstall方法有3个参数:

  • application:application对象
  • retainedDelayMillis:默认值5s,表示5s后检测对象是否被回收
  • watchersToInstall:安装的监控器,每个监控器抽象成InstallableWatcher,默认值在appDefaultWatchers方法中定义,包括ActivityWatcher、FragmentAndViewModelWatcher、RootViewWatcher、ServiceWatcher,后面单独分析。

创建InstallableWatcher时需要传入ReachabilityWatcher,实现类是ObjectWatcher,这是监控对象可达性的核心。AppWatcher创建了ObjectWatcher对象,并在checkRetainedExecutor里面加入了延迟5s的逻辑,下面配合ObjectWatcher源码一起分析。

ObjectWatcher

上面说到手动监控其它对象是调用ObjectWatcher的watch方法,这里是真正的核心逻辑,我们看下其部分代码

class ObjectWatcher constructor(
private val clock: Clock,
private val checkRetainedExecutor: Executor,
/**
* Calls to [watch] will be ignored when [isEnabled] returns false
*/
private val isEnabled: () -> Boolean = { true }
) : ReachabilityWatcher {

fun watch(
watchedObject: Any,
description: String
) {
expectWeaklyReachable(watchedObject, description)
}

@Synchronized override fun expectWeaklyReachable(
watchedObject: Any,
description: String
) {
if (!isEnabled()) {
return
}
removeWeaklyReachableObjects()
val key = UUID.randomUUID()
.toString()
val watchUptimeMillis = clock.uptimeMillis()
val reference =
KeyedWeakReference(watchedObject, key, description, watchUptimeMillis, queue)
SharkLog.d {
"Watching " +
(if (watchedObject is Class<*>) watchedObject.toString() else "instance of ${watchedObject.javaClass.name}") +
(if (description.isNotEmpty()) " ($description)" else "") +
" with key $key"
}

watchedObjects[key] = reference
checkRetainedExecutor.execute {
moveToRetained(key)
}
}

@Synchronized private fun moveToRetained(key: String) {
removeWeaklyReachableObjects()
val retainedRef = watchedObjects[key]
if (retainedRef != null) {
retainedRef.retainedUptimeMillis = clock.uptimeMillis()
onObjectRetainedListeners.forEach { it.onObjectRetained() }
}
}

private fun removeWeaklyReachableObjects() {
// WeakReferences are enqueued as soon as the object to which they point to becomes weakly
// reachable. This is before finalization or garbage collection has actually happened.
var ref: KeyedWeakReference?
do {
ref = queue.poll() as KeyedWeakReference?
if (ref != null) {
watchedObjects.remove(ref.key)
}
} while (ref != null)
}

}

调用watch方法会走到expectWeaklyReachable,里面大致做了4件事情:

  • 从watchedObjects中移除已经正常释放的引用 removeWeaklyReachableObjects()。这里用到了前面讲的ReferenceQueue,对象被回收会加入到queue中,将queu中存在的从watchedObjects中移除。
  • 创建KeyedWeakReference引用,KeyedWeakReference继承WeakReference,创建时传入了ReferenceQueue,监听对象的回收。
  • 将引用加入到watchedObjects中 watchedObjects[key] = reference
  • 延迟5s之后执行moveToRetained(key),确认对象是否回收(延迟逻辑在AppWatcher传入的checkRetainedExecutor中实现)。moveToRetained中首先执行removeWeaklyReachableObjects,之后再判断watchedObjects中是否还存在此key,若存在说明对象未被回收,发生内存泄露。

到这里我们基本明白了LeakCanary监控对象是否回收的逻辑,接下来我们再看看他是如何自动监控Activity、Fragment等组件的。前面讲到AppWatcher初始化时,会自动创建ActivityWatcher、FragmentAndViewModelWatcher、RootViewWatcher、ServiceWatcher,我们阅读下他们的源码。

ActivityWatcher

class ActivityWatcher(
private val application: Application,
private val reachabilityWatcher: ReachabilityWatcher
) : InstallableWatcher {

private val lifecycleCallbacks =
object : Application.ActivityLifecycleCallbacks by noOpDelegate() {
override fun onActivityDestroyed(activity: Activity) {
reachabilityWatcher.expectWeaklyReachable(
activity, "${activity::class.java.name} received Activity#onDestroy() callback"
)
}
}

override fun install() {
application.registerActivityLifecycleCallbacks(lifecycleCallbacks)
}

override fun uninstall() {
application.unregisterActivityLifecycleCallbacks(lifecycleCallbacks)
}
}

ActivityWatcher的代码非常简单,注册Activity生命周期回调,在onActivityDestroyed中调用ObjectWatcher的expectWeaklyReachable,监控activity对象5s之内是否被释放。

FragmentAndViewModelWatcher

FragmentAndViewModelWatcher监控Fragment和Fragment View的泄露,原理与Activity类似,在onFragmentDestroyed和onFragmentViewDestroyed中调用ObjectWatcher的expectWeaklyReachable方法。只不过监听Fragment的onDestroy相对复杂点,原理是先监听Activity生命周期,然后在Activity onCreate时通过fragmentManager.registerFragmentLifecycleCallbacks注册Fragment生命周期回调。而且同时兼容了android.app.Fragment、androidx.fragment.app.Fragment、android.support.v4.app.Fragment。

class FragmentAndViewModelWatcher(
private val application: Application,
private val reachabilityWatcher: ReachabilityWatcher
) : InstallableWatcher {

private val fragmentDestroyWatchers: List<(Activity) -> Unit> = run {
val fragmentDestroyWatchers = mutableListOf<(Activity) -> Unit>()

if (SDK_INT >= O) {
fragmentDestroyWatchers.add(
AndroidOFragmentDestroyWatcher(reachabilityWatcher)
)
}

getWatcherIfAvailable(
ANDROIDX_FRAGMENT_CLASS_NAME,
ANDROIDX_FRAGMENT_DESTROY_WATCHER_CLASS_NAME,
reachabilityWatcher
)?.let {
fragmentDestroyWatchers.add(it)
}

getWatcherIfAvailable(
ANDROID_SUPPORT_FRAGMENT_CLASS_NAME,
ANDROID_SUPPORT_FRAGMENT_DESTROY_WATCHER_CLASS_NAME,
reachabilityWatcher
)?.let {
fragmentDestroyWatchers.add(it)
}
fragmentDestroyWatchers
}
}

@SuppressLint("NewApi")
internal class AndroidOFragmentDestroyWatcher(
private val reachabilityWatcher: ReachabilityWatcher
) : (Activity) -> Unit {
private val fragmentLifecycleCallbacks = object : FragmentManager.FragmentLifecycleCallbacks() {

override fun onFragmentViewDestroyed(
fm: FragmentManager,
fragment: Fragment
) {
val view = fragment.view
if (view != null) {
reachabilityWatcher.expectWeaklyReachable(
view, "${fragment::class.java.name} received Fragment#onDestroyView() callback " +
"(references to its views should be cleared to prevent leaks)"
)
}
}

override fun onFragmentDestroyed(
fm: FragmentManager,
fragment: Fragment
) {
reachabilityWatcher.expectWeaklyReachable(
fragment, "${fragment::class.java.name} received Fragment#onDestroy() callback"
)
}
}

override fun invoke(activity: Activity) {
val fragmentManager = activity.fragmentManager
fragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, true)
}
}

RootViewWatcher

RootViewWatcher监控RootView的泄露,在rootView onDetachedFromWindow回调时,调用ObjectWatcher的expectWeaklyReachable方法。rootView的onDetachedFromWindow回调监听是通过Square开源的Curtains库实现。

class RootViewWatcher(
private val reachabilityWatcher: ReachabilityWatcher
) : InstallableWatcher {

private val listener = OnRootViewAddedListener { rootView ->
val trackDetached = when(rootView.windowType) {
PHONE_WINDOW -> {
when (rootView.phoneWindow?.callback?.wrappedCallback) {
// Activities are already tracked by ActivityWatcher
is Activity -> false
is Dialog -> rootView.resources.getBoolean(R.bool.leak_canary_watcher_watch_dismissed_dialogs)
// Probably a DreamService
else -> true
}
}
// Android widgets keep detached popup window instances around.
POPUP_WINDOW -> false
TOOLTIP, TOAST, UNKNOWN -> true
}
if (trackDetached) {
rootView.addOnAttachStateChangeListener(object : OnAttachStateChangeListener {

val watchDetachedView = Runnable {
reachabilityWatcher.expectWeaklyReachable(
rootView, "${rootView::class.java.name} received View#onDetachedFromWindow() callback"
)
}

override fun onViewAttachedToWindow(v: View) {
mainHandler.removeCallbacks(watchDetachedView)
}

override fun onViewDetachedFromWindow(v: View) {
mainHandler.post(watchDetachedView)
}
})
}
}

override fun install() {
Curtains.onRootViewsChangedListeners += listener
}

override fun uninstall() {
Curtains.onRootViewsChangedListeners -= listener
}
}

ServiceWatcher

ServiceWatcher监控Service的泄露,原理也是在Service的onDestroy时调用ObjectWatcher的expectWeaklyReachable方法。Service的onDestroy是通过反射和动态代理ActivityManager和ActivityThread,代码比较巧妙,可以仔细消化下。

@SuppressLint("PrivateApi")
class ServiceWatcher(private val reachabilityWatcher: ReachabilityWatcher) : InstallableWatcher {

private val servicesToBeDestroyed = WeakHashMap>()

private val activityThreadClass by lazy { Class.forName("android.app.ActivityThread") }

private val activityThreadInstance by lazy {
activityThreadClass.getDeclaredMethod("currentActivityThread").invoke(null)!!
}

private val activityThreadServices by lazy {
val mServicesField =
activityThreadClass.getDeclaredField("mServices").apply { isAccessible = true }

@Suppress("UNCHECKED_CAST")
mServicesField[activityThreadInstance] as Map
}

private var uninstallActivityThreadHandlerCallback: (() -> Unit)? = null
private var uninstallActivityManager: (() -> Unit)? = null

override fun install() {
checkMainThread()
check(uninstallActivityThreadHandlerCallback == null) {
"ServiceWatcher already installed"
}
check(uninstallActivityManager == null) {
"ServiceWatcher already installed"
}
try {
swapActivityThreadHandlerCallback { mCallback ->
uninstallActivityThreadHandlerCallback = {
swapActivityThreadHandlerCallback {
mCallback
}
}
Handler.Callback { msg ->
if (msg.what == STOP_SERVICE) {
val key = msg.obj as IBinder
activityThreadServices[key]?.let {
onServicePreDestroy(key, it)
}
}
mCallback?.handleMessage(msg) ?: false
}
}
swapActivityManager { activityManagerInterface, activityManagerInstance ->
uninstallActivityManager = {
swapActivityManager { _, _ ->
activityManagerInstance
}
}
Proxy.newProxyInstance(
activityManagerInterface.classLoader, arrayOf(activityManagerInterface)
) { _, method, args ->
if (METHOD_SERVICE_DONE_EXECUTING == method.name) {
val token = args!![0] as IBinder
if (servicesToBeDestroyed.containsKey(token)) {
onServiceDestroyed(token)
}
}
try {
if (args == null) {
method.invoke(activityManagerInstance)
} else {
method.invoke(activityManagerInstance, *args)
}
} catch (invocationException: InvocationTargetException) {
throw invocationException.targetException
}
}
}
} catch (ignored: Throwable) {
SharkLog.d(ignored) { "Could not watch destroyed services" }
}
}

override fun uninstall() {
checkMainThread()
uninstallActivityManager?.invoke()
uninstallActivityThreadHandlerCallback?.invoke()
uninstallActivityManager = null
uninstallActivityThreadHandlerCallback = null
}

private fun onServicePreDestroy(
token: IBinder,
service: Service
) {
servicesToBeDestroyed[token] = WeakReference(service)
}

private fun onServiceDestroyed(token: IBinder) {
servicesToBeDestroyed.remove(token)?.also { serviceWeakReference ->
serviceWeakReference.get()?.let { service ->
reachabilityWatcher.expectWeaklyReachable(
service, "${service::class.java.name} received Service#onDestroy() callback"
)
}
}
}

private fun swapActivityThreadHandlerCallback(swap: (Handler.Callback?) -> Handler.Callback?) {
val mHField =
activityThreadClass.getDeclaredField("mH").apply { isAccessible = true }
val mH = mHField[activityThreadInstance] as Handler

val mCallbackField =
Handler::class.java.getDeclaredField("mCallback").apply { isAccessible = true }
val mCallback = mCallbackField[mH] as Handler.Callback?
mCallbackField[mH] = swap(mCallback)
}

@SuppressLint("PrivateApi")
private fun swapActivityManager(swap: (Class<*>, Any) -> Any) {
val singletonClass = Class.forName("android.util.Singleton")
val mInstanceField =
singletonClass.getDeclaredField("mInstance").apply { isAccessible = true }

val singletonGetMethod = singletonClass.getDeclaredMethod("get")

val (className, fieldName) = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
"android.app.ActivityManager" to "IActivityManagerSingleton"
} else {
"android.app.ActivityManagerNative" to "gDefault"
}

val activityManagerClass = Class.forName(className)
val activityManagerSingletonField =
activityManagerClass.getDeclaredField(fieldName).apply { isAccessible = true }
val activityManagerSingletonInstance = activityManagerSingletonField[activityManagerClass]

// Calling get() instead of reading from the field directly to ensure the singleton is
// created.
val activityManagerInstance = singletonGetMethod.invoke(activityManagerSingletonInstance)

val iActivityManagerInterface = Class.forName("android.app.IActivityManager")
mInstanceField[activityManagerSingletonInstance] =
swap(iActivityManagerInterface, activityManagerInstance!!)
}

companion object {
private const val STOP_SERVICE = 116

private const val METHOD_SERVICE_DONE_EXECUTING = "serviceDoneExecuting"
}
}

总结

今天从源码角度分析了LeakCanary监控内存泄露的核心原理。除此之外,LeakCanary还可以导出、分析、分类堆栈,如果后面有时间咱们再单独讲吧。

收起阅读 »

Android即时通讯系列文章(1)多进程:为什么要把消息服务拆分到一个独立的进程?

这是即时通讯系列文章的第一篇,正式开始对IM开发技术的讲解之前,我们先来谈谈客户端在完整聊天系统中所扮演的角色,为此,我们必须先明确客户端的职责。现今主流的IM应用几乎都是采用服务器中转的方式来进行消息传输的,为的是更好地支持离线、群组等业务。在这种模式下,所...
继续阅读 »

这是即时通讯系列文章的第一篇,正式开始对IM开发技术的讲解之前,我们先来谈谈客户端在完整聊天系统中所扮演的角色,为此,我们必须先明确客户端的职责。

现今主流的IM应用几乎都是采用服务器中转的方式来进行消息传输的,为的是更好地支持离线、群组等业务。在这种模式下,所有客户端都需连接到服务端,服务端将不同客户端发给自己的消息根据消息里携带的用户标识进行转发或广播。

因此,作为消息收发的终端设备,客户端的重要职责之一就是保持与服务端的连接,该连接的稳定性直接决定消息收发的实时性和可靠性。而在上篇文章我们讲过,移动设备是资源受限的,这对连接的稳定性提出了极大的挑战,具体可体现在以下两个方面:

  • 为了维持多任务环境的正常运行,Android为每个应用的堆大小设置了硬性上限,不同设备的确切堆大小取决于设备的总体可用RAM大小,如果应用在达到堆容量上限后尝试分配更多内容,则可能引发OOM。
  • 当用户切换到其他应用时,系统会将原有应用的进程保留在缓存中,稍后如果用户返回该应用,系统就会重复使用该进程,以便加快应用切换速度。但当系统资源(如内存)不足时,系统会考虑终止占用最多内存的、优先级较低的进程以释放RAM。

虽然ART和Dalvik虚拟机会例行执行垃圾回收任务,但如果应用存在内存泄漏问题,并且只有一个主进程,势必会随着应用使用时间的延长而逐步增大内存使用量,从而增加引发OOM的概率和缓存进程被系统终止的风险。

因此,为了保证连接的稳定性,可考虑将负责连接保持工作的消息服务放入一个独立的进程中,分离之后即使主进程退出、崩溃或者出现内存消耗过高等情况,该服务仍可正常运行,甚至可以在适当的时机通过广播等方式重新唤起主进程。

但是,给应用划分进程,往往就意味着需要编写额外的进程通讯代码,特别是对于消息服务这种需要高度交互的场景。而由于各个进程都运行在相对独立的内存空间,因而是无法直接通讯的。为此,Android提供了AIDL(Android Interface Definition Language,Android接口定义语言)用于实现进程间通信,其本质就是实现对象的序列化、传输、接收和反序列化,得到可操作的对象后再进行常规的方法调用。

接下来,就让我们来一步步实现跨进程的通讯吧。

Step1 创建服务

由于连接保持的工作是需要在后台执行长时间执行的操作,通常不提供操作界面,符合这个特性的组件就是Service了,因此我们选用Service作为与远程进程进行进程间通信(IPC)的组件。创建Service的子类时,必须实现onBind回调方法,此处我们暂时返回空实现。

class MessageAccessService : Service() {
override fun onBind(intent: Intent?): IBinder? {
return null
}
}

另外使用Service还有一个好处就是,我们可以在适当的时机将其升级为前台服务,前台服务是用户主动意识到的一种服务,进程优先级较高,因此在内存不足时,系统也不会考虑将其终止。

使用前台服务唯一的缺点就是必须在抽屉式通知栏提供一条不可移除的通知,对于用户体验极不友好,但是我们可以通过定制通知样式进行协调,后续的文章中会讲到。

step2 指定进程

默认情况下,同一应用的所有组件均在相同的进程中运行。如需控制某个组件所属的进程,可通过在清单文件中设置android:process属性实现:

<manifest ...>
<application ...>
<service
android:name=".service.MessageAccessService"
android:exported="true"
android:process=":remote" />
</application>
</manifest>

另外,为使其他进程的组件能调用服务或与之交互,还需设置android:exported属性为true。

step3 创建.aidl 文件

让我们重新把目光放回onBind回调方法,该方法要求返回IBinder对象,客户端可使用该对象定义好的接口与服务进行通信。IBinder是远程对象的基础接口,该接口描述了与远程对象交互的抽象协议,但不建议直接实现此接口,而应从Binder扩展。通常做法是是使用.aidl文件来描述所需的接口,使其生成适当的Binder子类。

那么,这个最关键的.aidl文件该如何创建,又该定义哪些接口呢?

创建.aidl文件很简单,Android Studio本身就提供了创建AIDL文件方法:项目右键 -> New -> AIDL -> AIDL File

前面讲过,客户端是消息收发的终端设备,而接入服务则是为客户端提供了消息收发的出入口。客户端发出的消息经由接入服务发送到服务端,同时客户端会委托接入服务帮忙收取消息,当服务端有消息推送过来时通知自己。

如此一来便很清晰了,我们要定义的接口总共有三个,分别为:

  • 发送消息
  • 注册消息接收器
  • 反注册消息接收器

MessageCarrier.aidl

package com.xxx.imsdk.comp.remote;
import com.xxx.imsdk.comp.remote.bean.Envelope;
import com.xxx.imsdk.comp.remote.listener.MessageReceiver;

interface MessageCarrier {
void sendMessage(in Envelope envelope);
void registerReceiveListener(MessageReceiver messageReceiver);
void unregisterReceiveListener(MessageReceiver messageReceiver);
}

这里解释一下上述接口中携带的参数的含义:

Envelope ->

解释这个参数之前,得先介绍Envelope.java这个类,该类是多进程通讯中作为数据传输的实体类。AIDL支持的数据类型除了基本数据类型、String和CharSequence,还有就是实现了Parcelable接口的对象,以及其中元素为以上几种的List和Map。

Envelope.java

**
* 用于多进程通讯的信封类
* <p>
* 在AIDL中传递的对象,需要在类文件相同路径下,创建同名、但是后缀为.aidl的文件,并在文件中使用parcelable关键字声明这个类;
* 但实际业务中需要传递的对象所属的类往往分散在不同的模块,所以通过构建一个包装类来包含真正需要被传递的对象(必须也实现Parcelable接口)
*/
@Parcelize
data class Envelope(val messageVo: MessageVo? = null,
val noticeVo: NoticeVo? = null) : Parcelable {
}

另外,在AIDL中传递的对象,需要在上述类文件的相同包路径下,创建同名、但是后缀为.aidl的文件,并在文件中使用parcelable关键字声明这个类,Envelope.aidl就是对应Envelope.java而创建的;

Envelope.aidl

package com.xxx.imsdk.comp.remote.bean;

parcelable Envelope;

两个文件对应的路径比较如下:

clipboard.png

那为什么是Envelope类而不直接是MessageVO类(消息视图对象)呢?这是由于考虑到实际业务中需要传递的对象所属的类往往分散在不同的模块(MessageVO从属于另外一个模块,需要被其他模块引用),所以通过构建一个包装类来包含真正需要被传递的对象(该对象必须也实现Parcelable接口),这也是该类命名为Envelope(信封)的含义。

MessageReceiver ->

跨进程的消息收取回调接口,用于将消息接入服务收取到的服务端消息传递到客户端。但这里使用的回调接口有点不一样,在AIDL中传递的接口,不能是普通的接口,只能是AIDL接口,因此我们还需要新建多一个.aidl文件:

MessageReceiver.aidl

package com.xxx.imsdk.comp.remote.listener;
import com.xxx.imsdk.comp.remote.bean.Envelope;

interface MessageReceiver {
void onMessageReceived(in Envelope envelope);
}

包目录结构如下图:

FE55B9D0FFFC48829667C01C212B2668.jpg

step4 返回IBinder接口

构建应用时,Android SDK会生成基于.aidl 文件的IBinder接口文件,并将其保存到项目的gen/目录中。生成文件的名称与.aidl 文件的名称保持一致,区别在于其使用.java 扩展名(例如,MessageCarrier.aidl 生成的文件名是 MessageCarrier .java)。此接口拥有一个名为Stub的内部抽象类,用于扩展 Binder 类并实现 AIDL 接口中的方法。

/** 根据MessageCarrier.aidl文件自动生成的Binder对象,需要返回给客户端 */
private val messageCarrier: IBinder = object : MessageCarrier.Stub() {

override fun sendMessage(envelope: Envelope?) {

}

override fun registerReceiveListener(messageReceiver: MessageReceiver?) {
remoteCallbackList.register(messageReceiver)
}

override fun unregisterReceiveListener(messageReceiver: MessageReceiver?) {
remoteCallbackList.unregister(messageReceiver)
}

}

override fun onBind(intent: Intent?): IBinder? {
return messageCarrier
}
step5 绑定服务

组件(例如 Activity)可以通过调用bindService方法绑定到服务,该方法必须提供ServiceConnection 的实现以监控与服务的连接。当组件与服务之间的连接建立成功后, ServiceConnection上的 onServiceConnected()方法将被回调,该方法包含上一步返回的IBinder对象,随后便可使用该对象与绑定的服务进行通信。

/**
* ## 绑定消息接入服务
* 同时调用bindService和startService, 可以使unbind后Service仍保持运行
* @param context 上下文
*/
@Synchronized
fun setupService(context: Context? = null) {
if (!::appContext.isInitialized) {
appContext = context!!.applicationContext
}

val intent = Intent(appContext, MessageAccessService::class.java)

// 记录绑定服务的结果,避免解绑服务时出错
if (!isBound) {
isBound = appContext.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
}

startService(intent)
}

/** 监听与服务连接状态的接口 */
private val serviceConnection = object : ServiceConnection {

override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
// 取得MessageCarrier.aidl对应的操作接口
messageCarrier = MessageCarrier.Stub.asInterface(service)
...
}

override fun onServiceDisconnected(name: ComponentName?) {
}

}

可以同时将多个组件绑定到同一个服务,但当最后一个组件取消与服务的绑定时,系统会销毁该服务。为了使服务能够无限期运行,可同时调用startService()和bindService(),创建同时具有已启动和已绑定两种状态的服务。这样,即使所有组件均解绑服务,系统也不会销毁该服务,直至调用 stopSelf() 或 stopService() 才会显式停止该服务。

/**
* 启动消息接入服务
* @param intent 意图
* @param action 操作
*/
private fun startService(
intent: Intent = Intent(appContext, MessageAccessService::class.java),
action: String? = null
) {
// Android8.0不再允许后台service直接通过startService方式去启动,将引发IllegalStateException
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
&& !ProcessUtil.isForeground(appContext)
) {
if (!TextUtils.isEmpty(action)) intent.action = action
intent.putExtra(KeepForegroundService.EXTRA_ABANDON_FOREGROUND, false)
appContext.startForegroundService(intent)
} else {
appContext.startService(intent)
}
}

/**
* 停止消息接入服务
*/
fun stopService() {
// 立即清除缓存的WebSocket服务器地址,防止登录时再次使用旧的WebSocket服务器地址(带的会话已失效),导致收到用户下线的通知
GlobalScope.launch {
DataStoreUtil.writeString(appContext, RemoteDataStoreKey.WEB_SOCKET_SERVER_URL, "")
}

unbindService()

appContext.stopService(Intent(appContext, MessageAccessService::class.java))
}

/**
* 解绑消息接入服务
*/
@Synchronized
fun unbindService() {
if (!isBound) return // 必须判断服务是否已解除绑定,否则会报java.lang.IllegalArgumentException: Service not registered

// 解除消息监听接口
if (messageCarrier?.asBinder()?.isBinderAlive == true) {
messageCarrier?.unregisterReceiveListener(messageReceiver)
messageCarrier = null
}

appContext.unbindService(serviceConnection)

isBound = false
}

总结

通过以上代码的实践,最终我们得以将应用拆分为主进程和远程进程。主进程主要负责用户交互、界面展示,而远程进程则主要负责消息收发、连接保持等。由于远程进程仅保持了最小限度的业务逻辑处理,内存增长相对稳定,因此会大大降低系统内存紧张时远端进程被终止的概率,即使主进程因为意外情况退出了,远程进程仍可保持运行,从而保证连接的稳定性。

收起阅读 »

Jetpack太香了,系统App也想用,怎么办?

第三方App使用Jetpack等开源框架非常流行,在Gradle文件简单指定即可。然而ROM内置的系统App在源码环境下进行开发,与第三方App脱节严重,采用开源框架的情况并不常见。但如果系统App也集成了Jetpack或第三方框架,开发效率则会大大提高。前言...
继续阅读 »

第三方App使用Jetpack等开源框架非常流行,在Gradle文件简单指定即可。然而ROM内置的系统App在源码环境下进行开发,与第三方App脱节严重,采用开源框架的情况并不常见。但如果系统App也集成了Jetpack或第三方框架,开发效率则会大大提高。

前言

系统App开发者,很少采用Jetpack 以及第三方框架的原因主要有几点:

  1. 导入麻烦:有的框架过于庞大,可能依赖的库比较多,编译文件的构建比较繁琐,没有gradle那么智能

  2. 功能单一:系统App注重功能性,业务逻辑较少,依赖庞大库文件的场景不多

  3. license风险:引用第三方框架的话,需要特别声明license ,会尽量避免采用

但对于功能复杂,架构庞大的系统App而言,集成第三方框架显得尤为必要。比如Android系统里最核心的App SystemUI,就采用了知名的DI框架Dagger2 。Dagger2的引入使得功能庞杂的SystemUI管理各个依赖模块变得游刃有余。

SystemUI将Dagger2集成的方式给了我启发,探索和总结了Android 源码中如何配置Jetpack 以及第三方库,希望能够帮到大家。

源码编译说明

与Gradle不同,源码环境里的编译构建都是配置在.mk或者.bp文件里的,配置起来较为繁琐。

.bp文件::Android.bp是用来替换Android.mk的配置文件,它使用Blueprint框架来解析。Blueprint是生成、解析Android.bp的工具,是Soong的一部分。Soong则是专为Android编译而设计的工具,Blueprint只是解析文件的形式,而Soong则解释内容的含义,最终转换成Ninja文件。下文bp 就是指.bp的文件

**注意:**以下基于Android 11上进行的演示,Android 10及之前部分Jetpack框架没有集成进源码,需留意

gradle切换到bp

gradle和bp的对比

看一个使用aar和注解库的例子。

看一个AndroidStudio(以下简称AS)下build.gradle 文件里包的导入代码:

dependencies {
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.2.1'
implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
//room
def room_version = "2.3.0"
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
}

ROM环境里的编译依赖.bp 配置如下:

android_app {
......
static_libs: [
"androidx.appcompat_appcompat",
"com.google.android.material_material",
"androidx-constraintlayout_constraintlayout",
"androidx.room_room-runtime",
],
plugins: ["androidx.room_room-compiler-plugin"],
......
}

导入关键字的差异

依赖文件里的导入关键字:

在AS和AOSP里面导入包的关键字有些差异,又分为两种情况。

build.gradle.bp
代码库implementationstatic_libs
注解使用的库annotationProcessorplugins

引入库文件(libs):比较常见。引入的方式有多种。下文会讲具体的几种方式。

引入注解库:比较流行,源码中使用比较繁琐,下文会重点讲解。

库文件的导入规则

眼尖的同学已经看出规律了

如:implementation 'androidx.appcompat:appcompat:1.2.0'

bp 文件中:androidx.appcompat_appcompat,将“:” 改为 “”即可,不需要加版本号。其实就是group 与 name 中间用“”连接,基本上符合上述规则,当然也有特殊

注解库的导入规则

如今框架流行注解编程。

gradle 配置:annotationProcessor "androidx.room:room-compiler:$room_version"

bp 中就需要使用到plugins,对应配置plugins: ["androidx.room_room-compiler-plugin"]

根据jar 包的规则,那plugin 命名应该是“:” 改为 ”_" version+"-plugin" 。

SystemUI 使用Dagger2配置 plugins: ["dagger2-compiler-2.19"],所以命名规则并不是上文猜测的那样。

那如何确定Jetpack框架的名称呢?

确定Jetpack框架的名称

源码编译,所有的内容和都在源码中,都需要在源码环境中寻找。

以Room 为例

在prebuilts/sdk/current/androidx/Android.bp 配置了引入jar包 中有如下配置

android_library {
name: "androidx.room_room-runtime",//名称
......
manifest: "manifests/androidx.room_room-runtime/AndroidManifest.xml",//配置manifast
static_libs: [//两个room库文件,三个依赖的库文件
"androidx.room_room-runtime-nodeps",
"androidx.room_room-common",
"androidx.sqlite_sqlite-framework",
"androidx.sqlite_sqlite",
"androidx.arch.core_core-runtime",
],
}

插件配置在prebuilts/sdk/current/androidx/JavaPlugins.bp

java_plugin {
name: "androidx.room_room-compiler-plugin",//名称
static_libs: [//1个room库文件,1个依赖的库文件
"androidx.room_room-compiler",
"kotlin-reflect"
],
processor_class: "androidx.room.RoomProcessor",//需要指定处理的类
}

注意:AS 开发 并不需要配置 “processor_class”,我反编译了room-compiler,找到了RoomProcessor.java.(AS 为什么不需要指定,我这里我就不研究了)

看下图,META-INF/services/javax.annotation.processing.Processor 文件中配置了RoomProcessor.java(就按照这个文件配置就可以了)

2rVokj.png

如何确定源码中哪些jetpack 库可以使用呢?

在Android.bp 中搜索,或者看androidx目录下包含了什么

prebuilts/sdk/current/androidx/m2repository/androidx$ ls

导入第三方的开源框架

以上讲的是引入Jetpack相关jar包,其他常见的是否包含呢?如Glide,它是不属于androidx 的

第三方库,Android 源码中整理就不算好了,使用比较乱。下面我梳理下

导入下载的jar包

大家最常用的,把 jar 包 放到 libs,就可以了(当然,比较简单,与其他库关联较少可以采用此种方式)

java_import {
name: "disklrucache-jar",
jars: ["disklrucache-4.12.0.jar"],
sdk_version: "current",
}
android_library_import {
name: "glide-4.12.0",
aars: ["glide-4.12.0.aar"],
sdk_version: "current",
}
android_library_import {
name: "gifdecoder-4.12.0",
aars: ["gifdecoder-4.12.0.aar",],
sdk_version: "current",
}
android_library_import {
name: "exifinterface-1.2.0",
aars: ["exifinterface-1.2.0.aar",],
sdk_version: "current",
}
android_app {
......
static_libs: [
"disklrucache-jar",
"glide-4.12.0",
"gifdecoder-4.12.0",
"exifinterface-1.2.0"
],
}

导入AOSP内置的jar包

常用第三方放在了prebuilts/tools/common/m2/repository/下面包含了很多库文件,如Glide,Okhttp,但比较尴尬的是,.bp文件并没有写好。应用需要自己编写,编写方式可以参考上文。

以后google应该会把 external 下 的整合到这个里面,可以关注下prebuilts/tools/common/m2/repository 中Android.bp文件的变化。

如:prebuilts/maven_repo/bumptech/Android.bp

java_import {
name: "glide-prebuilt",
jars: [
"com/github/bumptech/glide/glide/4.8.0/glide-4.8.0.jar",
"com/github/bumptech/glide/disklrucache/4.8.0/disklrucache-4.8.0.jar",
"com/github/bumptech/glide/gifdecoder/4.8.0/gifdecoder-4.8.0.jar",
],
jetifier: true,
notice: "LICENSE",
}

Android.bp 直接用"glide"了

static_libs: [
"glide-prebuilt"
],

导入jar包源码

external 下面 很多第三方库的源码,如Glide的源码,目录为external/glide/

android_library {
name: "glide",
srcs: [
"library/src/**/*.java",
"third_party/disklrucache/src/**/*.java",
"third_party/gif_decoder/src/**/*.java",
"third_party/gif_encoder/src/**/*.java",
],
manifest: "library/src/main/AndroidManifest.xml",
libs: [
"android-support-core-ui",
"android-support-compat",
"volley",
],
static_libs: [
"android-support-fragment",
],
sdk_version: "current",
}

App 的Android.bp 直接用"glide"了

static_libs: [
"glide"
],

以上三种方式都是引入 Android 中源码存在的。不存在怎么办,Android源码 不像 AS,连上网,配置下版本号就可以下载。

内置新的Jetpack框架

引入第三方库文件方式,方式一:aar包导入。就可以。但这里不讨论,找些复杂的,包含annotationProcessor(bp 中的plugin) 。Hilt 是 Google 相对较新的框架。

Hilt基于Dagger2开发,又针对Android进行了专属的DI优化。

所以在导入Dagger2和它的依赖文件之外还需要导入Hilt专属的一堆库和依赖文件。

1. 获取框架的库文件

一般来说AS里导入完毕的目录下即可获取到对应的库文件,路径一般在 :C:\Users\xxx\.gradle\caches\modules-2\files-2.1\com.google.dagger\hilt-android

2. 确定额外的依赖文件

为什么需要额外的依赖文件?

完全依赖AS开发可能不知道,导入的包的同时可能引入其他的包。

如Hilt的是在dagger2基础上开发,当然会引入Dagger2,

使用注解,需要javax.annotation包。

Dagger2,javax.annotation 在Gradle 自动下载好的,非项目中明确配置的,我们称之为依赖包。

使用Gradle 自动下载,都会有pom 文件。“dependency”,表示需要依赖的jar 包,还包含了版本号等

如:hilt-android-2.28-alpha.pom

``
`com.google.dagger`
`dagger` //依赖的dagger2
`2.28`//dagger2的版本
`
`
``
`com.google.dagger`
`dagger-lint-aar`
`2.28`
`
`
``
`com.google.code.findbugs`
`jsr305`
`3.0.1`
`
`
......

3. 导入需要的依赖文件

比如SystemUI,已经导入了一些文件,只要导入剩余的文件即可。

一般常用的 源码中都是存在的,决定copy 之前,可以看下先源码中是否存在,存在可以考虑使用。

当然也有例外,如Hilt 我依赖的是源码中dagger2是2.19 版本,编译中报错,没有找到dagger2 中的class,反编译jar确实不存在,使用2.28 的dagger 版本,问题就解决了。所以说可能存在库文件版本较老的情况。

以下就是新增的文件夹,其中manifests 后文中有讲。

    manifests/ 
repository/com/google/dagger/dagger-compiler/2.28/
repository/com/google/dagger/dagger-producers/2.28/
repository/com/google/dagger/dagger-spi/2.28/
repository/com/google/dagger/dagger/2.28/
repository/com/google/dagger/hilt-android-compiler/
repository/com/google/dagger/hilt-android/

4. 编写最终的bp文件

这一步就是把依赖的包,关联起来,根据上文的 pom 文件。

  • 配置dagger2 2.28 的jar
java_import {

name: "dagger2-2.28",

jars: ["repository/com/google/dagger/dagger/2.28/dagger-2.28.jar"],

host_supported: true,

}
  • 配置 dagger2-compiler 2.28 的jar (annotationProcessor 依赖的jar包)
java_import_host {

name: "dagger2-compiler-2.28-import",

jars: [

"repository/com/google/dagger/dagger-compiler/2.28/dagger-compiler-2.28.jar",

"repository/com/google/dagger/dagger-producers/2.28/dagger-producers-2.28.jar",

"repository/com/google/dagger/dagger-spi/2.28/dagger-spi-2.28.jar",

"repository/com/google/dagger/dagger/2.28/dagger-2.28.jar",

"repository/com/google/guava/guava/25.1-jre/guava-25.1-jre.jar",

"repository/com/squareup/javapoet/1.11.1/javapoet-1.11.1.jar",

"repository/com/google/dagger/dagger-google-java-format/1.6/google-java-format-1.6-all-deps.jar",

],
}
  • 配置dagger2 的 plugin (annotationProcessor)
java_plugin {
name: "dagger2-compiler-2.28",
static_libs: [
"dagger2-compiler-2.28-import",
"jsr330",
],
processor_class: "dagger.internal.codegen.ComponentProcessor",
generates_api: true,
}
  • 配置 hilt 依赖的aar包
android_library_import {
name: "hilt-2.82-nodeps",
aars: ["repository/com/google/dagger/hilt-android/2.28-alpha/hilt-android-2.28-alpha.aar"],
sdk_version: "current",
apex_available: [
"//apex_available:platform",
"//apex_available:anyapex",
],
min_sdk_version: "14",
static_libs: [
"dagger2-2.28",
"jsr305",
"androidx.activity_activity",
"androidx.annotation_annotation",
"androidx.fragment_fragment",
],

}
  • 配置hilt 的包

    android_library 表示 aar 包,所以必须要配置manifests ,在上文中多出的manifasts文件夹中 放的就是这个文件,AndroidManifest.xml来自hilt-android-2.28-alpha.aar 中

android_library {
name: "hilt-2.82",
manifest: "manifests/dagger.hilt.android/AndroidManifest.xml",
static_libs: [
"hilt-2.82-nodeps",
"dagger2-2.28"
],
......
}
  • 配置 hilt-compiler 2.82 jar包
java_import_host {
name: "hilt-compiler-2.82-import",
jars: [
"repository/com/google/dagger/dagger-compiler/2.28/dagger-compiler-2.28.jar",
"repository/com/google/dagger/dagger-producers/2.28/dagger-producers-2.28.jar",
"repository/com/google/dagger/dagger-spi/2.28/dagger-spi-2.28.jar",
"repository/com/google/dagger/dagger/2.28/dagger-2.28.jar",
"repository/com/google/guava/guava/25.1-jre/guava-25.1-jre.jar",
"repository/com/squareup/javapoet/1.11.1/javapoet-1.11.1.jar",
"repository/com/google/dagger/dagger-google-java-format/1.6/google-java-format-1.6-all-deps.jar",
"repository/com/google/dagger/hilt-android-compiler/2.28-alpha/hilt-android-compiler-2.28-alpha.jar",
"repository/javax/inject/javax.inject/1/javax.inject-1.jar"
],
}
  • 配置hilt的 plugin (annotationProcessor)

    反编译查看需要配置的Processer

好吧,看到上图我傻眼了,11个。下文代码我只贴了一个,需要写11个,其他省略。

java_plugin {
name: "hilt-compiler-2.82",
static_libs: [
"hilt-compiler-2.82-import",
"jsr330",
],
processor_class: "dagger.hilt.processor.internal.root.RootProcessor",
generates_api: true,
}
  • 项目中引用
    `static_libs: [`
`"androidx-constraintlayout_constraintlayout",`
`"androidx.appcompat_appcompat",`
`"com.google.android.material_material",`
`"androidx.room_room-runtime",`
`"androidx.lifecycle_lifecycle-viewmodel",`
`"androidx.lifecycle_lifecycle-livedata",`
`"hilt-2.82",`
`"jsr330"`
`],`

`plugins: ["androidx.room_room-compiler-plugin",`
`"hilt-compiler-2.82",`
`"hilt-compiler-2.82-UninstallModulesProcessor",`
`"hilt-compiler-2.82-TestRootProcessor",`
`"hilt-compiler-2.82-DefineComponentProcessor",`
`"hilt-compiler-2.82-BindValueProcessor",`
`"hilt-compiler-2.82-CustomTestApplicationProcessor",`
`"hilt-compiler-2.82-AndroidEntryPointProcessor",`
`"hilt-compiler-2.82-AggregatedDepsProcessor",`
`"hilt-compiler-2.82-OriginatingElementProcessor",`
`"hilt-compiler-2.82-AliasOfProcessor",`
`"hilt-compiler-2.82-GeneratesRootInputProcessor",`
`],`
  • 编译确认

    编译失败了!看到报错,我的心也凉了。需要配置Gradle 插件。bp 可以配置Gradle插件?

    看了下com/google/dagger/hilt-android-gradle-plugin/,但是并不清楚bp 怎么配置,在源码里,只知道一处:prebuilts/gradle-plugin/Android.bp,但并没有尝试成功。有兴趣的同学,可以研究下。

    而且hilt-android-gradle-plugin 的jar包,依赖包 至少十几个。

public class MainActivity extends AppCompatActivity { ^ Expected @AndroidEntryPoint to have a value. Did you forget to apply the Gradle Plugin? [Hilt] Processing did not complete. See error above for details.

public class MainFragment extends BaseFragment { ^ Expected @AndroidEntryPoint to have a value. Did you forget to apply the Gradle Plugin? [Hilt] Processing did not complete. See error above for details.

public class AppApplication extends Application { ^ Expected @HiltAndroidApp to have a value. Did you forget to apply the Gradle Plugin? [Hilt] Processing did not complete. See error above for details.

虽然Hilt引入失败,但是整个过程我觉得有必要分享一下,给大家一些导入新框架的参考。

源码环境里集成开源框架的流程

2xipH1.png

常用开源框架的对照表

 
build.gradleAndroid.bpAOSP源码位置
androidx.appcompat:appcompatandroidx.appcompat_appcompat/sdk/current/androidx/Android.bp
androidx.core:coreandroidx.core_coreprebuilts/sdk/current/androidx/Android.bp
com.google.android.material:materialcom.google.android.material_materialprebuilts/sdk/current/extras/material-design-x/Android.bp
androidx.constraintlayout:constraintlayoutandroidx-constraintlayout_constraintlayoutprebuilts/sdk/current/extras/constraint-layout-x/Android.bp
androidx.lifecycle:lifecycle-livedataandroidx.lifecycle_lifecycle-livedataprebuilts/sdk/current/androidx/Android.bp
androidx.lifecycle:lifecycle-viewmodelandroidx.lifecycle_lifecycle-viewmodelprebuilts/sdk/current/androidx/Android.bp
androidx.recyclerview:recyclerviewandroidx.recyclerview_recyclerviewprebuilts/sdk/current/androidx/Android.bp
androidx.annotation:annotationandroidx.annotation_annotationprebuilts/sdk/current/androidx/Android.bp
androidx.viewpager2:viewpager2androidx.viewpager2_viewpager2prebuilts/sdk/current/androidx/Android.bp
androidx.room:room-runtimeandroidx.room_room-runtimeprebuilts/sdk/current/androidx/Android.bp
glideglide-prebuiltprebuilts/maven_repo/bumptech/Android.bp
gsongson-prebuilt-jarprebuilts/tools/common/m2/Android.bp
Robolectric相关Robolectric相关prebuilts/tools/common/m2/robolectric.bp
 

经验总结

1、build.gradle 需要配置 额外插件的,如hilt、databinding viewbinding 不建议使用源码编译。

2、建议使用 AOSP 源码 中 bp 已经配置好的。这样就可以直接使用了。

3、jetpack 包引入或者androidx 引入,建议先prebuilts/sdk/current/androidx 下寻找配置好的bp 文件

4、非androidx ,建议先在prebuilts/tools/common/m2下寻找寻找配置好的bp 文件

5、文章中的例子都是prebuilts目录下配置,项目中使用,也可以配置在项目中,都是可以的。

收起阅读 »

探究Android View绘制流程

1.简介在开发中,我们经常会遇到各种各样的View,这些View有的是系统提供的,有的是我们自定义的View,可见View在开发中的重要性,那么了解Android View的绘制流程对于我们更好地理解View的工作原理和自定义View相当有益,本文将依据And...
继续阅读 »

1.简介

在开发中,我们经常会遇到各种各样的View,这些View有的是系统提供的,有的是我们自定义的View,可见View在开发中的重要性,那么了解Android View的绘制流程对于我们更好地理解View的工作原理和自定义View相当有益,本文将依据Android源码(API=30)探究View的绘制流程,加深大家对其的理解和认知。

2.View绘制流程概览

应用的一个页面是由各种各样的View组合而成的,它们能够按照我们的期望呈现在屏幕上,实现我们的需求,其背后是有一套复杂的绘制流程的,主要涉及到以下三个过程:

  1. measure:顾名思义,是测量的意思,在这个阶段,做的主要工作是测量出View的尺寸大小并保存。

  2. layout:这是布局阶段,在这个阶段主要是根据上个测量阶段得到的View尺寸大小以及View本身的参数设置来确定View应该摆放的位置。

  3. draw:这是阶段相当重要,主要执行绘制的任务,它根据测量和布局的结果,完成View的绘制,这样我们就能看到丰富多彩的界面了。

    这些阶段执行的操作都比较复杂,幸运的是系统帮我们处理了很多这样的工作,并且当我们需要实现自定义View的时候,系统又给我们提供了onMeasure()、onLayout()、onDraw()方法,一般来说,我们重写这些方法,在其中加入我们自己的业务逻辑,就可以实现我们自定义View的需求了。

3.View绘制的入口

讲到View绘制的流程,就要提到ViewRootImpl类中的performTraversals()方法,这个方法中涉及到performMeasure()、performLayout()、performDraw()三个方法,其中performMeasure()方法是从ViewTree的根节点开始遍历执行测量View的工作,performLayout()方法是从ViewTree的根节点开始遍历执行View的布局工作,而performDraw()方法是从ViewTree的根节点开始遍历执行绘制View的工作,ViewTree的根节点是DecorView。performTraversals()方法内容很长,以下只是部分代码。

//ViewRootImpl
private void performTraversals() {
final View host = mView;
...
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
...
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
performLayout(lp, mWidth, mHeight);
...
performDraw();
}

4.measure阶段

measure是绘制流程的第一个阶段,在这个阶段主要是通过测量来确定View的尺寸大小。

4.1 MeasureSpec介绍

  1. MeasureSpec封装了从父View传递到子View的布局要求,MeasureSpec由大小和模式组成,它可能有三种模式。
  2. UNSPECIFIED模式:父View没有对子View施加任何约束,子View可以是它想要的任何大小。
  3. EXACTLY模式:父View已经为子View确定了精确的尺寸,不管子View想要多大尺寸,它都要在父View给定的界限内。
  4. AT_MOST模式:在父View指定的大小范围内,子View可以是它想要的大小。

4.2 View测量的相关方法

  1. ViewRootImpl.performMeasure()方法

    private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    if (mView == null) {
    return;
    }
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
    try {
    mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    } finally {
    Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
    }

    在performMeasure()中,从根布局DecorView开始遍历执行measure()操作。

  2. View.measure()方法

    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    boolean optical = isLayoutModeOptical(this);
    if (optical != isLayoutModeOptical(mParent)) {
    Insets insets = getOpticalInsets();
    int oWidth = insets.left + insets.right;
    int oHeight = insets.top + insets.bottom;
    widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth);
    heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
    }

    ...

    if (forceLayout || needsLayout) {
    // first clears the measured dimension flag
    mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

    resolveRtlPropertiesIfNeeded();

    int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
    if (cacheIndex < 0 || sIgnoreMeasureCache) {
    // measure ourselves, this should set the measured dimension flag back
    onMeasure(widthMeasureSpec, heightMeasureSpec);
    mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
    } else {
    long value = mMeasureCache.valueAt(cacheIndex);
    // Casting a long to int drops the high 32 bits, no mask needed
    setMeasuredDimensionRaw((int) (value >> 32), (int) value);
    mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
    }
    ...
    }

    ...
    }

    调用这个方法是为了找出视图应该有多大,父View在宽度和高度参数中提供约束信息,其中widthMeasureSpec参数是父View强加的水平空间要求,heightMeasureSpec参数是父View强加的垂直空间要求,这是一个final方法,实际的测量工作是通过调用onMeasure()方法执行的,因此只有onMeasure()方法可以被子类重写。

  3. View.onMeasure()方法

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
    getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

    这个方法的作用是测量视图及其内容,以确定测量的宽度和高度,这个方法被measure()方法调用,并且应该被子类重写去对它们的内容进行准确和有效的测量,当重写此方法时,必须调用setMeasuredDimension()方法去存储这个View被测量出的宽度和高度。

  4. View.setMeasuredDimension()方法

    protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
    boolean optical = isLayoutModeOptical(this);
    if (optical != isLayoutModeOptical(mParent)) {
    Insets insets = getOpticalInsets();
    int opticalWidth = insets.left + insets.right;
    int opticalHeight = insets.top + insets.bottom;

    measuredWidth += optical ? opticalWidth : -opticalWidth;
    measuredHeight += optical ? opticalHeight : -opticalHeight;
    }
    setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }

    setMeasuredDimension()方法必须被onMeasure()方法调用去存储被测量出的宽度和高度,在测量的时候如果setMeasuredDimension()方法执行失败将会抛出异常。

    private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
    mMeasuredWidth = measuredWidth;
    mMeasuredHeight = measuredHeight;

    mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }

    setMeasuredDimensionRaw()方法被setMeasuredDimension()方法调用来设置出被测量出的宽度和高度给View的变量mMeasuredWidth和mMeasuredHeight。

    public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    switch (specMode) {
    case MeasureSpec.UNSPECIFIED:
    result = size;
    break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
    result = specSize;
    break;
    }
    return result;
    }

    参数size是这个View的默认大小,参数measureSpec是父View对子View施加的约束,通过计算的得出这个View 应该的大小,如果MeasureSpec没有施加约束则使用提供的大小,如果是MeasureSpec.AT_MOST或MeasureSpec.EXACTLY模式则会使用specSize。

    protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

    getSuggestedMinimumWidth()方法返回View应该使用的最小宽度,这个返回值是View的最小宽度和背景的最小宽度二者之中较大的那一个值。当在onMeasure()方法内被使用的时候,调用者依然应该确保返回的宽度符合父View的要求。

4.3 ViewGroup测量的相关方法

ViewGroup继承View,是一个可以包含其他子View的一个特殊的View,在执行测量工作的时候,它有几个比较重要的方法,measureChildren()、measureChild()和getChildMeasureSpec()。

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}

measureChildren()方法要求这个View的子View们去测量它们自己,处于GONE状态的子View不会执行measureChild()方法。

protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();

final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);

child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

measureChild()方法要求子View去测量它自身,测量的同时需要考虑到父布局的MeasureSpec要求和它自身的padding。

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);

int size = Math.max(0, specSize - padding);

int resultSize = 0;
int resultMode = 0;

switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;

// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;

// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

这个方法做了测量子View过程中复杂的工作,计算出MeasureSpec传递给特定的子节点,目标是根据来自MeasureSpec的信息以及子View的LayoutParams信息去得到一个最可能的结果。

4.4 DecorView的测量

DecorView继承了FrameLayout,FrameLayout又继承了ViewGroup,它重写了onMeasure()方法,并且调用了父类的onMeasure()方法,在遍历循环去测量它的子View,之后又调用了setMeasuredDimension()。

//DecorView
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final DisplayMetrics metrics = getContext().getResources().getDisplayMetrics();
final boolean isPortrait = getResources().getConfiguration().orientation == ORIENTATION_PORTRAIT;

final int widthMode = getMode(widthMeasureSpec);
final int heightMode = getMode(heightMeasureSpec);
...
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
...
}
//FrameLayout
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
...
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
maxWidth = Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
maxHeight = Math.max(maxHeight,
child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
childState = combineMeasuredStates(childState, child.getMeasuredState());
if (measureMatchParentChildren) {
if (lp.width == LayoutParams.MATCH_PARENT ||
lp.height == LayoutParams.MATCH_PARENT) {
mMatchParentChildren.add(child);
}
}
}
}
...
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
...
}

5.layout阶段

当measure阶段完成后,就会进入到layout布局阶段,根据View测量的结果和其他参数来确定View应该摆放的位置。

5.1 performLayout()方法

测量完成后,在performTraverserals()方法中,会执行performLayout()方法,开始布局过程。

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
mScrollMayChange = true;
mInLayout = true;
final View host = mView;
if (host == null) {
return;
}
...
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
...
}

5.2 layout()方法

//ViewGroup
@Override
public final void layout(int l, int t, int r, int b) {
if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
if (mTransition != null) {
mTransition.layoutChange(this);
}
super.layout(l, t, r, b);
} else {
// record the fact that we noop'd it; request layout when transition finishes
mLayoutCalledWhileSuppressed = true;
}
}

这个是ViewGroup的layout()方法,它是一个final类型的方法,在其内部又调用了父类View的layout()方法。

//View
@SuppressWarnings({"unchecked"})
public void layout(int l, int t, int r, int b) {
...
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);

if (shouldDrawRoundScrollbar()) {
if(mRoundScrollbarRenderer == null) {
mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
}
} else {
mRoundScrollbarRenderer = null;
}
...
}
...
}

View的layout()方法作用是为它本身及其后代View分配大小和位置,派生类不应重写此方法,带有子View的派生类应该重写onLayout()方法,参数l、t、r、b指的是相对于父View的位置。

5.3 setFrame()方法

//View
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
...
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
changed = true;

// Remember our drawn bit
int drawn = mPrivateFlags & PFLAG_DRAWN;

int oldWidth = mRight - mLeft;
int oldHeight = mBottom - mTop;
int newWidth = right - left;
int newHeight = bottom - top;
boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);

// Invalidate our old position
invalidate(sizeChanged);

mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);

mPrivateFlags |= PFLAG_HAS_BOUNDS;


if (sizeChanged) {
sizeChange(newWidth, newHeight, oldWidth, oldHeight);
}
...
}
return changed;
}

在View的layout()方法内会调用setFrame()方法,其作用是给这个视图分配一个大小和位置,如果新的大小和位置与原来的不同,那么返回值为true。

5.4 onLayout()方法

//View
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {

}

View的onLayout()方法是一个空方法,内部没有代码实现,带有子节点的派生类应该重写此方法,并在其每个子节点上调用layout。

//ViewGroup
@Override
protected abstract void onLayout(boolean changed,
int l, int t, int r, int b);

ViewGroup的onLayout()方法是一个抽象方法,因此直接继承ViewGroup的类需要重写此方法。

//DecorView
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
...
}

5.5 DecorView的布局

//DecorView
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (mApplyFloatingVerticalInsets) {
offsetTopAndBottom(mFloatingInsets.top);
}
if (mApplyFloatingHorizontalInsets) {
offsetLeftAndRight(mFloatingInsets.left);
}

// If the application changed its SystemUI metrics, we might also have to adapt
// our shadow elevation.
updateElevation();
mAllowUpdateElevation = true;

if (changed
&& (mResizeMode == RESIZE_MODE_DOCKED_DIVIDER
|| mDrawLegacyNavigationBarBackground)) {
getViewRootImpl().requestInvalidateRootRenderNode();
}
}

DecorView重写了onLayout()方法,并且调用了其父类FrameLayout的onLayout()方法。

//FrameLayout
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}

void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
final int count = getChildCount();

final int parentLeft = getPaddingLeftWithForeground();
final int parentRight = right - left - getPaddingRightWithForeground();

final int parentTop = getPaddingTopWithForeground();
final int parentBottom = bottom - top - getPaddingBottomWithForeground();

for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();

final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();
...
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}

在FrameLayout的onLayout()方法中,调用了layoutChildren()方法,在此方法内开启循环,让子View调用layout()去完成布局。

收起阅读 »

字节跳动杨震原:没有“天才架构师”,技术团队需要市场化管理

在近日召开的火山引擎品牌发布会上,字节跳动副总裁杨震原在会上表示,火山引擎是把字节跳动内部的技术和工具开放给企业客户,由字节跳动的技术团队为外部客户提供服务。杨震原认为,把技术开放出去有助于提升科技公司的创新力。“当公司规模变得很大,有时候也会导致效率降低。因...
继续阅读 »

在近日召开的火山引擎品牌发布会上,字节跳动副总裁杨震原在会上表示,火山引擎是把字节跳动内部的技术和工具开放给企业客户,由字节跳动的技术团队为外部客户提供服务。杨震原认为,把技术开放出去有助于提升科技公司的创新力。

“当公司规模变得很大,有时候也会导致效率降低。因为没有人能够很好地规划一切,很难有一个‘天才架构师’去了解公司的方方面面,权衡各方面的优先级”,杨震原认为,公司在成长的过程中会达到一个临界值。超过这个值之后,只有引入一些市场机制,公司效率才会变得更高。

以亚马逊为例,亚马逊最强大的是它的AWS云计算能力,AWS就是把亚马逊核心技术开放给外部企业用,通过外部客户把AWS打磨得更好,既更好地服务内部客户,又给外部企业独立做大的机会。这个过程类似于种树,所以亚马逊的生态中长出了巨头企业,包括Uber、Airbnb等。

杨震原认为亚马逊是值得学习的楷模。公司本质是降低交易成本,提高效率,当公司规模变大后,技术团队需要市场化的管理。

“为什么会有公司,它的本质是什么?比如说,你需要一个程序员写代码,如果没有公司,你就要跟他结算,他写200行给他多少钱”,杨震原说,“但是有公司这个形态,你们形成一个小组,就可以对齐目标、使命、文化。他可以按月发工资,也可以考虑他的成长,交易成本变得很低。这就是为什么在市场上交易的是很多很多的公司,而非一个一个的个体”。

但是当公司规模逐步变大时,管理者很难规划所有团队的发展,必须让技术团队面对更大的市场、在更多的场景去服务外部的客户,才能打磨好团队,这也是字节跳动打造火山引擎为外部企业客户提供服务的重要动力。

据杨震原透露,火山引擎对外服务和对内服务在团队方面基本上是融合在一起的,或者说管理线的分叉很靠下,不是在比较高的管理层级就分对外或对内。以数据中台为例,细分到数据中台的一个子方向,才会有对外和对内的人员分工,相当于字节跳动是把技术中台直接市场化支持火山引擎。

收起阅读 »

手把手教你在Flutter项目优雅的使用ORM数据库

Flutter ORM数据库介绍Flutter现在开发上最大的槽点可能就是数据库使用了,Flutter现在只提供了sqflite插件,这表明开发者手动写sql代码,建表、建索引、transation、db线程控制等等繁琐的事情必然接踵而至,这种数据库使用方式是...
继续阅读 »

Flutter ORM数据库介绍

Flutter现在开发上最大的槽点可能就是数据库使用了,Flutter现在只提供了sqflite插件,这表明开发者手动写sql代码,建表、建索引、transation、db线程控制等等繁琐的事情必然接踵而至,这种数据库使用方式是最低效的了。例如IOS平台有coredata、realm等等的框架提供便捷的数据库操作,但来到flutter就又倒退回去裸写sql,这对大部分团队都是重大的成本。

本文将详细介绍一种在Flutter项目中优雅的使用ORM数据库的方法,我们使用的ORM框架是包含在一个Flutter插件flutter_luakit_plugin(如何使用可参考介绍文章)中的其中一个功能,本文只详细介绍这套ORM框架的使用和实现原理。我们给出了一个demo。

我们demo中实现了一个简单的功能,从一个天气网站上查询北京的天气信息,解析返回的json然后存数据库,下次启动优先从数据库查数据马上显示,再发请求向天气网站更新天气信息,就这么简单的一个功能。虽然功能简单,但是我们99%日常的业务逻辑也就是由这些简单的逻辑组成的了。下面是demo运行的效果图。


看完运行效果,我们开始看看ORM数据库的使用。ORM数据库的核心代码都是lua,其中WeatherManager.lua是业务逻辑代码,其他的lua文件是ORM数据库的核心代码,全部是lua实现的,所有代码文件加起来也就120k左右,非常轻量。

针对上面提到的天气信息的功能,我们来设计数据模型,从demo的展示我们看到每天天气信息包含几个信息,城市名、日出日落时间、最高温度、最低温度、风向、风力,然后为了区分是哪一天的数据,我们给每条信息加上个id的属性,作为主键。想好我们就开始定义第一个ORM数据模型,有几个必要的信息,db名,表名,后面的就是我们需要的各个字段了,我们提供IntegerField、RealField、BlobField、TextField、BooleandField。等常用的数据类型。weather 就是这个模型的名字,之后我们weather为索引使用这个数据模型。定义模型代码如下。

weather = {
__dbname__ = "test.db",
__tablename__ = "weather",
id = {"IntegerField",{unique = true, null = false, primary_key = true}},
wind = {"TextField",{}},
wind_direction = {"TextField",{}},
sun_info = {"TextField",{}},
low = {"IntegerField",{}},
high = {"IntegerField",{}},
city = {"TextField",{}},
},

定义好模型后,我们看看如何使用,我们跟着业务逻辑走,首先网络请求回来我们要生成模型对象存到数据库,分下面几步

获取模型对象

local Table = require('orm.class.table')
local _weatherTable = Table("weather”)

准备数据,建立数据对象

local t = {}
t.wind = flDict[v.fg]
t.wind_direction = fxDict[v.ff]
t.sun_info = v.fi
t.low = tonumber(v.fd)
t.high = tonumber(v.fc)
t.id = i
t.city = city
local weather = _weatherTable(t)

保存数据

weather:save()

读取数据

_weatherTable.get:all():getPureData()

是不是很简单,很优雅,什么建表、拼sql、transation、线程安全等等都不用考虑,傻瓜式地使用,一个业务就几行代码搞定。这里只演示了简单的存取,更多的select、update、联表等高级用法可参考db_test demo。

Flutter ORM数据库原理详解

好了,上面已经介绍完如何使用了,如果大家仅仅关心使用下面的可以不看了,如果大家想了解这套跨平台的ORM框架的实现原理,下面就会详细介绍,其实了解了实现原理,对大家具体业务使用还是很有好处的,虽然我感觉大家用的时候极少了解原理。

我们把orm框架分为三层接入层,cache层,db操作层,三个层分别处于对应的线程,具体可以参考下图。接入层可以在任意线程发起,接入层也是每次数据库操作的发起点,上面的demo所有操作都是在接入层,cache层,db操作层仅仅是ORM内部划分,对使用者来讲不需要关心cache层和db操作层。我们把所有的操作分成两种,db后续相关的,和db后续无关的。


db后续无关的操作是从接入层不同的线程进入到cache层的队列,所有操作在这个队列里先同步完成内存操作,然后即可马上返回接入层,异步再到db操作层进行db操作。db后续无关的操作包括 save、update、delete。

db后续相关的操作依赖db操作层操作的结果,这样的话就必须等真实的db操作完成了再返回接入层。db后续相关的操作包括select。

要做到这种数据同步,我们必须先把orm操作接口抽象化,只给几个常用的接口,所有操作都必须通过指定的接口来完成。我们总结了如下基本操作接口。

1、save

2、select where

3、select PrimaryKey

4、update where

5、update PrimaryKey

6、delete where

7、delete PrimaryKey

这七种操作只要在操作前返回前对内存中的cache做相应的处理,即可保证内存cache始终和db保持一致,这样以后我们就可以优先使用cache层的数据了。这七种操作的实现逻辑,这里先说明一下,cache里面的对象都是以主键为key,orm对象为value的形式存储在内存中的,这些控制逻辑是写在cache.lua里面的。

下面详细介绍七种基本操作的逻辑。

save操作,同步修改内存cache,然后马上返回接入层,再异步进行db replace into 的操作


where条件select,这个必须先同步到db线程获取查询结果,再同步修改内存里面的cache值,再返回给接入层


select PrimaryKey,就是选一定PrimaryKey值的orm对象,这个操作首先看cache里面是否有primarykey 值的orm对,如果有,直接返回,如果没有,先同步到db线程获取查询结果,再同步修改内存里面的cache值,再返回给接入层


update where,先同步到db线程通过where 条件select出需要update的主键值,根据主键值和需要update的内容,同步更新内存cache,然后异步进行db的update操作


update PrimaryKey,根据PrimaryKey进行update操作,先同步更新内存cache,然后异步进行db的update操作


delete where,先同步到db线程通过where 条件select出需要delete的主键值,根据主键值删除内存cache,然后异步进行db的delete操作


delete PrimaryKey,根据PrimaryKey进行delete操作,先同步删除内存cache,然后异步进行db的delete操作


只要保证上面七种基本操作逻辑,即可保证cache中的内容和db最终的内容是一致的,这种尽量使用cache的特性可以提升数据库操作的效率,而且保证同一个db的所有操作都在指定的cache线程和db线程里面完成,也可以保证线程安全。

最后,由于我们所有的db操作都集中起来了,我们可以定时的transation 保存,这样可以大幅提升数据库操作的性能。

结语

目前Flutter领域最大的痛点就是数据库操作,本文提供了一种优雅使用ORM数据库的方法,大幅降低了使用数据库的门槛。希望这篇文章和flutter_luakit_plugin可以帮到大家更方便的开发Flutter
应用。

链接:https://www.jianshu.com/p/62500ae08a07

收起阅读 »

纯 CSS 创建五彩斑斓的智慧阴影!让前景图片自动转化为对应彩色的背景阴影

几天前,我在 Home Depot(aka Toys "R" Us for big kids)处发现,他们有一个巨大的显示器来展示所有这些彩色的供销售的电灯泡!其中一项是y一组在电视后面的智能灯泡。它们会在电视的后面投影近似于电视在播出的内容的彩色阴影,与以下...
继续阅读 »

几天前,我在 Home Depot(aka Toys "R" Us for big kids)处发现,他们有一个巨大的显示器来展示所有这些彩色的供销售的电灯泡!其中一项是y一组在电视后面的智能灯泡。它们会在电视的后面投影近似于电视在播出的内容的彩色阴影,与以下内容 类似



注意电视后面发生的事情。屏幕前景中显示的颜色会被灯泡投影为电视机身后面的彩色阴影。随着屏幕上的颜色发生变化,投射在背景中的颜色也会发生变化。真的很酷,对吧?


自然,看到这个之后,我的第一个想法是,我们是否可以使用网络技术创建一个足够智能以模仿前景色的彩色阴影。事实证明,我们完全可以只使用 CSS 构建出这个案例。在本文中,我们将了解如何创建这种效果。


走起!


让它变成真的!


正如您将在以下部分中看到的,使用 CSS 创建这种彩色阴影似乎是一项艰巨的任务(当然,只是就刚开始而言)。当我们开始进入它并将这个任务的核心分解成更小的部分时,我们其实能够发现这真的很容易实现。在接下来的几节中,我们将创建以下示例:



你应该看到的是一张寿司的图片,后面出现了一个五颜六色的阴影。(只是为了强调我们正在做这一切,阴影被添加了脉冲的效果)抛开示例,让我们深入了解实现,看看 HTML 和 CSS 如何让这一切变为现实!


展示我们的照片


展示我们的寿司的图片对应的 HTML 起始没什么特别的:



<div class="parent">
<div class="colorfulShadow sushi"></div>
</div>


我们有一个父 div 元素,包含一个负责显示寿司的子 div 元素。我们显示寿司的方式是将其指定为背景图像,并由以下 .sushi 样式规则处理:


.sushi {
margin: 100px;
width: 150px;
height: 150px;
background-image: url("https://www.kirupa.com/icon/1f363.svg");
background-repeat: no-repeat;
background-size: contain;
}


在此样式规则中,我们将 div 的大小指定为 150 x 150 像素,并在其上设置 background-image 和相关的其他属性。就目前而言,我们所看到的 HTML 和 CSS 会给我们提供如下所示的内容:



现在是阴影时间


现在我们的图像出现了,剩下的就是我们定义阴影这一有趣的部分。我们要定义阴影的方法是指定一个子伪元素(使用 ::after),它将做三件事:



  1. 直接定位在我们的形象后面;

  2. 继承与父元素相同的背景图片;

  3. 依靠滤镜应用多彩的阴影效果;


这三件事是通过以下两条样式规则完成的:


.colorfulShadow {
position: relative;
}

.colorfulShadow::after {
content: "";
width: 100%;
height: 100%;
position: absolute;
background: inherit;
background-position: center center;
filter: drop-shadow(0px 0px 10px rgba(0, 0, 0, 0.50)) blur(20px);
z-index: -1;
}


让我们花一点时间来看看这里发生了些什么:先注意每一个属性和对应的值,有一些值得注意的标记是 backgroundfilterbackground 属性使用了 inherit 继承父元素,意味着能够继承父元素的背景:


.colorfulShadow::after {
content: "";
width: 100%;
height: 100%;
position: absolute;
background: inherit;
background-position: center center;
filter: drop-shadow(0px 0px 10px rgba(0, 0, 0, 0.50)) blur(20px);
z-index: -1;
}


我们为 filter 属性定义了两个过滤的属性,分别是 drop-shadowblur


.colorfulShadow::after {
content: "";
width: 100%;
height: 100%;
position: absolute;
background: inherit;
background-position: center center;
filter: drop-shadow(0px 0px 10px rgba(0, 0, 0, 0.50)) blur(20px);
z-index: -1;
}


我们的 drop-shadow 过滤器设置为显示不透明度为 50% 的黑色阴影,而我们的 blur 过滤器会将我们的伪元素模糊 20px。 这两个过滤器的组合最终创建了彩色的阴影,当应用这两个样式规则时,该阴影现在将出现在我们的寿司图像后面:



在这一点上,我们已经完成了。为完整起见,如果我们想要彩色阴影缩放的动画,如下 CSS 代码的添加能够助力我们实现目标:


.colorfulShadow {
position: relative;
}

.colorfulShadow::after {
content: "";
width: 100%;
height: 100%;
position: absolute;
background: inherit;
background-position: center center;
filter: drop-shadow(0px 0px 10px rgba(0, 0, 0, 0.50)) blur(20px);
z-index: -1;

/* animation time! */
animation: oscillate 1s cubic-bezier(.17, .67, .45, 1.32) infinite alternate;
}

@keyframes oscillate {
from {
transform: scale(1, 1);
}

to {
transform: scale(1.3, 1.3);
}
}


如果您想要一些交互性而没有不断循环的动画,您还可以使用 CSS 过渡来更改阴影在某些动作(如悬停)上的行为方式。困难的部分是像对待在 HTML 中明确定义或使用 JavaScript 动态创建的任何其他元素一样对待伪元素。唯一的区别是这个元素是完全使用 CSS 创建的!


结语


小结


伪元素允许我们使用 CSS 来完成一些历史上属于 HTML 和 JavaScript 领域的元素创建任务。对于我们多彩而智能的阴影,我们能够依靠父元素来设置背景图像。这使我们能够轻松定义一个既继承了父元素的背景图像细节,又允许我们为其设置一系列属性以实现模糊和阴影效果的子伪元素。虽然所有这些都很好,并且我们最大限度地减少了大量复制和粘贴,但这种方法不是很灵活。


如果我想将这样的阴影应用到一个不只是带有背景图像的空元素上怎么办?如果我有一个像 ButtonComboBox 这样的 HTML 元素想要应用这种阴影效果怎么办?一种解决方案是依靠 JavaScript 在 DOM 中复制适当的元素,将它们放置在前景元素下方,应用过滤器,然后就可以了。虽然这有效,但考虑到该过程的复杂程度,实在是有些不寒而栗。太糟糕了,JavaScript 没有等效的 renderTargetBitmap 这种能够把我们的视觉效果渲染成位图,然后你可以做任何你想做的事的 API…… 🥶


以上内容为译文翻译,下面为一些拓展:




拓展


说实在的,我们其实并不需要那么多复杂的内容,图片可以是任意的,比如说 PNG、SVG,最终精简后,HTML 代码仅仅为任意一个元素,附上 style 规定图片地址与大小:


<div class="shadowedImage" style="--data-width: 164px; --data-height: 48px; --data-image: url('https://sf3-scmcdn2-tos.pstatp.com/xitu_juejin_web/dcec27cc6ece0eb5bb217e62e6bec104.svg');"></div>


CSS 代码如下:


.shadowedImage {
position: relative;
margin: 100px;
width: var(--data-width);
height: var(--data-height);
max-height: 150px;
background-image: var(--data-image);
background-repeat: no-repeat;
background-size: contain;
}

.shadowedImage::after {
content: "";
width: 100%;
height: 100%;
position: absolute;
background: inherit;
background-position: center center;
filter: drop-shadow(0px 0px 10px rgba(0, 0, 0, 0.50)) blur(20px);
z-index: -1;
}


示例代码


一段示例代码如下:


<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>

<style>
.shadowedImage {
position: relative;
}

.shadowedImage::after {
content: "";
width: 100%;
height: 100%;
position: absolute;
background: inherit;
background-position: center center;
filter: drop-shadow(0px 0px 10px rgba(0, 0, 0, 0.50)) blur(20px);
z-index: -1;

/* animation time! */
animation: oscillate 1s cubic-bezier(.17, .67, .45, 1.32) infinite alternate;
}

@keyframes oscillate {
from {
transform: scale(1, 1);
}

to {
transform: scale(1.1, 1.1);
}
}

.shadowedImage {
margin: 100px;
width: var(--data-width);
height: var(--data-height);
max-height: 150px;
background-image: var(--data-image);
background-repeat: no-repeat;
background-size: contain;
}
</style>
</head>
<body>
<div class="parent">
<div class="shadowedImage" style="--data-width: 164px; --data-height: 48px; --data-image: url('https://sf3-scmcdn2-tos.pstatp.com/xitu_juejin_web/dcec27cc6ece0eb5bb217e62e6bec104.svg');"></div>
<div class="shadowedImage" style="--data-width: 164px; --data-height: 164px; --data-image: url('https://sf1-dycdn-tos.pstatp.com/img/bytedance-cn/4ac74bbefc4455d0b350fff1fcd530c7~noop.image');"></div>
<div class="shadowedImage" style="--data-width: 164px; --data-height: 164px; --data-image: url('https://sf1-ttcdn-tos.pstatp.com/img/bytedance-cn/4bcac7e2843bd01c3158dcaefda77ada~noop.image');"></div>
</div>
</body>
</html>


示例效果


效果如下:


image.png



如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。





链接:https://juejin.cn/post/6975818153376874503

收起阅读 »

小程序自定义TabBar 如何实现“keep-alive”

自定义TabBar方案 虽然在之前文章提到过了,本次采用组件化实现,具体实现如下: 我们可以新建一个home文件夹,在home/index.wxml中写一个tabBar,然后把TabBar页面写成组件,然后点击TabBar切换相应的组件展示就可以。代码...
继续阅读 »

自定义TabBar方案



虽然在之前文章提到过了,本次采用组件化实现,具体实现如下:





  • 我们可以新建一个home文件夹,在home/index.wxml中写一个tabBar,然后把TabBar页面写成组件,然后点击TabBar切换相应的组件展示就可以。代码如下:




  • wxml部分




<!-- home页面 -->

<view id='index'>
<!-- 自定义头部 -->
<head name='{{name}}' bgshow="{{bgshow}}" backShow='false'></head>

<!-- 首页 -->
<index change='{{activeIndex==0}}'></index>
<!-- 购物车 -->
<cart change='{{activeIndex==1}}'></cart>
<!-- 订单 -->
<order change='{{activeIndex==2}}'></order>
<!-- 我的 -->
<my change='{{activeIndex==2}}'></my>
<!-- tabbar -->
<view class="tab ios">
<view class="items {{activeIndex==index?'active':''}}" wx:for="{{tab}}" bindtap="choose" data-index='{{index}}' wx:key='index' wx:for-item="items">
<image wx:if="{{activeIndex==index}}" src="{{items.activeImage}}"></image>
<image wx:else src="{{items.image}}"></image>
<text>{{items.name}}</text>
</view>
</view>
</view>




  • home页面的ts


Page({
data: {
activeIndex:0,
tab:[
{
name:'商品',
image:'../../images/index.png',
activeImage:'../../images/index-hover.png',
},
{
name:'购物车',
image:'../../images/cart.png',
activeImage:'../../images/cart-hover.png',
},
{
name:'订单',
image:'../../images/order.png',
activeImage:'../../images/order-hover.png',
},
{
name:'我的',
image:'../../images/my.png',
activeImage:'../../images/my-hover.png',
}
]
},
// 切换事件
choose(e:any){
const _this=this;
const {activeIndex}=_this.data;
if(e.currentTarget.dataset.index==activeIndex){
return
}else{
_this.setData({
activeIndex:e.currentTarget.dataset.index
})
}
},
})




  • 上面代码不难理解,点击以后改变activeIndex从而控制每个组件的渲染和销毁,这样付出的代价还是比较大的,需要我们进一步的优化。


如何实现keep-alive



我们知道,这里主要是避免组件反复创建和渲染,有效提升系统性能。



实现思路




  • 1.在tab每个选项增加两个值:statusshowshow控制组件是否需要渲染,status控制组件display




  • 2.初始化时候设置首页的statusshow,其他都为false




  • 3.当我们切换时:把上一个tab页面的status改为false,然后把当前要切换页面的tab数据中的statusshow都改为true,最后再更新一下activeIndex的值。




  • wxml代码:




    <!-- 首页 -->
<view wx:if="{{tab[0].show}}" hidden="{{!tab[0].status}}">
<index></index>
</view>
<!-- 购物车 -->
<view wx:if="{{tab[1].show}}" hidden="{{!tab[1].status}}">
<cart></cart>
</view>
<!-- 订单 -->
<view wx:if="{{tab[2].show}}" hidden="{{!tab[2].status}}">
<order></order>
</view>
<!-- 我的 -->
<view wx:if="{{tab[3].show}}" hidden="{{!tab[3].status}}">
<my></my>
</view>



  • ts代码


Page({
data: {
activeIndex:0, //当前选中的index
tab:[
{
name:'商品',
image:'../../images/index.png',
activeImage:'../../images/index-hover.png',
status:true,//控制组件的display
show:true, //控制组件是否被渲染
},
{
name:'购物车',
image:'../../images/cart.png',
activeImage:'../../images/cart-hover.png',
status:false,
show:false,
},
{
name:'订单',
image:'../../images/order.png',
activeImage:'../../images/order-hover.png',
status:false,
show:false,
},
{
name:'我的',
image:'../../images/my.png',
activeImage:'../../images/my-hover.png',
status:false,
show:false,
}
]
},

choose(e:any){
const _this=this;
const {activeIndex}=_this.data;
//如果点击的选项是当前选中,就不执行
if(e.currentTarget.dataset.index==activeIndex){
return
}else{
//修改上一个tab页面的status
let prev='tab['+activeIndex+'].status',
//修改当前选中元素的status
status='tab['+e.currentTarget.dataset.index+'].status',
//修改当前选中元素的show
show='tab['+e.currentTarget.dataset.index+'].show';

_this.setData({
[prev]:false,
[status]:true,
[show]:true,
activeIndex:e.currentTarget.dataset.index,//更新activeIndex
})
}
},

})




  • 这样基本就大功告成了,来看一下效果:


Rp63gH.gif



  • 当我们点击切换时候,如果当前组件没有渲染就会进行渲染,如果渲染过后进行切换只是改变display,完美实现了需求,大功告成!


实际业务场景分析



在实际使用中还有两种种情况:



情况1:比如某些数据并不希望他首次加载后就数据保持不变,当切换页面时候希望数据进行更新,比如笔者做的电商小程序,在首页点击商品加入购物车,然后切换到购物车,每次切换时候肯定需要再次进行请求。

情况2:像个人中心这种页面,数据基本请求一次就可以,没必要每次切换请求数据,这种我们不需要进行改进。




  • 我们给组件传递一个值:status,然后在组件中监听这个值的变化,当值为true时候,去请求接口更新数据。具体代码如下:




  • wxml代码(只列举关键部分):




<!-- 首页 -->
<view wx:if="{{tab[0].show}}" hidden="{{!tab[0].status}}">
<index change='{{tab[0].status}}'></index>
</view>

<!-- 购物车 -->
<view wx:if="{{tab[1].show}}" hidden="{{!tab[1].status}}">
<cart change='{{tab[0].status}}'></cart>
</view>



  • 首页组件/购物车组件ts代码:


Component({
/**
* 组件的属性列表
*/
properties: {
change: {
type: String,//类型
value: ''//默认值
},
},
observers: {
//监听数据改变进行某种操作
'change': function(change) {
if(change=='true'){
console.log('更新首页数据'+change)
}
}
},
})



  • 来看一下最终效果:


Rp618e.gif



链接:https://juejin.cn/post/6976072584127315999

收起阅读 »

Android或前端开发中--不得不说的登录、授权(Cookie、Authorization)

Cookie起源:购物车他的起源比较早,那个时候还不是IE,更不是现在的Chrome,是更早的Netscape(网景)那个时候浏览器的开发者、开发浏览器的公司也会去帮别人开发网站。当时是是什么情况?有个电商网站希望有购物车这个功能。(购物车现在都是怎么做?不管...
继续阅读 »

Cookie

起源:购物车

  • 他的起源比较早,那个时候还不是IE,更不是现在的Chrome,是更早的Netscape(网景)
  • 那个时候浏览器的开发者、开发浏览器的公司也会去帮别人开发网站。
  • 当时是是什么情况?有个电商网站希望有购物车这个功能。(购物车现在都是怎么做?不管是淘宝还是什么网站,他们的购物车都是存在服务器的)
  • 可是那个时候的开发商他不想在自己服务器上面存信息,你又没有买,我存什么呀,你存本地吧,但是本地怎么存呢?没这功能呀。
  • 他就去给他做网站的开发人员,同时也是开发浏览器公司的人说, 你去给浏览器加这个功能吧, 你同时是浏览器的开发者,又是网站的开发者,这个时候做这个事是比较方便的。然后浏览器开发者就说,可以,我给你做,然后他们就干脆一不做二不休,做了一个完整的功能。
  • 这个功能叫做Cookie。他就是用来记录购物车的,只不过因为服务器不想记,想记到本地去,记到每一个添加购物车的电脑上。

工作机制:

  • 他是在本地记录,那么本地记录一个服务器需要的信息,怎么做呢?
    • 大致就是,服务器需要你保存什么,本来是服务器自己保存的信息,改成客户端记录。
    • 服务器需要你保存什么,然后发过来,你把他保存到本地就可以了。
  • 第一步,我在这个网站里面,我想往我的购物车里面加一个苹果,怎么加呢?
    • 我访问cart(购物车)这个接口,用post传过去一个数据,叫apple=1,就表示我要往我的购物车里面加一个apple。(请求响应报文完全不懂的可以补充一下知识http的原理和工作机制
  • 好,服务器就知道了。然后服务器处理完就会给我返回一个信息:
    • 好的(200 OK),同时会加一个header,这个header叫Set-Cookie:cart="apple=1"(不是Cookie,是Set-Cookie。
    • Cookie是客户端给服务器用的,而 Set-Cookie就是服务器给客户端用的,意思是你把这个cookie存下来吧),表示购物车里面有一个苹果。
    • 然后客户端就记下来了。好的,我知道了,我下次再访问shop.com的时候附加进去
    • (下图客户端下面这个shop.com就是服务器的请求地址,跟上面的请求是对应的,而且这个cookie存下来以后,他还有继续的自动机制,当浏览器再次访问这个内容的时候,他的cookie会自动附加进去,这是自动的,由浏览器来实现的,网站开发者不需要关心,用户也不需要关心)

image.png

  • 下次,我要加一个东西,比如我要加一个香蕉,
    • 我只要发过去我需要一个香蕉,同时将本地的这个cookie附加过去,而且这个cookie我不需要管,浏览器自动处理,服务器发消息的时候也不需要管,
    • 现在,我通过这个post告诉服务器要加一个香蕉,并且通过cookie告诉他我已经加了一个苹果,
  • 然后服务器就知道了,然后就返回消息告诉你记下来,记在本地。这个时候客户端自动更新这个东西。
  • 注意到没有,服务器什么都没记,他不管,客户端记了,所以依然不影响通讯。

image.png

  • 这个时候如果客户端要结账,
    • 他不需要发其他内容,因为cookie会把他的购物车给带过去,然后服务器再把这东西返回来。
    • 这里有一个东西很浪费,我把购物车信息发给你,你再发给我,好浪费啊,真麻烦,既然你帮我处理,我自己处理不就完了吗?
      • 这是早期,js这个东西在早期还是很落后的,服务器帮你做一个事,而且他还会做一些额外的事情,帮你做验证啊,还有没有货这些,我只是把他传过去,然后传回来,但是这个工作还是要服务器做的。
    • cookie是谁管理的?
      • 是服务器,每次修改都是服务器在做。客户端完全只是一个被动保存的机器。
    • 这个就是cookie设计这个机制的原因。就是服务器需要存什么,告诉他,客户端无条件的存下来,然后每次访问回去的时候,再把他带回去。

image.png

  • 关于Cookie大致知道就可以,因为我们在工作中,在移动开发中用cookie用得非常少,前些年还用一用,现在都不用了。逐渐在抛弃,但是抛弃的不是cookie,而是抛弃使用cookie来做登录,用cookie来做认证。

作用:

会话管理:登录状态,购物车

  • 那么我们怎么用cookie来管理登录状态呢?
    • 首先,我向服务器发一个http请求,去login,
      • 我要登录,我把我的用户名和登录密码都添加过去,给服务器确认。
    • 服务器怎么确认?
      • 他会记录a(username)登录了,
      • 同时他会为这件事情创建一个会话,表示我现在在和某个客户端,或者说某个用户代理,我们在通信,他现在是一个什么状态,他也许是一个登录状态,也许是一个非登录状态,总之我们正在会话中。
      • 他会记录下来他的会话信息(session),比如他的id是123。他就会把这个东西给存下来,
      • 然后把我的session id返回给我,服务器返回给客户端,session是一个名字,我可以叫session,可以叫metting都可以,他只是表示我和客户端之间交互了,然后他就会发回来,
      • session也是通过set-cookie。他是通过服务端定的,客户端什么都不管,我只把他记下来。

image.png

  • 然后客户端再次访问的时候,
  • 就会自动把这个session给移过去,然后服务器收到以后就会去对照,有没有一个session id=123的,
  • 一看,有,哦,原来他是username叫a的这个人,并且他现在是登录状态,好的,他要用户信息是吧?我给他,因为他已经登录了。
  • 然后就会正常地返回过来。这个是使用cookie来管理登录状态。

image.png

  • 类似session的标识,cookie里面是可以有多个的
    • 现在我用sessionid来管理我的登录信息是吧,同时我虽然已经用session存了你的登录信息了,我还不想存你的购物车,没关系,你要添加一个apple对吧?跟刚才那个过程是一样的,你发一个apple=1过来,我给你返回一个cart=“apple=1”,你记下来,
    • 你可以记两个cookie,
      • 一个sessionid用来管理你的登录,
      • 一个cart来管理你的购物车,两个东西互相不干扰。cookie是可以记多个的。

image.png

个性化:用户偏好、主题

  • 怎么用cookie来管理用户偏好呢?
    • 比如我现在有一个网站,叫shop.com,一个叫黑色主题,一个叫白色主题,我向服务器请求,请求完了以后,服务器给我返回一个client_id,他和session id是一样的,是一个标记,我把这个标记给你发过去,啪嗒贴脸上,然后每次你找我说话,我都会看见,你是做了那个标记的人。

image.png

  • 下次你再跟我说,我想换个蓝色主题, 专门发一个请求去改变主题,服务器觉得可以,就返回ok,这是一个正常的返回,然后服务器就记住了,这个client_id=123的人喜欢蓝色主题,他帮你保存了。

image.png

  • http是无状态的,假如你不存这个123,你下次再访问另一个页面,那肯定还是给你默认主题,因为记不住你,我不知道你就是上次那个人。
  • 那么下次我再访问过来,这个cookie是会自动附加的,然后他访问的是另外一个页面,并不是原页面,也不是改变主题的页面,可是服务器就是知道我要给他蓝色主题,因为他是123。
  • 这个就是网站用来管理用户偏好的一种记录方式,使用cookie在用户端记录的。

image.png

分析用户行为:

  • 关于分析用户行为,用户的追踪的使用

    • 这个在国外是比较有争议的,尤其是欧洲。我们在登录网址的时候,不知道你们注意到没有,尤其是一些国外的网站,国内的网站还不是很在意这个,因为跟法律有关跟当地法律有关,跟道德关系其实很小很小,一般都是跟法律有关,法律要求你必须这么干,或者就是民众的意识,你必须这么干这么干,你如果不这么干大家不用你网站了,那怎么办?做呗。分析用户行为,追踪用户行为,就是你能知道用户去了哪些网站,那么我们访问国外网站的时候,他在顶部或者底部跟你说,你现在访问的网站正在使用cookie对你进行行迹的追踪,这个对你是没有害处的,而且我们一定不会把你的消息给公开,只是我们自己来用的,请你理解。都会用这样的东西。我们看起来可能会觉得你怎么这么多余啊,你用就用呗,你还告诉我,让我不爽干嘛?但其实是这样的,在那些国家尤其是欧洲,你不通知用户,就使用cookie追踪用户是非法的,是会遭到惩罚的,比如巨额罚款,那么他们要做,就只有把他申明出来,用户点一个确定。都会点的,少数人不点,这个用户追踪是很有价值的。如果你真的那么在意的话那你别用了。没办法,我很想让你用,我很想赚你的钱,我只有不挣了。
  • 说一下这个东西怎么工作的。

    • 我有一个客户端和服务器,我想向你请求一个数据,
    • 然后你返回的数据有点意思,在body里面返回一个图片的链接,从你这个链接可以定到那个图片去,而且是自动的。就好像在你的网站打开一个在线图片一样,
    • 但他有什么关键的地方?
      • 就下面图片标记的那个地方,他加了一个和图片本身无关的信息from=shop.com,我在访问shop.com对吧,他去另外一个网站(3rd-part.com),这个网站就是帮助记录用户行踪的网站。
    • 追踪用户信息,从来都不是从a网站记录a网站,从b网站记录b网站,永远都是有一个统一的站点,他去记录用户去各个站点的行迹,然后他做一个统计。然后让其他网站一起来用,一个网站只记录一个网站在当前,只记录用户在自己的行为是没用的。这个不叫用户追踪。都是有一个统一的网站去记。
    • 这个例子里面就是这个3rd-part.com,他去记录了这个用户在shop.com的行为,那么这个链接有什么效果?
      • 就是这个用户打开这个网站之后,自动地去显示一张图片,可能会是一张广告什么的,然后这个图片他会附加一个信息,这个信息跟图片显示无关。也就是我去shop.com去放这个图片,后面就是追这个,如果我去tabao.com就是追的tabao.com,他们显示的是同样的图片,但是对于第三方记录网站来说不一样,
      • 他就会记录下来,当前这个用户是从shop.com来的,他就会在他的数据里面加一条,加什么呢?这个用户他去过shop.com。

image.png

  • 服务器返回这个Cookie信息,客户端会记下来(client_id = 123),
  • 然后客户端在访问这个图片的时候会自动往这个第三方发一个请求,不发请求怎么显示图片吧?怎么把图片下载下来?那么就只能请求这个第三方。
  • 然后第三方网站(3rd-part.com)就会记录,在他的数据库里面加一条,这个叫123的用户他来自shop.com,他就会增加这个用户记录。
  • 接下来,他会把图片显示给你,然后呢,也给你这么一个记录,会给一个针对第三方网站的一个id,这两个id可以不是同一个东西,他们名字也不一样,他们后面值也可以不一样,因为他们只是一个三方网站用来记录用户行踪的。
  • 如果接下来,这个用户去上另外一个网站,假如这个网站也捆绑了同一个第三方的统计,那么会有什么一个结果,他也会访问这个网站的图片。
  • 然后这个第三方就会知道,这个用户已经来过了,之前是去过shop.com,现在来了taobao.com,我记录一下,这个用户喜欢shop.com和taobao.com,喜欢去这么两个网站。
  • 那么这个时候就对这个用户有一定的画像了。他去了两个网站,如果用户去的网站很多的话,虽然我不知道你具体是谁,但是我知道你是一个什么样的人,那么说明什么?说明要推广告了。比如我之前去了一个旅游网站,然后我现在去一个电商网站,那么他就可能会推旅游团信息。另外,想象的空间是非常大的,就是你拿到足够多的用户数据之后,你对这个用户的画像足够精确之后,他能推的广告非常多。其实这种追踪用户行为,都是为了什么?都是为了推广告,都是为了从你兜里拿钱。

image.png

说两个额外的东西

XSS跨站脚本攻击(Cross-site scripting):HttpOnly

  • XSS跨站脚本攻击
    • 攻击?攻击不只是我打你,我对你有人任何的侵犯行为都叫攻击。
    • 我们的js,我们的网页脚本,他可能会帮我们的网页去做一些很方便的事情,另外他也可以拿到他的cookie,去用cookie做一些很方便的事情。
    • 可是,他也有可能去做坏事,比如我们记录用户信息靠的是什么?靠得就是cookie,那么假如你的cookie存的不是购物车,存的不是什么喜好,而是登录信息,如果js是一个坏人写的,他会怎么做呢?他可能会拿到你本地的cookie,然后直接给发出去,他去访问一个地址,比如访问他们自己的网站的一个存cookie的地址,把这个cookie发过去了, 你都不知道,后台就做这件事了。你的cookie就这么泄漏了,你的登录信息就这么泄漏了。一个js,一个本地脚本去获取header信息多正常啊,非常正常。
    • 这就是为什么cookie这个东西他为什么危险。因为我们登录这个事,本来是不存在的,后来慢慢出了,就用cookie做吧,然后慢慢就有人利用这个漏洞去做坏事。我的一个脚本,拿到你的cookie,直接给发走了,连邮件都不用,直接访问一个网页,一个url就可以了。
  • 他的应对政策有很多,其中一个就是在cookie这个header后面加上HttpOnly限制。
    • 比如:Set-Cookie:sessionid=123;HttpOnly
  • 他有什么效果?
    • 就是这个cookie你的本地脚本看不到,他只用于我们http的交换,只用于信息交换,本地脚本看不到,那么如果你是一个包含敏感信息,比如登录信息的cookie的话,那么你加这个东西去做限制。这都是后来被摸索出来的。

XSRF跨站请求伪造(Cross-site request forgery):Referer

  • 跨站请求伪造
    • 这个更加猥琐,也让我们有点想不到。他是什么呢?cookie是个自动机制,假如我现在访问一个网站,是个银行网站,然后我又去访问一个坏人的网站,我并不知道这个坏人网站,这个坏人在他的脚本里面加东西了,加什么呢?让我去访问一个地址,比如我使用图片的时候访问一个地址,访问地址的时候会附加一些别的操作,让我去访问银行,他会对各种银行都去试一试,我就赌你最近访问过银行,并且有cookie。
      • 比如这么一个url:bank.com/transfer?am…
      • 就这么一个url,你访问过去,就把钱转给坏人了,不需要我登录,不需要我确认,为什么?因为有cookie,cookie是自动的,对吧?假如我之前去登录过这个网站,那么他只要暗中去访问这个网址,刷,我钱转走了。
      • 当然实际操作中会有各种各样的防范,银行方面也会做这方面的防范,浏览器他们也会互相配合做这些东西,只是这是一个搞坏事的原型。
  • 那么解决方案,其中一点就是Referer这个header,Referer他的拼写是错的,Referrer(转发者)由于历史原因,就应该错着写才行。
    • 用法:Referer:http://www.google.com
    • 他就是用来显示你是从哪个网站跳转过来的。假如这个银行发现你这个url,你这个申请转账的url他是来自一个我不认识的网站,或者一个危险的网站,那么我拒绝对你转账。不过这种解决方案也有他的缺陷,你需要依赖浏览器,浏览器需要能给你做这个功能,假如浏览器不帮你做这个功能,我从a网站跳到b网站的时候,我不帮你自动加这个Referer这个header,那不就是瞎了吗。
  • 不过说来说去就比较长了,这两个都是cookie比较危险的点。cookie危险的点很多,但是不是因为他不好,而是这个东西天生劣势,由于要获得什么什么好处,所以会有什么什么危险。cookie为什么被遗弃?并不是他被遗弃,而是不再用于授权。

2.Authorization

  • 相比于cookie,Authorization就更加流行,而且越来越流行。

Authorization最常用的有两种:

Basic:

  • basic就是基本的授权方式,即使我们用的很少,但是也是很实用的。
  • Authorization:Basic<username:password(Base64ed)>
    • 例子:
    • get /user http/1.1
    • host: xxxxxx.xxx
    • Authorization:Basic eGlhb21pbmc6cWl1bG9uZw==
  • 比如现在我要做一个请求,我有一个header叫Authorization:Basic xxxx,这个就是认证信息,如果这个数据对了,那么我可以获取到用户信息,如果这个错了,用户信息就获取不到,他会跟我说你没有权限,你的权限不足。这是一个http请求,用法就是这样的,他用在header里面。

那么这个xxxx里面是什么内容呢?

  • base64转化后的用户名和密码。
  • 比如我的用户名和密码是xiaoming和qiulong,
  • 合在一起就是:xiaoming:qiulong
  • 转化后就是:eGlhb21pbmc6cWl1bG9uZw==

我这么请求数据服务器就会给我返回正确信息,这个就是basic,

  • 为什么叫basic,因为他是最基本的信息了,用户名密码都给我就完了。
  • 这个东西他有什么缺陷呢?他是有安全风险的,你这个东西万一被截获怎么办?
    • 其实,现在大多数网站,尤其是api,就是浏览器之外的,你的应用使用的时候已经全都是https了,那么安全就交给https,我真的可以把我的用户名和密码直接传过去,因为他们都会被加密的,别人看不到的。
    • 不过呢,他确实还有个安全缺陷,有一点点,就是假如你需要这么做,你就需要把经过base64转化的用户名和密码,或者是base64之前的,保存到本地,这样你下次再去请求才能够自动化这个东西。而不是要用户每次都输入用户名和密码,对吧?你换个页面就让用户输一次,用户不疯了吗?
    • 那么你把她存到你本地,不管是存到你电脑,还是存到你手机,假如你这个设备被人给黑了,比如你的手机你去获取了root权限,然后root权限你又把他给了某个软件,然后他就可以随便操作你手机了,他把你的东西盗走就是有可能的了。
    • 其实说回来,这种安全风险倒还好,其实他把自己的手机root权限获取到,本身就放弃了一定的安全性,对吧,手机被破解了你能怪这个机制不好吗?还是有些软件在用的,而且做得比较大比较重技术的公司也有在用这个的。所以本身在安全上是没问题的。刚才我说的安全风险相对来说还算好吧。设备被破解了才有风险,那叫什么风险呢。这是第一种,不过用的公司还是少一点。挺好用的,挺简单的。

Bearer:

  • Bearer(持票人)
    • 也就是拿着尚方宝剑的人,你做事需要亮你的尚方宝剑。这种就叫做token,上面的basic长着token这种形式,但是并不能叫token,严格说起来也是,你把用户名密码揉起来base64一下也可以用作票根。不过Bearer才是真正的比较形象的token形式。
    • 本来我不是那个人,但是那个人给了我权限,那么我拿这个令牌,就可以用这个令牌去获取信息,去操作。这个是很常用的一种方式。他的格式是这个样子:
    • Authorization:Bearer<Bearer token>
    • 前面也有个头Bearer,表示我这种认证用的不是basic,而是持票这种方式,后面把持票人的token填进来就可以了。而这个token他就不是某种算法得到的,而是需要找授权方给你,有的时候,你会使用github或者使用新浪微博,去给别的软件授权的时候,并不需要你从这个网站跳过去登录一下,而是直接从你的账户信息里面,会有一个获取token,获取api token,你把这个token复制出来给某个软件就可以了。这是一种方式,token本身就可以拿出来给别人去用。另外一种是OAuth2。

OAuth2:

  • 他是一种第三方认证的机制。
  • OAuth我们现在都是用的2,1 是好几年前的,2和1差别不大,他们核心都是一样的,但是他做了一些工作,让开发者关心的事情更少,同时并不降低安全性。
OAuth2流程:第三方授权示例
  • 首先去github-setting-application里面把掘金的授权回收,

image.png

  • 然后现在去掘金点登录,选择三方github,他就会进入这样一个页面。
  • 谁是第三方?
    • 现在我是github的用户,并不是我是掘金的用户,现在我用github的时候我需要做一个第三方的授权,我要把我的一些权限授给别的网站,比如这个网站叫掘金,这个第三方是掘金,第三方是我从github跳到做认证的地方,这个地方可能会有点迷,额,我不是使用github来登录吗?第三方怎么成了掘金了?这里不是我要强调一些概念,而是你要搞清楚才能理解一些其他东西。
    • 记住,第三方是掘金,第一方和第二方是你和github。你要把信息授权给别的网站你要做这么一回事。

image.png

image.png

  • 那么我要授权给第三方的时候,我现在点一个github登录,我就到了一个github页面了,是谁过来的?
    • 是掘金给我带过来的。看上面这个授权页面地址是github.com,而这个里面有一个关键信息,就是client_id。
    • 这个client_id是github授予给掘金的一个id,他什么时候授予的呢?
    • 掘金的开发者当初找github申请下来的。这个就是github他会对掘金有一个标记,那么这个标记有什么用处呢?
    • 他的用处就是当你打开这个页面的时候, 传入client_id,那么github就会自动把掘金的图标,以及这个掘金的名字,以及他需要哪些权限,还有下面这个url地址给你,然后用户看到这些的时候,他就会去判断,我现在需要授权的目标对象,到底是不是我以为的那个对象,因为有的时候网站会做伪造,会做劫持什么的,假如你没这个一步,他可能在授权过程中他给你换了。你以为你授权给掘金了,其实你授权给别的什么坏人网站了,那么坏人就要滥用你的信息了。这个client_id就这个作用,他用来做识别的。

image.png

  • 那么接下来,我在github上点那个确定,授权给xitu,然后请求发送,页面消失,登录成功。
  • 那么这一系列过程又发生了什么呢?
  • 我点了之后,这个github就会跳回掘金的网站去,同时跳回去的时候会返回一个授权码(Authorization code)。
    • Authorization code并不是一个token,为什么他不直接给token呢?
      • 有个很关键的原因,https这个过程他并不是被强制的,他在OAuth这个过程里面不是强制你要使用https,那么我这个过程可能被拦截了,可能被别人窃取了,窃取到之后,假如Authorization code就是最终的那个码的话,你别人窃取到之后不就可以使用这个权限了吗?
      • 还有什么呢?浏览器都是不可靠的,你不知道用户在用什么浏览器,你不知道用户在用什么操作系统,Authorization code传输到第三方以后,还是有可能泄漏。github只是给你一个code,code表示我的用户已经告诉我了,他确实愿意把权限授权给你(掘金),这个是一个证明,他愿意把权限授权给你,可是这并不是钥匙,他只是个证明,你拿这个code给我,跟我要用户数据,我不会给你,你还需要真正的授权的票据,那个token给我
      • 那么token怎么获取?接下来继续说。

image.png

  • 现在,我的浏览器已经获取到这个信息了,接下来他还要去找他的服务器,他会把这个code发送给服务器,通过http也好,通过https也好,这个东西不怕被窃取,他只是证明用户愿意授权。

image.png

  • 然后,到了服务器以后,服务器就会跟github去做请求了。就是第三方的服务器会去向授权方的服务器请求。请求的时候他会附加授权的code以及一个叫client_secret的东西。
  • 这个client_secret是什么呢?
    • 他是在第三方,也就是掘金去github申请的时候跟client_id一起发过来的,这两个数据本身没有什么区别,只是他们在实际用处上有点区别,这个secret是绝对保密的,任何地方都不会看到,只有第三方的服务器拿着。这次链接也绝对是https链接,是个绝对安全的链接。那么现在,我有code证明用户愿意授权给我,又有client_secret证明我就是掘金。这个时候github就知道了,他给我足够的身份信息,并且这个信息来得足够安全,他不会被人截获,他通过https过来的,那么这个时候github就足够放心,他把真正的access token返回回去了。

image.png

  • 这个时候Server拿到这个token,现在,这个OAuth2的流程已经结束了。他不需要把你的token发给客户端,不需要发给浏览器。用户把他在github上的一些权限授予了掘金,并且掘金已经拿到token了,接下来OAuth就不再参与了。
  • 接下来,比如你的Server调取信息,比如用户头像什么的。怎么做呢?Server去请求github.com,同时附加上这个token。怎么附加呢?
    • 假设我的token是abccccc

image.png

image.png

  • 这个过程是很安全的,但是由于一些事实的限制,或者是一些安全上的不在意,还有一种什么情况呢?Server会把这个token发给客户端,这个不是说不允许,只是他会对安全有一定的影响,就是别人把你这个token拿到了,他也可以去做事。

image.png

  • 然后这么请求,也是可以的,很多软件也这么做。不过这么做,就把OAuth流程的好处给浪费掉了,你既然这样,那干脆在用户授权之后直接返回回来不就可以了吗?费这么多事干嘛呀?费这么多事不就是为了让别人截取不到吗?让你的浏览器被人黑掉,你的手机被人黑掉,你的网络被人黑掉都没关系。你的token依然是安全的,对吧?但是你这么做这些东西有一点白费了。但是这种用法还是有一定的使用的概率的,还是有些公司是这么用的。只不过他不太具有OAuth2的安全性。

image.png

第三方登录示例:微信登录
  • 说到微信登录,我先说一下使用github登录,刚才我说使用github登录这个第三方授权,他的第三方是掘金。但是我要说,第三方登录这个事比第三方授权要来得晚一点,第三方登录他的第三方真的是github,你在掘金,使用第三方登录,他的第三方真的是github,由于第三方登录这个词的出现,他导致第三方授权这个概念非常非常含糊,非常非常让人难以理解。你应该能明白为什么吧?

    • 第三方授权是什么?本来是我跟github的信息,结果分享给你了,那掘金不是第三方吗?
    • 而第三方登录是什么呢?本来我要登录掘金的,但是我用了github,那第三方就是github。
  • 其实这个登录和授权都是很直观的东西,但是你要知道他们分别是谁,分别是谁不是为了考试,不是为了面试,但是你在思考问题的时候你会非常清晰。你把这些搞明白以后,你再看一些api文档什么的,你脑子会非常清晰,你会比谁都想得明白,这个是重点。

  • 继续说微信登录,他是什么?

    • 他是一种第三方登录。
    • 比如你有一个手机软件,然后他里面有一个登录按钮, 你可以使用用户名登录,也可以使用第三方登录。比如你可以使用微信登录。那么有些人会做微信的开发,不管你有没有做过,我要说一下微信登录的手机流程是什么。
  • 第一步,你会使用微信给你的api, 你通过这个api调用微信给你的接口,去打开微信的授权界面,那个授权界面叫微信登录。「你看授权登录,第三方授权和第三方登录真的是互相之间没法说。那个界面是微信对你进行第三方授权,微信对这个第三方(你的应用)授权,但其实他叫什么?他叫微信登录。」你点这个之后,微信就会把他的页面关闭,返回给你的软件一个Authorization code(授权码)。

  • 为什么给你授权码?这是一个完整的OAuth2的流程,接下来,正规做法,就是你把这个code告诉你的服务器,然后你的服务器再拿你的这个code,以及你的secret去找微信的服务器,去要你的access token,拿到这个token之后,你们的客户端需要什么数据,你们服务器就去请求什么数据,比如你的客户端需要微信的用户名和他的头像,好,你的服务器就去拿,不是客户端拿。客户端不应该持有token,除非不得以。 就算是不得以,也不能持有secret,你去找微信服务器去拿token的这个过程,一定不能发生在客户端。

  • 我知道有些公司在这么做的,有些公司就在这么做。他们的后端人员可能会推这个事,这个事不应该我们做呀, 你看微信api里面说的明明白白呀,要你去请求,你拿到那个code再去请求不就完了。你要secret吗?我给你呀,我们这存在有啊,你去吧。其实这个过程是不对的。你的客户端拿到code,code交给服务器,其实就完了。这个是为了安全考虑,不是为了省事考虑。如果是为了省事,根本就不需要OAuth了。直接用户同意之后,把token给你就完了。还要什么用code换token的过程啊?用code换token就因为你客户端获取了这个数据,未必足够安全。这个是微信登录。微信登录他是一个完整的OAuth过程。

在自家软件里面使用Bearer token
  • 也是使用这种方式,比如我的软件,有个接口
    • (api.xxx.com/login?username=qiulong&password=123)
    • 我输入这些信息,我的服务器就直接给我返回这个token(access_token=bdcj55s),
    • 当我再次使用的时候,我不需要附加其他信息,我只要附加(Authorization:Bearer bdcj55s),
    • 这就是下次我再请求的时候,我们的做法,这个过程他并没有OAuth的过程,他就是我前面说的做OAuth不要这么干的过程。我把用户名密码传过去,你直接把我要的那个token给我,我说的那个不安全的过程。他就是一个模仿了OAuth2的这种使用token的方式他的token的用法,但是他并不是一个OAuth2的过程。要知道这个并不是OAuth。很多人不懂OAuth2的原因就是这样的。就是有些api,他在用一个简化版本的OAuth2,你再去使用一些第三方OAuth2的过程你会发现,这怎么比我们公司麻烦这么多?好烦啊,他怎么还要code啊?其实是你们公司使用的是一个简化版的流程。你们自己登录自己的账户使用这种简化版是理所应当的,对吧?不然你还用code的话,你自己的Server拿着自己的code和自己的secret去找自己Server去换那个token,那不多此一举吗?
  • 这种过程,他只是使用了Bearer token这种模式,但是他并不是OAuth2的过程。
refresh token
  • 一个刷新的票根。

image.png

  • 大概长这个样子。就是服务器返回的时候不只是access_token,还返回了一个refresh_token,他是什么呢?

image.png

  • 这个过程中返回的不只是一个access token,还返回一个refresh_token,你的server可以使用refresh_token来找github.com,然后这个github.com就会返回这个新的access_token和一个新的refresh_token,然后之前那个老的access_token就失效了。或者你那个老的access_token在经过一段时间以后,比如七天,十五天后,他也会失效。你的assess_token会自动失效,或者会被refresh_token请求强制失效。那么这个过程是什么作用?他有什么意义呢?我本来有token你为什么要刷一下换一下呢?他其实就是为了安全,就是你的access_token不管怎么样,他还是有一定概率会丢掉的。那么你这个token丢掉之后,你要用户重新过来再认证一次,这就有点不现实,用户都很懒,用户每一个获取成本都很高,喂,你的token失效了,请你过来再认证一次。谁搭理你啊?对吧,这个用户可能就流失了。那么你怎么做,你为了安全你需要快速的把这个token给失效掉,然后你再获取一个新的 token,怎么获取?refresh_token。大致是下面这样的。

image.png

  • 然后服务器就会给你返回一个新的token,并且同时把你那个旧的让他失效。这个是refresh_token,他是肯定要https的。他的流程跟获取token的流程他都应该尽量发生在服务端。
收起阅读 »

Kotlin 源码 | 降低代码复杂度的法宝

随着码龄增大,渐渐意识到团队代码中的最大的敌人是“复杂度”。不合理的复杂度是降低代码质量,增加沟通成本的元凶。Kotlin 在降低代码复杂度方面有着诸多法宝。这一篇就以两个常见的业务场景来剖析下简单和复杂的关系。若要用一句话概括这关系,我最喜欢这一句:“一切简...
继续阅读 »

随着码龄增大,渐渐意识到团队代码中的最大的敌人是“复杂度”。不合理的复杂度是降低代码质量,增加沟通成本的元凶。

Kotlin 在降低代码复杂度方面有着诸多法宝。这一篇就以两个常见的业务场景来剖析下简单和复杂的关系。若要用一句话概括这关系,我最喜欢这一句:“一切简单的背后都蕴藏着复杂”。

启动线程和读取文件容是 Android 开发中两个颇为常见的场景。分别给出 Java 和 Kotlin 的实现,在惊叹两种语言表达力上悬殊的差距的同时,逐层剖析 Kotlin 语法简单背后的复杂。

启动线程

先看一个简单的业务场景,在 java 中用下面的代码启动一个新线程:

 Thread thread = new Thread() {
@Override
public void run() {
doSomething() // 业务逻辑
super.run();
}
};
thread.setDaemon(false);
thread.setPriority(-1);
thread.setName("thread");
thread.start();

启动线程是一个常用操作,其中除了 doSomething() 之外的其他代码都具有通用性。难道每次启动线程时都复制粘贴这一坨代码吗?不优雅!得抽象成一个静态方法以便到处调用:

public class ThreadUtil {
public static Thread startThread(Callback callback) {
Thread thread = new Thread() {
@Override
public void run() {
if (callback != null) callback.action();
super.run();
}
};
thread.setDaemon(false);
thread.setPriority(-1);
thread.setName("thread");
thread.start();
return thread;
}

public interface Callback {
void action();
}
}

仔细分析下这里引入的复杂度,一个新的类ThreadUtil及静态方法startThread(),还有一个新的接口Callback

然后就可以像这样构建线程了:

ThreadUtil.startThread( new Callback() {
@Override
public void action() {
doSomething();
}
})

对比下 Kotlin 的解决方案thread()

public fun thread(
start:
Boolean = true,
isDaemon:
Boolean = false,
contextClassLoader:
ClassLoader? = null,
name:
String? = null,
priority:
Int = -1,
block: () ->
Unit
)
: Thread {
val thread = object : Thread() {
public override fun run() {
block()
}
}
if (isDaemon)
thread.isDaemon = true
if (priority > 0)
thread.priority = priority
if (name != null)
thread.name = name
if (contextClassLoader != null)
thread.contextClassLoader = contextClassLoader
if (start)
thread.start()
return thread
}

thread()方法把构建线程的细节全都隐藏在方法内部。

然后就可以像这样启动一个新线程:

thread { doSomething() }

这简洁的背后是一系列语法特性的支持:

1. 顶层函数

Kotlin 中把定义在类体外,不隶属于任何类的函数称为顶层函数thread()就是这样一个函数。这样定义的好处是,可以在任意位置,方便地访问到该函数。

Kotlin 的顶层函数被编译成 java 代码后就变成一个类中的静态函数,类名是顶层函数所在文件名+Kt 后缀。

2. 高阶函数

若函数的参数或者返回值是 lambda 表达式,则称该函数为高阶函数

thread()方法的最后一个参数是 lambda 表达式。在 Kotlin 中当调用函数只传入一个 lambda 类型的参数时,可以省去括号。所以就有了thread { doSomething() }这样简洁的调用。

3. 参数默认值 & 命名参数

thread()函数包含了 6 个参数,为啥在调用时可以只传最后一个参数?因为其余的参数都在定义时提供了默认值。这个语法特性叫参数默认值

当然也可以忽略默认值,重新为参数赋值:

thread(isDaemon = true) { doSomething() }

当只想重新为某一个参数赋值时,不用将其余参数都重写一遍,只需用参数名 = 参数值,这个语法特性叫命名参数

逐行读取文件内容

再看一个稍复杂的业务场景:“读取文件中每一行的内容并打印”,用 Java 实现的代码如下:

File file = new File(path)
BufferedReader bufferedReader = null;
try {
bufferedReader = new BufferedReader(new InputStreamReader(new FileInputStream(file)));
String line;
// 循环读取文件中的每一行并打印
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭资源
if (bufferedReader != null) {
try {
bufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

对比一下 Kotlin 的解决方案:

File(path).readLines().foreach { println(it) }

一句话搞定,就算没学过 Kotlin 也能猜到这是在干啥,语义是如此简洁清晰。这样的代码写的时候畅快,读的时候悦目。

之所以简单,是因为 Kotlin 通过各种语法特性将复杂度分层并隐藏在了后背。

1. 扩展方法

拨开简单的面纱,探究背后隐藏的复杂:

// 为 File 扩展方法 readLines()
public fun File.readLines(charset: Charset = Charsets.UTF 8): List {
// 构建字符串列表
val result = ArrayList()
// 遍历文件的每一行并将内容添加到列表中
forEachLine(charset) { result.add(it) }
// 返回列表
return result
}

扩展方法是 Kotlin 在类体外给类新增方法的语法,它用类名.方法名()表达。

把 Kotlin 编译成 java,扩展方法就是新增了一个静态方法:

final class FilesKt  FileReadWriteKt {
// 静态函数的第一个参数是 File
public static final List readLines(@NotNull File $this$readLines, @NotNull Charset charset) {
Intrinsics.checkNotNullParameter($this$readLines, "$this$readLines");
Intrinsics.checkNotNullParameter(charset, "charset");
final ArrayList result = new ArrayList();
FilesKt.forEachLine($this$readLines, charset, (Function1)(new Function1() {
public Object invoke(Object var1) {
this.invoke((String)var1);
return Unit.INSTANCE;
}

public final void invoke(@NotNull String it) {
Intrinsics.checkNotNullParameter(it, "it");
result.add(it);
}
}));
return (List)result;
}
}

静态方法中的第一个参数是被扩展对象的实例,所以在扩展方法中可以通过this访问到类实例及其公共方法。

File.readLines() 的语义简单明了:遍历文件的每一行,将其添加到列表中并返回。

复杂度都被隐藏在了forEachLine(),它也是 File 的扩展方法,此处应该是this.forEachLine(charset) { result.add(it) },this 通常可以省略。forEachLine()是个好名字,一眼看去就知道是在遍历文件的每一行。

public fun File.forEachLine(charset: Charset = Charsets.UTF 8, action: (line: String) -> Unit): Unit {
BufferedReader(InputStreamReader(FileInputStream(this), charset)).forEachLine(action)
}

forEachLine()中将 File 层层包裹最终形成一个 BufferReader 实例,并且调用了 Reader 的扩展方法forEachLine()

public fun Reader.forEachLine(action: (String) -> Unit): Unit = 
useLines { it.forEach(action) }

forEachLine()调用了同是 Reader 的扩展方法useLines(),从名字细微的差别就可以看出uselines()完成了文件所有行内容的整合,而且这个整合的结果是可以被遍历的。

2. 泛型

哪个类能整合一组元素,并可以被遍历?沿着调用链继续往下:

public inline fun  Reader.useLines(block: (Sequence<String>) -> T): T =
buffered().use { block(it.lineSequence()) }

Reader 在useLines()中被缓冲化:

public inline fun Reader.buffered(bufferSize: Int = DEFAULT BUFFER SIZE): BufferedReader =
// 如果已经是 BufferedReader 则直接返回,否则再包一层
if (this is BufferedReader) this else BufferedReader(this, bufferSize)

紧接着调用了use(),使用 BufferReader:

// Closeable 的扩展方法
public inline fun T.use(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY ONCE)
}
var exception: Throwable? = null
try {
// 触发业务逻辑(扩展对象实例被传入)
return block(this)
} catch (e: Throwable) {
exception = e
throw e
} finally {
// 无论如何都会关闭资源
when {
apiVersionIsAtLeast(1, 1, 0) -> this.closeFinally(exception)
this == null -> {}
exception == null -> close()
else ->
try {
close()
} catch (closeException: Throwable) {}
}
}
}

这次的扩展函数不是一个具体类,而是一个泛型,并且该泛型的上界是Closeable,即为所有可以被关闭的类新增一个use()方法。

use()扩展方法中,lambda 表达式block代表了业务逻辑,扩展对象作为实参传入其中。业务逻辑在try-catch代码块中被执行,最后在finally中关闭了资源。上层可以特别省心地使用这个扩展方法,因为不再需要在意异常捕获和资源关闭。

3. 重载运算符 & 约定

读取文件内容的场景中,use() 中的业务逻辑是将BufferReader转换成LineSequence,然后遍历它。这里的遍历和类型转换分别是怎么实现的?

// 将 BufferReader 转化成 Sequence
public fun BufferedReader.lineSequence(): Sequence =
LinesSequence(this).constrainOnce()

还是通过扩展方法,直接构造了LineSequence对象并将BufferedReader传入。这种通过组合方式实现的类型转换和装饰者模式颇为类似(关于装饰者模式的详解可以点击使用组合的设计模式 | 美颜相机中的装饰者模式

LineSequence 是一个 Sequence:

// 序列
public interface Sequence<out T> {
// 定义如何构建迭代器
public operator fun iterator(): Iterator
}

// 迭代器
public interface Iterator<out T> {
// 获取下一个元素
public operator fun next(): T
// 判断是否有后续元素
public operator fun hasNext(): Boolean
}

Sequence是一个接口,该接口需要定义如何构建一个迭代器iterator。迭代器也是一个接口,它需要定义如何获取下一个元素及是否有后续元素。

2 个接口中的 3 个方法都被保留词operator修饰,它表示重载运算符,即重新定义运算符的语义。Kotlin 中预定义了一些函数名和运算符的对应关系,称为约定。当前这个约定就是iterator() + next() + hasNext()for循环的约定。

for 循环在 Kotlin 中被定义为“遍历迭代器提供的元素”,需要和in保留词一起使用:

public inline fun  Sequence.forEach(action: (T) -> Unit): Unit {
for (element in this) action(element)
}

Sequence 有一个扩展法方法forEach()来简化遍历语法,内部就使用了“for + in”来遍历序列中所有的元素。

所以才可以在Reader.forEachLine()中用如此简单的语法实现遍历文件中的所有行。

public fun Reader.forEachLine(action: (String) -> Unit): Unit = 
useLines { it.forEach(action) }

关于 Sequence 的用法实例可以点击Kotlin 基础 | 望文生义的 Kotlin 集合操作

LineSequence 的语义是 Sequence 中每一个元素都是文件中的一行,它在内部实现iterator()接口,构造了一个迭代器实例:

// 行序列:在 BufferedReader 外面包一层 LinesSequence
private class LinesSequence(private val reader: BufferedReader) : Sequence {
override public fun iterator(): Iterator {
// 构建迭代器
return object : Iterator {
private var nextValue: String? = null // 下一个元素值
private var done = false // 迭代是否结束

// 判断迭代器中是否有下一个元素,并顺便获取下一个元素存入 nextValue
override public fun hasNext(): Boolean {
if (nextValue == null && !done) {
// 下一个元素是文件中的一行内容
nextValue = reader.readLine()
if (nextValue == null) done = true
}
return nextValue != null
}

// 获取迭代器中下一个元素
override public fun next(): String {
if (!hasNext()) {
throw NoSuchElementException()
}
val answer = nextValue
nextValue = null
return answer!!
}
}
}
}

LineSequence 内部的迭代器在hasNext()中获取了文件中一行的内容,并存储在nextValue中,完成了将文件中每一行的内容转换成 Sequence 中的一个元素。

当在 Sequence 上遍历时,文件中每一行的内容就一个个出现在迭代中。这样做的好处是对内存更加友好,LineSequence 并没有持有文件中所有行的内容,它只是定义了如何获取文件中下一行的内容,所有的内容只有等待遍历时,才一个个地浮现出来。

用一句话总结 Kotlin 逐行读取文件内容的算法:用缓冲流(BufferReader)包裹文件,再用行序列(LineSequence)包裹缓冲流,序列迭代行为被定义为读取文件中一行的内容。遍历序列时,文件内容就一行行地被添加到列表中。

总结

顶层函数、高阶函数、默认参数、命名参数、扩展方法、泛型、重载运算符,Kotlin 利用了这些语法特性隐藏了实现常用业务功能的复杂度,并且在内部将复杂度分层。

分层是降低复杂度的惯用手段,它不仅让复杂度分散,使得同一时刻只需面对有限的复杂度,并且可以通过对每一层取一个好名字来概括本层的语义。除此之外,它还有助于定位问题(缩小问题范围)并增加代码可复用性(每层单独复用)。

是不是也可以效仿这种分层的思想方法,在写代码之前,琢磨一下,复杂度是不是太高了?可以运用那些语言特性实现合理的抽象将复杂度分层?以避免复杂度在一个层次被铺开。

收起阅读 »

Android内存优化工具

整理下Android内存优化常用的几种工具,top命令、adb shell dumpsys meminfo、Memory Profiler、LeakCanary、MAT1. toptop命令是Linux下常用的性能分析工具,能够实时显示系统中各个进程的资源占用...
继续阅读 »

整理下Android内存优化常用的几种工具,top命令、adb shell dumpsys meminfo、Memory Profiler、LeakCanary、MAT

1. top

top命令是Linux下常用的性能分析工具,能够实时显示系统中各个进程的资源占用状况。

查看top命令的用法

$ adb shell top --help
usage: top [-Hbq] [-k FIELD,] [-o FIELD,] [-s SORT] [-n NUMBER] [-m LINES] [-d SECONDS] [-p PID,] [-u USER,]

Show process activity in real time.

-H Show threads
-k Fallback sort FIELDS (default -S,-%CPU,-ETIME,-PID)
-o Show FIELDS (def PID,USER,PR,NI,VIRT,RES,SHR,S,%CPU,%MEM,TIME+,CMDLINE)
-O Add FIELDS (replacing PR,NI,VIRT,RES,SHR,S from default)
-s Sort by field number (1-X, default 9)
-b Batch mode (no tty)
-d Delay SECONDS between each cycle (default 3)
-m Maximum number of tasks to show
-n Exit after NUMBER iterations
-p Show these PIDs
-u Show these USERs
-q Quiet (no header lines)

Cursor LEFT/RIGHT to change sort, UP/DOWN move list, space to force
update, R to reverse sort, Q to exit.

使用top命令显示一次进程信息,以便讲解进程信息中各字段的含义

^[[41;173RTasks: 754 total,   1 running, 753 sleeping,   0 stopped,   0 zombie
Mem: 5.5G total, 5.4G used, 165M free, 76M buffers
Swap: 2.5G total, 789M used, 1.7G free, 2.4G cached
800%cpu 100%user 3%nice 54%sys 641%idle 0%iow 3%irq 0%sirq 0%host
PID USER PR NI VIRT RES SHR S[%CPU] %MEM TIME+ ARGS
15962 u0 a894 10 -10 6.6G 187M 76M S 75.6 3.2 8:16.55 asia.bluepay.cl+
785 system -2 -8 325M 13M 7.6M S 29.7 0.2 84:03.91 surfaceflinger
25255 shell 20 0 35M 2.7M 1.6M R 21.6 0.0 0:00.16 top -n 1
739 system -3 -8 177M 3.6M 2.2M S 10.8 0.0 16:00.36 android.hardwar+
16154 u0 i9086 10 -10 1.3G 40M 19M S 5.4 0.6 0:46.18 com.google.andr+
13912 u0 a87 20 0 17G 197M 86M S 5.4 3.4 23:56.88 com.tencent.mm
24789 root RT -2 0 0 0 D 2.7 0.0 0:01.36 [mdss fb0]
24704 root 20 0 0 0 0 S 2.7 0.0 0:01.20 [kworker/u16:12]
20096 u0 a94 30 10 6.1G 137M 53M S 2.7 2.3 0:31.45 com.xiaomi.mark+
2272 system 18 -2 8.7G 407M 267M S 2.7 7.1 191:11.32 system server
744 system RT 0 1.3G 1.6M 1.4M S 2.7 0.0 72:22.41 android.hardwar+
442 root RT 0 0 0 0 S 2.7 0.0 5:59.68 [cfinteractive]
291 root -3 0 0 0 0 S 2.7 0.0 5:00.17 [kgsl worker th+
10 root 20 0 0 0 0 S 2.7 0.0 1:55.84 [rcuop/0]
7 root 20 0 0 0 0 S 2.7 0.0 2:46.82 [rcu preempt]
25186 shell 20 0 34M 1.9M 1.4M S 0.0 0.0 0:00.71 logcat -v long +
25181 root 20 0 0 0 0 S 0.0 0.0 0:00.00 [kworker/2:3]
25137 root 20 0 0 0 0 S 0.0 0.0 0:00.00 [kworker/1:3]
25118 system 20 0 5.2G 83M 54M S 0.0 1.4 0:01.05 com.android.set+
24946 u0 a57 20 0 5.1G 60M 37M S 0.0 1.0 0:00.82 com.xiaomi.acco+
复制代码
第 1 行:进程信息
  • 总共(total):754个
  • 运行中(running)状态:1个
  • 休眠(sleeping)状态:753个
  • 停止(stopped)状态:0个
  • 僵尸(zombie)状态:0个
第 2 行:内存信息
  • 5.5G total:物理内存总量
  • 5.4G used:使用中的内存量
  • 165M free:空闲内存量
  • 76M buffers: 缓存的内存量
第 3 行:Swap分区信息
  • 2.5G total:交换区总量
  • 789M used:使用的交换区大小
  • 1.7G free:空闲交换区大小
  • 2.4G cached:缓冲的交换区大小

内存监控时,可以监控swap交换分区的used,如果这个数值在不断的变化,说明内核在不断进行内存和swap的数据交换,这是内存不够用了。

第 4 行:CPU信息
  • 800%cpu:8核cpu
  • 100%user:用户进程使用CPU占比
  • 3%nice:优先值为负的进程占比
  • 54%sys:内核进程使用CPU占比
  • 641%idle:除IO等待时间以外的其它等待时间占比
  • 0%iow:IO等待时间占比
  • 3%irq:硬中断时间占比
  • 0%sirq:软中断时间占比
第 5 行及以下:各进程的状态监控
  • PID:进程id
  • USER:进程所属用户
  • PR:进程优先级
  • NI:nice值,负值表示高优先级,正值表示低优先级
  • VIRT:进程使用的虚拟内存总量,VIRT=SWAP+RES
  • RES:进程使用的、未被换出的物理内存大小,RES=CODE+DATA
  • SHR:共享内存大小
  • S:进程状态
  • %CPU:上次更新到现在的CPU占用时间比
  • %MEM:使用物理内存占比
  • TIME+:进程时间的CPU时间总计,单位1/100秒
  • ARGS:进程名

2. dumpsys meminfo

首先了解下Android中最重要的四大内存指标的概念

指标全称含义等价
USSUnique Set Size独占物理内存进程独占的内存
PSSProportional Set Size实际使用物理内存PSS = USS + 按比例包含共享库内存
RSSResident Set Size实际使用物理内存RSS = USS + 包含共享库内存
VSSVirtual Set Size虚拟耗用内存VSS = 进程占用内存(包括虚拟耗用) + 共享库(包括比例分配部分)

我们主要使用USS和PSS来衡量进程的内存使用情况

dumpsys meminfo命令展示的是系统整体内存情况,内存项按进程进行分类

$ adb shell dumpsys meminfo
Applications Memory Usage (in Kilobytes):
Uptime: 168829244 Realtime: 1465769995

// 根据进程PSS占用值从大到小排序
Total PSS by process:
272,029K: system (pid 2272)
234,043K: com.tencent.mm (pid 13912 / activities)
185,914K: com.android.systemui (pid 13606)
107,294K: com.tencent.mm:appbrand0 (pid 5563)
101,526K: com.tencent.mm:toolsmp (pid 9287)
96,645K: com.miui.home (pid 15116 / activities)
...

// 以oom来划分,会详细列举所有的类别的进程
Total PSS by OOM adjustment:
411,619K: Native
62,553K: android.hardware.camera.provider@2.4-service (pid 730)
21,630K: logd (pid 579)
16,179K: surfaceflinger (pid 785)
...
272,029K: System
272,029K: system (pid 2272)
361,942K: Persistent
185,914K: com.android.systemui (pid 13606)
37,917K: com.android.phone (pid 2836)
23,510K: com.miui.contentcatcher (pid 3717)
...
36,142K: Persistent Service
36,142K: com.android.bluetooth (pid 26472)
101,198K: Foreground
72,743K: com.miui.securitycenter.remote (pid 4125)
28,455K: com.android.settings (pid 30919 / activities)
338,088K: Visible
96,645K: com.miui.home (pid 15116 / activities)
46,939K: com.miui.personalassistant (pid 31043)
36,491K: com.xiaomi.xmsf (pid 4197)
...
47,703K: Perceptible
17,826K: com.xiaomi.metoknlp (pid 4477)
10,748K: com.lbe.security.miui (pid 5097)
10,528K: com.xiaomi.location.fused (pid 4563)
8,601K: com.miui.mishare.connectivity (pid 4227)
13,088K: Perceptible Low
13,088K: com.miui.analytics (pid 19306)
234,043K: Backup
234,043K: com.tencent.mm (pid 13912 / activities)
22,028K: A Services
22,028K: com.miui.powerkeeper (pid 29762)
198,787K: Previous
33,375K: com.android.quicksearchbox (pid 31023)
23,278K: com.google.android.webview:sandboxed process0:org.chromium.content.app.SandboxedProcessService0:0 (pid 16154)
171,434K: B Services
45,962K: com.tencent.mm:push (pid 14095)
31,514K: com.tencent.mobileqq:MSF (pid 12051)
22,691K: com.xiaomi.mi connect service (pid 22821)
...
538,062K: Cached
107,294K: com.tencent.mm:appbrand0 (pid 5563)
101,526K: com.tencent.mm:toolsmp (pid 9287)
72,112K: com.tencent.mm:tools (pid 9187)
...

// 按内存的类别来进行划分
Total PSS by category:
692,040K: Native
328,722K: Dalvik
199,826K: .art mmap
129,981K: .oat mmap
126,624K: .dex mmap
124,509K: Unknown
92,666K: .so mmap
68,189K: Dalvik Other
53,491K: .apk mmap
44,104K: Gfx dev
28,099K: Other mmap
24,960K: .jar mmap
7,956K: Ashmem
3,700K: Stack
3,368K: Other dev
450K: .ttf mmap
4K: Cursor
0K: EGL mtrack
0K: GL mtrack
0K: Other mtrack

// 手机整体内存使用情况
Total RAM: 5,862,068K (status normal)
Free RAM: 3,794,646K ( 538,062K cached pss + 3,189,244K cached kernel + 0K cached ion + 67,340K free)
Used RAM: 2,657,473K (2,208,101K used pss + 449,372K kernel)
Lost RAM: 487,987K
ZRAM: 219,996K physical used for 826,852K in swap (2,621,436K total swap)
Tuning: 256 (large 512), oom 322,560K, restore limit 107,520K (high-end-gfx)
复制代码

查看单个进程的内存信息,命令如下

adb shell dumpsys meminfo [pid | packageName]
复制代码

我们查看下微信的内存信息

$ adb shell dumpsys meminfo com.tencent.mm
Applications Memory Usage (in Kilobytes):
Uptime: 169473031 Realtime: 1466413783

** MEMINFO in pid 13912 [com.tencent.mm] **
Pss Private Private SwapPss Heap Heap Heap
Total Dirty Clean Dirty Size Alloc Free
------ ------ ------ ------ ------ ------ ------
Native Heap 51987 51924 0 61931 159044 139335 19708
Dalvik Heap 74302 74272 8 2633 209170 184594 24576
Dalvik Other 10136 10136 0 290
Stack 84 84 0 8
Ashmem 2 0 0 0
Gfx dev 8808 8808 0 0
Other dev 156 0 156 0
.so mmap 9984 984 7436 8493
.jar mmap 1428 0 560 0
.apk mmap 2942 0 1008 0
.ttf mmap 1221 0 1064 0
.dex mmap 31302 44 30004 528
.oat mmap 2688 0 232 0
.art mmap 2792 2352 40 3334
Other mmap 6932 2752 632 0
Unknown 4247 4232 4 7493
TOTAL 293721 155588 41144 84710 368214 323929 44284

App Summary
Pss(KB)
------
Java Heap: 76664
Native Heap: 51924
Code: 41332
Stack: 84
Graphics: 8808
Private Other: 17920
System: 96989

TOTAL: 293721 TOTAL SWAP PSS: 84710

Objects
Views: 623 ViewRootImpl: 1
AppContexts: 9 Activities: 1
Assets: 12 AssetManagers: 0
Local Binders: 198 Proxy Binders: 183
Parcel memory: 46 Parcel count: 185
Death Recipients: 125 OpenSSL Sockets: 1
WebViews: 0

SQL
MEMORY USED: 156
PAGECACHE OVERFLOW: 13 MALLOC SIZE: 117

DATABASES
pgsz dbsz Lookaside(b) cache Dbname
4 28 46 721/26/4 /data/user/0/com.tencent.mm/databases/Scheduler.db

Asset Allocations
: 409K
: 12K
: 1031K
复制代码
  1. App Summary各项指标解读如下,通常我们需要重点关注Java Heap和Native Heap的大小,如果持续上升,有可能存在内存泄露。
属性内存组成
Java HeapDalvik Heap的Private Dirty + .art mmap的Private Dirty&Private Clean
Native HeapNative Heap的Private Dirty
Code.so mmap + .jar mmap + .apk mmap + .ttf.mmap + .dex.mmap + .oat mmap的Private Dirty&Private Clean
StackStack的Private Dirty
GraphicsGfx dev + EGL mtrack + GL mtrack的Private Dirty&Private Clean
  1. Objects中Views、Activities、AppContexts的异常可以判断有内存泄露,比如刚退出应用,查看Activites是否为0,如果不为0,则有Activity没有销毁。

3. Memory Profiler

Memory Profiler是 Android Profiler 中的一个组件,实时图表展示应用内存使用量,识别内存泄露和抖动,提供捕获堆转储,强制GC以及跟踪内存分配的能力。

Android Profiler官方文档

4. Leak Canary

非常好用的内存泄露检测工具,对于Activity/Fragment的内存泄露检测非常方便。

Square公司开源 官网地址,原理后面单独分析。

5. MAT

MAT是Memory Analyzer tool的缩写,是一个非常全面的分析工具,使用相对复杂点。 关于安装和配置有很多很好的文章结束,这里就不单独讲了,后面分析具体案例。

Android 内存优化篇 - 使用profile 和 MAT 工具进行内存泄漏检测

使用Android Studio和MAT进行内存泄漏分析

内存问题高效分析方法

  1. 接入LeakCanary,监控所有Activity和Fragment的释放,App所有功能跑一遍,观察是否有抓到内存泄露的地方,分析引用链找到并解决问题,如此反复,直到LeakCanary检查不到内存泄露。
  2. adb shell dumpsys meminfo命令查看退出界面后Objects的Views和Activities数目,特别是退出App后数目为否为0。
  3. 打开Android Studio Memory Profiler,反复打开关闭页面多次,点击GC,如果内存没有恢复到之前的数值,则可能发生了内存泄露。再点击Profiler的垃圾桶图标旁的heap dump按钮查看当面内存堆栈情况,按包名找到当前测试的Activity,如果存在多份实例,则很可能发生了内存泄露。
  4. 对于可疑的页面dump出内存快照文件,转换后用MAT打开,针对性的分析。
  5. 观察Memory Profiler每个页面打开时的内存波峰和抖动情况,针对性分析。
  6. 开发者选项中打开“不保留后台活动”,App运行一段时间后退到后台,触发GC,dump内存快照。MAT分析静态内容是否有可以优化的地方,比如图片缓存、单例、内存缓存等。
收起阅读 »

环信IM会话列表和聊天界面修改头像和昵称

如何修改会话列表和聊天界面的头像和昵称?方法简单,但这里先说明一下设计思路:MVVMModel view viewModel思路明确后,我们需要拿到其中的viewModel,然后修改其中的值.会话列表控制器和viewModel聊天控制器和viewModel如果...
继续阅读 »

如何修改会话列表和聊天界面的头像和昵称?


方法简单,但这里先说明一下设计思路:

MVVM

Model view viewModel

思路明确后,我们需要拿到其中的viewModel,然后修改其中的值.




会话列表控制器和viewModel



聊天控制器和viewModel


如果我们不考虑其中的结构/思路/思想,单纯为了解决问题,那么上述截图已经可以解决问题了.


我的理解:
为什么返回的viewModel一定是遵循某协议的?



我们正常理解的协议是:制定协议,指定委托,实现协议方法.

小了!格局小了!

当我思考上面截图这个协议之后.才明白,这里的协议是为了要求子类遵循标准.

这里协议本意并非是为了让实现什么,而是为了限定参数类型/参数名.是对数据模型的一种约束.

对于一个类型,无论是这个类型持有的方法还是属性,都是其特有的特点,既然是特点,便可继承.而这些方法啊,属性啊,不都是对此类型的一种约束吗?所以,我们可以看做 类型持有其特有的属性和方法,一些属性和一些方法约束了某一个类型.

如果同时了解java的同学都知道.java中有一个类型关键字为interface,我们称之为接口类,抽象类的一种,那么本意指的是,它也是一个类,只是无法实例化.

回头再看oc语言中的protocol,不就是java中的interface吗?

看到如此高质量的demo,使我的技术提升很大.多看大神的代码和多思考其思路,都是学习机会.

收起阅读 »

Android字体系列 (四):全局替换字体方式

前言 很高兴遇见你~ 在本系列的上一篇文章中,我们了解了 Xml 中的字体,还没有看过上一篇文章的朋友,建议先去阅读Android字体系列 (三):Xml中的字体,有了前面的基础,接下来我们就看下 Android 中全局替换字体的几种方式 注意:本文所展...
继续阅读 »

前言


很高兴遇见你~


在本系列的上一篇文章中,我们了解了 Xml 中的字体,还没有看过上一篇文章的朋友,建议先去阅读

Android字体系列 (三):Xml中的字体

,有了前面的基础,接下来我们就看下 Android 中全局替换字体的几种方式


注意:本文所展示的系统源码都是基于Android-30 ,并提取核心部分进行分析


Github Demo 地址 , 大家可以看 Demo 跟随我的思路一起分析


一、方式一:通过遍历 ViewTree,全局替换字体


之前我讲过:在 Android 中,我们一般会直接或间接的通过 TextView 控件去承载字体的显示,因为关于 Android 提供的承载字体显示的控件都会直接或间接继承 TextView。


那么这就是一个突破口:我们可以在 Activity 或 Fragment 的基类里面获取当前布局的 ViewTree,遍历 ViewTree ,获取 TextView 及其子类,批量修改它们的字体,从而达到全局替换字体的效果。


代码如下:


//全局替换字体工具类
object ChangeDefaultFontUtils {

private const val NOTO_SANS_BOLD = R.font.noto_sans_bold
/**
* 方式一: 遍历布局的 ViewTree, 找到 TextView 及其子类进行批量替换
*
*
@param mContext 上下文
*
@param rootView 根View
*/

fun changeDefaultFont(mContext: Context?, rootView: View?){
when(rootView){
is ViewGroup -> {
rootView.forEach {
changeDefaultFont(mContext,it)
}
}
is TextView -> {
try {
val typeface = ResourcesCompat.getFont(mContext!!, NOTO_SANS_BOLD)
val fontStyle = rootView.typeface?.style ?: Typeface.NORMAL
rootView.setTypeface(typeface,fontStyle)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
}

//Activity 基类
abstract class BaseActivity: AppCompatActivity(){

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val mRootView = LayoutInflater.from(this).inflate(getLayoutId(), null)
setContentView(mRootView)
ChangeDefaultFontUtils.changeDefaultFont(this,mRootView)
initView()
}

/**获取布局Id*/
abstract fun getLayoutId(): Int

/**初始化*/
abstract fun initView()
}

//MainActivity
class MainActivity : BaseActivity() {

override fun getLayoutId(): Int {
return R.layout.activity_main
}

override fun initView() {

}
}

上述代码:


1、创建了一个全局替换字体的工具类,主要逻辑:


判断当前 rootView 是否是一个 ViewGroup,如果是,遍历取出其所有的子 View,然后递归调用 changeDefaultFont 方法。再判断是否是 TextView 或其子类,如果是就替换字体


2、创建了一个 Activity 基类,并在其中写入字体替换的逻辑


3、最后让上层 Activity 继承基类 Activity


逻辑很简单,在看下我们编写的 Xml 的一个效果:


image-20210616144417422


接下来我们运行看下实际替换后的一个效果:


image-20210616144927196

可以看到,字体被替换了。


现在我们来讨论一下这种方式的优缺点:


优点:我们不需要修改 Xml 布局,不需要重写多个控件,只需要在 inflate View 之后调一下就可以了


缺点:不难发现这种方式会遍历 Xml 文件中的所有 View 和 ViewGroup,但是如果出现 RecyclerView , ListView,或者其他 ViewGroup 里面动态添加 View,那么我们还是需要去手动添加替换的逻辑,否则字体不会生效。而且它每次递归遍历 ViewTree,性能上多少会有点影响


接下来我们看第二种方式


二、方式二:通过 LayoutInflater,全局替换字体


讲这种方式前,我们首先要对 LayoutInflater 的 inflate 过程有一定的了解,以 AppCompatActivity 的 setContentView 为例大致说下流程:


我们在 Activity 的 setContentView 中传入一个布局 Xml,Activity 会通过代理类 AppCompatDelegateImpl 把它交由 LayoutInflater 进行解析,解析出来后,会交由自己的 3 个工厂去创建 View,优先级分别是mFactory2、mFactory、mPrivateFactory


流程大概就说到这里,具体过程我后续会写一篇文章专门去讲。


mFactory2、mFactory ,系统提供了开放的 Api 给我们去设置,如下:


//以下两个方法在 LayoutInflaterCompat.java 文件中
@Deprecated
public static void setFactory(@NonNull LayoutInflater inflater, @NonNull LayoutInflaterFactory factory) {
if (Build.VERSION.SDK_INT >= 21) {
inflater.setFactory2(factory != null ? new Factory2Wrapper(factory) : null);
} else {
final LayoutInflater.Factory2 factory2 = factory != null
? new Factory2Wrapper(factory) : null;
inflater.setFactory2(factory2);

final LayoutInflater.Factory f = inflater.getFactory();
if (f instanceof LayoutInflater.Factory2) {
forceSetFactory2(inflater, (LayoutInflater.Factory2) f);
} else {
forceSetFactory2(inflater, factory2);
}
}
}

public static void setFactory2(@NonNull LayoutInflater inflater, @NonNull LayoutInflater.Factory2 factory) {
inflater.setFactory2(factory);

if (Build.VERSION.SDK_INT < 21) {
final LayoutInflater.Factory f = inflater.getFactory();
if (f instanceof LayoutInflater.Factory2) {
forceSetFactory2(inflater, (LayoutInflater.Factory2) f);
} else {
forceSetFactory2(inflater, factory);
}
}
}

这两个方法在 LayoutInflaterCompat 这个类中,LayoutInflaterCompat 是 LayoutInflater 一个辅助类,可以看到:


1、setFactory 方法使用了 @Deprecated 注解表示这个 Api 被弃用


2、setFactory2 是 Android 3.0 引入的,它和 setFactory 功能是一致的,区别就在于传入的接口参数不一样,setFactory2 的接口参数要多实现一个方法


利用 setFactory 系列方法,我们可以:


1)、拿到 LayoutInflater inflate 过程中 Xml 控件对应的名称和属性


2)、我们可以对控件进行替换或者做相关的逻辑处理


看个实际例子:还是方式一的代码,我们在 BaseActivity 中增加如下代码:


//Activity 基类
abstract class BaseActivity: AppCompatActivity(){

//新增部分
private val TAG: String? = javaClass.simpleName

override fun onCreate(savedInstanceState: Bundle?) {
//...
//新增部分,其余代码省略
LayoutInflaterCompat.setFactory2(layoutInflater,object : LayoutInflater.Factory2{
override fun onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet
)
: View? {
Log.d(TAG, "name: $name" )
for (i in 0 until attrs.attributeCount){
Log.d(TAG, "attr: ${attrs.getAttributeName(i)} ${attrs.getAttributeValue(i)}")
}
return null
}

override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
return null
}

})
super.onCreate(savedInstanceState)
//...
}

//...
}

注意:上面 LayoutInflaterCompat.setFactory2 方法必须放在 super.onCreate(savedInstanceState) 的前面,不然会报错,因为系统会在 AppCompatActivity 的 oncreate 方法给 LayoutInflater 设置一个 Factory,而如果在已经设置的情况下再去设置,LayoutInflater 的 setFactory 系列方法就会抛异常,源码如下:


//AppCompatActivity 的 oncreate
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
final AppCompatDelegate delegate = getDelegate();
//调用 AppCompatDelegateImpl 的 installViewFactory 设置 Factory
delegate.installViewFactory();
//...
}

//AppCompatDelegateImpl 的 installViewFactory
@Override
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(mContext);
if (layoutInflater.getFactory() == null) {
//如果当前 LayoutInflater 的 Factory 为空,则进行设置
LayoutInflaterCompat.setFactory2(layoutInflater, this);
} else {
//如果不为空,则进行 Log 日志打印
if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {
Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
+ " so we can not install AppCompat's");
}
}
}

//LayoutInflater 的 setFactory2
public void setFactory2(Factory2 factory) {
//如果已经设置,则抛异常
if (mFactorySet) {
throw new IllegalStateException("A factory has already been set on this LayoutInflater");
}
if (factory == null) {
throw new NullPointerException("Given factory can not be null");
}
mFactorySet = true;
//...
}

注意:上面 AppCompatActivity 中设置 Factory 是 android.appcompat 1.1.0 版本,而如果是更高的版本,如 1.3.0,可能设置的地方会有点变化,但是不影响我们设置位置的变化,感兴趣的可以去看下源码,这里你只要知道我们必须在 Activity 的 super.onCreate(savedInstanceState) 之前设置 Factory 就可以了


运行应用程序,看下几个主要控件的截图打印信息:


image-20210616150016885

从 Log 输出可以看出,你所有的 Xml 控件,都会经过 LayoutInflaterFactory.onCreateView 方法走一遍去实现初始化的过程,在其中可以有效的分辨出是什么控件,以及它有什么属性。并且 onCreateView 方法的返回值就是一个 View,因此我们在此处可以对控件进行替换或者做相关的逻辑处理


到这里,你是否有了全体替换字体的思路了呢?


答案已经很明了:利用自定义的 Factory 进行字体的替换


这种方式我们只需要在 BaseActivity 里面操作就可以了,而且有效的解决了方式一带来的问题,提高了效率,如下:


abstract class BaseActivity: AppCompatActivity(){

override fun onCreate(savedInstanceState: Bundle?) {
LayoutInflaterCompat.setFactory2(layoutInflater,object : LayoutInflater.Factory2{
override fun onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet
)
: View? {
var view: View? = null
if(1 == name.indexOf(".")){
//表示自定义 View
//通过反射创建
view = layoutInflater.createView(name,null,attrs)
}

if(view == null){
//通过系统创建一系列 appcompat 的 View
view = delegate.createView(parent, name, context, attrs)
}

if(view is TextView){
//如果是 TextView 或其子类,则进行字体的替换
ChangeDefaultFontUtils.changeDefaultFont(this@BaseActivity,view)
}

return view
}

override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
return null
}

})
super.onCreate(savedInstanceState)
setContentView(getLayoutId())
initView()
}

/**获取布局Id*/
abstract fun getLayoutId(): Int

/**初始化*/
abstract fun initView()
}

上述代码我们做了:


1、判断是自定义 View ,通过反射创建


2、判断是系统提供的一些控件,使用 appcompat 系列 View 进行替换


3、判断是 TextView 或其子类,进行字体的替换


运行应用程序,最终实现了和方式一一样的效果:


image-20210616144927196

三、方式三:通过配置应用主题,全局替换默认字体


这种方式挺简单的,在 application 中,通过 android:theme 来配置一个 App 的主题。一般新创建的项目,都是会有一个默认基础主题。在其中追加关于字体的属性,就可以完成全局默认字体的替换,在主题中我们可以对以下三个属性进行配置:


 <item name="android:typeface">item>
<item name="android:fontFamily">item>
<item name="android:textStyle">item>

这三者的设置和关系我们在本系列的第一篇文章中已经讲过,还不清楚的可以去看下 传送门


关于 Xml 中使用字体的功能,我们上篇文章也已经讲过,还不清楚的可以去看下 传送门


因为我们只需要配置默认字体,所以新增一行如下配置,就可以实现全局替换默认字体的效果了:


<style name="Theme.ChangeDefaultFontDemo" parent="Theme.MaterialComponents.DayNight.DarkActionBar.Bridge">
//...
<item name="android:fontFamily">@font/noto_sans_bolditem>

//...
style>

那么凡事都有意外,假如你的 Activity 引用了自定义主题,且自定义主题没有继承基础主题,那么你就需要补上这一行配置,不然配置的默认字体不会生效


四、方式四:通过反射,全局替换默认字体


通过反射修改,其实和方式三有点类似。因为在 Android Support Library 26 之前,我们不能直接在 Xml 中设置第三方字体,而只能设置系统提供的一些默认字体,所以通过反射这种方式,可以把系统默认的字体替换为第三方的字体。而现在我们使用的版本基本上都会大于等于 26,因此通过配置应用主题的方式就可以实现全局替换默认字体的效果。但是这里并不妨碍我们讲反射修改默认字体。


1、步骤一:在 App 的主题配置默认字体


<style name="Theme.ChangeDefaultFontDemo" parent="Theme.MaterialComponents.DayNight.DarkActionBar.Bridge">
//...
<item name="android:typeface">serifitem>

//...
style>

这里随便选一个默认字体,后续我们反射的时候需要拿到你这个选的默认字体,然后进行一个替换


注意: 这里必须配置 android:typeface ,其他两个不行,在本系列的第一篇中,关于 typeface,textStyle 和 fontFamily 属性三者的关系我们分析过,还不清楚的可以去看看 传送门


setTypefaceFromAttrs 方法是 TextView 最终设置字体的方法,当 typeface 和 familyName 都为空,则会根据 typefaceIndex 的值取相应的系统默认字体。当我们设置 android:typeface 属性时,会将对应的属性值赋给 typefaceIndex ,并把 familyName 置为 null,而 typeface 默认为 null,因此满足条件


2、通过反射修改 Typeface 默认字体


注意:Google 在 Android 9.0 及之后对反射做了限制,被使用 @hide 标记的属性和方法通过反射拿不到


在 Typeface 中,自带的一些默认字体被标记的是 public static final,因此这里无需担心反射的限制


image-20210618174439624


因为在上一步配置的主题中,我们设置的是 serif ,所以这里替换它就好了,完整的方法就是通过反射拿到 Typeface 的默认字体 SERIF,然后使用反射将它修改成我们需要的字体即可:


object ChangeDefaultFontUtils {
const val NOTO_SANS_BOLD = R.font.noto_sans_bold

fun changeDefaultFont(mContext: Context) {
try {
val typeface = ResourcesCompat.getFont(mContext, NOTO_SANS_BOLD)
val defaultField = Typeface::class.java.getDeclaredField("SERIF")
defaultField.isAccessible = true
defaultField[null] = typeface
} catch (e: Exception) {
e.printStackTrace()
}
}
}

3、在 Application 里面,调用替换的方法


class MyApplication : Application() {

override fun onCreate() {
super.onCreate()
ChangeDefaultFontUtils.changeDefaultFont(this)
}
}

那么经过上面的三个步骤,我们同样可以实现全局替换默认字体的效果


五、项目实践


回到我们剩下的需求:全局替换默认字体


1、方式一和方式二都是全局替换字体,会将我们之前已经设置好的字体给覆盖,因此并不适合


2、方式三和方式四都是全局替换默认字体,我们之前已经设置好的字体不会被覆盖,满足我们的要求,但是方式四通过反射,是因为之前我们不能直接在 Xml 里面设置第三方字体。从 Android Support Library 26 及之后支持在 Xml 里面设置默认字体了,因此我在项目实践中,最终选择了方式三实现了全局替换默认字体的效果,需求完结 ?


六、总结


最后回顾一下我们讲的重点知识:


1、通过遍历 ViewTree,全局替换字体,这种方式每次都需要递归遍历,有性能问题


2、通过 LayoutInflater 设置自定义 Factory 全局替换字体,效率高


3、通过配置应用主题全局替换默认字体,简单高效


4、通过反射全局替换默认字体,相对于 3,性能会差点,使用步骤也相对复杂


5、我在项目实践过程中的一个选择


好了,本系列文章到这里就结束了,希望能给你带来帮助 ?


感谢你阅读这篇文章


参考和推荐


全局修改默认字体,通过反射也能做到


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

Android字体系列 (三):Xml中的字体

前言 很高兴遇见你~ 在本系列的上一篇文章中,我们对 Typeface 进行了深入的解析,还没有看过上一篇文章的朋友,建议先去阅读 Android字体系列(二):Typeface完全解析。接下来我们看下 Google 推出的 Xml 中使用字体 ...
继续阅读 »

前言


很高兴遇见你~


在本系列的上一篇文章中,我们对 Typeface 进行了深入的解析,还没有看过上一篇文章的朋友,建议先去阅读 

Android字体系列(二):Typeface完全解析

。接下来我们看下 Google 推出的 Xml 中使用字体


一、Xml 中字体介绍


Google 在 Android Support Library 26 引入了 Xml 中设置字体这项新功能,它可以让你将字体当成资源去使用,你可以在 res/font/ 文件夹中添加 font 文件,将字体捆绑为资源。这些字体会在 R 文件中编译,可直接在 Android Studio 中使用,如:


@font/myfont 
R.font.myfont

注意:要使用 Xml 字体功能,需引入 Android Support Library 26 及更高版本且要在 Android 4.1 及更高版本的设备


二、使用步骤


1、右键点击 res 文件夹,然后转到 New > Android resource directory


2、在 Resource type 列表中,选择 font,然后点击 OK


image-20210616203615018

3、在 font 文件夹中添加字体文件



关于字体,推荐两个免费下载的网站


fonts.google.com/


http://www.1001freefonts.com/



image-20210616203940427

添加之后就会生成 R.font.ma_shan_zhenng_regular 和 R.font.noto_sans_bold


4、双击字体文件可预览当前字体


image-20210616204148155


以上 4 个步骤完成后我们就可以在 Xml 中使用字体了


5、创建 font family


1)、右键点击 font 文件夹,然后转到 New > Font resource file。此时将显示 New Resource File 窗口。


2)、输入文件名,然后点击 OK。新的字体资源 Xml 会在编辑器中打开。


3)、将各个字体文件、样式和粗细属性都封装在 元素中。如下:



<font-family xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
tools:ignore="UnusedAttribute">


<font
android:fontStyle="normal"
android:fontWeight="400"
android:font="@font/ma_shan_zheng_regular"
tools:ignore="UnusedAttribute" />


<font
android:fontStyle="normal"
android:fontWeight="400"
android:font="@font/noto_sans_bold"
/>

font-family>

实践发现使用 font family 存在一些坑:


1、例如我上面添加了两个 font 标签,这个时候在 Xml 里面引用将不会有任何效果,而且设置的 fontStyle 等属性不会生效。


2、当只添加了一个 font 标签,此时字体会生效,但是设置的 fontStyle 等属性还是不会生效


因此我们在使用的时候建议直接对字体资源进行引用,样式粗细这些在进行单独的设置


三、在 XML 布局中使用字体


直接在布局 Xml 中使用 fontFamily 属性进行引用,如下图:


image-20210616205129045


四、在样式中添加并使用字体


1、在 style.xml 中添加样式


<style name="customfontstyle" parent="Theme.ChangeDefaultFontDemo">
<item name="android:fontFamily">@font/noto_sans_bolditem>

style>

2、在布局 Xml 中使用,如下图:


image-20210616205611588


五、在代码中使用字体


在代码中,我们可以通过 ResourcesCompat 或 Resource 的 gontFont 方法拿到 Typeface 对象,然后调用相关的 Api 去设置就行了,例如:


//方式1
val typeface = ResourcesCompat.getFont(context, R.font.myfont)
//方式2
val typeface = resources.getFont(R.font.myfont)
//设置字体
textView.typeface = typeface

为了方便在代码中使用,我们可以进行合理的封装:


object FontUtil {

const val NOTO_SANS_BOLD = R.font.noto_sans_bold
const val MA_SHAN_ZHENG_REGULAR = R.font.ma_shan_zheng_regular

/**缓存字体 Map*/
private val cacheTypeFaceMap: HashMap<Int,Typeface> = HashMap()

/**
* 设置 NotoSanUIBold 字体
*/

fun setNotoSanUIBold(mTextView: TextView){
try {
mTextView.typeface = getTypeface(NOTO_SANS_BOLD)
} catch (e: Exception) {
e.printStackTrace()
}
}

/**
* 设置 MaShanZhengRegular 字体
*/

fun setMaShanZhengRegular(mTextView: TextView){
try {
mTextView.typeface = getTypeface(MA_SHAN_ZHENG_REGULAR)
} catch (e: Exception) {
e.printStackTrace()
}
}

/**
* 获取字体 Typeface 对象
*/

fun getTypeface(fontResName: Int): Typeface? {
val cacheTypeface = cacheTypeFaceMap[fontResName]
if (cacheTypeface != null) {
return cacheTypeface
}
return try {
val typeface: Typeface? = ResourcesCompat.getFont(MyApplication.mApplication, fontResName)
cacheTypeFaceMap[fontResName] = typeface!!
typeface
} catch (e: Exception) {
e.printStackTrace()
Typeface.DEFAULT
}
}
}

那么后续我们在代码中使用字体,就只需调一行代码就 Ok 了


FontUtil.setMaShanZhengRegular(mTextView1)
FontUtil.setNotoSanUIBold(mTextView2)

六、项目需求实践


回顾一下我接到的项目需求:全局替换当前项目中的默认字体,并引入 UI 设计师提供的一些新字体


在学习本篇文章之前,我们引入字体都是放在 assets 文件目录下,这个目录下的字体文件,我们只能在代码中获取并使用。那么通过本篇文章的讲解,我们不仅可以在代码中进行使用,还可以在 Xml 中进行使用。现在我们解决了一半的需求,关于全局替换默认字体还需等到下一篇文章?


七、总结


回顾下本篇文章我们讲的一些重点内容:


1、将字体放在 res 的 font 目录下,这样我们就可以在 Xml 中使用字体了


2、通过字体 R 资源索引获取字体文件,封装相应的字体工具类,在代码中优雅的使用


好了,本篇文章到这里就结束了,希望能给你带来帮助 ?


Github Demo 地址


感谢你阅读这篇文章


下篇预告


下篇文章我会讲 Android 全局替换字体的几种方式,敬请期待吧 ?


参考和推荐


XML 中的字体



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

Android字体系列(二):Typeface完全解析

前言 很高兴遇见你~ 在本系列的上一篇文章中,我们介绍了关于 Android 字体的一些基础知识,还没有看过上一篇文章的朋友,建议先去阅读 Android字体系列 (一):Android字体基础,你会发现,我们设置的那三个属性最终都会去构建一个 ...
继续阅读 »

前言


很高兴遇见你~


在本系列的上一篇文章中,我们介绍了关于 Android 字体的一些基础知识,还没有看过上一篇文章的朋友,建议先去阅读 

Android字体系列 (一):Android字体基础

,你会发现,我们设置的那三个属性最终都会去构建一个 Typeface 对象,今天我们就好好的来讲讲它


注意:本文所展示的系统源码都是基于Android-30 ,并提取核心部分进行分析


一、Typeface 介绍


Typeface 负责 Android 字体的加载以及对上层提供相关字体 API 的调用


如果你想要操作字体,无论是使用 Android 系统自带的字体,还是加载自己内置的 .ttf(TureType) 或者 .otf(OpenType) 格式的字体文件,你都需要使用到 Typeface 这个类。因此我们要全局修改字体,首先就要把 Typeface 给弄明白


二、Typeface 源码分析


源码分析环节可能比较枯燥,坚持就是胜利 ??


1、Typeface 初始化


Typeface 这个类会在 Android 应用程序启动的过程中,通过反射的方式被加载。点击源码可以看到它里面有一个 static 代码块,它会随着类的加载而加载,并且只会加载一次,Typeface 就是通过这种方式来进行初始化的,如下:


static {
//创建一个存放字体的 Map
final HashMap systemFontMap = new HashMap<>();
//将系统的一些默认字体放入 Map 中
initSystemDefaultTypefaces(systemFontMap,SystemFonts.getRawSystemFallbackMap(),SystemFonts.getAliases());
//unmodifiableMap 方法的作用就是将当前 Map 进行包装,返回一个不可修改的Map,如果调用修改方法就会抛异常
sSystemFontMap = Collections.unmodifiableMap(systemFontMap);

// We can't assume DEFAULT_FAMILY available on Roboletric.
/**
* 设置系统默认字体 DEFAULT_FAMILY = "sans-serif";
* 因此系统默认的字体就是 sans-serif
*/

if (sSystemFontMap.containsKey(DEFAULT_FAMILY)) {
setDefault(sSystemFontMap.get(DEFAULT_FAMILY));
}

// Set up defaults and typefaces exposed in public API
//一些系统默认的字体
DEFAULT = create((String) null, 0);
DEFAULT_BOLD = create((String) null, Typeface.BOLD);
SANS_SERIF = create("sans-serif", 0);
SERIF = create("serif", 0);
MONOSPACE = create("monospace", 0);
//初始化一个 sDefaults 数组,并预加载好粗体、斜体等一些常用的 Style
sDefaults = new Typeface[] {
DEFAULT,
DEFAULT_BOLD,
create((String) null, Typeface.ITALIC),
create((String) null, Typeface.BOLD_ITALIC),
};

//...
}

上述代码写了详细的注释,我们可以发现,Typeface 初始化主要做了:


1、将系统的一些默认字体放入一个 Map 中


2、设置默认的字体


3、初始化一些默认字体


4、初始化一个 sDefaults 数组,存放一些常用的 Style


完成了 Typeface 的初始化,接下来看 Typeface 提供了一系列创建字体的 API ,其中对上层开放调用的有如下几个:


image-20210614130149262.png


下面我们来重点分析这几个方法


2、通过 Typeface 和 Style 获取新的 Typeface


对应上面截图的第一个 API , 看下它的源码:


public static Typeface create(Typeface family, @Style int style) {
//判断当前是否设置了 style , 如果没有设置,置为 NORMAL
if ((style & ~STYLE_MASK) != 0) {
style = NORMAL;
}
//判断当前传入的 Typeface 是否为空,如果是,置为默认字体
if (family == null) {
family = sDefaultTypeface;
}

// Return early if we're asked for the same face/style
//如果当前 Typeface 的 mStyle 属性和传入的 style 相同,直接返回 Typeface 对象
if (family.mStyle == style) {
return family;
}

final long ni = family.native_instance;

Typeface typeface;
//使用 sStyledCacheLock 保证线程安全
synchronized (sStyledCacheLock) {
//从缓存中获取存放 Typeface 的 SparseArray
SparseArray styles = sStyledTypefaceCache.get(ni);
if (styles == null) {
//存放 Typeface 的 SparseArray 为空,新创建一个,容量为 4
styles = new SparseArray(4);
//将当前 存放 Typeface 的 SparseArray 放入缓存中
sStyledTypefaceCache.put(ni, styles);
} else {
//存放 Typeface 的 SparseArray 不为空,直接获取 Typeface 并返回
typeface = styles.get(style);
if (typeface != null) {
return typeface;
}
}

//通过 native 层构建创建 Typeface 的参数并创建 Typeface 对象
typeface = new Typeface(nativeCreateFromTypeface(ni, style));
//将新创建的 Typeface 对象放入 SparseArray 中缓存起来
styles.put(style, typeface);
}
return typeface;
}

从上述代码我们可以知道:


1、当你设置的 Typeface 和 Style 为 null 和 0 时,会给它们设置一个默认值


注意:这里的 Style ,对应上一篇中讲的 android:textStyle 属性传递的值,用于设定字体的粗体、斜体等参数


2、如果当前设置的 Typeface 的 mStyle 属性和传入的 Style 相同,直接将 Typeface 给返回


3、从缓存中获取存放 Typeface 的容器,如果缓存中存在,则从容器中取出该 Typeface 并返回


4、如果不存在,则创建新的容器并加入缓存,然后通过 native 层创建 Typeface,并把当前 Typeface 放入到容器中


因此我们在使用的时候无需担心效率问题,它会把我们传入的字体进行一个缓存,后续都是从缓存中去拿的


3、通过字体名称和 Style 获取字体


对应上面截图的第二个 API:


public static Typeface create(String familyName, @Style int style) {
//调用截图的第一个 API
return create(getSystemDefaultTypeface(familyName), style);
}

//获取系统提供的一些默认字体,如果获取不到则返回系统的默认字体
private static Typeface getSystemDefaultTypeface(@NonNull String familyName) {
Typeface tf = sSystemFontMap.get(familyName);
return tf == null ? Typeface.DEFAULT : tf;
}

1、这个创建 Typeface 的 API 很简单,就是调用它的一个重载方法,我们已经分析过


2、getSystemDefaultTypeface 主要是通过 sSystemFontMap 获取字体,而这个 sSystemFontMap 在 Typeface 初始化的时候会存放系统提供的一些默认字体,因此这里直接取就可以了


4、通过 Typeface 、weight(粗体) 和 italic(斜体) 获取新的 Typeface


对应上面截图的第三个 API


public static @NonNull Typeface create(@Nullable Typeface family,
@IntRange(from = 1, to = 1000) int weight, boolean italic)
{
//校验传入的 weight 属性是否在范围内
Preconditions.checkArgumentInRange(weight, 0, 1000, "weight");
if (family == null) {
//如果当前传入的 Typeface 为 null, 则置为默认值
family = sDefaultTypeface;
}
//调用 createWeightStyle 方法创建 Typeface
return createWeightStyle(family, weight, italic);
}

private static @NonNull Typeface createWeightStyle(@NonNull Typeface base,
@IntRange(from = 1, to = 1000) int weight, boolean italic)
{
final int key = (weight << 1) | (italic ? 1 : 0);

Typeface typeface;
//使用 sWeightCacheLock 保证线程安全
synchronized(sWeightCacheLock) {
SparseArray innerCache = sWeightTypefaceCache.get(base.native_instance);
if (innerCache == null) {
//缓存 Typeface 的 SparseArray 为 null, 新建并缓存
innerCache = new SparseArray<>(4);
sWeightTypefaceCache.put(base.native_instance, innerCache);
} else {
//从缓存中拿取 typeface 并返回
typeface = innerCache.get(key);
if (typeface != null) {
return typeface;
}
}
//通过 native 创建 Typeface 对象
typeface = new Typeface(
nativeCreateFromTypefaceWithExactStyle(base.native_instance, weight, italic));
//将 Typeface 加入缓存
innerCache.put(key, typeface);
}
return typeface;
}

通过上述代码可以知道,他与截图一 API 的源码很类似,无非就是将之前需要设置的 Style 换成了 weight 和 italic,里面的实现机制是类似的


5、通过 AssetManager 和对应字体路径获取字体


对应上面截图的第四个 API


public static Typeface createFromAsset(AssetManager mgr, String path) {
//参数检查
Preconditions.checkNotNull(path); // for backward compatibility
Preconditions.checkNotNull(mgr);

//通过 Typeface 的 Builder 模式构建 typeface
Typeface typeface = new Builder(mgr, path).build();
//如果构建的 typeface 不为空则返回
if (typeface != null) return typeface;
// check if the file exists, and throw an exception for backward compatibility
//看当前字体路径是否存在,不存在直接抛异常
try (InputStream inputStream = mgr.open(path)) {
} catch (IOException e) {
throw new RuntimeException("Font asset not found " + path);
}
//如果构建的字体为 null 则返回默认字体
return Typeface.DEFAULT;
}

//接着看 Typeface 的 Builder 模式构建 typeface
//Builder 构造方法 主要就是初始化 mFontBuilder 和一些参数
public Builder(@NonNull AssetManager assetManager, @NonNull String path, boolean isAsset,
int cookie)
{
mFontBuilder = new Font.Builder(assetManager, path, isAsset, cookie);
mAssetManager = assetManager;
mPath = path;
}

//build 方法
public Typeface build() {
//如果 mFontBuilder 为 null,则会调用 resolveFallbackTypeface 方法
//resolveFallbackTypeface 内部会调用 createWeightStyle 创建 Typeface 并返回
if (mFontBuilder == null) {
return resolveFallbackTypeface();
}
try {
//通过 mFontBuilder 构建 Font
final Font font = mFontBuilder.build();
//使用 createAssetUid 方法获取到这个字体的唯一 key
final String key = mAssetManager == null ? null : createAssetUid(
mAssetManager, mPath, font.getTtcIndex(), font.getAxes(),
mWeight, mItalic,
mFallbackFamilyName == null ? DEFAULT_FAMILY : mFallbackFamilyName);
if (key != null) {
// Dynamic cache lookup is only for assets.
//使用 sDynamicCacheLock 保证线程安全
synchronized (sDynamicCacheLock) {
//通过 key 从缓存中拿字体
final Typeface typeface = sDynamicTypefaceCache.get(key);
//如果当前字体不为 null 直接返回
if (typeface != null) {
return typeface;
}
}
}
//如果当前字体不存在,通过 Builder 模式构建 FontFamily 对象
//通过 FontFamily 构建 CustomFallbackBuilder 对象
//最终通过 CustomFallbackBuilder 构建 Typeface 对象
final FontFamily family = new FontFamily.Builder(font).build();
final int weight = mWeight == RESOLVE_BY_FONT_TABLE
? font.getStyle().getWeight() : mWeight;
final int slant = mItalic == RESOLVE_BY_FONT_TABLE
? font.getStyle().getSlant() : mItalic;
final CustomFallbackBuilder builder = new CustomFallbackBuilder(family)
.setStyle(new FontStyle(weight, slant));
if (mFallbackFamilyName != null) {
builder.setSystemFallback(mFallbackFamilyName);
}
//builder.build 方法内部最终会通过调用 native 层创建 Typeface 对象
final Typeface typeface = builder.build();
//缓存 Typeface 对象并返回
if (key != null) {
synchronized (sDynamicCacheLock) {
sDynamicTypefaceCache.put(key, typeface);
}
}
return typeface;
} catch (IOException | IllegalArgumentException e) {
//如果流程有任何异常,则内部会调用 createWeightStyle 创建 Typeface 并返回
return resolveFallbackTypeface();
}
}

上述代码步骤:


1、大量运用了 Builder 模式去构建相关对象


2、具体逻辑就是使用 createAssetUid 方法获取到当前字体的唯一 key ,通过这个唯一 key ,从缓存中获取已经被加载过的字体,如果没有,则创建一个 FontFamily 对象,经过一系列 Builder 模式,最终调用 native 层创建 Typeface 对象,并将这个 Typeface 对象加入缓存并返回


3、如果流程有任何异常,内部会调用 createWeightStyle 创建 Typeface 并返回


6、通过字体文件获取字体


对应上面截图的第五个 API


public static Typeface createFromFile(@Nullable File file) {
// For the compatibility reasons, leaving possible NPE here.
// See android.graphics.cts.TypefaceTest#testCreateFromFileByFileReferenceNull
//通过 Typeface 的 Builder 模式构建 typeface
Typeface typeface = new Builder(file).build();
if (typeface != null) return typeface;

// check if the file exists, and throw an exception for backward compatibility
//文件不存在,抛异常
if (!file.exists()) {
throw new RuntimeException("Font asset not found " + file.getAbsolutePath());
}
//如果构建的字体为 null 则返回默认字体
return Typeface.DEFAULT;
}

//Builder 另外一个构造方法 主要是初始化 mFontBuilder
public Builder(@NonNull File path) {
mFontBuilder = new Font.Builder(path);
mAssetManager = null;
mPath = null;
}

从上述代码可以知道,这种方式主要也是通过 Builder 模式去构建 Typeface 对象,具体逻辑我们刚才已经分析过


7、通过字体路径获取字体


对应上面截图的第六个 API


public static Typeface createFromFile(@Nullable String path) {
Preconditions.checkNotNull(path); // for backward compatibility
return createFromFile(new File(path));
}

这个就更简单了,主要就是创建文件对象然后调用另外一个重载方法


8、Typeface 相关 Native 方法


在 Typeface 中,所有最终操作到加载字体的部分,全部都是 native 的方法。而 native 方法就是以效率著称的,这里只需要保证不频繁的调用(Typeface 已经做好了缓存,不会频繁的调用),基本上也不会存在效率的问题。


private static native long nativeCreateFromTypeface(long native_instance, int style);
private static native long nativeCreateFromTypefaceWithExactStyle(
long native_instance, int weight, boolean italic)
;
// TODO: clean up: change List to FontVariationAxis[]
private static native long nativeCreateFromTypefaceWithVariation(
long native_instance, List axes)
;
@UnsupportedAppUsage
private static native long nativeCreateWeightAlias(long native_instance, int weight);
@UnsupportedAppUsage
private static native long nativeCreateFromArray(long[] familyArray, int weight, int italic);
private static native int[] nativeGetSupportedAxes(long native_instance);

@CriticalNative
private static native void nativeSetDefault(long nativePtr);

@CriticalNative
private static native int nativeGetStyle(long nativePtr);

@CriticalNative
private static native int nativeGetWeight(long nativePtr);

@CriticalNative
private static native long nativeGetReleaseFunc();

private static native void nativeRegisterGenericFamily(String str, long nativePtr);

到这里,关于 Typeface 源码部分我们就介绍完了,下面看下它的一些其他细节


三、Typeface 其它细节


1、默认使用


在初始化那部分,Typeface 对字体和 Style 有一些默认实现


如果我们只想用系统默认的字体,直接拿上面的常量用就 ok 了,如:


Typeface.DEFAULT
Typeface.DEFAULT_BOLD
Typeface.SANS_SERIF
Typeface.SERIF
Typeface.MONOSPACE

而如果想要设置 Style ,我们不能通过 sDefaults 直接去拿,因为上层调用不到 sDefaults,但是可以通过 Typeface 提供的 API 获取:


public static Typeface defaultFromStyle(@Style int style) {
return sDefaults[style];
}

//具体调用
Typeface.defaultFromStyle(Typeface.NORMAL)
Typeface.defaultFromStyle(Typeface.BOLD)
Typeface.defaultFromStyle(Typeface.ITALIC)
Typeface.defaultFromStyle(Typeface.BOLD_ITALIC)

2、Typeface 中的 Style


1)、Typeface 中的 Style 可以通过 android:textStyle 属性去设置粗体、斜体等样式


2)、在 Typeface 中,这些样式也对应了一个个的常量,并且 Typeface 也提供了对应的 Api,让我们获取到当前字体的样式


// Style
public static final int NORMAL = 0;
public static final int BOLD = 1;
public static final int ITALIC = 2;
public static final int BOLD_ITALIC = 3;

/** Returns the typeface's intrinsic style attributes */
public @Style int getStyle() {
return mStyle;
}

/** Returns true if getStyle() has the BOLD bit set. */
public final boolean isBold() {
return (mStyle & BOLD) != 0;
}

/** Returns true if getStyle() has the ITALIC bit set. */
public final boolean isItalic() {
return (mStyle & ITALIC) != 0;
}

3、FontFamily 介绍


FontFamily 主要就是用来构建 Typeface 的一个类,注意和在 Xml 属性中设置的 android:fontFamily 区分开来就好了


四、总结


总结下本篇文章所讲的一些重点内容:


1、Typeface 初始化对字体和 Style 会有一些默认实现


2、Typeface create 系列方法支持从系统默认字体、 assets 目录、字体文件以及字体路径去获取字体


3、Typeface 本身支持缓存,我们在使用的时候无需注意效率问题


好了,本篇文章到这里就结束了,希望能给你带来帮助 ?


感谢你阅读这篇文章


下篇预告


下篇文章我会讲在 Xml 中使用字体,敬请期待吧 ?


参考和推荐


Android 修改字体,跳不过的 Typeface


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

Android字体系列 (一):Android字体基础

前言 很高兴遇见你~ 最近接到一个需求,大致内容是:全局替换当前项目中的默认字体,并引入 UI 设计师提供的一些新字体。于是对字体做了些研究,把自己的一些心得分享给大家。 注意:本文所展示的系统源码都是基于Android-30 ,并提取核心部分进行分析 ...
继续阅读 »

前言


很高兴遇见你~


最近接到一个需求,大致内容是:全局替换当前项目中的默认字体,并引入 UI 设计师提供的一些新字体。于是对字体做了些研究,把自己的一些心得分享给大家。


注意:本文所展示的系统源码都是基于Android-30 ,并提取核心部分进行分析


一、Android 默认字体介绍


1、Android 系统默认使用的是一款叫做 Roboto 的字体,这也是 Google 推荐使用的一款字体 传送门。它提供了多种字体形式的选择,例如:粗体,斜体等等。


2、在 Android 中,我们一般会直接或间接的通过 TextView 控件去承载字体的显示,因为关于 Android 提供的承载字体显示的控件都会直接或间接继承 TextView,例如:EditText,Button 等等,下面给出一张 TextView 继承图:


image-20210612124458481


3、TextView 中有三个属性可以设置字体的显示:


1)、textStyle


2)、typeface


3)、fontFamily


下面我们重点介绍下这三个属性


二、textStyle


textStyle 主要用来设置字体的样式,我们看下它在 TextView 的自定义属性中的一个体现:


//TextView 的自定义属性 textStyle
<attr name="textStyle">
<flag name="normal" value="0" />
<flag name="bold" value="1" />
<flag name="italic" value="2" />
</attr>

从上述自定义属性中我们可以知道:


1、textStyle 主要有 3 种样式:



  • normal:默认字体

  • bold:粗体

  • italic:斜体


2、textStyle 是用 flag 来承载的,flag 表示的值可以做或运算,也就是说我们可以设置多种字体样式进行叠加


接下来我们在 xml 中设置一下,如下图:


image-20210612205549971


可以看到,我们给 TextView 的 textStyle 属性设置了粗体和斜体两种样式叠加,右边可以看到预览效果


同样我们也可以在代码中对其进行设置,但是在代码中设置字体样式只能设置一种,不能叠加:


mTextView.setTypeface(null, Typeface.BOLD)

三、typeface


typeface 主要用于设置 TextView 的字体,我们看下它在 TextView 的自定义属性中的一个体现:


//TextView 的自定义属性 typeface
<attr name="typeface">
<enum name="normal" value="0" />
<enum name="sans" value="1" />
<enum name="serif" value="2" />
<enum name="monospace" value="3" />
</attr>

从上述自定义属性中我们可以知道:


1、typeface 提供了 4 种字体:



  • noraml:普通字体,系统默认使用的字体

  • sans:非衬线字体

  • serif:衬线字体

  • monospace:等宽字体


2、typeface 是用 enum 来承载的,enum 表示枚举类型,每次只能选择一个,因此我们每次只能设置一种字体,不能叠加


接下来我们在 xml 中设置一下,如下图:


image-20210612133722082


简单介绍这几种字体的区别:


serif (衬线字体):在字的笔划开始及结束的地方有额外的装饰,而且笔划的粗细会因直横的不同而有不同相


sans (非衬线字体):没有 serif 字体这些额外的装饰,和 noraml 字体是一样的


image-20210612134441993


monospace (等宽字体):限制每个字符的宽度,让它们达到一个等宽的效果


同样我们也可以在代码中进行设置:


mTv.setTypeface(Typeface.SERIF)

四、fontFamily


fontFamily 相当于是加强版的 typeface,它表示 android 系统支持的一系列字体,每个字体都有一个别名,我们通过别名就能设置这种字体,看下它在 TextView 的自定义属性中的一个体现:


//TextView 的自定义属性 fontFamily
<attr name="fontFamily" format="string" />

从上述自定义属性中我们可以知道:


fontFamily 接收的是一个 String 类型的值,也就是我们可以通过字体别名设置这种字体,如下图:


fontFamily


可以看到,它细致的区分了每个系列字体的样式,同样我们在 xml 中对它进行一个设置:


image-20210612212209243 我们在代码中在对他进行一个设置:


mTv.setTypeface(Typeface.create("sans-serif-medium",Typeface.NORMAL))

值的注意的是:fontFamily 设置的某些字体有兼容性问题,如我上面设置的 sans-serif-medium 字体,它在 Android 系统版本大于等于 21 才会生效,如果小于 21 ,则会使用默认字体,因此我们在使用 fontFamily 属性时,需要注意这个问题


到这里,我们就把影响 Android 字体的 3 个属性给讲完了,但是我心里有个疑问?? ?假设我这三个属性同时设置,会一起生效吗?


带着这个问题,我们探索一下源码


五、textStyle,typeface,fontFamily 三者关系分析


TextView 在我们使用它之前需进行一个初始化,最终会调用它参数最多的那个构造方法:


public TextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
//省略成吨代码.....
//读取设置的属性
readTextAppearance(context, appearance, attributes, false /* styleArray */);
//设置字体
applyTextAppearance(attributes);
}

private void applyTextAppearance(TextAppearanceAttributes attributes) {
//省略成吨代码.....
setTypefaceFromAttrs(attributes.mFontTypeface, attributes.mFontFamily,
attributes.mTypefaceIndex, attributes.mTextStyle, attributes.mFontWeight);
}

上面这条调用链,首先会读取 TextView 设置的相关属性,我们看下与字体相关的几个:


private void readTextAppearance(Context context, TypedArray appearance,
TextAppearanceAttributes attributes, boolean styleArray)
{
//...
switch (index) {
case com.android.internal.R.styleable.TextAppearance_typeface:
attributes.mTypefaceIndex = appearance.getInt(attr, attributes.mTypefaceIndex);
if (attributes.mTypefaceIndex != -1 && !attributes.mFontFamilyExplicit) {
attributes.mFontFamily = null;
}
break;
case com.android.internal.R.styleable.TextAppearance_fontFamily:
if (!context.isRestricted() && context.canLoadUnsafeResources()) {
try {
attributes.mFontTypeface = appearance.getFont(attr);
} catch (UnsupportedOperationException | Resources.NotFoundException e) {
// Expected if it is not a font resource.
}
}
if (attributes.mFontTypeface == null) {
attributes.mFontFamily = appearance.getString(attr);
}
attributes.mFontFamilyExplicit = true;
break;
case com.android.internal.R.styleable.TextAppearance_textStyle:
attributes.mTextStyle = appearance.getInt(attr, attributes.mTextStyle);
break;
//...
default:
}
}

从上述代码中我们可以看到:


1、当我们设置 typeface 属性时,会将对应的属性值赋给 mTypefaceIndex ,并把 mFontFamily 置为 null


2、当我们设置 fontFamily 属性时,首先会通过 appearance.getFont() 方法去获取字体文件,如果能获取到,则赋值给 mFontTypeface,如果获取不到,则通过 appearance.getString() 方法取获取当前字体别名并赋值给 mFontFamily


注意:当我们给 fontFamily 设置了一些第三方字体,那么此时 appearance.getFont() 方法就获取不到字体


3、当我们设置 textStyle 属性时,会将获取的属性值赋给 mTextStyle


上述方法走完了,会调 setTypefaceFromAttrs() 方法,这个方法就是最终 TextView 设置字体的方法,我们来解析下这个方法:


private void setTypefaceFromAttrs(@Nullable Typeface typeface, @Nullable String familyName,
@XMLTypefaceAttr int typefaceIndex, @Typeface.Style int style,
@IntRange(from = -1, to = FontStyle.FONT_WEIGHT_MAX) int weight)
{
if (typeface == null && familyName != null) {
// Lookup normal Typeface from system font map.
final Typeface normalTypeface = Typeface.create(familyName, Typeface.NORMAL);
resolveStyleAndSetTypeface(normalTypeface, style, weight);
} else if (typeface != null) {
resolveStyleAndSetTypeface(typeface, style, weight);
} else { // both typeface and familyName is null.
switch (typefaceIndex) {
case SANS:
resolveStyleAndSetTypeface(Typeface.SANS_SERIF, style, weight);
break;
case SERIF:
resolveStyleAndSetTypeface(Typeface.SERIF, style, weight);
break;
case MONOSPACE:
resolveStyleAndSetTypeface(Typeface.MONOSPACE, style, weight);
break;
case DEFAULT_TYPEFACE:
default:
resolveStyleAndSetTypeface(null, style, weight);
break;
}
}
}

上述代码步骤:


1、当 typeface 为空并且 familyName 不为空时,取 familyName 的字体


2、当 typeface 不为空并且 familyName 为空时,取 typeface 的字体


3、当 typeface 和 familyName 都为空,则根据 typefaceIndex 的值取相应的字体


4、typeface ,familyName 和 typefaceIndex 在我们分析的 readTextAppearance 方法会被赋值


5、resolveStyleAndSetTypefce 方法会进行字体和字体样式的设置


6、style 是在 readTextAppearance 方法中赋值的,他和设置字体并不冲突


好,现在代码分析的差不多了,我们再来看下上面那个疑问?我们使用假设法来进行推导:


假设在 Xml 中, typeface,familyName 和 textStyle 我都设置了,那么根据上面分析:


1、textStyle 肯定会生效


2、当设置了 typeface 属性,typefaceIndex 会被赋值,同时 familyName 会置为空


3、当设置了 familyName 属性,分情况:1、如果设置的是系统字体,typeface 会被赋值,familyName 还是为空。2、如果设置的是第三方字体,typeface 为空,familyName 被赋值


因此,当我们设置了这个三个属性,typeface 和 familyName 总有一个不会为空,因此不会走第三个条件体,那么 typeface 设置的属性就不会生效了,而剩下的两个属性都能够生效


最后对这三个属性做一个总结:


1、fontFamily、typeface 属性用于字体设置,如果都设置了,优先使用 fontFamily 属性,typeface 属性不会生效


2、textStyle 用于字体样式设置,与字体设置不会产生冲突


上面这段源码分析可能有点绕,如果有不清楚的地方,欢迎评论区给我留言提问


六、TextView 设置字体属性源码分析


通过上面源码的分析,我们清楚了 fontFamily,typeface 和 textStyle 这三者的关系。接下来我们研究一下,我们设置的这些属性是怎么实现这些效果的呢?又到了源码分析环节?,可能会有点枯燥,但是如果你能够认真看完,一定会收获很多,干就完了


我们上面用 Xml 或代码设置的字体属性,最终都会走到 TextView 的 setTypeface 重载方法:


//重载方法一
public void setTypeface(@Nullable Typeface tf) {
if (mTextPaint.getTypeface() != tf) {
//通过 mTextPaint 设置字体
mTextPaint.setTypeface(tf);

//刷新重绘
if (mLayout != null) {
nullLayouts();
requestLayout();
invalidate();
}
}
}

//重载方法二
public void setTypeface(@Nullable Typeface tf, @Typeface.Style int style) {
if (style > 0) {
if (tf == null) {
tf = Typeface.defaultFromStyle(style);
} else {
tf = Typeface.create(tf, style);
}
//调用重载方法一,设置字体
setTypeface(tf);
//经过一些算法
int typefaceStyle = tf != null ? tf.getStyle() : 0;
int need = style & ~typefaceStyle;
//打开画笔的粗体和斜体
mTextPaint.setFakeBoldText((need & Typeface.BOLD) != 0);
mTextPaint.setTextSkewX((need & Typeface.ITALIC) != 0 ? -0.25f : 0);
} else {
mTextPaint.setFakeBoldText(false);
mTextPaint.setTextSkewX(0);
setTypeface(tf);
}
}

分析下上述代码:


重载方法一:


TextView 设置字体实际上就是操作 mTextPaint,mTextPaint 是 TextPaint 的类对象,继承自 Paint 即画笔,因此我们设置的字体实际上会通过调用画笔的方法来进行绘制


重载方法二:


相对于重载方法一,法二多传递了一个 textStyle 参数,主要用来标记粗体和斜体的:


1)、如果设置了 textStyle ,进入第一个条件体,分情况:1、如果传进来的 tf 为 null ,则会根据传入的 style 去获取 Typeface 字体,2、如果不为 null ,则会根据传入的 tf 和 style 去获取 Typeface 字体。设置好字体后,接下来还会打开画笔的粗体和斜体设置


2)、如果没有设置 textStyle,则只会设置字体,并把画笔的粗斜体设置置为 false 和 0


从上述分析我们可以得知:TextView 设置字体和字体样式最终都是通过画笔来完成的


七、总结


本篇文章主要讲了:


1、Android 字体大概的一个介绍


2、关于影响 Android 字体显示的三个属性


3、textStyle,typeface,fontFamily 三者的一个关系


4、设置的这三个属性是怎么实现这些效果的?




好了,本篇文章到这里就结束了,如果有任何问题,欢迎给我留言,我们评论区一起讨论?


感谢你阅读这篇文章


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

【Kotlin篇】差异化分析,let,run,with,apply及also

作用域函数是Kotlin比较重要的一个特性,共分为以下5种:let、run、with、apply 以及 also,这五个函数的工作方式可以说非常相似,但是我们需要了解的是这5种函数的差异,以便在不同的场景更好的利用它。 读完这篇文章您将了解到: 什么是...
继续阅读 »

作用域函数是Kotlin比较重要的一个特性,共分为以下5种:letrunwithapply 以及 also,这五个函数的工作方式可以说非常相似,但是我们需要了解的是这5种函数的差异,以便在不同的场景更好的利用它。 读完这篇文章您将了解到:



  • 什么是Kotlin的作用域函数?

  • letrunwithapply 以及 also这5种作用域函数各自的角色定位;

  • 5种作用域函数的差异区分;

  • 何时何地使用这5种作用域?


Kotlin的作用域函数



Kotlin 标准库包含几个函数,它们的唯一目的是在对象的上下文中执行代码块。当对一个对象调用这样的函数并提供一个 lambda 表达式时,它会形成一个临时作用域。在此作用域中,可以访问该对象而无需其名称。这些函数称为作用域函数。



简单来说,作用域函数是为了方便对一个对象进行访问和操作,你可以对它进行空检查或者修改它的属性或者直接返回它的值等操作,下面提供了案例对作用域函数进行了详细说明。


角色定位


2.1 let


public inline fun <T, R> T.let(block: (T) -> R): R 

let函数是参数化类型 T 的扩展函数。在let块内可以通过 it 指代该对象。返回值为let块的最后一行或指定return表达式。


我们以一个Book对象为例,类中包含Book的name和price,如下:


class Book() {
var name = "《数据结构》"
var price = 60
fun displayInfo() = print("Book name : $name and price : $price")
}

fun main(args: Array<String>) {
val book = Book().let {
it.name = "《计算机网络》"
"This book is ${it.name}"
}
print(book)
}

控制台输出:
This book is 《计算机网络》

在上面案例中,我们对Book对象使用let作用域函数,在函数块的最后一句添加了一行字符串代码,并且对Book对象进行打印,我们可以看到最后控制台输出的结果为字符串“This book is 《计算机网络》”。


按照我们的编程思想,打印一个对象,输出必定是对象,但是使用let函数后,输出为最后一句字符串。这是由于let函数的特性导致。因为在Kotlin中,如果let块中的最后一条语句是非赋值语句,则默认情况下它是返回语句。


那如果我们将let块中最后一条语句修改为赋值语句,会发生什么变化?


fun main(args: Array<String>) {
val book = Book().let {
it.name = "《计算机网络》"
}
print(book)
}

控制台输出:
kotlin.Unit

可以看到我们将Book对象的name值进行了赋值操作,同样对Book对象进行打印,但是最后控制台的输出结果为“kotlin.Unit”,这是因为在let函数块的最后一句是赋值语句,print则将其当做是一个函数来看待。


这是let角色设定的第一点:1??



  • let块中的最后一条语句如果是非赋值语句,则默认情况下它是返回语句,反之,则返回的是一个 Unit类型


我们来看let第二点:2??



  • let可用于空安全检查。


如需对非空对象执行操作,可对其使用安全调用操作符 ?. 并调用 let 在 lambda 表达式中执行操作。如下案例:


var name: String? = null
fun main(args: Array<String>) {
val nameLength = name?.let {
it.length
} ?: "name为空时的值"
print(nameLength)
}

我们设置name为一个可空字符串,利用name?.let来进行空判断,只有当name不为空时,逻辑才能走进let函数块中。在这里,我们可能还看不出来let空判断的优势,但是当你有大量name的属性需要编写的时候,就能发现let的快速和简洁。


let第三点:3??



  • let可对调用链的结果进行操作。


关于这一点,官方教程给出了一个案例,在这里就直接使用:


fun main(args: Array<String>) { 
val numbers = mutableListOf("One","Two","Three","Four","Five")
val resultsList = numbers.map { it.length }.filter { it > 3 }
print(resultsList)
}

我们的目的是获取数组列表中长度大于3的值。因为我们必须打印结果,所以我们将结果存储在一个单独的变量中,然后打印它。但是使用“let”操作符,我们可以将代码修改为:


fun main(args: Array<String>) {
val numbers = mutableListOf("One","Two","Three","Four","Five")
numbers.map { it.length }.filter { it > 3 }.let {
print(it)
}
}

使用let后可以直接对数组列表中长度大于3的值进行打印,去掉了变量赋值这一步。


另外,let函数还存在一个特点。


let第四点:4??



  • let可以将“It”重命名为一个可读的lambda参数。


let是通过使用“It”关键字来引用对象的上下文,因此,这个“It”可以被重命名为一个可读的lambda参数,如下将it重命名为book


fun main(args: Array<String>) {
val book = Book().let {book ->
book.name = "《计算机网络》"
}
print(book)
}

2.2 run


run函数以“this”作为上下文对象,且它的调用方式与let一致。


另外,第一点:1?? 当 lambda 表达式同时包含对象初始化和返回值的计算时,run更适合


这句话是什么意思?我们还是用案例来说话:


fun main(args: Array<String>) {

Book().run {
name = "《计算机网络》"
price = 30
displayInfo()
}
}

控制台输出:
Book name : 《计算机网络》 and price : 30

如果不使用run函数,相同功能下代码会怎样?来看一看:


fun main(args: Array<String>) {

val book = Book()
book.name = "《计算机网络》"
book.price = 30
book.displayInfo()
}

控制台输出:
Book name : 《计算机网络》 and price : 30

输出结果还是一样,但是run函数所带来的代码简洁程度已经显而易见。


除此之外,让我们来看看run函数的其他优点:


通过查看源码,了解到run函数存在两种声明方式,


1、与let一样,run是作为T的扩展函数;


inline fun <T, R> T.run(block: T.() -> R): R 

2、第二个run的声明方式则不同,它不是扩展函数,并且块中也没有输入值,因此,它不是用于传递对象并更改属性的类型,而是可以使你在需要表达式的地方就可以执行一个语句。


inline fun <R> run(block: () -> R): R

如下利用run函数块执行方法,而不是作为一个扩展函数:


run {
val book = Book()
book.name = "《计算机网络》"
book.price = 30
book.displayInfo()
}

2.3 with


inline fun <T, R> with(receiver: T, block: T.() -> R): R 

with属于非扩展函数,直接输入一个对象receiver,当输入receiver后,便可以更改receiver的属性,同时,它也与run做着同样的事情。


还是提供一个案例说明:


fun main(args: Array<String>) {
val book = Book()

with(book) {
name = "《计算机网络》"
price = 40
}
print(book)
}

以上面为例,with(T)类型传入了一个参数book,则可以在with的代码块中访问book的name和price属性,并做更改。


with使用的是非null的对象,当函数块中不需要返回值时,可以使用with。


2.4 apply


inline fun <T> T.apply(block: T.() -> Unit): T

apply是 T 的扩展函数,与run函数有些相似,它将对象的上下文引用为“this”而不是“it”,并且提供空安全检查,不同的是,apply不接受函数块中的返回值,返回的是自己的T类型对象。


fun main(args: Array<String>) {
Book().apply {
name = "《计算机网络》"
price = 40

}
print(book)
}

控制台输出:
com.fuusy.kotlintest.Book@61bbe9ba

前面看到的 letwithrun 函数返回的值都是 R。但是,apply 和下面查看的 also 返回 T。例如,在 let 中,没有在函数块中返回的值,最终会成为 Unit 类型,但在 apply 中,最后返回对象本身 (T) 时,它成为 Book 类型。


apply函数主要用于初始化或更改对象,因为它用于在不使用对象的函数的情况下返回自身。


2.5 also


inline fun <T> T.also(block: (T) -> Unit): T 

also是 T 的扩展函数,返回值与apply一致,直接返回T。also函数的用法类似于let函数,将对象的上下文引用为“it”而不是“this”以及提供空安全检查方面


因为T作为block函数的输入,可以使用also来访问属性。所以,在不使用或不改变对象属性的情况下也使用also。


fun main(args: Array<String>) {
val book = Book().also {
it.name = "《计算机网络》"
it.price = 40
}
print(book)
}

控制台输出:
com.fuusy.kotlintest.Book@61bbe9ba

差异化


3.1 let & run



  • let将上下文对象引用为it ,而run引用为this;

  • run无法将“this”重命名为一个可读的lambda参数,而let可以将“it”重命名为一个可读的lambda参数。 在let多重嵌套时,就可以看到这个特点的优势所在。


3.2 with & run


with和run其实做的是同一种事情,对上下文对象都称之为“this”,但是他们又存在着不同,我们来看看案例。


先使用with函数:



fun main(args: Array<String>) {
val book: Book? = null
with(book){
this?.name = "《计算机网络》"
this?.price = 40
}
print(book)

}

我们创建了一个可空对象book,利用with函数对book对象的属性进行了修改。代码很直观,那么我们接着将with替换为run,代码更改为:


fun main(args: Array<String>) {
val book: Book? = null
book?.run{
name = "《计算机网络》"
price = 40
}
print(book)
}

首先run函数的调用省略了this引用,在外层就进行了空安全检查,只有非空时才能进入函数块内对book进行操作。



  • 相比较with来说,run函数更加简便,空安全检查也没有with那么频繁。


3.3 apply & let



  • apply不接受函数块中的返回值,返回的是自己的T类型对象,而let能返回。

  • apply上下文对象引用为“this”,let为“it”。


何时应该使用 apply、with、let、also 和 run ?



  • 用于初始化对象或更改对象属性,可使用apply

  • 如果将数据指派给接收对象的属性之前验证对象,可使用also

  • 如果将对象进行空检查并访问或修改其属性,可使用let

  • 如果是非null的对象并且当函数块中不需要返回值时,可使用with

  • 如果想要计算某个值,或者限制多个本地变量的范围,则使用run


总结


以上便是Kotlin作用域函数的作用以及使用场景,在Android实际开发中,5种函数使用的频次非常高,在使用过程中发现,当代码逻辑少的时候,作用域函数能带给我们代码的简洁性可读性,但是当逻辑复杂时,使用不同的函数,多次叠加都将降低可读性。这就要我们去区分它们各自的特点,以便在适合且复杂的场景下去使用它。


希望这篇文章能帮到您,感谢阅读。




作者:付十一
链接:https://juejin.cn/post/6975384870675546126
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

iOS开发中的小玩意儿-加速计和陀螺仪

前言最近因为工作需要对加速计和陀螺仪进行学习和了解,过程中有所收获。正文一、加速计iPhone在静止时会受到地球引力,以屏幕中心为坐标原点,建立一个三维坐标系(如右图),此时iPhone收到的地球引力会分布到三个轴上。iOS开发者可以通过CoreMotion框...
继续阅读 »

前言

最近因为工作需要对加速计和陀螺仪进行学习和了解,过程中有所收获。

正文

一、加速计

iPhone在静止时会受到地球引力,以屏幕中心为坐标原点,建立一个三维坐标系(如右图),此时iPhone收到的地球引力会分布到三个轴上。
iOS开发者可以通过CoreMotion框架获取分布到三个轴的值。如果iPhone是如图放置,则分布情况为x=0,y=-1.0,z=0。
在CoreMotion中地球引力(重力)的表示为1.0。

手机如果屏幕朝上的放在水平桌面上,此时的(x,y,z)分布是什么?


上面答案是(0,0, -1.0);

如何检测手机的运动?
CoreMotion框架中有CMDeviceMotion类,其中的gravity属性用来描述前面介绍的重力;另外的userAcceleration是用来描述手机的运动。
当手机不动时,userAcceleration的(x, y, z)为(0, 0, 0);
当手机运动,比如在屏幕水平朝上的自由落体时,检测到的(x, y, z)将为(0, 0, 1);
当手机屏幕水平朝上,往屏幕左边以9.8m/s2的加速度运动时,检测到的(x, y, z)将为(1, 0, 0);

1、gravity是固定不变,因为地球引力的不变;但是xyz的分布会变化,收到手机朝向的影响;
2、userAcceleration是手机的运动相关属性,但是检测到的值为运动加速度相反的方向;
3、一种理解加速计的方式:在水平的路上有一辆车,车上有一个人;当车加速向右运动时,人会向左倾斜;此时可以人不需要知道外面的环境如何,根据事先在车里建立好的方向坐标系,可以知道车在向右加速运动。

二、加速计的简单应用

图片悬浮
手机旋转,但是图片始终保持水平。


实现流程
1、加载图片,创建CMMotionManager;
2、监听地球重力的变化,根据x和y轴的重力变化计算出来手机与水平面的夹角;
3、将图片逆着旋转相同的角度;
x、y轴和UIKit坐标系相反,原点在屏幕中心,向上为y轴正方向,向右为x轴正方向,屏幕朝外是z轴正方向;
在处理图片旋转角度时需要注意。

三、陀螺仪

如图,建立三维坐标系;
陀螺仪描述的是iPhone关于x、y、z轴的旋转速率;
静止时(x, y, z)为(0, 0, 0);
当右图手机绕Y轴正方向旋转,速率为每秒180°,则(x, y, z)为(0, 0, 3.14);


陀螺仪和加速计是同样的坐标系,但是新增了旋转的概念,可以用右手法则来辅助记忆;
陀螺仪回调结构体的单位是以弧度为单位,这个不是加速度而是速率;

四、CoreMotion的使用
CoreMotion的使用有两种方式 :

1、Push方式:设置间隔,由manager不断回调;

self.motionManager = [[CMMotionManager alloc] init];
self.motionManager.deviceMotionUpdateInterval = 0.2;
[self.motionManager startDeviceMotionUpdatesToQueue:[NSOperationQueue mainQueue]
withHandler:^(CMDeviceMotion * _Nullable motion, NSError * _Nullable error) {

}];

2、Pull方式:启动监听,自定义定时器,不断读取manager的值;

self.motionManager = [[CMMotionManager alloc] init];
self.motionManager.deviceMotionUpdateInterval = 0.2;
[self.motionManager startDeviceMotionUpdates];
// self.motionManager.deviceMotion 后续通过这个属性可以直接读取结果

iOS系统在监听到运动信息的时候,需要把信息回调给开发者,方式就有push和pull两种;
push 是系统在规定的时间间隔,不断的回调;
pull 是由开发则自己去读取结果值,但同样需要设定一个更新频率;
两种方式的本质并无太大区别,都需要设置回调间隔,只是读取方式的不同;
在不使用之后(比如说切后台)要关闭更新,这是非常耗电量的操作。

五、demo实践

基于加速计,做了一个小游戏,逻辑不复杂详见具体代码,分享几个处理逻辑:

1、圆球的边界处理;(以球和右边界的碰撞为例)

if (self.ballView.right > self.gameContainerView.width) {
self.ballView.right = self.gameContainerView.width;
self.ballSpeedX /= -1;
}

2、圆球是否触碰目标的检测;

- (BOOL)checkTarget {
CGFloat disX = (self.ballView.centerX - self.targetView.centerX);
CGFloat disY = (self.ballView.centerY - self.targetView.centerY);
return sqrt(disX * disX + disY * disY) <= (kConstBallLength / 2 + kConstTargetLength / 2);
}

3、速度的平滑处理;

static CGFloat lySlowLowPassFilter(NSTimeInterval elapsed,
GLfloat target,
GLfloat current) {
return current + (4.0 * elapsed * (target - current));
}


总结

加速计和陀螺仪的原理复杂但使用简单,实际应用也比较广。
之前就用过加速计和陀螺仪,但是没有系统的学习过。在完整的学习一遍之后,我才知道原来加速计的单位是以重力加速度(9.8 m/s2)为标准单位,陀螺仪的数据仅仅是速率,单位是弧度每秒。
上面的小游戏代码地址在Github

链接:https://www.jianshu.com/p/6d6b213912f5

收起阅读 »

当前端基建任务落到你身上,该如何推动协作?

前言 作为一名野生的前端开发,自打本猿入行起,就未经过什么系统的学习,待过的团队也是大大小小没个准儿: 要么大牛带队,但是后端大牛。要么临时凑的团队,受制于从前,前端不自由。要么从0到项目部署,都是为了敏捷而敏捷,颇不规范。 话虽如此,经过4年生涯摧残的废猿...
继续阅读 »

前言


作为一名野生的前端开发,自打本猿入行起,就未经过什么系统的学习,待过的团队也是大大小小没个准儿:


要么大牛带队,但是后端大牛。
要么临时凑的团队,受制于从前,前端不自由。
要么从0到项目部署,都是为了敏捷而敏捷,颇不规范。


话虽如此,经过4年生涯摧残的废猿我,也是有自己的一番心得体会的。


1. 从DevOps流程看前端基建



很多专注于切图的萌新前端看到这张图是蒙圈的:


DevOps是什么?这些工具都是啥?我在哪?


很多前端在接触到什么前端工程化,什么持续构建/集成相关知识时就犯怂。也有觉得这与业务开发无关,不必理会。


但是往长远想,切图是不可能一辈子切图的,你业务再怎么厉害,前端代码再如何牛,没有了后端运维测试大佬们相助,一个完整的软件生产周期就没法走完。


成为一名全栈很难,更别说全链路开发者了。


言归正传,当你进入一个新团队,前端从0开始,怎样从DevOps的角度去提高团队效能呢?



一套简易的DevOps流程包含了协作、构建、测试、部署、运行。


而前端常说的开发规范、代码管理、测试、构建部署以及工程化其实都是在这一整个体系中。


当然,中小团队想玩好DevOps整套流程,需要的时间与研发成本,不比开发项目少。


DevOps核心思想就是:“快速交付价值,灵活响应变化”。其基本原则如下:


高效的协作和沟通;
自动化流程和工具;
快速敏捷的开发;
持续交付和部署;
不断学习和创新。


接下来我将从协作、构建、测试、部署、运行五个方面谈谈,如何快速打造用于中小团队的前端基建。


2. 在团队内/外促进协作


前端基建协作方面可以写的东西太多了,暂且粗略分为:团队内 与 团队外。



以下可能是前端们都能遇到的问题:


成员间水平各异,编写代码的风格各不相同,项目间难以统一管理。
不同项目Webpack配置差异过大,基础工具函数库和请求封装不一样。
项目结构与技术栈上下横跳,明明是同一UI风格,基础组件没法复用,全靠复制粘贴。
代码没注释,项目没文档,新人难以接手,旧项目无法维护。


三层代码规范约束



  • 第一层,ESLint


常见的ESLint风格有:airbnb,google,standard


在多个项目间,规则不应左右横跳,如果项目周期紧张,可以适当放宽规则,让warning类弱警告可以通过。且一般建议成员的IDE和插件要统一,将客观因素影响降到最低。



  • 第二层,Git Hooks


git 自身包含许多 hooks,在 commitpushgit 事件前后触发执行。


husky能够防止不规范代码被commitpushmerge等等。


代码提交不规范,全组部署两行泪。


npm install husky pre-commit  --save-dev


拿我以前的项目为例子:


// package.json
"scripts": {
// ...
"lint": "node_modules/.bin/eslint '**/*.{js,jsx}' && node_modules/.bin/stylelint '**/*.{css,scss}'",
"lint:fix": "node_modules/.bin/eslint '**/*.{js,jsx}' --fix && node_modules/.bin/stylelint '**/*.{css,scss}' --fix"
},
"husky": {
"hooks": {
"pre-commit": "npm run lint",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
},


通过简单的安装配置,无论你通过命令行还是Sourcetree提交代码,都需要通过严格的校验。



建议在根目录README.md注明提交规范:


## Git 规范

使用 [commitlint](https://github.com/conventional-changelog/commitlint
) 工具,常用有以下几种类型:

-
feat :新功能
- fix :修复 bug
- chore :对构建或者辅助工具的更改
- refactor :既不是修复 bug 也不是添加新功能的代码更改
- style :不影响代码含义的更改 (例如空格、格式化、少了分号)
- docs : 只是文档的更改
- perf :提高性能的代码更改
- revert :撤回提交
- test :添加或修正测试

举例
git commit -m 'feat: add list'



  • 第三层,CI(持续集成)。



《前端代码规范最佳实践》



前两步的校验可以手动跳过(找骂),但CI中的校验是绝对绕不过的,因为它在服务端校验。使用 gitlab CI 做持续集成,配置文件 .gitlab-ci.yaml 如下所示:


lint:
stage:lint
only:
-/^feature\/.*$/
script:
-npmlint


这层校验,一般在稍大点的企业中,会由运维部的配置组完成。



统一前端物料


公共组件、公共UI、工具函数库、第三方sdk等该如何规范?


如何快速封装部门UI组件库?

  • 将业务从公共组件中抽离出来。

  • 在项目中安装StoryBook(多项目时另起)

  • 按官方文档标准,创建stories,并设定参数(同时也建议先写Jest测试脚本),写上必要的注释。

  • 为不同组件配置StoryBook控件,最后部署。

如何统一部门所用的工具函数库和第三方sdk


其实这里更多的是沟通的问题,首先需要明确的几点:



  • 部门内对约定俗成的工具库要有提前沟通,不能这头装一个MomentJs,另一头又装了DayJS。一般的原则是:轻量的自己写,超过可接受大小的找替代,譬如:DayJS替代MomentJsImmerJS替代immutableJS等。

  • 部门间的有登录机制,请求库封装协议等。如果是SSO/扫码登录等,就协定只用一套,不允许后端随意变动。如果是请求库封装,就必须要后端统一Restful风格,相信我,不用Restful规范的团队都是灾难。前端联调会生不如死。

  • Mock方式、路由管理以及样式写法也应当统一。


在团队外促进协作


核心原则就是:“能用文档解决的就尽量别BB。”


虽说现今前端的地位愈发重要,但我们经常在项目开发中遇到以下问题:


不同的后端接口规范不一样,前端需要耗费大量时间去做数据清洗兼容。
前端静态页开发完了,后端迟迟不给接口,因为没有接口文档,天天都得问。
测试反馈的问题,在原型上没有体现。


首先是原型方面:

  • 一定要看明白产品给的原型文档!!!多问多沟通,这太重要了。

  • 好的产品一般都会提供项目流程详图,但前端还是需要基于实际,做一张页面流程图。

  • 要产品提供具体字段类型相关定义,不然得和后端扯皮。。。

其次是后端:

执行Restful接口规范,不符合规范的接口驳回。

劝退师就经历过,前东家有个JAVA架构师,连跨域和Restful都不知道,定的规范不成规范,一个简单查询接口返回五六级,其美名曰:“结构化数据”

遇到这种沉浸于自己世界不听劝的后端,我只有一句劝:要么把他搞走,要么跑路吧

必要的接口文档站点与API测试(如SwaggerApidoc),不接受文件传输形式的接口

早期的联调都是通过呐喊告知对方接口的标准。刚开始有什么不清楚的直接问就好了,但是到了后面的时候连写接口代码的那个人都忘了这接口怎么用,维护成本巨高

在没有接口文档站点出现前,接口文档以word文档出现,辅以postmanhttpcurl等工具去测试。但仍然不够直观,维护起来也难

以web交互为主的Swagger解决了测试,维护以及实时性的问题。从一定程度上也避免了扯皮问题:只有你后端没更新文档,这联调滞后时间就不该由前端担起。

最后是运维方面:

除了CI/CD相关的,其实很可以和运维一起写写nginx和插件开发。

效率沟通工具


可能大家比较习惯的是使用QQ或者微信去传输文件,日常沟通还行,就是对开发者不太友好。


如何是跨国家沟通,一般都是建议jira+slack的组合,但这两个工具稍微有些水土不服。


这四个工具随意选择都不会有太大问题。


链接:https://juejin.cn/post/6844904145602740231

收起阅读 »

手把手带你入门Webpack Plugin

关于 Webpack 在讲 Plugin 之前,我们先来了解下 Webpack。本质上,Webpack 是一个用于现代 JavaScript 应用程序的静态模块打包工具。它能够解析我们的代码,生成对应的依赖关系,然后将不同的模块达成一个或多个 bundle。 ...
继续阅读 »

关于 Webpack


在讲 Plugin 之前,我们先来了解下 Webpack。本质上,Webpack 是一个用于现代 JavaScript 应用程序的静态模块打包工具。它能够解析我们的代码,生成对应的依赖关系,然后将不同的模块达成一个或多个 bundle。



Webpack 的基本概念包括了如下内容:



  1. Entry:Webpack 的入口文件,指的是应该从哪个模块作为入口,来构建内部依赖图。

  2. Output:告诉 Webpack 在哪输出它所创建的 bundle 文件,以及输出的 bundle 文件该如何命名、输出到哪个路径下等规则。

  3. Loader:模块代码转化器,使得 Webpack 有能力去处理除了 JS、JSON 以外的其他类型的文件。

  4. Plugin:Plugin 提供执行更广的任务的功能,包括:打包优化,资源管理,注入环境变量等。

  5. Mode:根据不同运行环境执行不同优化参数时的必要参数。

  6. Browser Compatibility:支持所有 ES5 标准的浏览器(IE8 以上)。


了解完 Webpack 的基本概念之后,我们再来看下,为什么我们会需要 Plugin。


Plugin 的作用


我先举一个我们政采云内部的案例:


在 React 项目中,一般我们的 Router 文件是写在一个项目中的,如果项目中包含了许多页面,不免会出现所有业务模块 Router 耦合的情况,所以我们开发了一个 Plugin,在构建打包时,该 Plugin 会读取所有文件夹下的 index.js 文件,再合并到一起形成一个统一的 Router 文件,轻松解决业务耦合问题。这就是 Plugin 的应用(具体实现会在最后一小节说明)。


来看一下我们合成前项目代码结构:


├── package.json
├── README.md
├── zoo.config.js
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .stylelintrc
├── buildWebpack 配置目录)
│ └── webpack.dev.conf.js
├── src
│ ├── index.hbs
│ ├── main.js (入口文件)
│ ├── common (通用模块,包权限,统一报错拦截等)
│ └── ...
│ ├── components (项目公共组件)
│ └── ...
│ ├── layouts (项目顶通)
│ └── ...
│ ├── utils (公共类)
│ └── ...
│ ├── routes (页面路由)
│ │ ├── Hello (对应 Hello 页面的代码)
│ │ │ ├── config (页面配置信息)
│ │ │ └── ...
│ │ │ ├── modelsdva数据中心)
│ │ │ └── ...
│ │ │ ├── services (请求相关接口定义)
│ │ │ └── ...
│ │ │ ├── views (请求相关接口定义)
│ │ │ └── ...
│ │ │ └── index.jsrouter定义的路由信息)
├── .eslintignore
├── .eslintrc
├── .gitignore
└── .stylelintrc


再看一下经过 Plugin 合成 Router 之后的结构:


├── package.json
├── README.md
├── zoo.config.js
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .stylelintrc
├── buildWebpack 配置目录)
│ └── webpack.dev.conf.js
├── src
│ ├── index.hbs
│ ├── main.js (入口文件)
│ ├── router-config.js (合成后的router文件)
│ ├── common (通用模块,包权限,统一报错拦截等)
│ └── ...
│ ├── components (项目公共组件)
│ └── ...
│ ├── layouts (项目顶通)
│ └── ...
│ ├── utils (公共类)
│ └── ...
│ ├── routes (页面路由)
│ │ ├── Hello (对应 Hello 页面的代码)
│ │ │ ├── config (页面配置信息)
│ │ │ └── ...
│ │ │ ├── modelsdva数据中心)
│ │ │ └── ...
│ │ │ ├── services (请求相关接口定义)
│ │ │ └── ...
│ │ │ ├── views (请求相关接口定义)
│ │ │ └── ...
├── .eslintignore
├── .eslintrc
├── .gitignore
└── .stylelintrc


总结来说 Plugin 的作用如下:



  1. 提供了 Loader 无法解决的一些其他事情

  2. 提供强大的扩展方法,能执行更广的任务


了解完 Plugin 的大致作用之后,我们来聊一聊如何创建一个 Plugin。


创建一个 Plugin


Hook


在聊创建 Plugin 之前,我们先来聊一下什么是 Hook。


Webpack 在编译的过程中会触发一系列流程,而在这样一连串的流程中,Webpack 把一些关键的流程节点暴露出来供开发者使用,这就是 Hook,可以类比 React 的生命周期钩子。


Plugin 就是在这些 Hook 上暴露出方法供开发者做一些额外操作,在写 Plugin 的时候,也需要先了解我们应该在哪个 Hook 上做操作。


如何创建 Plugin


我们先来看一下 Webpack 官方给的案例:


const pluginName = 'ConsoleLogOnBuildWebpackPlugin';

class ConsoleLogOnBuildWebpackPlugin {
apply(compiler) {
// 代表开始读取 records 之前执行
compiler.hooks.run.tap(pluginName, compilation => {
console.log("webpack 构建过程开始!");
});
}
}


从上面的代码我们可以总结如下内容:



  • Plugin 其实就是一个类。

  • 类需要一个 apply 方法,执行具体的插件方法。

  • 插件方法做了一件事情就是在 run 这个 Hook 上注册了一个同步的打印日志的方法。

  • apply 方法的入参注入了一个 compiler 实例,compiler 实例是 Webpack 的支柱引擎,代表了 CLI 和 Node API 传递的所有配置项。

  • Hook 回调方法注入了 compilation 实例,compilation 能够访问当前构建时的模块和相应的依赖。


Compiler 对象包含了 Webpack 环境所有的的配置信息,包含 options,loaders,plugins 这些信息,这个对象在 Webpack 启动时候被实例化,它是全局唯一的,可以简单地把它理解为 Webpack 实例;

Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。当 Webpack 以开发模式运行时,每当检测到一个文件变化,一次新的 Compilation 将被创建。Compilation 对象也提供了很多事件回调供插件做扩展。通过 Compilation 也能读取到 Compiler 对象。
—— 摘自「深入浅出 Webpack」



  • compiler 实例和 compilation 实例上分别定义了许多 Hooks,可以通过 实例.hooks.具体Hook 访问,Hook 上还暴露了 3 个方法供使用,分别是 tap、tapAsync 和 tapPromise。这三个方法用于定义如何执行 Hook,比如 tap 表示注册同步 Hook,tapAsync 代表 callback 方式注册异步 hook,而 tapPromise 代表 Promise 方式注册异步 Hook,可以看下 Webpack 中关于这三种类型实现的源码,为方便阅读,我加了些注释。


// tap方法的type是sync,tapAsync方法的type是async,tapPromise方法的type是promise
// 源码取自Hook工厂方法:lib/HookCodeFactory.js
create(options) {
this.init(options);
let fn;
// Webpack 通过new Function 生成函数
switch (this.options.type) {
case "sync":
fn = new Function(
this.args(), // 生成函数入参
'"use strict";\n' +
this.header() + // 公共方法,生成一些需要定义的变量
this.contentWithInterceptors({ // 生成实际执行的代码的方法
onError: err => `throw ${err};\n`, // 错误回调
onResult: result => `return ${result};\n`, // 得到值的时候的回调
resultReturns: true,
onDone: () => "",
rethrowIfPossible: true
})
);
break;
case "async":
fn = new Function(
this.args({
after: "_callback"
}),
'"use strict";\n' +
this.header() + // 公共方法,生成一些需要定义的变量
this.contentWithInterceptors({
onError: err => `_callback(${err});\n`, // 错误时执行回调方法
onResult: result => `_callback(null, ${result});\n`, // 得到结果时执行回调方法
onDone: () => "_callback();\n" // 无结果,执行完成时
})
);
break;
case "promise":
let errorHelperUsed = false;
const content = this.contentWithInterceptors({
onError: err => {
errorHelperUsed = true;
return `_error(${err});\n`;
},
onResult: result => `_resolve(${result});\n`,
onDone: () => "_resolve();\n"
});
let code = "";
code += '"use strict";\n';
code += this.header(); // 公共方法,生成一些需要定义的变量
code += "return new Promise((function(_resolve, _reject) {\n"; // 返回的是 Promise
if (errorHelperUsed) {
code += "var _sync = true;\n";
code += "function _error(_err) {\n";
code += "if(_sync)\n";
code +=
"_resolve(Promise.resolve().then((function() { throw _err; })));\n";
code += "else\n";
code += "_reject(_err);\n";
code += "};\n";
}
code += content; // 判断具体执行_resolve方法还是执行_error方法
if (errorHelperUsed) {
code += "_sync = false;\n";
}
code += "}));\n";
fn = new Function(this.args(), code);
break;
}
this.deinit(); // 清空 options 和 _args
return fn;
}


Webpack 共提供了以下十种 Hooks,代码中所有具体的 Hook 都是以下这 10 种中的一种。


// 源码取自:lib/index.js
"use strict";

exports.__esModule = true;
// 同步执行的钩子,不能处理异步任务
exports.SyncHook = require("./SyncHook");
// 同步执行的钩子,返回非空时,阻止向下执行
exports.SyncBailHook = require("./SyncBailHook");
// 同步执行的钩子,支持将返回值透传到下一个钩子中
exports.SyncWaterfallHook = require("./SyncWaterfallHook");
// 同步执行的钩子,支持将返回值透传到下一个钩子中,返回非空时,重复执行
exports.SyncLoopHook = require("./SyncLoopHook");
// 异步并行的钩子
exports.AsyncParallelHook = require("./AsyncParallelHook");
// 异步并行的钩子,返回非空时,阻止向下执行,直接执行回调
exports.AsyncParallelBailHook = require("./AsyncParallelBailHook");
// 异步串行的钩子
exports.AsyncSeriesHook = require("./AsyncSeriesHook");
// 异步串行的钩子,返回非空时,阻止向下执行,直接执行回调
exports.AsyncSeriesBailHook = require("./AsyncSeriesBailHook");
// 支持异步串行 && 并行的钩子,返回非空时,重复执行
exports.AsyncSeriesLoopHook = require("./AsyncSeriesLoopHook");
// 异步串行的钩子,下一步依赖上一步返回的值
exports.AsyncSeriesWaterfallHook = require("./AsyncSeriesWaterfallHook");
// 以下 2 个是 hook 工具类,分别用于 hooks 映射以及 hooks 重定向
exports.HookMap = require("./HookMap");
exports.MultiHook = require("./MultiHook");


举几个简单的例子:



  • 上面官方案例中的 run 这个 Hook,会在开始读取 records 之前执行,它的类型是 AsyncSeriesHook,查看源码可以发现,run Hook 既可以执行同步的 tap 方法,也可以执行异步的 tapAsync 和 tapPromise 方法,所以以下写法也是可以的:


const pluginName = 'ConsoleLogOnBuildWebpackPlugin';

class ConsoleLogOnBuildWebpackPlugin {
apply(compiler) {
compiler.hooks.run.tapAsync(pluginName, (compilation, callback) => {
setTimeout(() => {
console.log("webpack 构建过程开始!");
callback(); // callback 方法为了让构建继续执行下去,必须要调用
}, 1000);
});
}
}



  • 再举一个例子,比如 failed 这个 Hook,会在编译失败之后执行,它的类型是 SyncHook,查看源码可以发现,调用 tapAsync 和 tapPromise 方法时,会直接抛错。


对于一些同步的方法,推荐直接使用 tap 进行注册方法,对于异步的方案,tapAsync 通过执行 callback 方法实现回调,如果执行的方法返回的是一个 Promise,推荐使用 tapPromise 进行方法的注册


Hook 的类型可以通过官方 API 查询,地址传送门


// 源码取自:lib/SyncHook.js
const TAP_ASYNC = () => {
throw new Error("tapAsync is not supported on a SyncHook");
};

const TAP_PROMISE = () => {
throw new Error("tapPromise is not supported on a SyncHook");
};

function SyncHook(args = [], name = undefined) {
const hook = new Hook(args, name);
hook.constructor = SyncHook;
hook.tapAsync = TAP_ASYNC;
hook.tapPromise = TAP_PROMISE;
hook.compile = COMPILE;
return hook;
}


讲解完具体的执行方法之后,我们再聊一下 Webpack 流程以及 Tapable 是什么。


Webpack && Tapable


Webpack 运行机制


要理解 Plugin,我们先大致了解 Webpack 打包的流程



  1. 我们打包的时候,会先合并 Webpack config 文件和命令行参数,合并为 options。

  2. 将 options 传入 Compiler 构造方法,生成 compiler 实例,并实例化了 Compiler 上的 Hooks。

  3. compiler 对象执行 run 方法,并自动触发 beforeRun、run、beforeCompile、compile 等关键 Hooks。

  4. 调用 Compilation 构造方法创建 compilation 对象,compilation 负责管理所有模块和对应的依赖,创建完成后触发 make Hook。

  5. 执行 compilation.addEntry() 方法,addEntry 用于分析所有入口文件,逐级递归解析,调用 NormalModuleFactory 方法,为每个依赖生成一个 Module 实例,并在执行过程中触发 beforeResolve、resolver、afterResolve、module 等关键 Hooks。

  6. 将第 5 步中生成的 Module 实例作为入参,执行 Compilation.addModule() 和 Compilation.buildModule() 方法递归创建模块对象和依赖模块对象。

  7. 调用 seal 方法生成代码,整理输出主文件和 chunk,并最终输出。



Tapable


Tapable 是 Webpack 核心工具库,它提供了所有 Hook 的抽象类定义,Webpack 许多对象都是继承自 Tapable 类。比如上面说的 tap、tapAsync 和 tapPromise 都是通过 Tapable 进行暴露的。源码如下(截取了部分代码):


// 第二节 “创建一个 Plugin” 中说的 10 种 Hooks 都是继承了这两个类
// 源码取自:tapable.d.ts
declare class Hook {
tap(options: string | Tap & IfSet, fn: (...args: AsArray) => R): void;
}

declare class AsyncHook extends Hook {
tapAsync(
options: string | Tap & IfSet,
fn: (...args: Append, InnerCallback>) => void
): void;
tapPromise(
options: string | Tap & IfSet,
fn: (...args: AsArray) => Promise
): void;
}



常见 Hooks API


可以参考 Webpack


本文列举一些常用 Hooks 和其对应的类型:


Compiler Hooks
































Hooktype调用
runAsyncSeriesHook开始读取 records 之前
compileSyncHook一个新的编译 (compilation) 创建之后
emitAsyncSeriesHook生成资源到 output 目录之前
doneSyncHook编译 (compilation) 完成

Compilation Hooks



























Hooktype调用
buildModuleSyncHook在模块构建开始之前触发
finishModulesSyncHook所有模块都完成构建
optimizeSyncHook优化阶段开始时触发

Plugin 在项目中的应用


讲完这么多理论知识,接下来我们来看一下 Plugin 在项目中的实战:如何将各个子模块中的 router 文件合并到 router-config.js 中。


背景:


在 React 项目中,一般我们的 Router 文件是写在一个项目中的,如果项目中包含了许多页面,不免会出现所有业务模块 Router 耦合的情况,所以我们开发了一个 Plugin,在构建打包时,该 Plugin 会读取所有文件夹下的 Router 文件,再合并到一起形成一个统一的 Router Config 文件,轻松解决业务耦合问题。这就是 Plugin 的应用。


实现:


const fs = require('fs');
const path = require('path');
const _ = require('lodash');

function resolve(dir) {
return path.join(__dirname, '..', dir);
}

function MegerRouterPlugin(options) {
// options是配置文件,你可以在这里进行一些与options相关的工作
}

MegerRouterPlugin.prototype.apply = function (compiler) {
// 注册 before-compile 钩子,触发文件合并
compiler.plugin('before-compile', (compilation, callback) => {
// 最终生成的文件数据
const data = {};
const routesPath = resolve('src/routes');
const targetFile = resolve('src/router-config.js');
// 获取路径下所有的文件和文件夹
const dirs = fs.readdirSync(routesPath);
try {
dirs.forEach((dir) => {
const routePath = resolve(`src/routes/${dir}`);
// 判断是否是文件夹
if (!fs.statSync(routePath).isDirectory()) {
return true;
}
delete require.cache[`${routePath}/index.js`];
const routeInfo = require(routePath);
// 多个 view 的情况下,遍历生成router信息
if (!_.isArray(routeInfo)) {
generate(routeInfo, dir, data);
// 单个 view 的情况下,直接生成
} else {
routeInfo.map((config) => {
generate(config, dir, data);
});
}
});
} catch (e) {
console.log(e);
}

// 如果 router-config.js 存在,判断文件数据是否相同,不同删除文件后再生成
if (fs.existsSync(targetFile)) {
delete require.cache[targetFile];
const targetData = require(targetFile);
if (!_.isEqual(targetData, data)) {
writeFile(targetFile, data);
}
// 如果 router-config.js 不存在,直接生成文件
} else {
writeFile(targetFile, data);
}

// 最后调用 callback,继续执行 webpack 打包
callback();
});
};
// 合并当前文件夹下的router数据,并输出到 data 对象中
function generate(config, dir, data) {
// 合并 router
mergeConfig(config, dir, data);
// 合并子 router
getChildRoutes(config.childRoutes, dir, data, config.url);
}
// 合并 router 数据到 targetData 中
function mergeConfig(config, dir, targetData) {
const { view, models, extraModels, url, childRoutes, ...rest } = config;
// 获取 models,并去除 src 字段
const dirModels = getModels(`src/routes/${dir}/models`, models);
const data = {
...rest,
};
// view 拼接到 path 字段
data.path = `${dir}/views${view ? `/${view}` : ''}`;
// 如果有 extraModels,就拼接到 models 对象上
if (dirModels.length || (extraModels && extraModels.length)) {
data.models = mergerExtraModels(config, dirModels);
}
Object.assign(targetData, {
[url]: data,
});
}
// 拼接 dva models
function getModels(modelsDir, models) {
if (!fs.existsSync(modelsDir)) {
return [];
}
let files = fs.readdirSync(modelsDir);
// 必须要以 js 或者 jsx 结尾
files = files.filter((item) => {
return /\.jsx?$/.test(item);
});
// 如果没有定义 models ,默认取 index.js
if (!models || !models.length) {
if (files.indexOf('index.js') > -1) {
// 去除 src
return [`${modelsDir.replace('src/', '')}/index.js`];
}
return [];
}
return models.map((item) => {
if (files.indexOf(`${item}.js`) > -1) {
// 去除 src
return `${modelsDir.replace('src/', '')}/${item}.js`;
}
});
}
// 合并 extra models
function mergerExtraModels(config, models) {
return models.concat(config.extraModels ? config.extraModels : []);
}
// 合并子 router
function getChildRoutes(childRoutes, dir, targetData, oUrl) {
if (!childRoutes) {
return;
}
childRoutes.map((option) => {
option.url = oUrl + option.url;
if (option.childRoutes) {
// 递归合并子 router
getChildRoutes(option.childRoutes, dir, targetData, option.url);
}
mergeConfig(option, dir, targetData);
});
}

// 写文件
function writeFile(targetFile, data) {
fs.writeFileSync(targetFile, `module.exports = ${JSON.stringify(data, null, 2)}`, 'utf-8');
}

module.exports = MegerRouterPlugin;



结果:


合并前的文件:


module.exports = [
{
url: '/category/protocol',
view: 'protocol',
},
{
url: '/category/sync',
models: ['sync'],
view: 'sync',
},
{
url: '/category/list',
models: ['category', 'config', 'attributes', 'group', 'otherSet', 'collaboration'],
view: 'categoryRefactor',
},
{
url: '/category/conversion',
models: ['conversion'],
view: 'conversion',
},
];



合并后的文件:


module.exports = {
"/category/protocol": {
"path": "Category/views/protocol"
},
"/category/sync": {
"path": "Category/views/sync",
"models": [
"routes/Category/models/sync.js"
]
},
"/category/list": {
"path": "Category/views/categoryRefactor",
"models": [
"routes/Category/models/category.js",
"routes/Category/models/config.js",
"routes/Category/models/attributes.js",
"routes/Category/models/group.js",
"routes/Category/models/otherSet.js",
"routes/Category/models/collaboration.js"
]
},
"/category/conversion": {
"path": "Category/views/conversion",
"models": [
"routes/Category/models/conversion.js"
]
},
}



最终项目就会生成 router-config.js 文件



结尾


希望大家看完本章之后,对 Webpack Plugin 有一个初步的认识,能够上手写一个自己的 Plugin 来应用到自己的项目中。


文章中如有不对的地方,欢迎指正。


链接:https://juejin.cn/post/6968988552075952141

收起阅读 »

当面试官问Webpack的时候他想知道什么

前言 在前端工程化日趋复杂的今天,模块打包工具在我们的开发中起到了越来越重要的作用,其中webpack就是最热门的打包工具之一。 说到webpack,可能很多小伙伴会觉得既熟悉又陌生,熟悉是因为几乎在每一个项目中我们都会用上它,又因为webpack复杂的配置和...
继续阅读 »

前言


在前端工程化日趋复杂的今天,模块打包工具在我们的开发中起到了越来越重要的作用,其中webpack就是最热门的打包工具之一。


说到webpack,可能很多小伙伴会觉得既熟悉又陌生,熟悉是因为几乎在每一个项目中我们都会用上它,又因为webpack复杂的配置和五花八门的功能感到陌生。尤其当我们使用诸如umi.js之类的应用框架还帮我们把webpack配置再封装一层的时候,webpack的本质似乎离我们更加遥远和深不可测了。


当面试官问你是否了解webpack的时候,或许你可以说出一串耳熟能详的webpack loaderplugin的名字,甚至还能说出插件和一系列配置做按需加载和打包优化,那你是否了解他的运行机制以及实现原理呢,那我们今天就一起探索webpack的能力边界,尝试了解webpack的一些实现流程和原理,拒做API工程师。


CgqCHl6pSFmAC5UzAAEwx63IBwE024.png


你知道webpack的作用是什么吗?


从官网上的描述我们其实不难理解,webpack的作用其实有以下几点:




  • 模块打包。可以将不同模块的文件打包整合在一起,并且保证它们之间的引用正确,执行有序。利用打包我们就可以在开发的时候根据我们自己的业务自由划分文件模块,保证项目结构的清晰和可读性。




  • 编译兼容。在前端的“上古时期”,手写一堆浏览器兼容代码一直是令前端工程师头皮发麻的事情,而在今天这个问题被大大的弱化了,通过webpackLoader机制,不仅仅可以帮助我们对代码做polyfill,还可以编译转换诸如.less, .vue, .jsx这类在浏览器无法识别的格式文件,让我们在开发的时候可以使用新特性和新语法做开发,提高开发效率。




  • 能力扩展。通过webpackPlugin机制,我们在实现模块化打包和编译兼容的基础上,可以进一步实现诸如按需加载,代码压缩等一系列功能,帮助我们进一步提高自动化程度,工程效率以及打包输出的质量。




说一下模块打包运行原理?


如果面试官问你Webpack是如何把这些模块合并到一起,并且保证其正常工作的,你是否了解呢?


首先我们应该简单了解一下webpack的整个打包流程:



  • 1、读取webpack的配置参数;

  • 2、启动webpack,创建Compiler对象并开始解析项目;

  • 3、从入口文件(entry)开始解析,并且找到其导入的依赖模块,递归遍历分析,形成依赖关系树;

  • 4、对不同文件类型的依赖模块文件使用对应的Loader进行编译,最终转为Javascript文件;

  • 5、整个过程中webpack会通过发布订阅模式,向外抛出一些hooks,而webpack的插件即可通过监听这些关键的事件节点,执行插件任务进而达到干预输出结果的目的。


其中文件的解析与构建是一个比较复杂的过程,在webpack源码中主要依赖于compilercompilation两个核心对象实现。


compiler对象是一个全局单例,他负责把控整个webpack打包的构建流程。
compilation对象是每一次构建的上下文对象,它包含了当次构建所需要的所有信息,每次热更新和重新构建,compiler都会重新生成一个新的compilation对象,负责此次更新的构建过程。


而每个模块间的依赖关系,则依赖于AST语法树。每个模块文件在通过Loader解析完成之后,会通过acorn库生成模块代码的AST语法树,通过语法树就可以分析这个模块是否还有依赖的模块,进而继续循环执行下一个模块的编译解析。


最终Webpack打包出来的bundle文件是一个IIFE的执行函数。


// webpack 5 打包的bundle文件内容

(() => { // webpackBootstrap
var __webpack_modules__ = ({
'file-A-path': ((modules) => { // ... })
'index-file-path': ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => { // ... })
})

// The module cache
var __webpack_module_cache__ = {};

// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// Create a new module (and put it into the cache)
var module = __webpack_module_cache__[moduleId] = {
// no module.id needed
// no module.loaded needed
exports: {}
};

// Execute the module function
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);

// Return the exports of the module
return module.exports;
}

// startup
// Load entry module and return exports
// This entry module can't be inlined because the eval devtool is used.
var __webpack_exports__ = __webpack_require__("./src/index.js");
})


webpack4相比,webpack5打包出来的bundle做了相当的精简。在上面的打包demo中,整个立即执行函数里边只有三个变量和一个函数方法,__webpack_modules__存放了编译后的各个文件模块的JS内容,__webpack_module_cache__ 用来做模块缓存,__webpack_require__Webpack内部实现的一套依赖引入函数。最后一句则是代码运行的起点,从入口文件开始,启动整个项目。


其中值得一提的是__webpack_require__模块引入函数,我们在模块化开发的时候,通常会使用ES Module或者CommonJS规范导出/引入依赖模块,webpack打包编译的时候,会统一替换成自己的__webpack_require__来实现模块的引入和导出,从而实现模块缓存机制,以及抹平不同模块规范之间的一些差异性。


你知道sourceMap是什么吗?


提到sourceMap,很多小伙伴可能会立刻想到Webpack配置里边的devtool参数,以及对应的evaleval-cheap-source-map等等可选值以及它们的含义。除了知道不同参数之间的区别以及性能上的差异外,我们也可以一起了解一下sourceMap的实现方式。


sourceMap是一项将编译、打包、压缩后的代码映射回源代码的技术,由于打包压缩后的代码并没有阅读性可言,一旦在开发中报错或者遇到问题,直接在混淆代码中debug问题会带来非常糟糕的体验,sourceMap可以帮助我们快速定位到源代码的位置,提高我们的开发效率。sourceMap其实并不是Webpack特有的功能,而是Webpack支持sourceMap,像JQuery也支持souceMap


既然是一种源码的映射,那必然就需要有一份映射的文件,来标记混淆代码里对应的源码的位置,通常这份映射文件以.map结尾,里边的数据结构大概长这样:


{
"version" : 3, // Source Map版本
"file": "out.js", // 输出文件(可选)
"sourceRoot": "", // 源文件根目录(可选)
"sources": ["foo.js", "bar.js"], // 源文件列表
"sourcesContent": [null, null], // 源内容列表(可选,和源文件列表顺序一致)
"names": ["src", "maps", "are", "fun"], // mappings使用的符号名称列表
"mappings": "A,AAAB;;ABCDE;" // 带有编码映射数据的字符串
}


其中mappings数据有如下规则:



  • 生成文件中的一行的每个组用“;”分隔;

  • 每一段用“,”分隔;

  • 每个段由1、4或5个可变长度字段组成;


有了这份映射文件,我们只需要在我们的压缩代码的最末端加上这句注释,即可让sourceMap生效:


//# sourceURL=/path/to/file.js.map

有了这段注释后,浏览器就会通过sourceURL去获取这份映射文件,通过解释器解析后,实现源码和混淆代码之间的映射。因此sourceMap其实也是一项需要浏览器支持的技术。


如果我们仔细查看webpack打包出来的bundle文件,就可以发现在默认的development开发模式下,每个_webpack_modules__文件模块的代码最末端,都会加上//# sourceURL=webpack://file-path?,从而实现对sourceMap的支持。


sourceMap映射表的生成有一套较为复杂的规则,有兴趣的小伙伴可以看看以下文章,帮助理解soucrMap的原理实现:


Source Map的原理探究


Source Maps under the hood – VLQ, Base64 and Yoda


是否写过Loader?简单描述一下编写loader的思路?


从上面的打包代码我们其实可以知道,Webpack最后打包出来的成果是一份Javascript代码,实际上在Webpack内部默认也只能够处理JS模块代码,在打包过程中,会默认把所有遇到的文件都当作 JavaScript代码进行解析,因此当项目存在非JS类型文件时,我们需要先对其进行必要的转换,才能继续执行打包任务,这也是Loader机制存在的意义。


Loader的配置使用我们应该已经非常的熟悉:


// webpack.config.js
module.exports = {
// ...other config
module: {
rules: [
{
test: /^your-regExp$/,
use: [
{
loader: 'loader-name-A',
},
{
loader: 'loader-name-B',
}
]
},
]
}
}

通过配置可以看出,针对每个文件类型,loader是支持以数组的形式配置多个的,因此当Webpack在转换该文件类型的时候,会按顺序链式调用每一个loader,前一个loader返回的内容会作为下一个loader的入参。因此loader的开发需要遵循一些规范,比如返回值必须是标准的JS代码字符串,以保证下一个loader能够正常工作,同时在开发上需要严格遵循“单一职责”,只关心loader的输出以及对应的输出。


loader函数中的this上下文由webpack提供,可以通过this对象提供的相关属性,获取当前loader需要的各种信息数据,事实上,这个this指向了一个叫loaderContextloader-runner特有对象。有兴趣的小伙伴可以自行阅读源码。


module.exports = function(source) {
const content = doSomeThing2JsString(source);

// 如果 loader 配置了 options 对象,那么this.query将指向 options
const options = this.query;

// 可以用作解析其他模块路径的上下文
console.log('this.context');

/*
* this.callback 参数:
* error:Error | null,当 loader 出错时向外抛出一个 error
* content:String | Buffer,经过 loader 编译后需要导出的内容
* sourceMap:为方便调试生成的编译后内容的 source map
* ast:本次编译生成的 AST 静态语法树,之后执行的 loader 可以直接使用这个 AST,进而省去重复生成 AST 的过程
*/
this.callback(null, content);
// or return content;
}

更详细的开发文档可以直接查看官网的 Loader API


是否写过Plugin?简单描述一下编写plugin的思路?


如果说Loader负责文件转换,那么Plugin便是负责功能扩展。LoaderPlugin作为Webpack的两个重要组成部分,承担着两部分不同的职责。


上文已经说过,webpack基于发布订阅模式,在运行的生命周期中会广播出许多事件,插件通过监听这些事件,就可以在特定的阶段执行自己的插件任务,从而实现自己想要的功能。


既然基于发布订阅模式,那么知道Webpack到底提供了哪些事件钩子供插件开发者使用是非常重要的,上文提到过compilercompilationWebpack两个非常核心的对象,其中compiler暴露了和 Webpack整个生命周期相关的钩子(compiler-hooks),而compilation则暴露了与模块和依赖有关的粒度更小的事件钩子(Compilation Hooks)。


Webpack的事件机制基于webpack自己实现的一套Tapable事件流方案(github


// Tapable的简单使用
const { SyncHook } = require("tapable");

class Car {
constructor() {
// 在this.hooks中定义所有的钩子事件
this.hooks = {
accelerate: new SyncHook(["newSpeed"]),
brake: new SyncHook(),
calculateRoutes: new AsyncParallelHook(["source", "target", "routesList"])
};
}

/* ... */
}


const myCar = new Car();
// 通过调用tap方法即可增加一个消费者,订阅对应的钩子事件了
myCar.hooks.brake.tap("WarningLampPlugin", () => warningLamp.on());

Plugin的开发和开发Loader一样,需要遵循一些开发上的规范和原则:



  • 插件必须是一个函数或者是一个包含 apply 方法的对象,这样才能访问compiler实例;

  • 传给每个插件的 compilercompilation 对象都是同一个引用,若在一个插件中修改了它们身上的属性,会影响后面的插件;

  • 异步的事件需要在插件处理完任务时调用回调函数通知 Webpack 进入下一个流程,不然会卡住;


了解了以上这些内容,想要开发一个 Webpack Plugin,其实也并不困难。


class MyPlugin {
apply (compiler) {
// 找到合适的事件钩子,实现自己的插件功能
compiler.hooks.emit.tap('MyPlugin', compilation => {
// compilation: 当前打包构建流程的上下文
console.log(compilation);

// do something...
})
}
}

更详细的开发文档可以直接查看官网的 Plugin API


最后


本文也是结合一些优秀的文章和webpack本身的源码,大概地说了几个相对重要的概念和流程,其中的实现细节和设计思路还需要结合源码去阅读和慢慢理解。


Webpack作为一款优秀的打包工具,它改变了传统前端的开发模式,是现代化前端开发的基石。这样一个优秀的开源项目有许多优秀的设计思想和理念可以借鉴,我们自然也不应该仅仅停留在API的使用层面,尝试带着问题阅读源码,理解实现的流程和原理,也能让我们学到更多知识,理解得更加深刻,在项目中才能游刃有余的应用。




链接:https://juejin.cn/post/6943468761575849992

收起阅读 »

是什么让尤大选择放弃Webpack?面向未来的前端构建工具 Vite

前两天在知乎看到过一篇文章,大致意思是讲:字节跳动已经开始“弃用Webpack”,尝试在自研构建工具中使用类似Vite的ESmodule构建方式。 引起下方一大片焦虑: Webpack是不是要被取代了?现在学Vite就行了吧 Webpack还没学会,就又来新...
继续阅读 »

前两天在知乎看到过一篇文章,大致意思是讲:字节跳动已经开始“弃用Webpack”,尝试在自研构建工具中使用类似Vite的ESmodule构建方式。


引起下方一大片焦虑:



  • Webpack是不是要被取代了?现在学Vite就行了吧

  • Webpack还没学会,就又来新的了!


甚至有人搬出了去年尤大所发的一个动态:再也回不去Webpack了。


在这里插入图片描述



PS:最近的vite比较火,而且发布了2.0版本,vue的作者尤雨溪也是在极力推荐


全方位对比vite和webpack


webpack打包过程


1.识别入口文件


2.通过逐层识别模块依赖。(Commonjs、amd或者es6的import,webpack都会对其进行分析。来获取代码的依赖)


3.webpack做的就是分析代码。转换代码,编译代码,输出代码


4.最终形成打包后的代码


webpack打包原理


1.先逐级递归识别依赖,构建依赖图谱


2.将代码转化成AST抽象语法树


3.在AST阶段中去处理代码


4.把AST抽象语法树变成浏览器可以识别的代码, 然后输出



重点:这里需要递归识别依赖,构建依赖图谱。图谱对象就是类似下面这种



{ './app.js':
{ dependencies: { './test1.js': './test1.js' },
code:
'"use strict";\n\nvar _test = _interopRequireDefault(require("./test1.js"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n\nconsole.log(test
1);' },
'./test1.js':
{ dependencies: { './test2.js': './test2.js' },
code:
'"use strict";\n\nvar _test = _interopRequireDefault(require("./test2.js"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n\nconsole.log(\'th
is is test1.js \', _test["default"]);' },
'./test2.js':
{ dependencies: {},
code:
'"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\nexports["default"] = void 0;\n\nfunction test2() {\n console.log(\'this is test2 \');\n}\n\nvar _default = tes
t2;\nexports["default"] = _default;' } }


在这里插入图片描述


Vite原理


当声明一个 script 标签类型为 module 时





浏览器就会像服务器发起一个GET


http://localhost:3000/src/main.js请求main.js文件:

// /src/main.js:
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')


浏览器请求到了main.js文件,检测到内部含有import引入的包,又会对其内部的 import 引用发起 HTTP 请求获取模块的内容文件



Vite 的主要功能就是通过劫持浏览器的这些请求,并在后端进行相应的处理将项目中使用的文件通过简单的分解与整合,然后再返回给浏览器,vite整个过程中没有对文件进行打包编译,所以其运行速度比原始的webpack开发编译速度快出许多!


webpack缺点一:缓慢的服务器启动


当冷启动开发服务器时,基于打包器的方式是在提供服务前去急切地抓取和构建你的整个应用。


Vite改进



  • Vite 通过在一开始将应用中的模块区分为 依赖 和 源码 两类,改进了开发服务器启动时间。

  • 依赖 大多为纯 JavaScript 并在开发时不会变动。一些较大的依赖(例如有上百个模块的组件库)处理的代价也很高。依赖也通常会以某些方式(例如 ESM 或者 CommonJS)被拆分到大量小模块中。

  • Vite 将会使用 esbuild 预构建依赖。Esbuild 使用 Go 编写,并且比以 JavaScript 编写的打包器预构建依赖快 10-100 倍。

  • 源码 通常包含一些并非直接是 JavaScript 的文件,需要转换(例如 JSX,CSS 或者 Vue/Svelte 组件),时常会被编辑。同时,并不是所有的源码都需要同时被加载。(例如基于路由拆分的代码模块)。

  • Vite 以 原生 ESM 方式服务源码。这实际上是让浏览器接管了打包程序的部分工作:Vite 只需要在浏览器请求源码时进行转换并按需提供源码。根据情景动态导入的代码,即只在当前屏幕上实际使用时才会被处理。


webpack缺点2:使用的是node.js去实现


在这里插入图片描述
Vite改进


Vite 将会使用 esbuild 预构建依赖。Esbuild 使用 Go 编写,并且比以 Node.js 编写的打包器预构建依赖快 10-100 倍。


webpack致命缺点3:热更新效率低下



  • 当基于打包器启动时,编辑文件后将重新构建文件本身。显然我们不应该重新构建整个包,因为这样更新速度会随着应用体积增长而直线下降。

  • 一些打包器的开发服务器将构建内容存入内存,这样它们只需要在文件更改时使模块图的一部分失活[1],但它也仍需要整个重新构建并重载页面。这样代价很高,并且重新加载页面会消除应用的当前状态,所以打包器支持了动态模块热重载(HMR):允许一个模块 “热替换” 它自己,而对页面其余部分没有影响。这大大改进了开发体验 - 然而,在实践中我们发现,即使是 HMR 更新速度也会随着应用规模的增长而显著下降。


Vite改进



  • 在 Vite 中,HMR 是在原生 ESM 上执行的。当编辑一个文件时,Vite 只需要精确地使已编辑的模块与其最近的 HMR 边界之间的链失效(大多数时候只需要模块本身),使 HMR 更新始终快速,无论应用的大小。

  • Vite 同时利用 HTTP 头来加速整个页面的重新加载(再次让浏览器为我们做更多事情):源码模块的请求会根据 304 Not Modified 进行协商缓存,而依赖模块请求则会通过 Cache-Control: max-age=31536000,immutable 进行强缓存,因此一旦被缓存它们将不需要再次请求。


Vite缺点1:生态,生态,生态不如webpack


wepback牛逼之处在于loader和plugin非常丰富,不过我认为生态只是时间问题,现在的vite,更像是当时刚出来的M1芯片Mac,我当时非常看好M1的Mac,毫不犹豫买了,现在也没什么问题


Vite缺点2:prod环境的构建,目前用的Rollup


原因在于esbuild对于css和代码分割不是很友好


Vite缺点3:还没有被大规模使用,很多问题或者诉求没有真正暴露出来


vite真正崛起那一天,是跟vue3有关系的,当vue3广泛开始使用在生产环境的时候,vite也就大概率意味着被大家慢慢开始接受了


总结


1.Vite,就像刚出来的M1芯片Mac,都说好,但是一开始买的人不多,担心生态问题,后面都说真香


2.相信vue3作者的大力支持下,vite即将大放异彩!


3.但是 Webpack 在现在的前端工程化中仍然扮演着非常重要的角色。


4.vite相关生态没有webpack完善,vite可以作为开发的辅助。



链接:https://juejin.cn/post/6975038104650383374

收起阅读 »

Vue3发布半年我不学,摸鱼爽歪歪,哎~就是玩儿

vue
是从 Vue 2 开始学基础还是直接学 Vue 3 ?尤雨溪给出的答案是:“直接学 Vue 3 就行了,基础概念是一模一样的。” 以上内容源引自最新一期的《程序员》期刊,原文链接为《直接学 Vue 3 吧 —— 对话 Vue.js 作者尤雨溪》。 前言 Vue...
继续阅读 »

是从 Vue 2 开始学基础还是直接学 Vue 3 ?尤雨溪给出的答案是:“直接学 Vue 3 就行了,基础概念是一模一样的。”


以上内容源引自最新一期的《程序员》期刊,原文链接为《直接学 Vue 3 吧 —— 对话 Vue.js 作者尤雨溪》


前言


Vue 3.0 出来之后,我一直在不断的尝试学习和接受新的概念。没办法,作为一个前端开发,并且也不是毕业于名校或就职于大厂,不断地学习,培养学习能力,才是我们这些普通前端开发的核心竞争力。


当然,有些同学抬杠,我专精一门技术,也能开发出自己的核心竞争力。好!!!有志气。但是多数同学,很难有这种意志力。如 CSS 大佬张鑫旭Canva 大佬老姚、可视化大佬月影大大、面试题大佬敖丙等等等等。这些大佬在一件事情上花费的精力,是需要极高的意志力和执行力才能做到的。我反正做不到(逃)。


学无止境!


一定要动手敲代码。仅仅学习而不实践,这种做法也不可取。


本文主要是介绍一些我学习 Vue 3.0 期间,看过的一些比较有用的资源,和大家分享一下,不喜勿喷,喷了我也学着 @尼克陈 顺着网线找到你家。


我与 Vue 3.0


其实一直都有在关注 Vue 3.0 相关的进度和新闻,不过真正学习是在它正式 release 后,2020 年 9 月我也发布了一篇文章《Vue 3.0 来了,我们该做些什么?》阐述了自己的看法,也制定了自己的学习计划。


其实,学习任何一门新技术的步骤都一样:


看文档 → 学习新语法 → 做小 demo → 做几个实战项目 → 看源码 → 整理心得并分享。


学习 Vue 3.0 亦是如此,虽然我这个人比较爱开玩笑,也爱写段子,标题取的也吊儿郎当,但是学习和行动起来我可不比别人差。


学习过程中看文档、做 demo,然后也一直在学习和分享 Vue3 的知识点,比如发布一些 Vue3 的教程:



也做了几个 Vue 3.0 实战的项目练手,之后发布到也开源了 GitHub 中,访问地址如下:



in GitHub : github.com/newbee-ltd


in Gitee : gitee.com/newbee-ltd



一个是 Vue3 版本的商城项目:


img


一个是 Vue3 版本的后台管理项目:


panban1 (1)


源码全部开放,后台 API 也有,都是很实用的项目。目前的反响还不错,得到了很多的正向反馈,这些免费的开源项目让大家有了一个不错的 Vue3 练手项目,顺利的完成了课程作业或者在简历里多了一份项目经验,因此也收到了很多感谢的话。


接下来就是学习过程中,我觉得非常有用的资源了,大家在学习 Vue 3 时可以参考和使用。


image-20210228175425067


Vue 3.0 相关技术栈



















































相关库名称在线地址 🔗
Vue 3.0 官方文档(英文)在线地址
Vue 3.0 中文文档在线地址 国内加速版
Composition-API手册在线地址
Vue 3.0 源码学习在线地址
Vue-Router 官方文档在线地址
Vuex 4.0Github
vue-devtoolsGithub(Vue3.0 需要使用最新版本)
Vite 源码学习线上地址
Vite 2.0 中文文档线上地址
Vue3 新动态线上地址

Vue3 新动态 这个仓库我经常看,里面有最新的 Vue 3 文章、仓库等等,都是中文的,作者应该是咱们的大兄弟,大家也可以关注一下。


更新 Vue 3.0 的开源 UI 组件库


Vue 2.0 时期,产生了不少好的开源组件库,这些组件库伴随着我们的成长,我们看看哪些组件库更新了 Vue 3.0 版本。


Element-plus


简介:大家想必也不陌生,它的 Vue 2.0 版本是 Element-UI,后经坤哥和他的小伙伴开发出了 Vue 3.0 版本的  Element-plus,确实很优秀,目前点赞数快破万了,持续关注。


仓库地址 🏠 :github.com/element-plu… ⭐ : 9.8k


文档地址 📖 :element-plus.gitee.io/#/zh-CN


开源项目 🔗 :



目前 Element-plus 的开源项目还不多,之前 Element-UI 相关开源项目,大大小小都在做 Element-plus 的适配。在此也感谢坤哥和他的小伙伴们,持续 Element 系列的维护,这对 Vue 生态是非常强大的贡献。


Ant Design of Vue


简介:它是最早一批做 Vue 3.0 适配的组件库, Antd 官方推荐的组件库。


仓库地址 🏠 :github.com/vueComponen… ⭐ : 14.8k


文档地址 📖 :antdv.com/docs/vue/in…


开源项目 🔗 :



他们的更新维护还是很积极的,最近一次更新实在 2021 年 2 月 27 号,可见这个组件库还是值得信赖的,有问题可以去 issue 提。


Vant


简介:国内移动端首屈一指的组件库,用过的都说好,个人已经在两个项目中使用过该组件库,也算是比较早支持 Vue 3.0 的框架,该有的都有。


仓库地址 🏠 :github.com/youzan/vant ⭐ : 16.9k


文档地址 📖 :vant-contrib.gitee.io/vant/v3/#/z…


开源项目 🔗 :



NutUI 3


简介:京东团队开发的移动端组件库,近期才升级到 Vue 3.0 版本,文章在此。虽然我没有使用过这个组件库,但是从他们的更新速度来看,比其他很多组件库要快,说明对待最近技术,还是有态度的。


仓库地址 🏠 :github.com/jdf2e/nutui ⭐ : 3.1k


文档地址 📖 :nutui.jd.com (看看这简短的域名,透露出壕的气息)


开源项目 🔗 :基本上还没有见到有公开的开源项目,如果有还望大家积极评论


链接:https://juejin.cn/post/6955129410705948702

收起阅读 »

iOS-使用SDCycleScrollView定制各种自定义样式的上下滚动的跑马灯

SDCycleScrollView的优点及实现技巧:1.利用UICollectionView的复用机制,只会创建屏幕可见个cell。2.如果是无限循环 ,会存在100*self.imagePathsGroup.count个item,第一次出现的位置在(100*...
继续阅读 »

SDCycleScrollView的优点及实现技巧:

1.利用UICollectionView的复用机制,只会创建屏幕可见个cell。
2.如果是无限循环 ,会存在100*self.imagePathsGroup.count个item,第一次出现的位置在(100*self.imagePathsGroup.count)/2的位置。
3.每次滚动到100*self.imagePathsGroup.count位置的item自动切换到(100*self.imagePathsGroup.count)/2的位置。
4.使用取余index % self.imagePathsGroup.count确定现在显示的imageView

缺点:

手动拖拽到最后、不会跳到初始位置

原因:

因为作者设置的100足够大、未对拖拽最后一个item做处理

解决方法:

同时监听NSTimer和拖拽,在(100 - 1)*self.imagePathsGroup.count和self.imagePathsGroup.count位置时实现切换到(100*self.imagePathsGroup.count)/2的位置

使用SDCycleScrollView制作各种自定义样式的上下滚动的跑马灯

效果图:


.m

@interface ViewController () <SDCycleScrollViewDelegate>
@end
@implementation ViewController
{
NSArray *_imagesURLStrings;
SDCycleScrollView *_customCellScrollViewDemo;
}

- (void)customCellScrollView {

// 如果要实现自定义cell的轮播图,必须先实现customCollectionViewCellClassForCycleScrollView:和 setupCustomCell:forIndex:代理方法

_customCellScrollViewDemo = [SDCycleScrollView cycleScrollViewWithFrame:CGRectMake(0, 820, w, 40) delegate:self placeholderImage:[UIImage imageNamed:@"placeholder"]];
_customCellScrollViewDemo.currentPageDotImage = [UIImage imageNamed:@"pageControlCurrentDot"];
_customCellScrollViewDemo.pageDotImage = [UIImage imageNamed:@"pageControlDot"];
_customCellScrollViewDemo.imageURLStringsGroup = imagesURLStrings;
_customCellScrollViewDemo.scrollDirection = UICollectionViewScrollDirectionVertical;
_customCellScrollViewDemo.showPageControl = NO;
[demoContainerView addSubview:_customCellScrollViewDemo];
}

// 不需要自定义轮播cell的请忽略下面的代理方法

// 如果要实现自定义cell的轮播图,必须先实现customCollectionViewCellClassForCycleScrollView:和setupCustomCell:forIndex:代理方法
- (Class)customCollectionViewCellClassForCycleScrollView:(SDCycleScrollView *)view
{
if (view != _customCellScrollViewDemo) {
return nil;
}
return [CustomCollectionViewCell class];
}

- (void)setupCustomCell:(UICollectionViewCell *)cell forIndex:(NSInteger)index cycleScrollView:(SDCycleScrollView *)view
{
CustomCollectionViewCell *myCell = (CustomCollectionViewCell *)cell;
//[myCell.imageView sd_setImageWithURL:_imagesURLStrings[index]];

NSArray *titleArray = @[@"新闻",
@"娱乐",
@"体育"];
NSArray *contentArray = @[@"新闻新闻新闻新闻新闻新闻新闻新闻新闻新闻新闻新闻",
@"娱乐娱乐娱乐娱乐娱乐娱乐娱乐娱乐娱乐娱乐",
@"体育体育体育体育体育体育体育体育体育体育体育体育"];
myCell.titleLabel.text = titleArray[index];
myCell.contentLabel.text = contentArray[index];
}

自定义cell-根据不同的cell定制各种自定义样式的上下滚动的跑马灯

.h
#import <UIKit/UIKit.h>

@interface CustomCollectionViewCell : UICollectionViewCell

@property (nonatomic, strong) UIImageView *imageView;
@property (nonatomic, strong) UILabel *titleLabel;
@property (nonatomic, strong) UILabel *contentLabel;

@end
.m
#import "CustomCollectionViewCell.h"
#import "UIView+SDExtension.h"

@implementation CustomCollectionViewCell

#pragma mark - 懒加载
- (UIImageView *)imageView {
if (!_imageView) {
_imageView = [UIImageView new];
_imageView.layer.borderColor = [[UIColor redColor] CGColor];
_imageView.layer.borderWidth = 0;
_imageView.hidden = YES;
}
return _imageView;
}
- (UILabel *)titleLabel {
if (!_titleLabel) {
_titleLabel = [[UILabel alloc]init];
_titleLabel.text = @"新闻";
_titleLabel.textColor = [UIColor redColor];
_titleLabel.numberOfLines = 0;
_titleLabel.textAlignment = NSTextAlignmentCenter;
_titleLabel.font = [UIFont systemFontOfSize:12];
_titleLabel.backgroundColor = [UIColor yellowColor];
_titleLabel.layer.masksToBounds = YES;
_titleLabel.layer.cornerRadius = 5;
_titleLabel.layer.borderColor = [UIColor redColor].CGColor;
_titleLabel.layer.borderWidth = 1.f;
}
return _titleLabel;
}
- (UILabel *)contentLabel {
if (!_contentLabel) {
_contentLabel = [[UILabel alloc]init];
_contentLabel.text = @"我是label的内容";
_contentLabel.textColor = [UIColor blackColor];
_contentLabel.numberOfLines = 0;
_contentLabel.font = [UIFont systemFontOfSize:12];
}
return _contentLabel;
}
#pragma mark - 页面初始化
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
self.contentView.backgroundColor = [UIColor whiteColor];
[self setupViews];
}
return self;
}

#pragma mark - 添加子控件
- (void)setupViews {
[self.contentView addSubview:self.imageView];
[self.contentView addSubview:self.titleLabel];
[self.contentView addSubview:self.contentLabel];
}

#pragma mark - 布局子控件
- (void)layoutSubviews {
[super layoutSubviews];
_imageView.frame = self.bounds;
_titleLabel.frame = CGRectMake(15, 10, 45, 20);
_contentLabel.frame = CGRectMake(15 + 45 + 15, 10, 200, 20);
}

实际情况自己可下载SDCycleScrollView自行研究。。。

转自:https://www.jianshu.com/p/641403879f7b

收起阅读 »

国内知名Wchat团队荣誉出品顶级IM通讯聊天系统

iOS
国内知名Wchat团队荣誉出品顶级IM通讯聊天系统团队言语在先:想低价购买者勿扰(团队是在国内首屈一指的通信公司离职后组建,低价购买者/代码代码贩子者/同行勿扰/)。想购买劣质低等产品者勿扰(行业鱼龙混杂,想购买类似低能协议xmpp者勿扰)。想购买由类似ope...
继续阅读 »



国内知名Wchat团队荣誉出品顶级IM通讯聊天系统



团队言语在先:

想低价购买者勿扰(团队是在国内首屈一指的通信公司离职后组建,低价购买者/代码代码贩子者/同行勿扰/)

。想购买劣质低等产品者勿扰(行业鱼龙混杂,想购买类似低能协议xmpp者勿扰)

。想购买由类似openfire第三方开源改造而来的所谓第三方通信server者勿扰

。想购买没有做任何安全加密场景者勿扰(随便一句api 一个接口就构成了红包收发/转账/密码设置等没有任何安全系数可言的低质产品)

。想购买非运营级别通信系统勿扰(到处呼喊:最稳定/真正可靠/大并发/真正安全!所有一切都需要实际架构支撑以及理论数值测验)

。想购买无保障/无支撑者勿扰(1W/4W/10W低质产品不可谓没有,必须做到:大并发支持合同保障/合作支持运维保障/在线人数支持架构保障)

。想购买消息丢包者勿扰(满天飞的所谓消息确认机制,最简单的测验既是前端支持消息收发demo测试环境,低质产品一秒收发百条消息必丢必崩,

别提秒发千条/万条,更低质产品可测验:同时发九张图片/根据数字12345678910发送出去,必丢!android vs ios)

。想购买大容量群uer者勿扰(随便宣传既是万人大群/几千大群/群组无限,小团队产品群组上线用户超过4000群消息体量不用很大手机前端必卡)

。最重要一点:口口声声说要运营很大的系统 却想出十几个money的人群勿扰,买产品做系统一要稳定二要长久用三要抛开运维烦恼,预算有限那就干脆

别买,买了几万的系统你一样后面用不起来会烂掉!

。产品体系包括:android ios server adminweb maintenance httpapi h5 webpc (支持server压测/前端消息收发压测/httpapi压测)

。。支持源码,但需要您拿去做一个伟大的系统出来!

。。团队产品目前国内没有同质化,客户集中在国外,有求高质量产品的个人或团队可通过以下方式联系到我们(低价者勿扰!)

。。。球球:383189941 q 513275129

。。。。产品不多介绍直接加我 测试产品更直接

。。。。。创新从未停止 更新不会终止 大陆唯一一家支持大并发保障/支持合同费用包含运维支撑的团队 

收起阅读 »

iOS第三方——JazzHands

JazzHands是UIKit一个简单的关键帧基础动画框架。可通过手势、scrollView,kvo或者ReactiveCocoa控制动画。JazzHands很适合用来创建很酷的引导页。Swift中的JazzHands想在Swift中使用Jazz Hands?...
继续阅读 »

JazzHands是UIKit一个简单的关键帧基础动画框架。可通过手势、scrollView,kvo或者ReactiveCocoa控制动画。JazzHands很适合用来创建很酷的引导页。


Swift中的JazzHands

想在Swift中使用Jazz Hands?可以试试RazzleDazzle。

安装

JazzHands可以通过CocoaPods安装,在Podfile中加入如下的一行:

pod "JazzHands"

你也可以把JazzHands文件夹的内容复制到工程中。

快速开始

首先,在UIViewController中加入JazzHands:

#import <IFTTTJazzHands.h>

现在创建一个Animator来管理UIViewController中所有的动画。

@property (nonatomic, strong) IFTTTAnimator *animator;

// later...

self.animator = [IFTTTAnimator new];

为你想要动画的view,创建一个animation。这儿有许多可以应用到view的animation。例如,我们使用IFTTTAlphaAnimation,可以使view淡入淡出。

IFTTTAlphaAnimation *alphaAnimation = [IFTTTAlphaAnimation animationWithView: viewThatYouWantToAnimate];

使用animator注册这个animation。

[self.animator addAnimation: alphaAnimation];

为animation添加一些keyframe关键帧。我们让这个view在times的30和60之间变淡(Let’s fade this view out between times 30 and 60)。

[alphaAnimation addKeyframeForTime:30 alpha:1.f];
[alphaAnimation addKeyframeForTime:60 alpha:0.f];

现在,让view动起来,要让animator知道what time it is。例如,把这个animation和UIScrollView绑定起来,在scroll的代理方法中来通知animator。

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
[super scrollViewDidScroll:scrollView];
[self.animator animate:scrollView.contentOffset.x];
}

这样会产生的效果是,view在滚动位置的0到30之间时,view会淡入,变的可见。在滚动位置的30到60之间,view会淡出,变的不可见。而且在滚动位置大于60的时候会保持fade out。

动画的类型

Jazz Hands支持多种动画:

IFTTTAlphaAnimation 动画的是 alpha 属性 (创造的是淡入淡出的效果).
IFTTTRotationAnimation 动画的是旋转变换 (旋转效果).
IFTTTBackgroundColorAnimation 动画的是 backgroundColor 属性.
IFTTTCornerRadiusAnimation 动画的是 layer.cornerRadius 属性.
IFTTTHideAnimation 动画的是 hidden属性 (隐藏和展示view).
IFTTTScaleAnimation 应用一个缩放变换 (缩放尺寸).
IFTTTTranslationAnimation 应用一个平移变换 (平移view的位置).
IFTTTTransform3DAnimation 动画的是 layer.transform 属性 (是3D变换).
IFTTTTextColorAnimation 动画的是UILabel的 textColor 属性。
IFTTTFillColorAnimation 动画的是CAShapeLayer的fillColor属性。
IFTTTStrokeStartAnimation 动画的是CAShapeLayer的strokeStart属性。(does not work with IFTTTStrokeEndAnimation).
IFTTTStrokeEndAnimation 动画的是CAShapeLayer的strokeEnd属性。 (does not work with IFTTTStrokeStartAnimation).
IFTTTPathPositionAnimation 动画的是UIView的layer.position属性。
IFTTTConstraintConstantAnimation animates an AutoLayout constraint constant.
IFTTTConstraintMultiplierAnimation animates an AutoLayout constraint constant as a multiple of an attribute of another view (to offset or resize views based on another view’s size)
IFTTTScrollViewPageConstraintAnimation animates an AutoLayout constraint constant to place a view on a scroll view page (to position views on a scrollView using AutoLayout)
IFTTTFrameAnimation animates the frame property (moves and sizes views. Not compatible with AutoLayout).

更多例子

Easy Paging Scrollview Layouts in an AutoLayout World
JazzHands的IFTTTAnimatedPagingScrollViewController中的 keepView:onPage:方法,可以非常简单的在scroll view上布局分页。

调用keepView:onPages: 可以在多个pages上展示一个view,当其它view滚动的时候。

具体应用的例子

在开源项目coding/Coding-iOS中的IntroductionViewController有使用到,IntroductionViewController继承自IFTTTAnimatedPagingScrollViewController。

- (void)configureTipAndTitleViewAnimations{
for (int index = 0; index < self.numberOfPages; index++) {
NSString *viewKey = [self viewKeyForIndex:index];
UIView *iconView = [self.iconsDict objectForKey:viewKey];
UIView *tipView = [self.tipsDict objectForKey:viewKey];
if (iconView) {
if (index == 0) {//第一个页面
[self keepView:iconView onPages:@[@(index +1), @(index)] atTimes:@[@(index - 1), @(index)]];

[iconView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(kScreen_Height/7);
}];
}else{
[self keepView:iconView onPage:index];

[iconView mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerY.mas_equalTo(-kScreen_Height/6);//位置往上偏移
}];
}
IFTTTAlphaAnimation *iconAlphaAnimation = [IFTTTAlphaAnimation animationWithView:iconView];
[iconAlphaAnimation addKeyframeForTime:index -0.5 alpha:0.f];
[iconAlphaAnimation addKeyframeForTime:index alpha:1.f];
[iconAlphaAnimation addKeyframeForTime:index +0.5 alpha:0.f];
[self.animator addAnimation:iconAlphaAnimation];
}
if (tipView) {
[self keepView:tipView onPages:@[@(index +1), @(index), @(index-1)] atTimes:@[@(index - 1), @(index), @(index + 1)]];

IFTTTAlphaAnimation *tipAlphaAnimation = [IFTTTAlphaAnimation animationWithView:tipView];
[tipAlphaAnimation addKeyframeForTime:index -0.5 alpha:0.f];
[tipAlphaAnimation addKeyframeForTime:index alpha:1.f];
[tipAlphaAnimation addKeyframeForTime:index +0.5 alpha:0.f];
[self.animator addAnimation:tipAlphaAnimation];

[tipView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(iconView.mas_bottom).offset(kScaleFrom_iPhone5_Desgin(45));
}];
}
}
}

效果如下:


转自:https://blog.csdn.net/u014084081/article/details/53610215

收起阅读 »

网易换肤第二篇:本地换肤实现!

完整脑图:https://note.youdao.com/s/V2csJmYS Demo源码:点击下载 技术分析 我们在换肤的第一篇介绍了换肤的核心思想。就是在setContentView()之前调用setFactory2()。 第一篇的Demo利...
继续阅读 »


在这里插入图片描述
完整脑图:https://note.youdao.com/s/V2csJmYS


Demo源码:点击下载


技术分析




我们在换肤的第一篇介绍了换肤的核心思想。就是在setContentView()之前调用setFactory2()


第一篇的Demo利用的是AOP切面方法registerActivityLifecycleCallbacks(xxx)回调在setContentView()之前,从而在registerActivityLifecycleCallbacks的onActivityCreated()方法中设置Factory。如此就能拦截到控件的属性,根据拦截到的控件的属性,重新赋值控件的textColor、background等属性,从而实现换肤的。


本Demo的实现,主要基于以下两个狙击点。



1、super.onCreate(savedInstanceState)方法
2、Activity实现了Factory接口



前面说过,只要在setContentView()之前setFactory2()就行。super.onCreate(savedInstanceState)方法就是在setContentView()方法之前执行的。


一直跟踪super.onCreate(savedInstanceState)方法,最终会发现setFactory的逻辑,如下:


AppCompatDelegateImpl.java(1008)


public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(this.mContext);
if (layoutInflater.getFactory() == null) {
LayoutInflaterCompat.setFactory2(layoutInflater, this);
} else if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {
Log.i("AppCompatDelegate", "The Activity's LayoutInflater already has a Factory installed so we can not install AppCompat's");
}
}

它这里传了this,可以预见AppCompatDelegateImpl是实现了Factory接口的,最后会通过AppCompatDelegateImpl自身的onCreateView()方法创建的View。


onCreateView()中如何创建的View的,下面再看源码,先知道是通过AppCompatViewInflater来做控件的具体初始化的。


第一个狙击点可以抽出下图内容:


在这里插入图片描述
细心地同学肯定注意到了AppCompatDelegateImpl的installViewFactory()方法中,只有当layoutInflater.getFactory() == null的时候,才会去setFactory。


也就是说我在super.onCreate(savedInstanceState)之前,先给它setFactory就能走自己Factory的onCreateView()回调。


换肤第一篇中我们是自己去实现Factory2接口,在本例中,就用到了我们第二个狙击点。


Activity实现了Factory接口!!!


在这里插入图片描述


也就是说,只要我们在super.onCreate(savedInstanceState)之前,setFactory的时候,传this,就能走ActivityonCreateView()回调,来对控件属性做操作。


用归纳法,见下图:


在这里插入图片描述
最后,也就剩下Activity的onCreateView()中的回调怎么实现了。


直接模拟super.onCreate(savedInstanceState)中AppCompatViewInflater类中的实现就好了。


在这里插入图片描述
参考代码:


/**
* 自定义控件加载器(可以考虑该类不被继承)
*/

public final class CustomAppCompatViewInflater extends AppCompatViewInflater {

private String name; // 控件名
private Context context; // 上下文
private AttributeSet attrs; // 某控件对应所有属性

public CustomAppCompatViewInflater(@NonNull Context context) {
this.context = context;
}

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

public void setAttrs(AttributeSet attrs) {
this.attrs = attrs;
}

/**
* @return 自动匹配控件名,并初始化控件对象
*/

public View autoMatch() {
View view = null;
switch (name) {
case "LinearLayout":
// view = super.createTextView(context, attrs); // 源码写法
view = new SkinnableLinearLayout(context, attrs);
this.verifyNotNull(view, name);
break;
case "RelativeLayout":
view = new SkinnableRelativeLayout(context, attrs);
this.verifyNotNull(view, name);
break;
case "TextView":
view = new SkinnableTextView(context, attrs);
this.verifyNotNull(view, name);
break;
case "ImageView":
view = new SkinnableImageView(context, attrs);
this.verifyNotNull(view, name);
break;
case "Button":
view = new SkinnableButton(context, attrs);
this.verifyNotNull(view, name);
break;
}

return view;
}

/**
* 校验控件不为空(源码方法,由于private修饰,只能复制过来了。为了代码健壮,可有可无)
*
* @param view 被校验控件,如:AppCompatTextView extends TextView(v7兼容包,兼容是重点!!!)
* @param name 控件名,如:"ImageView"
*/

private void verifyNotNull(View view, String name) {
if (view == null) {
throw new IllegalStateException(this.getClass().getName() + " asked to inflate view for <" + name + ">, but returned null");
}
}
}

详细实现就参考Demo吧,思路其实很简单,只是会有对setFactory这块逻辑的流程不了解的。建议跟踪着点几遍源码。





————————————————
版权声明:本文为CSDN博主「csdn小瓯」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u014158743/article/details/117995256

收起阅读 »

网易换肤第一篇:换肤技术解密!

参考 脑图:https://note.youdao.com/s/Q1e6r39j 最终效果: Demo源码:点击跳转 技术点分析 换肤的核心思路主要是在setContentView()之前调用setFactory2()来收集控件属性,然后在F...
继续阅读 »


参考




脑图:https://note.youdao.com/s/Q1e6r39j


最终效果:
在这里插入图片描述
Demo源码:点击跳转


技术点分析




换肤的核心思路主要是在setContentView()之前调用setFactory2()来收集控件属性,然后在Factory的onCreateView()中利用收集到的属性来创建view。


不懂?没事,往下看。


在这里插入图片描述
弄明白换肤技术的实现之前,得有上图这几个知识储备。


首先得知道控件是在setContentView()方法中通过XmlPullParser解析我们在xml中定义的控件,然后显示在界面上的


LayoutInflater.java(451,注:本文源码为安卓9.0,api 28,下同


public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
...
if (TAG_MERGE.equals(name)) {
...
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// Temp is the root view that was found in the xml
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
...
}
...

而且在createViewFromTag()方法中,有一个判断:当mFactory2 != null的时候,就会把从xml中解析到的属性等传给mFactory2.onCreateView(parent, name, context, attrs)方法,利用mFactory2来创建view。


先看源码片段:
LayoutInflater.java(748)


View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
...

try {
View view;
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}

if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}

if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf('.')) {
view = onCreateView(parent, name, attrs);
} else {
view = createView(name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}

return view;
} catch (InflateException e) {
...
}
}

所以,我们只要通过LayoutlnflaterCompat.setFactory2(xx, yу)设置了Factory,就可以拦截到所有控件及其在xml中定义的属性了。


如此一来,问题就变成了如何在setContentView(R.layout.xxx)之前setFactory2()


答案就是利用AOP方法切面:registerActivityLifecycleCallbacks(xxx)ActivityLifecycleCallbacksonActivityCreated()方法正是在setContentView(R.layout.xxx)之前执行。


所以,我们可以实现Application.ActivityLifecycleCallbacks,然后在onActivityCreated()方法中LayoutInflaterCompat.setFactory2(xx, yy),这样换肤技术的核心部分,就被我们突破了。


参考代码:


public class SkinActivityLifecycleCallbacks implements Application.ActivityLifecycleCallbacks {
...
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
...

skinFactory = new SkinFactory(activity);
// mFactorySet = true是无法设置成功的(源码312行)
LayoutInflaterCompat.setFactory2(layoutInflater, skinFactory);

// 注册观察者(监听用户操作,点击了换肤,通知观察者更新)
SkinEngine.getInstance().addObserver(skinFactory);
}

...
}





————————————————
版权声明:本文为CSDN博主「csdn小瓯」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u014158743/article/details/117921500

收起阅读 »

带着问题学,协程到底是什么?

前言 随着kotlin在Android开发领域越来越火,协程在各个项目中的应用也逐渐变得广泛 但是协程到底是什么呢? 协程其实是个古老的概念,已经非常成熟了,但大家对它的概念一直存在各种疑问,众说纷纷 有人说协程是轻量级的线程,也有人说kotlin协程其...
继续阅读 »



前言


随着kotlinAndroid开发领域越来越火,协程在各个项目中的应用也逐渐变得广泛


但是协程到底是什么呢?


协程其实是个古老的概念,已经非常成熟了,但大家对它的概念一直存在各种疑问,众说纷纷
有人说协程是轻量级的线程,也有人说kotlin协程其实本质是一套线程切换方案


显然这对初学者不太友好,当不清楚一个东西是什么的时候,就很难进入为什么怎么办的阶段了
本文主要就是回答这个问题,主要包括以下内容
1.关于协程的一些前置知识
2.协程到底是什么?
3.kotlin协程的一些基本概念,挂起函数,CPS转换,状态机等
以上问题总结为思维导图如下:



1. 关于协程的一些前置知识


为了了解协程,我们可以从以下几个切入点出发
1.什么是进程?为什么要有进程?
2.什么是线程?为什么要有线程?进程和线程有什么区别?
3.什么是协作式,什么是抢占式?
4.为什么要引入协程?是为了解决什么问题?


1.1 什么是进程?


我们在背进程的定义的时候,可能会经常看到一句话



进程是资源分配的最小单位



这个资源分配怎么理解呢?


在单核CPU中,同一时刻只有一个程序在内存中被CPU调用运行



假设有AB两个程序,A正在运行,此时需要读取大量输入数据(IO操作),那么CPU只能干等,直到A数据读取完毕,再继续往下执行,A执行完,再去执行程序B,白白浪费CPU资源。



这种方式会浪费CPU资源,我们可能更想要下面这种方式



当程序A读取数据的时,切换 到程序B去执行,当A读取完数据,让程序B暂停,切换 回程序A执行?



在计算机里 切换 这个名词被细分为两种状态:



挂起:保存程序的当前状态,暂停当前程序; 激活:恢复程序状态,继续执行程序;



这种切换,涉及到了 程序状态的保存和恢复,而且程序AB所需的系统资源(内存、硬盘等)是不一样的,那还需要一个东西来记录程序AB各自需要什么资源,还有系统控制程序AB切换,要一个标志来识别等等,所以就有了一个叫 进程的抽象。


1.1.1 进程的定义


进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体主要由以下三部分组成:


1.程序:描述进程要完成的功能及如何完成;
2.数据集:程序在执行过程中所需的资源;
3.进程控制块:记录进程的外部特征,描述执行变化过程,系统利用它来控制、管理进程,系统感知进程存在的唯一标志。


1.1.2 为什么要有进程


其实上文我们已经分析过了,操作系统之所以要支持多进程,是为了提高CPU的利用率
而为了切换进程,需要进程支持挂起恢复,不同进程间需要的资源不同,所以这也是为什么进程间资源需要隔离,这也是进程是资源分配的最小单位的原因


1.2 什么是线程?


1.2.1 线程的定义


轻量级的进程,基本的CPU执行单元,亦是 程序执行过程中的最小单元,由 线程ID程序计数器寄存器组合堆栈 共同组成。
线程的引入减小了程序并发执行时的开销,提高了操作系统的并发性能。


1.2.2 为什么要有线程?


这个问题也很好理解,进程的出现使得多个程序得以 并发 执行,提高了系统效率及资源利用率,但存在下述问题:




  1. 单个进程只能干一件事,进程中的代码依旧是串行执行。

  2. 执行过程如果堵塞,整个进程就会挂起,即使进程中某些工作不依赖于正在等待的资源,也不会执行。

  3. 多个进程间的内存无法共享,进程间通讯比较麻烦。



线程的出现是为了降低上下文切换消耗,提高系统的并发性,并突破一个进程只能干一件事的缺陷,使得进程内并发成为可能。


1.2.3 进程与线程的区别



  • 1.一个程序至少有一个进程,一个进程至少有一个线程,可以把进程理解做 线程的容器;

  • 2.进程在执行过程中拥有 独立的内存单元,该进程里的多个线程 共享内存;

  • 3.进程可以拓展到 多机,线程最多适合 多核;

  • 4.每个独立线程有一个程序运行的入口、顺序执行列和程序出口,但不能独立运行,需依存于应用程序中,由应用程序提供多个线程执行控制;

  • 5.「进程」是「资源分配」的最小单位,「线程」是 「CPU调度」的最小单位

  • 6.进程和线程都是一个时间段的描述,是 CPU工作时间段的描述,只是颗粒大小不同。


1.3 协作式 & 抢占式


单核CPU,同一时刻只有一个进程在执行,这么多进程,CPU的时间片该如何分配呢?


1.3.1 协作式多任务


早期的操作系统采用的就是协作时多任务,即:由进程主动让出执行权,如当前进程需等待IO操作,主动让出CPU,由系统调度下一个进程。
每个进程都循规蹈矩,该让出CPU就让出CPU,是挺和谐的,但也存在一个隐患:单个进程可以完全霸占CPU


计算机中的进程良莠不齐,先不说那种居心叵测的进程了,如果是健壮性比较差的进程,运行中途发生了死循环、死锁等,会导致整个系统陷入瘫痪!
在这种鱼龙混杂的大环境下,把执行权托付给进程自身,肯定是不科学的,于是由操作系统控制的抢占式多任务横空出世


1.3.2 抢占式多任务


由操作系统决定执行权,操作系统具有从任何一个进程取走控制权和使另一个进程获得控制权的能力。
系统公平合理地为每个进程分配时间片,进程用完就休眠,甚至时间片没用完,但有更紧急的事件要优先执行,也会强制让进程休眠。
这就是所谓的时间片轮转调度



时间片轮转调度是一种最古老,最简单,最公平且使用最广的算法。每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。
如果在时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程。如果进程在时间片结束前阻塞或结束,则CPU当即进行切换。调度程序所要做的就是维护一张就绪进程列表,当进程用完它的时间片后,它被移到队列的末尾。



有了进程设计的经验,线程也做成了抢占式多任务,但也带来了新的——线程安全问题,这个一般通过加锁的方式来解决,这里就不缀述了。


1.4 为什么要引入协程?


上面介绍进程与线程的时候也提到了,之所以引入进程与线程是为了异步并发的执行任务,提高系统效率及资源利用率
但作为Java开发者,我们很清楚线程并发是多么的危险,写出来的异步代码是多么的难以维护。


Java中,我们一般通过回调来处理异步任务,但是当异步任务嵌套时,往往程序就会变得很复杂与难维护


举个例子,当我们需要完成这样一个需求:查询用户信息 --> 查找该用户的好友列表 --> 查找该好友的动态
看一下Java回调的代码


getUserInfo(new CallBack() {
@Override
public void onSuccess(String user) {
if (user != null) {
System.out.println(user);
getFriendList(user, new CallBack() {
@Override
public void onSuccess(String friendList) {
if (friendList != null) {
System.out.println(friendList);
getFeedList(friendList, new CallBack() {
@Override
public void onSuccess(String feed) {
if (feed != null) {
System.out.println(feed);
}
}
});
}
}
});
}
}
});

这就是传说中的回调地狱,如果用kotlin协程实现同样的需求呢?


val user = getUserInfo()
val friendList = getFriendList(user)
val feedList = getFeedList(friendList)

相比之下,可以说是非常简洁了


Kotlin 协程的核心竞争力在于:它能简化异步并发任务,以同步方式写异步代码
这也是为什么要引入协程的原因了:简化异步并发任务


2.到底什么是协程


2.1 什么是协程?


一种非抢占式(协作式)的任务调度模式,程序可以主动挂起或者恢复执行。


2.2 协程与线程的区别是什么?


协程基于线程,但相对于线程轻量很多,可理解为在用户层模拟线程操作;每创建一个协程,都有一个内核态进程动态绑定,用户态下实现调度、切换,真正执行任务的还是内核线程。


线程的上下文切换都需要内核参与,而协程的上下文切换,完全由用户去控制,避免了大量的中断参与,减少了线程上下文切换与调度消耗的资源。


线程是操作系统层面的概念,协程是语言层面的概念


线程与协程最大的区别在于:线程是被动挂起恢复,协程是主动挂起恢复


2.3 协程可以怎样分类?


根据 是否开辟相应的函数调用栈 又分成两类:



  • 有栈协程:有自己的调用栈,可在任意函数调用层级挂起,并转移调度权;

  • 无栈协程:没有自己的调用栈,挂起点的状态通过状态机或闭包等语法来实现;


2.4 Kotlin中的协程是什么?


"假"协程,Kotlin在语言级别并没有实现一种同步机制(锁),还是依靠Kotlin-JVM的提供的Java关键字(如synchronized),即锁的实现还是交给线程处理
因而Kotlin协程本质上只是一套基于原生Java线程池 的封装。


Kotlin 协程的核心竞争力在于:它能简化异步并发任务,以同步方式写异步代码。
下面介绍一些kotin协程中的基本概念


3. 什么是挂起函数?


我们知道使用suspend关键字修饰的函数叫做挂起函数,挂起函数只能在协程体内或者其他挂起函数内使用.


协程内部挂起函数的调用处被称为挂起点,挂起点如果出现异步调用,那么当前协程就被挂起,直到对应的Continuationresume函数被调用才会恢复执行


我们下面来看看挂起函数具体执行的细节



可以看出kotlin协程可以做到一行代码切换线程
这些是怎么做到的呢,主要是通过suspend关键字


3.1 什么是suspend


suspend 的本质,就是 CallBack


suspend fun getUserInfo(): String {
withContext(Dispatchers.IO) {
delay(1000L)
}
return "BoyCoder"
}

不过当我们写挂起函数的时候,并没有写callback,所谓的callback从何而来呢?
我们看下反编译的结果


//                              Continuation 等价于 CallBack
// ↓
public static final Object getUserInfo(Continuation $completion) {
...
return "BoyCoder";
}

public interface Continuation<in T> {
public val context: CoroutineContext
// 相当于 onSuccess 结果
// ↓ ↓
public fun resumeWith(result: Result<T>)
}
复制代码

可以看出


1.编译器会给挂起函数添加一个Continuation参数,这被称为CPS 转换(Continuation-Passing-Style Transformation)
2.suspend函数不能在协程体外调用的原因也可以知道了,就是因为这个Continuation实例的传递


4. 什么是CPS转换


下面用动画演示挂起函数在 CPS 转换过程中,函数签名的变化:


可以看出主要有两点变化
1.增加了Continuation类型的参数
2.返回类型从String转变成了Any


参数的变化我们之前讲过,为什么返回值要变呢?


4.1 挂起函数返回值


挂起函数经过 CPS 转换后,它的返回值有一个重要作用:标志该挂起函数有没有被挂起。
听起来有点奇怪,挂起函数还会不挂起吗?



只要被suspend修饰的函数都是挂起函数,但是不是所有挂起函数都会被挂起
只有当挂起函数里包含异步操作时,它才会被真正挂起



由于 suspend 修饰的函数,既可能返回 CoroutineSingletons.COROUTINE_SUSPENDED,表示挂起
也可能返回同步运行的结果,甚至可能返回 null为了适配所有的可能性,CPS 转换后的函数返回值类型就只能是 Any?了。


4.2 小结


1.suspend修饰的函数就是挂起函数
2.挂起函数,在执行的时候并不一定都会挂起
3.挂起函数只能在其他挂起函数中被调用
4.挂起函数里包含异步操作的时候,它才会真正被挂起


5. Continuation是什么?


Continuation词源是continue,也就是继续,接下来要做的事的意思
放到程序中Continuation则代表了,接下来要执行的代码
以上面的代码为例,当程序运行 getUserInfo() 的时候,它的 Continuation则是下图红框的代码:


Continuation 就是接下来要运行的代码,剩余未执行的代码
理解了 Continuation,以后,CPS就容易理解了,它其实就是:将程序接下来要执行的代码进行传递的一种模式


CPS 转换,就是将原本的同步挂起函数转换成CallBack 异步代码的过程。
这个转换是编译器在背后做的,我们程序员对此无感知。


当然有人会问,这么简单粗暴?三个挂起函数最终变成三个 Callback 吗?
当然不是,思想仍然是CPS的思想,不过需要结合状态机
CPS状态机就是协程实现的核心


6. 状态机


kotlin协程的实现依赖于状态机
想要查看其实现,可以将kotin源码反编译成字节码来查看编译后的代码
关于字节码的分析之前已经有很多人做过了,而且做的很好。下面给出状态机的演示。



  1. 协程实现的核心就是CPS变换与状态机

  2. 协程执行到挂起函数,一个函数如果被挂起了,它的返回值会是:CoroutineSingletons.COROUTINE_SUSPENDED

  3. 挂起函数执行完成后,通过Continuation.resume方法回调,这里的Continuation是通过CPS传入的

  4. 传入的Continuation实际上是ContinuationImpl,resume方法最后会再次回到invokeSuspend方法中

  5. invokeSuspend方法即是我们写的代码执行的地方,在协程运行过程中会执行多次

  6. invokeSuspend中通过状态机实现状态的流转

  7. continuation.label 是状态流转的关键,label改变一次代表协程发生了一次挂起恢复

  8. 通过break label实现goTo的跳转效果

  9. 我们写在协程里的代码,被拆分到状态机里各个状态中,分开执行

  10. 每次协程切换后,都会检查是否发生异常

  11. 切换协程之前,状态机会把之前的结果以成员变量的方式保存在 continuation 中。


以上是状态机流转的大概流程,读者可跟着参考链接,过一下编译后的字节码执行流程后,再来判断这个流程是否正确


7. CoroutineContext是什么?


我们上面说了Continuation是继续要执行的代码,在实现上它也是一个接口


public interface Continuation<in T> {
public val context: CoroutineContext
public fun resumeWith(result: Result<T>)
}

1.Continuation主要由两部分组成,一个context,一个resumeWith方法
2.通过resumeWith方法执行接下去的代码
3.通过context获取上下文资源,保存挂起时的一些状态与资源



CoroutineContext即上下文,主要承载了资源获取,配置管理等工作,是执行环境相关的通用数据资源的统一提供者



CoroutineContext是一个特殊的集合,这个集合它既有Map的特点,也有Set的特点


集合的每一个元素都是Element,每个Element都有一个Key与之对应,对于相同KeyElement是不可以重复存在的Element之间可以通过+号组合起来,Element有几个子类,CoroutineContext也主要由这几个子类组成:



  • Job:协程的唯一标识,用来控制协程的生命周期(newactivecompletingcompletedcancellingcancelled);

  • CoroutineDispatcher:指定协程运行的线程(IODefaultMainUnconfined);

  • CoroutineName: 指定协程的名称,默认为coroutine;

  • CoroutineExceptionHandler: 指定协程的异常处理器,用来处理未捕获的异常.


7.1 CoroutineContext的数据结构


先来看看CoroutineContext的全家福


public interface CoroutineContext {

//操作符[]重载,可以通过CoroutineContext[Key]这种形式来获取与Key关联的Element
public operator fun <E : Element> get(key: Key<E>): E?

//它是一个聚集函数,提供了从left到right遍历CoroutineContext中每一个Element的能力,并对每一个Element做operation操作
public fun <R> fold(initial: R, operation: (R, Element) -> R): R

//操作符+重载,可以CoroutineContext + CoroutineContext这种形式把两个CoroutineContext合并成一个
public operator fun plus(context: CoroutineContext): CoroutineContext

//返回一个新的CoroutineContext,这个CoroutineContext删除了Key对应的Element
public fun minusKey(key: Key<*>): CoroutineContext

//Key定义,空实现,仅仅做一个标识
public interface Key<E : Element>

//Element定义,每个Element都是一个CoroutineContext
public interface Element : CoroutineContext {

//每个Element都有一个Key实例
public val key: Key<*>

//...
}
}

1.CoroutineContext内主要存储的就是Element,可以通过类似map[key] 来取值


2.Element也实现了CoroutineContext接口,这看起来很奇怪,为什么元素本身也是集合呢?主要是为了API设计方便,Element内只会存放自己


3.除了plus方法,CoroutineContext中的其他三个方法都被CombinedContextElementEmptyCoroutineContext重写


4.CombinedContext就是CoroutineContext集合结构的实现,它里面是一个递归定义,Element就是CombinedContext中的元素,而EmptyCoroutineContext就表示一个空的CoroutineContext,它里面是空实现


7.2 为什么CoroutineContext可以通过+号连接


CoroutineContext能通过+号连接,主要是因为重写了plus方法
当通过+号连接时,实际上是包装到了CombinedContext中,并指向上一个Context


如上所示,是一个单链表结构,在获取时也是通过这种方式去查询对应的key,操作大体逻辑都是先访问当前element,不满足,再访问leftelement,顺序都是从rightleft


最近我整理一些Android 开发相关的学习文档、面试题,希望能帮助到大家学习提升,如有需要参考的可以点击链接领取**点击这里免费领取点击这里免费领取





————————————————
版权声明:本文为CSDN博主「码农 小生」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/m0_58350991/article/details/117933297

收起阅读 »

Android tess_two Android图片文字识别

ocr
先看效果图 我主要是识别截图,所以图片比较规范,识别率应该很高。 简介什么都不说了,直接看简单的用法吧 首先肯定是引入依赖了 dependencies { compile 'com.rmtheis:tess-two:6.2.0' } 简单的用法...
继续阅读 »


先看效果图


我主要是识别截图,所以图片比较规范,识别率应该很高。


简介什么都不说了,直接看简单的用法吧


首先肯定是引入依赖了


dependencies {
compile 'com.rmtheis:tess-two:6.2.0'
}

简单的用法其实就几行代码:


TessBaseAPI tessBaseAPI = new TessBaseAPI();
tessBaseAPI.init(DATAPATH, DEFAULT_LANGUAGE);//参数后面有说明。
tessBaseAPI.setImage(bitmap);
String text = tessBaseAPI.getUTF8Text();

就这样简单的把一个bitmap设置进去,就能识别到里面的文字并输出了。
但是真正用的时候还是遇到了点麻烦,虽然只是简单的识别。
主要是tessBaseAPI.init(DATAPATH, DEFAULT_LANGUAGE)这个方法容易出错。
先看一下这个方法的源码吧:


public boolean init(String datapath, String language) {
return init(datapath, language, OEM_DEFAULT);
}
/**
* Initializes the Tesseract engine with the specified language model(s). Returns
* true on success.
*
* @see #init(String, String)
*
* @param datapath the parent directory of tessdata ending in a forward
* slash
* @param language an ISO 639-3 string representing the language(s)
* @param ocrEngineMode the OCR engine mode to be set
* @return true on success
*/

public boolean init(String datapath, String language, int ocrEngineMode) {
if (datapath == null)
throw new IllegalArgumentException("Data path must not be null!");
if (!datapath.endsWith(File.separator))
datapath += File.separator;

File datapathFile = new File(datapath);
if (!datapathFile.exists())
throw new IllegalArgumentException("Data path does not exist!");

File tessdata = new File(datapath + "tessdata");
if (!tessdata.exists() || !tessdata.isDirectory())
throw new IllegalArgumentException("Data path must contain subfolder tessdata!");

//noinspection deprecation
if (ocrEngineMode != OEM_CUBE_ONLY) {
for (String languageCode : language.split("\\+")) {
if (!languageCode.startsWith("~")) {
File datafile = new File(tessdata + File.separator +
languageCode + ".traineddata");
if (!datafile.exists())
throw new IllegalArgumentException("Data file not found at " + datafile);
}
}
}

boolean success = nativeInitOem(mNativeData, datapath, language, ocrEngineMode);

if (success) {
mRecycled = false;
}

return success;
}

注意


从下面的方法中抛出的几个异常可以看出来,初始化的时候,第一个参数是个文件夹,而且这个文件夹中必须有一个tessdata的文件夹;而且这个文件夹中要有个文件叫做 第二个参数.traineddata 。具体的可以看下面代码里的注释。这些文件夹和文件没有的一定要创建好,不然会报错。


第二个参数.traineddata 是个什么文件呢?
这个是识别用到的语言库还是文字库什么的,按那个初始化方法的意思是哟啊放到SD卡中的。可以在下面的地址下载。我的demo里把这个文件放在了assets中,启动的时候复制到内存卡里。
https://github.com/tesseract-ocr/tessdata


chi_sim.traineddata应该是健体中文吧,我用的是这个。中英文都能识别。


代码


下面是主要代码:


import android.Manifest;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.support.annotation.NonNull;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import com.googlecode.tesseract.android.TessBaseAPI;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

public class MainActivity extends AppCompatActivity {

private static final String TAG = "MainActivity";
private Button btn;
private TextView tv;

/**
* TessBaseAPI初始化用到的第一个参数,是个目录。
*/

private static final String DATAPATH = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator;
/**
* 在DATAPATH中新建这个目录,TessBaseAPI初始化要求必须有这个目录。
*/

private static final String tessdata = DATAPATH + File.separator + "tessdata";
/**
* TessBaseAPI初始化测第二个参数,就是识别库的名字不要后缀名。
*/

private static final String DEFAULT_LANGUAGE = "chi_sim";
/**
* assets中的文件名
*/

private static final String DEFAULT_LANGUAGE_NAME = DEFAULT_LANGUAGE + ".traineddata";
/**
* 保存到SD卡中的完整文件名
*/

private static final String LANGUAGE_PATH = tessdata + File.separator + DEFAULT_LANGUAGE_NAME;

/**
* 权限请求值
*/

private static final int PERMISSION_REQUEST_CODE=0;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btn = (Button) findViewById(R.id.btn);
tv = (TextView) findViewById(R.id.tv);

if (Build.VERSION.SDK_INT >= 23) {
if (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED ||
checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE}, PERMISSION_REQUEST_CODE);
}
}

//Android6.0之前安装时就能复制,6.0之后要先请求权限,所以6.0以上的这个方法无用。
copyToSD(LANGUAGE_PATH, DEFAULT_LANGUAGE_NAME);

btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Thread(new Runnable() {
@Override
public void run() {
Log.i(TAG, "run: kaishi " + System.currentTimeMillis());

Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.quanbu);
Log.i(TAG, "run: bitmap " + System.currentTimeMillis());

TessBaseAPI tessBaseAPI = new TessBaseAPI();

tessBaseAPI.init(DATAPATH, DEFAULT_LANGUAGE);

tessBaseAPI.setImage(bitmap);
final String text = tessBaseAPI.getUTF8Text();
Log.i(TAG, "run: text " + System.currentTimeMillis() + text);
runOnUiThread(new Runnable() {
@Override
public void run() {
tv.setText(text);
}
});

tessBaseAPI.end();
}
}).start();
}
});

}

/**
* 将assets中的识别库复制到SD卡中
* @param path 要存放在SD卡中的 完整的文件名。这里是"/storage/emulated/0//tessdata/chi_sim.traineddata"
* @param name assets中的文件名 这里是 "chi_sim.traineddata"
*/

public void copyToSD(String path, String name) {
Log.i(TAG, "copyToSD: "+path);
Log.i(TAG, "copyToSD: "+name);

//如果存在就删掉
File f = new File(path);
if (f.exists()){
f.delete();
}
if (!f.exists()){
File p = new File(f.getParent());
if (!p.exists()){
p.mkdirs();
}
try {
f.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}

InputStream is=null;
OutputStream os=null;
try {
is = this.getAssets().open(name);
File file = new File(path);
os = new FileOutputStream(file);
byte[] bytes = new byte[2048];
int len = 0;
while ((len = is.read(bytes)) != -1) {
os.write(bytes, 0, len);
}
os.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (is != null)
is.close();
if (os != null)
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}

}

/**
* 请求到权限后在这里复制识别库
* @param requestCode
* @param permissions
* @param grantResults
*/

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
Log.i(TAG, "onRequestPermissionsResult: "+grantResults[0]);
switch (requestCode){
case PERMISSION_REQUEST_CODE:
if (grantResults.length>0&&grantResults[0]==PackageManager.PERMISSION_GRANTED){
Log.i(TAG, "onRequestPermissionsResult: copy");
copyToSD(LANGUAGE_PATH, DEFAULT_LANGUAGE_NAME);
}
break;
default:
break;
}
}
}



GitHub:https://github.com/rmtheis/tess-two

Demo的GitHub地址:https://github.com/wangyisll/TessTwoDemo

下载地址:tess-two

收起阅读 »

Android 注解知多少

注解的概念什么是注解?注解又称为标注,用于为代码提供元数据。 作为元数据,注解不直接影响你的代码执行,但也有一些类型的注解实际上可以用于这一目的。可以作用在类、方法、变量、参数和包等上。 你可以通俗的理解成“标签”,这个标签可以标记类、方法、变量、参数和包。什...
继续阅读 »

注解的概念

什么是注解?

注解又称为标注,用于为代码提供元数据。 作为元数据,注解不直接影响你的代码执行,但也有一些类型的注解实际上可以用于这一目的。可以作用在类、方法、变量、参数和包等上。 你可以通俗的理解成“标签”,这个标签可以标记类、方法、变量、参数和包。

什么用处?

  1. 生成文档;
  2. 标识代码,便于查看;
  3. 格式检查(编译时);
  4. 注解处理(编译期生成代码、xml文件等;运行期反射解析;常用于三方框架)。

分类

  1. 元注解

元注解是用于定义注解的注解,元注解也是 Java 自带的标准注解,只不过用于修饰注解,比较特殊。 2. 内置的标准注解 就是用在代码上的注解,不同的语言或环境提供有不同的注解(Java Kotlin Android)。使用这些注解后编译器就会进行检查。 3. 自定义注解 用户可以根据自己的需求定义注解。

标准的注解讲解

Java 的标准注解

元注解在后面讲解自定义注解时再一起介绍,这里只先介绍标准注解。

名称描述
@Override检查该方法是否正确地重写了父类的方法。如果重写错误,会报编译错误;
@Deprecated标记过时方法。如果使用该方法,会报编译警告;
@SuppressWarnings指示编译器去忽略注解中声明的警告;
@SafeVarargs忽略任何使用参数为泛型变量的方法或构造函数调用产生的警告;(Java 7 开始支持)
@FunctionalInterface标识一个匿名函数或函数式接口(Java 8 开始支持)

Android 注解库

support.annotation 是 Android 提供的注解库,与 Android Studio 内置的代码检查工具配合,注解可以帮助检测可能发生的问题,例如 null 指针异常和资源类型冲突等。

使用前配置 在 Module 的 build.gradle 中添加配置:

implementation 'com.android.support:support-annotations:版本号'

注意:如果您使用 appcompat 库,则无需添加 support-annotations 依赖项。因为 appcompat 库已经依赖注解库。(一般创建项目时已自动导入)

Null 性注解

  • @Nullable 可以为 null
  • @NonNull 不可为 null

用于给定变量、参数或返回值是否可以为 null 。

import android.support.annotation.NonNull;
...
@NonNull // 检查 onCreateView() 方法本身是否会返回 null。
@Override
public View onCreateView(String name, @NonNull Context context,
@NonNull AttributeSet attrs) {
...
}
...

资源注解

验证资源类型时非常有用,因为 Android 对资源的引用以整型形式传递。如果代码需要一个参数来引用特定类型的资源,可以为该代码传递预期的引用类型 int,但它实际上会引用其他类型的资源,如 R.string.xxx 资源。

Android 中的资源类型有很多,Android 注解为每种资源类型都提供了相对应的注解。

  • AnimatorRes //动画资源(一般为属性动画)
  • AnimRes //动画资源(一般为视图动画)
  • AnyRes //任何类型的资源引用,int 格式
  • ArrayRes //数组资源 e.g. android.R.array.phoneTypes
  • AttrRes //属性资源 e.g. android.R.attr.action
  • BoolRes //布尔资源
  • ColorRes //颜色资源
  • DimenRes //尺寸资源
  • DrawableRes //可绘制资源
  • FontRes //字体资源
  • FractionRes //百分比数字资源
  • IdRes //Id 引用
  • IntegerRes //任意整数类型资源引用
  • InterpolatorRes //插值器资源 e.g. android.R.interpolator.cycle
  • LayoutRes //布局资源
  • MenuRes //菜单资源
  • NavigationRes //导航资源
  • PluralsRes //字符串集合资源
  • RawRes //Raw 资源
  • StringRes //字符串资源
  • StyleableRes //样式资源
  • StyleRes //样式资源
  • TransitionRes //转场动画资源
  • XmlRes //xml 资源
  • 使用 @AnyRes 可以指明添加了此类注解的参数可以是任何类型的 R 资源。
  • 尽管可以使用 @ColorRes 指定某个参数应为颜色资源,但系统不会将颜色整数(采用 RRGGBB 或 AARRGGBB 格式)识别为颜色资源。您可以改用 @ColorInt 注解来指明某个参数必须为颜色整数。

线程注解

线程注解可以检查某个方法是否从特定类型的线程调用。支持以下线程注解:

  • @MainThread
  • @UiThread
  • @WorkerThread
  • @BinderThread
  • @AnyThread

注意:构建工具会将 @MainThread 和 @UiThread 注解视为可互换,因此您可以从 @MainThread 方法调用 @UiThread 方法,反之亦然。不过,如果系统应用有多个视图在不同的线程上,那么界面线程可能会与主线程不同。因此,您应使用 @UiThread 为与应用的视图层次结构关联的方法添加注解,并使用 @MainThread 仅为与应用生命周期关联的方法添加注解。

如果某个类中的所有方法具有相同的线程要求,您可以为该类添加一个线程注解,以验证该类中的所有方法是否从同一类型的线程调用。

值约束注解

值约束注解可以验证所传递参数的值是否在指定范围内:

  • @IntRange
  • @FloatRange
  • @Size

@IntRange 和 @FloatRange 在应用到用户可能会弄错范围的参数时最为有用。

// 确保 alpha 参数是包含 0 到 255 之间的整数值
public void setAlpha(@IntRange(from=0,to=255) int alpha) { ... }

// 确保 alpha 参数是包含 0.0 到 1.0 之间的浮点值
public void setAlpha(@FloatRange(from=0.0, to=1.0) float alpha) {...}

@Size 注解可以检查集合或数组的大小,以及字符串的长度。@Size 注解可用于验证以下特性:

  • 最小大小(例如 @Size(min=2)
  • 最大大小(例如 @Size(max=2)
  • 确切大小(例如 @Size(2)
  • 大小必须是指定数字的倍数(例如 @Size(multiple=2)

例如,@Size(min=1) 可以检查某个集合是否不为空,@Size(3) 可以验证某个数组是否正好包含三个值。

// 确保 location 数组至少包含一个元素
void getLocation(View button, @Size(min=1) int[] location) {
button.getLocationOnScreen(location);
}

权限注解

使用 @RequiresPermission 注解可以验证方法调用方的权限。要检查有效权限列表中是否存在某个权限,请使用 anyOf 属性。要检查是否具有某组权限,请使用 allOf 属性。

// 以确保 setWallpaper() 方法调用方具有 permission.SET_WALLPAPERS 权限
@RequiresPermission(Manifest.permission.SET_WALLPAPER)
public abstract void setWallpaper(Bitmap bitmap) throws IOException;
// 要求 copyImageFile() 方法的调用方具有对外部存储空间的读取权限,以及对复制的映像中的位置元数据的读取权限
@RequiresPermission(allOf = {
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.ACCESS_MEDIA_LOCATION})

public static final void copyImageFile(String dest, String source) {
//...
}

对于 intent 的权限,请在用来定义 intent 操作名称的字符串字段上添加权限要求:

@RequiresPermission(android.Manifest.permission.BLUETOOTH)
public static final String ACTION_REQUEST_DISCOVERABLE =
"android.bluetooth.adapter.action.REQUEST_DISCOVERABLE";

如果您需要对内容提供程序拥有单独的读取和写入访问权限,则需要将每个权限要求封装在 @RequiresPermission.Read 或 @RequiresPermission.Write 注解中:

@RequiresPermission.Read(@RequiresPermission(READ_HISTORY_BOOKMARKS))
@RequiresPermission.Write(@RequiresPermission(WRITE_HISTORY_BOOKMARKS))
public static final Uri BOOKMARKS_URI = Uri.parse("content://browser/bookmarks");

返回值注解

使用 @CheckResult 注解可检查是否对方法的返回值进行处理,验证是否实际使用了方法的结果或返回值。

这个可能比较难理解,这里借助 Java String.trim() 举个例子解释一下(通过例子应该能够很直观的理解了,不需要过多解释了):

String str = new String("    http://www.ocnyang.com    ");
// 删除头尾空白
System.out.println("网站:" + str.trim() + "。");//打印结果:网站:www.ocnyang.com。
System.out.println("网站:" + str + "。");//打印结果:网站: http://www.ocnyang.com

以下示例为 checkPermissions() 方法添加了注解,以确保会实际引用该方法的返回值。此外,这还会将 enforcePermission() 方法指定为要向开发者建议的替代方法:

@CheckResult(suggest="#enforcePermission(String,int,int,String)")
public abstract int checkPermission(@NonNull String permission, int pid, int uid);

CallSuper 注解

@CallSuper 注解主要是用来强调在覆盖父类方法的时候,在实现父类的方法时及时调用对应的 super.xxx() 方法,当使用 @CallSuper 修饰了某个方法,如果子类覆盖父类该方法后没有实现对父类方法的调用就会报错。

Keep 注解

使用 @Keep 注解可以确保在构建混淆缩减代码大小时,不会移除带有该注解的类或方法。 该注解通常添加到通过反射访问的方法和类,以防止编译器将代码视为未使用。

注意:使用 @Keep 添加注解的类和方法会始终包含在应用的 APK 中,即使您从未在应用逻辑中引用这些类和方法也是如此。

代码公开范围注解(了解)

单元测试中可能要访问到一些不可见的类、函数或者变量,这时可以使用@VisibleForTesting 注解来对其可见。

Typedef 注解

枚举 Enum 在 Java 中是一个完整的类。而枚举中的每一个值在枚举类中都是一个对象。所以在我们使用时枚举的值将比整数常量消耗更多的内存。 那么我们最好使用常量来替代枚举。可是使用了常量代替后又不能限制取值了。上面这两个注解就是为了解决这个问题的。

@IntDef 和 @StringDef 注解是 Android 提供的魔术变量注解,您可以创建整数集和字符串集的枚举来代理 Java 的枚举类。 它将帮助我们在编译代码时期像 Enum 那样选择变量的功能。 @IntDef 和 typedef 作用非常类似,你可以创建另外一个注解,然后用 @IntDef 指定一个你期望的整型常量值列表,最后你就可以用这个定义好的注解修饰你的 API 了。接下来我们来使用 @IntDef 来替换 Enum 看一下.

public class MainActivity extends Activity {
public static final int SUNDAY = 0;
public static final int MONDAY = 1;
{...省略部分}

@IntDef({SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY})
@Retention(RetentionPolicy.SOURCE)
public @interface WeekDays {
}

@WeekDays
int currentDay = SUNDAY;

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

setCurrentDay(WEDNESDAY);

@WeekDays int today = getCurrentDay();
switch (today) {
case SUNDAY:
break;
case MONDAY:
break;
{...省略部分}
default:
break;
}
}

/**
* 参数只能传入在声明范围内的整型,不然编译通不过
* @param currentDay
*/

public void setCurrentDay(@WeekDays int currentDay) {
this.currentDay = currentDay;
}

@WeekDays
public int getCurrentDay() {
return currentDay;
}
}

说明:

  1. 声明一些必要的 int 常量
  2. 声明一个注解为 WeekDays
  3. 使用 @IntDef 修饰 WeekDays,参数设置为待枚举的集合
  4. 使用 @Retention(RetentionPolicy.SOURCE) 指定注解仅存在与源码中,不加入到 class 文件中

需要在调用时只能传入指定类型,如果传入类型不对,编译不通过。

我们也可以指定整型值作为标志位,也就是说这些整型值可以使用 ’|’ 或者 ’&’ 进行与或等操作。如果我们把上面代码中的注解定义为如下标志位:

@IntDef(flag = true, value = {SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY})
public @interface Flavour {
}

那么可以如下调用:

setCurrentDay(SUNDAY & WEDNESDAY);

@StringDef 同理。

自定义注解

Java 元注解

名字描述
@Retention标识这个注解怎么保存,是只在代码中,还是编入class文件中,或者是在运行时可以通过反射访问
@Documented标记这些注解是否包含在用户文档中,即包含到 Javadoc 中去
@Target标记这个注解的作用目标
@Inherited标记这个注解是继承于哪个注解类
@RepeatableJava 8 开始支持,标识某注解可以在同一个声明上使用多次

@Retention
表示注解保留时间长短。可选的参数值在枚举类型 java.lang.annotation.RetentionPolicy 中,取值为:

  • RetentionPolicy.SOURCE:注解只在源码阶段保留,在编译器进行编译时它将被丢弃忽视,不会写入 class 文件;
  • RetentionPolicy.CLASS:注解只被保留到编译进行的时候,会写入 class 文件,它并不会被加载到 JVM 中;
  • RetentionPolicy.RUNTIME:注解可以保留到程序运行的时候,它会被加载进入到 JVM 中,所以在程序运行时可以反射获取到它们。

@Target
用于指明被修饰的注解最终可以作用的目标是谁,也就是指明,你的注解到底是用来修饰方法的?修饰类的?还是用来修饰字段属性的。 可能的值在枚举类 java.lang.annotation.ElementType 中,包括:

  • ElementType.TYPE:允许被修饰的注解作用在类、接口和枚举上;
  • ElementType.FIELD:允许作用在属性字段上;
  • ElementType.METHOD:允许作用在方法上;
  • ElementType.PARAMETER:允许作用在方法参数上;
  • ElementType.CONSTRUCTOR:允许作用在构造器上;
  • ElementType.LOCAL_VARIABLE:允许作用在本地局部变量上;
  • ElementType.ANNOTATION_TYPE:允许作用在注解上;
  • ElementType.PACKAGE:允许作用在包上。

@Target 注解的参数也可以接收一个数组,表示可以作用在多种目标类型上,如: @Target({ElementType.FIELD, ElementType.LOCAL_VARIABLE})

自定义注解

你可以根据需要自定义一些自己的注解,然后要在需要的地方加上自定义的注解。需要注意的是每当自定义注解时,相对应的一定要有处理这些自定义注解的流程,要不然可以说是没有实用价值的。注解真真的发挥作用,主要就在于注解处理方法。 注解的处理一般分为两种:

  • 保留注解信息到运行时,这时通过反射操作获取到类、方法和字段的注解信息,然后做相对应的处理
  • 保留到编译期,一般此方式是利用 APT 注释解释器,根据注解自动生成代码。简单来说,可以通过 APT,根据规则,帮我们生成代码、生成类文件。ButterKnife、Dagger、EventBus 等开源库都是利用注解实现的。

因为自定义注解的涉及到的内容较多。本期先不对自定义注解详细展开介绍,后续找时间再对它进行单独的文章讲解。

收起阅读 »

手把手带你走一遍Compose重组流程

前言我们都知道 Jetpack Compose 是一套声明式 UI 系统,当 UI 组件所依赖的状态发生改变时会自动发生重绘刷新,这个过程被官方称作重组,前面已经有人总结过 Compose 的重组范围了,文章详见 《Compose 的重组会影响性能吗?聊一聊 ...
继续阅读 »

前言

我们都知道 Jetpack Compose 是一套声明式 UI 系统,当 UI 组件所依赖的状态发生改变时会自动发生重绘刷新,这个过程被官方称作重组,前面已经有人总结过 Compose 的重组范围了,文章详见 《Compose 的重组会影响性能吗?聊一聊 recomposition scope》 ,并且也有人总结过重组过程使用到的快照系统,文章详见《Jetpack Compose · 快照系统》。本文就就带领大家一起来看看 Compose 源码中从状态更新到 recompose 过程的发生到底是如何进行的,并且快照系统是在 recompose 过程中如何被使用到的。

意义

本文将通过阅读源码的方式来解读 recompose 流程,阅读源码其实每个人都可以做到,但阅读源码本身是一个非常枯燥的过程,源码中存在着大量逻辑分支导致许多人看着看着就被绕晕了。本文将带领大家以 recompose 主线流程为导向来进行源码过程分析,许多与主线流程无关的逻辑分支都已被我剔除了,大家可以放心进行阅读。希望后来者能够在本文源码过程分析基础上继续深入探索下去。

⚠️ Tips:由于 recompose 流程十分复杂,本文目前仅对 recompose 主线流程进行了描述,其中很多很多技术细节没有深挖,等待后续进行补充。本人采用动静结合的方式进行源码分析,可能有些case流程没有覆盖到,如果文章存在错误欢迎在评论区进行补充。

recompose 流程分析

从 MutableState 更新开始

当你为 MutableState 赋值时将会默认调用 MutableState 的扩展方法 MutableState.setValue

// androidx.compose.runtime.SnapshotState
inline operator fun MutableState.setValue(thisObj: Any?, property: KProperty<*>, value: T) {
this.value = value
}

通过查看 mutableStateOf 源码我们可以发现 MutableState 实际上是一个 SnapshotMutableStateImpl 类型实例

// androidx.compose.runtime.SnapshotState
fun mutableStateOf(
value: T,
policy: SnapshotMutationPolicy<T> = structuralEqualityPolicy()
): MutableState = createSnapshotMutableState(value, policy)

// androidx.compose.runtime.ActualAndroid.android
internal actual fun createSnapshotMutableState(
value: T,
policy: SnapshotMutationPolicy<T>
): SnapshotMutableState = ParcelableSnapshotMutableState(value, policy)

// androidx.compose.runtime.ParcelableSnapshotMutableState
internal class ParcelableSnapshotMutableState<T>(
value: T,
policy: SnapshotMutationPolicy
) : SnapshotMutableStateImpl(value, policy), Parcelable

当 value 属性发生改变时会调用这个属性的 setter ,当然如果读取状态时也会走 getter。

此时的next是个 StateStateRecord 实例,其真正记录着当前state状态信息(通过当前value的getter与setter就可以看出)。此时首先会对当前值和要更新的值根据规则进行diff判断。当确定发生改变时会调用到 StateStateRecord 的 overwritable 方法。

internal open class SnapshotMutableStateImpl<T>(
value: T,
override val policy: SnapshotMutationPolicy
) : StateObject, SnapshotMutableState {
@Suppress("UNCHECKED_CAST")
override var value: T
get() = next.readable(this).value
set(value) = next.withCurrent {
if (!policy.equivalent(it.value, value)) {
// 此时的this还是当前SnapshotMutableStateImpl
next.overwritable(this, it) {
this.value = value // 此时的this指向的next,这部操作也就是更新next其中的value
}
}
}
...
private var next: StateStateRecord = StateStateRecord(value)
}

接下来会通过 Snapshot.current 获取当前上下文中的 Snapshot,如果你对 mutableState 更新操作在非Compose Scope中,其返回的实例类型是 GlobalSnapshot ,否则就是一个 MutableSnapShot。这将会影响到后续写入通知的执行流程(因为毕竟需要进行 recompose 嘛)。

⚠️ Tips:GlobalSnapshot 是 MutableSnapShot 的子类

// androidx.compose.runtime.snapshots.Snapshot
internal inline fun T.overwritable(
state: StateObject,
candidate: T,
block: T.() -> R
): R {
var snapshot: Snapshot = snapshotInitializer
return sync {
snapshot = Snapshot.current
this.overwritableRecord(state, snapshot, candidate).block() // 更新 next
}.also {
notifyWrite(snapshot, state) // 写入通知
}
}

我们进入 overwritableRecord 看看其中做了什么,注意此时 state 其实是 mutableState。在这其中通过 recordModified 方法记录了修改。我们可以看到此时将当前修改的 state 添加到当前 Snapshot 的 modified 中了,这个后续会用到的。

// androidx.compose.runtime.snapshots.Snapshot
internal fun T.overwritableRecord(
state: StateObject,
snapshot: Snapshot,
candidate: T
): T {
if (snapshot.readOnly) {
snapshot.recordModified(state)
}
val id = snapshot.id

if (candidate.snapshotId == id) return candidate

val newData = newOverwritableRecord(state, snapshot)
newData.snapshotId = id

snapshot.recordModified(state) // 记录修改

return newData
}

// androidx.compose.runtime.snapshots.Snapshot
override fun recordModified(state: StateObject) {
(modified ?: HashSet().also { modified = it }).add(state)
}

可能你对 mutableState 更新操作是否在 ComposeScope 中而感到困惑,举个例子其实就明白了。recompose 能够执行到就在 ComposeScope 中,不能执行到就不在 ComposeScope 中。

这个在后面 takeMutableSnapshot读观察者与写观察者 部分是会进行解释。

var display by mutableStateOf("Init")
@Preview
@Composable
fun Demo() {
Text (
text = display,
fontSize = 50.sp,
modifier = Modifier.clickable {
display = "change" // recompose不能执行到,此时是 GlobalSnapshot
}
)
display = "change" // recompose能够执行到,此时是 MutableSnapShot
}

接下来就是通过 notifyWrite 执行事件通知此时可以看到调用了写观察者 writeObserver 。

// androidx.compose.runtime.snapshots.Snapshot
@PublishedApi
internal fun notifyWrite(snapshot: Snapshot, state: StateObject) {
snapshot.writeObserver?.invoke(state)
}

此时会根据当前 Snapshot 不同而调用到不同的写观察者 writeObserver 。

GlobalSnapshot 写入通知

全局的写入观察者是在 setContent 时就进行了注册, 此时会回调 registerGlobalWriteObserver 的尾lambda,可以看到这里就一个channel (没错就是Kotlin协程那个热数据流Channel),我门可以看到很容易看到在上方以AndroidUiDispatcher.Main 作为调度器的 CoroutineScope 中进行了挂起等待消费,所以执行流程自然会进入 sendApplyNotifications() 之中。 (AndroidUiDispatcher.Main 与 Choreographer 息息相关,篇幅有限就不展开讨论了,有兴趣可以自己去跟源码)

internal object GlobalSnapshotManager {
private val started = AtomicBoolean(false)

fun ensureStarted() {
if (started.compareAndSet(false, true)) {
val channel = Channel<Unit>(Channel.CONFLATED)
CoroutineScope(AndroidUiDispatcher.Main).launch {
channel.consumeEach {
Snapshot.sendApplyNotifications()
}
}
Snapshot.registerGlobalWriteObserver {
channel.offer(Unit)
}
}
}
}

sendApplyNotifications

接下来,我们进入 sendApplyNotifications() 其中看看做了什么,可以看到这里使用我们前面提到的那个 modified ,当发生修改时 changes 必然为 true,所以接着会调用到 advanceGlobalSnapshot

// androidx.compose.runtime.snapshots.Snapshot
fun sendApplyNotifications() {
val changes = sync {
currentGlobalSnapshot.get().modified?.isNotEmpty() == true
}
if (changes)
advanceGlobalSnapshot()
}

我们继续往下跟下去走到了 advanceGlobalSnapshot ,此时将所有 modified 取出并便利调用 applyObservers 中包含的所有观察者。

// androidx.compose.runtime.snapshots.Snapshot
private fun advanceGlobalSnapshot() = advanceGlobalSnapshot { }

private fun advanceGlobalSnapshot(block: (invalid: SnapshotIdSet) -> T): T {
val previousGlobalSnapshot = currentGlobalSnapshot.get()
val result = sync {
takeNewGlobalSnapshot(previousGlobalSnapshot, block)
}
val modified = previousGlobalSnapshot.modified
if (modified != null) {
val observers: List<(Set, Snapshot) -> Unit> = sync { applyObservers.toMutableList() }
observers.fastForEach { observer ->
observer(modified, previousGlobalSnapshot)
}
}
....
return result
}

applyObservers之recompositionRunner

据我调查此时 applyObservers 中包含的观察者仅有两个,一个是 SnapshotStateObserver.applyObserver 用来更新快照状态信息,另一个就是 recompositionRunner 用来处理 recompose流程 的。由于我们是在研究recompose 流程的所以就不分开去讨论了。我们来看看处理 recompose 的 observer 都做了什么,首先他将所有改变的 mutableState 添加到了 snapshotInvalidations,这个后续会用到。后面可以看到有一个resume,说明lambda的最后调用的 deriveStateLocked 返回了一个协程 Continuation 实例。使得挂起点位置恢复执行,所以我们进入deriveStateLocked 看看这个协程 Continuation 实例到底是谁。

// androidx.compose.runtime.Recomposer
@OptIn(ExperimentalComposeApi::class)
private suspend fun recompositionRunner(
block: suspend CoroutineScope.(parentFrameClock: MonotonicFrameClock) -> Unit
) {
withContext(broadcastFrameClock) {
...
// 负责处理 recompose 的 observer 就是他
val unregisterApplyObserver = Snapshot.registerApplyObserver {
changed, _ ->
synchronized(stateLock) {
if (_state.value >= State.Idle) {
snapshotInvalidations += changed
deriveStateLocked()
} else null
}?.resume(Unit)
}
....
}
}

通过函数返回值可以看到这是一个可取消的Continuation实例 workContinuation

// androidx.compose.runtime.Recomposer
private fun deriveStateLocked(): CancellableContinuation<Unit>? {
....
return if (newState == State.PendingWork) {
workContinuation.also {
workContinuation = null
}
} else null
}

那这个workContinuation是在哪里赋值的呢,我们很容易就找到了其唯一被赋值的地方。此时 workContinuation 就是 co,此时resume也就是恢复执行 awaitWorkAvailable 调用挂起点。

// androidx.compose.runtime.Recomposer
private suspend fun awaitWorkAvailable() {
if (!hasSchedulingWork) {
suspendCancellableCoroutine<Unit> { co ->
synchronized(stateLock) {
if (hasSchedulingWork) {
co.resume(Unit)
} else {
workContinuation = co
}
}
}
}
}

runRecomposeAndApplyChanges 三步骤

我们可以找到在 runRecomposeAndApplyChanges 中调用 awaitWorkAvailable 而产生了挂起,所以此时会恢复调用 runRecomposeAndApplyChanges ,这里主要有三步操作接下来进行介绍

// androidx.compose.runtime.Recomposer
suspend fun runRecomposeAndApplyChanges() = recompositionRunner { parentFrameClock ->
val toRecompose = mutableListOf()
val toApply = mutableListOf()
while (shouldKeepRecomposing) {
awaitWorkAvailable()
// 从这开始恢复执行
if (
synchronized(stateLock) {
if (!hasFrameWorkLocked) {
// 步骤1
recordComposerModificationsLocked()
!hasFrameWorkLocked
} else false
}
) continue

// 等待Vsync信号,类似于传统View系统中scheduleTraversals?
parentFrameClock.withFrameNanos { frameTime ->
...
trace("Recomposer:recompose") {
synchronized(stateLock) {
recordComposerModificationsLocked()
// 步骤2
compositionInvalidations.fastForEach { toRecompose += it }
compositionInvalidations.clear()
}

val modifiedValues = IdentityArraySet()
val alreadyComposed = IdentityArraySet()
while (toRecompose.isNotEmpty()) {
try {
toRecompose.fastForEach { composition ->
alreadyComposed.add(composition)
// 步骤3
performRecompose(composition, modifiedValues)?.let {
toApply += it
}
}
} finally {
toRecompose.clear()
}
....
}
....
}
}
}
}

对于这三个步骤,我们分别来看首先是步骤1调用了 recordComposerModificationsLocked 方法, 还记得 snapshotInvalidations 嘛, 他记录着所有更改的 mutableState,此时回调所有已知composition的recordModificationsOf 方法。

// androidx.compose.runtime.Recomposer
private fun recordComposerModificationsLocked() {
if (snapshotInvalidations.isNotEmpty()) {
snapshotInvalidations.fastForEach { changes ->
knownCompositions.fastForEach { composition ->
composition.recordModificationsOf(changes)
}
}
snapshotInvalidations.clear()
if (deriveStateLocked() != null) {
error("called outside of runRecomposeAndApplyChanges")
}
}
}

经过一系列调用会将所有依赖当前 mutableState 的所有 Composable Scope 存入到 compositionInvalidations 这个 List 中。

// androidx.compose.runtime.Recomposer
internal override fun invalidate(composition: ControlledComposition) {
synchronized(stateLock) {
if (composition !in compositionInvalidations) {
compositionInvalidations += composition
deriveStateLocked()
} else null
}?.resume(Unit)
}

步骤2就很简单了,将 compositionInvalidations 的所有元素转移到了 toRecompose,而步骤3则是 recompose的重中之重,通过 performRecompose 使所有受到影响的 Composable Scope 重新执行。

performRecompose

我们可以看到 performRecompose 中间接调用了 composing ,而其中最关键 recompose 也在回调中完成,那么我们需要再进入 composing 看看什么时候会回调。

// androidx.compose.runtime.Recomposer
private fun performRecompose(
composition: ControlledComposition,
modifiedValues: IdentityArraySet<Any>?
): ControlledComposition? {
if (composition.isComposing || composition.isDisposed) return null
return if (
composing(composition, modifiedValues) {
if (modifiedValues?.isNotEmpty() == true) {
composition.prepareCompose {
modifiedValues.forEach { composition.recordWriteOf(it) }
}
}
composition.recompose() // 真正发生recompose的地方
}
) composition else null
}

composing 内部首先拍摄了一次快照,然后将我们的recompose过程在这次快照中执行,最后进行了apply。又关于快照系统的讲解详见 《Jetpack Compose · 快照系统》

// androidx.compose.runtime.Recomposer
private inline fun composing(
composition: ControlledComposition,
modifiedValues: IdentityArraySet<Any>?,
block: () -> T
): T {
val snapshot = Snapshot.takeMutableSnapshot(
readObserverOf(composition), writeObserverOf(composition, modifiedValues)
)
try {
return snapshot.enter(block)
} finally {
applyAndCheck(snapshot)
}
}

takeMutableSnapshot 读观察者与写观察者

值得注意的是此时调用的 takeMutableSnapshot 方法同时传入了一个读观察者和写观察者,而这两个观察者在什么时机回调呢?当我们每次 recompose 时都会拍摄一次快照,然后我们的重新执行过程在这次快照中执行,在重新执行过程中如果出现了 mutableState 的读取或写入操作都会相应的回调这里的读观察者和写观察者。也就说明每次recompose都会进行重新一次绑定。 读观察者回调时机比较好理解,写观察者在什么时机回调呢? 还记得我们刚开始说的 GlobalSnapshot 和 MutableSnapshot 嘛?

到这里我们一直都在分析 GlobalSnapshot 这条执行过程,通过调用 takeMutableSnapshot 将返回一个 MutableSnapshot 实例,我们的recompose重新执行过程发生在当前MutableSnapshot 实例的enter 方法中,此时重新执行过程中通过调用Snapshot.current 将返回当前MutableSnapshot 实例,所以重新执行过程中发生的写操作就会回调 takeMutableSnapshot 所传入的写观察者。也就是以下这种情况,当 Demo 发生recompose时 display所在 Snapshot 就是拍摄的MutableSnapshot 快照。

var display by mutableStateOf("Init")
@Preview
@Composable
fun Demo() {
Text (
text = display,
fontSize = 50.sp
)
display = "change" // recompose能够执行到,此时是 MutableSnapShot
}

MutableSnapshot 写入通知

接下来,我们来看看 takeMutableSnapshot 的写观察者是如何实现的。此时会将更新的值传入当前recompose composition 的 recordWriteOf 方法。

// androidx.compose.runtime.Recomposer
private fun writeObserverOf(
composition: ControlledComposition,
modifiedValues: IdentityArraySet<Any>?
): (Any) -> Unit {
return { value ->
composition.recordWriteOf(value)
modifiedValues?.add(value)
}
}

通过对于流程分析发现,实际上在recompose过程中进行状态写入操作时,并不会通过写观察者立即进行recompose 过程,而是等待到当前recompose过程结束后进行 apply 时再进行重新 recompose。

applyAndCheck

让我们回到Recomposer的 composing 方法,我们通过 applyAndCheck 完成后续 apply 操作。applyAndCheck 内部使用了 MutableSnapshot.apply

// androidx.compose.runtime.Recomposer
private inline fun composing(
composition: ControlledComposition,
modifiedValues: IdentityArraySet<Any>?,
block: () -> T
): T {
val snapshot = Snapshot.takeMutableSnapshot(
readObserverOf(composition), writeObserverOf(composition, modifiedValues)
)
try {
return snapshot.enter(block)
} finally {
applyAndCheck(snapshot) // 在这里
}
}

private fun applyAndCheck(snapshot: MutableSnapshot) {
val applyResult = snapshot.apply()
if (applyResult is SnapshotApplyResult.Failure) {
error(
"Unsupported concurrent change during composition. A state object was " +
"modified by composition as well as being modified outside composition."
)
}
}

apply中使用的applyObservers

我们再进入MutableSnapshot.apply 一探究竟,此时将当前 modified 在 snapshot.recordModified(state) 已经更新过了,忘记的话可以回头看看,前面已经讲过了。此时仍然使用了 applyObservers 进行遍历通知。这个applyObservers 其实是个静态变量,所以不同的 GlobalSnapshot 与MutableSnapshot 可以共享,接下来仍然通过预先订阅好的 recompositionRunner 用来处理 recompose 过程,详见 applyObservers之recompositionRunner,接下来的recompose流程就完全相同了。

// androidx.compose.runtime.snapshots.Snapshot
open fun apply(): SnapshotApplyResult {
val modified = modified
....
val (observers, globalModified) = sync {
validateOpen(this)
if (modified == null || modified.size == 0) {
....
} else {
....
applyObservers.toMutableList() to globalModified
}
}
....
if (modified != null && modified.isNotEmpty()) {
observers.fastForEach {
it(modified, this)
}
}
return SnapshotApplyResult.Success
}
收起阅读 »

偷师 - Kotlin 委托

关键字synchorinzedCAS委托/代理模式委托要理解 kotlin-委托 的作用和用法首先要理解什么是委托。初看委托二字如果不太理解的话不妨转换成代理二字。委托模式和代理模式是一种设计模式的两种称呼而已。委托/代理模式代理模式,字面...
继续阅读 »

关键字

  • synchorinzed
  • CAS
  • 委托/代理模式

委托

要理解 kotlin-委托 的作用和用法首先要理解什么是委托。初看委托二字如果不太理解的话不妨转换成代理二字。委托模式和代理模式是一种设计模式的两种称呼而已。

委托/代理模式

代理模式,字面理解就是自己不方便做或者不能做的事情,需要第三方代替来做,最终通过第三方来达到自己想要的目的或效果。举例:员工小李在B总公司打工,B总成天让小李加班不给加班费,小李忍受不住了,就想去法院告B总。虽然法律上允许打官司不请律师,允许自辩。但是小李第一不熟悉法律起诉的具体流程,第二嘴比较笨,人一多腿就抖得厉害。因此,小李决定去找律师帮忙打官司。找律师打官司和自己打官司相比,有相同的地方,也有不同的地方。

相同的地方在于:

  • 都需要提交原告的资料,如姓名、年龄、事情缘由、想达到的目的。
  • 都需要经过法院的取证调查,开庭争辩等过程。
  • 最后拿到审判结果。

不同地方在于:

  • 小李省事了,让专业的人做专业的事,不需要自己再去了解法院那一套繁琐复杂的流程。
  • 把握更大了。

通过上面的例子,我们注意到代理模式有几个重点。

  • 被代理的角色(小李)
  • 代理角色(律师)
  • 协议(不管是代理和被代理谁去做,都需要做的事情,抽象出来就是协议)

UML 类图: image

代码实现如下:

//协议
interface Protocol{
//登记资料
public void register(String name);
//调查案情,打官司
public void dosomething();
//官司完成,通知雇主
public void notifys();
}

//代理角色:律师类
class LawyerProxy implements Protocol{
private Employer employer;
public LawyerProxy(Employer employer){
this.employer=employer;
}
@Override
public void register(String name) {
// TODO Auto-generated method stub
this.employer.register(name);
}
public void collectInfo(){
System.out.println("作为律师,我需要根据雇主提供的资料,整理与调查,给法院写出书面文字,并提供证据。");
}
@Override
public void dosomething() {
// TODO Auto-generated method stub
collectInfo();
this.employer.dosomething();
finish();
}
public void finish(){
System.out.println("本次官司打完了...............");
}
@Override
public void notifys() {
// TODO Auto-generated method stub
this.employer.notifys();
}
}

//被代理角色:雇主类
class Employer implements Protocol{
String name=null;
@Override
public void register(String name) {
// TODO Auto-generated method stub
this.name=name;
}
@Override
public void dosomething() {
// TODO Auto-generated method stub
System.out.println("我是'"+this.name+"'要告B总,他每天让我不停的加班,还没有加班费。");
}
@Override
public void notifys() {
// TODO Auto-generated method stub
System.out.println("法院裁定,官司赢了,B总需要赔偿10万元精神补偿费。");
}
}

public class Client {
public static void main(String[] args) {
Employer employer=new Employer();
System.out.println("我受不了了,我要打官司告老板");
System.out.println("找律师解决一下吧......");
Protocol lawyerProxy=new LawyerProxy(employer);
lawyerProxy.register("朵朵花开");
lawyerProxy.dosomething();
lawyerProxy.notifys();
}
}
复制代码

运行后,打印如下:

我受不了了,我要打官司告老板
找律师解决一下吧......
作为律师,我需要根据雇主提供的资料,整理与调查,给法院写出书面文字,并提供证据。
我是'朵朵花开'要告B总,他每天让我不停的加班,还没有加班费。
本次官司打完了...............
法院裁定,官司赢了,B总需要赔偿10万元精神补偿费。
复制代码

类委托

对代理模式有了一些了解之后我们再来看 kotlin-类委托 是如何实现的:

interface Base {
fun print()
}

class BaseImpl(val x: Int) : Base {
override fun print() { print(x) }
}

class Derived(b: Base) : Base by b

fun main() {
val b = BaseImpl(10)
Derived(b).print()
}
复制代码

这是Kotlin 语言中文站的示例,转成 Javaa 代码如下:


public interface Base {
void print();
}

// BaseImpl.java
public final class BaseImpl implements Base {
private final int x;

public void print() {
int var1 = this.x;
boolean var2 = false;
System.out.print(var1);
}

public final int getX() {
return this.x;
}

public BaseImpl(int x) {
this.x = x;
}
}

// Derived.java
public final class Derived implements Base {
// $FF: synthetic field
private final Base $$delegate_0;

public Derived(@NotNull Base b) {
Intrinsics.checkNotNullParameter(b, "b");
super();
this.$$delegate_0 = b;
}

public void print() {
this.$$delegate_0.print();
}
}

// DelegateTestKt.java
public final class DelegateTestKt {
public static final void main() {
BaseImpl b = new BaseImpl(10);
(new Derived((Base)b)).print();
}

// $FF: synthetic method
public static void main(String[] var0) {
main();
}
}
复制代码

可以看到在 Derived 中已经实现了 Base 接口的抽象方法,而且方法的实际调用者是构造对象时传入的 Base 实例对象,也就是 BaseImpl 的实例对象。

对比上文介绍的代理模式:

  • Base:代理协议
  • BaseImpl:代理角色
  • Derived:被代理被代理角色

这样看的话,d上文类委托示例的结果包括重写方法实现和成员变量产生的结果的原因也就清晰明了了。

属性委托

kotlin 标准库中提供的属性委托有:

  • lazy:延迟属性;
  • Delegates.notNull():不能为空;
  • Delegates.observable():可观察属性;
  • Delegates.vetoable():可观察属性,可拒绝修改属性;

lazy 延迟属性下面再来分析,先来看 Delegates 的几个方法。

在 Delegate.kt 文件中定义了提供的标准属性委托方法,代码量很少就不贴代码了。可以看到三种委托方法都返回 ReadWriteProperty 接口的实例对象,它们的顶层接口是 ReadOnlyProperty 接口。名字就很提现它们各自的功用了:

  • ReadOnlyProperty:仅用于可读属性,val
  • ReadWriteProperty:用于可读-写属性,var

在属性委托的实现里,对应代理模式的角色如下:

  • 协议:ReadOnlyProperty 和 ReadWriteProperty
  • 代理者:Delegate
  • 被代理者:实际使用属性。

Delegates.notNull() 比较简单,拿它来分析下属性委托是如何实现的。

private class NotNullVar<T : Any>() : ReadWriteProperty<Any?, T> {
private var value: T? = null

public override fun getValue(thisRef: Any?, property: KProperty<*>): T {
return value ?: throw IllegalStateException("Property ${property.name} should be initialized before get.")
}

public override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
this.value = value
}
}
复制代码
class DelegateTest {
private val name: String by Delegates.notNull()
}
复制代码

kotlin 转 Java

public final class DelegateTest {
// $FF: synthetic field
static final KProperty[] $$delegatedProperties = new KProperty[]{(KProperty)Reflection.property1(new PropertyReference1Impl(DelegateTest.class, "name", "getName()Ljava/lang/String;", 0))};
private final ReadWriteProperty name$delegate;

private final String getName() {
return (String)this.name$delegate.getValue(this, $$delegatedProperties[0]);
}

public DelegateTest() {
this.name$delegate = Delegates.INSTANCE.notNull();
}
}
复制代码

可以看到 name 属性委托给了 NotNullVar 的 value 属性。当访问 name 属性时,其实访问的是 NotNullVar 的 value 属性。

自定义委托

上文提到 Delegates 中的委托方法都返回 ReadWriteProperty 接口的实例对象。如果需要自定义委托的话当然也是通过实现 ReadWriteProperty 接口了。

  • var 属性自定义委托:继承 ReadWriteProperty 接口,并实现 getValue()、setValue() 方法;
  • val 属性自定义委托:实现 ReadOnlyProperty 接口,并实现 getValue 方法。
public override operator fun getValue(thisRef: T, property: KProperty<*>): V

public operator fun setValue(thisRef: T, property: KProperty<*>, value: V)
复制代码

参数如下:

  • thisRef —— 必须与属性所有者类型相同或者是其超类型,通俗说就是属性所在类的类型或其父类型;
  • property —— 必须是 KProperty<*> 类型或其超类型。

Lazy

lazy 放到这里来分析是因为它虽然也是将属性委托给了其他类的属性,但它并没有继承 ReadWriteProperty 或 ReadOnlyProperty 接口并不是标准的属性委托。

lazy 源码如下:

public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)

public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
when (mode) {
LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
}
复制代码

lazy 函数接收两个参数:

  • LazyThreadSafetyMode:线程安全模式;
  • initializer:初始化函数。

LazyThreadSafetyMode:不同模式的作用如下:

  • SYNCHRONIZED:通过 Volatile + synchorinzed 锁的方式保证在多线程情况下初始化函数仅调用一次,变量仅赋值一次;
  • PUBLICATION:通过 Volatile + CAS 的方式保证在多线程情况下变量仅赋值一次;
  • NONE:线程不安全。

lazy 函数返回 Lazy 接口实例。

注意:除非你能保证 lazy 实例的永远不会在多个线程初始化,否则不应该使用 NONE 模式。

lazy 函数会根据所选模式的不同返回不同的实例对象:SynchronizedLazyImplSafePublicationLazyImplUnsafeLazyImpl。这三者之间最大的的区别在于 getter() 函数的实现,但不管如何最终都是各自类中的 value 属性代理 lazy 函数所修饰的属性。

synchorinzedCAS 都是多线程中实现锁的常用烦恼干是,关于他们的介绍可以看我之前的文章:

应用

在项目中可以应用 kotlin 委托 可以辅助简写如下功能:

  • Fragment / Activity 传参
  • ViewBinding

本节所写的两个示例是摘自

Kotlin | 委托机制 & 原理 & 应用 -- 彭丑丑 View Binding 与Kotlin委托属性的巧妙结合,告别垃圾代码! -- Kirill Rozov 著,依然范特稀西 译

kotlin 委托 + Fragment / Activity 传参

示例来源: 彭丑丑 - Kotlin | 委托机制 & 原理 & 应用 项目地址: Github - DemoHall

属性委托前:

class OrderDetailFragment : Fragment(R.layout.fragment_order_detail) {

private var orderId: Int? = null
private var orderType: Int? = null

companion object {

const val EXTRA_ORDER_ID = "orderId"
const val EXTRA_ORDER_TYPE = "orderType";

fun newInstance(orderId: Int, orderType: Int?) = OrderDetailFragment().apply {
Bundle().apply {
putInt(EXTRA_ORDER_ID, orderId)
if (null != orderType) {
putInt(EXTRA_ORDER_TYPE, orderType)
}
}.also {
arguments = it
}
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

arguments?.let {
orderId = it.getInt(EXTRA_ORDER_ID, 10000)
orderType = it.getInt(EXTRA_ORDER_TYPE, 2)
}
}
}
复制代码

定义 ArgumentDelegate.kt

fun <T> fragmentArgument() = FragmentArgumentProperty<T>()

class FragmentArgumentProperty<T> : ReadWriteProperty<Fragment, T> {

override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
return thisRef.arguments?.getValue(property.name) as? T
?: throw IllegalStateException("Property ${property.name} could not be read")
}

override fun setValue(thisRef: Fragment, property: KProperty<*>, value: T) {
val arguments = thisRef.arguments ?: Bundle().also { thisRef.arguments = it }
if (arguments.containsKey(property.name)) {
// The Value is not expected to be modified
return
}
arguments[property.name] = value
}
}
复制代码

使用属性委托后:

class OrderDetailFragment : Fragment(R.layout.fragment_order_detail) {

private lateinit var tvDisplay: TextView

private var orderId: Int by fragmentArgument()
private var orderType: Int? by fragmentArgumentNullable(2)

companion object {
fun newInstance(orderId: Int, orderType: Int?) = OrderDetailFragment().apply {
this.orderId = orderId
this.orderType = orderType
}
}

override fun onViewCreated(root: View, savedInstanceState: Bundle?) {
// Try to modify (UnExcepted)
this.orderType = 3
// Display Value
tvDisplay = root.findViewById(R.id.tv_display)
tvDisplay.text = "orderId = $orderId, orderType = $orderType"
}
}
复制代码

kotlin 委托 + ViewBinding

示例来源: ViewBindingPropertyDelegate

属性委托前:

class ProfileActivity : AppCompatActivity(R.layout.activity_profile) {

private var binding: ActivityProfileBinding? = null

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

binding = ActivityProfileBinding.inflate(layoutInflater)
binding!!.profileFragmentContainer
}
}
复制代码

属性委托后:

class ProfileActivity : AppCompatActivity(R.layout.activity_profile) {

private val viewBinding: ActivityProfileBinding by viewBinding()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
with(viewBinding) {
profileFragmentContainer
}
}
}
复制代码

使用过后代码非常的简洁而且也不需要再用 !! 或者定义一个新的变量,有兴趣的同学可以去看下源码。

收起阅读 »

【iOS】自动布局之Purelayout

masonry这个第三方库件在github上很出名,貌似也很好用,但是我在看过masonry的介绍和使用方法之后,觉得有点隐隐的蛋疼。因为本人工作时间不多,加上一直都用的是Objective-C,看着masonry提供的方法基本上都是点语法,我的[]呢?!!怎...
继续阅读 »

masonry这个第三方库件在github上很出名,貌似也很好用,但是我在看过masonry的介绍和使用方法之后,觉得有点隐隐的蛋疼。
因为本人工作时间不多,加上一直都用的是Objective-C,看着masonry提供的方法基本上都是点语法,我的[]呢?!!怎么不在了?

于是在github上搜索到另外一个较出名的布局,便有了这段Purelayout的尝试。

生成一个UIView:

UIView *view = [UIView newAutoLayoutView];
+ (instancetype)newAutoLayoutView
{
ALView *view = [self new];
view.translatesAutoresizingMaskIntoConstraints = NO;
return view;
}

newAutoLayoutView是UIView的一个扩展方法,其实达到的目的就是生成一个UIView实例,并把该实例的translatesAutoresizingMaskIntoConstraints属性置为NO。这个属性值在默认情况下是YES,如果设置为 NO,那么在运行时,程序不会自动将AutoresizingMask转化成 Constraint。

1.view相对于父容器间距的位置

[view autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:10];//相对于父容器顶部距离10
[view autoPinEdgeToSuperviewEdge:ALEdgeLeft withInset:10];//相对于父容器左部距离10
[view autoPinEdgeToSuperviewEdge:ALEdgeRight withInset:10];//相对于父容器右部距离10
[view autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:10];//相对于父容器底部距离10

值得注意的是Purelayout对UILabel做了一些人性化的处理:
在有的国家地区文字是从右至左的,以下代码就是将label的起始位置距离父容器10

[label autoPinEdgeToSuperviewEdge:ALEdgeLeading withInset:10];

2.相对于父容器的中心位置:

[view autoCenterInSuperview];//view在父容器中心位置
[view autoAlignAxisToSuperviewAxis:ALAxisHorizontal];//view在父容器水平中心位置
[view autoAlignAxisToSuperviewAxis:ALAxisVertical];//view在父容器垂直中心位置

3.设置大小

[view autoSetDimensionsToSize:CGSizeMake(300, 300)];//设置view的大小为300*300
[view autoSetDimension:ALDimensionHeight toSize:300];//设置view的高度为300
[view autoSetDimension:ALDimensionWidth toSize:300];//设置view的宽度为300

4.相对位置
NSLayoutRelation是一个枚举类型:

typedef NS_ENUM(NSInteger, NSLayoutRelation) {
NSLayoutRelationLessThanOrEqual = -1,
NSLayoutRelationEqual = 0,
NSLayoutRelationGreaterThanOrEqual = 1,
};

见名知意,你懂的。

[view1 autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:view2 withOffset:20 relation:NSLayoutRelationEqual];//view1的顶部在view2的底部的20像素的位置

5.中心对齐

[view1 autoAlignAxis:ALAxisVertical toSameAxisOfView:view2];//view1相对于view2保持在同一个垂直中心上

view1相对于view2保持在同一个垂直中心上

6.相对大小

[view1 autoMatchDimension:ALDimensionWidth toDimension:ALDimensionWidth ofView:view2];

view1的宽度和view2的宽度相等

在使用purelayout的时候值得注意:
1.purelayout提供的方法有些是只支持iOS8及以上的,如果iOS7及以下的调用了是会奔溃的,本人就是因为这个被搞得欲仙欲死。好在purelayout在方法中都有介绍。以上介绍的几种使用场景的方法,也都是支持iOS7及以下系统的。
2.在view父容器为nil的时候,执行purelayout的方法会崩溃。

有兴趣的可以直接去github下载官方的demo,写的也是相当ok的。

持续更新~~~

链接:https://www.jianshu.com/p/15bb1bfec5e9

收起阅读 »

【开源项目】使用环信IM开发的一款仿微信APP

项目背景:为了让更多的小伙伴们能够使用环信快速开发出一款自己的社交通讯APP,现进行开源 产品功能:易用IM是一款仿微信APP,包含以下主要功能:1. 单聊,群聊,群聊天中可发随机红包2. 通讯录:管理好友和群组3. 朋友圈:展示自己和好友发的全部可见的动态,...
继续阅读 »

项目背景

为了让更多的小伙伴们能够使用环信快速开发出一款自己的社交通讯APP,现进行开源

 

产品功能:

易用IM是一款仿微信APP,包含以下主要功能:

1. 单聊群聊,群聊天中可发随机红包

2. 通讯录:管理好友和群组

3. 朋友圈展示自己和好友发的全部可见的动态,可点赞、评论、回复和收藏

4. 支付宝充值余额、提现

5. 余额充值提现功能

6. 表情商店:后台维护表情包,用户可一键添加到自己的聊天中

 

软件架构

1. 使用ThinkPHP3.2.3框架开发

2. 数据库mysql5.7

3. IM功能集成环信即时通讯

4. 集成极光推送、阿里云OSS

5. 百度地图



资源地址:

服务端 https://gitee.com/491290710/EasyIM_Service.git

安卓端 https://gitee.com/491290710/EasyIM_Android.git

IOS端 https://gitee.com/491290710/EasyIM_IOS.git 

 

安装教程

1. 服务器建议使用centos7+,运行环境使用lnmp1.5-1.6一键安装

2. 第三方开发参数请在Application/Common/Conf/config.php中进行配置

3. WEB端代码在layim目录中,访问方式为 您的域名/layim

4. 推荐使用阿里云服务器ECS,优惠购买请点击

https://partner.aliyun.com/shop/20690101/newusers?marketer=286

 

使用说明:

1. WEB端体验地址 http://weixin.pro2.liuniukeji.net/layim

2. 可自行注册账号,注册时验证码输入 654321

 

 

项目截图:

 

 

 

 

 

安卓端下载地址:



本开源项目仅做个人学习使用如需商业合作,请联系:

电话: 18660911357

微信  liuniukeji-js

公司官网: https://www.liuniukeji.com/index/easemob

收起阅读 »

SVProgressHUD简单使用以及自定义动画

SVProgressHUD 是一个干净,易于使用的HUD,旨在显示iOS和tvOS正在进行的任务的进展。常用的还有MBProgressHUD.这两个都是很常用的HUD,大体相似,但是还是有一些不同的.MBProgressHUD和SVProgressHUD的区别...
继续阅读 »

SVProgressHUD 是一个干净,易于使用的HUD,旨在显示iOS和tvOS正在进行的任务的进展。
常用的还有MBProgressHUD.这两个都是很常用的HUD,大体相似,但是还是有一些不同的.
MBProgressHUD和SVProgressHUD的区别:
svprogresshud 使用起来很方便,但 可定制 差一些,看它的接口貌似只能添加一个全屏的HUD,不能把它添加到某个视图上面去.
MBProgressHUD 功能全一些,可定制 高一些,而且可以指定加到某一个View上去.用起来可能就没上面那个方便了.
具体还要看你的使用场景.
附上GitHub源码地址:
SVProgressHUD:https://github.com/SVProgressHUD/SVProgressHUD
MBProgressHUD:https://github.com/jdg/MBProgressHUD
今天我们不对二者的区别做详解,有空我会专门写文章对它们的区别做一个详解.
今天我们主要简单介绍一下SVProgressHUD的使用.


安装

通过CocoaPods安装,在Podfile中加入pod 'SVProgressHUD',这里不多做介绍.可以参考文章: CocoaPods的简单使用

使用

SVProgressHUD是已经被创建为单例的,所以不需要被实例化了,可以直接使用.调用它的方法[SVProgressHUD method].

[SVProgressHUD show ];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0),^ {
//耗时的任务
dispatch_async(dispatch_get_main_queue(),^ {
[SVProgressHUD dismiss ];
});
});

显示HUD

可以在下拉刷新或者执行其他耗时任务的时候,使用下面方法之一,来显示不确定任务的状态:

+ (void)show;
+ (void)showWithStatus:(NSString*)string;

效果图分别为:



如果你希望HUD反应任务的进度,可以使用下面方法的其中一个:

+ (void)showProgress:(CGFloat)progress;
+ (void)showProgress:(CGFloat)progress status:(NSString*)status;

通过其他方式可以实现进度条的速度把控.比如:

- (IBAction)clickButtonsShowWithProgress:(id)sender {
progress = 0.0f;
[SVProgressHUD showProgress:0 status:@"Loading"];
[self performSelector:@selector(increaseProgress) withObject:nil afterDelay:0.1f];
}

- (void)increaseProgress {
progress += 0.05f;
[SVProgressHUD showProgress:progress status:@"xuanhe Loading"];

if(progress < 1.0f){
[self performSelector:@selector(increaseProgress) withObject:nil afterDelay:0.1f];
} else {
[self performSelector:@selector(dismiss) withObject:nil afterDelay:0.4f];
}
}

效果如下


还有其他常用的语法:

+(void)showInfoWithStatus :( NSString *)string;
+(void)showSuccessWithStatus :( NSString *)string;
+(void)showErrorWithStatus :( NSString *)string;
+(void)showImage:(UIImage *)image status :( NSString *)string;

取消HUD

HUD可以使用以下方式解除:

+(void)dismiss;
+(void)dismissWithDelay :( NSTimeInterval)delay;
+ (void)dismissWithDelay:(NSTimeInterval)delay completion:(SVProgressHUDDismissCompletion)completion;

可以对这些代码进行改进,比如,在弹框结束后执行其他操作.可以封装一个方法,弹框结束后,执行Block.

定制

SVProgressHUD 可以通过以下方法定制:

+ (void)setDefaultStyle:(SVProgressHUDStyle)style;                  // default is SVProgressHUDStyleLight
+ (void)setDefaultMaskType:(SVProgressHUDMaskType)maskType; // default is SVProgressHUDMaskTypeNone
+ (void)setDefaultAnimationType:(SVProgressHUDAnimationType)type; // default is SVProgressHUDAnimationTypeFlat
+ (void)setContainerView:(UIView*)containerView; // default is window level
+ (void)setMinimumSize:(CGSize)minimumSize; // default is CGSizeZero, can be used to avoid resizing
+ (void)setRingThickness:(CGFloat)width; // default is 2 pt
+ (void)setRingRadius:(CGFloat)radius; // default is 18 pt
+ (void)setRingNoTextRadius:(CGFloat)radius; // default is 24 pt
+ (void)setCornerRadius:(CGFloat)cornerRadius; // default is 14 pt
+ (void)setBorderColor:(nonnull UIColor*)color; // default is nil
+ (void)setBorderWidth:(CGFloat)width; // default is 0
+ (void)setFont:(UIFont*)font; // default is [UIFont preferredFontForTextStyle:UIFontTextStyleSubheadline]
+ (void)setForegroundColor:(UIColor*)color; // default is [UIColor blackColor], only used for SVProgressHUDStyleCustom
+ (void)setBackgroundColor:(UIColor*)color; // default is [UIColor whiteColor], only used for SVProgressHUDStyleCustom
+ (void)setBackgroundLayerColor:(UIColor*)color; // default is [UIColor colorWithWhite:0 alpha:0.4], only used for SVProgressHUDMaskTypeCustom
+ (void)setImageViewSize:(CGSize)size; // default is 28x28 pt
+ (void)setInfoImage:(UIImage*)image; // default is the bundled info image provided by Freepik
+ (void)setSuccessImage:(UIImage*)image; // default is bundled success image from Freepik
+ (void)setErrorImage:(UIImage*)image; // default is bundled error image from Freepik
+ (void)setViewForExtension:(UIView*)view; // default is nil, only used if #define SV_APP_EXTENSIONS is set
+ (void)setGraceTimeInterval:(NSTimeInterval)interval; // default is 0 seconds
+ (void)setMinimumDismissTimeInterval:(NSTimeInterval)interval; // default is 5.0 seconds
+ (void)setMaximumDismissTimeInterval:(NSTimeInterval)interval; // default is CGFLOAT_MAX
+ (void)setFadeInAnimationDuration:(NSTimeInterval)duration; // default is 0.15 seconds
+ (void)setFadeOutAnimationDuration:(NSTimeInterval)duration; // default is 0.15 seconds
+ (void)setMaxSupportedWindowLevel:(UIWindowLevel)windowLevel; // default is UIWindowLevelNormal
+ (void)setHapticsEnabled:(BOOL)hapticsEnabled; // default is NO

样式

作为标准SVProgressHUD提供两种预先配置的样式:

SVProgressHUDStyleLight白色背景黑色图标和文字
SVProgressHUDStyleDark黑色背景与白色图标和文本
如果要使用自定义颜色使用setForegroundColor和setBackgroundColor:。这些方法将HUD的风格置为SVProgressHUDStyleCustom。

触觉反馈

对于具有较新设备的用户(从iPhone 7开始),SVProgressHUD可以根据显示的HUD来自动触发触觉反馈。反馈图如下:

showSuccessWithStatus: < - > UINotificationFeedbackTypeSuccess

showInfoWithStatus: < - > UINotificationFeedbackTypeWarning

showErrorWithStatus: < - > UINotificationFeedbackTypeError

要启用此功能,请使用setHapticsEnabled: 。

具有iPhone 7之前的设备的用户将不会改变功能。

通知

SVProgressHUD发布四个通知,NSNotificationCenter以响应被显示/拒绝:

SVProgressHUDWillAppearNotification 提示框即将出现
SVProgressHUDDidAppearNotification 提示框已经出现
SVProgressHUDWillDisappearNotification 提示框即将消失
SVProgressHUDDidDisappearNotification 提示框已经消失

每个通知通过一个userInfo保存HUD状态字符串(如果有的话)的字典,可以通过检索SVProgressHUDStatusUserInfoKey。

SVProgressHUD SVProgressHUDDidReceiveTouchEventNotification当用户触摸整个屏幕或SVProgressHUDDidTouchDownInsideNotification用户直接触摸HUD时也会发布。由于此通知userInfo未被传递,而对象参数包含UIEvent与触摸相关的参数。

应用扩展

这里对这个功能不做详解.自行摸索.

自定义动画

SVProgressHUD提供了方法可以自定义图片.但是不支持gif格式,直接利用下面的方法依然显示一张静态的图片

[SVProgressHUD showImage:[UIImage imageNamed:@"loading.gif"] status:@"加载中..."];

我们可以把gif转化为一个动态的image.
下面是我在百度上搜的一个方法.仅供参考.

#import <UIKit/UIKit.h>

typedef void (^GIFimageBlock)(UIImage *GIFImage);
@interface UIImage (GIFImage)

/** 根据本地GIF图片名 获得GIF image对象 */
+ (UIImage *)imageWithGIFNamed:(NSString *)name;

/** 根据一个GIF图片的data数据 获得GIF image对象 */
+ (UIImage *)imageWithGIFData:(NSData *)data;

/** 根据一个GIF图片的URL 获得GIF image对象 */
+ (void)imageWithGIFUrl:(NSString *)url and:(GIFimageBlock)gifImageBlock;

下面是.m的方法实现.

#import "UIImage+GIFImage.h"
#import <ImageIO/ImageIO.h>
@implementation UIImage (GIFImage)
+ (UIImage *)imageWithGIFData:(NSData *)data{

if (!data) return nil;
CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
size_t count = CGImageSourceGetCount(source);
UIImage *animatedImage;
if (count <= 1) {
animatedImage = [[UIImage alloc] initWithData:data];
} else {
NSMutableArray *images = [NSMutableArray array];
NSTimeInterval duration = 0.0f;
for (size_t i = 0; i < count; i++) {
// 拿出了Gif的每一帧图片
CGImageRef image = CGImageSourceCreateImageAtIndex(source, i, NULL);
//Learning... 设置动画时长 算出每一帧显示的时长(帧时长)
NSTimeInterval frameDuration = [UIImage sd_frameDurationAtIndex:i source:source];
duration += frameDuration;
// 将每帧图片添加到数组中
[images addObject:[UIImage imageWithCGImage:image scale:[UIScreen mainScreen].scale orientation:UIImageOrientationUp]];
// 释放真图片对象
CFRelease(image);
}
// 设置动画时长
if (!duration) {
duration = (1.0f / 10.0f) * count;
}
animatedImage = [UIImage animatedImageWithImages:images duration:duration];
}

// 释放源Gif图片
CFRelease(source);
return animatedImage;
}
+ (UIImage *)imageWithGIFNamed:(NSString *)name{
NSUInteger scale = (NSUInteger)[UIScreen mainScreen].scale;
return [self GIFName:name scale:scale];
}

+ (UIImage *)GIFName:(NSString *)name scale:(NSUInteger)scale{
NSString *imagePath = [[NSBundle mainBundle] pathForResource:[NSString stringWithFormat:@"%@@%zdx", name, scale] ofType:@"gif"];
if (!imagePath) {
(scale + 1 > 3) ? (scale -= 1) : (scale += 1);
imagePath = [[NSBundle mainBundle] pathForResource:[NSString stringWithFormat:@"%@@%zdx", name, scale] ofType:@"gif"];
}
if (imagePath) {
// 传入图片名(不包含@Nx)
NSData *imageData = [NSData dataWithContentsOfFile:imagePath];
return [UIImage imageWithGIFData:imageData];
} else {
imagePath = [[NSBundle mainBundle] pathForResource:name ofType:@"gif"];
if (imagePath) {
// 传入的图片名已包含@Nx or 传入图片只有一张 不分@Nx
NSData *imageData = [NSData dataWithContentsOfFile:imagePath];
return [UIImage imageWithGIFData:imageData];
} else {
// 不是一张GIF图片(后缀不是gif)
return [UIImage imageNamed:name];
}
}
}
+ (void)imageWithGIFUrl:(NSString *)url and:(GIFimageBlock)gifImageBlock{
NSURL *GIFUrl = [NSURL URLWithString:url];
if (!GIFUrl) return;
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSData *CIFData = [NSData dataWithContentsOfURL:GIFUrl];
// 刷新UI在主线程
dispatch_async(dispatch_get_main_queue(), ^{
gifImageBlock([UIImage imageWithGIFData:CIFData]);
});
});
}
#pragma mark - <关于GIF图片帧时长(Learning...)>
+ (float)sd_frameDurationAtIndex:(NSUInteger)index source:(CGImageSourceRef)source {
float frameDuration = 0.1f;
CFDictionaryRef cfFrameProperties = CGImageSourceCopyPropertiesAtIndex(source, index, nil);
NSDictionary *frameProperties = (__bridge NSDictionary *)cfFrameProperties;
NSDictionary *gifProperties = frameProperties[(NSString *)kCGImagePropertyGIFDictionary];
NSNumber *delayTimeUnclampedProp = gifProperties[(NSString *)kCGImagePropertyGIFUnclampedDelayTime];
if (delayTimeUnclampedProp) {
frameDuration = [delayTimeUnclampedProp floatValue];
}
else {
NSNumber *delayTimeProp = gifProperties[(NSString *)kCGImagePropertyGIFDelayTime];
if (delayTimeProp) {
frameDuration = [delayTimeProp floatValue];
}
}
// Many annoying ads specify a 0 duration to make an image flash as quickly as possible.
// We follow Firefox's behavior and use a duration of 100 ms for any frames that specify
// a duration of <= 10 ms. See and
// for more information.
if (frameDuration < 0.011f) {
frameDuration = 0.100f;
}
CFRelease(cfFrameProperties);
return frameDuration;
}
@end

这个是UIimage的分类,在用到的控制器里面调用代码方法即可.这个分类实现我也不太懂.只会用.

_imgView1.image = [UIImage imageWithGIFNamed:@"xuanxuan"];

NSString *path = [[NSBundle mainBundle] pathForResource:@"xuanxuan" ofType:@"gif"];
NSData *imgData = [NSData dataWithContentsOfFile:path];
_imgView2.image = [UIImage imageWithGIFData:imgData];


[UIImage imageWithGIFUrl:@"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1495708809771&di=da92fc5cf3bdd684711ab5124ee43183&imgtype=0&src=http%3A%2F%2Fimgsrc.baidu.com%2Fforum%2Fw%253D580%2Fsign%3D91bd6cd2d42a60595210e1121835342d%2F212eb9389b504fc215d0301ee6dde71190ef6d1a.jpg" and:^(UIImage *GIFImage) {
_imgView3.image = GIFImage;
}];

转自:https://www.jianshu.com/p/fa22b7c27e1d

收起阅读 »

IPFS对标HTTP,IPFS的优势是什么?

FIL
区块链技术的高速发展,离不开底层技术的支持,而且肯定先于区块链技术的发展。目前来看,IPFS—Filecoin是最有可能成为区块链底层基础设施的技术。这也表明IPFS—Filecoin必然会随之快速发展。造成这一现象的原因之一在于区块链技术本身的限制,它不能存...
继续阅读 »

区块链技术的高速发展,离不开底层技术的支持,而且肯定先于区块链技术的发展。目前来看,IPFS—Filecoin是最有可能成为区块链底层基础设施的技术。这也表明IPFS—Filecoin必然会随之快速发展。造成这一现象的原因之一在于区块链技术本身的限制,它不能存储存储数据,这也是自区块链技术诞生之后限制区块链技术发展的重要原因之一。IPFS矿机布局,避免踩坑(FIL37373)

Filecoin与IPFS(InterPlanetary File System,星际文件系统)是近两年来非常热门的概念。所谓IPFS是一个基于内容寻址的、分布式的、新型超媒体传输协议。IPFS支持创建完全分布式的应用。它旨在使用网络更快、更安全、更开放。IPFS是一个分布式文件系统,它的目标是将所有计算设备连接到同一个文件系统,从而成为一个全球统一的储存系统。而Filecoin是IPFS的激励层。

IPFS对标HTTP,IPFS的优势是什么?

IPFS星际文件存储系统,是一种p2p协议。相对于传统云存储有以下几个优点:

1. 便宜。IPFS存储空间不由服务商提供,而是接入网络的节点来提供,可以说是任何人都可以成为节点的一部分,所以非常便宜。

2. 速度快。IPFS协议下,文件冗余存储在世界各地,类似于CDN一样。当用户发起下载请求时,附近的借点都会收到信息并传送文件给你,而你只接收最先到达的文件。而传统云服务依赖于中心服务器到你的主机的线路和带宽。IPFS矿机布局,避免踩坑(FIL37373)

3. 安全性高。目前没有任何云存储敢保证自己的服务器不会遭到黑客袭击并保证数据安全。但是IPFS协议下文件在上传的时候会在每个节点保留其记录,系统检测单到文件丢失的时候会自动恢复。且由于其分布性存储的特征,黑客无法同时攻击所有节点。IPFS矿机布局,避免踩坑(FIL37373)

4.隐私保护。对于加密文件的上传使用非对称加密的方式,即除非对方掌握了私钥,否则无法破解。

IPFS分布式存储结构,各项数值优于HTTP,且发布区块链项目Filecoin,能够为IPFS技术存储提供足够的微型存储空间(节点),IPFS,与Filecoin即形成紧密的共生关系,相辅相成。

IPFS网络要想稳定运行需要用户贡献他们的存储空间、网络带宽,如果没有恰当的奖励机制,那么巨大的资源开销很难维持网络持久运转。受到比特币网络的启发,将Filecoin作为IPFS的激励层就是一种解决方案了。对于用户而言,Filecoin能够提高存取速度和效率,能带来去中心化的应用;对于矿工,贡献网络资源可以获得一笔不错的收益。

收起阅读 »

iOS缓存设计(YYCache思路)

iOS缓存设计(YYCache思路)前言:前段时间业务有缓存需求,于是结合YYCache和业务需求,做了缓存层(内存&磁盘)+ 网络层的方案尝试由于YYCache 采用了内存缓存和磁盘缓存组合方式,性能优良,这里拿它的原理来说下如何设计一套缓存的思路,...
继续阅读 »

iOS缓存设计(YYCache思路)

前言:
前段时间业务有缓存需求,于是结合YYCache和业务需求,做了缓存层(内存&磁盘)+ 网络层的方案尝试
由于YYCache 采用了内存缓存和磁盘缓存组合方式,性能优良,这里拿它的原理来说下如何设计一套缓存的思路,并结合网络整理一套完整流程

目录

初步认识缓存
如何优化缓存(YYCache设计思想)
网络和缓存同步流程
一、初步认识缓存

1. 什么是缓存?

我们做一个缓存前,先了解它是什么,缓存是本地数据存储,存储方式主要包含两种:磁盘储存和内存存储

1.1 磁盘存储

磁盘缓存,磁盘也就是硬盘缓存,磁盘是程序的存储空间,磁盘缓存容量大速度慢,磁盘是永久存储东西的,iOS为不同数据管理对存储路径做了规范如下:
1、每一个应用程序都会拥有一个应用程序沙盒。
2、应用程序沙盒就是一个文件系统目录。
沙盒根目录结构:Documents、Library、temp。

磁盘存储方式主要有文件管理和数据库,其特性:


1.2 内存存储

内存缓存,内存缓存是指当前程序运行空间,内存缓存速度快容量小,它是供cpu直接读取,比如我们打开一个程序,他是运行在内存中的,关闭程序后内存又会释放。
iOS内存分为5个区:栈区,堆区,全局区,常量区,代码区

栈区stack:这一块区域系统会自己管理,我们不用干预,主要存一些局部变量,以及函数跳转时的现场保护。因此大量的局部变量,深递归,函数循环调用都可能导致内存耗尽而运行崩溃。
堆区heap:与栈区相对,这一块一般由我们自己管理,比如alloc,free的操作,存储一些自己创建的对象。
全局区(静态区static):全局变量和静态变量都存储在这里,已经初始化的和没有初始化的会分开存储在相邻的区域,程序结束后系统会释放
常量区:存储常量字符串和const常量
代码区:存储代码

在程序中声明的容器(数组 、字典)都可看做内存中存储,特性如下:


2. 缓存做什么?

我们使用场景比如:离线加载,预加载,本地通讯录...等,对非网络数据,使用本地数据管理的一种,具体使用场景有很多

3. 怎么做缓存?

简单缓存可以仅使用磁盘存储,iOS主要提供四种磁盘存储方式:

NSKeyedArchiver: 采用归档的形式来保存数据, 该数据对象需要遵守NSCoding协议, 并且该对象对应的类必须提供encodeWithCoder:和initWithCoder:方法.

//自定义Person实现归档解档
//.h文件
#import <Foundation/Foundation.h>
@interface Person : NSObject<NSCoding>
@property(nonatomic,copy) NSString * name;

@end

//.m文件
#import "Person.h"
@implementation Person
//归档要实现的协议方法
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:_name forKey:@"name"];
}
//解档要实现的协议方法
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
if (self = [super init]) {
_name = [aDecoder decodeObjectForKey:@"name"];
}
return self;
}
@end

使用归档解档

// 将数据存储在path路径下归档文件
[NSKeyedArchiver archiveRootObject:p toFile:path];
// 根据path路径查找解档文件
Person *p = [NSKeyedUnarchiver unarchiveObjectWithFile:path];

缺点:归档的形式来保存数据,只能一次性归档保存以及一次性解压。所以只能针对小量数据,如果想改动数据的某一小部分,需要解压整个数据或者归档整个数据。

NSUserDefaults: 用来保存应用程序设置和属性、用户保存的数据。用户再次打开程序或开机后这些数据仍然存在。
NSUserDefaults可以存储的数据类型包括:NSData、NSString、NSNumber、NSDate、NSArray、 NSDictionary。

// 以键值方式存储
[[NSUserDefaults standardUserDefaults] setObject:@"value" forKey:@"key"];
// 以键值方式读取
[[NSUserDefaults standardUserDefaults] objectForKey:@"key"];

Write写入方式:永久保存在磁盘中。具体方法为:

//将NSData类型对象data写入文件,文件名为FileName
[data writeToFile:FileName atomically:YES];
//从FileName中读取出数据
NSData *data=[NSData dataWithContentsOfFile:FileName options:0 error:NULL];

SQLite:采用SQLite数据库来存储数据。SQLite作为⼀一中小型数据库,应用ios中跟其他三种保存方式相比,相对复杂一些

//打开数据库
if (sqlite3_open([databaseFilePath UTF8String], &database)==SQLITE_OK) {
NSLog(@"sqlite dadabase is opened.");
} else { return;}//打开不成功就返回

//在打开了数据库的前提下,如果数据库没有表,那就开始建表了哦!
char *error;
const char *createSql="create table(id integer primary key autoincrement, name text)"; if (sqlite3_exec(database, createSql, NULL, NULL, &error)==SQLITE_OK) {
NSLog(@"create table is ok.");
} else {
sqlite3_free(error);//每次使用完毕清空error字符串,提供给下⼀一次使用
}

// 建表完成之后, 插入记录
const char *insertSql="insert into a person (name) values(‘gg’)";
if (sqlite3_exec(database, insertSql, NULL, NULL, &error)==SQLITE_OK) {
NSLog(@"insert operation is ok.");
} else {
sqlite3_free(error);//每次使用完毕清空error字符串,提供给下一次使用
}

上面提到的磁盘存储特性,具备空间大、可持久、但是读取慢,面对大量数据频繁读取时更加明显,以往测试中磁盘读取比内存读取保守测量低于几十倍,那我们怎么解决磁盘读取慢的缺点呢? 又如何利用内存的优势呢?

二、 如何优化缓存(YYCache设计思想)

YYCache背景知识:
源码中由两个主要类构成


YYMemoryCache (内存缓存)
操作YYLinkedMap中数据, 为实现内存优化,采用双向链表数据结构实现 LRU算法,YYLinkedMapItem 为每个子节点
YYDiskCache (磁盘缓存)
不会直接操作缓存对象(sqlite/file),而是通过 YYKVStorage 来间接的操作缓存对象。
容量管理:

ageLimit :时间周期限制,比如每天或每星期开始清理
costLimit: 容量限制,比如超出10M后开始清理内存
countLimit : 数量限制, 比如超出1000个数据就清理
这里借用YYCache设计, 来讲述缓存优化

1. 磁盘+内存组合优化
利用内存和磁盘特性,融合各自优点,整合如下:


APP会优先请求内存缓冲中的资源
如果内存缓冲中有,则直接返回资源文件, 如果没有的话,则会请求资源文件,这时资源文件默认资源为本地磁盘存储,需要操作文件系统或数据库来获取。
获取到的资源文件,先缓存到内存缓存,方便以后不再重复获取,节省时间。
然后就是从缓存中取到数据然后给app使用。
这样就充分结合两者特性,利用内存读取快特性减少读取数据时间,

YYCache 源码解析:

- (id<NSCoding>)objectForKey:(NSString *)key {
// 1.如果内存缓存中存在则返回数据
id<NSCoding> object = [_memoryCache objectForKey:key];
if (!object) {
// 2.若不存在则查取磁盘缓存数据
object = [_diskCache objectForKey:key];
if (object) {
// 3.并将数据保存到内存中
[_memoryCache setObject:object forKey:key];
}
}
return object;
}

2. 内存优化-- 提高内存命中率

但是我们想在基础上再做优化,比如想让经常访问的数据保留在内存中,提高内存的命中率,减少磁盘的读取,那怎么做处理呢? -- LRU算法


LRU算法:我们可以将链表看成一串数据链,每个数据是这个串上的一个节点,经常访问的数据移动到头部,等数据超出容量后从链表后面的一些节点销毁,这样经常访问数据在头部位置,还保留在内存中。

链表实现结构图:


YYCache 源码解析

/**
A node in linked map.
Typically, you should not use this class directly.
*/
@interface _YYLinkedMapNode : NSObject {
@package
__unsafe_unretained _YYLinkedMapNode *_prev; // retained by dic
__unsafe_unretained _YYLinkedMapNode *_next; // retained by dic
id _key;
id _value;
NSUInteger _cost;
NSTimeInterval _time;
}
@end
@implementation _YYLinkedMapNode
@end
/**
A linked map used by YYMemoryCache.
It's not thread-safe and does not validate the parameters.
Typically, you should not use this class directly.
*/
@interface _YYLinkedMap : NSObject {
@package
CFMutableDictionaryRef _dic; // do not set object directly
NSUInteger _totalCost;
NSUInteger _totalCount;
_YYLinkedMapNode *_head; // MRU, do not change it directly
_YYLinkedMapNode *_tail; // LRU, do not change it directly
BOOL _releaseOnMainThread;
BOOL _releaseAsynchronously;
}

/// Insert a node at head and update the total cost.
/// Node and node.key should not be nil.
- (void)insertNodeAtHead:(_YYLinkedMapNode *)node;

/// Bring a inner node to header.
/// Node should already inside the dic.
- (void)bringNodeToHead:(_YYLinkedMapNode *)node;

/// Remove a inner node and update the total cost.
/// Node should already inside the dic.
- (void)removeNode:(_YYLinkedMapNode *)node;

/// Remove tail node if exist.
- (_YYLinkedMapNode *)removeTailNode;

/// Remove all node in background queue.
- (void)removeAll;

@end

_YYLinkedMapNode *_prev 为该节点的头指针,指向前一个节点
_YYLinkedMapNode *_next为该节点的尾指针,指向下一个节点
头指针和尾指针将一个个子节点串连起来,形成双向链表

来看下bringNodeToHead:的源码实现,它是实现LRU算法主要方法,移动node子结点到链头。

(详细已注释在代码中)

- (void)bringNodeToHead:(_YYLinkedMapNode *)node {
if (_head == node) return; // 如果当前节点是链头,则不需要移动

// 链表中存了两个指向链头(_head)和链尾(_tail)的指针,便于链表访问
if (_tail == node) {
_tail = node->_prev; // 若当前节点为链尾,则更新链尾指针
_tail->_next = nil; // 链尾的尾节点这里设置为nil
} else {
// 比如:A B C 链表, 将 B拿走,将A C重新联系起来
node->_next->_prev = node->_prev; // 将node的下一个节点的头指针指向node的上一个节点,
node->_prev->_next = node->_next; // 将node的上一个节点的尾指针指向node的下一个节点
}
node->_next = _head; // 将当前node节点的尾指针指向之前的链头,因为此时node为最新的第一个节点
node->_prev = nil; // 链头的头节点这里设置为nil
_head->_prev = node; // 之前的_head将为第二个节点
_head = node; // 当前node成为新的_head
}

其他方法就不挨个举例了,具体可翻看源码,这些代码结构清晰,类和函数遵循单一职责,接口高内聚,低耦合,是个不错的学习示例!

3. 磁盘优化 - 数据分类存储

YYDiskCache 是一个线程安全的磁盘缓存,基于 sqlite 和 file 来做的磁盘缓存,我们的缓存对象可以自由的选择存储类型,
下面简单对比一下:

sqlite: 对于小数据(例如 NSNumber)的存取效率明显高于 file。
file: 对于较大数据(例如高质量图片)的存取效率优于 sqlite。
所以 YYDiskCache 使用两者配合,灵活的存储以提高性能。

另外:
YYDiskCache 具有以下功能:

它使用 LRU(least-recently-used) 来删除对象。
支持按 cost,count 和 age 进行控制。
它可以被配置为当没有可用的磁盘空间时自动驱逐缓存对象。
它可以自动抉择每个缓存对象的存储类型(sqlite/file)以便提供更好的性能表现。
YYCache源码解析

// YYKVStorageItem 是 YYKVStorage 中用来存储键值对和元数据的类
// 通常情况下,我们不应该直接使用这个类
@interface YYKVStorageItem : NSObject
@property (nonatomic, strong) NSString *key; ///< key
@property (nonatomic, strong) NSData *value; ///< value
@property (nullable, nonatomic, strong) NSString *filename; ///< filename (nil if inline)
@property (nonatomic) int size; ///< value's size in bytes
@property (nonatomic) int modTime; ///< modification unix timestamp
@property (nonatomic) int accessTime; ///< last access unix timestamp
@property (nullable, nonatomic, strong) NSData *extendedData; ///< extended data (nil if no extended data)
@end


/**
YYKVStorage 是基于 sqlite 和文件系统的键值存储。
通常情况下,我们不应该直接使用这个类。

@warning
这个类的实例是 *非* 线程安全的,你需要确保
只有一个线程可以同时访问该实例。如果你真的
需要在多线程中处理大量的数据,应该分割数据
到多个 KVStorage 实例(分片)。
*/
@interface YYKVStorage : NSObject

#pragma mark - Attribute
@property (nonatomic, readonly) NSString *path; /// storage 路径
@property (nonatomic, readonly) YYKVStorageType type; /// storage 类型
@property (nonatomic) BOOL errorLogsEnabled; /// 是否开启错误日志

#pragma mark - Initializer
- (nullable instancetype)initWithPath:(NSString *)path type:(YYKVStorageType)type NS_DESIGNATED_INITIALIZER;

#pragma mark - Save Items
- (BOOL)saveItem:(YYKVStorageItem *)item;
...

#pragma mark - Remove Items
- (BOOL)removeItemForKey:(NSString *)key;
...

#pragma mark - Get Items
- (nullable YYKVStorageItem *)getItemForKey:(NSString *)key;
...

#pragma mark - Get Storage Status
- (BOOL)itemExistsForKey:(NSString *)key;
- (int)getItemsCount;
- (int)getItemsSize;

@end

我们只需要看一下 YYKVStorageType 这个枚举,它决定着 YYKVStorage 的存储类型。

YYKVStorageType

/**
存储类型,指示“YYKVStorageItem.value”存储在哪里。

@discussion
通常,将数据写入 sqlite 比外部文件更快,但是
读取性能取决于数据大小。在测试环境 iPhone 6s 64G,
当数据较大(超过 20KB)时从外部文件读取数据比 sqlite 更快。
*/
typedef NS_ENUM(NSUInteger, YYKVStorageType) {
YYKVStorageTypeFile = 0, // value 以文件的形式存储于文件系统
YYKVStorageTypeSQLite = 1, // value 以二进制形式存储于 sqlite
YYKVStorageTypeMixed = 2, // value 将根据你的选择基于上面两种形式混合存储
};

总结:

这里说了YYCache几个主要设计优化之处,其实细节上也有很多不错的处理,比如:

线程安全
如果说 YYCache 这个类是一个纯逻辑层的缓存类(指 YYCache 的接口实现全部是调用其他类完成),那么 YYMemoryCache 与 YYDiskCache 还是做了一些事情的(并没有 YYCache 当甩手掌柜那么轻松),其中最显而易见的就是 YYMemoryCache 与 YYDiskCache 为 YYCache 保证了线程安全。
YYMemoryCache 使用了 pthread_mutex 线程锁来确保线程安全,而 YYDiskCache 则选择了更适合它的 dispatch_semaphore,上文已经给出了作者选择这些锁的原因。

性能

YYCache 中对于性能提升的实现细节:

异步释放缓存对象
锁的选择
使用 NSMapTable 单例管理的 YYDiskCache
YYKVStorage 中的 _dbStmtCache
甚至使用 CoreFoundation 来换取微乎其微的性能提升

3. 网络和缓存同步流程

结合网络层和缓存层,设计了一套接口缓存方式,比较灵活且速度得到提升; 比如首页界面可能由多个接口提供数据,没有采用整块存储而是将存储细分到每个接口中,有API接口控制,基本结构如下:

主要分为:

应用层 :显示数据
管理层: 管理网络层和缓存层,为应用层提供数据支持
网络层: 请求网络数据
缓存层: 缓存数据
层级图:


服务端每套数据对应一个version (或时间戳),若后台数据发生变更,则version发生变化,在返回客户端数据时并将version一并返回。
当客户端请求网络时,将本地上一次数据对应version上传。
服务端获取客户端传来得version后,与最新的version进行对比,若version不一致,则返回最新数据,若未发生变化,服务端不需要返回全部数据只需返回304(No Modify) 状态值
客户端接到服务端返回数据,若返回全部数据非304,客户端则将最新数据同步到本地缓存中;客户端若接到304状态值后,表示服务端数据和本地数据一致,直接从缓存中获取显示
这也是ETag的大致流程;详细可以查看 https://baike.baidu.com/item/ETag/4419019?fr=aladdin

源码示例

- (void)getDataWithPage:(NSNumber *)page pageSize:(NSNumber *)pageSize option:(DataSourceOption)option completion:(void (^)(HomePageListCardModel * _Nullable, NSError * _Nullable))completionBlock {
NSString *cacheKey = CacheKey(currentUser.userId, PlatIndexRecommendation);// 全局静态常量 (userid + apiName)
// 根据需求而定是否需要缓存方式,网络方式走304逻辑
switch (option) {
case DataSourceCache:
{
if ([_cache containsObjectForKey:cacheKey]) {
completionBlock((HomePageListCardModel *)[self->_cache objectForKey:cacheKey], nil);
} else {
completionBlock(nil, LJDError(400, @"缓存中不存在"));
}
}
break;
case DataSourceNetwork:
{
[NetWorkServer requestDataWithPage:page pageSize:pageSize completion:^(id _Nullable responseObject, NSError * _Nullable error) {
if (responseObject && !error) {
HomePageListCardModel *model = [HomePageListCardModel yy_modelWithJSON:responseObject];
if (model.errnonumber == 304) { //取缓存数据
completionBlock((HomePageListCardModel *)[self->_cache objectForKey:cacheKey], nil);
} else {
completionBlock(model, error);
[self->_cache setObject:model forKey:cacheKey]; //保存到缓存中
}
} else {
completionBlock(nil, error);
}
}];
}
break;

default:
break;
}
}

这样做好处:

对于不频繁更新数据的接口,节省了大量JSON数据转化时间
节约流量,节省加载时长
用户界面显示加快
总结:项目中并不一定完全这样做,有时候过渡设计也是一种浪费,多了解其他设计思路后,针对项目找到适合的才是最好的!

参考文献:
YYCache: https://github.com/ibireme/YYCache
YYCache 设计思路 :https://blog.ibireme.com/2015/10/26/yycache/

链接:https://www.jianshu.com/p/b592ee20f09a

收起阅读 »

iOS进阶:WebViewJavascriptBridge源码解读

WebViewJavascriptBridge GitHub地址jsBridge框架是解决客户端与网页交互的方法之一。最主要的实现思路是客户端在webivew的代理方法中拦截url,根据url的类型来做不同处理。接下去会以jsBridge提供demo中的为例,...
继续阅读 »

WebViewJavascriptBridge GitHub地址

jsBridge框架是解决客户端与网页交互的方法之一。最主要的实现思路是客户端在webivew的代理方法中拦截url,根据url的类型来做不同处理。接下去会以jsBridge提供demo中的为例,从使用的角度,一步步分析它是如何实现的。

注:在iOS8后,苹果推出了WKWebView。对于UIWebView和WKWebView,jsBridge都能实现客户端与网页交互,且实现的方式类似,因此本文会以UIWebView为例来分析。

本文会通过以下几点来介绍框架的实现:

框架结构
WebViewJavascriptBridge_JS
WebViewJavascriptBridge WKWebViewJavascriptBridge
WebViewJavascriptBridgeBase
网页通知客户端的实现
客户端通知网页的实现
js环境注入问题
总结

框架结构


WebViewJavascriptBridge_JS

WebViewJavascriptBridge_JS 简单的说就是网页的js环境,需要客户端在网页初始化的时候注入到网页中去。如果不注入就无法实现网页与客户端的交互。该类只有一个返回值为NSString 的方法:NSString * WebViewJavascriptBridge_js(); 。

至于究竟何时注入,如何注入,会在接下去的分析中写到。

WebViewJavascriptBridge WKWebViewJavascriptBridge

这两个类分别对应UIWebView和WKWebView。看名字就可以知道这两个类是交互的桥梁,不管是网页同时客户端还是客户端通知网页,都是通过这两个类来完成通知的。

WebViewJavascriptBridgeBase

WebViewJavascriptBridgeBase个人认为类似数据处理工具类。

该类中存着客户端注册的方法以及对应实现:@property (strong, nonatomic) NSMutableDictionary* messageHandlers;

也存着客户端通知网页后的回调实现:@property (strong, nonatomic) NSMutableDictionary* responseCallbacks;

同时,该类还实现了之前提的网页js环境注入方法:-(void)injectJavascriptFile;

还有一些url类别判断方法,这里不一一举例了。

网页通知客户端的实现

要让客户端能够响应网页的通知,首先必须使用桥梁注册方法名和实现,然后存起来,等待网页的通知。

[_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponseCallback responseCallback) {
NSLog(@"testObjcCallback called: %@", data);
responseCallback(@"Response from testObjcCallback");
}];

客户端注册方法时,bridge做了些什么事情呢?其实bridge只是简单地将方法名和实现block分别作为键值存到了messageHandlers属性中。

- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
_base.messageHandlers[handlerName] = [handler copy];
}

接下来,网页想要调用客户端的testObjcCallback方法了。网页上有一个按钮,点击后调用客户端方法,网页的js代码如下:

var callbackButton = document.getElementById('buttons').appendChild(document.createElement('button'))
callbackButton.innerHTML = 'Fire testObjcCallback'
callbackButton.onclick = function(e) {
bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) {
log('JS got response', response)
})
}

这里网页调用的方法为bridge.callHandler,这里你可能会有疑问,为什么bridge对象哪来的,callHandler方法又是哪来的。关于这个,这边先简单的说一下:这个bridge其实就是我们之前提到的js环境提供的,callHandler方法也是环境中的代码实现的,如果没有js环境,网页就拿不到bridge,也就无法成功调起客户端的方法。这边可以简单的理解为这个环境就相当于是我们客户端的WebViewJavascriptBridge框架,客户端如果不导入,也就无法使用jsbridge。网页也是类似,如果不注入,就无法使用jsbridge。而区别就在于,客户端的这个框架是运行前导入的,而网页这个环境是由客户端加载到该网页时,动态注入的。

至于详细的注入,会在下文中分析说明。

js环境文件中,bridge.callHandler方法实现:

function callHandler(handlerName, data, responseCallback) {
if (arguments.length == 2 && typeof data == 'function') {
responseCallback = data;
data = null;
}
_doSend({ handlerName:handlerName, data:data }, responseCallback);
}


function _doSend(message, responseCallback) {
if (responseCallback) {
var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime();
responseCallbacks[callbackId] = responseCallback;
message['callbackId'] = callbackId;
}
sendMessageQueue.push(message);
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}

由于本质上网页处理发送通知的思路和客户端的一致,而我们队客户端的oc代码更好理解,因此我打算将这段代码的分析跳过,等到分析客户端通知网页时,再仔细讲。这边只需要知道

1.字典中加了一个callbackId字段,这个字段是用来等客户端调用完方法后,网页能找到对应的实现的。同时网页将实现存到了它管理的字典中:responseCallbacks[callbackId] = responseCallback;

2.网页最终将字典压到了sendMessageQueue中,并调用了messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;

var CUSTOM_PROTOCOL_SCHEME = 'https';
var QUEUE_HAS_MESSAGE = '__wvjb_queue_message__';

3.字典中的数据是:

{   
handlerName : "testObjcCallback",
data : {'foo': 'bar'},
callbackId : 'cb_'+(uniqueId++)+'_'+new Date().getTime()
}

这时,客户端的webview代码方法就能拦截到url:


正是网页调用的:https://__wvjb_queue_message__/。然后客户端是如果去判断url并做相应处理呢?下面为拦截的源码:

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
if (webView != _webView) { return YES; }

NSURL *url = [request URL];
__strong WVJB_WEBVIEW_DELEGATE_TYPE* strongDelegate = _webViewDelegate;
if ([_base isWebViewJavascriptBridgeURL:url]) {
if ([_base isBridgeLoadedURL:url]) {
[_base injectJavascriptFile];
} else if ([_base isQueueMessageURL:url]) {
NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]];
[_base flushMessageQueue:messageQueueString];
} else {
[_base logUnkownMessage:url];
}
return NO;
} else if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:shouldStartLoadWithRequest:navigationType:)]) {
return [strongDelegate webView:webView shouldStartLoadWithRequest:request navigationType:navigationType];
} else {
return YES;
}
}

这时,由于传过来的是https://__wvjb_queue_message__/,会进[_base isQueueMessageURL:url]的判断中,然后做以下处理:

NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]];
[_base flushMessageQueue:messageQueueString];

第一行代码为从网页的sendMessageQueue中获取到数据,还记得之前网页把调用的相关数据存到了sendMessageQueue中吗?这个时候,客户端又把它取出来了。然后第二行代码,客户端开始处理这个数据:

- (void)flushMessageQueue:(NSString *)messageQueueString{
if (messageQueueString == nil || messageQueueString.length == 0) {
NSLog(@"WebViewJavascriptBridge: WARNING: ObjC got nil while fetching the message queue JSON from webview. This can happen if the WebViewJavascriptBridge JS is not currently present in the webview, e.g if the webview just loaded a new page.");
return;
}

id messages = [self _deserializeMessageJSON:messageQueueString];
for (WVJBMessage* message in messages) {
if (![message isKindOfClass:[WVJBMessage class]]) {
NSLog(@"WebViewJavascriptBridge: WARNING: Invalid %@ received: %@", [message class], message);
continue;
}
[self _log:@"RCVD" json:message];

NSString* responseId = message[@"responseId"];
if (responseId) {
WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
responseCallback(message[@"responseData"]);
[self.responseCallbacks removeObjectForKey:responseId];
} else {
WVJBResponseCallback responseCallback = NULL;
NSString* callbackId = message[@"callbackId"];
if (callbackId) {
responseCallback = ^(id responseData) {
if (responseData == nil) {
responseData = [NSNull null];
}

WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
[self _queueMessage:msg];
};
} else {
responseCallback = ^(id ignoreResponseData) {
// Do nothing
};
}

WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];

if (!handler) {
NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message);
continue;
}

handler(message[@"data"], responseCallback);
}
}
}

这段代码有点多,核心思路是将获得的数据转换成字典,然后从客户端的messageHandlers中取出方法名对应的block,并调用:handler(message[@"data"], responseCallback);


这边还需要特别注意的是,callbackId问题。在这个例子中,是存在callbackId的,因为网页是有写调用完客户端后的回调的,所以这边做了处理,如果有callbackId的话,再创建一个responseCallback,等客户端调用完网页通知的方法后再调用。

还记得当初客户端注册方法时的代码吗:

[_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponseCallback responseCallback) {
NSLog(@"testObjcCallback called: %@", data);
responseCallback(@"Response from testObjcCallback");
}];

这边就将这个handler的block取出来,然后将message[@"data"]和responseCallback作为参数调用。调用完后又调用了responseCallback,将数据又发回网页去。这边具体的发送会在下文客户端通知网页分析中写到。这边这需要知道,如果存在callbackId,就会将callbackId和数据又发回网页。

WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
[self _queueMessage:msg];

以上就是网页通知客户端的大致实现。

客户端通知网页

其实客户端通知网页的大致思路是和上文类似的。在客户端调用之前,网页肯定是已经注册好了客户端要调用的方法,就如上文中,客户端也已经注册好了网页通知的方法一样。下面为网页注册的代码:

bridge.registerHandler('testJavascriptHandler', function(data, responseCallback) {
log('ObjC called testJavascriptHandler with', data)
var responseData = { 'Javascript Says':'Right back atcha!' }
log('JS responding with', responseData)
responseCallback(responseData)
})

看看registerHandler方法如何实现:

function registerHandler(handlerName, handler) {
messageHandlers[handlerName] = handler;
}

恩,是不是和客户端的注册非常相似?

接下来再看看客户端是如何调用的:

- (void)callHandler:(id)sender {
id data = @{ @"greetingFromObjC": @"Hi there, JS!" };
[_bridge callHandler:@"testJavascriptHandler" data:data responseCallback:^(id response) {
NSLog(@"testJavascriptHandler responded: %@", response);
}];
}

callHandler方法实现:

- (void)callHandler:(NSString *)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback {
[_base sendData:data responseCallback:responseCallback handlerName:handlerName];
}

sendData实现:

- (void)sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerName {
NSMutableDictionary* message = [NSMutableDictionary dictionary];

if (data) {
message[@"data"] = data;
}

if (responseCallback) {
NSString* callbackId = [NSString stringWithFormat:@"objc_cb_%ld", ++_uniqueId];
self.responseCallbacks[callbackId] = [responseCallback copy];
message[@"callbackId"] = callbackId;
}

if (handlerName) {
message[@"handlerName"] = handlerName;
}
[self _queueMessage:message];
}

客户端将数据封装成一个字段,这时这个字典的值为:

{
callbackId = "objc_cb_1";
data = {
greetingFromObjC = "Hi there, JS!";
};
handlerName = testJavascriptHandler;
}

还是和网页的处理非常一致。下面看看客户端是如何通知网页的:

- (void)_queueMessage:(WVJBMessage*)message {
if (self.startupMessageQueue) {
[self.startupMessageQueue addObject:message];
} else {
[self _dispatchMessage:message];
}
}

- (void)_dispatchMessage:(WVJBMessage*)message {
NSString *messageJSON = [self _serializeMessage:message pretty:NO];
[self _log:@"SEND" json:messageJSON];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\'" withString:@"\\\'"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\r" withString:@"\\r"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\f" withString:@"\\f"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\u2028" withString:@"\\u2028"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\u2029" withString:@"\\u2029"];

NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
if ([[NSThread currentThread] isMainThread]) {
[self _evaluateJavascript:javascriptCommand];

} else {
dispatch_sync(dispatch_get_main_queue(), ^{
[self _evaluateJavascript:javascriptCommand];
});
}
}

客户端将字段转成js字符串,然后注入到网页中实现通知。具体方法是调用了js环境中的_handleMessageFromObjC方法,参数为字典转换后的字符串。下面看看_handleMessageFromObjC方法的实现:

function _handleMessageFromObjC(messageJSON) {
_dispatchMessageFromObjC(messageJSON);
}

function _dispatchMessageFromObjC(messageJSON) {
if (dispatchMessagesWithTimeoutSafety) {
setTimeout(_doDispatchMessageFromObjC);
} else {
_doDispatchMessageFromObjC();
}

function _doDispatchMessageFromObjC() {
var message = JSON.parse(messageJSON);
var messageHandler;
var responseCallback;

if (message.responseId) {
responseCallback = responseCallbacks[message.responseId];
if (!responseCallback) {
return;
}
responseCallback(message.responseData);
delete responseCallbacks[message.responseId];
} else {
if (message.callbackId) {
var callbackResponseId = message.callbackId;
responseCallback = function(responseData) {
_doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
};
}

var handler = messageHandlers[message.handlerName];
if (!handler) {
console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
} else {
handler(message.data, responseCallback);
}
}
}
}

这边的处理其实和上文客户端处理message字典时没什么区别的。

这边要提一下的是这个responseId的判断逻辑,还记得网页通知客户端分析中,由于网页有实现通知完客户端后的代码,所以客户端将网页传递过来的callbackId作为responseId参数又传回去了:

WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
[self _queueMessage:msg];

这边网页的处理是,从responseCallbacks中根据这个"responseId":callbackId字段取出block并调用,代码如下:

if (message.responseId) {
responseCallback = responseCallbacks[message.responseId];
if (!responseCallback) {
return;
}
responseCallback(message.responseData);
delete responseCallbacks[message.responseId];
}

如果看到这里有点乱了,可以再看看网页通知客户端时对于字典的处理部分。

以上就是客户端通知网页的大致实现。

js环境注入问题

上文一提到这个,就说下文讲解,现在终于可以分析这一块了。

其实这个比较简单,本质上就是网页调用了一个特殊的,jsbridge规定的url,使得客户端可以拦截到并分析出是需要注入js环境的通知。然后客户端开始注入。

网页部分的代码:

WVJBIframe.src = 'https://__bridge_loaded__';

一般这个是放在网页代码的最前面的。这样做可以让客户端在最早的情况下将环境注入到网页中。

而客户端是如何处理的呢?

if ([_base isBridgeLoadedURL:url]) {
[_base injectJavascriptFile];
}
- (void)injectJavascriptFile {
NSString *js = WebViewJavascriptBridge_js();
[self _evaluateJavascript:js];
}

看到了吧,客户端调用WebViewJavascriptBridge_JS类的唯一的方法:NSString * WebViewJavascriptBridge_js(); ,然后通过_evaluateJavascript注入。

总结

以网页通知客户端为例:客户端会将要被调用的方法存到字典中,同时拦截网页的调用,当网页调用时,从字典中取出方法并调用。调用完后,判断网页是否有调用完的回调,如果有,再将回调的id和参数通过客户端调用网页的方式通知过去。这就完成了网页通知客户端的总体流程。

最后

这个框架是在去年就已经看完了,由于忙+懒,拖到今天才终于准备写一下。花了一下午的时间,将大体的逻辑理清楚并用文字的方式表达出来,但是由于昨晚没睡舒服,现在脑子还是有点乱,所以文章中应该有部分错别字,麻烦看到了指出一下方便我改正。还有一点,对于之前没接触过的同学,由于在调用时有responseId和callbackId,会比较乱,在此建议多看几遍。如果实在理解不了,可以评论或加我微信,我会尽我努力让你理解。最后,谢谢你的耐心阅读😆😆

链接:https://www.jianshu.com/p/7bd7260daf94

收起阅读 »

Flutter IM跨端架构设计和实现

作者:闲鱼技术——祈晴1. 闲鱼IM现状闲鱼IM框架构建于2016-2017年,期间多次迭代升级导致历史包袱累积多,后经IM界面Flutter化,造成架构更复杂,开发层面总结闲鱼当前架构主要存在如下几个问题:•研发效率较低:当前架构开发需求涉及到Android...
继续阅读 »

作者:闲鱼技术——祈晴

1. 闲鱼IM现状

闲鱼IM框架构建于2016-2017年,期间多次迭代升级导致历史包袱累积多,后经IM界面Flutter化,造成架构更复杂,开发层面总结闲鱼当前架构主要存在如下几个问题:

•研发效率较低:当前架构开发需求涉及到Android/iOS双端的逻辑代码以及Flutter的UI界面代码,定位问题往往只能从Flutter UI表相追查到Native逻辑漏洞;•架构层次较差:架构设计上分层不清晰,业务逻辑夹杂在核心的逻辑层致使代码变更风险大;•性能测试略差:核心数据源存储Native内存,需经Flutter Plugin将数据源序列化上抛Flutter侧,在大批量数据源情况下性能表现较差;

从舆情层面总结闲鱼IM当前架构的主要问题如下:

•定位问题困难:线上舆情反馈千奇百怪,测试始终无法复现相关场景,因此很多时候只能靠现象猜测本质;•疑难杂症较多:架构不稳定性造成出现的问题反复出现,当前疑难杂症主要包括未读红点计数,iPhone5C低端机器架构,以及多媒体发送等多个问题;•问题差异性大:Android和iOS两端逻辑代码差异大,包括现存埋点逻辑都不尽相同,因此排查问题根源时候双端都会有不同问题根因,解决问题方案也不相同;

2.业界跨端方案

为解决当前IM痛点,闲鱼今年特起关于IM架构升级项目,重在解决客户端中双端一致性痛点,初步设想方案就是实现跨端统一的Android/iOS逻辑架构;在当前行业内跨端方案可初步归类如下图架构,在GUI层面的跨端方案有Weex,ReactNative,H5,Uni-APP等,其内存模型大多需要通过桥接到Native模式存储;在逻辑层面的跨端方案大致有C/C++等与虚拟机无关语言实现跨端,当然汇编语言也可行;此外有两个独立于上述体系之外的架构就是Flutter和KMM(谷歌基于Kotlin实现类似Flutter架构),其中Flutter运行特定DartVM,将内存数据挂载其自身的isolate中;undefined

考虑闲鱼是Flutter的前沿探索者,方案上优先使用Flutter;然而Flutter的isolate更像一个进程的概念(底层实现非使用进程模式),相比Android,同一进程场景中,Android的Dalvik虚拟机多个线程运行共享一个内存Heap,而DartVM的Isolate运行隔离各自的Heap,因而isolate之间通讯方式比较繁琐(需经过序列化反序列化过程);整个模型如下图所示:undefined

若按官方混合架构实现Flutter应用,开启多个FlutterAcitivty/FlutterController,底层会生成多个Engine,对应会存在多个isolate,而isolate通讯类似于进程通讯(类似socket或AIDL),这里借鉴闲鱼FlutterBoost的设计理念,FlutterIM架构将多个页面的Engine共享,则内存模型就天然支持共享读取,原理图如下:

undefined

3.Flutter IM架构设计

3.1 新老架构对比

如下图是一个老架构方案,其核心问题主要集中于Native逻辑抽象差,其中逻辑层面还设计到多线程并发使得问题倍增,Android/iOS/Flutter交互繁杂,开发维护成本高,核心层耦合较为严重,无插拔式概念;undefined

考虑到历史架构的问题,演进如下新架构设计undefined

架构从上至下依次为业务层分发层逻辑层以及数据源层,数据源层来源于推送或网络请求,其封装于Native层,通过Flutter插件将消息协议数据上抛到Flutter侧的核心逻辑层,处理完成后变成Flutter DB的Enitity实体,实体中挂载一些消息协议实体;核心逻辑层将繁杂数据扁平化打包挂载到分发层中的会话内存模型数据或消息内存模型数据,最后通过观察者模式的订阅分发到业务逻辑中;Flutter IM重点集中改造逻辑层和分发层,将IM核心逻辑和业务层面数据模型进行封装隔离,核心逻辑层和数据库交互后将数据封装到分发层的moduleData中,通过订阅方式分发到业务层数据模型中;此外在IM模型中DB也是重点依赖的,个人对DB数据库管理进行全面封装解,实现一种轻量级,性能佳的Flutter DB管理框架;

3.2 DB存储模型

Flutter IM架构的DB存储依赖数据库插件,目前主流插件是Sqflite,其存储模型如下:undefined依据上图Sqflite插件的DB存储模型会有2个等待队列,一个是Flutter层同步执行队列,一个是Native层的线程执行队列,其Android实现机制是HandlerThread,因此Query/Save读写在会同一线程队列中,导致响应速度慢,容易造成DB SQL堆积,此外缺失缓存模型,于是个人定制如下改进方案undefinedFlutter侧通过表的主键设计查询时候会优先从Entity Cache层去获取,若缓存不存在,则通过Sqflite插件查询,同时改造Sqflite插件成支持sync/Async同步异步两种方式操作,对应到Native侧也会有同步线程队列和异步线程队列,保证数据吞吐率;但是这里建议查询使用异步,存储使用同步更稳妥,主要怕出现多个相同的数据元model同一时间进入异步线程池中,存储先后顺序无法有效的保证;

3.3 ORM数据库方案

IM架构重度依赖DB数据库,而当前业界还没有一个完备的数据库ORM管理方案,参考了Android的OrmLite/GreenDao,个人自行设计一套Flutter ORM数据库管理方案,其核心思想如下:undefined由于Flutter不支持反射,因此无法直接像Android的开源数据库方式操作,但可通过APT方式,将Entity和Orm Entity绑定于一身,操作OrmEntity即操作Entity,整个代码风格设计也和OrmLite极其相似,参考代码如下:

undefined

3.4 IM内存数据模型

FlutterIM架构在内存数据模型主要划分为会话和消息两个颗粒度,会话内存数据模型交托于SessionModuleData,消息内存数据模型交托于MessageModuleData;会话内存数据有一个根节点RootNotice,然后其挂载PSessionMessageNotice(这里PSessionMessageNotice是ORM映射的会话DB表模型)子节点集合;消息内存数据会有一个MessageConatiner容器管理,其内部挂载此会话中的PMessage(PMessage是ORM映射的消息DB表模型)消息集合;

依据上一章节,PSessionMessageNotice设计了一个OrmEnitity Cache,考虑到IM中会话数是有限的,因此PSessionMessageNotice都是直接缓存到Cache中,这种做法的好处是各地去拿会话数据元时候都是缓存中同一个对象,容易保证多次重复读写的数据一致性;而PSessionMessageNotice考虑到其数量可以无限多的特殊性,因此这里将其挂载到MessageContainer的内存管理中,在退出会话的时机会校验容器中PMessage集合的数量,适当缩容可以减少内存开销,模型如下图所示:undefined

3.5 状态管理方案

Flutter IM状态管理方案比较简单,对数据源Session/Message维度使用观察者模式的订阅分发方式实现,架构类似于EventBus模式,页面级的状态管理无论使用fish-redux,scopeModel或者provider几乎影响面不大,核心还是需保留一种插拔式抽象更重要;架构如下图:undefined

3.6 IM同步模型方案

如下是当前现状的消息同步模型,模型中存在ACCS Thread/Main Thread/Region Thread等多线程并发场景,导致易出现多线程高并发的问题;native的推送和网络请求同步的隔离方案通过Lock的锁机制,并且通过队列降频等方式处理,流程繁琐且易出错。整体通过Region Version Gap去判断是否有域空洞,进而执行域同步补充数据。undefined改进的同步模型如下,在Flutter侧天然没多线程场景,通过一种标记位的转化同步异步实现类似Handler消息队列,架构清晰简约了很多,避免锁带来的开销以及同步问题,undefined

4.进展以及性能对比

•针对架构层面:在FlutterIM架构中,重点将双端逻辑差异性统一成同一份Dart代码,完全磨平Android/iOS的代码差异性带来的问题,降低开发维护,测试回归,视觉验收的一半成本,极大提高研发效率;架构上进行重构分层,实现一种解耦合,插拔式的IM架构;同时Native到Flutter侧的大量数据上抛序列化过程改造程Flutter引用传递,解决极限测试场景下的私聊卡顿问题;•针对线上舆情:补齐UT和TLog的集团日志方式做到可追踪,可排查;另外针对于很多现存的疑难杂症重点集中专项解决,比如iphone5C的架构在Flutter侧统一规划,未读红点计数等问题也在架构模型升级中修复,此外多媒体音视频发送模块进行改造升级;•性能数据对比:当IM架构的逻辑层和UI层都切换成Flutter后,和原先架构模式初步对比,整体内存水位持平,其中私聊场景下小米9测试结构内存下降40M,功耗降低4mah,CPU降低1%;极限测试场景下新架构内存数据相比于旧架构有一个较为明显的改观,主要由于两个界面都使用Flutter场景下,页面切换的开销降低很多;

5.展望

JS跨端不安全,C++跨端成本有点高,Flutter会是一个较好选择;彼时闲鱼FlutterIM架构升级根本目的从来不是因Flutter而Flutter,是由于历史包袱的繁重,代码层面的维护成本高,新业务的扩展性差,人力配比不协调以及疑难杂症的舆情持续反馈等等因素造成我们不得不去探索新方案。经过闲鱼IM超复杂业务场景验证Flutter模式的逻辑跨端可行性,闲鱼在Flutter路上会一直保持前沿探索,最后能反馈到生态圈;总结一句话,探索过程在于你勇于迈出第一步,后面才会不断惊喜发现

收起阅读 »

Jetpack—架构组件—App Startup

App Startup介绍作用这是官网的截图,大意就是 App Startup 是一种用来在 app 启动时候规范初始化数据的 library。同时使用 App Startup 可以解决我们平时滥用 ContentProvider 导致的启动变慢问题。还有一点...
继续阅读 »

App Startup

介绍作用

这是官网的截图,大意就是 App Startup 是一种用来在 app 启动时候规范初始化数据的 library。同时使用 App Startup 可以解决我们平时滥用 ContentProvider 导致的启动变慢问题。

还有一点,App Startup 可以用于 app 开发,也可以用来进行 sdk 开发

App Startup 的优势

  1. 平时使用 ContentProvider 自动获取 ApplicationContext 的方式管理混乱,并且多个 ContentProvider 初始化的方式也无法保证初始化的顺序

  2. 统一管理的方式可以明显提升 app 初始化速度,注:仅限于用较多 ContentProvider 来初始化应用的 app,反之不是不能用,只是没有优化效果

依赖

dependencies {
implementation("androidx.startup:startup-runtime:1.0.0")
}
复制代码

使用 AppStartup 初始化全局单例对象(main 分支)

  1. Car 对象
class Car(private val name: String) {
companion object {
var instance: Car? = null
fun getInstance(name: String): Car {
if (instance == null) {
instance = Car(name)
}
return instance!!
}
}

override fun toString(): String {
return "$name ${Random.nextInt(100)}"
}
}
复制代码
  1. 首先需要实现一个 Initializer
class AndroidInitializer : Initializer<Car> {
override fun create(context: Context): Car {
return Car.getInstance("出租车")
}

override fun dependencies(): MutableList<Class<out Initializer<*>>> {
return mutableListOf()
}
}
复制代码
  1. 在代码中注册 AndroidInitializer
 <provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
>
<meta-data
android:name="com.ananananzhuo.appstartupdemo.AndroidInitializer"
android:value="androidx.startup" />
</provider>
复制代码
  1. 分析

本例中 Car 对象,Car 对象内部维护了一个全局单例方法 getInstance。

前面说了,AppStartup 是用来维护全局单例的,那么实际上这个单例的初始化就是通过我们定义的 AndroidInitializer 对象 create 方法来初始化的。

  1. 我们会在 MainActivity 中调用 Car 的 toString 方法,代码如下
 logEE(Car.getInstance("小汽车:").toString())
logEE(Car.getInstance("小汽车:").toString())
logEE(Car.getInstance("小汽车:").toString())
复制代码

我们调用了,三次 toString 方法

代码输出如下:

我们 MainActivity 中代码 getInstance 传入的参数是 "小汽车",但是打印的却是 "出租车"。查看 AndroidInitializer 中的代码发现,我们在 AndroidInitializer 中的 create 方法中创建对象的参数是 "出租车"。

由此可以证明,我们的全局 Car 单例在 AndroidInitializer 中就已经初始化完成了。

手动初始化组件

上一节中我们使用在 Manifest 中注册组件的方式实现 Car 对象的自动初始化。

但是,实际上我们是可以不在 Manifest 中注册的方式实现初始化的,手动初始化的方式如下:

 AppInitializer.getInstance(this)
.initializeComponent(AndroidInitializer::class.java)
复制代码

这种方式的弊端是一次只能初始化一个组件

实现相互依赖的多实例的初始化(分支:multimodule)

通过上一节的学习,你可能会有这样的疑问:AppStartup 啥用没有吧,我直接在 Application 中一行代码初始化不香吗,非要用你这种方式???

那么现在我就要用 AppStartup 实现多实例的初始化,让你进一步了解 AppStartup 的应用

我们这一节的逻辑先描述一下:

本例中我们需要创建两个对象,Person 和 Noodle,两者都是全局单例的。

Person 持有 Noodle 对象的引用,

Person 中有一个 eat 方法,本例中我们的 eat 会输出一行 "某某人" 吃 "面条" 的日志

废话不多说,上代码:

不要嫌代码长,都是一看就懂的逻辑

  1. Person 和 Noodle
class Person(val name:String) {
private var noodle: Noodle? = null
companion object {
private var instance: Person? = null
fun getInstance(name:String): Person {
if (instance == null) {
instance = Person(name)
}
return instance!!
}
}

fun addNoodle(paramsnoodle: Noodle) {
noodle = paramsnoodle
}


fun eat() {
logEE("${name} 吃 ${noodle?.name}")
}
}
复制代码
class Noodle {
val name = "面条"

companion object {
private var instance: Noodle? = null
fun getInstance(): Noodle {
if (instance == null) {
instance = Noodle()
}
return instance!!
}
}
}
复制代码
  1. PersonInitializer、NoodleInitializer
class PersonInitializer : Initializer<Person> {
override fun create(context: Context): Person {
return Person.getInstance("李白").apply {
addNoodle(Noodle.getInstance())
}
}

override fun dependencies(): MutableList<Class<out Initializer<*>>> {
return mutableListOf(NoodleInitializer::class.java)
}
}
复制代码

class NoodleInitializer:Initializer<Noodle> {
override fun create(context: Context): Noodle {
return Noodle.getInstance()
}

override fun dependencies(): MutableList<Class<out Initializer<*>>> {
return mutableListOf()
}
}
复制代码

这两个组件中 PersonInitializer 的 create 方法中创建了 Person 的实例,并向里面添加 Noodle 的实例。

划重点:

PersonInitializer 的 dependencies 方法中返回了 mutableListOf(NoodleInitializer::class.java)。这句代码的意思是在 PersonInitializer 中的 Person 初始化之前会先初始化 NoodleInitializer 中的 Noodle 实例,然后当 PersonInitializer 中 addNoodle 的时候 Noodle 全局单例已经创建好了。

  1. 调用吃面条方法
Person.getInstance("杜甫").eat()
复制代码
  1. 打印日志输出

日志输出符合我们的预期

多实例的注册组件方式如下,我们将 PersonInitializer、NoodleInitializer 都被注册到 meta-data 中了。

实际上,NoodleInitializer 的组件是完全可以不注册的,因为在 PersonInitializer 的 dependencies 中已经声明了 NoodleInitializer 组件。

  <provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false">
<meta-data
android:name="com.ananananzhuo.appstartupdemo.PersonInitializer"
android:value="androidx.startup" />
<meta-data
android:name="com.ananananzhuo.appstartupdemo.NoodleInitializer"
android:value="androidx.startup" />
</provider>
复制代码

使用 AppStartup 进行 sdk 开发(分支:sdk_develop)

本例介绍 sdk 开发中 AppStartup 的使用,实际上与应用开发是一样的,但是感觉还是有必要说一下。

在本例中我们新建了一个 library 的 module,在 library 里面编写了我们的 AppStartup 的代码逻辑,然后将 Library 打包成 arr,集成到 app 模块中,在 app 的 Manifest 中注册组件,并调用组件的相关方法。

  1. aar 集成 

  2. library 中的代码

class LibraryInitializer:Initializer<Student> {
override fun create(context: Context): Student {
return Student.getInstance()
}

override fun dependencies(): MutableList<Class<out Initializer<*>>> {
return mutableListOf()
}
}
复制代码
class Student(val name: String) {
companion object {
private val student = Student("安安安安卓")
fun getInstance(): Student {
return student
}
}

fun study() {
Log.e("tag", "${name} 好好学习")
}
}
复制代码
  1. Manifest 中注册组件
 <provider
android:name="androidx.startup.InitializationProvider"
android:authorities="com.ananananzhuo.appstartupdemo.androidx-startup"
android:exported="false"
>
<meta-data
android:name="com.ananananzhuo.library.LibraryInitializer"
android:value="androidx.startup" />
</provider>
复制代码
  1. 日志打印

  1. 结论

通过这种方式,第三方 sdk 只需要定义自己的 AppStartup 组件就可以,我们在注册组件的时候在 manifest 中添加第三方组件的信息就可以完成第三方组件的初始化了。

这极大的避免了某些自以为是的 sdk,打着方便我们集成的名义搞 ContentProvider 初始化恶心我们

以后如果你合作的第三方 sdk 提供方再出现 ContentProvider 的初始化方式恶心你,那么拿出我的文章好好教他做人。

收起阅读 »

SpannableStringBuiler封装Kotlin

前言SpannableStringBuilder和SpannableString功能基本一样,不过SpannableStringBuilder可以拼接,主要是通过setSpan来实现各种效果,主要的方法如下:start: 指定Span的开始位置 end: 指定...
继续阅读 »

前言

SpannableStringBuilder和SpannableString功能基本一样,不过SpannableStringBuilder可以拼接,主要是通过setSpan来实现各种效果,主要的方法如下:

start: 指定Span的开始位置
end: 指定Span的结束位置,并不包括这个位置。
flags:取值有如下四个
Spannable. SPAN_INCLUSIVE_EXCLUSIVE:前面包括,后面不包括,即在文本前插入新的文本会应用该样式,而在文本后插入新文本不会应用该样式
Spannable. SPAN_INCLUSIVE_INCLUSIVE:前面包括,后面包括,即在文本前插入新的文本会应用该样式,而在文本后插入新文本也会应用该样式
Spannable. SPAN_EXCLUSIVE_EXCLUSIVE:前面不包括,后面不包括
Spannable. SPAN_EXCLUSIVE_INCLUSIVE:前面不包括,后面包括
what: 对应的各种Span,不同的Span对应不同的样式。已知的可用类有:
BackgroundColorSpan : 文本背景色
ForegroundColorSpan : 文本颜色
MaskFilterSpan : 修饰效果,如模糊(BlurMaskFilter)浮雕
RasterizerSpan : 光栅效果
StrikethroughSpan : 删除线
SuggestionSpan : 相当于占位符
UnderlineSpan : 下划线
AbsoluteSizeSpan : 文本字体(绝对大小)
DynamicDrawableSpan : 设置图片,基于文本基线或底部对齐。
ImageSpan : 图片
RelativeSizeSpan : 相对大小(文本字体)
ScaleXSpan : 基于x轴缩放
StyleSpan : 字体样式:粗体、斜体等
SubscriptSpan : 下标(数学公式会用到)
SuperscriptSpan : 上标(数学公式会用到)
TextAppearanceSpan : 文本外貌(包括字体、大小、样式和颜色)
TypefaceSpan : 文本字体
URLSpan : 文本超链接
ClickableSpan : 点击事件

简单使用示例

初始化SpannableString或SpannableStringBuilder,然后设置对应的setPan就可以实现对应的效果。

SpannableString spannableString = new SpannableString("要设置的内容");
ForegroundColorSpan colorSpan = new ForegroundColorSpan(Color.parseColor("#009ad6"));
spannableString.setSpan(colorSpan, 0, 8, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
((TextView)findViewById(R.id.mode1)).setText(spannableString);

具体使用详情可以参考:强大的SpannableStringBuilder

封装使用

对很多功能都可以封装,简化使用,这里使用了扩展函数,更方便在Kotlin中使用,不过在Java中也可以使用,使用方法如下:

第一种情况,要设置的内容已经是一段完整的内容

注意:链式调用时,只需要初始化第一个src就可以了,后续都会默认使用第一个,如果后续继续初始化src, 会导致前面的设置无效,只有最后一个生效。target和range都是为了确定要改变的文字的范围,两个初始化一个即可。

  1. 对整个字符串设置效果

    src 和target默认等于TextView的text

    //对整个 text 设置方式一,textView已经设置过内容,可以不用初始化src
    tvTvOne.sizeSpan(textSize = 20f)
    //对整个 text 设置方式二
    tvTvOne2.typeSpan(src = "全部文字加粗",target = "全部文字加粗",
    type = SsbKtx.type_bold)
  2. 设置部分文字效果

    type 有3个,对应加粗,倾斜,加粗倾斜

    //设置部分文字效果
    //tvTv2.typeSpan(range = 2..4,type = SsbKtx.type_bold)
    tvTv2.typeSpan(target = "部分",type = SsbKtx.type_bold)
    //设置加粗倾斜效果
    tvTv3.typeSpan(range = 0..4,type = SsbKtx.type_bold_italic)
  3. 对同一个文字设置多个效果

    对同一个部分做多种效果,只能第一个设置 src, 后续设置会导致前面的无效。

    //        tvTv4.typeSpan(range = 0..4,type = SsbKtx.type_bold_italic)
    // .foregroundColorIntSpan(range = 0..4,color = Color.GREEN)
    // .strikethroughSpan(range = 0..4)
    tvTv4.typeSpan(src = "只能这个可以设置 src,后面的再设置会导致前面效果无效",
    range = 0..4,type = SsbKtx.type_bold_italic)
    .foregroundColorIntSpan(range = 0..4,color = Color.GREEN)
    .strikethroughSpan(range = 0..4)
  4. 对多个不同的文字分别设置不同的效果

     tvTv5.typeSpan(range = 0..4,type = SsbKtx.type_bold_italic)
    .foregroundColorIntSpan(range = 7..11,color = Color.BLUE)
  5. 设置部分点击

    tvTv6.clickIntSpan(range = 0..4){
    Toast.makeText(this, "hello", Toast.LENGTH_SHORT).show()
    }
  6. 设置部分超链接

    tvTv7.urlSpan(range = 0..4,url = "https://www.baidu.com")

第二种情况,拼接成一个完整的字符串

  1. 拼接成完整的内容

     tvTv8.text = "拼接一段文字"
    tvTv8.appendTypeSpan("加粗",SsbKtx.type_bold)
    .strikethroughSpan(target = "加粗")//对同一部分文字做多个效果
    .appendForegroundColorIntSpan("改变字体颜色",Color.RED)

    如果想对拼接的内容做多个效果,可以在其后面调用对应的方法,只要traget或是range正确即可。

完整代码

object SsbKtx {
const val flag = SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE
const val type_bold = Typeface.BOLD
const val type_italic = Typeface.ITALIC
const val type_bold_italic = Typeface.BOLD_ITALIC

}
//-------------------CharSequence相关扩展-----------------------
/**
*CharSequence不为 null 或者 empty
*/
fun CharSequence?.isNotNullOrEmpty() = !isNullOrEmpty()

/**
*获取一段文字在文字中的范围
* @param target
* @return
*/
fun CharSequence.range(target: CharSequence): IntRange {
val start = this.indexOf(target.toString())
return start..(start + target.length)
}

/**
*将一段指定的文字改变大小
* @return
*/
fun CharSequence.sizeSpan(range: IntRange, textSize: Int): CharSequence {
return SpannableStringBuilder(this).apply {
setSpan(AbsoluteSizeSpan(textSize), range.first, range.last, SsbKtx.flag)
}
}


/**
*设置文字颜色
* @param range
* @return
*/
fun CharSequence.foregroundColorSpan(range: IntRange, color: Int = Color.RED): CharSequence {
return SpannableStringBuilder(this).apply {
setSpan(ForegroundColorSpan(color), range.first, range.last, SsbKtx.flag)
}
}

/**
*设置click,将一段文字中指定range的文字添加颜色和点击事件
* @param range
* @return
*/
fun CharSequence.clickSpan(
range: IntRange,
color: Int = Color.RED,
isUnderlineText: Boolean = false,
clickAction: () -> Unit
): CharSequence {
return SpannableString(this).apply {
val clickableSpan = object : ClickableSpan() {
override fun onClick(widget: View) {
clickAction()
}

override fun updateDrawState(ds: TextPaint) {
ds.color = color
ds.isUnderlineText = isUnderlineText
}
}
setSpan(clickableSpan, range.first, range.last, SsbKtx.flag)
}
}


//-------------------TextView相关扩展--------------------------
/**
*设置目标文字大小, src,target 为空时,默认设置整个 text
* @return
*/
fun TextView?.sizeSpan(
src: CharSequence? = this?.text,
target: CharSequence? = this?.text,
range: IntRange? = null,
@DimenRes textSize: Int
): TextView? {
return when {
this == null -> this
src.isNullOrEmpty() -> this
target.isNullOrEmpty() && range == null -> this
textSize == 0 -> this
range != null -> {
text = src.sizeSpan(range, ResUtils.getDimensionPixelSize(textSize))
this
}
target.isNotNullOrEmpty() -> {
text = src.sizeSpan(src.range(target!!), ResUtils.getDimensionPixelSize(textSize))
this
}
else -> this
}
}

/**
*设置目标文字大小, src,target 为空时,默认设置整个 text
* @return
*/
fun TextView?.sizeSpan(
src: CharSequence? = this?.text,
target: CharSequence? = this?.text,
range: IntRange? = null,
textSize: Float
): TextView? {
return when {
this == null -> this
src.isNullOrEmpty() -> this
target.isNullOrEmpty() && range == null -> this
textSize == 0f -> this
range != null -> {
text = src.sizeSpan(range, DensityUtils.dp2px(textSize))
this
}
target.isNotNullOrEmpty() -> {
text = src.sizeSpan(src.range(target!!), DensityUtils.dp2px(textSize))
this
}
else -> this
}
}

/**
*追加内容设置字体大小
* @param str
* @param textSize
* @return
*/
fun TextView?.appendSizeSpan(str: String?, textSize: Float): TextView? {
str?.let {
this?.append(it.sizeSpan(0..it.length, DensityUtils.dp2px(textSize)))
}
return this
}

fun TextView?.appendSizeSpan(str: String?, @DimenRes textSize: Int): TextView? {
str?.let {
this?.append(it.sizeSpan(0..it.length, ResUtils.getDimensionPixelSize(textSize)))
}
return this
}

/**
*设置目标文字类型(加粗,倾斜,加粗倾斜),src,target 为空时,默认设置整个 text
* @return
*/
fun TextView?.typeSpan(
src: CharSequence? = this?.text,
target: CharSequence? = this?.text,
range: IntRange? = null,
type: Int
): TextView? {
return when {
this == null -> this
src.isNullOrEmpty() -> this
target.isNullOrEmpty() && range == null -> this
range != null -> {
text = src.typeSpan(range, type)
this
}
target.isNotNullOrEmpty() -> {
text = src.typeSpan(src.range(target!!), type)
this
}
else -> this
}
}

fun TextView?.appendTypeSpan(str: String?, type: Int): TextView? {
str?.let {
this?.append(it.typeSpan(0..it.length, type))
}
return this
}

/**
*设置目标文字下划线
* @return
*/
fun TextView?.underlineSpan(
src: CharSequence? = this?.text,
target: CharSequence? = this?.text,
range: IntRange? = null
): TextView? {
return when {
this == null -> this
src.isNullOrEmpty() -> this
target.isNullOrEmpty() && range == null -> this
range != null -> {
text = src.underlineSpan(range)
this
}
target.isNotNullOrEmpty() -> {
text = src.underlineSpan(src.range(target!!))
this
}
else -> this
}
}


/**
*设置目标文字对齐方式
* @return
*/
fun TextView?.alignSpan(
src: CharSequence? = this?.text,
target: CharSequence? = this?.text,
range: IntRange? = null,
align: Layout.Alignment
): TextView? {
return when {
this == null -> this
src.isNullOrEmpty() -> this
target.isNullOrEmpty() && range == null -> this
range != null -> {
text = src.alignSpan(range, align)
this
}
target.isNotNullOrEmpty() -> {
text = src.alignSpan(src.range(target!!), align)
this
}
else -> this
}
}

fun TextView?.appendAlignSpan(str: String?, align: Layout.Alignment): TextView? {
str?.let {
this?.append(it.alignSpan(0..it.length, align))
}
return this
}

/**
*设置目标文字超链接
* @return
*/
fun TextView?.urlSpan(
src: CharSequence? = this?.text,
target: CharSequence? = this?.text,
range: IntRange? = null,
url: String
): TextView? {
return when {
this == null -> this
src.isNullOrEmpty() -> this
target.isNullOrEmpty() && range == null -> this
range != null -> {
movementMethod = LinkMovementMethod.getInstance()
text = src.urlSpan(range, url)
this
}
target.isNotNullOrEmpty() -> {
movementMethod = LinkMovementMethod.getInstance()
text = src.urlSpan(src.range(target!!), url)
this
}
else -> this
}
}

fun TextView?.appendUrlSpan(str: String?, url: String): TextView? {
str?.let {
this?.append(it.urlSpan(0..it.length, url))
}
return this
}

/**
*设置目标文字点击
* @return
*/
fun TextView?.clickIntSpan(
src: CharSequence? = this?.text,
target: CharSequence? = this?.text,
range: IntRange? = null,
color: Int = Color.RED,
isUnderlineText: Boolean = false,
clickAction: () -> Unit
): TextView? {
return when {
this == null -> this
src.isNullOrEmpty() -> this
target.isNullOrEmpty() && range == null -> this
range != null -> {
movementMethod = LinkMovementMethod.getInstance()
highlightColor = Color.TRANSPARENT // remove click bg color
text = src.clickSpan(range, color, isUnderlineText, clickAction)
this
}
target.isNotNullOrEmpty() -> {
movementMethod = LinkMovementMethod.getInstance()
highlightColor = Color.TRANSPARENT // remove click bg color
text = src.clickSpan(src.range(target!!), color, isUnderlineText, clickAction)
this
}
else -> this
}
}

fun TextView?.appendClickIntSpan(
str: String?, color: Int = Color.RED,
isUnderlineText: Boolean = false,
clickAction: () -> Unit
): TextView? {
str?.let {
this?.append(it.clickSpan(0..it.length, color, isUnderlineText, clickAction))
}
return this
}

/**
*设置目标文字点击
* @return
*/
fun TextView?.clickSpan(
src: CharSequence? = this?.text,
target: CharSequence? = this?.text,
range: IntRange? = null,
@ColorRes color: Int,
isUnderlineText: Boolean = false,
clickAction: () -> Unit
): TextView? {
return when {
this == null -> this
src.isNullOrEmpty() -> this
target.isNullOrEmpty() && range == null -> this
range != null -> {
movementMethod = LinkMovementMethod.getInstance()
highlightColor = Color.TRANSPARENT // remove click bg color
text = src.clickSpan(range, ResUtils.getColor(color), isUnderlineText, clickAction)
this
}
target.isNotNullOrEmpty() -> {
movementMethod = LinkMovementMethod.getInstance()
highlightColor = Color.TRANSPARENT // remove click bg color
text = src.clickSpan(
src.range(target!!),
ResUtils.getColor(color),
isUnderlineText,
clickAction
)
this
}
else -> this
}
}

fun TextView?.appendClickSpan(
str: String?,
@ColorRes color: Int,
isUnderlineText: Boolean = false,
clickAction: () -> Unit
): TextView? {
str?.let {
this?.append(
it.clickSpan(
0..it.length,
ResUtils.getColor(color),
isUnderlineText,
clickAction
)
)
}
return this
}

里面的ResUtils只是简单的获取资源文件,如果想直接引入,可以参考Github直接使用gradle依赖。

收起阅读 »

iOS组件化开发实践

目录:1.组件化需求来源2.组件化初识3.组件化必备的工具使用4.模块拆分5.组件工程兼容swift环境6.组件之间的通讯7.组件化后的资源加载8.OC工程底层换swift代码9.总结1. 组件化需求来源起初的这个项目,App只有一条产品线,代码逻辑相对比较清...
继续阅读 »

目录:

1.组件化需求来源
2.组件化初识
3.组件化必备的工具使用
4.模块拆分
5.组件工程兼容swift环境
6.组件之间的通讯
7.组件化后的资源加载
8.OC工程底层换swift代码
9.总结

1. 组件化需求来源

起初的这个项目,App只有一条产品线,代码逻辑相对比较清晰,后期随着公司业务的迅速发展,现在App里面承载了大概五六条产品线,每个产品线的流程有部分是一样的,也有部分是不一样的,这就需要做各种各样的判断及定制化需求。大概做了一年多后,出现了不同产品线提过来的需求,开发人员都需要在主工程中开发,但是开发人员开发的是不同的产品线,也得将整个工程跑起来,代码管理、并行开发效率、分支管理、上线时间明显有所限制。大概就在去年底,我们的领导提出了这个问题,希望作成组件化,将代码重构拆分成模块,在主工程中组装拆分的模块,形成一个完整的App。

2. 组件化初识

随着业务线的增多,业务的复杂度增加,App的代码逻辑复杂度也增加了,后期的开发维护成本也增加了,为什么这么说呢?业务逻辑没有分类,查找问题效率降低(针对新手),运行也好慢哦,真的好烦哦......我们要改变这种局面。而组件化开发,就是将一个臃肿,复杂的单一工程的项目, 根据功能或者属性进行分解,拆分成为各个独立的功能模块或者组件 ; 然后根据项目和业务的需求,按照某种方式, 任意组织成一个拥有完整业务逻辑的工程。

组件化开发的缺点:

1、代码耦合严重
2、依赖严重
3、其它app接入某条产品线难以集成
4、项目复杂、臃肿、庞大,编译时间过长
5、难以做集成测试
6、对开发人员,只能使用相同的开发模式
......
组件化开发的优点:

1、项目结构清晰
2、代码逻辑清晰
3、拆分粒度小
4、快速集成
5、能做单元测试
6、代码利用率高
7、迭代效率高
......
组件化的实质:就是对现有项目或新项目进行基础、功能及业务逻辑的拆分,形成一个个的组件库,使宿主工程能在拆分的组件库里面查找需要的功能,组装成一个完整的App。

3. 组件化必备的工具使用

组件的存在方式是以每个pod库的形式存在的。那么我们组合组件的方法就是通过利用CocoaPods的方式添加安装各个组件,我们就需要制作CocoaPods远程私有库,将其发不到公司的gitlab或GitHub,使工程能够Pod下载下来。

Git的基础命令:

echo "# test" >> README.md
git init
git add README.md
git commit -m "first commit"
git remote add origin https://github.com/c/test.git
git push -u origin master

CocoaPods远程私有库制作:
1、Create Component Project

pod lib create ProjectName

2、Use Git

echo "# test" >> README.md
git init
git add README.md
git commit -m "first commit"
git remote add origin https://github.com/c/test.git
git push -u origin master

3、Edit podspec file

vim CoreLib.podspec
Pod::Spec.new do |s|
s.name = '组件工程名'
s.version = '0.0.1'
s.summary = 'summary'

s.description = <<-DESC
description
DESC

s.homepage = '远程仓库地址'
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { '作者' => '作者' }
s.source = { :git => '远程仓库地址', :tag => s.version.to_s }

s.ios.deployment_target = '8.0'

s.source_files = 'Classes/**/*.{swift,h,m,c}'
s.resources = 'Assets/*'

s.dependency 'AFNetworking', '~> 2.3'
end

4、Create tag

//create local tag
git tag '0.0.1'

git tag 0.0.1

//local tag push to remote
git push --tags

git push origin 0.0.1

//delete local tag
git tag -d 0.0.1

//delete remote tag
git tag origin :0.0.1

5、Verify Component Project

pod lib lint --allow-warnings --no-clean

6、Push To CocoaPods

pod repo add CoreLib git@git.test/CoreLib.git
pod repo push CoreLib CoreLib.podspec --allow-warnings

4. 模块拆分


基础组件库:
基础组件库放一些最基础的工具类,比如金额格式化、手机号/shenfen证/邮箱的有效校验,实质就是不会依赖业务,不会和业务牵扯的文件。

功能组件库:
分享的封装、图片的轮播、跑马灯功能、推送功能的二次封装,即开发一次,以后都能快速集成的功能。

业务组件库:
登录组件、实名组件、消息组件、借款组件、还款组件、各条产品线组件等。

中间件(组件通讯):
各个业务组件拆分出来后,组件之间的通讯、传参、回调就要考虑了,此时就需要一个组件通讯的工具类来处理。

CocoaPods远程私有库:
每个拆分出去的组件存在的形式都是以Pod的形式存在的,并能达到单独运行成功。

宿主工程:
宿主工程就是一个壳,在组件库中寻找这个工程所需要的组件,然后拿过来组装成一个App。

5. 组件工程兼容swift环境

在做组件化之前,这个项目使用的是Objective-C语言写的,还没有支持在项目里面使用Swift语言的能力,考虑到后期肯定会往Swift语言切过去的,于是借着这次重构的机会,创建的组件工程都是swift工程。

Podfile文件需要添加==use_frameworks!==

source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '8.0'
inhibit_all_warnings!
use_frameworks!

target 'CoreLib_Example' do
pod 'CoreLib', :path => '../'
end

这里其实有个大坑需要特别注意,在支持Swift环境后,部分Objective-C语言的三方库采用的是==静态库==,在OC文件中引用三方库头文件,会一直报头文件找不到,我们在遇到这个问题时找遍了百度,都没找到解决方案,整整花了一个星期的时间尝试。

解决方案:我们对这些三方库(主要有:UMengAnalytics、Bugly、AMapLocation-NO-IDFA)再包一层,使用CocoaPods远程私有库管理,对外暴露我们写的文件,引用我们写的头文件,就能调用到。

Pod::Spec.new do |s|
s.name = ''
s.version = '0.0.1'
s.summary = '包装高德地图、分享、友盟Framework.'

s.description = <<-DESC
DESC

s.homepage = ''
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { '' => '' }
s.source = { :git => '', :tag => s.version.to_s }

s.ios.deployment_target = '8.0'

s.source_files = ['Classes/UMMob/**/*.{h,m}','Classes/Bugly/**/*.{h,m}','Classes/AMap/**/*.{h,m}']
s.public_header_files = ['Classes/*.h']
s.libraries = 'sqlite3', 'c++', 'z', 'z.1.1.3', 'stdc++', 'stdc++.6.0.9'
s.frameworks = 'SystemConfiguration', 'CoreTelephony', 'JavaScriptcore', 'CoreLocation', 'Security', 'Foundation'
s.vendored_frameworks = 'Frameworks/**/*.framework'
s.xcconfig = { "FRAMEWORK_SEARCH_PATHS" => "Pods/WDContainerLib/Frameworks" }

s.requires_arc = true
end

6. 组件之间的通讯

在将业务控制器拆分出去后,如果一个组件要调用另一个组件里面的控制器,平常的做法是直接==#import "控制器头文件"==,现在在不同的组件里面是无法import的,那该怎么做呢?答案就是使用==消息发送机制==。

思路:

1.每个业务组件库里面会有一个控制器的配置文件(路由配置文件),标记着每个控制器的key;
2.在App每次启动时,组件通讯的工具类里面需要解析控制器配置文件(路由配置文件),将其加载进内存;
3.在内存中查询路由配置,找到具体的控制器并动态生成类,然后使用==消息发送机制==进行调用函数、传参数、回调,都能做到。

((id (*)(id, SEL, NSDictionary *)) objc_msgSend)((id) cls, @selector(load:), param);
((void(*)(id, SEL,NSDictionary*))objc_msgSend)((id) vc, @selector(callBack:), param);

Or

[vc performSelector:@selector(load:) withObject:param];
[vc performSelector:@selector(callBack:) withObject:param];

好处:

解除了控制器之间的依赖;
使用iOS的消息发送机制进行传参数、回调参数、透传参数;
路由表配置文件,能实现界面动态配置、动态生成界面;
路由表配置文件放到服务端,还可以实现线上App的跳转逻辑;
将控制器的key提供给H5,还可以实现H5跳转到Native界面;

7. 组件化后的资源加载

新项目已开始就采用组件化开发,还是特别容易的,如果是老项目重构成组件化,那就比较悲剧了,OC项目重构后,app包里面会有一个==Frameworks==文件夹,所有的组件都在这个文件夹下,并且以==.framework==(比如:WDComponentLogin.framework)结尾。在工程中使用的==xib、图片==,使用正常的方式加载,是加载不到的,原因就是xib、图片的路径==(工程.app/Frameworks/WDComponentLogin.framework/LoginViewController.nib、工程.app/Frameworks/WDComponentLogin.framework/login.png)==发生了变化。

以下是在组件库中加载nib文件/图片文件的所有情况:

/**
从主工程mainBundle或从所有的组件(组件名.framework)中加载图片

@param imageName 图片名称
@return 返回查找的图片结果
*/
+ (UIImage *_Nullable)loadImageNamed:(NSString *_Nonnull)imageName;

/**
从指定的组件中加载图片,主要用于从当前组件加载其他组件中的图片

@param imageName 图片名称
@param frameworkName 组件名称
@return 返回查找的图片结果
*/
+ (UIImage *_Nullable)loadImageNamed:(NSString *_Nonnull)imageName frameworkName:(NSString *_Nonnull)frameworkName;

/**
从指定的组件的Bundle文件夹中加载图片,主要用于从当前组件加载其他组件Bundle文件夹中的图片

@param imageName 图片名称
@param bundleName Bundle文件夹名
@param frameworkName 组件名称
@return 返回查找的图片结果
*/
+ (UIImage *_Nullable)loadImageNamed:(NSString *_Nonnull)imageName bundleName:(NSString *_Nonnull)bundleName frameworkName:(NSString *_Nonnull)frameworkName;

/**
从主工程mainBundle的指定Bundle文件夹中去加载图片

@param imageName 图片名称
@param bundleName Bundle文件夹名
@return 返回查找的图片结果
*/
+ (UIImage *_Nullable)loadImageNamed:(NSString *_Nonnull)imageName bundleName:(NSString *_Nonnull)bundleName;

/**
从指定的组件(组件名.framework)中加载图片
说明:加载组件中的图片,必须指明图片的全名和图片所在bundle的包名

@param imageName 图片名称
@param targetClass 当前类
@return 返回查找的图片结果
*/
+ (UIImage *_Nullable)loadImageNamed:(NSString *_Nonnull)imageName targetClass:(Class _Nonnull)targetClass;

/**
从指定的组件(组件名.framework)中的Bundle文件夹中加载图片
说明:加载组件中的图片,必须指明图片的全名和图片所在bundle的包名

@param imageName 图片名称
@param bundleName Bundle文件夹名
@param targetClass 当前类
@return 返回查找的图片结果
*/
+ (UIImage *_Nullable)loadImageNamed:(NSString *_Nonnull)imageName bundleName:(NSString *_Nonnull)bundleName targetClass:(Class _Nonnull)targetClass;

/**
加载工程中的nib文件
eg:[_tableview registerNib:[WDLoadResourcesUtil loadNibClass:[WDRepaymentheaderView class]] forHeaderFooterViewReuseIdentifier:kWDRepaymentheaderView]
@param class nib文件名
@return 返回所需要的nib对象
*/
+ (UINib *_Nullable)loadNibClass:(NSObject *_Nonnull)targetClass;

控制器加载方式:

@implementation WDBaseViewController

- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
NSString *classString = [[NSStringFromClass(self.class) componentsSeparatedByString:@"."] lastObject];
if ([[NSBundle bundleForClass:[self class]] pathForResource:classString ofType:@"nib"] != nil) {
//有xib
return [super initWithNibName:classString bundle:[NSBundle bundleForClass:[self class]]];
}else if ([[NSBundle mainBundle] pathForResource:classString ofType:@"nib"] == nil) {
//没有xib
return [super initWithNibName:nil bundle:nibBundleOrNil];
} else {
return [super initWithNibName:(nibNameOrNil == nil ? classString : nibNameOrNil) bundle:nibBundleOrNil];
}
}
@end

UIView视图加载方式:

OC版本

+ (id)loadFromNIB {
if ([[NSFileManager defaultManager] fileExistsAtPath:[NSBundle bundleForClass:[self class]].bundlePath]) {
return [[[NSBundle bundleForClass:[self class]] loadNibNamed:[self description]
owner:self
options:nil] lastObject];
}else{
return [[[NSBundle mainBundle] loadNibNamed:[self description] owner:self options:nil] lastObject];
}

}

+ (id)loadFromNIB:(NSInteger)index {
if ([[NSFileManager defaultManager] fileExistsAtPath:[NSBundle bundleForClass:[self class]].bundlePath]) {
return [[NSBundle bundleForClass:[self class]] loadNibNamed:[self description]
owner:self
options:nil][index];
}else{
return [[NSBundle mainBundle] loadNibNamed:[self description] owner:self options:nil][index];
}

}

Swift版本

// MARK: - 通过nib加载视图
@objc public static func loadFromNIB() -> UIView! {
return (Bundle(for: self.classForCoder()).loadNibNamed(self.description().components(separatedBy: ".")[1], owner: self, options: nil)?.first as? UIView)!
}

8. OC工程底层换swift代码

目前正在做OC底层的统一,换成swift写的代码。

1、控制器Base、Web控制器Base使用OC代码,因为OC控制器不能继承Swift,而Swift控制器可以继承OC写的控制器。
2、导航栏、工具栏、路由、基础组件、功能组件、混合开发插件都是用Swift语言。
3、Swift移动组件大部分完成,OC工程、Swift工程都统一使用开发的移动组件库。

9. 总结

经过半年的努力重构,终于将工程拆分成组件化开发了,也从中学到了很多,希望自己能再接再厉和同事一起进步。

链接:https://www.jianshu.com/p/196ec57cdc75

收起阅读 »

APP路由框架与组件化简析

前端开发经常遇到一个词:路由,在Android APP开发中,路由还经常和组件化开发强关联在一起,那么到底什么是路由,一个路由框架到底应该具备什么功能,实现原理是什么样的?路由是否是APP的强需求呢?与组件化到底什么关系,本文就简单分析下如上几个问题。 路由...
继续阅读 »

前端开发经常遇到一个词:路由,在Android APP开发中,路由还经常和组件化开发强关联在一起,那么到底什么是路由,一个路由框架到底应该具备什么功能,实现原理是什么样的?路由是否是APP的强需求呢?与组件化到底什么关系,本文就简单分析下如上几个问题。


路由的概念


路由这个词本身应该是互联网协议中的一个词,维基百科对此的解释如下:


路由(routing)就是通过互联的网络把信息从源地址传输到目的地址的活动。路由发生在OSI网络参考模型中的第三层即网络层。

个人理解,在前端开发中,路由就是通过一串字符串映射到对应业务的能力。APP的路由框首先能够搜集各组件的路由scheme,并生成路由表,然后,能够根据外部输入字符串在路由表中匹配到对应的页面或者服务,进行跳转或者调用,并提供会获取返回值等,示意如下


image.png


所以一个基本路由框架要具备如下能力:





    1. APP路由的扫描及注册逻辑




    1. 路由跳转target页面能力




    1. 路由调用target服务能力



APP中,在进行页面路由的时候,经常需要判断是否登录等一些额外鉴权逻辑所以,还需要提供拦截逻辑等,比如:登陆。


三方路由框架是否是APP强需求


答案:不是,系统原生提供路由能力,但功能较少,稍微大规模的APP都采用三方路由框架。


Android系统本身提供页面跳转能力:如startActivity,对于工具类APP,或单机类APP,这种方式已经完全够用,完全不需要专门的路由框架,那为什么很多APP还是采用路由框架呢?这跟APP性质及路由框架的优点都有关。比如淘宝、京东、美团等这些大型APP,无论是从APP功能还是从其研发团队的规模上来说都很庞大,不同的业务之间也经常是不同的团队在维护,采用组件化的开发方式,最终集成到一个APK中。多团队之间经常会涉及业务间的交互,比如从电影票业务跳转到美食业务,但是两个业务是两个独立的研发团队,代码实现上是完全隔离的,那如何进行通信呢?首先想到的是代码上引入,但是这样会打破了低耦合的初衷,可能还会引入各种问题。例如,部分业务是外包团队来做,这就牵扯到代码安全问题,所以还是希望通过一种类似黑盒的方式,调用目标业务,这就需要中转路由支持,所以国内很多APP都是用了路由框架的。其次我们各种跳转的规则并不想跟具体的实现类扯上关系,比如跳转商详的时候,不希望知道是哪个Activity来实现,只需要一个字符串映射过去即可,这对于H5、或者后端开发来处理跳转的时候,就非常标准。


原生路由的限制:功能单一,扩展灵活性差,不易协同


传统的路由基本上就限定在startActivity、或者startService来路由跳转或者启动服务。拿startActivity来说,传统的路由有什么缺点:startActivity有两种用法,一种是显示的,一种是隐式的,显示调用如下:


<!--1 导入依赖-->
import com.snail.activityforresultexample.test.SecondActivity;

public class MainActivity extends AppCompatActivity {

void jumpSecondActivityUseClassName(){
<!--显示的引用Activity类-->
Intent intent =new Intent(MainActivity.this, SecondActivity.class);
startActivity(intent);
}


显示调用的缺点很明显,那就是必须要强依赖目标Activity的类实现,有些场景,尤其是大型APP组件化开发时候,有些业务逻辑出于安全考虑,并不想被源码或aar依赖,这时显式依赖的方式就无法走通。再来看看隐式调用方法。


第一步:manifest中配置activity的intent-filter,至少要配置一个action


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.snail.activityforresultexample">
<application
...
<activity android:name=".test.SecondActivity">
<intent-filter>
<!--隐式调用必须配置android.intent.category.DEFAULT-->
<category android:name="android.intent.category.DEFAULT"/>
<!--至少配置一个action才能通过隐式调用-->
<action android:name="com.snail.activityforresultexample.SecondActivity" />
<!--可选-->
<!-- <data android:mimeType="video/mpeg" android:scheme="http" ... />-->
</intent-filter>
</activity>
</application>
</manifest>

第二步:调用


void jumpSecondActivityUseFilter() {
Intent intent = new Intent();
intent.setAction("com.snail.activityforresultexample.SecondActivity");
startActivity(intent);
}

如果牵扯到数据传递写法上会更复杂一些,隐式调用的缺点有如下几点:



  • 首先manifest中定义复杂,相对应的会导致暴露的协议变的复杂,不易维护扩展。

  • 其次,不同Activity都要不同的action配置,每次增减修改Activity都会很麻烦,对比开发者非常不友好,增加了协作难度。

  • 最后,Activity的export属性并不建议都设置成True,这是降低风险的一种方式,一般都是收归到一个Activity,DeeplinkActivitiy统一处理跳转,这种场景下,DeeplinkActivitiy就兼具路由功能,隐式调用的场景下,新Activitiy的增减势必每次都要调整路由表,这会导致开发效率降低,风险增加。


可以看到系统原生的路由框架,并没太多考虑团队协同的开发模式,多限定在一个模块内部多个业务间直接相互引用,基本都要代码级依赖,对于代码及业务隔离很不友好。如不考虑之前Dex方法树超限制,可以认为三方路由框架完全是为了团队协同而创建的


APP三方路由框架需具备的能力


目前市面上大部分的路由框架都能搞定上述问题,简单整理下现在三方路由的能力,可归纳如下:



  • 路由表生成能力:业务组件**[UI业务及服务]**自动扫描及注册逻辑,需要扩展性好,无需入侵原有代码逻辑

  • scheme与业务映射逻辑 :无需依赖具体实现,做到代码隔离

  • 基础路由跳转能力 :页面跳转能力的支持

  • 服务类组件的支持 :如去某个服务组件获取一些配置等

  • [扩展]路由拦截逻辑:比如登陆,统一鉴权

  • 可定制的降级逻辑:找不到组件时的兜底


可以看下一个典型的Arouter用法,第一步:对新增页面添加Router Scheme 声明,


	@Route(path = "/test/activity2")
public class Test2Activity extends AppCompatActivity {
...
}

build阶段会根据注解搜集路由scheme,生成路由表。第二步使用


        ARouter.getInstance()
.build("/test/activity2")
.navigation(this);

如上,在ARouter框架下,仅需要字符串scheme,无需依赖任何Test2Activity就可实现路由跳转。


APP路由框架的实现


路由框架实现的核心是建立scheme和组件**[Activity或者其他服务]**的映射关系,也就是路由表,并能根据路由表路由到对应组件的能力。其实分两部分,第一部分路由表的生成,第二部分,路由表的查询


路由表的自动生成


生成路由表的方式有很多,最简单的就是维护一个公共文件或者类,里面映射好每个实现组件跟scheme,


image.png


不过,这种做法缺点很明显:每次增删修改都要都要修改这个表,对于协同非常不友好,不符合解决协同问题的初衷。不过,最终的路由表倒是都是这条路,就是将所有的Scheme搜集到一个对象中,只是实现方式的差别,目前几乎所有的三方路由框架都是借助注解+APT[Annotation Processing Tool]工具+AOP(Aspect-Oriented Programming,面向切面编程)来实现的,基本流程如下:


image.png


其中牵扯的技术有注解、APT(Annotation Processing Tool)、AOP(Aspect-Oriented Programming,面向切面编程)。APT常用的有JavaPoet,主要是遍历所有类,找到被注解的Java类,然后聚合生成路由表,由于组件可能有很多,路由表可能也有也有多个,之后,这些生成的辅助类会跟源码一并被编译成class文件,之后利用AOP技术【如ASM或者JavaAssist】,扫描这些生成的class,聚合路由表,并填充到之前的占位方法中,完成自动注册的逻辑。



JavaPoet如何搜集并生成路由表集合?



以ARouter框架为例,先定义Router框架需要的注解如:


@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface Route {

/**
* Path of route
*/
String path();

该注解用于标注需要路由的组件,用法如下:


@Route(path = "/test/activity1", name = "测试用 Activity")
public class Test1Activity extends BaseActivity {
@Autowired
int age = 10;

之后利用APT扫描所有被注解的类,生成路由表,实现参考如下:


@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (CollectionUtils.isNotEmpty(annotations)) {
<!--获取所有被Route.class注解标注的集合-->
Set<? extends Element> routeElements = roundEnv.getElementsAnnotatedWith(Route.class);
<!--解析并生成表-->
this.parseRoutes(routeElements);
...
return false;
}

<!--生成中间路由表Java类-->
private void parseRoutes(Set<? extends Element> routeElements) throws IOException {
...
// Generate groups
String groupFileName = NAME_OF_GROUP + groupName;
JavaFile.builder(PACKAGE_OF_GENERATE_FILE,
TypeSpec.classBuilder(groupFileName)
.addJavadoc(WARNING_TIPS)
.addSuperinterface(ClassName.get(type_IRouteGroup))
.addModifiers(PUBLIC)
.addMethod(loadIntoMethodOfGroupBuilder.build())
.build()
).build().writeTo(mFiler);

产物如下:包含路由表,及局部注册入口。


image.png



自动注册:ASM搜集上述路由表并聚合插入Init代码区



为了能够插入到Init代码区,首先需要预留一个位置,一般定义一个空函数,以待后续填充:


	public class RouterInitializer {

public static void init(boolean debug, Class webActivityClass, IRouterInterceptor... interceptors) {
...
loadRouterTables();
}
//自动注册代码
public static void loadRouterTables() {

}
}

首先利用AOP工具,遍历上述APT中间产物,聚合路由表,并注册到预留初始化位置,遍历的过程牵扯是gradle transform的过程,



  • 搜集目标,聚合路由表

      /**扫描jar*/
    fun scanJar(jarFile: File, dest: File?) {

    val file = JarFile(jarFile)
    var enumeration = file.entries()
    while (enumeration.hasMoreElements()) {
    val jarEntry = enumeration.nextElement()
    if (jarEntry.name.endsWith("XXRouterTable.class")) {
    val inputStream = file.getInputStream(jarEntry)
    val classReader = ClassReader(inputStream)
    if (Arrays.toString(classReader.interfaces)
    .contains("IHTRouterTBCollect")
    ) {
    tableList.add(
    Pair(
    classReader.className,
    dest?.absolutePath
    )
    )
    }
    inputStream.close()
    } else if (jarEntry.name.endsWith("HTRouterInitializer.class")) {
    registerInitClass = dest
    }
    }
    file.close()
    }

  • 对目标Class注入路由表初始化代码

      fun asmInsertMethod(originFile: File?) {

    val optJar = File(originFile?.parent, originFile?.name + ".opt")
    if (optJar.exists())
    optJar.delete()
    val jarFile = JarFile(originFile)
    val enumeration = jarFile.entries()
    val jarOutputStream = JarOutputStream(FileOutputStream(optJar))

    while (enumeration.hasMoreElements()) {
    val jarEntry = enumeration.nextElement()
    val entryName = jarEntry.getName()
    val zipEntry = ZipEntry(entryName)
    val inputStream = jarFile.getInputStream(jarEntry)
    //插桩class
    if (entryName.endsWith("RouterInitializer.class")) {
    //class文件处理
    jarOutputStream.putNextEntry(zipEntry)
    val classReader = ClassReader(IOUtils.toByteArray(inputStream))
    val classWriter = ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
    val cv = RegisterClassVisitor(Opcodes.ASM5, classWriter,tableList)
    classReader.accept(cv, EXPAND_FRAMES)
    val code = classWriter.toByteArray()
    jarOutputStream.write(code)
    } else {
    jarOutputStream.putNextEntry(zipEntry)
    jarOutputStream.write(IOUtils.toByteArray(inputStream))
    }
    jarOutputStream.closeEntry()
    }
    //结束
    jarOutputStream.close()
    jarFile.close()
    if (originFile?.exists() == true) {
    Files.delete(originFile.toPath())
    }
    optJar.renameTo(originFile)
    }


最终RouterInitializer.class的 loadRouterTables会被修改成如下填充好的代码:


 public static void loadRouterTables() {

<!---->
register("com.alibaba.android.arouter.routes.ARouter$$Root$$modulejava");
register("com.alibaba.android.arouter.routes.ARouter$$Root$$modulekotlin");
register("com.alibaba.android.arouter.routes.ARouter$$Root$$arouterapi");
register("com.alibaba.android.arouter.routes.ARouter$$Interceptors$$modulejava");
...
}

如此就完成了路由表的搜集与注册,大概的流程就是如此。当然对于支持服务、Fragment等略有不同,但大体类似。


Router框架对服务类组件的支持


通过路由的方式获取服务属于APP路由比较独特的能力,比如有个用户中心的组件,我们可以通过路由的方式去查询用户是否处于登陆状态,这种就不是狭义上的页面路由的概念,通过一串字符串如何查到对应的组件并调用其方法呢?这种的实现方式也有多种,每种实现方式都有自己的优劣。



  • 一种是可以将服务抽象成接口,沉到底层,上层实现通过路由方式映射对象

  • 一种是将实现方法直接通过路由方式映射


先看第一种,这种事Arouter的实现方式,它的优点是所有对外暴露的服务都暴露接口类【沉到底层】,这对于外部的调用方,也就是服务使用方非常友好,示例如下:



先定义抽象服务,并沉到底层



image.png


public interface HelloService extends IProvider {
void sayHello(String name);
}


实现服务,并通过Router注解标记



@Route(path = "/yourservicegroupname/hello")
public class HelloServiceImpl implements HelloService {
Context mContext;

@Override
public void sayHello(String name) {
Toast.makeText(mContext, "Hello " + name, Toast.LENGTH_SHORT).show();
}


使用:利用Router加scheme获取服务实例,并映射成抽象类,然后直接调用方法。



  ((HelloService) ARouter.getInstance().build("/yourservicegroupname/hello").navigation()).sayHello("mike");

这种实现方式对于使用方其实是很方便的,尤其是一个服务有多个可操作方法的时候,但是缺点是扩展性,如果想要扩展方法,就要改动底层库。


再看第二种:将实现方法直接通过路由方式映射


服务的调用都要落到方法上,参考页面路由,也可以支持方法路由,两者并列关系,所以组要增加一个方法路由表,实现原理与Page路由类似,跟上面的Arouter对比,不用定义抽象层,直接定义实现即可:



定义Method的Router



	public class HelloService {

<!--参数 name-->
@MethodRouter(url = {"arouter://sayhello"})
public void sayHello(String name) {
Toast.makeText(mContext, "Hello " + name, Toast.LENGTH_SHORT).show();
}


使用即可



 RouterCall.callMethod("arouter://sayhello?name=hello");

上述的缺点就是对于外部调用有些复杂,尤其是处理参数的时候,需要严格按照协议来处理,优点是,没有抽象层,如果需要扩展服务方法,不需要改动底层。


上述两种方式各有优劣,不过,如果从左服务组件的初衷出发,第一种比较好:对于调用方比较友好。另外对于CallBack的支持,Arouter的处理方式可能也会更方便一些,可以比较方便的交给服务方定义。如果是第二种,服务直接通过路由映射的方式,处理起来就比较麻烦,尤其是Callback中的参数,可能要统一封装成JSON并维护解析的协议,这样处理起来,可能不是很好。


路由表的匹配


路由表的匹配比较简单,就是在全局Map中根据String输入,匹配到目标组件,然后依赖反射等常用操作,定位到目标。


组件化与路由的关系


组件化是一种开发集成模式,更像一种开发规范,更多是为团队协同开发带来方便。组件化最终落地是一个个独立的业务及功能组件,这些组件之间可能是不同的团队,处于不同的目的在各自维护,甚至是需要代码隔离,如果牵扯到组件间的调用与通信,就不可避免的借助路由,因为实现隔离的,只能采用通用字符串scheme进行通信,这就是路由的功能范畴。


组件化需要路由支撑的根本原因:组件间代码实现的隔离


总结



  • 路由不是一个APP的必备功能,但是大型跨团队的APP基本都需要

  • 路由框架的基本能力:路由自动注册、路由表搜集、服务及UI界面路由及拦截等核心功能

  • 组件化与路由的关系:组件化的代码隔离导致路由框架成为必须




作者:看书的小蜗牛
链接:https://juejin.cn/post/6973905775940861966
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »