注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

Activity启动流程(基于AOSP 11)

当点击Launcher的App icon的时候,点击事件传递给ItemClickHandler的onClickAppShortcut,并最终调用到launcher.startActivitySafely->BaseDraggingActivity.sta...
继续阅读 »

当点击Launcher的App icon的时候,点击事件传递给ItemClickHandler的onClickAppShortcut,并最终调用到launcher.startActivitySafely->BaseDraggingActivity.startActivitySafely:

    public boolean startActivitySafely(View v, Intent intent, @Nullable ItemInfo item,
@Nullable String sourceContainer) {
// ...
// 将Intent的Flag设置为FLAG_ACTIVITY_NEW_TASK
// 这样启动Activity就会在一个新的Activity任务栈中
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
// ...
try {
// ... 省略一些快捷方式启动
else if (user == null || user.equals(Process.myUserHandle())) {
// Could be launching some bookkeeping activity
// 启动应用Activity
startActivity(intent, optsBundle);
AppLaunchTracker.INSTANCE.get(this).onStartApp(intent.getComponent(),
Process.myUserHandle(), sourceContainer);
}
// ...
return true;
} catch (NullPointerException|ActivityNotFoundException|SecurityException e) {
Toast.makeText(this, R.string.activity_not_found, Toast.LENGTH_SHORT).show();
Log.e(TAG, "Unable to launch. tag=" + item + " intent=" + intent, e);
}
return false;
}

startActivity的实现位于Activity中:

    public void startActivity(Intent intent, @Nullable Bundle options) {
// ... 都是调用startActivityForResult启动Activity
// 传入的requestCode是-1
if (options != null) {
startActivityForResult(intent, -1, options);
} else {
startActivityForResult(intent, -1);
}
}

启动Activity最终通过Instrumentation的execStartActivity方法启动,而Instrumentation的作用其实是监控应用和系统交互的中间这,是安卓提供的一层Hook操作,让开发者有能力介入到Activity的各项声明周期中,比如可以用来测试,Activity的各项生命周期的回调也是通过这个类作为中间层实现的,execStartActivity核心是调用到:

int result = ActivityTaskManager.getService().startActivity(whoThread,
who.getBasePackageName(), who.getAttributionTag(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()), token,
target != null ? target.mEmbeddedID : null, requestCode, 0, null, options);

ActivityTaskManager是Android 10中新增加的一个类,接替了ActivityManager中Activity与ActivityTask交互的工作,而ActivitTaskManager.getService返回的就是IActivityTaskManager这个AIDL文件中声明的接口,而SystemServer进程中的系统服务ActivityTaskManagerService只需要继承IActivityTaskManager.Stub就可以实现用户进程与服务进程的通信。

new Singleton<IActivityTaskManager>() {
@Override
protected IActivityTaskManager create() {
final IBinder b = ServiceManager.getService(Context.ACTIVITY_TASK_SERVICE);
return IActivityTaskManager.Stub.asInterface(b);
}
};

首先获取到SystemServer进程中ActivityTaskManagerService这个系统服务的IBinder接口,然后将这个IBinder接口转换为IActivityTaskManager接口,这样引用进程就可以调用到IActivityTaskManager的方法,与ATMS进行通信了。

那么应用进程调用startActivity终于还是调用到ATMS的startActivity方法了,到这里,创建Activity的逻辑就从Launcher进程进入到了ATMS所在的SystemServer进程:

在ATMS的startActivity中,调用到ATMS.startActivityAsUser:

    private int startActivityAsUser(IApplicationThread caller, String callingPackage,
@Nullable String callingFeatureId, Intent intent, String resolvedType,
IBinder resultTo, String resultWho, int requestCode, int startFlags,
ProfilerInfo profilerInfo, Bundle bOptions, int userId, boolean validateIncomingUser) {
// 前置权限校验
// 下面获取ActivityStart对象
// 这个对象是控制Activity启动的代理对象
// 7.0以后加入,收集Activity启动相关的各种参数逻辑
// 决定如何启动Activity一级Activity的Task和TaskStack关联
return getActivityStartController().obtainStarter(intent, "startActivityAsUser")
// ... 设置了很多参数
.execute();

}

在对ActivityStarter对象设置完一堆参数之后,调用其execute执行启动:

    int execute() {
try {
// ...
int res;
synchronized (mService.mGlobalLock) {
// ...
// 启动信息都在mRequest中了,执行这个请求
res = executeRequest(mRequest);
// 启动完成之后的一些通知等工作......
}
} finally {
onExecutionComplete();
}
}

executeRequest里边的逻辑很多,主要包含各种校验逻辑,比如检验启动参数,Flag等,总之,正常启动流程会去根据各种启动参数创建一个ActivityRecord对象,ActivityRecord对象就代表着一个在任务栈中的Activity实体,最后调用到ActivityStarter的startActivityUnchecked。

startActivityUnchecked->startActivityInner,在startActivityInner中,会先去对Activity启动Flag进行各种校验,比如如果启动的是一个根Activity,但是启动Flag并不是FLAG_ACTIVITY_NEW_TASK,那么会将LaunchFlag修改为FLAG_ACTIVITY_NEW_TASK。然后找到是否有可以重用的任务栈,如果没有,会去创建一个新的任务栈,在startActivityInner中关于启动的核心逻辑是调用:

mRootWindowContainer.resumeFocusedStacksTopActivities(
mTargetStack, mStartActivity, mOptions);

RootWindowContainer是在WMS中创建的对象,可以理解为其管理者屏幕显示的内容。在RootWindowContainer中,会调用到ActivityStack的resumeTopActivityUncheckedLocked->resumeTopActivityInnerLocked。这个方法在11上长达400+行,逻辑很多,和我们要分析的主流程相关的重点如下:

// attachedToProcess的判断逻辑:要启动的Activity的进程是否存在以及
// 对应进程的ActivityThread是否存在,而我们这里启动的是一个新进程
// 的根Activity,此时新进程还未创建以及ActivityRecord还没有绑定到新进程中
if (next.attachedToProcess()) {
// ...
} else {
// ...
mStackSupervisor.startSpecificActivity(next, true, true);
}

ActivityStackSupervisor的startSpecificActivity如下:

    void startSpecificActivity(ActivityRecord r, boolean andResume, boolean checkConfig) {
// Is this activity's application already running?
final WindowProcessController wpc =
mService.getProcessController(r.processName, r.info.applicationInfo.uid);

boolean knownToBeDead = false;
// 如果应用所在的进程已经存在,那么执行realStartActivityLocked
// 进行Activity启动
if (wpc != null && wpc.hasThread()) {
try {
// 这个函数后面再做分析
realStartActivityLocked(r, wpc, andResume, checkConfig);
return;
} catch (RemoteException e) {
Slog.w(TAG, "Exception when starting activity "
+ r.intent.getComponent().flattenToShortString(), e);
}

// If a dead object exception was thrown -- fall through to
// restart the application.
knownToBeDead = true;
}

r.notifyUnknownVisibilityLaunchedForKeyguardTransition();

final boolean isTop = andResume && r.isTopRunningActivity();
// 如果应用进程不存在,ATMS(最终是AMS)将会异步启动应用进程
mService.startProcessAsync(r, knownToBeDead, isTop, isTop ? "top-activity" : "activity");
}

对于应用进程不存在的情况,AMS会往SystemServer的消息队列中发送一个Lambda类型的Message(其实就是类似Runnable类型的消息),然后消息执行的时候,调用关系:

AMS.startProcessAsync->AMS.startProcess->AMS.startProcessLocked->ProcessList.startProcessLocked...->ProcessList.startProcess->Process.start,最终通过ZygoteProcess调用startViaZygote,通过Zygote进程fork出应用进程,完成应用进程的创建,在应用进程创建的同时,会往SystemServer的消息队列中发送一条超时消息(默认10s),超时到了应用进程仍然未启动的话,就会杀死这个启动失败的进程。

而在Zygote fork出子进程之后,就会执行到ActivityThrea的main方法,在ActivityThread的main方法中,做的最重要的两件事,开启主线程的Looper机制和创建ActivityThread并调用ActivityThread的attach函数,其中:

// mAppThread是ApplicationThread对象,在ActivityThread被创建的时候创建
// 是AMS与应用进程通信的桥梁
// 这里初始化RuntimeInit.mApplicationObject值
RuntimeInit.setApplicationObject(mAppThread.asBinder());
// 获取到AMS的Binder接口IActivityManager
final IActivityManager mgr = ActivityManager.getService();
try {
// 调用AMS的attachApplication
mgr.attachApplication(mAppThread, startSeq);
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}

AMS中attachApplication实现如下:

    public final void attachApplication(IApplicationThread thread, long startSeq) {
// 如果传入的ApplicationThread是空的,那么就会异常终止应用进程
if (thread == null) {
throw new SecurityException("Invalid application interface");
}
synchronized (this) {
// 获取binder调用方进程的pid和uid,这里的调用方就是应用进程
int callingPid = Binder.getCallingPid();
final int callingUid = Binder.getCallingUid();
// 修改binder调用端的pid、uid为调用进程的pid、uid,并返回原uid和pid的组合
// 高32位表示上面的uid,低32位表示pid
final long origId = Binder.clearCallingIdentity();
attachApplicationLocked(thread, callingPid, callingUid, startSeq);
// 恢复pid、uid
Binder.restoreCallingIdentity(origId);
}
}

attachApplicationLocked又是一个超长函数,内部主要是对App进程做一些检查设置等工作。

// 内部首先会去查找有没有旧的进程记录,如果有,做一些清理类的工作
// ...
// 然后比如debug模式也会做一些特殊的设置工作
// ...
// 再比如建立Binder死亡回调,当进AMS所在服务程异常退出的时候,
// Binder驱动可以通知到应用进程,并释放死亡进程锁占用的没有正常关闭的binder文件。
// 当SystemServer进程因为异常挂掉的时候,是有重启机制的
// ...
// 移除在AMS启动进程的过程中埋下的PROC_START_TIMEOUT_MSG消息
mHandler.removeMessages(PROC_START_TIMEOUT_MSG, app);
// ...
// IPC调用应用进程ApplicationThread的bindApplication:
thread.bindApplication(...);

AMS在完成对ApplicationThread一些设置工作后,会回到应用进程的bindApplication,bindApplication的时候,AMS会将一些通用的系统服务传递给应用进程,比如WMS,IMS等等。

public final void bindApplication(...) {
if (services != null) {
// 将AMS传过来的通用系统服务缓存到ServiceManager的
// 静态cache中
ServiceManager.initServiceCache(services);
}
// 通过ActivityThread的H(继承自Handler)
// 发送一条SET_CORE_SETTINGS的消息给主线程,
// 将coreSettings设置给ActivityThread对象
// 并触发当前进程所有Activity的重启(因为ActivityThread的核心设置被修改了)
setCoreSettings(coreSettings);
// AppBindData是bindApplication的一堆数据的封装
AppBindData data = new AppBindData();
// 通过mH发送BIND_APPLICATION消息给主线程
sendMessage(H.BIND_APPLICATION, data);
}

下面bindApplication的逻辑进入H中,H是ActivityThread中的自定义Handler,比如Service的创建,绑定,Activity的生命周期处理都在这里边:

而对于BIND_APPLICATION,handleMessage将调用到handleBindApplication。

在handleBindApplication中,会填充data.info字段,其实是LoadApk对象,通过getPackageInfo获取,其包含了比如mPackagerName应用包名,mDataDir应用数据目录,mResources,mApplication等信息。

然后是完成Context的创建:

final ContextImpl appContext = ContextImpl.createAppContext(this, data.info);
// createAppContext
static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo,
String opPackageName) {
if (packageInfo == null) throw new IllegalArgumentException("packageInfo");
// 创建ContextImpl对象,初始化了ApplicationContext的mMainThread、packageInfo等字段
// 在ContextImpl的构造方法中,还创建了ApplicationContentResolver这个对象
ContextImpl context = new ContextImpl(null, mainThread, packageInfo, null, null, null, null,
0, null, opPackageName);
context.setResources(packageInfo.getResources());
// 是否是系统状态栏的Context
context.mIsSystemOrSystemUiContext = isSystemOrSystemUI(context);
return context;
}

完成Context的创建之后,后面会继续Application对象的创建,还是在handleBindApplication中:

Application app;
// ...
app = data.info.makeApplication(data.restrictedBackupMode, null);

// LoadedApk.makeApplication:
// ...
ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
// 调用newApplication创建Application对象
app = mActivityThread.mInstrumentation.newApplication(
cl, appClass, appContext);
// ...
// 将Application赋值给LoadedApk的mApplication,并添加到ActivityThread的Application列表
mApplication = app;

在newApplication中:

public Application newApplication(ClassLoader cl, String className, Context context)
throws InstantiationException, IllegalAccessException,
ClassNotFoundException {
// 内部就是反射Application Class的newInstance
Application app = getFactory(context.getPackageName())
.instantiateApplication(cl, className);
// 将Context对象绑定给Application
app.attach(context);
return app;
}

// attach如下:
final void attach(Context context) {
// attachBaseContext就是将context赋值给Application(继承自ContextWrapper的)的mBase字段
attachBaseContext(context);
mLoadedApk = ContextImpl.getImpl(context).mPackageInfo;
}

继续回到应用进程的handleBindApplication中,在创建完Application对象之后:

// 回调到Application对象的onCreate方法
mInstrumentation.callApplicationOnCreate(app);

应用进程终于创建完成了,现在我们回到AMS的attachApplicationLocked中,在bindApplication完成Application对象的创建之后:

// 如果Application有待启动的Activity,那么执行Activity的启动
// 对于我们从Launcher中点击icon启动一个App来说,创建完新的进程和Application之后
// 就需要启动MainActivity
if (normalMode) {
try {
didSomething = mAtmInternal.attachApplication(app.getWindowProcessController());
} catch (Exception e) {
Slog.wtf(TAG, "Exception thrown launching activities in " + app, e);
badApp = true;
}
}
// ActivityTaskManagerService的内部类LocalService实现的attachApplication方法:
// 内部调用
mRootWindowContainer.attachApplication(wpc);

RootWindowContainer.attachApplication如下:

    boolean attachApplication(WindowProcessController app) throws RemoteException {
boolean didSomething = false;
for (int displayNdx = getChildCount() - 1; displayNdx >= 0; --displayNdx) {
mTmpRemoteException = null;
mTmpBoolean = false; // Set to true if an activity was started.

final DisplayContent display = getChildAt(displayNdx);
for (int areaNdx = display.getTaskDisplayAreaCount() - 1; areaNdx >= 0; --areaNdx) {
final TaskDisplayArea taskDisplayArea = display.getTaskDisplayAreaAt(areaNdx);
for (int taskNdx = taskDisplayArea.getStackCount() - 1; taskNdx >= 0; --taskNdx) {
final ActivityStack rootTask = taskDisplayArea.getStackAt(taskNdx);
if (rootTask.getVisibility(null /*starting*/) == STACK_VISIBILITY_INVISIBLE) {
break;
}
// 找到待启动的ActivityRecord,然后
// 调用startActivityForAttachedApplicationIfNeeded
final PooledFunction c = PooledLambda.obtainFunction(
RootWindowContainer::startActivityForAttachedApplicationIfNeeded, this,
PooledLambda.__(ActivityRecord.class), app,
rootTask.topRunningActivity());
rootTask.forAllActivities(c);
c.recycle();
if (mTmpRemoteException != null) {
throw mTmpRemoteException;
}
}
}
didSomething |= mTmpBoolean;
}
if (!didSomething) {
ensureActivitiesVisible(null, 0, false /* preserve_windows */);
}
return didSomething;
}

而startActivityForAttachedApplicationIfNeeded的实现,就是把我们找到的待启动的ActivityRecord,调用到mStackSupervisor.realStartActivityLocked。

回到ActivityStackSupervisor的startSpecificActivity:

    void startSpecificActivity(ActivityRecord r, boolean andResume, boolean checkConfig) {
// Is this activity's application already running?
final WindowProcessController wpc =
mService.getProcessController(r.processName, r.info.applicationInfo.uid);

boolean knownToBeDead = false;
// 如果应用所在的进程已经存在,那么执行realStartActivityLocked
// 进行Activity启动
if (wpc != null && wpc.hasThread()) {
try {
// 这个函数后面再做分析
realStartActivityLocked(r, wpc, andResume, checkConfig);
return;
} catch (RemoteException e) {
Slog.w(TAG, "Exception when starting activity "
+ r.intent.getComponent().flattenToShortString(), e);
}

// If a dead object exception was thrown -- fall through to
// restart the application.
knownToBeDead = true;
}

r.notifyUnknownVisibilityLaunchedForKeyguardTransition();

final boolean isTop = andResume && r.isTopRunningActivity();
// 如果应用进程不存在,ATMS(最终是AMS)将会异步启动应用进程
mService.startProcessAsync(r, knownToBeDead, isTop, isTop ? "top-activity" : "activity");
}

AMS在调用到mService.startProcessAsync启动一个新的应用进程之后,最终还是通过ActivityStackSupervisor的realStartActivityLocked,启动应用进程的首个Activity,那么再往下看,就是一般Activity的启动流程了,realStartActivityLocked仍然比较长,取我们关心的逻辑:

// 创建一个Activity启动的事务,ClientTransaction是一个实现了Parcelable接口的类
// 即该事务对象可以跨进程传递
final ClientTransaction clientTransaction = ClientTransaction.obtain(
proc.getThread(), r.appToken);
final DisplayContent dc = r.getDisplay().mDisplayContent;
// 往事务中添加一个LaunchActivityItem,代表事务的Callback,后面会执行Callback中的逻辑
clientTransaction.addCallback(LaunchActivityItem.obtain(new Intent(r.intent),...));
// ...
// 调度事务,
mService.getLifecycleManager().scheduleTransaction(clientTransaction);

先来看下事务是怎样获取的:

public static ClientTransaction obtain(IApplicationThread client, IBinder activityToken) {
// 先从对象池中找,找不到就创建
ClientTransaction instance = ObjectPool.obtain(ClientTransaction.class);
if (instance == null) {
instance = new ClientTransaction();
}
// 将IApplicationThread赋值给事务的mClient字段,
// 表示这个事务的接收端是哪个进程的ApplicationThread
instance.mClient = client;
instance.mActivityToken = activityToken;
return instance;
}

ClinetLifeCycleManager的调度事务实现如下:

void scheduleTransaction(ClientTransaction transaction) throws RemoteException {
final IApplicationThread client = transaction.getClient();
transaction.schedule();
// ...
}
//transaction.schedule();实现如下,IPC调用到应用进程ApplicationThread的scheduleTransaction
mClient.scheduleTransaction(this);

而ApplicationThread中的scheduleTransaction实现,就是调用ActivityThread的scheduleTransaction方法(AT继承自ClientTransactionHandler,实现在父类中):

void scheduleTransaction(ClientTransaction transaction) {
// 当事务在客户端进程执行的时候,在执行之前做一个hook callback操作
transaction.preExecute(this);
sendMessage(ActivityThread.H.EXECUTE_TRANSACTION, transaction);
}

发送出去的消息最终还是交给H来处理:

final ClientTransaction transaction = (ClientTransaction) msg.obj;
mTransactionExecutor.execute(transaction);

mTransactionExecutor并不是什么多线程的Executor,不过是对事务有序处理的逻辑封装,其execute如下:

public void execute(ClientTransaction transaction) {
// ...
// executeCallbacks中的重点就是执行传入事务的callback的execute方法
// item.execute(mTransactionHandler, token, mPendingActions);
//
executeCallbacks(transaction);
executeLifecycleState(transaction);
mPendingActions.clear();
// ...
}

而AMS在调用realStartActivityLocked传入事务的callback就是LaunchActivityItem,他的execute如下:

public void execute(ClientTransactionHandler client, IBinder token,
PendingTransactionActions pendingActions) {
// 创建一个ActivityClientRecord对象
ActivityClientRecord r = new ActivityClientRecord(token, mIntent, mIdent, mInfo,
mOverrideConfig, mCompatInfo, mReferrer, mVoiceInteractor, mState, mPersistentState,
mPendingResults, mPendingNewIntents, mIsForward,
mProfilerInfo, client, mAssistToken, mFixedRotationAdjustments);
// 这里client实际上就是TransactionExecutor中的mTransactionHandler
client.handleLaunchActivity(r, pendingActions, null /* customIntent */);
}

在ActivityThread中,ActivityThread对象创建的时候就会创建mTransactionExecutor对象,并把this引用传入到TransactionExecutor的构造方法中,所以这里mTransactionHandler正是实现了ClientTransactionHandler接口的ActivityThread对象,在ActivityThread的handleLaunchActivity中:

final Activity a = performLaunchActivity(r, customIntent);

而performLaunchActivity就会去创建对应的Activity对象,关于performLaunchActivity的分析,可以看前两篇Android UI系统工作流程。

收起阅读 »

完美解决macOS Homebrew安装JDK的一些问题

自从Oracle接手JDK之后,更新变快了,之前的“旧版本”也不容易下载了。最近一段时间Oracle一直不安生, 搞出来一堆幺蛾子, 所以安装方式也一直在变, 之前的方法已经不能用了, 网上找各种办法都不好使,下面针对各个版本给出了不同建议, 安装结束后, 可...
继续阅读 »

自从Oracle接手JDK之后,更新变快了,之前的“旧版本”也不容易下载了。最近一段时间Oracle一直不安生, 搞出来一堆幺蛾子, 所以安装方式也一直在变, 之前的方法已经不能用了, 网上找各种办法都不好使,下面针对各个版本给出了不同建议, 安装结束后, 可输入java -version确认是否安装成功。


一、JDK8~JDK12、OpenJDK及AdoptOpenJDK


这些都是比较主流的JDK版本, 目前大多数企业还在使用,但想要通过 Homebrew 却并不容易,网上查询的90%Homebrew安装JDK8的方式都是不能用的, 必须要寻求开源世界的帮助, 对于 JDK8 ~ JDK12, 这时会推荐 AdoptOpenJDK.


AdoptOpenJDK 是免费的、完全无品牌的 OpenJDK 版本,基于 GPL 开源协议(+Classpath Extension),以免费软件的形式提供社区版的 OpenJDK 二进制包,公司也可安全且放心使用。与由 Oracle 的 OpenJDK 构建版本不同,这些版本会提供更长的支持,像 Java 11 一样,至少提供 4 年的免费长期支持(LTS)计划。


通过 AdoptOpenJDK 可以安装最多版本的 JDK.


brew cask install AdoptOpenJDK/openjdk/adoptopenjdk8
brew cask install AdoptOpenJDK/openjdk/adoptopenjdk9
brew cask install AdoptOpenJDK/openjdk/adoptopenjdk10
brew cask install AdoptOpenJDK/openjdk/adoptopenjdk11
brew cask install AdoptOpenJDK/openjdk/adoptopenjdk12
brew cask install AdoptOpenJDK/openjdk/adoptopenjdk

二、JDK12、JDK13及OracleJDK


如果你想在电脑上装最新版的 JDK, 那么 Oracle 或许是你最想要的选择, 而 Oracle 家的最新版 JDK 也有两款, 一个是 Oracle 提供的 OpenJDK, 一个是商业版本 Oracle JDK, 但请注意 Oracle JDK 并不比 OpenJDK “更好”, 大家需要理性看待.


# 运行以下命令会安装 Oracle 提供的 Oracle JDK12
brew cask install oracle-jdk

# 在2019年5月
## 该命令会安装由 Oracle 提供的 OpenJDK12
brew cask install java
## 而该命令则安装由 Oracle 提供的 OpenJDK11
brew cask install java11

三、JDK7和Zulu


JDK7 甚至 AdoptOpenJDK 都不提供了, 这时候需要的是有商业背景的 Azul Zulu, zulu 是 OpenJDK 的免费版本, 在提供商业付费支持之外, Azul 也有为 zulu 提供免费的社区技术支持.


通过安装 zulu7 我们可以安装 OpenJDK7.


# Azul Zulu 也提供其他版本的 OpenJDK 像 zulu8 zulu11 和最新版的 zulu 均可使用
brew cask install homebrew/cask-versions/zulu7
brew cask install homebrew/cask-versions/zulu8
brew cask install homebrew/cask-versions/zulu11
brew cask install homebrew/cask-versions/zulu

四、JDK6


估计现在连好多企业都不用了吧,所以放到左后。 JDK6 主要由 Apple 自身提供。


brew cask install homebrew/cask-versions/java6



遇到问题耐心解决,如果还没解决,那肯定是时间没花够



  • 9月21日心心念想、梦寐以求的MacBook Pro (13-inch, 2019, Four Thunderbolt 3 ports)入手的第一天晚上,我拆机用了多半个小时,就是边拆边拍照,拆个盒子都要洗个手😂那种。在这之前,我平时抽空就提前“上手macOS”

  • 依稀还记得初一自己捣鼓的那台组装机,主板:华硕,CPU:英特尔奔腾E5400,硬盘512G HDD,金士顿2G,现在确实已经很卡了,尽管装了最新版的Windows v1903专业版。不过作为我的第一台电脑,好多东西都是在它上面学的,感谢下老爸将它作为考了年级第一奖励台电脑送给我。

  • 9月21日那天晚上,emmm,是晚上,大家应该和我有同感,一到晚上一些站点就特别慢,比如GitHub之类的,但强迫症的我肯定得在第一时间部署好我的开发环境吧,毕竟大二那会儿我快成专业的运维了~~同学电脑有毛病就找我,开发环境(Java/Android/Python/Node~)有问题还是找我。所以就先装Git、JDK、Maven吧。

  • Git简单,现在macOS 虽然不自带Git,但是安装Homebrew之前安装的Command Line Tools里面包括了Git、gcc等工具,很方便,也比较快(前提是切换了Homebrew的源

  • JDK确实费了老大劲才装好,还是第二天早晨6点10分起来干的,因为早晨访问GitHub这种站点确实比下午快很多。最后诞生了此文。

  • Maven不说了,可以Homebrew安装,也可以在Maven官网下载包之后解压,然后使用,好多人可能会想着配Mavne的环境变量,其实我个人认为没必要。直接说下我是怎么使用的吧:IDEA现在是Java开发的主流IDE工具,里面的终端可以自己配置,结合zsh加上oh my zsh简直无可挑剔!我是使用了“两个Maven”,一个是公司用(公司有自己的Maven仓库),另一个是自己玩(配置的阿里云镜像),在同一个IDEA切换不同的settings.xml便可以实现多Maven切换,之间互不影响,主要是IDEA确实智能,智能在部分配置是每个项目单独的,多个项目可以完全配置不同的Maven,公司项目和自己项目随意切换,Maven也跟着换,很方便。至于不配置Maven环境mvn命令不能使用的问题我想说:IDEA里面依然可以执行你手敲的mvn -U clean package -Dmaven.test.skip=true,所以我不推荐配置Maven的环境变量。用多个Maven的原因我想大家也没明白,如果公司项目里修改了某个包的源码或者重写了某个方法,而你自己项目同样使用了该包的,那么这种情况下极易出错,使用Maven或多或少可能会遇到Maven存在一些Bug,这种迭代了N个版本依然存在的Bug,也许这就是包统一管理的缺陷,如果至今你还没碰过这种情况,说明你的项目比较稳定或者emmmm...你平时自己不捣鼓学习一些东西。


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

收起阅读 »

Swift算法俱乐部:Swift队列数据结构(Queue)

准备开始队列(Queue)是一个列表,您只能在后面插入新项目并从前面删除项目。 这可确保入队的第一个元素也是首先出队的元素。 先到先出在许多算法中,我们希望在某个时间点将项目添加到临时列表中,然后在以后再次将它们从列表中拉出。 添加和删除这些项目的顺序非常重要...
继续阅读 »

准备开始

队列(Queue)是一个列表,您只能在后面插入新项目并从前面删除项目。 这可确保入队的第一个元素也是首先出队的元素。 先到先出
在许多算法中,我们希望在某个时间点将项目添加到临时列表中,然后在以后再次将它们从列表中拉出。 添加和删除这些项目的顺序非常重要。

队列提供先进先出或先入先出的顺序。 首先插入的元素也是第一个出来的元素(和堆栈(Stack)非常类似,是LIFO或后进先出。)

这是一个栗子
理解队列的最简单方法是看看它是如何使用的。

想象一下你有一个队列。 以下是你如何入选一个数字:

queue.enqueue(10)

队列现在是[10]。 然后,继续将下一个号码添加到队列中:

queue.enqueue(3)

队列现在是[10,3]。 继续添加:

queue.enqueue(57)

队列现在是[10,3,57]。 我们可以将队列中的第一个元素从队列中拉出:

queue.dequeue()

将返回10,因为这是插入的第一个数字。 队列现在将是[3,57]。 每个项目都向上移动一个地方。

queue.dequeue()

这将返回3.下一个出列将返回57,依此类推。 如果队列为空,则出队将返回零。

实现队列
在本节中,将实现一个存储Int值的简单通用队列。
创建一个新的playground,添加如下代码:

public struct Queue {

}

playground还包含LinkedList的代码(可以通过转到查看 Project Navigators Show Project Navigator并打开Sources LinkedList来看到这一点。

入队(Enqueue)

队列需要入队方法。 我们使用项目中包含的LinkedList实现来实现队列。 在花括号之间添加以下内容:

// 1
fileprivate var list = LinkedList<Int>()

// 2
public mutating func enqueue(_ element: Int) {
list.append(element)
}
  1. 添加了一个fileprivate LinkedList变量,用于将这些项目存储在队列中。
  2. 已经添加了一个方法来排列项目。 这个方法会改变底层的LinkedList,所以明确地指定了在方法前加上mutating关键字。

出列(Dequeue)

队列也需要一个出队方法。

// 1
public mutating func dequeque() -> Int? {
// 2
guard !list.isEmpty, let element = list.first else { return nil}

list.remove(element)

return element.value
}
  1. 添加一个返回队列中第一个项目的出队方法。 返回类型可以为空来处理队列为空。
  2. 使用guard语句处理队列为空。 如果这个队列是空的,那么guard将会进入else块。

查看(Peek)

队列还需要一个peek方法,它在队列的开始处返回该项目而不删除它。

public func peek() -> Int? {
return list.first?.value
}

IsEmpty

队列可以是空的。 添加一个isEmpty属性,该属性将返回基于LinkedList的值:

public var isEmpty: Bool {
return list.isEmpty
}

打印队列

让我们试试新队列。 在队列实现下面,将以下内容写入playground中:

var queue = Queue()
queue.enqueue(10)
queue.enqueue(3)
queue.enqueue(57)

定义队列后,尝试将队列打印到控制台:

print(queue)

输出如下:

Queue(list: [10, 3, 57])

这输出的样式不是很好。 要显示更可读的输出字符串,可以使队列采用CustomStringConvertable协议。 为此,请在Queue类的实现下方添加以下内容:

// 1
extension Queue: CustomStringConvertible {
// 2
public var description: String {
// 3
return list.description
}
}
  1. 声明Queue类的扩展,让它遵循CustomStringConvertible协议。 该协议期望使用字符串类型实现带名称描述的计算属性。
  2. 声明了description属性。 这是一个计算属性,它是一个返回String的只读属性。
  3. 返回基于LinkedList的描述。

现在控制台的输出编程如下样式:

[10, 3, 57]

Swift通用队列实现
此时,我们已经实现了一个存储Int值的通用队列,并提供了在Queue类中查看,排队和出列项目的功能。
在本节中,我们使用泛型从队列中抽象出类型需求。

将Queue类的实现更新为以下内容:

// 1
public struct Queue<T> {
// 2
fileprivate var list = LinkedList<T>()

// 3
public mutating func enqueue(_ element: T) {
list.append(element)
}

// 4
public mutating func dequeque() -> T? {

guard !list.isEmpty, let element = list.first else { return nil}

list.remove(element)

return element.value
}
// 5
public func peek() -> T? {
return list.first?.value
}

public var isEmpty: Bool {
return list.isEmpty
}
}

修正测试代码如下:

var queue = Queue<Int>()
queue.enqueue(10)
queue.enqueue(3)
queue.enqueue(57)
print(queue)

还可以尝试使用不同类型的Queue:

var queue2 = Queue<String>()
queue2.enqueue("mad")
queue2.enqueue("lad")
if let first = queue2.dequeque() {
print(first)
}
print(queue2)


收起阅读 »

iOS 类方法load和initialize的区别

Objective-C作为一门面向对象语言,有类和对象的概念。编译后,类相关的数据结构会保留在目标文件中,在运行时得到解析和使用。在应用程序运行起来的时候,类的信息会有加载和初始化过程。就像Application有生命周期回调方法一样,在Objective-C...
继续阅读 »

Objective-C作为一门面向对象语言,有类和对象的概念。编译后,类相关的数据结构会保留在目标文件中,在运行时得到解析和使用。在应用程序运行起来的时候,类的信息会有加载和初始化过程。
就像Application有生命周期回调方法一样,在Objective-C的类被加载和初始化的时候,也可以收到方法回调,可以在适当的情况下做一些定制处理。而这正是load和initialize方法可以帮我们做到的。

  • (void)load;
  • (void)initialize;

可以看到这两个方法都是以“+”开头的类方法,返回为空。通常情况下,我们在开发过程中可能不必关注这两个方法。如果有需要定制,我们可以在自定义的NSObject子类中给出这两个方法的实现,这样在类的加载和初始化过程中,自定义的方法可以得到调用。
+load

顾名思义,+load方法在这个文件被程序装载时调用。只要是在Compile Sources中出现的文件总是会被装载,这与这个类是否被用到无关,因此+load方法总是在main函数之前调用。
调用方式:
会循环调用所有类的 +load 方法。注意,这里是(调用分类的 +load 方法也是如此)直接使用函数内存地址的方式 (*load_method)(cls, SEL_load); 对 +load 方法进行调用的,而不是使用发送消息 objc_msgSend 的方式。
这样的调用方式就使得 +load 方法拥有了一个非常有趣的特性,那就是子类、父类和分类中的 +load 方法的实现是被区别对待的。也就是说如果子类没有实现 +load 方法,那么当它被加载时 runtime 是不会去调用父类的 +load 方法的。同理,当一个类和它的分类都实现了 +load 方法时,两个方法都会被调用。
要点:

  • 调用时机比较早,运行环境有不确定因素。具体说来,在iOS上通常就是App启动时进行加载,但当load调用的时候,并不能保证所有类都加载完成且可用,必要时还要自己负责做auto release处理。

补充上面一点,对于有依赖关系的两个库中,被依赖的类的+load会优先调用。但在一个库之内,父、子类、类别之间调用有顺序,不同类之间调用顺序是不确定的。

  • 关于继承:对于一个类而言,没有+load方法实现就不会调用,不会考虑对NSObject的继承,就是不会沿用父类的+load。
  • 父类和本类的调用:父类的方法优先于子类的方法。一个类的+load方法不用写明[super load],父类就会收到调用。
  • 本类和Category的调用:本类的方法优先于类别(Category)中的方法。Category的+load也会收到调用,但顺序上在本类的+load调用之后。
  • 不会直接触发initialize的调用。

+initialize

+initialize 方法是在类或它的子类收到第一条消息之前被调用的,这里所指的消息包括实例方法和类方法的调用,并且只会调用一次。initialize方法实际上是一种惰性调用,也就是说如果一个类一直没被用到,那它的initialize方法也不会被调用,这一点有利于节约资源。
调用方式:
runtime 使用了发送消息 objc_msgSend 的方式对 +initialize 方法进行调用。也就是说 +initialize 方法的调用与普通方法的调用是一样的,走的都是发送消息的流程。换言之,如果子类没有实现 +initialize 方法,那么继承自父类的实现会被调用;如果一个类的分类实现了 +initialize 方法,那么就会对这个类中的实现造成覆盖。
要点:

  • initialize的自然调用是在第一次主动使用当前类的时候。
  • 在initialize方法收到调用时,运行环境基本健全。
  • 关于继承:和load不同,即使子类不实现initialize方法,会把父类的实现继承过来调用一遍,就是会沿用父类的+initialize。(沿用父类的方法中,self还是指子类)
  • 父类和本类的调用:子类的+initialize将要调用时会激发父类调用的+initialize方法,所以也不需要在子类写明[super initialize]。(本着除主动调用外,只会调用一次的原则,如果父类的+initialize方法调用过了,则不会再调用)
  • 本类和Category的调用:Category中的+initialize方法会覆盖本类的方法,只执行一个Category的+initialize方法。

类别(Category)

对于+initialize,只有最后一个类别执行,本类的+initialize和前面类别的+initialize被隐藏。
而对于+load,本类和本类的所有类别都执行,并且如果Apple的文档中介绍顺序一样:先执行类自身的实现,再执行类别中的实现。
扩展

因为两个方法只会被系统调用一次(除主动调用外),并且是线程安全的,可以用来作为单例的实现。(可以用+initialize,+load有些隐患,看这里)
�注意

  • 在使用时都不要过重地依赖于这两个方法,除非真正必要。
  • 谨慎在分类中实现+initialize方法,因为如果在分类中实现了,本类实现的+initialize方法将不会被调用。
  • 谨慎在分类中实现+load方法。因为如果在本类中实现+load方法混淆A、B两个方法,分类中也混淆A、B,因为本类和分类的+load都实现了,所以都会调用,A、B在本类中置换后,又在分类中置换了回来。
  • load方法通常用来进行Method Swizzle,initialize方法一般用于初始化全局变量或静态变量。
  • load和initialize方法内部使用了锁,因此它们是线程安全的。实现时要尽可能保持简单,避免阻塞线程,不要再使用锁。

问题

问题:

  1. 子类、父类、分类中的相应方法什么时候会被调用?
  2. 需不需要在子类的实现中显式地调用父类的实现?

解答:

  1. super的方法会成功调用,但是这是多余的,因为runtime会自动对父类的+load方法进行调用,而+initialize则会随子类自动激发父类的方法(如Apple文档中所言)不需要显示调用。另一方面,如果父类中的方法用到的self(像示例中的方法),其指代的依然是类自身,而不是父类。

总结

收起阅读 »

如何将react-native的style样式转换成css样式

背景: 我们总是倾向于一套代码走天下,正所谓一招鲜,吃遍天。刚接触RN项目的时候,常常为RN style样式的写法而头痛,等到熟悉了RN样式写法时,一个web端项目从天而降,于是,你又不得不操练起日渐陌生的css写法。更过分的是,有时你还得在RN样式和css样...
继续阅读 »

背景: 我们总是倾向于一套代码走天下,正所谓一招鲜,吃遍天。刚接触RN项目的时候,常常为RN style样式的写法而头痛,等到熟悉了RN样式写法时,一个web端项目从天而降,于是,你又不得不操练起日渐陌生的css写法。更过分的是,有时你还得在RN样式和css样式之间来回切换,时刻处于水深火热之中。抬首间,不禁叹息一声:人间不值得。


一、准备工作


本文中详细讲解sass样式的转换,其它诸如less、css、PostCss的转换请参考:(https://github.com/kristerkari/react-native-css-modules)
这里面有较为详细说明。


我们需要准备四个依赖:

react-native-sass-transformer 将 Sass 转换为与 React Native 兼容的样式对象并处理实时重新加载

babel-plugin-react-native-platform-specific-extensions 如果磁盘上存在特定于平台的文件,则将 ES6 导入语句转换为特定于平台的 require 语句

babel-plugin-react-native-classname-to-style 将 className 属性转换为 style 属性

node-sass


二、 创建一个React-Native APP


参考官方文档创建即可。


三、安装依赖


yarn add babel-plugin-react-native-classname-to-style babel-plugin-react-native-platform-specific-extensions react-native-sass-transformer node-sass --dev

四、设置babel配置


对于React Native v0.57 或者更新版本


.babelrc (or babel.config.js)


{
"presets": ["module:metro-react-native-babel-preset"],
"plugins": [
"react-native-classname-to-style",
[
"react-native-platform-specific-extensions",
{
"extensions": ["scss", "sass"]
}
]
]
}

对于React Native v0.57以下版本


{
"presets": ["react-native"],
"plugins": [
"react-native-classname-to-style",
[
"react-native-platform-specific-extensions",
{
"extensions": ["scss", "sass"]
}
]
]
}

五、设置Metro配置


在项目根目录下新增一个metro.config.js的文件


const { getDefaultConfig } = require("metro-config");

module.exports = (async () => {
const {
resolver: { sourceExts }
} = await getDefaultConfig();
return {
transformer: {
babelTransformerPath: require.resolve("react-native-sass-transformer")
},
resolver: {
sourceExts: [...sourceExts, "scss", "sass"]
}
};
})()
;


对于React Native v0.57以下版本,在根目录下新增rn-cli.config.js文件


module.exports = {
getTransformModulePath() {
return require.resolve("react-native-sass-transformer");
},
getSourceExts() {
return ["js", "jsx", "scss", "sass"];
}
};

六、接下来你就可以愉快的使用sass来写样式


style.scss


.container {
flex: 1;
justify-content: center;
align-items: center;
background-color: #f5fcff;
}

.blue {
color: blue;
font-size: 30px;
}


你既可以使用className来写样式,也可以使用style


import React, { Component } from "react";
import { Text, View } from "react-native";
import styles from "./styles.scss";

const BlueText = () => {
return Blue Text;
};

export default class App extends Component<{}> {
render() {
return (



);
}
}

七、为sass配置TypeScript


在ts项目中,为sass配置类型提示很有必要。首先我们需要把在第三步第五步中把react-native-sass-transformer依赖替换成react-native-typed-sass-transformer


为了让className 属性正常工作,我们还需要安装下面的依赖包:


对于React Native v0.57 或者更新版本


yarn add typescript --dev

老版本:


yarn add react-native-typescript-transformer typescript --dev

在package.json中添加下面依赖,然后运行yarn命令


"@types/react-native": "^0.57.55",

如果版本versions >=0.52.4


"@types/react-native": "kristerkari/react-native-types-for-css-modules#v0.57.55",

你也可以删掉版本号,但是不建议这样做


"@types/react-native": "kristerkari/react-native-types-for-css-modules",

如果你使用的rn版本>=0.57,这样就OK了,如果不是,请参照文档:github.com/kristerkari…


八、原生提供的属性和方法如何添加到scss文件中,如何做不同机型的适配?


我们需要自定义一个transform用于sass文件的转换。


metro.config.js文件中,修改如下:


const { getDefaultConfig } = require("metro-config");

module.exports = (async () => {
const {
resolver: { sourceExts }
} = await getDefaultConfig();
return {
transformer: {
babelTransformerPath: require.resolve("./transformer.js")
},
resolver: {
sourceExts: [...sourceExts, "scss", "sass"]
}
};
})()
;


metro.config.js


const upstreamTransformer = require("metro-react-native-babel-transformer");
const sassTransformer = require("react-native-typed-sass-transformer");
const DtsCreator = require("typed-css-modules");
const css2rn = require("css-to-react-native-transform").default;

const creator = new DtsCreator();

/** 引入原生的属性和方法 */
const preImport = `
import { PixelRatio, Dimensions, StatusBar, Platform } from 'react-native';
let DEVICE_WIDTH = Dimensions.get('window').width;
let DEVICE_HEIGHT = Dimensions.get('window').height;
let S=(designPx) => {
return PixelRatio.roundToNearestPixel((designPx / 750) * DEVICE_WIDTH);
}
`

function renderCSSToReactNative(css) {
return css2rn(css, { parseMediaQueries: true });
}

/** px转换成pt,做一个标记 */
function pxToPtForMark(code){
let newCode=code;
try {
newCode=code.replace(/([0-9]+)px/g,(...arg)=>{
const px=Number(arg[1]);
return `${px}pt`;
})
} catch (error) {
throw Error('样式解析错误');
}
return newCode;
}

/** px 或者 pt单位的适配 需要注意正负值 */
function unitAdaption(code){
let newCode=code;
try {
newCode=code.replace(/"([-+]{0,1})([0-9]+)pt"/g,(...arg)=>{
const px=arg[1]+arg[2];
return `S(${px})`;
})
} catch (error) {
throw Error('样式解析错误');
}
return newCode;
}

/** vh和vw的适配 */
function vhAndVwAdaption(code){
let newCode=code;
try {
newCode=code.replace(/"([0-9]+)vw"/g,(...arg)=>{
const vw=Number(arg[1]);
return `${vw/100} * DEVICE_WIDTH`;
}).replace(/"([0-9]+)vh"/g,(...arg)=>{
const vh=Number(arg[1]);
return `${vh/100} * DEVICE_HEIGHT`;
});

} catch (error) {
throw Error('样式解析错误');
}
return newCode;
}

function isPlatformSpecific(filename) {
var platformSpecific = [".native.", ".ios.", ".android."];
return platformSpecific.some(name => filename.includes(name));
}

module.exports ={
transform:async function({ src, filename, options }) {
if (filename.endsWith(".scss") || filename.endsWith(".sass")) {

let newSrc=pxToPtForMark(src);

let css =await sassTransformer.renderToCSS({ src:newSrc, filename, options });
let cssObject = renderCSSToReactNative(css);
let cssObjectStr=JSON.stringify(cssObject);

cssObjectStr=unitAdaption(cssObjectStr);

cssObjectStr=vhAndVwAdaption(cssObjectStr);

//特殊文件直接return
if (isPlatformSpecific(filename)) {
return upstreamTransformer.transform({
src: preImport+";module.exports = " + cssObjectStr,
filename,
options
});
}

//一般文件创建types文件之后再return
return creator.create(filename, css).then(content => {
return content.writeFile().then(() => {
return upstreamTransformer.transform({
src: preImport+";module.exports = " + cssObjectStr,
filename,
options
});
});
});
} else {
return upstreamTransformer.transform({ src, filename, options });
}
}
}

在scss文件中,px单位转换成style对象时,会自动去掉,如下:


.unpaidRemind {
position: absolute;
bottom: 56px;
right: 28px;
background-color: #999;
padding: 20px;
border-radius: 16px;
}
.unpaidRemindText {
color: rgba(255, 255, 255, 0.9);
font-size: 28px;
}

转换之后变成


{
unpaidRemind: {
position: 'absolute',
bottom: 56,
right: 28,
backgroundColor: '#999',
padding: 20,
borderRadius: 16,
},
unpaidRemindText: {
color: 'rgba(255,255,255,0.9)',
fontSize: 28,
},
}

我们的目标是在在转后之后把所有px都换成我们的适配方法:


{
unpaidRemind: {
position: 'absolute',
bottom: S(55),
right: S(28),
backgroundColor: '#999',
padding: S(20),
borderRadius: S(16),
},
unpaidRemindText: {
color: 'rgba(255,255,255,0.9)',
fontSize: S(28),
},
}

最终拿到的代码类似于这样,它是可以直接执行的,同样的道理,我们可以注入更多的RN属性到我们的文件中,这取决于我们是否需要这些属性。


import { PixelRatio, Dimensions, StatusBar, Platform } from 'react-native'; 
let DEVICE_WIDTH = Dimensions.get('window').width;
let DEVICE_HEIGHT = Dimensions.get('window').height;
let S=(designPx) => { return PixelRatio.roundToNearestPixel((designPx / 750) * DEVICE_WIDTH); }
module.exports ={
unpaidRemind: {
position: 'absolute',
bottom: S(55),
right: S(28),
backgroundColor: '#999',
padding: S(20),
borderRadius: S(16),
},
unpaidRemindText: {
color: 'rgba(255,255,255,0.9)',
fontSize: S(28),
},
}

pxToPtForMark方法将px转换成pt,这一步主要是方便我们后续把pt转成S(28)这种形式,unitAdaption方法就是实现这一功能。为什么不是直接把px转成S(28)这种形式?renderCSSToReactNative会把px转没掉,我们无法区分flex:1这种属性和fontSize:28的区别,但是它不会吧pt转没,而是变成fontSize:"28pt".


为了使vhvw这两个单位能够生效,我们使用vhAndVwAdaption方法做了处理,width:100vw最后会变成width:100/100 * DEVICE_WIDTH,其中DEVICE_WIDTH就是我们前面注入的设备宽度这个变量。


九、referenceError:'xx' is not defined 报错


const Button=(props)=>{ 
const {style}=props;
return
}
const Page=()=>{
return
}

//以上写法会导致报referenceError:'xx' is not defined,而且是非必现,偶尔会报
//要这样写
const Button=(props)=>{
const {style}=props;
return
}
const Page=()=>{
return
}

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

收起阅读 »

React Native JSI:将BridgeModule转换为JSIModule

我们原有的项目中有大量的使用OC或者Java编写的原生模块,其中的一些可以使用C++重写,但大多数模块使用了平台特有的API和SDK,他们没有对应的C++实现。 在本文中,将带领大家如何将原有的模块转化为JSI模块。本文不再讲解基础概念,如果你有不明白的地方请...
继续阅读 »

我们原有的项目中有大量的使用OC或者Java编写的原生模块,其中的一些可以使用C++重写,但大多数模块使用了平台特有的API和SDK,他们没有对应的C++实现。


在本文中,将带领大家如何将原有的模块转化为JSI模块。本文不再讲解基础概念,如果你有不明白的地方请参考上一篇文章


使用JSI实现js与原生交互

上图描述了两端是如何进行交互的,这里面没有了React Native 的 Bridge,而是使用了C++作为中介。



  1. 在iOS端可以很简单的实现,因为OC和C++可以混编。

  2. 在Android端要麻烦一些,需要通过JNI进行C++ 与 Java的交互。


iOS端实现


首先我们在SimpleJsi.mm 中增加 getModelsetItemgetItem 用以模拟原生模块。这些方法都使用到了平台特有的API。


- (NSString *)getModel {

struct utsname systemInfo;

uname(&systemInfo);

return [NSString stringWithCString:systemInfo.machine
encoding:NSUTF8StringEncoding];
}

- (void)setItem:(NSString *)key :(NSString *)value {

NSUserDefaults *standardUserDefaults = [NSUserDefaults standardUserDefaults];

[standardUserDefaults setObject:value forKey:key];

[standardUserDefaults synchronize];
}

- (NSString *)getItem:(NSString *)key {

NSUserDefaults *standardUserDefaults = [NSUserDefaults standardUserDefaults];

return [standardUserDefaults stringForKey:key];
}


接下来我们需要实现一个新的install方法:


static void install(facebook::jsi::Runtime &jsiRuntime, SimpleJsi *simpleJsi) {

auto getDeviceName = Function::createFromHostFunction(
jsiRuntime, PropNameID::forAscii(jsiRuntime, "getDeviceName"), 0,
[simpleJsi](Runtime &runtime, const Value &thisValue,
const Value *arguments, size_t count) -> Value {

facebook::jsi::String deviceName =
convertNSStringToJSIString(runtime, [simpleJsi getModel]);

return Value(runtime, deviceName);
});
jsiRuntime.global().setProperty(jsiRuntime, "getDeviceName", move(getDeviceName));
}

这个方法接收两个参数。其中SimpleJsi 用来调用 getModel 方法。这个方法的返回值是NSString。我们需要将其转化为JSI认识的String类型。这里我们使用了convertNSStringToJSIString 方法。这个放开来自开源代码YeetJSIUtils


然后,我们在修改RN端,修改APP.js


const press = () => {
// setResult(global.multiply(2, 2));
// global.multiplyWithCallback(4, 5, alertResult);
alert(global.getDeviceName());
};

执行结果。


执行结果

同理,我们适配一下其他两个方法。


关键的地方还是参数的获取与转换。


auto setItem = Function::createFromHostFunction(
jsiRuntime, PropNameID::forAscii(jsiRuntime, "setItem"), 2,
[simpleJsi](Runtime &runtime, const Value &thisValue,
const Value *arguments, size_t count) -> Value {
NSString *key =
convertJSIStringToNSString(runtime, arguments[0].getString(runtime));
NSString *value =
convertJSIStringToNSString(runtime, arguments[1].getString(runtime));

[simpleJsi setItem:key :value];

return Value(true);
});
jsiRuntime.global().setProperty(jsiRuntime, "setItem", move(setItem));


auto getItem = Function::createFromHostFunction(
jsiRuntime, PropNameID::forAscii(jsiRuntime, "getItem"), 0,
[simpleJsi](Runtime &runtime, const Value &thisValue,
const Value *arguments, size_t count) -> Value {

NSString *key =
convertJSIStringToNSString(runtime, arguments[0].getString(runtime));
facebook::jsi::String value =
convertNSStringToJSIString(runtime, [simpleJsi getItem:key]);

return Value(runtime, value);
});
jsiRuntime.global().setProperty(jsiRuntime, "getItem", move(getItem));

修改App.js


const press = () => {
global.setItem('RiverLi', '大前端');
setTimeout(() => {
alert(global.getItem('RiverLi'));
}, 300);
};

执行结果


image-20210816113702360

总结


使用JSI进行moudle开发虽然看着有些复杂,但还是值得我们花时间去研究的。因为它的性能是最佳的,没有不必要的转换,所有的操作都是那么直接的发生在一层上。





作者:RiverLi
链接:https://juejin.cn/post/6999799689155444773

收起阅读 »

聊聊 RN 中 Android 提供 View 的那些坑

最近笔者研究 Android 中使用自定义 View 提供原生组件给 React Native(下面统一写成 RN ) 端的时候,遇到一些实际问题,在这里从 RN 的一些...
继续阅读 »


最近笔者研究 Android 中使用自定义 View 提供原生组件给 React Native(下面统一写成 RN ) 端的时候,遇到一些实际问题,在这里从 RN 的一些工作机制入手,分享一下问题的原因和解决方案。

自定义 View 内容不生效

原因

在给 RN 提供自定义 View 的时候发现自定义 View 内部很多 UI 逻辑没有生效。
例如下图,根据逻辑隐藏/展示了一些控件,但是应显示控件的位置没有变化。被隐藏控件的位置还是空出来的。很明显整个自定义 View 的 requestLayout 没有执行。


问题的答案就在 RN 根布局 ReactRootView 的 measure 方法里面。


在这个View的测量过程中,会判断 measureSpec 是否有更新。


当 measureSpec 有变化,或者宽高有变化的时候,才会触发 updateRootLayoutSpecs 的逻辑。
继续看下 updateRootLayoutSpecs 里做了一些什么事情,跟着源码最后会执行到 UIImplementation 的 dispatchViewUpdates 方法:


最终执行:


这里会从根节点往下一直更新子 View ,执行 View的 measure 和 layout
所以 ReactRootView 在宽高和测量模式都没有变化的情况下,就相当于把子 View 发出的 requestLayout 请求都拦截了。

解决方案

知道了原因就非常好解决了,既然你不让我通知我的根控件需要重新布局,那我就自己给自己重新布局好了。参考了 RN 一些自带的自定义 View 的实现,我们可以在这个自定义 View 重新布局的时候,注册一个 FrameCallback 去执行自己的 measure 和 layout 方法。

RN 自定义View 必须在JS端设置宽高

实现了自定义 View 之后,在 JSX 里面指定标签之后,会发现这个原生组件并没有显示。通过 IDE 的 Layout Inspect 可以发现此时这个自定义 View 的 width 和 height 都是 0 。如果设置了 width 和height 的话就可以展示了。
这时候就很奇怪了, 为什么我的自定义 View 里面的内容明明是 WRAP_CONTENT 的,很多自定义 View 又是直接继承的 ConstraintLayout 、 RelativeLayout 这种 Android 的 ViewGroup ,但还是要指定宽高才能在 RN 中渲染出来呢?
要解决这个疑惑,就需要了解一下 RN 的渲染流程。

RN 是怎么确定 Native View的宽高的

我们顺着 RN 更新 View 结构的 UIImplementation#updateViewHierarchy 方法,发现有两处关键的逻辑:


calculateRootLayout 中调用了 cssRoot 的布局计算逻辑:


接下来就是 applyUpdatesRecursive,顾名思义就是递归的更新根节点的所有子节点,在我们的场景中即整个页面的布局。


需要更新的节点则调用了 dispatchUpdates 方法,执行 enqueueUpdateLayout, 调用 NativeViewHierarchyManager#updateLayout 逻辑。


updateLayout 的核心流程如下:

  • 调用 resolveView 方法获取到真实的控件对象。
  • 调用这个控件的 measure 方法。


  • 调用updateLayout,执行这个控件的 layout方法



发现了没有?这里的 widthheight 已经是固定的值分别传给了 meausre 和 layout, 也就是说,这些 View 的宽高根本不是 Android 的绘制流程决定的,那么这个 width 和 height 的值是从哪里来的呢?
回头看看就发现了答案:


宽高是 lefttoprightbottom坐标相减得到的,而这些坐标则是通过
getLayoutWidth 和 getLayoutHeight 得到的:


而这个 layoutWidth 和 layoutHeight,则都是 Yoga 帮我们计算好,存放在 YogoNode里面的。
关于 Yoga

Yoga 是 Facebook 实现的一个高性能、易用、 Flex 的跨端布局引擎。
React Native 内部则是使用 Yoga 来布局的。
具体内容可以看 Yoga 的官网:https://yogalayout.com/

这里也就解释了为什么自定义 View 需要在 jsx 中指定了 width 和 height 才会渲染出来。因为这些自定义 View 原本在 Android系统的 measure layout 流程都已经被 RN 给控制住了。
这里可以总结成一句话:
RN 中最终渲染出来的控件的宽高,都由 Yoga 引擎来计算决定,系统自身的布局流程无法直接决定这些控件的宽高
但是这时候还是有一个疑问,为什么RN自己的一些组件,例如  ,没有指定
宽高也可以正常自适应显示呢?

为什么 RN 自己的 Text 是有自己的宽高的

我们来看一下RN是怎么定义渲染出来的 TextView 的,找到对应的 TextView 的 ViewManager,
com.facebook.react.views.text.ReactTextViewManager
我们关注两个方法:

  1. createViewInstance


  1. createShadowNodeInstance



其中,ReactTextView 其实就是实现了一个普通的 Android TextViewReactTextShadowNode 则表示了这个 TextView 对应的 YogaNode 的实现。


在它的实现中,我们可以看到一个成员变量,从名字上看是负责这个 YogaNode 的 measure 工作。


YogaNodeJNIBase 会调用这个JNI的方法,给JNI的逻辑注册这样一个回调函数。


这个 YogaMeasureFunction 的具体实现:


这里截个图,可以看到这里调用了 Android 中 Text 绘制的 API 来确定的文本的宽高。函数返回的是


这里是使用了 YogaMeasureOutput.make 把 Layout 算出来的宽高转成一定格式的二进制回调给 Yoga 引擎,这也是为什么 RN 自己的 Text 标签是可以自适应宽高展示的。
这里我们也可以得到一个结论:如果 Android 端封装的自定义 View 可以是确定宽高或者内部的控件是非常固定可以通过 measure 和 layout 就能算出宽高的,我们可以通过注册 measureFunction 回调的方式告诉 Yoga 我们 View 的宽高。
但是在实际业务中,我们很多业务组件是封装在 ConstraintLayout 、RelativeLayout 等 ViewGroup 中,所以我们还需要其他的方法来解决组件宽高设置的问题。

解决方案

那么这个问题可以重写 View 的 onMeasure 和 layout 方法来解决吗?看起来是这个做法是可以解决 View 宽高为 0 渲染不出来的问题。但是如果 jsx 这样描述布局的时候:


这时候 AndroidView 和 Text 会同时显示,并且 AndroidView 被 Text 遮住。
稍微思考一下就能得到原因:对于 Yoga 引擎来说,AndroidView 所代表的的节点仍然是没有宽高的,YogaNode 里面的 widthheight 仍然是 0,那么当重写 onMeasure 和 onLayout 的逻辑生效后,View 显示的左上方顶点是 (0,0) 的坐标。
而 Yoga 引擎自己计算出 Text 的宽高后, Text 的左上方顶点坐标肯定也是 (0,0) ,所以这时候2个 View 会显示在同一个位置(重叠或者覆盖)。
所以这时候问题就变成了,我们想通过 Android 自己的布局流程来确定并刷新这个自定义控件,但是 Yoga 引擎并不知道。
所以想要解决这个问题,可行的有两条路:

  • 改变 UI 层级和自定义 View 的粒度
  • Native 测量出实际需要的宽高后同步给Yoga 引擎
增加自定义控件的粒度

举一个自定义控件的例子:


我们希望把这个图上第一行的控件拆分成粒度较低的自定义 View 交给 RN 来布局实现布局动态配置的能力。但是这类场景的左右两边控件都是自适应宽度。这时候在 JS 端其实没有办法提供一个合适的宽度。考虑到更多场景下同一个方向轴上的自适应宽度控件是有位置上的依赖性的,所以可以不拆分这两个部分,直接都定义在同一个自定义 View 内:


提供给 JS 端使用,没有宽高的话,就把整个 SingHeaderView 的宽度设置成


这时候内部的两个控件会自己去进行布局。最终展示出来的就是左右都是 Wrap_Content 的。

Native 测量出实际需要的宽高后同步给Yoga引擎

但是控制自定义 View 的粒度的方式总归是不够灵活,开发的时候也往往会让人犹豫是否拆分。接着之前的内容,既然这个问题的矛盾点在于 Yoga 不知道 Android 可以自己再次调用 measure 来确定宽高,那如果能把最新的宽高传给 Yoga,不就可以解决我们的问题吗?
具体怎么触发 YogaNode 的刷新呢?通过阅读源码可以找到解决方法。在 UIManage里面,有一个叫做 updateNodeSize 的 api:


这个 api 会更新 View 对应的 cssNode 的大小,然后分发刷新 View 的逻辑。这个逻辑是需要保证在后台消息队列里面执行的,所以需要把这个刷新的消息发送到 nativeModulesQueueThread 里面去执行。
我们在 ViewManager 里面保存这个 Manager 对应的 View 和 ReactNodeImpl 实例。例如 Android 端封装了一个 LinearLayout , 对应的 node 是 MyLinearLayoutNode


重写自定义 View 的 onMeasure, 让自己是 wrap_content 的布局:


在 requestLayout 中根据自己真实的宽高布局并触发以下逻辑:




不过上面这个方案虽然可以解决 View 的 wrap_content 显示的问题,但是存在一些缺点:
刷新 YogaNode 实际是在 requestLayout 的时候触发的,这就相当于 requestLayout 这种比较耗费性能的操作会双倍的执行。对于一些可能会频繁触发 requestLayout 的业务场景来说需要慎重考虑。如果遇到这种场景,还是需要根据自己的需求来灵活选择解决方式。

收起阅读 »

ReactNative在游戏营销场景中的实践和探索-新架构介绍

客户端跨端框架已经发展了很多年了,最近比较流行的小程序、Flutter、ReactNative,都算是比较成功、成熟的框架,面向的开发者也不一样,很多大型App都广泛的使用了,笔者有幸很早就参与学习使用了这些优秀的跨端方案,在这几年的开发和架构设计中,除了在A...
继续阅读 »

客户端跨端框架已经发展了很多年了,最近比较流行的小程序、Flutter、ReactNative,都算是比较成功、成熟的框架,面向的开发者也不一样,很多大型App都广泛的使用了,笔者有幸很早就参与学习使用了这些优秀的跨端方案,在这几年的开发和架构设计中,除了在App中支撑了千万级DAU,也慢慢将ReactNative跨端方案运用到了游戏,来提升开发、迭代效率。本次文章我们会分5个章节介绍我们在游戏中的一些探索和实践,相信大家也能从中有所收获:

前面章节介绍了我们使用ReactNative在游戏中的一些实践,通过不断的迭代,我们完成了游戏平台的搭建,整体性能和稳定性已经达到了最优,算得上是一个比较成熟的平台了,当然该平台同样适用于现在的客户端开发,集成成本很低。但是框架本身的设计缺陷还是没有办法解决,在复杂的交互性很强的UI场景中,渲染瓶颈很明显,在游戏中也能深刻的体验到。


相信大家也看过我的另外一篇关于ReactNative架构重构的文章《庖丁解牛!深入剖析React Native下一代架构重构》,Facebook 在 2018 年 6 月官方宣布了大规模重构 React Native 的计划及重构路线图。目的是为了让 ReactNative 更加轻量化、更适应混合开发,接近甚至达到原生的体验。文章写的时间比较久了,笔者一直忙于其他事情,对于新进展更新较少,而且最初也只是初步分析了下Facebook的设计想法,经过这么久的迭代新架构有了很多进展,或者说无限接近正式release了,很值得和大家分享分享,这篇文章会向大家更深层次介绍新架构的现状和开发流程。


下面我们会从原理上简单介绍新架构带来的一些变化,下图是新老架构的变化对比:



相信大家也能从中发现一些区别,原有架构JS层与Native的通讯都过多的依赖bridge,而且是异步通讯,导致一些通讯频率较高的交互和设计就很难实现,同时也影响了渲染性能,而新架构正是从这点,对bridge这层做了大量的改造,使得UI和API调用,从原有异步方式,调整到可以同步或者异步与Native通讯,解决了需要频繁通讯的瓶颈问题。




  1. 旧架构设计




在了解新架构前,我们还是先聊下目前的ReactNative框架的主要工作原理,这样也方便大家了解整体架构设计,以及为什么facebook要重构整个框架:



  • ReactNative是采用前端的方式及UI渲染了原生的组件,他同时提供了API和UI组件,也方便开发者自己设计、扩展自己的API,提供了ReactContextBaseJavaModule、ViewGroupManager,其中ReactNative的UI是通过UIManger来管理的,其实在Android端就是UIManagerModule,原理上也是一个BaseJavaModule,和API共享一个native module。

  • ReactNative页面所有的API和UI组件都是通过ReactPackageManger来管理的,引擎初始化instanceManager过程中会读取注入的package,并根据名称生成对应的NativeModule和Views,这里还仅仅是Java层的,实际在C++层会对应生成JNativeModule

  • 切换到以上架构图的部分来看,Native Module的作用就是打通了前端到原生端的API调用,前端代码运行在JSC的环境中,采用C++实现,为了打通到native调用,需要在运行前注入到global环境中,前端通过global对象来操作proxy Native Module,继而执行了JNativeModule





  • 前端代码render生成UI diff树后,通过ReactNativeRenderer来完成对原生端的UIManager的调用,以下是具体的API,主要作用是通知原生端创建、更新View、批量管理组件、measure高度、宽度等:




  • 通过上述一系列的API操作后,会在原生端生成shadow tree,用来管理各个node的关系,这点和前端是一一对应的,然后待整体UI刷新后,更新这些UI组件到ReactRootView


通过上面的分析,不难发现现在的架构是强依赖nativemodule,也就是大家通常说的bridge,对于简单的Native API调用来说性能还能接受,而对于UI来说,每次的操作都是需要通过bridge的,包括高度计算、更新等,且bridge限制了调用频率、只允许异步操作,导致一些前端的更新很难及时反应到UI上,特别是类似于滑动、动画,更新频率较高的操作,所以经常能看到白屏或者卡顿。





  1. 新架构设计




旧的架构JS层与Native的通讯都太依赖bridge,导致一些通讯频率较高的交互和设计就很难实现,同时也影响了渲染性能,这就是Facebook这次重构的主要目标,在新的设计上,ReactNative提出了几个新的概念和设计:



  1. JSI(javascript interface):这是本次架构重构的核心重点,也正是因为这层的调整,将原有重度依赖的native bridge架构解耦,实现了自由通讯。

  2. Fabric:依赖JSI的设计,并将旧架构下的shadow tree层移到C++层,这样可以透过JSI,实现前端组件对UI组件的一对一控制,摆脱了旧架构下对于UI的异步、批量操作。

  3. TuborModule:新的原生API架构,替换了原有的java module架构,数据结构上除了支持基础类型外,开始支持JSI对象,让前端和客户端的API形成一对一的调用

  4. 社区化:在不断迭代中,facebook团队发现,开源社区提供的组件和API越来越多,而且很多组件设计和架构上比ReactNative要好,而且官方组件因为资源问题,投入度并不够,对于一些社区问题的反馈,响应和解决问题也不太及时。社区化后,大量的系统组件会开放到社区中,交个开发者维护,例如现在的webview组件


上面这些概念其实在架构图上已经体现了,主要用于替换原有的bridge设计,下面我们将重点剖析这些模块的原理和作用:


JSI :


JSI在0.60后的版本就已经开始支持,它是Facebook在js引擎上设计的一个适配架构,允许我们向 Javascript 运行时注册方法的 Javascript 接口,这些方法可通过 Javascript 世界中的全局对象获得,可以完全用 C++ 编写,也可以作为一种与 iOS 上的 Objective C 代码和 Android 中的 Java 代码进行通信的方式。任何当前使用Bridge在 Javascript 和原生端之间进行通信的原生模块都可以通过用 C++ 编写一个简单的层来转换为 JSI 模块



  • 标准化的JS引擎接口,ReactNative可以替换v8、Hermes等引擎。




  • 它是架起 JS 和原生 java 或者 Objc 的桥梁,类似于老的 JSBridge架构的作用,但是不同的是采用的是内存共享、代理类的方式,JS所有的运行环境都是在 JSRuntime 环境下的,为了实现和 native 端直接通讯,我们需要有一层 C++ 层实现的 JSI::HostObject,该数据结构只有 get、set 两个接口,通过 prop 来区分不同接口的调用。




  • 原有JS与Native的数据沟通,更多的是采用json和基础类型数据,但有了JSI后,数据类型更丰富,支持JSI object。


所以API调用流程: JS->JSI->C++->JNI->JAVA,每个API更加独立化,不再全部依赖native module,但这也带来了另外一个问题,相比以前的设计更复杂了,设计一个API,开发者需要封装JS、C++、JNI、Java等一套接口。当然Facebook早已经想到了这个问题,所以在设计JSI的时候,就提供了一个codegen模块,帮忙大家完成基础代码和环境的搭建,以下我们会简单为大家介绍怎么使用这些工具:



  1. Facebook提供了一个脚手架工程,方便大家创建Native Module 模块,需提前增加npx命令


npx create-react-native-library react-native-simple-jsi


前面的步骤更多的是在配置一些模块的信息,值得注意的是在选择模块的开发语言时要注意,这边是支持很多种类型的,针对原生端开发我们用Java&OC比较多,也可以选择纯JS 或者C++的类型,大家根据自己的实际情况来选择,完成后需要选择是UI模块还是API模块,这里我们选择API(Native Module)来做测试:



以上是完成后的目录结构,大家可以看到这是个完整的ReactNative App工程,相应的API需要开发者在对应的Android、iOS目录中开发。



下面我们看下C++ Moulde的模式,相比Java模式,多了cpp 模块,并在Moudle中以Native lib的方式加载so:





  1. 其实到这里我们还是没有创建JSI的模块,删掉删掉example目录后,运行下面命令,完成后在Android studio中导入 example/android,编译后app 工程,就能打包我们cpp目录下的C++文件到so


npx react-native init example
cd example
yarn add ../



  1. 到这里我们完成了C++库的打包,但是不是我们想要的JSI Module,需要修改Module模块,代码如下,从代码中我们可以看到,不再有reactmethod标记,而是直接的一些install方法,在这个JSI Module 创建的时候调用注入环境


public class NewswiperJsiModule extends ReactContextBaseJavaModule {
public static final String NAME = "NewswiperJsi";
public NewswiperJsiModule(ReactApplicationContext reactContext) {
super(reactContext);
}

@Override
@NonNull
public String getName() {
return NAME;
}

static {
try {
// Used to load the 'native-lib' library on application startup.
System.loadLibrary("cpp");
} catch (Exception ignored) {
}
}

private native void nativeInstall(long jsi);

public void installLib(JavaScriptContextHolder reactContext) {
if (reactContext.get() != 0) {
this.nativeInstall(
reactContext.get()
);
} else {
Log.e("SimpleJsiModule", "JSI Runtime is not available in debug mode");
}
}
}

public class SimpleJsiModulePackage implements JSIModulePackage {
@Override
public List<JSIModuleSpec> getJSIModules(ReactApplicationContext reactApplicationContext, JavaScriptContextHolder jsContext) {
reactApplicationContext.getNativeModule(SimpleJsiModule.class).installLib(jsContext);
return Collections.emptyList();
}
}


  1. 后面就是我们要创建JSI Object了,用来直接和JS通讯,主要是通过createFromHostFunction 来创建JSI的代理对象,并通过global().setProperty注入到JS运行环境


void install(Runtime &jsiRuntime) {
auto multiply = Function::createFromHostFunction(jsiRuntime,
PropNameID::forAscii(jsiRuntime,
"multiply"),
2,
[](Runtime &runtime,
const Value &thisValue,
const Value *arguments,
size_t count) -> Value {
int x = arguments[0].getNumber();
int y = arguments[1].getNumber();

return Value(x * y);

});

jsiRuntime.global().setProperty(jsiRuntime, "multiply", move(multiply));

global.multiply(2,4) // 8

到这里相信大家知道了怎么通过JSI完成JSIMoudle的搭建了,这也是我们TurboModule和Fabric设计的核心底层设计。


Fabric :


Fabric是新架构的UI框架,和原有UImanager框架是类似,前面章节也说明UIManager框架的一些问题,特别在渲染性能上的瓶颈,似乎基于原有架构已经很难再有优化,体验上与原生端组件和动画的渲染性能还是差距比较大的,举个比较常见的问题,Flatlist快速滑动的状态下,会存在很长的白屏时间,交互比较强的动画、手势很难支持,这也是此次架构升级的重点,下面我们也从原理上简单说明下新架构的特点:



  1. JS层新设计了FabricUIManager,目的是支持Fabric render完成组件的更新,它采用了JSI的设计,可以和cpp层沟通,对应C++层UIManagerBinding,其实每个操作和API调用都有对应创建了不同的JSI,从这里就彻底解除了原有的全部依赖UIManager单个Native bridge的问题,同时组件大小的measure也摆脱了对Java、bridge的依赖,直接在C++层shadow完成,提升渲染效率


export type Spec = {|
+createNode: (
reactTag: number,
viewName: string,
rootTag: RootTag,
props: NodeProps,
instanceHandle: InstanceHandle,
) => Node,
+cloneNode: (node: Node) => Node,
+cloneNodeWithNewChildren: (node: Node) => Node,
+cloneNodeWithNewProps: (node: Node, newProps: NodeProps) => Node,
+cloneNodeWithNewChildrenAndProps: (node: Node, newProps: NodeProps) => Node,
+createChildSet: (rootTag: RootTag) => NodeSet,
+appendChild: (parentNode: Node, child: Node) => Node,
+appendChildToSet: (childSet: NodeSet, child: Node) => void,
+completeRoot: (rootTag: RootTag, childSet: NodeSet) => void,
+measure: (node: Node, callback: MeasureOnSuccessCallback) => void,
+measureInWindow: (
node: Node,
callback: MeasureInWindowOnSuccessCallback,
) => void,
+measureLayout: (
node: Node,
relativeNode: Node,
onFail: () => void,
onSuccess: MeasureLayoutOnSuccessCallback,
) => void,
+configureNextLayoutAnimation: (
config: LayoutAnimationConfig,
callback: () => void, // check what is returned here
// This error isn't currently called anywhere, so the `error` object is really not defined
// $FlowFixMe[unclear-type]
errorCallback: (error: Object) => void,
) => void,
+sendAccessibilityEvent: (node: Node, eventType: string) => void,
|};

const FabricUIManager: ?Spec = global.nativeFabricUIManager;

module.exports = FabricUIManager;

if (methodName == "createNode") {
return jsi::Function::createFromHostFunction(
runtime,
name,
5,
[uiManager](
jsi::Runtime &runtime,
jsi::Value const &thisValue,
jsi::Value const *arguments,
size_t count) noexcept -> jsi::Value {
auto eventTarget =
eventTargetFromValue(runtime, arguments[4], arguments[0]);
if (!eventTarget) {
react_native_assert(false);
return jsi::Value::undefined();
}
return valueFromShadowNode(
runtime,
uiManager->createNode(
tagFromValue(arguments[0]),
stringFromValue(runtime, arguments[1]),
surfaceIdFromValue(runtime, arguments[2]),
RawProps(runtime, arguments[3]),
eventTarget));
});
}


  1. 有了JSI后,以前批量依赖bridge的UI操作,都可以同步的执行到c++层,而在c++层,新架构完成了一个shadow层的搭建,而旧架构是在java层实现,以下也重点说明下几个重要的设计:



  • FabricUIManager (JS,Java) ,JS 端和原生端 UI 管理模块。




  • UIManager/UIManagerBinding(C++),C++中用来管理UI的模块,并通过binding JNI的方式通过FabricUIManager(Java)管理原生端组件




  • ComponentDescriptor (C++) ,原生端组件的唯一描述及组件属性定义,并注册在CoreComponentsRegistry模块中




  • Platform-specific




  • Component Impl (Java,ObjC++),原生端组件Surface,通过FabricUIManager来管理




  1. 新架构下,开发一个原生组件,需要完成Java层的原生组件及ComponentDescriptor (C++) 开发,难度相较于原有的viewManager有所提升,但ComponentDescriptor本身很多是shadow层代码,比较固定,Facebook后续也会提供codegen工具,帮助大家完成这部分代码的自动生成,简化代码难度



TurboModule:


实际上0.64版本已经支持TurboModule,在分析它的设计原理前,我们先说明下设计这个模块的目的,从上面架构图来看,主要用来替换NativeModule的重要一环:



  1. NativeModule 会包含很多我们初始化过程中就需要注册的的API,随着开发迭代,依赖NativeMoude的API和package会越来越多,解析及校验这些pakcages的时间会越来越长,最终会影响TTI时长

  2. 另外Native module其实大部分都是提供API服务,其实是可以采用单例子模式运行的,而不用跟随bridge的关闭打开,创建很多次


TurboModule的设计就是为了解决这些问题,原理上还是采用JSI提供的能力,方便JS可以直接调用到c++ 的host object,下面我们从代码层简单分析原理:



上面代码就是目前项目里面给出的一个例子,通过实现TurboModule来完NativeModule的开发,其实代码流程和原有的BaseJavaModule大致是一样的,不同的是底层的实现:



  1. 现有版本可以通过 ReactFeatureFlags.useTurboModules来打开这个模块功能

  2. TurboModule 组件是通过TurboModuleManager.java来管理的,被注入的modules可以分为初始化加载的和非初始化加载的组件

  3. 同样JNI/C++层也有一层TurboModuleManager用来管理注册java/C++的module,并通过TurboModuleBinding C++层的proxy moudle注入到JS层,到这里基本就和上面说的基础架构JSI接上轨了,js中可以通过代理的__turboModuleProxy来完成c++层的module调用,c++层透过jni最终完成对java代码的执行,这里facebook设计了两种类型的moudles,longLivedObject 和 非常驻的,设计思路上就和我们上面要解决的问题吻合了


void TurboModuleBinding::install(
jsi::Runtime &runtime,
const TurboModuleProviderFunctionType &&moduleProvider) {
runtime.global().setProperty(
runtime,
"__turboModuleProxy",
jsi::Function::createFromHostFunction(
runtime,
jsi::PropNameID::forAscii(runtime, "__turboModuleProxy"),
1,

// Create a TurboModuleBinding that uses the global
// LongLivedObjectCollection
[binding =
std::make_shared<TurboModuleBinding>(std::move(moduleProvider))](
jsi::Runtime &rt,
const jsi::Value &thisVal,
const jsi::Value *args,
size_t count) {
return binding->jsProxy(rt, thisVal, args, count);
}));
}

const NativeModules = require('../BatchedBridge/NativeModules');
import type {TurboModule} from './RCTExport';
import invariant from 'invariant';

const turboModuleProxy = global.__turboModuleProxy;

function requireModule<T: TurboModule>(name: string): ?T {
// Bridgeless mode requires TurboModules
if (!global.RN$Bridgeless) {
// Backward compatibility layer during migration.
const legacyModule = NativeModules[name];
if (legacyModule != null) {
return ((legacyModule: $FlowFixMe): T);
}
}

if (turboModuleProxy != null) {
const module: ?T = turboModuleProxy(name);
return module;
}

return null;
}

CodeGen:



  1. 新架构UI增加了C++层的shadow、component层,而且大部分组件都是基于JSI,因而开发UI组件和API的流程更复杂了,要求开发者具有c++、JNI的编程能力,为了方便开发者快速开发Facebook也提供了codegen工具,帮助生成一些自动化的代码,具体工具参看:github.com/facebook/re…

  2. 以下是代码生成的大概流程,因codegen目前还没有正式release,关于如何使用的文档几乎没有,但也有开发者尝试使用生成了一些代码,可以参考github.com/karol-biszt…





  1. 总结:




上面我们从API、UI角度重新学习了新架构,JSI、Turbormodule已经在最新的版本上已经可以体验,而且开发者社区也用JSI开发了大量的API组件,例如以下的一些比较依赖C++实现的模块:

























从最新的代码结构来看,新架构离发布似乎已经进入倒计时了,作为一直潜心学习、研究ReactNative的开发者相信一定和我一样很期待,从Facebook官方了解到Facebook App已经采用了新的架构,预计今年应该就能正式release了,这一次我们可以相信ReactNative应该要正式进入1.0版本了吧,reactnative.dev/blog/2021/0…





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

收起阅读 »

ReactNative在游戏营销场景中的实践和探索-性能优化

客户端跨端框架已经发展了很多年了,最近比较流行的小程序、Flutter、ReactNative,都算是比较成功、成熟的框架,面向的开发者也不一样,很多大型App都广泛的使用了,笔者有幸很早就参与学习使用了这些优秀的跨端方案,在这几年的开发和架构设计中,除了在A...
继续阅读 »

客户端跨端框架已经发展了很多年了,最近比较流行的小程序、Flutter、ReactNative,都算是比较成功、成熟的框架,面向的开发者也不一样,很多大型App都广泛的使用了,笔者有幸很早就参与学习使用了这些优秀的跨端方案,在这几年的开发和架构设计中,除了在App中支撑了千万级DAU,也慢慢将ReactNative跨端方案运用到了游戏,来提升开发、迭代效率。本次文章我们会分5个章节介绍我们在游戏中的一些探索和实践,相信大家也能从中有所收获:

(随着版本不断迭代完善,基本具有大量上线游戏的能力,随着游戏业务越来越多,在不同的游戏环境中,也碰到不少问题,这也从侧面体现出了游戏场景和架构的复杂性,主要核心问题还是在于ReactNative的沉浸式体验、启动性能、内存、渲染性能问题等,似乎这些问题也是ReactNative的通病,为了解决这些问题,我们开始专项优化。


1. 启动性能优化


针对启动性能问题,我们也测试列大量数据,ReactNative在纯客户端App中,性能表现还算不错,但在游戏低内存、cpu过度占用的情况下,该问题显得格外突出,要解决这些问题,首先我们需要了解ReactNative加载的主要时间消耗,可以参考下图:



整体页面渲染显示前,需要首先加载加载初始化 React Native Core Bridge,主要包含ReactNative的运行环境、UI和API组件功能等,然后才能运行业务的 JS,执行render绘制UI,完成后,React Native 才能将 JS 的组件渲染成原生的组件。因页面的加载流程是固定不变的,所以我们可以采用了提前预加载Core bridge的方案来提升加载性能,当游戏营销页面启动前,预先加载好原生端bridge,这样在打开业务是指需要运行前端JS代码渲染,设计思路上我们也根据业务场景设计了模式:



  • 预加载业务包:提前加载好完整的业务包到内存,生成并缓存ReactInstanceManager对象,在业务启动时,从内存缓存中获取该对象,并直接运行绑定rootview,经过改造,该方案能提升整体的打开速度30%-50%左右,游戏环境下,手机设备基本都达到秒开,模拟器设备在2s内,但这种通过内存换取速度的方法,在业务量大后,很明显是不可取的,所以整包预加载的局限性比较强。




  • Common包预加载:针对全包预加载的局限性,我们提出了分包方案,预加载common包,研究发现ReactNative打包生成的业务包其实有两部分内容,一部分是公共的基础组件、API包,统称common包,一部分是业务的核心逻辑包。改造打包方式,可以把原有的全包模式分离成common+bussiness,在多业务包模式下,可以共享统一的common包,在打开业务前,我们会优先预加载common包,并缓存对应的ReactInstanceManager对象,用户触发打开业务后,再加载bussiness 包,该方案相对于全包预加载性能略差,但比不预加载能提升15%-20%左右,同时支持多业务运行环境,具体思路可以参考开源项目react-native-multibundler




  • 从时序运行上,除了core bridge的初始化外,js 运行到页面显示,实际上也占用了不少时间,在预加载core bridge上,我们更近一步,支持了预加载rootview,提前将要渲染页面的rootview运行起来缓存在内存,当然这里加载的还是基础模块,在业务打开时,路由触发展示页面即可,可以做到页面无延时打开,但是对内存的开销,比预加载core bridge 更高。


当然上述方案都是通过内存换性能,不同的加载方式都做到了云控,随时切换、关闭。除了这些方案外同样还有其他方式能优化启动性能:



  1. Lazy module,将引擎自定义的API Native Module改造成懒加载方式,整体性能提升在5% 左右。

  2. 业务代码做到按需require,不需要展示的部分,采用lazy require,提升页面的显示、渲染速度。

  3. 裁剪业务包,将业务代码没有用到React的module、API、组件删除,减少业务包大小来提升启动性能。

  4. 分包方案,从测试数据来看,业务包越小,启动性能越好,包大小无法减小后,将业务包按照路由拆分为子包,也能立竿见影的解决启动速度问题。将业务包按照路由页面和功能分成多个子的业务子包,让首屏业务逻辑包变小,做到按需加载其他业务包,提升首页启动性能。


这些方案都从引擎加载的角度解决了启动性能慢,做到了按需加载,整体性能达到了最优化。但是在游戏中,业务页面的显示还是太依赖服务度请求来完成页面的渲染,所以在逐步优化后,发现网络请求对于页面的显示也占了很大一部分,为了进一步提升首屏显示,我么增加了网络请求预拉取、图片预缓冲方案:



  1. 网络预拉取,对于一些对首屏显示影响较大的网络请求,在引擎加载后,在合适时机从云控平台获取后,根据配置拉取并缓存到内存,打开业务后,优先从缓存中读取网络接口内容并显示。

  2. 图片预缓存,对于一些加载较慢的图片,将链接配置到云端后,在合适时机提前预加载到Fresco内存,页面打开后Fresco会从缓存中直接读取bitmap


除了这些方案外,替换JSC引擎到hermes,也能很好的解决启动性能问题,后面章节会重点介绍。


2. 内存优化


以上所有的优化更多是针对启动性能的优化设计,也是业内用于提升加载性能的方案,在游戏的复杂环境下,除了性能外,对于内存的要求也是很严格的,游戏启动后,本身对于内存的消耗就比一般的原生app高,所以在内存使用上会更精确和严格,那ReactNative是怎么优化内存的:

分包方案,分包方案除了在启动速度上有很大优化外,实现了按需加载,对于内存来说也做到了最优化。

字体加载,因游戏字体库无法和原生字体共享,导致在ReactNative页面使用字体会大大增加整体的内存,为了降低字体的内存,我们支持了字体的裁剪方案,按需打入字体,删掉一些生僻的字,大大降低了字体包的大小。另外字体文件对于业务包大小影响也比较大,我们支持字体的动态下发和加载。

图片优化,除了业务UI和JS本身占用的内存外,内存上占用比较大的是图片,而且图片有缓存,为了降低图片的内存消耗,我们支持了webp、gif等格式的图片,有损压缩,同时对于网络图片做到了按手机分辨率下发。另外提供API到前端业务,按需清理不使用的图片,及时释放内存,并控制图片缓存大小。


3. 渲染性能


除了内存、启动性能外,在游戏中的渲染性能也至关重要,ReactNative受限于游戏内的内存和CPU负载高,同等复杂度页面,表现不如原生App。为了能优化这些指标,我们对ReactNative的渲染流程做了分析和优化,支持静止状态下帧率基本达到了60fps,大致优化如下:

ReactNative是前端事件驱动原生UI渲染的,所以设计上ReactNative会在Frame Buffer每一帧绘画结束后的回调在UI线程中处理UI更新,即使没有更新的情况下也会空运转,这在UI线程负载本就较高的游戏中,增加了UI的负担

动画、点击事件都是同样的设计,会不断的有任务空转占用UI线程,增加了UI线程每次绘制的时间

解决这个问题,就是要支持资源的按需加载,我们将动画、UI更新事件放到了消息map,每次一帧渲染完成后,我们会检查map消息,是否有需要处理的消息,没有后续就不再在一帧渲染完成后调度UI线程,当用户触发了动画或者UI更新,会发送消息map,并注册帧渲染的callback,在callback中检查map消息更新UI


另外ReactNative采用的是原生UI渲染,在打开硬件加速的情况,整体渲染性能表现比较高,但是在游戏环境中,大部分游戏都是不开硬件加速的(自渲染组件和引擎的缘故),对于比较复杂的ReactNative UI,更新UI时整体FPS会偏低,UI响应会比较慢,特别是在模拟器(限制fps30)的情况下,渲染性能更加差强人意。在复杂交互的情况,要怎么提升性能?

简单的UI设计,没有大图背景的情况下,不开硬件加速,整体渲染性还不算差,但有大的背景情况下,UI性能表现尤其差,所以解决渲染问题,其实更多的是要解决大图渲染的问题

ReactNative 提供了renderToHardwareTextureAndroid 来用native内存换渲染的性能,导致的问题是内存消耗较高,对于图片不是太多、内存限制不是很严格的业务,可以采用该方式提升性能

对于大量使用图片的业务,我们设计一套采用opengl渲染方式的组件,支持纹理图(比较通用的etc1),从内存和渲染性能上,明显都得到了很大的提升,但这种模式依赖硬件加速,所以一般是在Dialog窗口模式中使用,具体的实现原理,大家可以关注作者文章,后面会详细和大家分享


核心示例代码:


 /* GLES20.glCompressedTexImage2D(target, 0, ETC1.ETC1_RGB8_OES , bitmap.getWidth(), bitmap.getHeight(), 0, etc1tex.getData().capacity(), etc1tex.getData());*/

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

收起阅读 »

ReactNative——react-native-video实现视频全屏播放

react-native-video是github上一个专用于React Native做视频播放的组件。这个组件是React Native上功能最全最好用的视频播放组件,还在持续开发之中,虽然还有些bug,但基本不影响使用,强力推荐。 本篇文章主要介绍下怎么使...
继续阅读 »

react-native-video是github上一个专用于React Native做视频播放的组件。这个组件是React Native上功能最全最好用的视频播放组件,还在持续开发之中,虽然还有些bug,但基本不影响使用,强力推荐。


本篇文章主要介绍下怎么使用react-native-video播放视频,以及如何实现全屏播放,屏幕旋转时视频播放器大小随之调整,显示全屏或收起全屏。


首先来看看react-native-video有哪些功能。


基本功能



  1. 控制播放速率

  2. 控制音量大小

  3. 支持静音功能

  4. 支持播放和暂停

  5. 支持后台音频播放

  6. 支持定制样式,比如设置宽高

  7. 丰富的事件调用,如onLoad,onEnd,onProgress,onBuffer等等,可以通过对应的事件进行UI上的定制处理,如onBuffer时我们可以显示一个进度条提示用户视频正在缓冲。

  8. 支持全屏播放,使用presentFullscreenPlayer方法。这个方法在iOS上可行,在android上不起作用。参看issue#534,#726也是同样的问题。

  9. 支持跳转进度,使用seek方法跳转到指定的地方进行播放

  10. 可以加载远程视频地址进行播放,也可以加载RN本地存放的视频。


注意事项


react-native-video通过source属性设置视频,播放远程视频时使用uri来设置视频地址,如下:


source={{uri: "http://www.xxx.com/xxx/xxx/xxx.mp4"}}

播放本地视频时,使用方式如下:


source={require('../assets/video/turntable.mp4')}

需要注意的是,source属性不能为空,uri或本地资源是必须要设置的,否则会导致app闪退。uri不能设置为空字符串,必须是一个具体的地址。


安装配置


使用npm i -S react-native-videoyarn add react-native-video安装,完成之后使用react-native link react-native-video命令link这个库。


Android端在执行完link命令后,gradle中就已经完成了配置。iOS端还需要手动配置一下,这里简单说一下,与官方说明不同的是,我们一般不使用tvOS的,选中你自己的target,在build phases中先移除掉自动link进来的libRCTVideo.a这个库,然后点击下方加号重新添加libRCTVideo.a,注意不要选错。


视频播放


实现视频播放其实很简单,我们只需要给Video组件设置一下source资源,然后设置style调整Video组件宽高就行了。



    ref={(ref) => this.videoPlayer = ref}
source={{uri: this.state.videoUrl}}
rate={1.0}
volume={1.0}
muted={false}
resizeMode={'cover'}
playWhenInactive={false}
playInBackground={false}
ignoreSilentSwitch={'ignore'}
progressUpdateInterval={250.0}
style={{width: this.state.videoWidth, height: this.state.videoHeight}}
/>

其中videoUrl是我们用来设置视频地址的变量,videoWidth和videoHeight是用来控制视频宽高的。


全屏播放的实现


视频全屏播放其实就是在横屏情况下全屏播放,竖屏一般都是非全屏的。要实现设备横屏时视频全屏显示,说起来很简单,就是通过改变Video组件宽高来实现。


上面我们把videoWidth和videoHeight存放在state中,目的就是为了通过改变两个变量的值来刷新UI,使视频宽高能随之改变。问题是,怎样在设备的屏幕旋转时及时获取到改变后的宽高呢?


竖屏时我设置的视频初始宽度为设备屏幕的宽度,高度为宽度的9/16,即按16:9的比例显示。横屏时视频的宽度应为屏幕的宽度,高度应为当前屏幕的高度。由于横屏时设备宽高发生了变化,及时获取到宽高就能及时刷新UI,视频就能全屏展示了。


刚开始我想到的办法是使用react-native-orientation监听设备转屏的事件,在回调方法中判断当前是横屏还是竖屏,这个在iOS上是可行的,但是在Android上横屏和竖屏时获取到宽高值总是不匹配的(比如,横屏宽384高582,竖屏宽582高384,显然不合理),这样就无法做到统一处理。


所以,监听转屏的方案是不行的,不仅费时还得不到想要的结果。更好的方案是在render函数中使用View作为最底层容器,给它设置一个"flex:1"的样式,使其充满屏幕,在View的onLayout方法中获取它的宽高。无论屏幕怎么旋转,onLayout都可以获取到当前View的宽高和x、y坐标。


/// 屏幕旋转时宽高会发生变化,可以在onLayout的方法中做处理,比监听屏幕旋转更加及时获取宽高变化
_onLayout = (event) => {
//获取根View的宽高
let {width, height} = event.nativeEvent.layout;
console.log('通过onLayout得到的宽度:' + width);
console.log('通过onLayout得到的高度:' + height);

// 一般设备横屏下都是宽大于高,这里可以用这个来判断横竖屏
let isLandscape = (width > height);
if (isLandscape){
this.setState({
videoWidth: width,
videoHeight: height,
isFullScreen: true,
})
} else {
this.setState({
videoWidth: width,
videoHeight: width * 9/16,
isFullScreen: false,
})
}
};

这样就实现了屏幕在旋转时视频也随之改变大小,横屏时全屏播放,竖屏回归正常播放。注意,Android和iOS需要配置转屏功能才能使界面自动旋转,请自行查阅相关配置方法。


播放控制


上面实现了全屏播放还不够,我们还需要一个工具栏来控制视频的播放,比如显示进度,播放暂停和全屏按钮。具体思路如下:



  1. 使用一个View将Video组件包裹起来,View的宽高和Video一致,便于转屏时改变大小

  2. 设置一个透明的遮罩层覆盖在Video组件上,点击遮罩层显示或隐藏工具栏

  3. 工具栏中要显示播放按钮、进度条、全屏按钮、当前播放时间、视频总时长。工具栏以绝对位置布局,覆盖在Video组件底部

  4. 使用react-native-orientation中的lockToPortrait和lockToLandscape方法强制旋转屏幕,使用unlockAllOrientations在屏幕旋转以后撤销转屏限制。


这样才算是一个有模有样的视频播放器。下面是竖屏和横屏的效果图




再也不必为presentFullscreenPlayer方法不起作用而烦恼了,全屏播放实现起来其实很简单。具体代码请看demo:github.com/mrarronz/re…


总结



  1. react-native-orientation和react-native-video都还有缺陷,但是已经可以运用到项目中了

  2. 有时候解决问题要换种思路,不能一棵树上吊死。坐下来喝杯茶,换种心态、换个搜索关键词说不定就得到了你想要的答案。

作者:不變旋律
链接:https://juejin.cn/post/6844903570999869448

收起阅读 »

面试常问的ACTION_CANCEL到底何时触发,滑出子View范围会发生什么?

看完本文你将了解:ACTION_CANCEL的触发时机滑出子View区域会发生什么?为什么不响应onClick()事件首先看一下官方的解释:/** * Constant for {@link #getActionMasked}: The current ge...
继续阅读 »

看完本文你将了解:

  • ACTION_CANCEL的触发时机
  • 滑出子View区域会发生什么?为什么不响应onClick()事件

首先看一下官方的解释:

/**
* Constant for {@link #getActionMasked}: The current gesture has been aborted.
* You will not receive any more points in it. You should treat this as
* an up event, but not perform any action that you normally would.
*/

public static final int ACTION_CANCEL = 3;

说人话就是:当前的手势被中止了,你不会再收到任何事件了,你可以把它当做一个ACTION_UP事件,但是不要执行正常情况下的逻辑。

ACTION_CANCEL的触发时机

有四种情况会触发ACTION_CANCEL:

  • 在子View处理事件的过程中,父View对事件拦截
  • ACTION_DOWN初始化操作
  • 在子View处理事件的过程中被从父View中移除时
  • 子View被设置了PFLAG_CANCEL_NEXT_UP_EVENT标记时
1,父view拦截事件

首先要了解ViewGroup什么情况下会拦截事件,Look the Fuck Resource Code:

/**
* {@inheritDoc}
*/

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...

boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
...
// Check for interception.
final boolean intercepted;
// 判断条件一
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
// 判断条件二
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
...
}
...
}

有两个条件

  • MotionEvent.ACTION_DOWN事件或者mFirstTouchTarget非空也就是有子view在处理事件
  • 子view没有做拦截,也就是没有调用ViewParent#requestDisallowInterceptTouchEvent(true)

如果满足上面的两个条件才会执行onInterceptTouchEvent(ev)

如果ViewGroup拦截了事件,则intercepted变量为true,接着往下看:

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {

boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
...

// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
// 当mFirstTouchTarget != null,也就是子view处理了事件
// 此时如果父ViewGroup拦截了事件,intercepted==true
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}

...

// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
...
} else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
...
} else {
// 判断一:此时cancelChild == true
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;

// 判断二:给child发送cancel事件
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
...
}
...
}
}
...
}
...
return handled;
}

以上判断一处cancelChild为true,然后进入判断二中一看究竟:

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;

// Canceling motions is a special case. We don't need to perform any transformations
// or filtering. The important part is the action, not the contents.
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
// 将event设置成ACTION_CANCEL
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
...
} else {
// 分发给child
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
...
}

当参数cancel为ture时会将event设置为MotionEvent.ACTION_CANCEL,然后分发给child。

2,ACTION_DOWN初始化操作
public boolean dispatchTouchEvent(MotionEvent ev) {

boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;

// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
// 取消并清除所有的Touch目标
cancelAndClearTouchTargets(ev);
resetTouchState();
}
...
}
...
}

系统可能会由于App切换、ANR等原因丢失了up,cancel事件。

因此需要在ACTION_DOWN时丢弃掉所有前面的状态,具体代码如下:

private void cancelAndClearTouchTargets(MotionEvent event) {
if (mFirstTouchTarget != null) {
boolean syntheticEvent = false;
if (event == null) {
final long now = SystemClock.uptimeMillis();
event = MotionEvent.obtain(now, now,
MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
syntheticEvent = true;
}

for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
resetCancelNextUpFlag(target.child);
// 分发事件同情况一
dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
}
...
}
}

PS:在dispatchDetachedFromWindow()中也会调用cancelAndClearTouchTargets()

3,在子View处理事件的过程中被从父View中移除时
public void removeView(View view) {
if (removeViewInternal(view)) {
requestLayout();
invalidate(true);
}
}

private boolean removeViewInternal(View view) {
final int index = indexOfChild(view);
if (index >= 0) {
removeViewInternal(index, view);
return true;
}
return false;
}

private void removeViewInternal(int index, View view) {

...
cancelTouchTarget(view);
...
}

private void cancelTouchTarget(View view) {
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (target.child == view) {
...
// 创建ACTION_CANCEL事件
MotionEvent event = MotionEvent.obtain(now, now,
MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
分发给目标view
view.dispatchTouchEvent(event);
event.recycle();
return;
}
predecessor = target;
target = next;
}
}
4,子View被设置了PFLAG_CANCEL_NEXT_UP_EVENT标记时

在情况一种的两个判断处:

// 判断一:此时cancelChild == true
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;

// 判断二:给child发送cancel事件
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}

resetCancelNextUpFlag(target.child)为true时同样也会导致cancel,查看代码:

/**
* Indicates whether the view is temporarily detached.
*
* @hide
*/

static final int PFLAG_CANCEL_NEXT_UP_EVENT = 0x04000000;

private static boolean resetCancelNextUpFlag(View view) {
if ((view.mPrivateFlags & PFLAG_CANCEL_NEXT_UP_EVENT) != 0) {
view.mPrivateFlags &= ~PFLAG_CANCEL_NEXT_UP_EVENT;
return true;
}
return false;
}

根据注释大概意思是,该view暂时detacheddetached是什么意思?就是和attached相反的那个,具体什么时候打了这个标记,我觉得没必要深究。

以上四种情况最重要的就是第一种,后面的只需了解即可。

滑出子View区域会发生什么?

了解了什么情况下会触发ACTION_CANCEL,那么针对问题:滑出子View区域会触发ACTION_CANCEL吗?这个问题就很明确了:不会。

实践是检验真理的唯一标准,代码撸起来:

public class MyButton extends androidx.appcompat.widget.AppCompatButton {

@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
LogUtil.d("ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
LogUtil.d("ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
LogUtil.d("ACTION_UP");
break;
case MotionEvent.ACTION_CANCEL:
LogUtil.d("ACTION_CANCEL");
break;
}
return super.onTouchEvent(event);
}
}

一波操作以后日志如下:

(MyButton.java:32) -->ACTION_DOWN (MyButton.java:36) -->ACTION_MOVE (MyButton.java:36) -->ACTION_MOVE (MyButton.java:36) -->ACTION_MOVE (MyButton.java:36) -->ACTION_MOVE (MyButton.java:36) -->ACTION_MOVE (MyButton.java:39) -->ACTION_UP

滑出view后依然可以收到ACTION_MOVEACTION_UP事件。

为什么有人会认为滑出view后会收到ACTION_CANCEL呢?

我想是因为滑出view后,view的onClick()不会触发了,所以有人就以为是触发了ACTION_CANCEL

那么为什么滑出view后不会触发onClick呢?再来看看View的源码:

在view的onTouchEvent()中:

case MotionEvent.ACTION_MOVE:
// Be lenient about moving outside of buttons
// 判断是否超出view的边界
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
if ((mPrivateFlags & PRESSED) != 0) {
// 这里改变状态为 not PRESSED
// Need to switch from pressed to not pressed
mPrivateFlags &= ~PRESSED;
}
}
break;

case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
// 可以看到当move出view范围后,这里走不进去了
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
...
performClick();
...
}
mIgnoreNextUpEvent = false;
break;

1,在ACTION_MOVE中会判断事件的位置是否超出view的边界,如果超出边界则将mPrivateFlags置为not PRESSED状态。

2,在ACTION_UP中判断只有当mPrivateFlags包含PRESSED状态时才会执行performClick()等。

因此滑出view后不会执行onClick()

结论:
  • 滑出view范围后,如果父view没有拦截事件,则会继续受到ACTION_MOVEACTION_UP等事件。
  • 一旦滑出view范围,view会被移除PRESSED标记,这个是不可逆的,然后在ACTION_UP中不会执行performClick()等逻辑。
收起阅读 »

Android 高仿支付宝手势密码

前言支付宝的手势密码 支持两种方式,第一种是进入app 时启动,第二种是进入理财时启动。实现1,我们先来分析下第一种方式,进入APP 时启动手势密码进入app 时启动手势密码,有一个关键的知识点,前后台切换,如何判断app 应用做了前后台切换了呢?(1) 使用...
继续阅读 »

前言

支付宝的手势密码 支持两种方式,第一种是进入app 时启动,第二种是进入理财时启动。

实现

1,我们先来分析下第一种方式,进入APP 时启动手势密码

进入app 时启动手势密码,有一个关键的知识点,前后台切换,如何判断app 应用做了前后台切换了呢?

(1) 使用ProcessLifecycleOmner

ProcessLifecycleOwner

该类提供了整个 app 进程的 lifecycle。

可以将其视为所有 activity 的 LifecycleOwner ,其中 Lifecycle.Event.ON_START 代表app 进入前台,而 Lifecycle.Event.ON_STOP 代表app 进入后台。当然(Lifecycle.Event.On_RESUME 和 Lifecycle.Event.ON_PAUSE 也可以分别代表进入前台和后台)。

ProcessLifecycleOwner.get().lifecycle.addObserver(object:LifecycleObserver{

@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun onForeground(){
EasyLog.e(TAG,"== onForeground==")
}

@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun onBackground(){
EasyLog.e(TAG,"== onBackground==")
}


});

ProcessLifecycle 能很好的监听前后台切换,但是 不太适合做手势密码的前后台切换,首先首页和登录页是不需要弹出手势密码的,这些页面要过滤,ProcessLifecycle 不好做到这一点。下面看第二种方法。

(2)使用 lifecycleCallbacks接口:

通过这个接口,我们对onActivityStart回调方法里记录启动的次数 mActivityCount++,onActivityStop 回调里对 mActivityCount-- ,当mActivityCount == 1 时认为在前台,mActivityCount ==0 在后台。代码如下:

 
/**
* 监听 前后台启动
* 自定义 可以很容易过滤一些不需要跳出手势密码的特殊的场景,比如 登录页
*/
class GestureLifecycleHandler constructor(context:Context): Application.ActivityLifecycleCallbacks {


companion object{
private const val TAG = "GestureLifecycleHandler"
}

private val uiScope = CoroutineScope(Dispatchers.Main)

private var isOpenHandLock = false
init {


}

/**
* 记录 activity 前后台情况
*/
private var mActivityCount: Int = 0

override fun onActivityPaused(activity: Activity?) {

}

override fun onActivityResumed(activity: Activity?) {



}

override fun onActivityStarted(activity: Activity?) {
if(activityFilter(activity)){
return
}

mActivityCount ++
EasyLog.e(TAG,"onForeground = $mActivityCount")
uiScope.launch {
withContext(Dispatchers.IO){
isOpenHandLock = GestureManager.getAppGestureState()
if(isOpenHandLock && mActivityCount == 1){
GestureActivity.actionStart(activity!!,GestureActivity.GestureState.Verify)
}
}

}

}

override fun onActivityDestroyed(activity: Activity?) {

}

override fun onActivitySaveInstanceState(activity: Activity?, outState: Bundle?) {

}

override fun onActivityStopped(activity: Activity?) {
if(activityFilter(activity)){
return
}
mActivityCount--
EasyLog.e(TAG,"onBackground = $mActivityCount")

}

override fun onActivityCreated(activity: Activity?, savedInstanceState: Bundle?) {

}

private fun activityFilter(activity: Activity?):Boolean{
return activity is SplashActivity
}
}

202109030924010.gif

2,我们分析第二种方式,进入理财时弹出手势密码

理财模块是个fragment ,也就是说要对财富fragment 监听前后台的变化,这个时候可以使用ProcessLifecycleOwner 对Fragment监听,代码如下:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
ProcessLifecycleOwner.get().lifecycle.addObserver(GestureLife(this))
}
private const val TAG = "GestureLife"

open class GestureLife(val fragment: GestureLockFragment) :LifecycleObserver{


private val uiScope = CoroutineScope(Dispatchers.Main)


@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun onVisible() {
EasyLog.e(TAG,"==ON_RESUME==")
if(fragment.isHidden){
EasyLog.e(TAG,"等待台跳出手势密码")
fragment.waitingGesture = true

}
if(!fragment.isHidden||fragment.isVisible){
EasyLog.e(TAG,"==isVisible==")
uiScope.launch {
withContext(Dispatchers.IO){
val isOpenHandLock = GestureManager.getFragmentGestureState()
if(isOpenHandLock && !GestureLockFragment.showGesture){
GestureLockFragment.showGesture = true
GestureActivity.actionStart(ActivityUtils.getTopActivity(),GestureActivity.GestureState.Verify)
}
}

}
}


}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun onBackground(){
EasyLog.e(TAG,"==onBackground==")
GestureLockFragment.showGesture = false
}

}

这里要处理两种情况,第一种财富这个fragment 前后台切换后,fragment 是可见的,那么久直接弹出手势,如果不可见要等待可见的时再次弹出,所以还要处理onHidden,

override fun onHiddenChanged(hidden: Boolean) {
super.onHiddenChanged(hidden)
EasyLog.e(TAG,"onHiddenChanged")
if(!hidden){
if(waitingGesture && !showGesture){
waitingGesture = false
showGesture = true
GestureActivity.actionStart(activity!!, GestureActivity.GestureState.Verify)
}
}
}

20210903094115133.gif

收起阅读 »

Kotlin 写自定义 ViewGroup

Android 最近推行的 Compose ,有着 Kotlin 的加持,使写 UI 更加方便快速,不用担心布局嵌套,还是声明式 UI,那么 Compose 有这么多好处,原生写法还有 “出路” 吗?今天给大家分享一种非传统的自定义 ViewGroup 写法,...
继续阅读 »

Android 最近推行的 Compose ,有着 Kotlin 的加持,使写 UI 更加方便快速,不用担心布局嵌套,还是声明式 UI,那么 Compose 有这么多好处,原生写法还有 “出路” 吗?

今天给大家分享一种非传统的自定义 ViewGroup 写法,让你对自定义 ViewGroup 不再 “恐惧”,再借助 Kotlin ,我们用原生写法,也可以快速写出无嵌套的布局。

为什么要用自定义 ViewGroup

平时大家写 UI,都直接用 xml 去写布局,或多或少都会注意去避免布局嵌套,自从有了 ConstraintLayout,嵌套的情况就减少了许多,但用 ConstraintLayout 就能达到极致性能吗?整体上相较于其他 Layout ,性能确实有所提升。但对于产品中具体的页面,可能就不会达到极致性能,因为 ConstraintLayout 要考虑的场景太多了,导致其逻辑很复杂,对于确定的页面来说,一个有 “针对性” 的自定义 ViewGroup ,是能够超越 ConstraintLayout 的,因为你只需要对一个页面负责即可,不用考虑那么全。

这里我所说的自定义 ViewGroup 就是用代码去写布局,不是写一个公共的控件让别人去使的那种。Telegram 的布局就全部用代码去写的。

那么我们平时为什么不去用代码写布局呢?

  • 自定义 ViewGroup 太复杂了,什么 MeasureSpec 情况有一堆。
  • 每次写都忘,还得去查,学了就忘
  • 效率太低,我为了那点性能提升,没必要

总结一下,就是因为自定义 ViewGroup 比较难,还费事。以前用 Java 写代码确实会比较麻烦,但现在有了 Kotlin,也可以很优雅的去用代码写布局了。接下来,我就带大家来捋一下自定义 ViewGroup 的流程,然后用 Kotlin 去实现。

自定义 ViewGroup 要做什么

一个 ViewGroup 有哪几步,我想大家都知道,无非就是测量、布局、绘制,就这三步,测量就是把子 View 的大小测量一下,再算一下自己的大小;布局就是设置一下子 View 的位置;绘制对于 ViewGroup 来说一般不需要,无非就是在自己这画点什么东西。这么看也不是很难嘛,那么难的是哪里呢?我想就是因为下面这张表:

img_01.png

就是测量的时候的各种模式,很多书上讲自定义 View 的时候都会给出这张表,其实这是作者自己总结出来的,Android 官网上是没有这些东西的。

现在让我们忘记上面这张表,就只看一下有几种测量模式:EXACTLY、AT_MOST、UNSPECIFIED,这三个英文意思已经很明确了。

  • EXACTLY 就是精确的,就是你设置多少就是多少
  • AT_MOST 就是最多能用多少,就是子 View 有多大,就给多大
  • UNSPECIFIED 就是不确定的,这种一般都是需要再次测量的,就比如 LinearLayout 使用 weight

其中 UNSPECIFIED 对于我们用代码写布局这种情况,几乎就不会用到,这种就可以不用考虑,那么就只剩下两种模式了,总结一下就是 “View 实际多少就是多少” 和 "View 最多能用多少",这么一想,是不是就没那么复杂了。光说大家估计也没有具体的概念,接下来上代码。

如何借助 Kotlin 提升写 ViewGroup 效率

接下来,让我们用 Kotlin 的扩展方法,来一步一步去完成自定义 ViewGroup 需要的东西。

测量的时候要传一个 MeasureSpec 对象,这个对象是根据宽高 Int 值和测量模式确定的,有了 Kotlin ,我们是不是可以直接给 Int 定义一个扩展方法,来获取这个 Int 值的 MeasureSpec 不就行了,来,看代码:

// EXACTLY 的测量模式
fun Int.toExactlyMeasureSpec() = MeasureSpec.makeMeasureSpec(this, MeasureSpec.EXACTLY)
// AT_MOST 的测量模式
fun Int.toAtMostMeasureSpec() = MeasureSpec.makeMeasureSpec(this, MeasureSpec.AT_MOST)

我们给一个控件设置宽高,一般都是给个具体的值,要么就是 MATCH_PARENT 或者 WRAP_CONTENT,那么我们是不是也这种常见的情况抽成一个方法呢?有了 Kotlin 我们可以直接在这个 View 上弄个扩展方法,来获取它的默认宽高:

// 获取 View 宽度的默认测量值
fun View.defaultWidthMeasureSpec(parent: ViewGroup): Int {
return when (layoutParams.width) {
// 如果是 MATCH_PARENT,就说明它要填满父布局,那就给它一个父布局宽度的精确值呗
MATCH_PARENT -> parent.measuredWidth.toExactlyMeasureSpec()
// 如果是 WRAP_CONTENT,就说明它满足自己的大小就行,那就给它最多能用的大小就行了
WRAP_CONTENT -> WRAP_CONTENT.toAtMostMeasureSpec()
// 0 就是不确定的,这里我们有 UI 稿,就没有不确定的情况了,所以这里就不用考虑了
0 -> throw IllegalAccessException("我不考虑这种情况 $this")
// 最后就是具体的值了,那就给你具体的呗
else -> layoutParams.width.toExactlyMeasureSpec()
}
}
// 获取 View 高度的默认测量值,和上面获取宽度的原理一样
fun View.defaultHeightMeasureSpec(parent: ViewGroup): Int {
return when (layoutParams.height) {
MATCH_PARENT -> parent.measuredHeight.toExactlyMeasureSpec()
WRAP_CONTENT -> WRAP_CONTENT.toAtMostMeasureSpec()
0 -> throw IllegalAccessException("我不考虑这种情况 $this")
else -> layoutParams.height.toExactlyMeasureSpec()
}
}

好了,有了这些,我们再写自定义 ViewGroup 是不是就简单多了,我们测量一个控件,直接这些写就可以了:

textView.measure(textView.defaultWidthMeasureSpec(this), textView.defaultHeightMeasureSpec(this))

等等,这样写还是有点复杂,我们为什么不干脆再定义一个扩展方法,让 View 直接按默认的测量好了:

fun View.autoMeasure(parent: ViewGroup) {
measure(
this.defaultWidthMeasureSpec(parent),
this.defaultHeightMeasureSpec(parent)
)
}

这样下次使用就可以这样写了:

textView.autoMeasure(this)

是不是更简单了,到这,测量的基本代码差不多就写完了,顺便把布局的基础方法也写一下吧,布局就比较简单了,就是告诉子 View 的位置就好了。

// 设置 view 的位置
fun View.autoLayout(parent: ViewGroup, x: Int = 0, y: Int = 0, fromRight: Boolean = false) {
// 判断布局是不是从右边开始
if (!fromRight) {
// 注意这里为什么用 measuredWidth 而不是用 width
// 因为 width 是通过 mRight - mLeft 计算的,而这时它俩都没有被赋值,所以都是 0
layout(x, y, x + measuredWidth, y + measuredHeight)
} else {
autoLayout(parent.measuredWidth - x - measuredWidth, y)
}
}

我们其实可以把这些方法都写到一个类,以后写自定义 ViewGroup,直接继承它就可以了,就像下面这样:

 // 为了方便设置 dp sp,直接在这里声明了扩展属性
val Int.dp
get() = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, this.toFloat(),
Resources.getSystem().displayMetrics
).toInt()
val Float.sp
get() = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP, this,
Resources.getSystem().displayMetrics
)

abstract class CustomViewGroup(context: Context) : ViewGroup(context) {

// 方便获取带 Margin 的宽高
protected val View.measuredWidthWithMargins get() = measuredWidth + marginStart + marginEnd
protected val View.measuredHeightWithMargins get() = measuredHeight + marginTop + marginBottom

protected fun Int.toExactlyMeasureSpec() = MeasureSpec.makeMeasureSpec(this, MeasureSpec.EXACTLY)

protected fun Int.toAtMostMeasureSpec() = MeasureSpec.makeMeasureSpec(this, MeasureSpec.AT_MOST)

protected fun View.defaultWidthMeasureSpec(parent: ViewGroup): Int {
return when (layoutParams.width) {
MATCH_PARENT -> parent.measuredWidth.toExactlyMeasureSpec()
WRAP_CONTENT -> WRAP_CONTENT.toAtMostMeasureSpec()
0 -> throw IllegalAccessException("我不考虑这种情况 $this")
else -> layoutParams.width.toExactlyMeasureSpec()
}
}

protected fun View.defaultHeightMeasureSpec(parent: ViewGroup): Int {
return when (layoutParams.height) {
MATCH_PARENT -> parent.measuredHeight.toExactlyMeasureSpec()
WRAP_CONTENT -> WRAP_CONTENT.toAtMostMeasureSpec()
0 -> throw IllegalAccessException("我不考虑这种情况 $this")
else -> layoutParams.height.toExactlyMeasureSpec()
}
}

protected fun View.autoMeasure() {
measure(
this.defaultWidthMeasureSpec(this@CustomViewGroup),
this.defaultHeightMeasureSpec(this@CustomViewGroup)
)
}

protected fun View.autoLayout(x: Int = 0, y: Int = 0, fromRight: Boolean = false) {
if (!fromRight) {
layout(x, y, x + measuredWidth, y + measuredHeight)
} else {
autoLayout(this@CustomViewGroup.measuredWidth - x - measuredWidth, y)
}
}
}

写个自定义 ViewGroup 试试

img_02.png

就以计算器界面为例吧。上面👆是通过 ConstraintLayout 实现的,看看有哪些控件,1 个 EditText,17 个 Button。我们来试试用自定义 ViewGroup 来简单复刻一下,直接上代码吧:

class CalculatorLayout(context: Context) : CustomViewGroup(context) {
// 我们可以直接这样在把控件 new 出来,设置一些属性,这样我们还省去了 findViewById,而且不用担心空指针
val etResult = AppCompatEditText(context).apply {
typeface = ResourcesCompat.getFont(context, R.font.comfortaa_regular)
setTextColor(ResourcesCompat.getColor(resources, R.color.white, null))
background = null
textSize = 65f
gravity = Gravity.BOTTOM or Gravity.END
maxLines = 1
isFocusable = false
isCursorVisible = false
setPadding(16.dp, paddingTop, 16.dp, paddingBottom)
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
// 注意,这里直接 add 不会触发 onMeasure 这些流程,可以放心 add
addView(this)
}
// 数字键盘后面的背景
val keyboardBackgroundView = View(context).apply {...}

// 抽出一个相同样式的按钮
class NumButton(context: Context, text: String, parent: ViewGroup) : AppCompatTextView(context) {
init {
setText(text)
gravity = Gravity.CENTER
background =
ResourcesCompat.getDrawable(resources, R.drawable.ripple_cal_btn_num, null)
layoutParams =
MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT).apply {
leftMargin = 2.dp
rightMargin = 2.dp
topMargin = 6.dp
bottomMargin = 6.dp
}
isClickable = true
setTextAppearance(context, R.style.StyleCalBtn)
parent.addView(this)
}
}

// 具体的数组按钮
val btn0 = NumButton(context, "0", this)
...

init {
// 给自己设置个背景
background = ResourcesCompat.getDrawable(resources, R.drawable.shape_cal_bg, null)
}

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
// 先算一下数字按钮的大小
val allSize =
measuredWidth - keyboardBackgroundView.paddingLeft - keyboardBackgroundView.paddingRight -
btn0.marginLeft * 8
val numBtSize = (allSize * (1 / 3.8)).toInt()
// 计算操作按钮的大小
val operatorBtWidth = (allSize * (0.8 / 3.8)).toInt()
val operatorBtHeight = (numBtSize * 4 + btn0.marginTop * 6 - btnDel.marginTop * 8) / 5

// 再计算数字盘的高度
val keyboardHeight =
keyboardBackgroundView.paddingTop + keyboardBackgroundView.paddingBottom +
numBtSize * 4 + btn0.marginTop * 8

// 最后把高度剩余空间都给 EditText
val editTextHeight = measuredHeight - keyboardHeight

// 测量背景
keyboardBackgroundView.measure(
measuredWidth.toExactlyMeasureSpec(),
keyboardHeight.toExactlyMeasureSpec()
)

// 测量按钮
btn0.measure(numBtSize.toExactlyMeasureSpec(), numBtSize.toExactlyMeasureSpec())
...
btnDiv.measure(operatorBtWidth.toExactlyMeasureSpec(), operatorBtHeight.toExactlyMeasureSpec())
...

// 测量 EditText
etResult.measure(
measuredWidth.toExactlyMeasureSpec(),
editTextHeight.toExactlyMeasureSpec()
)

// 最后设置自己的宽高
setMeasuredDimension(measuredWidth, measuredHeight)
}

override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
// 好了,测量都完了,就一个个放吧
// 先放 EditText
etResult.autoLayout()

// 把背景放上
keyboardBackgroundView.autoLayout(0, etResult.bottom)

// 开始放按钮吧
btn7.let {
it.autoLayout(
keyboardBackgroundView.paddingLeft + it.marginLeft,
keyboardBackgroundView.top + keyboardBackgroundView.paddingTop + it.marginTop
)
}
btn8.let {
it.autoLayout(
btn7.right + btn7.marginRight + it.marginLeft,
btn7.top
)
}
btn9.let {
it.autoLayout(
btn8.right + btn8.marginRight + it.marginLeft,
btn7.top
)
}
...
}
}

Ok,以上就完成了自定义 ViewGroup,我们可以直接这样用了:

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val contentView = CalculatorLayout(this)
setContentView(contentView)

// 不用 findViewById 和 ktx插件、ViewBinding 这些东西,直接用就可以
contentView.btnDel.setOnClickListener {
contentView.etResult.setText("")
}
}

看看最终的对比效果:

img_09.jpg

看上去还算可以,怎么样,是不是用 Kotlin 代码写布局也不是很复杂,缺点就是不能在 Android Studio 上预览。

结束了

虽然与 xml 的书写相比,确实有些麻烦,但熟练之后,我感觉都差不多,还能帮助我们对自定义 View 这块更加熟悉,感兴趣的小伙伴可以在项目不忙的时候先试试这种写法。当然,也可以试试 Compose,其实 Compose 最终也是一个 ViewGroup ,可以看看 AndroidComposeView ,它最终也是会添加到 DecorView 上。

收起阅读 »

Android开发太难了:Java Lambda ≠ Android Lambda

我又来了,继续回归写作中,目标 1 月 2 篇。需要两篇才能阐述清楚Java Lambda ≠ Android Lambda,本篇为上篇,先解释清楚 Java Lambda 的一些知识。耐心阅读本文,你一定会有收获。一、Java Lambda 不等于 匿名内部...
继续阅读 »

我又来了,继续回归写作中,目标 1 月 2 篇。

需要两篇才能阐述清楚Java Lambda ≠ Android Lambda,本篇为上篇,先解释清楚 Java Lambda 的一些知识。

耐心阅读本文,你一定会有收获。

一、Java Lambda 不等于 匿名内部类

测试环境JDK8。

首先我们看一段比较简单的代码片段:

public class TestJavaAnonymousInnerClass {
public void test() {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("hello java lambda");
}
};
runnable.run();
}
}

先问个简单的问题,如果我javac编译一下,你觉得会生成几个class文件?

不用问,肯定是两个,一个是TestJavaLambda.class,一个是TestJavaLambda$1.class,那么试下:

没错,确实两个,扎实的Java基础怎么会被这种问题打败。

大家都知道上面这个匿名内部类的写法,我们可以换成lambda表达式的写法对吧,甚至编译器都会提醒你使用lambda,我们改成lambda表达式的写法:

public class TestJavaLambda {
public void test() {
Runnable runnable = () -> {
System.out.println("hello java lambda");
};
runnable.run();
}
}

再问个简单的问题,如果我javac编译一下,你觉得会生成几个class文件?

嗯...你在搞我?这和刚才的问题有啥区别?

还认为是两个吗?我们再javac试一下?

不好意思,只有一个class文件了。

那么,我的一个新的问题来了:

Java匿名内部类的写法和Lambda表达式的写法,在编译期这么看肯定有区别的,那么有何区别?

二、Java Lambda的背后,invokedynamic的出现

看这类问题,第一件事肯定是对比字节码了,那我们javap -v 一哈,看一下test()方法区别:

匿名内部类的test():

public void test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=2, args_size=1
0: new #2 // class com/example/zhanghongyang/blog02/TestJavaAnonymousInnerClass$1
3: dup
4: aload_0
5: invokespecial #3 // Method com/example/zhanghongyang/blog02/TestJavaAnonymousInnerClass$1."<init>":(Lcom/example/zhanghongyang/blog02/TestJavaAnonymousInnerClass;)V
8: astore_1
9: aload_1
10: invokeinterface #4, 1 // InterfaceMethod java/lang/Runnable.run:()V
15: return

很简单,就是new了一个TestJavaAnonymousInnerClass$1对象,然后调用其run()方法。

有个比较有意思的,就是调用构造方法的时候先aload_0,0就是当前对象this,把this传过去了,这个就是匿名内部类可以持有外部类对象的秘密,其实把当前对象this引用给了人家。

再来看lambda的test():

public void test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=2, args_size=1
0: invokedynamic #2, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;
5: astore_1
6: aload_1
7: invokeinterface #3, 1 // InterfaceMethod java/lang/Runnable.run:()V
12: return

和匿名内部类不同,取而代之的是一个invokedynamic指令。

如果大家比较熟悉Java字节码方法调用相关,应该经常会看到一个问题:invokespecial,invokevirtual,invokeinterface,invokestatic,invokedynamic有和区别?

invokespecial 其实上面一段字节码上也出现了,一般指的是调用super方法,构造方法,private方法等;special嘛,指定的意思,调用的都是一些确定调用者的方法。

你可能会问,调用一个类的方法,调用者还能有不确定的时候?

有呀,比如重载,是不是能将父类的方法调用转而变成子类的?

所以类中非private成员方法,一般调用指令为invokevirtual。

invokeinterface,invokestatic字面意思理解就可以了。

这块大概解释是这样的,如果有困惑自己打字节码看就好了,例如抽象类抽象方法调用和接口方法调用指令一样吗?加了final修饰的方法不能被复写,指令会有变化吗?

最后一个就是invokedynamic了:

一般很罕见,今天我们也算是见到了,在Java lambda表达式的时候能够见到。

一些深入的研究,可以看这里:

每日一问 | Java中匿名内部类写成 lambda,真的只是语法糖吗?

我们现在知道使用了lambda表达式之后,和匿名内部类去比较,字节码有比较大的变化,那么更好奇了:

lambda表达式运行的时候,背后到底是什么样的呢?

三、lambda表达式不是真的没有内部类生成

想了解一段代码运行时状态,最简单的方式是什么呢?

嗯...debug?

现在IDE都越来越智能了,很多时候debug一些编译细节都给你抹去了。

有个比较简单的方式,打堆栈,我们修改下代码:

public class TestJavaLambda {
public void test() {
Runnable runnable = () -> {
System.out.println("hello java lambda");

int a = 1/0;
};
runnable.run();
}

public static void main(String[] args) {
new TestJavaLambda().test();
}
}

运行下,看下出错的堆栈:

hello java lambda
Exception in thread "main" java.lang.ArithmeticException: / by zero
at com.example.zhanghongyang.blog02.TestJavaLambda.lambda$test$0(TestJavaLambda.java:8)
at com.example.zhanghongyang.blog02.TestJavaLambda.test(TestJavaLambda.java:10)
at com.example.zhanghongyang.blog02.TestJavaLambda.main(TestJavaLambda.java:14)

看下到底和何方神圣调用的我们的run方法:

嗯...最后的堆栈是:

TestJavaLambda.lambda$test$0(TestJavaLambda.java:8)

是我们TestJavaLambda中的lambdatest0方法调用的?

是我们刚才发编译看漏了,还有这个方法?我们再反编译看下:

javap /Users/zhanghongyang/repo/KotlinLearn/app/src/main/java/com/example/zhanghongyang/blog02/TestJavaLambda.class 
Compiled from "TestJavaLambda.java"
public class com.example.zhanghongyang.blog02.TestJavaLambda {
public com.example.zhanghongyang.blog02.TestJavaLambda();
public void test();
public static void main(java.lang.String[]);
private void lambda$test$0();
}

这次javap -p 查看,-p代表private方法也输出出来。

还真有这个方法,看下这个方法的字节码:

private static void lambda$test$0();
descriptor: ()V
flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
Code:
stack=2, locals=0, args_size=0
0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #8 // String hello java lambda
5: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 7: 0
line 8: 8

很简单,就是我们上面lambda表达式{}里面的内容,打印一行日志。

那这个方法是test调用的?不对呀,这个堆栈好像有问题,我们在回头看下刚才堆栈:

Exception in thread "main" java.lang.ArithmeticException: / by zero
at com.example.zhanghongyang.blog02.TestJavaLambda.lambda$test$0(TestJavaLambda.java:8)
at com.example.zhanghongyang.blog02.TestJavaLambda.test(TestJavaLambda.java:10)
at com.example.zhanghongyang.blog02.TestJavaLambda.main(TestJavaLambda.java:14)

有没有发现这个堆栈太过于简单了,我们的Runnable.run的调用栈呢?

这个堆栈应该是被简化了,那我们再加一行日志,看下run()方法执行时,自己身处于哪个类?

我们在run方法里面加了一行

System.out.println(this.getClass().getCanonicalName());

看下输出:

com.example.zhanghongyang.blog02.TestJavaLambda

嗯..其实我们执行了一个废操作,当前这个方法里面的代码都被放到lambdatest0()了,当然输出是TestJavaLambda。

不行了,我要放大招了。

我们修改下方法,让这个进程活的久一点:

public void test() {
Runnable runnable = () -> {
System.out.println("hello java lambda");
System.out.println(this.getClass().getCanonicalName());
// 新增
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
int a = 1 / 0;
};
runnable.run();
}

运行后...

切到命令行,执行jps命令,查看当前程序进程的pid:

java zhanghongyang$ jps
99315 GradleDaemon
3682 TestJavaLambda
21298 Main
3685 Jps
3258 GradleDaemon
1275
3261 KotlinCompileDaemon

看到了3682,然后执行

jstack 3682

太感人了,终于把这行隐藏的run方法的堆栈找出来了。

这里大家不要太在意jps,jstack这些指令,都是jdk自带的,你就知道能查堆栈就行了,别出去搜这两个命令去啦,文章看完再说。
另外获取堆栈其实也能通过方法调用,小缘是通过Reflection.getCallerClass看的。

到现在我们具体真相又进了一步:

我们lambda$test$0()方法是这个对象:com.example.zhanghongyang.blog02.TestJavaLambda$$Lambda$1/1313922862的run方法调用的。

我们又能下个结论了:

文中lambda表达式的写法,在运行时,会帮我们生成中间类,类名格式为 原类名$$Lambda$数字,然后通过这个中间类最终完成调用。

那么你可能表示不服:

你说运行时生成就生成呀?你拿出来给我看看?

嗯...等会我拿出来给你看。

不过我们先思考另一个问题。

四、编译产物中遗漏的信息

上文我们一直在说:

  1. 对于文中例子中的Lambda表达式编译时没有生成中间类;
  2. 运行时帮我们生成了中间类;

那有个很明显的问题,编译时你没给我生成,运行时生成了;运行时它怎么知道要不要生成,生成什么样的类,你编译产物就那一个class文件,里面肯定要包含这类信息的呀?

是这么个道理。

我们再次发编译javap -v查看,在输出信息的最后:

SourceFile: "TestJavaLambda.java"
InnerClasses:
public static final #78= #77 of #81; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
BootstrapMethods:
0: #35 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
Method arguments:
#36 ()V
#37 invokespecial com/example/zhanghongyang/blog02/TestJavaLambda.lambda$test$0:()V
#36 ()V

果然包含一段信息,而且包含TestJavaLambda.lambda$test$0关键词。

大家不用管那么多,你就知道,文中lambda的例子,会在编译的class文件中新增一个方法lambdatest0(),并且会携带一段信息告知JVM在运行时创建一个中间class。

其实LambdaMetafactory.metafactory正是用来生成中间class的,jdk中也有相关类可以查看,后续我们再详细说这个。

五、把中间类拿出来看看?

我们一直说运行时帮我们生成了一个中间类,类名大概为:TestJavaLambda$$Lambda$1,但是口说无凭,得拿出来大伙才信,对吧。

还好不是说我吃了两碗凉粉...

我们刚才说了JVM帮我们生成了中间类,其实java在运行的时候可以带很多参数,其中有个系统属性很神奇,我用给你们看:

java -Djdk.internal.lambda.dumpProxyClasses com.example.zhanghongyang.blog02.TestJavaLambda

懂了吧,加上这个系统属性运行,可以dump出生成的类:

是不是有点意思。

其实动态代理中间也会生成代理类,也可以通过类似方式导出。

然后我们看看这个类呗,这个类我们就不太在乎细节了,直接AS里面看反编译之后的:

真简单...

所以,本文中的例子,Lambda表达式和匿名内部类的区别还是挺大的,大家只要了解:

  1. invokedynamic可以用于lambda;
  2. Java lambda表达式的中间类并不是没有,而是在首次运行时生成的。

至于性能问题,影响应该是微乎其微的,几乎没有的。

下面有个灵魂一问:

你看这些有啥用?

毕竟我是搞Android的,其实我更在乎Android中lambda的实现,所以就先以Java Lambda为开始了,至于你问我为啥要看Android Lambda实现,毕竟现在经常要字节码插抓桩,自定义Transform,对于一些类背后的行为还是要搞清楚的。

但是,大家一定要注意,本文讲的是 Java lambda 的原理。

不要套用到Android上! 不要套用到Android上! 不要套用到Android上!

那 Android Lambda 是怎么一回事,后续会单独写一篇,Android 脱糖与D8 的一些事儿,还想起来上次有个同事被Android Lambda 坑了一次,会一起写出来。

本文基于1.8.0_181。


收起阅读 »

Android 11 绕过反射限制

1. 问题出现的背景腾讯视频在集成我们 replay sdk 的时候发现这么个错误,导致整个 db mock 功能完全失效。Accessing hidden field Landroid/database/sqlite/SQLiteCursor; ->m...
继续阅读 »

1. 问题出现的背景

腾讯视频在集成我们 replay sdk 的时候发现这么个错误,导致整个 db mock 功能完全失效。

Accessing hidden field Landroid/database/sqlite/SQLiteCursor;
->mDriver:Landroid/database/sqlite/SQLiteCursorDriver; (greylist-max-o, reflection, denied)

java.lang.NoSuchFieldException: No field mDriver in class Landroid/database/sqlite/SQLiteCursor;
(declaration of 'android.database.sqlite.SQLiteCursor' appears in /system/framework/framework.jar)

我清晰的记得我们引入了一个第三方解决方案,在 9.0 以上已经解决了这个问题,大致的方案是这样的:

if (SDK_INT >= Build.VERSION_CODES.P) {
try {
Method forName = Class.class.getDeclaredMethod("forName", String.class);
Method getDeclaredMethod = Class.class.getDeclaredMethod("getDeclaredMethod", String.class, Class[].class);

Class<?> vmRuntimeClass = (Class<?>) forName.invoke(null, "dalvik.system.VMRuntime");
Method getRuntime = (Method) getDeclaredMethod.invoke(vmRuntimeClass, "getRuntime", null);
setHiddenApiExemptions = (Method) getDeclaredMethod.invoke(vmRuntimeClass, "setHiddenApiExemptions", new Class[]{String[].class});
sVmRuntime = getRuntime.invoke(null);
} catch (Throwable e) {
Log.e(TAG, "reflect bootstrap failed:", e);
}
}

吓得我赶紧去看下到底有没有猫腻,发现在 Android 11 上果然有问题:

Accessing hidden method Ldalvik/system/VMRuntime;
->setHiddenApiExemptions([Ljava/lang/String;)V (blacklist,core-platform-api, reflection, denied)

Caused by: java.lang.NoSuchMethodException: dalvik.system.VMRuntime.setHiddenApiExemptions [class [Ljava.lang.String;]
......

2. 分析问题出现的原因

本着时间紧任务重尽量不影响进度的情况下,我还是想去网上搜索看看,但是发现都是一堆旧的方案。迫不得已去看看到底为什么?到底为什么?刚好前几天找同事要了一份 Android 11 的源码。

static jobject Class_getDeclaredMethodInternal(JNIEnv* env, jobject javaThis, jstring name, jobjectArray args) {
// ……
Handle<mirror::Method> result = hs.NewHandle(
mirror::Class::GetDeclaredMethodInternal<kRuntimePointerSize>(
soa.Self(),
klass,
soa.Decode<mirror::String>(name),
soa.Decode<mirror::ObjectArray<mirror::Class>>(args),
GetHiddenapiAccessContextFunction(soa.Self())));
if (result == nullptr || ShouldDenyAccessToMember(result->GetArtMethod(), soa.Self())) {
return nullptr;
}
return soa.AddLocalReference<jobject>(result.Get());
}

如果 ShouldDenyAccessToMember 返回 true,那么就会返回 null,上层就会抛出方法找不到的异常。这里和 Android P 没什么不同,只是把 ShouldBlockAccessToMember 改了个名而已。 ShouldDenyAccessToMember 会调用到 hiddenapi::ShouldDenyAccessToMember,该函数是这样实现的:

template<typename T>
inline bool ShouldDenyAccessToMember(T* member,
const std::function<AccessContext()>& fn_get_access_context,
AccessMethod access_method)
REQUIRES_SHARED(Locks::mutator_lock_) {

const uint32_t runtime_flags = GetRuntimeFlags(member);

// 1:如果该成员是公开API,直接通过
if ((runtime_flags & kAccPublicApi) != 0) {
return false;
}

// 2:不是公开API(即为隐藏API),获取调用者和被访问成员的 Domain
// 主要看这个
const AccessContext caller_context = fn_get_access_context();
const AccessContext callee_context(member->GetDeclaringClass());

// 3:如果调用者是可信的,直接返回
if (caller_context.CanAlwaysAccess(callee_context)) {
return false;
}
// ......
}

原来的方案失效了能在 FirstExternalCallerVisitor 的 VisitFrame 方法中找到答案

bool VisitFrame() override REQUIRES_SHARED(Locks::mutator_lock_) {
ArtMethod *m = GetMethod();
......
ObjPtr<mirror::Class> declaring_class = m->GetDeclaringClass();
if (declaring_class->IsBootStrapClassLoaded()) {
......
// 如果 PREVENT_META_REFLECTION_BLACKLIST_ACCESS 为 Enabled,跳过来自 java.lang.reflect.* 的访问
// 系统对“套娃反射”的限制的关键就在此
ObjPtr<mirror::Class> proxy_class = GetClassRoot<mirror::Proxy>();
if (declaring_class->IsInSamePackage(proxy_class) && declaring_class != proxy_class) {
if (Runtime::Current()->isChangeEnabled(kPreventMetaReflectionBlacklistAccess)) {
return true;
}
}
}

caller = m;
return false;
}

3. 解决方案

  • native hook 住 ShouldDenyAccessToMember 方法,直接返回 false
  • 破坏调用堆栈绕过去,使 VM 无法识别调用方

我们采用的是第二种方案,有什么方法可以让 VM 无法识别我的调用栈呢?这可以通过 JniEnv::AttachCurrentThread(…) 函数创建一个新的 Thread 来完成。具体我们可以看下这里 developer.android.com/training/ar… ,然后配合 std::async(…) 与 std::async::get(..) 就能搞定了,下面是关键代码:

// java 层直接用 jni 调用这个方法
static jobject Java_getDeclaredMethod(
JNIEnv *env,
jclass interface,
jobject clazz,
jstring method_name,
jobjectArray params) {
// ...... 省掉一些转换代码
// 先用 std::async 调用 getDeclaredMethod_internal 方法
auto future = std::async(&getDeclaredMethod_internal, global_clazz,
global_method_name,
global_params);
auto result = future.get();
return result;
}

static jobject getDeclaredMethod_internal(
jobject clazz,
jstring method_name,
jobjectArray params) {
// 这里就是一些普通的 jni 操作了
JNIEnv *env = attachCurrentThread();
jclass clazz_class = env->GetObjectClass(clazz);
jmethodID get_declared_method_id = env->GetMethodID(clazz_class, "getDeclaredMethod",
"(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;");
jobject res = env->CallObjectMethod(clazz, get_declared_method_id,
method_name, params);
detachCurrentThread();
return env->NewGlobalRef(res);
}

JNIEnv *attachCurrentThread() {
JNIEnv *env;
// AttachCurrentThread 核心在这里
int res = _vm->AttachCurrentThread(&env, nullptr);
return env;
}
收起阅读 »

初识 Jetpack Compose(三) :修饰符(Modifier)

Modifier modifier elements装饰或添加行为到 Compose UI 元素的有序的、不可变的集合。例如,背景、填充和单击事件侦听器装饰行、文本或按钮或向其添加行为。 正如其名,modifier主要为Compose组件提供修饰功能,包括...
继续阅读 »

Modifier



modifier elements装饰或添加行为到 Compose UI 元素的有序的、不可变的集合。例如,背景、填充和单击事件侦听器装饰行、文本或按钮或向其添加行为。



正如其名,modifier主要为Compose组件提供修饰功能,包括但不限于 样式修改事件监听等一系列对Compose组件的装饰。


有序的


需要注意官方描述的“有序的”一词,由于Modifier的使用方式是链式的,所以属性定义的先后顺序会影响到UI的展示效果。
比如:


@Composable
fun RoundButton() {
Box(
modifier = Modifier
.width(300.dp)
.height(90.dp)
.background(Color(0xFF3ADF00), shape = RoundedCornerShape(50))
.padding(20.dp),
contentAlignment = Alignment.Center){
Text(text = "RoundButton",color = Color.White)
}
}

由于backgroundpadding前,所以Modifier会先给Box设置背景,然后再设置边距,如图:
1630476697(1).jpg
同理,将backgroundpadding顺序替换,效果则如下:


1630477241(1).jpg


通过查看Modifier的源码实现可以发现,在我们通过链式点点点叠加属性的过程中,Modifier会创建一个CombinedModifier将旧的和新的属性组合在一起,合成一个单独的Modifier,相当于给被修饰的组件套上了一层又一层的Modifier,这也是Modifier为有序的原因。


可拓展的


Modifier.padding()源码为例:


@Stable
fun Modifier.padding(all: Dp) =
this.then(
PaddingModifier(
start = all,
top = all,
end = all,
bottom = all,
rtlAware = true,
inspectorInfo = debugInspectorInfo {
name = "padding"
value = all
}
)
)

可以发现,padding()Modifier的一个拓展函数,它调用了Modifier的 then() 函数,而这个 then() 需要接收一个Modifier对象,而PaddingModifier就是这个对象。


所以我们也当然可以为Modifier添加拓展函数,传入我们自定义的Modifier来达到一些特定的效果,比如以下这些属性我们会经常为不同的组件配置:


modifier = Modifier
.width(300.dp)
.height(90.dp)
.padding(20.dp)
.background(Color(0xFF3ADF00), RoundedCornerShape(50))
复制代码

为了方便调用,我们可以给Modifier添加一个拓展方法:


@Stable
fun Modifier.buttonDefault() = this.then(
width(300.dp)
.height(90.dp)
.padding(20.dp)
.background(Color(0xFF3ADF00), RoundedCornerShape(50))
)

这样在组件中,我们只需要这样使用就可以了。


modifier = Modifier.buttonDefault()
复制代码

Modifier 属性


Modifier可配置的属性多且杂,这里不一一列举了,具体可前往Andrid Developer查看




以下是根据职能分类列出的一些常用属性


宽&高



























































属性名含义
Modifier.width(width: Dp)设置自身的宽度,单位dp
Modifier.fillMaxWidth(fraction: Float = 1f)默认横向填充满父容器的宽度,参数可以控制宽度的比例。例如0.5就是当前元素占父元素宽度的一半
Modifier.wrapContentWidth(align: Alignment.Horizontal = Alignment.CenterHorizontally, unbounded: Boolean = false)根据子级元素的宽度来确定自身的宽度,如果自身设置了最小宽度的话则会被忽略。当unbounded参数为true的时候,自身设置了最大宽度的话也会被忽略
-----------------------------------------------------------------------------
Modifier.height(height: Dp)设置自身的高度,单位dp
Modifier.fillMaxHeight(fraction: Float = 1f)默认纵向填充满父容器的宽度,参数可以控制宽度的比例。例如0.5就是当前元素占父元素高度的一半
Modifier.wrapContentHeight(align: Alignment.Vertical = Alignment.CenterVertically, unbounded: Boolean = false)根据子级元素的高度来确定自身的高度,如果自身设置了最小高度的话则会被忽略。当unbounded参数为true的时候,自身设置了最大高度的话也会被忽略
-----------------------------------------------------------------------------
Modifier.size(size: Dp)设置自的宽高,单位dp
Modifier.size(width: Dp, height: Dp)设置自的宽高,单位dp
Modifier.fillMaxSize(fraction: Float = 1f)默认填充满父容器,参数可以控制比例。例如0.5就是当前元素占父元素的一半
Modifier.wrapContentSize(align: Alignment = Alignment.Center, unbounded: Boolean = false)根据子级元素的宽高来确定自身的宽高,如果自身设置了最小宽高的话则会被忽略。当unbounded参数为true的时候,自身设置了最大宽高的话也会被忽略

间距



























属性名含义
Modifier.padding(start: Dp = 0.dp, top: Dp = 0.dp, end: Dp = 0.dp, bottom: Dp = 0.dp)分别在四个方向上设置填充
Modifier.padding(horizontal: Dp = 0.dp, vertical: Dp = 0.dp)分别在横向和纵向上设置填充
Modifier.padding(all: Dp)统一设置所有方向上的填充
Modifier.padding(padding: PaddingValues)根据参数PaddingValues来设置填充,PaddingValues参数可以理解为以上三种方式的封装

绘制



































属性名含义
Modifier.alpha(alpha: Float)不透明度,范围从0-1
Modifier.clip(shape: Shape)裁剪为相应的形状,例如shape = RoundedCornerShape(20) 表示裁剪为20%圆角的矩形。
Modifier.shadow(elevation: Dp, shape: Shape = RectangleShape, clip: Boolean = elevation > 0.dp)绘制阴影效果
Modifier.rotate(degrees: Float)设置视图围绕其中心旋转的角度
Modifier.scale(scale: Float)设置视图的缩放比例
Modifier.scale(scaleX: Float, scaleY: Float)设置视图的缩放比例

背景&边框































属性名含义
Modifier.background(color: Color, shape: Shape = RectangleShape)设置背景色
Modifier.background(brush: Brush, shape: Shape = RectangleShape, alpha: Float = 1.0f)使用Brush来设置背景色,例如渐变色效果
Modifier.border(border: BorderStroke, shape: Shape = RectangleShape)绘制指定形状的边框
Modifier.border(width: Dp, color: Color, shape: Shape = RectangleShape)绘制指定宽度、颜色、形状的边框
Modifier.border(width: Dp, brush: Brush, shape: Shape)绘制指定宽度、brush、形状的边框

行为



























属性名含义
Modifier.clickable(  enabled: Boolean = true, onClickLabel: String? = null, role: Role? =null,  onClick: () -> Unit)点击事件
Modifier.combinedClickable( enabled: Boolean = true,onClickLabel: String? = null,role: Role? = null,onLongClickLabel: String? = null,onLongClick: () -> Unit = null,onDoubleClick: () -> Unit = null,onClick: () -> Unit)组合点击事件,包括单击、长按、双击
Modifier.horizontalScroll(state: ScrollState, enabled: Boolean = true, reverseScrolling: Boolean = false)使组件支持横向滚动模式
Modifier.verticalScroll(state: ScrollState, enabled: Boolean = true, reverseScrolling: Boolean = false)使组件支持纵向滚动模式

二、最后


好记性不如烂笔头,初识 Jetpack Compose 系列是我自己的学习笔记,在加深知识巩固的同时,也可以锻炼一下写作技能。文章中的内容仅作参考,如有问题请留言指正。


1. 参考



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

初识 Jetpack Compose(五) :组件-Text

一、Text Compose中的Text的作用与 xml 中的TextView无二,作用于最基本的文本显示。 1.属性 @Composable fun Text( // text: String, text: AnnotatedString, ...
继续阅读 »

一、Text


Compose中的Text的作用与 xml 中的TextView无二,作用于最基本的文本显示。


1.属性


@Composable
fun Text(
// text: String,
text: AnnotatedString,
modifier: Modifier = Modifier,
color: Color = Color.Unspecified,
fontSize: TextUnit = TextUnit.Unspecified,
fontStyle: FontStyle? = null,
fontWeight: FontWeight? = null,
fontFamily: FontFamily? = null,
letterSpacing: TextUnit = TextUnit.Unspecified,
textDecoration: TextDecoration? = null,
textAlign: TextAlign? = null,
lineHeight: TextUnit = TextUnit.Unspecified,
overflow: TextOverflow = TextOverflow.Clip,
softWrap: Boolean = true,
maxLines: Int = Int.MAX_VALUE,
inlineContent: Map<String, InlineTextContent> = mapOf(),
onTextLayout: (TextLayoutResult) -> Unit = {},
style: TextStyle = LocalTextStyle.current
) {
...
}



























































































属性名类型作用
textString要显示的文本
textAnnotatedString要显示的文本,可设置文本中指定字符的颜色、字体、大小等属性
modifierModifier修饰符
fontSizeTextUnit文本字体大小
fontStyleFontStyle?文本字体变体
fontWeightfontWeight?文本字体字重
fontFamilyFontFamily?文本字体
letterSpacingTextUnit文本字符之间的间距
textDecorationTextDecoration?用于为文本绘制装饰(例如:下划线、删除线)
textAlignTextAlign?文本段落的对齐方式
lineHeightTextUnit文本段落之间的行高
overflowTextOverflow文本字符溢出的显示方式 ,同xml ellipsize
softWrapBoolean文本是否应在换行符处中断。如果为false,则文本的宽度会在水平方向上无限延伸,且textAlign属性失效,可能会出现异常情况
maxLinesInt文本可跨越的可选最大行数,必要时可以换行。如果文本超过给定的行数,则会根据textAlignsoftWrap属性截断文本。它的值必须大于零。
onTextLayout(TextLayoutResult) -> Unit计算新的文本布局时执行的回调
styleTextStyle文本的样式配置,例如颜色,字体,行高等。可参考主题-排版 使用

2.使用示例


2.1 示例一


@Composable
fun TextExampleMoney(value:Float){
var str = value.toString()
if(!str.contains("."))
str+=".00"
var strSplit = str.split(".")

val annotatedStringBuilder = AnnotatedString.Builder()
annotatedStringBuilder.apply {
pushStyle(
SpanStyle(
color = Color.Gray,
fontSize = 16.sp,
)
)
append("¥")
pop()
pushStyle(
SpanStyle(
color = Color.DarkGray,
fontSize = 26.sp,
fontWeight = FontWeight.Bold
)
)
append(strSplit[0])
pop()
pushStyle(
SpanStyle(
color = Color.Gray,
fontSize = 16.sp,
fontWeight = FontWeight.Bold
)
)
append(".${strSplit[1]}")
pop()
}

Text(
text = annotatedStringBuilder.toAnnotatedString(),
)

}

//调用
TextExampleMoney(98.99f)

1630916637(1).jpg


2.2 示例二


@Composable
fun TextExample(){
val annotatedStringBuilder = AnnotatedString.Builder()
annotatedStringBuilder.apply {
append("Jetpack Compose ")
pushStyle(
SpanStyle(
color = Color.Blue,
fontSize = 16.sp,
)
)
append("widget")
pop()
append(" [ ")
pushStyle(
SpanStyle(
color = Color.Red,
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
)
append("Text")
pop()
append(" ] usage example")
}

Text(
text = annotatedStringBuilder.toAnnotatedString(),
fontSize = 16.sp,
overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Center
)

}

//调用
TextExample()

1630916734(1).jpg


2.3 示例三


Text(
text = "High level element that displays text and provides semantics / accessibility information.\n" +
"\n" +
"The default style uses the LocalTextStyle provided by the MaterialTheme / components. If you are setting your own style, you may want to consider first retrieving LocalTextStyle, and using TextStyle.copy to keep any theme defined attributes, only modifying the specific attributes you want to override.\n" +
"\n" +
"For ease of use, commonly used parameters from TextStyle are also present here. The order of precedence is as follows:\n" +
"\n" +
"If a parameter is explicitly set here (i.e, it is notnull or TextUnit.Unspecified), then this parameter will always be used.\n" +
"\n" +
"If a parameter is not set, (null or TextUnit.Unspecified), then the corresponding value from style will be used instead.\n" +
"\n" +
"Additionally, for color, if color is not set, and style does not have a color, then LocalContentColor will be used with an alpha of LocalContentAlpha- this allows this Text or element containing this Text to adapt to different background colors and still maintain contrast and accessibility.",
fontSize = 16.sp,
overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Start,
style = MaterialTheme.typography.Description,
maxLines = 20,
lineHeight = 20.sp,
textDecoration = TextDecoration.Underline
)

image.png


二、ClickableText


在一些开发需求中,我们需要监听一个Text的某一段区域的Touch事件,比如:

请阅读并同意《xxx用户使用协议》,我们需要监听书名号内的文字的点击事件并进行跳转,ClickableText可以很轻松的帮我们做到这一点。


@Composable
fun ClickableText(
text: AnnotatedString,
modifier: Modifier = Modifier,
style: TextStyle = TextStyle.Default,
softWrap: Boolean = true,
overflow: TextOverflow = TextOverflow.Clip,
maxLines: Int = Int.MAX_VALUE,
onTextLayout: (TextLayoutResult) -> Unit = {},
//点击事件监听,参数为触发事件时点击的字符在文本中的位置
onClick: (Int) -> Unit
) {
...
}

通过源码得知,ClickableText区别于Text外,额外提供了onClick回调,会返回我们点击文本时,手指所点击的字符在文本中的位置。
比如:


ClickableText(
text = annotatedStringBuilder.toAnnotatedString(),
overflow = TextOverflow.Ellipsis
){
Log.d("ClickableText", "$it -> character is clicked.")
}

image.png


预计在未来还会提供LongClickableText:文本的长按事件,不过目前暂未支持。


三、最后


好记性不如烂笔头,初识 Jetpack Compose 系列是我自己的学习笔记,在加深知识巩固的同时,也可以锻炼一下写作技能。文章中的内容仅作参考,如有问题请留言指正。


1. 参考



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

【开源项目】简单易用的Compose版骨架屏,了解一下~

前言 骨架屏是页面的一个空白版本,通常会在页面完全渲染之前,通过一些灰色的区块大致勾勒出轮廓,待数据加载完成后,再替换成真实的内容。骨架屏加载中效果,比起传统的加载中效果可以提供更多信息,用户体验更好,因此也变得越来越流行 本文主要介绍如何使用Compose实...
继续阅读 »

前言


骨架屏是页面的一个空白版本,通常会在页面完全渲染之前,通过一些灰色的区块大致勾勒出轮廓,待数据加载完成后,再替换成真实的内容。骨架屏加载中效果,比起传统的加载中效果可以提供更多信息,用户体验更好,因此也变得越来越流行

本文主要介绍如何使用Compose实现一个简单易用的骨架屏效果,有兴趣的同学可以点个StarCompose版骨架屏


效果图


首先看下最终的效果图



特性



  1. 简单易用,可复用页面UI,不需要针对骨架屏定制UI

  2. 支持设置骨架屏是否显示,一般结合加载状态使用

  3. 支持设置骨架屏背景与高亮颜色

  4. 支持设置骨架屏高度部分宽度,渐变部分宽度

  5. 支持设置骨架屏动画的角度与方向

  6. 支持设置骨架屏动画的时间与两次动画间隔


使用


接入


第 1 步:在工程的build.gradle中添加:


allprojects {
repositories {
...
mavenCentral()
}
}

第2步:在应用的build.gradle中添加:


dependencies {
implementation 'io.github.shenzhen2017:shimmer:1.0.0'
}

简单使用


@Composable
fun ShimmerSample() {
var loading: Boolean by remember {
mutableStateOf(true)
}
Column(
modifier = Modifier
.fillMaxWidth()
.shimmer(loading,config = ShimmerConfig())
) {
repeat(3) {
PlaceHolderItem()
Spacer(modifier = Modifier.height(10.dp))
}
}
}

如上所示:



  1. 只需要在ColumnModifier中加上shimmerColumn下的所有组件即可实现骨架屏效果

  2. 可通过loading参数,控制骨架屏效果是否显示

  3. 如果需要定制骨架屏动画效果,也可通过一些参数配置


具体主要有以下这些参数


data class ShimmerConfig(
// 未高亮部分颜色
val contentColor: Color = Color.LightGray.copy(alpha = 0.3f),
// 高亮部分颜色
val higLightColor: Color = Color.LightGray.copy(alpha = 0.9f),
// 渐变部分宽度
@FloatRange(from = 0.0, to = 1.0)
val dropOff: Float = 0.5f,
// 高亮部分宽度
@FloatRange(from = 0.0, to = 1.0)
val intensity: Float = 0.2f,
//骨架屏动画方向
val direction: ShimmerDirection = ShimmerDirection.LeftToRight,
//动画旋转角度
val angle: Float = 20f,
//动画时长
val duration: Float = 1000f,
//两次动画间隔
val delay: Float = 200f
)

主要原理


通过图像混合模式复用页面UI


如果我们要实现骨架屏效果,首先想到的是需要按照页面的结构再写一套UI,然后在加载中的时候,显示这套UI,否则隐藏

一般的加载中效果都是这样实现的,但这样会带来一个问题,不同的页面结构不同,那我们岂不是要一个页面就重写一套UI?这显然是不可接受的


我们可以想到,页面的结构其实我们已经写过一遍了,如果我们能复用我们写的页面结构不就好了吗?

我们可以通过图像混合模式来实现这一点


图像混合模式定义的是,当两个图像合成时,图像最终的展示方式。在Androd中,有相应的API接口来支持图像混合模式,即Xfermode.

图像混合模式主要有以下16种,以下这张图片从一定程度上形象地说明了图像混合的作用,两个图形一圆一方通过一定的计算产生不同的组合效果,具体如下



我们介绍几个常用的,其它的感兴趣的同学可自行查阅



  • SRC_IN:只在源图像和目标图像相交的地方绘制【源图像】

  • DST_IN:只在源图像和目标图像相交的地方绘制【目标图像】,绘制效果受到源图像对应地方透明度影响

  • SRC_OUT:只在源图像和目标图像不相交的地方绘制【源图像】,相交的地方根据目标图像的对应地方的alpha进行过滤,目标图像完全不透明则完全过滤,完全透明则不过滤

  • DST_OUT:只在源图像和目标图像不相交的地方绘制【目标图像】,在相交的地方根据源图像的alpha进行过滤,源图像完全不透明则完全过滤,完全透明则不过滤


如果我们把页面的UI结构作为目标图像,骨架屏效果作为源图像,然后使用SRC_IN混合模式

就可以实现只在页面的结构上显示骨架屏,在空白部分不显示,这样就可以避免重复写UI


通过平移实现动画效果


上面我们已经实现了在页面结构上显示骨架屏,但是骨架屏效果还有一个动画效果

其实也很简单,给骨架屏设置一个渐变效果,然后做一个平移动画,然后看起来就是现在的骨架屏闪光动画了


fun Modifier.shimmer(): Modifier = composed {
var progress: Float by remember { mutableStateOf(0f) }
val infiniteTransition = rememberInfiniteTransition()
progress = infiniteTransition.animateFloat().value // 动画效果,计算百分比
ShimmerModifier(visible = visible, progress = progress, config = config)
}

internal class ShimmerModifier(progress:Float) : DrawModifier, LayoutModifier {
private val paint = Paint().apply {
blendMode = BlendMode.SrcIn //设置混合模式
shader = LinearGradientShader(Offset(0f, 0f),toOffset,colors,colorStops)//设置渐变色
}

override fun ContentDrawScope.draw() {
drawContent()
val (dx, dy) = getOffset(progress) //根据progress,设置平移的位置
paint.shader?.postTranslate(dx, dy) // 平移操作
it.drawRect(Rect(0f, 0f, size.width, size.height), paint = paint)//绘制骨架屏效果
}
}

如上所示,主要是几步:



  1. 启动动画,获得当前进度progress,并根据progress获得当前平移的位置

  2. 设置骨架屏的背景渐变颜色与混合模式

  3. 绘制骨架屏效果


自定义骨架屏效果


上面介绍了我们提供了一些参数,可以自定义骨架屏的效果,其它参数都比较好理解,主要是以下两个参数有点难理解



  1. dropOff:渐变部分宽度

  2. intensity: 高亮部分宽度


我们知道,可以通过contentColor自定义普通部分颜色,higLightColor自定义高亮部分颜色

但是这两种颜色是如何分布的呢?渐变的比例是怎样的呢?可以看下下面的代码:


    private val paint = Paint().apply {
shader = LinearGradientShader(Offset(0f, 0f),toOffset,colors,colorStops)//设置渐变色
}

private val colors = listOf(
config.contentColor,
config.higLightColor,
config.higLightColor,
config.contentColor
)

private val colorStops: List<Float> = listOf(
((1f - intensity - dropOff) / 2f).coerceIn(0f, 1f),
((1f - intensity - 0.001f) / 2f).coerceIn(0f, 1f),
((1f + intensity + 0.001f) / 2f).coerceIn(0f, 1f),
((1f + intensity + dropOff) / 2f).coerceIn(0f, 1f)
)

可以看出,我们的颜色渐变有以下特点:



  1. 渐变颜色分布为:contentColor->higLightColor->higLightColor->contentColor

  2. LinearGradientShader使用colors定义颜色,colorStops定义颜色渐变的分布,colorStopsintensitydropoff计算得来

  3. intensity决定了高亮部分的宽度,即intensity越大,高亮部分越大

  4. dropOff决定了渐变部分的宽度,即dropOff越大,渐变部分越大


总结


特别鸣谢


在实现Compose版本骨架屏的过程中,主要借鉴了以下开源框架的思想,有兴趣的同学也可以了解下

Facebook开源的shimmer-android

Habib Kazemi开源的compose-shimmer


项目地址


简单易用的Compose版骨架屏

开源不易,如果项目对你有所帮助,欢迎点赞,Star,收藏~


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

iOS - swift常用的关键词解释和用法

deinit: 当一个类的实例即将被销毁时,会调用这个方法。class Person { var name:String var age:Int var gender:String deinit {...
继续阅读 »

deinit: 当一个类的实例即将被销毁时,会调用这个方法。

class Person  
{
var name:String
var age:Int
var gender:String

deinit
{
//从堆中释放,并释放的资源
}
}

extension:允许给已有的类、结构体、枚举、协议类型,添加新功能。

class Person  
{
var name:String = ""
var age:Int = 0
var gender:String = ""
}

extension Person
{
func printInfo()
{
print("My name is \(name), I'm \(age) years old and I'm a \(gender).")
}
}

inout:将一个值传入函数,并可以被函数修改,然后将值传回到调用处,来替换初始值。适用于引用类型和值类型。 其实就是声明参数为指针

func dangerousOp(_ error:inout NSError?)  
{
error = NSError(domain: "", code: 0, userInfo: ["":""])
}

var potentialError:NSError?
dangerousOp(&potentialError)

//代码运行到这里,potentialError 不再是 nil,而是已经被初始化

internal:访问控制权限,允许同一个模块下的所有源文件访问,如果在不同模块下则不允许访问。


public:可以被任何人访问。但其他 module 中不可以被 override 和继承,而在 module 内可以被 override 和继承。


open:访问控制权限,允许在定义的模块外也可以访问源文件里的所有类,并进行子类化。对于类成员,允许在定义的模块之外访问和重写。

open var foo:String? //这个属性允许在 app 内或 app 外重写和访问。在开发框架的时候,会应用到这个访问修饰符。

private:访问控制权限,只允许实体在定义的类以及相同源文件内的 extension 中访问。

class Person  
{
private var jobTitle:String = ""
}

// 当 extension 和 class 不在同一个源文件时
extension Person
{
// 无法编译通过,只有在同一个源文件下才可以访问
func printJobTitle()
{
print("My job is \(jobTitle)")
}
}

fileprivate:访问控制权限,只允许在定义源文件中访问。// 同文件中访问

class Person  
{
fileprivate var jobTitle:String = ""
}

extension Person
{
//当 extension 和 class 在同一个文件中时,允许访问
func printJobTitle()
{
print("My job is (jobTitle)")
}
}


从高到低排序如下:

open > public > interal > fileprivate > private



static:用于定义类方法,在类型本身进行调用。此外还可以定义静态成员。

class Person  
{
var jobTitle:String?

static func assignRandomName(_ aPerson:Person)
{
aPerson.jobTitle = "Some random job"
}
}

let somePerson = Person()
Person.assignRandomName(somePerson)
//somePerson.jobTitle 的值是 "Some random job"


在方法的 func 关键字之前加上关键字 static 或者 class 都可以用于指定类方法.不同的是用class关键字指定的类方法可以被子类重写, 如下:

override class func work() { print("Teacher: University Teacher")}

但是用 static 关键字指定的类方法是不能被子类重写的, 根据报错信息: Class method overrides a 'final' class method.



我们可以知道被 static 指定的类方法包含 final 关键字的特性--防止被重写.


struct:通用、灵活的结构体,是程序的基础组成部分,并提供了默认初始化方法。与 class 不同,当 struct 在代码中被传递时,是被拷贝的,并不使用引用计数。除此之外,struct 没有下面的这些功能:



  • 使用继承。

  • 运行时的类型转换。

  • 使用析构方法。



数据类型:struct是值类型,class是引用类型。

值类型变量直接包含数据,赋值时也是值拷贝,或者叫深拷贝,所以多个变量的操作不会相互影响。

引用类型变量存储的是对数据的引用地址,后者称为对象,赋值时,是将对象的引用地址复制过去,也叫浅拷贝,因此若多个变量指向同一个对象时,操作会相互影响。

值类型数据没有引用计数,也就不会因为循环引用导致内存泄漏,而引用类型存在引用计数,需要小心循环引用导致的内存泄漏

拷贝时,struct是深拷贝,拷贝的是内容,class则需要选用正确的深浅拷贝类型。



因为值类型数据是深拷贝,所以是线程安全的,而引用类型数据则不是



  • property的初始化:初始化属性时,class 需要创建一个带形参的constructor;struct可以把属性放在默认的constructor 的参数里。

  • immutable变量:swift用var和let区分可变数据和不可变数据,struct遵循这个特性;对class则不适用。

  • mutating function:struct 的 function 改变 property 时,需加上 mutating,而 class 不用。

  • 速度:struct分配在栈中,class分配在堆中,也就意味着struct更迅速。

  • NSUserDefaults:struct 不能被序列化成 NSData 对象,class可以。

  • 继承: struct不可以继承,class可以继承。

  • swift与oc混合开发时,oc调用swift需要继承NSObject,这就导致了class可以继承,所以可以调用class,但struct不能继承,所以不能调用struct


typealias:给代码中已经存在的类,取别名。

typealias JSONDictionary = [String: AnyObject]

func parseJSON(_ deserializedData:JSONDictionary){}

defer:用于在程序离开当前作用域之前,执行一段代码。 // dafer 是倒叙 先加入后执行




  • 关闭文件

    func foo() {
    let fileDescriptor = open(url.path, O_EVTONLY)
    defer {
    close(fileDescriptor)
    }
    // use fileDescriptor...
    }



  • 加/解锁:下面是 swift 里类似 Objective-C 的 synchronized block 的一种写法,可以使用任何一个 NSObject 作 lock

    func foo() {
    objc_sync_enter(lock)
    defer {
    print("003")
    objc_sync_exit(lock)
    }
    defer {
    print("002")
    }
    print("001")
    // do something...
    }



defer 的执行时机:

defer 的执行时机紧接在离开作用域之后,但是是在其他语句之前。这个特性为 defer 带来了一些很“微妙”的使用方式。比如从 0 开始的自增:

class Foo {
var num = 0
func foo() -> Int {
defer { num += 1 }
return num
}

// 没有 `defer` 的话我们可能要这么写
// func foo() -> Int {
// num += 1
// return num - 1
// }
}

let f = Foo()
f.foo() // 0
f.foo() // 1
f.num // 2

fallthrough:显式地允许从当前 case 跳转到下一个相邻 case 继续执行代码。

let box = 1

switch box
{
case 0:
print("Box equals 0")
fallthrough
case 1:
print("Box equals 0 or 1")
default:
print("Box doesn't equal 0 or 1")
// Box equals 0 和 Box equals 0 or 1 都执行了
}

guard:当有一个以上的条件不满足要求时,将离开当前作用域。同时还提供解包可选类型的功能。

private func printRecordFromLastName(userLastName: String?)
{
guard let name = userLastName, name != "Null" else
{
//userLastName = "Null",需要提前退出
return
}
//继续执行代码
print(dataStore.findByLastName(name))
}


1.guard关键字必须使用在函数中。

2.guard关键字必须和else同时出现。

3.guard关键字只有条件为false的时候才能走else语句 相反执行后边语句。



repeat:在使用循环的判断条件之前,先执行一次循环中的代码。类似于 do while 循环


repeat

{

print("Always executes at least once before the condition is considered")

}

while 1 > 2


where:要求关联类型必须遵守特定协议,或者类型参数和关联类型必须保持一致。也可以用于在 case 中提供额外条件,用于满足控制表达式。



  • 增加判断条件
for i in 0…3 where i % 2 == 0  
{
print(i) //打印 0 和 2
}


  • 协议使用where, 只有基类实现了当前协议才能添加扩展。 换个说法, 多个类实现了同一个协议,该语法根据类名分别为这些类添加扩展, 注意是分别(以类名区分)!!!
protocol SomeProtocol {
func someMethod()
}
class A: SomeProtocol {
let a = 1
func someMethod() {
print("call someMethod")
}
}
class B {
let a = 2
}
//基类A继承了SomeProtocol协议才能添加扩展
extension SomeProtocol where Self: A {
func showParamA() {
print(self.a)
}
}
//反例,不符合where条件
extension SomeProtocol where Self: B {
func showParamA() {
print(self.a)
}
}
let objA = A()
let objB = B() //类B没实现SomeProtocol, 所有没有协议方法
objA.showParamA() //输出1

as:类型转换运算符,用于尝试将值转成其它类型。




  • as : 数值类型转换

    let age = 28 as Int
    let money = 20 as CGFloat
    let cost = (50 / 2) as Double
    switch person1 { 
    case let person1 as Student:
    print("是Student类型,打印学生成绩单...")
    case let person1 as Teacher:
    print("是Teacher类型,打印老师工资单...")
    default: break
    }



  • as!:向下转型(Downcasting)时使用。由于是强制类型转换,如果转换失败会报 runtime 运行错误。

    let a = 13 as! String
    print(a)
    //会crash
    let a = 13 as? String
    print(a)
    //输出为nil



is:类型检查运算符,用于确定实例是否为某个子类类型。

class Person {}  
class Programmer : Person {}
class Nurse : Person {}

let people = [Programmer(), Nurse()]

for aPerson in people
{
if aPerson is Programmer
{
print("This person is a dev")
}
else if aPerson is Nurse
{
print("This person is a nurse")
}
}

nil:在 Swift 中表示任意类型的无状态值。



Swift的nil和OC中的nil不一样.在OC中,nil是一个指向不存在对象的指针.而在Swift中,nil不是指针,它是一个不确定的值.用来表示值缺失.任何类型的optional都可以被设置为nil. 而在OC中,基本数据类型和结构体是不能被设置为nil的. 给optional的常量或者变量赋值为nil.来表示他们的值缺失情况.一个optional常量或者变量如果在初始化的时候没有被赋值,他们自动会设置成nil.

class Person{}  
struct Place{}

//任何 Swift 类型或实例可以为 nil
var statelessPerson:Person? = nil
var statelessPlace:Place? = nil
var statelessInt:Int? = nil
var statelessString:String? = nil

super:在子类中,暴露父类的方法、属性、下标。

class Person  
{
func printName()
{
print("Printing a name. ")
}
}

class Programmer : Person
{
override func printName()
{
super.printName()
print("Hello World!")
}
}

let aDev = Programmer()
aDev.printName() //打印 Printing a name. Hello World!

self:任何类型的实例都拥有的隐式属性,等同于实例本身。此外还可以用于区分函数参数和成员属性名称相同的情况。

class Person  
{
func printSelf()
{
print("This is me: \(self)")
}
}

let aPerson = Person()
aPerson.printSelf() //打印 "This is me: Person"

Self:在协议中,表示遵守当前协议的实体类型。

protocol Printable  
{
func printTypeTwice(otherMe:Self)
}

struct Foo : Printable
{
func printTypeTwice(otherMe: Foo)
{
print("I am me plus \(otherMe)")
}
}

let aFoo = Foo()
let anotherFoo = Foo()

aFoo.printTypeTwice(otherMe: anotherFoo) //打印 I am me plus Foo()

_:用于匹配或省略任意值的通配符。

for _ in 0..<3  
{
print("Just loop 3 times, index has no meaning")
}

另外一种用法:

let _ = Singleton() //忽略不使用的变量

convenience:


在 Swift 中,为保证安全性,init 方法只能调用一次,且在 init 完成后,保证所有非 Optional 的属性都已经被初始化。


每个类都有指定的初始化方法:designated initializer,这些初始化方法是子类必须调用的,为的就是保证父类的属性都初始化完成了。


而如果不想实现父类的 designated initializer,可以添加 convenience 关键字,自己实现初始化逻辑。

convenience 初始化不能调用父类的初始化方法,只能调用同一个类中的 designated initializer。

由于 convenience 初始化不安全,所以 Swift 不允许 convenience initializer 被子类重写,限制其作用范围。

class People {
var name: String
init(name: String) {
self.name = name
}
}

通过extension给原有的People类增加init方法:

// 使用convenience增加init方法
extension People {
convenience init(smallName: String) {
self.init(name: smallName)
}
}

接下来,Student类继承父类People

class Student: People {
var grade: Int

init(name: String, grade: Int) {
self.grade = grade
super.init(name: name)
// 无法调用
// super.init(smallName: name)
}

// 可以被重写
override init(name: String) {
grade = 1
super.init(name: name)
}

// 无法重写,编译不通过
override init(smallName: String) {
grade = 1
super.init(smallName: smallName)
}
}

子类对象调用父类的convenience的init方法:只要在子类中实现重写了父类convenience方法所需要的init方法的话,我们在子类中就可以使用父类的convenience初始化方法了

class People {

var name: String

init(name: String) {
self.name = name
}
}
// 使用convenience增加init方法
extension People {
convenience init(smallName: String) {
self.init(name: smallName)
}
}


// 子类
class Teacher: People {

var course: String

init(name: String, course: String) {
self.course = course
super.init(name: name)
}

override init(name: String) {
self.course = "math"
super.init(name: name)
}
}

// 调用convenience的init方法
let xiaoming = Teacher(smallName: "xiaoming")


  • 总结:子类的designated初始化方法必须调用父类的designated方法,以保证父类也完成初始化。


required


对于某些我们希望子类中一定实现的designated初始化方法,我们可以通过添加required关键字进行限制,强制子类对这个方法重写。

required修饰符的使用规则:



  • required修饰符只能用于修饰类初始化方法。

  • 当子类含有异于父类的初始化方法时(初始化方法参数类型和数量异于父类),子类必须要实现父类的required初始化方法,并且也要使用required修饰符而不是override。

  • 当子类没有初始化方法时,可以不用实现父类的required初始化方法。
    class MyClass {
var str:String
required init(str:String) {
self.str = str
}
}
class MySubClass:MyClass
{
init(i:Int) {
super.init(str:String(i))
}

}
// 编译错误
MySubClass(i: 123)

会报错,因为你没有实现父类中必须实现的方法。正确的写法:


    class MyClass {
var str:String
required init(str:String) {
self.str = str
}
}
class MySubClass:MyClass
{
init(i:Int) {
super.init(str:String(i))
}
required init(str: String) {
fatalError("init(str:) has not been implemented")
}
}
// 编译错误
MySubClass(i: 123)

从上面的代码中,不难看出子类需要添加异于父类的初始化方法,必须要重写有required的修饰符的初始化方法,并且也要使用required修饰符而不是override,请千万注意!


如果子类中并没有不同于父类的初始化方法,Swift会默认使用父类的初始化方法:

class MyClass{
var str: String?
required init(str: String?) {
self.str = str
}
}
class MySubClass: MyClass{
}
var MySubClass(str: "hello swift")

在这种情况下,编译器不会报错,因为如果子类没有任何初始化方法时,Swift会默认使用父类的初始化方法。


以#开头的关键字


#available:基于平台参数,通过 if,while,guard 语句的条件,在运行时检查 API 的可用性。

if #available(iOS 10, *)  
{
print("iOS 10 APIs are available")
}

#colorLiteral:在 playground 中使用的字面表达式,用于创建颜色选取器,选取后赋值给变量。

let aColor = #colorLiteral //创建颜色选取器

#column:一种特殊的字面量表达式,用于获取字面量表示式的起始列数。

class Person  
{
func printInfo()
{
print("Some person info - on column \(#column)")
}
}

let aPerson = Person()
aPerson.printInfo() //Some person info - on column 47

#function:特殊字面量表达式,返回函数名称。在方法中,返回方法名。在属性的 getter 或者 setter 中,返回属性名。在特殊的成员中,比如 init 或 subscript 中,返回关键字名称。在文件的最顶层时,返回当前所在模块名称。

class Person
{
func printInfo()
{
print("Some person info - inside function \(#function)")
}
}

let aPerson = Person()
aPerson.printInfo() //Some person info - inside function printInfo()

#line:特殊字面量表达式,用于获取当前代码的行数。

class Person  
{
func printInfo()
{
print("Some person info - on line number \(#line)")
}
}

let aPerson = Person()
aPerson.printInfo() //Some person info - on line number 5

#selector:用于创建 Objective-C selector 的表达式,可以静态检查方法是否存在,并暴露给 Objective-C。

//静态检查,确保 doAnObjCMethod 方法存在  
control.sendAction(#selector(doAnObjCMethod), to: target, forEvent: event)

dynamic && @objc


@objc


OC 是基于运行时,遵循了 KVC 和动态派发,而 Swift 为了追求性能,在编译时就已经确定,而不需要在运行时的,在 Swift 类型文件中,为了解决这个问题,需要暴露给 OC 使用的任何地方(类,属性,方法等)的生命前面加上 @objc 修饰符

如果用 Swift 写的 class 是继承 NSObject 的话, Swift 会默认自动为所有非 private 的类和成员加上@objc


在Swift中,我们在给button添加点击事件时,对应的点击事件的触发方法就需要用@objc来修饰


dynamic


Swift 中的函数可以是静态调用,静态调用会更快。当函数是静态调用的时候,就不能从字符串查找到对应的方法地址了。这样 Swift 跟 Objective-C 交互时,Objective-C 动态查找方法地址,就有可能找不到 Swift 中定义的方法。


这样就需要在 Swift 中添加一个提示关键字,告诉编译器这个方法是可能被动态调用的,需要将其添加到查找表中。这个就是关键字 dynamic 的作用。


didSet


属性观察者,当值存储到属性后马上调用。

var data = [1,2,3]  
{
didSet
{
tableView.reloadData()
}
}

final


防止方法、属性、下标被重写。

final class Person {}  
class Programmer : Person {} //编译错误


get


返回成员的值。还可以用在计算型属性上,间接获取其它属性的值。

class Person  
{
var name:String
{
get { return self.name }
set { self.name = newValue}
}

var indirectSetName:String
{
get
{
if let aFullTitle = self.fullTitle
{
return aFullTitle
}
return ""
}

set (newTitle)
{
//如果没有定义 newTitle,可以使用 newValue
self.fullTitle = "(self.name) :(newTitle)"
}
}
}


infix


指明一个用于两个值之间的运算符。如果一个全新的全局运算符被定义为 infix,还需要指定优先级。

let twoIntsAdded = 2 + 3


indirect


指明在枚举类型中,存在成员使用相同枚举类型的实例作为关联值的情况。

indirect enum Entertainment  
{
case eventType(String)
case oneEvent(Entertainment)
case twoEvents(Entertainment, Entertainment)
}

let dinner = Entertainment.eventType("Dinner")
let movie = Entertainment.eventType("Movie")

let dateNight = Entertainment.twoEvents(dinner, movie)


lazy


指明属性的初始值,直到第一次被使用时,才进行初始化。

class Person  
{
lazy var personalityTraits = {
//昂贵的数据库开销
return ["Nice", "Funny"]
}()
}
let aPerson = Person()
aPerson.personalityTraits //当 personalityTraits 首次被访问时,数据库才开始工作

left


指明运算符的结合性是从左到右。在没有使用大括号时,可以用于正确判断同一优先级运算符的执行顺序。

//"-" 运算符的结合性是从左到右
10-2-4 //根据结合性,可以看做 (10-2) - 4


mutating


允许在方法中修改结构体或者枚举实例的属性值。

struct Person  
{
var job = ""

mutating func assignJob(newJob:String)
{
self = Person(job: newJob)
}
}

var aPerson = Person()
aPerson.job //""

aPerson.assignJob(newJob: "iOS Engineer at Buffer")
aPerson.job //iOS Engineer at Buffer


none


是一个没有结合性的运算符。不允许这样的运算符相邻出现。

//"<" 是非结合性的运算符
1 < 2 < 3 //编译失败


nonmutating


指明成员的 setter 方法不会修改实例的值,但可能会有其它后果。

enum Paygrade  
{
case Junior, Middle, Senior, Master

var experiencePay:String?
{
get
{
database.payForGrade(String(describing:self))
}

nonmutating set
{
if let newPay = newValue
{
database.editPayForGrade(String(describing:self), newSalary:newPay)
}
}
}
}

let currentPay = Paygrade.Middle

//将 Middle pay 更新为 45k, 但不会修改 experiencePay 值
currentPay.experiencePay = "$45,000"

optional


用于指明协议中的可选方法。遵守该协议的实体类可以不实现这个方法。

@objc protocol Foo  
{
func requiredFunction()
@objc optional func optionalFunction()
}

class Person : Foo
{
func requiredFunction()
{
print("Conformance is now valid")
}
}

override


指明子类会提供自定义实现,覆盖父类的实例方法、类型方法、实例属性、类型属性、下标。如果没有实现,则会直接继承自父类。

class Person  
{
func printInfo()
{
print("I'm just a person!")
}
}

class Programmer : Person
{
override func printInfo()
{
print("I'm a person who is a dev!")
}
}

let aPerson = Person()
let aDev = Programmer()

aPerson.printInfo() //打印 I'm just a person!
aDev.printInfo() //打印 I'm a person who is a dev!

postfix


位于值后面的运算符。

var optionalStr:String? = "Optional"  
print(optionalStr!)

precedence


指明某个运算符的优先级高于别的运算符,从而被优先使用。

infix operator ~ { associativity right precedence 140 }  
4 ~ 8

prefix


位于值前面的运算符。


var anInt = 2  
anInt = -anInt //anInt 等于 -2


required


确保编译器会检查该类的所有子类,全部实现了指定的构造器方法。

class Person  
{
var name:String?

required init(_ name:String)
{
self.name = name
}
}

class Programmer : Person
{
//如果不实现这个方法,编译不会通过
required init(_ name: String)
{
super.init(name)
}
}

right


指明运算符的结合性是从右到左的。在没有使用大括号时,可以用于正确判断同一优先级运算符的顺序。

//"??" 运算符结合性是从右到左
var box:Int?
var sol:Int? = 2

let foo:Int = box ?? sol ?? 0 //Foo 等于 2


set


通过获取的新值来设置成员的值。同样可以用于计算型属性来间接设置其它属性。如果计算型属性的 setter 没有定义新值的名称,可以使用默认的 newValue。

class Person  
{
var name:String
{
get { return self.name }
set { self.name = newValue}
}

var indirectSetName:String
{
get
{
if let aFullTitle = self.fullTitle
{
return aFullTitle
}
return ""
}

set (newTitle)
{
//如果没有定义 newTitle,可以使用 newValue
self.fullTitle = "(self.name) :(newTitle)"
}
}
}


Type


表示任意类型的类型,包括类类型、结构类型、枚举类型、协议类型。

class Person {}  
class Programmer : Person {}

let aDev:Programmer.Type = Programmer.self

unowned


让循环引用中的实例 A 不要强引用实例 B。前提条件是实例 B 的生命周期要长于 A 实例。

class Person  
{
var occupation:Job?
}

//当 Person 实例不存在时,job 也不会存在。job 的生命周期取决于持有它的 Person。
class Job
{
unowned let employee:Person

init(with employee:Person)
{
self.employee = employee
}
}

weak


允许循环引用中的实例 A 弱引用实例 B ,而不是强引用。实例 B 的生命周期更短,并会被先释放。

class Person  
{
var residence:House?
}

class House
{
weak var occupant:Person?
}

var me:Person? = Person()
var myHome:House? = House()

me!.residence = myHome
myHome!.occupant = me

me = nil
myHome!.occupant // myHome 等于 nil


willSet


属性观察者,在值存储到属性之前调用。

class Person  
{
var name:String?
{
willSet(newValue) {print("I've got a new name, it's (newValue)!")}
}
}

let aPerson = Person()
aPerson.name = "Jordan" //在赋值之前,打印 "I've got a new name, it's Jordan!"



链接:https://www.jianshu.com/p/46cf5c77dee7
收起阅读 »

iOS 常用技巧

1、递归查看 view 的子视图(私有方法,没有代码提示)[self.view recursiveDescription] 2、// 定义一个特殊字符的集合 NSCharacterSet *set = [NSCharacterSet characterSet...
继续阅读 »

1、递归查看 view 的子视图(私有方法,没有代码提示)

[self.view recursiveDescription]

2、

// 定义一个特殊字符的集合
NSCharacterSet *set = [NSCharacterSet characterSetWithCharactersInString:
@"@/:;()¥「」"、[]{}#%-*+=_\\|~<>$€^•'@#$%^&*()_+'\""];
// 过滤字符串的特殊字符
NSString *newString = [trimString stringByTrimmingCharactersInSet:set];

3、Transform 属性

//平移按钮
CGAffineTransform transForm = self.buttonView.transform;
self.buttonView.transform = CGAffineTransformTranslate(transForm, 10, 0);

//旋转按钮
CGAffineTransform transForm = self.buttonView.transform;
self.buttonView.transform = CGAffineTransformRotate(transForm, M_PI_4);

//缩放按钮
self.buttonView.transform = CGAffineTransformScale(transForm, 1.2, 1.2);

//初始化复位
self.buttonView.transform = CGAffineTransformIdentity;

4、去掉分割线多余15pt

首先在viewDidLoad方法加入以下代码:
if ([self.tableView respondsToSelector:@selector(setSeparatorInset:)]) {
[self.tableView setSeparatorInset:UIEdgeInsetsZero];
}
if ([self.tableView respondsToSelector:@selector(setLayoutMargins:)]) {
[self.tableView setLayoutMargins:UIEdgeInsetsZero];
}
然后在重写willDisplayCell方法
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell
forRowAtIndexPath:(NSIndexPath *)indexPath{
if ([cell respondsToSelector:@selector(setSeparatorInset:)]) {
[cell setSeparatorInset:UIEdgeInsetsZero];
}
if ([cell respondsToSelector:@selector(setLayoutMargins:)]) {
[cell setLayoutMargins:UIEdgeInsetsZero];
}
}

5、计算耗时方法时间间隔

// 获取时间间隔
#define TICK CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
#define TOCK NSLog(@"Time: %f", CFAbsoluteTimeGetCurrent() - start)

6、Color颜色宏定义

// 随机颜色
#define RANDOM_COLOR [UIColor colorWithRed:arc4random_uniform(256) / 255.0 green:arc4random_uniform(256) / 255.0 blue:arc4random_uniform(256) / 255.0 alpha:1]
// 颜色(RGB)
#define RGBCOLOR(r, g, b) [UIColor colorWithRed:(r)/255.0f green:(g)/255.0f blue:(b)/255.0f alpha:1]
// 利用这种方法设置颜色和透明值,可不影响子视图背景色
#define RGBACOLOR(r, g, b, a) [UIColor colorWithRed:(r)/255.0f green:(g)/255.0f blue:(b)/255.0f alpha:(a)]

7、退出iOS应用

- (void)exitApplication {
AppDelegate *app = [UIApplication sharedApplication].delegate;
UIWindow *window = app.window;

[UIView animateWithDuration:1.0f animations:^{
window.alpha = 0;
} completion:^(BOOL finished) {
exit(0);
}];
}

8、NSArray 快速求总和 最大值 最小值 和 平均值

NSArray *array = [NSArray arrayWithObjects:@"2.0", @"2.3", @"3.0", @"4.0", @"10", nil];
CGFloat sum = [[array valueForKeyPath:@"@sum.floatValue"] floatValue];
CGFloat avg = [[array valueForKeyPath:@"@avg.floatValue"] floatValue];
CGFloat max =[[array valueForKeyPath:@"@max.floatValue"] floatValue];
CGFloat min =[[array valueForKeyPath:@"@min.floatValue"] floatValue];
NSLog(@"%f\n%f\n%f\n%f",sum,avg,max,min);

9、Debug栏打印时自动把Unicode编码转化成汉字


 DXXcodeConsoleUnicodePlugin 插件

10、自动生成模型代码的插件

ESJsonFormat-for-Xcode

11、设置滑动的时候隐藏navigationbar

self.navigationController.hidesBarsOnSwipe = YES

12、隐藏导航栏上的返回字体

//Swift
UIBarButtonItem.appearance().setBackButtonTitlePositionAdjustment(UIOffsetMake(0, -60), forBarMetrics: .Default)
//OC
[[UIBarButtonItem appearance] setBackButtonTitlePositionAdjustment:UIOffsetMake(0, -60) forBarMetrics:UIBarMetricsDefault];

13、设置导航栏透明

//方法一:设置透明度
[[[self.navigationController.navigationBar subviews]objectAtIndex:0] setAlpha:0.1];
//方法二:设置背景图片
/**
* 设置导航栏,使其透明
*
*/
- (void)setNavigationBarColor:(UIColor *)color targetController:(UIViewController *)targetViewController{
//导航条的颜色 以及隐藏导航条的颜色targetViewController.navigationController.navigationBar.shadowImage = [[UIImage alloc]init];
CGRect rect=CGRectMake(0.0f, 0.0f, 1.0f, 1.0f); UIGraphicsBeginImageContext(rect.size);
CGContextRef context = UIGraphicsGetCurrentContext(); CGContextSetFillColorWithColor(context, [color CGColor]); CGContextFillRect(context, rect);
UIImage *theImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); [targetViewController.navigationController.navigationBar setBackgroundImage:theImage forBarMetrics:UIBarMetricsDefault];
}

14、解决同时按两个按钮进两个view的问题

[button setExclusiveTouch:YES];

15、修改 textFieldplaceholder 字体颜色和大小

  textField.placeholder = @"请输入手机号码";
[textField setValue:[UIColor redColor] forKeyPath:@"_placeholderLabel.textColor"];
[textField setValue:[UIFont boldSystemFontOfSize:13] forKeyPath:@"_placeholderLabel.font"];

16、UIImage 与字符串互转

//图片转字符串  
-(NSString *)UIImageToBase64Str:(UIImage *) image
{
NSData *data = UIImageJPEGRepresentation(image, 1.0f);
NSString *encodedImageStr = [data base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength];
return encodedImageStr;
}

//字符串转图片
-(UIImage *)Base64StrToUIImage:(NSString *)_encodedImageStr
{
NSData *_decodedImageData = [[NSData alloc] initWithBase64Encoding:_encodedImageStr];
UIImage *_decodedImage = [UIImage imageWithData:_decodedImageData];
return _decodedImage;
}


链接:https://www.jianshu.com/p/adb3ebf5354c 收起阅读 »

iOS - 图层性能 二

混合和过度绘制    在第12章有提到,GPU每一帧可以绘制的像素有一个最大限制(就是所谓的fill rate),这个情况下可以轻易地绘制整个屏幕的所有像素。但是如果由于重叠图层的关系需要不停地重绘同一区域的话,掉帧就可...
继续阅读 »

混合和过度绘制

    在第12章有提到,GPU每一帧可以绘制的像素有一个最大限制(就是所谓的fill rate),这个情况下可以轻易地绘制整个屏幕的所有像素。但是如果由于重叠图层的关系需要不停地重绘同一区域的话,掉帧就可能发生了。

    GPU会放弃绘制那些完全被其他图层遮挡的像素,但是要计算出一个图层是否被遮挡也是相当复杂并且会消耗处理器资源。同样,合并不同图层的透明重叠像素(即混合)消耗的资源也是相当客观的。所以为了加速处理进程,不到必须时刻不要使用透明图层。任何情况下,你应该这样做:

  • 给视图的backgroundColor属性设置一个固定的,不透明的颜色
  • 设置opaque属性为YES

    这样做减少了混合行为(因为编译器知道在图层之后的东西都不会对最终的像素颜色产生影响)并且计算得到了加速,避免了过度绘制行为因为Core Animation可以舍弃所有被完全遮盖住的图层,而不用每个像素都去计算一遍。

    如果用到了图像,尽量避免透明除非非常必要。如果图像要显示在一个固定的背景颜色或是固定的背景图之前,你没必要相对前景移动,你只需要预填充背景图片就可以避免运行时混色了。

    如果是文本的话,一个白色背景的UILabel(或者其他颜色)会比透明背景要更高效。

    最后,明智地使用shouldRasterize属性,可以将一个固定的图层体系折叠成单张图片,这样就不需要每一帧重新合成了,也就不会有因为子图层之间的混合和过度绘制的性能问题了。

减少图层数量

    初始化图层,处理图层,打包通过IPC发给渲染引擎,转化成OpenGL几何图形,这些是一个图层的大致资源开销。事实上,一次性能够在屏幕上显示的最大图层数量也是有限的。

    确切的限制数量取决于iOS设备,图层类型,图层内容和属性等。但是总得说来可以容纳上百或上千个,下面我们将演示即使图层本身并没有做什么也会遇到的性能问题。

裁切

    在对图层做任何优化之前,你需要确定你不是在创建一些不可见的图层,图层在以下几种情况下回事不可见的:

  • 图层在屏幕边界之外,或是在父图层边界之外。
  • 完全在一个不透明图层之后。
  • 完全透明

    Core Animation非常擅长处理对视觉效果无意义的图层。但是经常性地,你自己的代码会比Core Animation更早地想知道一个图层是否是有用的。理想状况下,在图层对象在创建之前就想知道,以避免创建和配置不必要图层的额外工作。

    举个例子。清单15.3 的代码展示了一个简单的滚动3D图层矩阵。这看上去很酷,尤其是图层在移动的时候(见图15.1),但是绘制他们并不是很麻烦,因为这些图层就是一些简单的矩形色块。

清单15.3 绘制3D图层矩阵

#import "ViewController.h"
#import

#define WIDTH 10
#define HEIGHT 10
#define DEPTH 10
#define SIZE 100
#define SPACING 150
#define CAMERA_DISTANCE 500

@interface ViewController ()

@property (nonatomic, strong) IBOutlet UIScrollView *scrollView;

@end

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad];

//set content size
self.scrollView.contentSize = CGSizeMake((WIDTH - 1)*SPACING, (HEIGHT - 1)*SPACING);

//set up perspective transform
CATransform3D transform = CATransform3DIdentity;
transform.m34 = -1.0 / CAMERA_DISTANCE;
self.scrollView.layer.sublayerTransform = transform;

//create layers
for (int z = DEPTH - 1; z >= 0; z--) {
for (int y = 0; y < HEIGHT; y++) {
for (int x = 0; x < WIDTH; x++) {
//create layer
CALayer *layer = [CALayer layer];
layer.frame = CGRectMake(0, 0, SIZE, SIZE);
layer.position = CGPointMake(x*SPACING, y*SPACING);
layer.zPosition = -z*SPACING;
//set background color
layer.backgroundColor = [UIColor colorWithWhite:1-z*(1.0/DEPTH) alpha:1].CGColor;
//attach to scroll view
[self.scrollView.layer addSublayer:layer];
}
}
}

//log
NSLog(@"displayed: %i", DEPTH*HEIGHT*WIDTH);
}
@end

图15.1 滚动的3D图层矩阵

    WIDTHHEIGHTDEPTH常量控制着图层的生成。在这个情况下,我们得到的是10*10*10个图层,总量为1000个,不过一次性显示在屏幕上的大约就几百个。

    如果把WIDTHHEIGHT常量增加到100,我们的程序就会慢得像龟爬了。这样我们有了100000个图层,性能下降一点儿也不奇怪。

    但是显示在屏幕上的图层数量并没有增加,那么根本没有额外的东西需要绘制。程序慢下来的原因其实是因为在管理这些图层上花掉了不少功夫。他们大部分对渲染的最终结果没有贡献,但是在丢弃这么图层之前,Core Animation要强制计算每个图层的位置,就这样,我们的帧率就慢了下来。

    我们的图层是被安排在一个均匀的栅格中,我们可以计算出哪些图层会被最终显示在屏幕上,根本不需要对每个图层的位置进行计算。这个计算并不简单,因为我们还要考虑到透视的问题。如果我们直接这样做了,Core Animation就不用费神了。

    既然这样,让我们来重构我们的代码吧。改造后,随着视图的滚动动态地实例化图层而不是事先都分配好。这样,在创造他们之前,我们就可以计算出是否需要他。接着,我们增加一些代码去计算可视区域这样就可以排除区域之外的图层了。清单15.4是改造后的结果。

清单15.4 排除可视区域之外的图层

#import "ViewController.h"
#import

#define WIDTH 100
#define HEIGHT 100
#define DEPTH 10
#define SIZE 100
#define SPACING 150
#define CAMERA_DISTANCE 500
#define PERSPECTIVE(z) (float)CAMERA_DISTANCE/(z + CAMERA_DISTANCE)

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIScrollView *scrollView;

@end

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad];
//set content size
self.scrollView.contentSize = CGSizeMake((WIDTH - 1)*SPACING, (HEIGHT - 1)*SPACING);
//set up perspective transform
CATransform3D transform = CATransform3DIdentity;
transform.m34 = -1.0 / CAMERA_DISTANCE;
self.scrollView.layer.sublayerTransform = transform;
}

- (void)viewDidLayoutSubviews
{
[self updateLayers];
}

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
[self updateLayers];
}

- (void)updateLayers
{
//calculate clipping bounds
CGRect bounds = self.scrollView.bounds;
bounds.origin = self.scrollView.contentOffset;
bounds = CGRectInset(bounds, -SIZE/2, -SIZE/2);
//create layers
NSMutableArray *visibleLayers = [NSMutableArray array];
for (int z = DEPTH - 1; z >= 0; z--)
{
//increase bounds size to compensate for perspective
CGRect adjusted = bounds;
adjusted.size.width /= PERSPECTIVE(z*SPACING);
adjusted.size.height /= PERSPECTIVE(z*SPACING);
adjusted.origin.x -= (adjusted.size.width - bounds.size.width) / 2;
adjusted.origin.y -= (adjusted.size.height - bounds.size.height) / 2;
for (int y = 0; y < HEIGHT; y++) {
//check if vertically outside visible rect
if (y*SPACING < adjusted.origin.y || y*SPACING >= adjusted.origin.y + adjusted.size.height)
{
continue;
}
for (int x = 0; x < WIDTH; x++) {
//check if horizontally outside visible rect
if (x*SPACING < adjusted.origin.x ||x*SPACING >= adjusted.origin.x + adjusted.size.width)
{
continue;
}

//create layer
CALayer *layer = [CALayer layer];
layer.frame = CGRectMake(0, 0, SIZE, SIZE);
layer.position = CGPointMake(x*SPACING, y*SPACING);
layer.zPosition = -z*SPACING;
//set background color
layer.backgroundColor = [UIColor colorWithWhite:1-z*(1.0/DEPTH) alpha:1].CGColor;
//attach to scroll view
[visibleLayers addObject:layer];
}
}
}
//update layers
self.scrollView.layer.sublayers = visibleLayers;
//log
NSLog(@"displayed: %i/%i", [visibleLayers count], DEPTH*HEIGHT*WIDTH);
}
@end

    这个计算机制并不具有普适性,但是原则上是一样。(当你用一个UITableView或者UICollectionView时,系统做了类似的事情)。这样做的结果?我们的程序可以处理成百上千个『虚拟』图层而且完全没有性能问题!因为它不需要一次性实例化几百个图层。

对象回收

    处理巨大数量的相似视图或图层时还有一个技巧就是回收他们。对象回收在iOS颇为常见;UITableViewUICollectionView都有用到,MKMapView中的动画pin码也有用到,还有其他很多例子。

    对象回收的基础原则就是你需要创建一个相似对象池。当一个对象的指定实例(本例子中指的是图层)结束了使命,你把它添加到对象池中。每次当你需要一个实例时,你就从池中取出一个。当且仅当池中为空时再创建一个新的。

    这样做的好处在于避免了不断创建和释放对象(相当消耗资源,因为涉及到内存的分配和销毁)而且也不必给相似实例重复赋值。

    好了,让我们再次更新代码吧(见清单15.5)

清单15.5 通过回收减少不必要的分配

@interface ViewController () 

@property (nonatomic, weak) IBOutlet UIScrollView *scrollView;
@property (nonatomic, strong) NSMutableSet *recyclePool;


@end

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad]; //create recycle pool
self.recyclePool = [NSMutableSet set];
//set content size
self.scrollView.contentSize = CGSizeMake((WIDTH - 1)*SPACING, (HEIGHT - 1)*SPACING);
//set up perspective transform
CATransform3D transform = CATransform3DIdentity;
transform.m34 = -1.0 / CAMERA_DISTANCE;
self.scrollView.layer.sublayerTransform = transform;
}

- (void)viewDidLayoutSubviews
{
[self updateLayers];
}

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
[self updateLayers];
}

- (void)updateLayers {

//calculate clipping bounds
CGRect bounds = self.scrollView.bounds;
bounds.origin = self.scrollView.contentOffset;
bounds = CGRectInset(bounds, -SIZE/2, -SIZE/2);
//add existing layers to pool
[self.recyclePool addObjectsFromArray:self.scrollView.layer.sublayers];
//disable animation
[CATransaction begin];
[CATransaction setDisableActions:YES];
//create layers
NSInteger recycled = 0;
NSMutableArray *visibleLayers = [NSMutableArray array];
for (int z = DEPTH - 1; z >= 0; z--)
{
//increase bounds size to compensate for perspective
CGRect adjusted = bounds;
adjusted.size.width /= PERSPECTIVE(z*SPACING);
adjusted.size.height /= PERSPECTIVE(z*SPACING);
adjusted.origin.x -= (adjusted.size.width - bounds.size.width) / 2; adjusted.origin.y -= (adjusted.size.height - bounds.size.height) / 2;
for (int y = 0; y < HEIGHT; y++) {
//check if vertically outside visible rect
if (y*SPACING < adjusted.origin.y ||
y*SPACING >= adjusted.origin.y + adjusted.size.height)
{
continue;
}
for (int x = 0; x < WIDTH; x++) {
//check if horizontally outside visible rect
if (x*SPACING < adjusted.origin.x ||
x*SPACING >= adjusted.origin.x + adjusted.size.width)
{
continue;
}
//recycle layer if available
CALayer *layer = [self.recyclePool anyObject]; if (layer)
{

recycled ++;
[self.recyclePool removeObject:layer]; }
else
{
layer = [CALayer layer];
layer.frame = CGRectMake(0, 0, SIZE, SIZE); }
//set position
layer.position = CGPointMake(x*SPACING, y*SPACING); layer.zPosition = -z*SPACING;
//set background color
layer.backgroundColor =
[UIColor colorWithWhite:1-z*(1.0/DEPTH) alpha:1].CGColor;
//attach to scroll view
[visibleLayers addObject:layer]; }
} }
[CATransaction commit]; //update layers
self.scrollView.layer.sublayers = visibleLayers;
//log
NSLog(@"displayed: %i/%i recycled: %i",
[visibleLayers count], DEPTH*HEIGHT*WIDTH, recycled);
}
@end

    本例中,我们只有图层对象这一种类型,但是UIKit有时候用一个标识符字符串来区分存储在不同对象池中的不同的可回收对象类型。

    你可能注意到当设置图层属性时我们用了一个CATransaction来抑制动画效果。在之前并不需要这样做,因为在显示之前我们给所有图层设置一次属性。但是既然图层正在被回收,禁止隐式动画就有必要了,不然当属性值改变时,图层的隐式动画就会被触发。

Core Graphics绘制

    当排除掉对屏幕显示没有任何贡献的图层或者视图之后,长远看来,你可能仍然需要减少图层的数量。例如,如果你正在使用多个UILabel或者UIImageView实例去显示固定内容,你可以把他们全部替换成一个单独的视图,然后用-drawRect:方法绘制出那些复杂的视图层级。

    这个提议看上去并不合理因为大家都知道软件绘制行为要比GPU合成要慢而且还需要更多的内存空间,但是在因为图层数量而使得性能受限的情况下,软件绘制很可能提高性能呢,因为它避免了图层分配和操作问题。

    你可以自己实验一下这个情况,它包含了性能和栅格化的权衡,但是意味着你可以从图层树上去掉子图层(用shouldRasterize,与完全遮挡图层相反)。

-renderInContext: 方法

    用Core Graphics去绘制一个静态布局有时候会比用层级的UIView实例来得快,但是使用UIView实例要简单得多而且比用手写代码写出相同效果要可靠得多,更边说Interface Builder来得直接明了。为了性能而舍弃这些便利实在是不应该。

    幸好,你不必这样,如果大量的视图或者图层真的关联到了屏幕上将会是一个大问题。没有与图层树相关联的图层不会被送到渲染引擎,也没有性能问题(在他们被创建和配置之后)。

    使用CALayer-renderInContext:方法,你可以将图层及其子图层快照进一个Core Graphics上下文然后得到一个图片,它可以直接显示在UIImageView中,或者作为另一个图层的contents。不同于shouldRasterize —— 要求图层与图层树相关联 —— ,这个方法没有持续的性能消耗。

    当图层内容改变时,刷新这张图片的机会取决于你(不同于shouldRasterize,它自动地处理缓存和缓存验证),但是一旦图片被生成,相比于让Core Animation处理一个复杂的图层树,你节省了相当客观的性能。

总结

    本章学习了使用Core Animation图层可能遇到的性能瓶颈,并讨论了如何避免或减小压力。你学习了如何管理包含上千虚拟图层的场景(事实上只创建了几百个)。同时也学习了一些有用的技巧,选择性地选取光栅化或者绘制图层内容在合适的时候重新分配给CPU和GPU。这些就是我们要讲的关于Core Animation的全部了(至少可以等到苹果发明什么新的玩意儿)。


收起阅读 »

巧用CSS filter,让你的网站更加酷炫!

前言 我们在处理图片时,经常使用的一个功能就是滤镜,它能使一张图像呈现各种不同的视觉效果。 在 CSS 中,也有一个filter属性,让我们能用 CSS 代码为元素指定各种滤镜效果,比如模糊、灰度、明暗度、颜色偏移等。 CSS filter的基础使用非常简单...
继续阅读 »

前言


我们在处理图片时,经常使用的一个功能就是滤镜,它能使一张图像呈现各种不同的视觉效果。


image.png


在 CSS 中,也有一个filter属性,让我们能用 CSS 代码为元素指定各种滤镜效果,比如模糊、灰度、明暗度、颜色偏移等。


CSS filter的基础使用非常简单,CSS 标准里包含了一些已实现预定义效果的函数(下面blur、brightness、contrast等),我们可以通过指定这些函数的值来实现想要的效果:


/* 使用单个滤镜 (如果传入的参数是百分数,那么也可以传入对应的小数:40% --> 0.4)*/
filter: blur(5px);
filter: brightness(40%);
filter: contrast(200%);
filter: drop-shadow(16px 16px 20px blue);
filter: grayscale(50%);
filter: hue-rotate(90deg);
filter: invert(75%);
filter: opacity(25%);
filter: saturate(30%);
filter: sepia(60%);

/* 使用多个滤镜 */
filter: contrast(175%) brightness(3%);

/* 不使用任何滤镜 */
filter: none;

官方demo:MDN


filter-demo.gif


滤镜在日常开发中是很常见的,比如使用drop-shadow给不规则形状添加阴影;使用blur来实现背景模糊,以及毛玻璃效果等。


下面我们将进一步使用CSS filter实现一些动画效果,让网站交互更加酷炫,同时也加深对CSS filter的理解。一起开始吧!


( 下面要使用到的 动画 和 伪类 知识,在 CSS的N个编码技巧 中都有详细的介绍,这里就不重复了,有需要的朋友可以前往查看哦。 )


电影效果


滤镜中的brightness用于调整图像的明暗度。默认值是1;小于1时图像变暗,为0时显示为全黑图像;大于1时图像显示比原图更明亮。


我们可以通过调整 背景图的明暗度文字的透明度 ,来模拟电影谢幕的效果。


movie.gif


<div>
<div></div>
<div>
<p>如果生活中有什么使你感到快乐,那就去做吧</p>
<br>
<p>不要管别人说什么</p>
</div>
</div>

.pic{
height: 100%;
width: 100%;
position: absolute;
background: url('./images/movie.webp') no-repeat;
background-size: cover;
animation: fade-away 2.5s linear forwards; //forwards当动画完成后,保持最后一帧的状态
}
.text{
position: absolute;
line-height: 55px;
color: #fff;
font-size: 36px;
text-align: center;
left: 50%;
top: 50%;
transform: translate(-50%,-50%);
opacity: 0;
animation: show 2s cubic-bezier(.74,-0.1,.86,.83) forwards;
}

@keyframes fade-away { //背景图的明暗度动画
30%{
filter: brightness(1);
}
100%{
filter: brightness(0);
}
}
@keyframes show{ //文字的透明度动画
20%{
opacity: 0;
}
100%{
opacity: 1;
}
}

模糊效果


在下面的单词卡片中,当鼠标hover到某一张卡片上时,其他卡片背景模糊,使用户焦点集中到当前卡片。


card-blur.gif


html结构:


<ul>
<li>
<p>Flower</p>
<p>The flowers mingle to form a blaze of color.</p>
</li>
<li>
<p>Sunset</p>
<p>The sunset glow tinted the sky red.</p>
</li>
<li>
<p>Plain</p>
<p>The winds came from the north, across the plains, funnelling down the valley. </p>
</li>
</ul>

实现的方式,是将背景加在.card元素的伪类上,当元素不是焦点时,为该元素的伪类加上滤镜。


.card:before{
z-index: -1;
content: '';
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
border-radius: 20px;
filter: blur(0px) opacity(1);
transition: filter 200ms linear, transform 200ms linear;
}
/*
这里不能将滤镜直接加在.card元素,而是将背景和滤镜都加在伪类上。
因为,父元素加了滤镜,它的子元素都会一起由该滤镜改变。
如果滤镜直接加在.card元素上,会导致上面的文字也变模糊。
*/

//通过css选择器选出非hover的.card元素,给其伪类添加模糊、透明度和明暗度的滤镜 

.cards:hover > .card:not(:hover):before{
filter: blur(5px) opacity(0.8) brightness(0.8);
}

//对于hover的元素,其伪类增强饱和度,尺寸放大

.card:hover:before{
filter: saturate(1.2);
transform: scale(1.05);
}

褪色效果


褪色效果可以打造出一种怀旧的风格。下面这组照片墙,我们通过sepia滤镜将图像基调转换为深褐色,再通过降低 饱和度saturate 和 色相旋转hue-rotate 微调,模拟老照片的效果。


old-photo-s.gif


.pic{
border: 3px solid #fff;
box-shadow: 0 10px 50px #5f2f1182;
filter: sepia(30%) saturate(40%) hue-rotate(5deg);
transition: transform 1s;
}
.pic:hover{
filter: none;
transform: scale(1.2) translateX(10px);
z-index: 1;
}

灰度效果


怎样让网站变成灰色?在html元素上加上filter: grayscale(100%)即可。


grayscale(amount)函数将改变输入图像灰度。amount 的值定义了灰度转换的比例。值为 100% 则完全转为灰度图像,值为 0% 图像无变化。若未设置值,默认值是 0


gray-scale.gif


融合效果


要使两个相交的元素产生下面这种融合的效果,需要用到的滤镜是blurcontrast


merge.gif


<div>
<div></div>
<div></div>
</div>

.container{
margin: 50px auto;
height: 140px;
width: 400px;
background: #fff; //给融合元素的父元素设置背景色
display: flex;
align-items: center;
justify-content: center;
filter: contrast(30); //给融合元素的父元素设置contrast
}
.circle{
border-radius: 50%;
position: absolute;
filter: blur(10px); //给融合元素设置blur
}
.circle-1{
height: 90px;
width: 90px;
background: #03a9f4;
transform: translate(-50px);
animation: 2s moving linear infinite alternate-reverse;
}
.circle-2{
height: 60px;
width: 60px;
background: #0000ff;
transform: translate(50px);
animation: 2s moving linear infinite alternate;
}
@keyframes moving { //两个元素的移动
0%{
transform: translate(50px)
}
100%{
transform: translate(-50px)
}
}

实现融合效果的技术要点:



  1. contrast滤镜应用在融合元素的父元素(.container)上,且父元素必须设置background

  2. blur滤镜应用在融合元素(.circle)上。


blur设置图像的模糊程度,contrast设置图像的对比度。当两者像上面那样组合时,就会产生神奇的融合效果,你可以像使用公式一样使用这种写法。


在这种融合效果的基础上,我们可以做一些有趣的交互设计。



  • 加载动画:


loading-l.gif
htmlcss如下所示,这个动画主要通过控制子元素.circle的尺寸和位移来实现,但是由于父元素和子元素都满足 “融合公式” ,所以当子元素相交时,就出现了融合的效果。


<div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
</div>

.container {
margin: 10px auto;
height: 140px;
width: 300px;
background: #fff; //父元素设置背景色
display: flex;
align-items: center;
filter: contrast(30); //父元素设置contrast
}
.circle {
height: 50px;
width: 60px;
background: #1aa7ff;
border-radius: 50%;
position: absolute;
filter: blur(20px); //子元素设置blur
transform: scale(0.1);
transform-origin: left top;
}
.circle{
animation: move 4s cubic-bezier(.44,.79,.83,.96) infinite;
}
.circle:nth-child(2) {
animation-delay: .4s;
}
.circle:nth-child(3) {
animation-delay: .8s;
}
.circle:nth-child(4) {
animation-delay: 1.2s;
}
.circle:nth-child(5) {
animation-delay: 1.6s;
}
@keyframes move{ //子元素的位移和尺寸动画
0%{
transform: translateX(10px) scale(0.3);
}
45%{
transform: translateX(135px) scale(0.8);
}
85%{
transform: translateX(270px) scale(0.1);
}
}


  • 酷炫的文字出场方式:


gooey-text.gif
主要通过不断改变letter-spacingblur的值,使文字从融合到分开:


<div>
<span>fantastic</span>
</div>

.container{
margin-top: 50px;
text-align: center;
background-color: #000;
filter: contrast(30);
}
.text{
font-size: 100px;
font-family: 'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif;
letter-spacing: -40px;
color: #fff;
animation: move-letter 4s linear forwards; //forwards当动画完成后,保持最后一帧的状态
}
@keyframes move-letter{
0% {
opacity: 0;
letter-spacing: -40px;
filter: blur(10px);
}
25% {
opacity: 1;
}
50% {
filter: blur(5px);
}
100% {
letter-spacing: 20px;
filter: blur(2px);
}
}

水波效果


filter还可以通过 URL 链接到 SVG 滤镜元素,SVG滤镜元素MDN 。


下面的水波纹效果就是基于 SVG 的feTurbulence滤镜实现的,原理参考了 说说SVG的feTurbulence滤镜

SVG feTurbulence滤镜深入介绍,有兴趣的朋友可以深入阅读。



feTurbulence滤镜借助Perlin噪声算法模拟自然界真实事物那样的随机样式。它接收下面5个属性:



  • baseFrequency表示噪声的基本频率参数,频率越高,噪声越密集。

  • numOctaves就表示倍频的数量,倍频的数量越多,噪声看起来越自然。

  • seed属性表示feTurbulence滤镜效果中伪随机数生成的起始值,不同数量的seed不会改变噪声的频率和密度,改变的是噪声的形状和位置。

  • stitchTiles定义了Perlin噪声在边框处的行为表现。

  • type属性值有fractalNoiseturbulence,模拟随机样式使用turbulence



wave.gif


在这个例子,两个img标签使用同一张图片,将第二个img标签使用scaleY(-1)实现垂直方向的镜像翻转,模拟倒影。


并且,对倒影图片使用feTurbulence滤镜,通过动画不断改变feTurbulence滤镜的baseFrequency值实现水纹波动的效果。


<div>
<img src="images/moon.jpg">
<img src="images/moon.jpg">
</div>

<!--定义svg滤镜,这里使用的是feTurbulence滤镜-->
<svg width="0" height="0">
<filter id="displacement-wave-filter">

<!--baseFrequency设置0.01 0.09两个值,代表x轴和y轴的噪声频率-->
<feTurbulence baseFrequency="0.01 0.09">

<!--这是svg动画的定义方式,通过动画不断改变baseFrequency的值,从而形成波动效果-->
<animate attributeName="baseFrequency"
dur="20s" keyTimes="0;0.5;1" values="0.01 0.09;0.02 0.13;0.01 0.09"
repeatCount="indefinite" ></animate>

</feTurbulence>
<feDisplacementMap in="SourceGraphic" scale="10" />
</filter>
</svg>

.container{
height: 520px;
width: 400px;
display: flex;
clip-path: inset(10px);
flex-direction: column;
}
img{
height: 50%;
width: 100%;
}
.reflect {
transform: translateY(-2px) scaleY(-1);
//对模拟倒影的元素应用svg filter
//url中对应的是上面svg filter的id
filter: url(#displacement-wave-filter);
}

抖动效果


在上面的水波动画中改变的是baseFrequency值,我们也通过改变seed的值,实现文字的抖动效果。
text-shaking.gif


<div>
<p>Such a joyful night!</p>
</div>
<svg width="0" height="0">
<filter id="displacement-text-filter">

<!--定义feTurbulence滤镜-->
<feTurbulence baseFrequency="0.02" seed="0">

<!--这是svg动画的定义方式,通过动画不断改变seed的值,形成抖动效果-->
<animate attributeName="seed"
dur="1s" keyTimes="0;0.5;1" values="1;2;3"
repeatCount="indefinite" ></animate>
</feTurbulence>
<feDisplacementMap in="SourceGraphic" scale="10" />
</filter>
</svg>

.shaky{
font-size: 60px;
filter: url(#displacement-text-filter); //url中对应的是上面svg filter的id
}

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

收起阅读 »

iOS - 图层性能 一

隐式绘制    寄宿图可以通过Core Graphics直接绘制,也可以直接载入一个图片文件并赋值给contents属性,或事先绘制一个屏幕之外的CGContext上下文。在之前的两章中我们讨论了这些场景下的优化。但是除...
继续阅读 »

隐式绘制

    寄宿图可以通过Core Graphics直接绘制,也可以直接载入一个图片文件并赋值给contents属性,或事先绘制一个屏幕之外的CGContext上下文。在之前的两章中我们讨论了这些场景下的优化。但是除了常见的显式创建寄宿图,你也可以通过以下三种方式创建隐式的:1,使用特性的图层属性。2,特定的视图。3,特定的图层子类。

    了解这个情况为什么发生何时发生是很重要的,它能够让你避免引入不必要的软件绘制行为。

文本

    CATextLayerUILabel都是直接将文本绘制在图层的寄宿图中。事实上这两种方式用了完全不同的渲染方式:在iOS 6及之前,UILabel用WebKit的HTML渲染引擎来绘制文本,而CATextLayer用的是Core Text.后者渲染更迅速,所以在所有需要绘制大量文本的情形下都优先使用它吧。但是这两种方法都用了软件的方式绘制,因此他们实际上要比硬件加速合成方式要慢。

    不论如何,尽可能地避免改变那些包含文本的视图的frame,因为这样做的话文本就需要重绘。例如,如果你想在图层的角落里显示一段静态的文本,但是这个图层经常改动,你就应该把文本放在一个子图层中。

光栅化

    在第四章『视觉效果』中我们提到了CALayershouldRasterize属性,它可以解决重叠透明图层的混合失灵问题。同样在第12章『速度的曲调』中,它也是作为绘制复杂图层树结构的优化方法。

    启用shouldRasterize属性会将图层绘制到一个屏幕之外的图像。然后这个图像将会被缓存起来并绘制到实际图层的contents和子图层。如果有很多的子图层或者有复杂的效果应用,这样做就会比重绘所有事务的所有帧划得来得多。但是光栅化原始图像需要时间,而且还会消耗额外的内存。

    当我们使用得当时,光栅化可以提供很大的性能优势(如你在第12章所见),但是一定要避免作用在内容不断变动的图层上,否则它缓存方面的好处就会消失,而且会让性能变的更糟。

    为了检测你是否正确地使用了光栅化方式,用Instrument查看一下Color Hits Green和Misses Red项目,是否已光栅化图像被频繁地刷新(这样就说明图层并不是光栅化的好选择,或则你无意间触发了不必要的改变导致了重绘行为)。

离屏渲染

    当图层属性的混合体被指定为在未预合成之前不能直接在屏幕中绘制时,屏幕外渲染就被唤起了。屏幕外渲染并不意味着软件绘制,但是它意味着图层必须在被显示之前在一个屏幕外上下文中被渲染(不论CPU还是GPU)。图层的以下属性将会触发屏幕外绘制:

  • 圆角(当和maskToBounds一起使用时)
  • 图层蒙板
  • 阴影

    屏幕外渲染和我们启用光栅化时相似,除了它并没有像光栅化图层那么消耗大,子图层并没有被影响到,而且结果也没有被缓存,所以不会有长期的内存占用。但是,如果太多图层在屏幕外渲染依然会影响到性能。

    有时候我们可以把那些需要屏幕外绘制的图层开启光栅化以作为一个优化方式,前提是这些图层并不会被频繁地重绘。

    对于那些需要动画而且要在屏幕外渲染的图层来说,你可以用CAShapeLayercontentsCenter或者shadowPath来获得同样的表现而且较少地影响到性能。

CAShapeLayer

    cornerRadiusmaskToBounds独立作用的时候都不会有太大的性能问题,但是当他俩结合在一起,就触发了屏幕外渲染。有时候你想显示圆角并沿着图层裁切子图层的时候,你可能会发现你并不需要沿着圆角裁切,这个情况下用CAShapeLayer就可以避免这个问题了。

    你想要的只是圆角且沿着矩形边界裁切,同时还不希望引起性能问题。其实你可以用现成的UIBezierPath的构造器+bezierPathWithRoundedRect:cornerRadius:(见清单15.1).这样做并不会比直接用cornerRadius更快,但是它避免了性能问题。

清单15.1 用CAShapeLayer画一个圆角矩形

#import "ViewController.h"
#import

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *layerView;

@end

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad];

//create shape layer
CAShapeLayer *blueLayer = [CAShapeLayer layer];
blueLayer.frame = CGRectMake(50, 50, 100, 100);
blueLayer.fillColor = [UIColor blueColor].CGColor;
blueLayer.path = [UIBezierPath bezierPathWithRoundedRect:
CGRectMake(0, 0, 100, 100) cornerRadius:20].CGPath;

//add it to our view
[self.layerView.layer addSublayer:blueLayer];
}
@end

可伸缩图片

    另一个创建圆角矩形的方法就是用一个圆形内容图片并结合第二章『寄宿图』提到的contensCenter属性去创建一个可伸缩图片(见清单15.2).理论上来说,这个应该比用CAShapeLayer要快,因为一个可拉伸图片只需要18个三角形(一个图片是由一个3*3网格渲染而成),然而,许多都需要渲染成一个顺滑的曲线。在实际应用上,二者并没有太大的区别。

清单15.2 用可伸缩图片绘制圆角矩形

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad];

//create layer
CALayer *blueLayer = [CALayer layer];
blueLayer.frame = CGRectMake(50, 50, 100, 100);
blueLayer.contentsCenter = CGRectMake(0.5, 0.5, 0.0, 0.0);
blueLayer.contentsScale = [UIScreen mainScreen].scale;
blueLayer.contents = (__bridge id)[UIImage imageNamed:@"Circle.png"].CGImage;
//add it to our view
[self.layerView.layer addSublayer:blueLayer];
}
@end

    使用可伸缩图片的优势在于它可以绘制成任意边框效果而不需要额外的性能消耗。举个例子,可伸缩图片甚至还可以显示出矩形阴影的效果。

shadowPath

    在第2章我们有提到shadowPath属性。如果图层是一个简单几何图形如矩形或者圆角矩形(假设不包含任何透明部分或者子图层),创建出一个对应形状的阴影路径就比较容易,而且Core Animation绘制这个阴影也相当简单,避免了屏幕外的图层部分的预排版需求。这对性能来说很有帮助。

    如果你的图层是一个更复杂的图形,生成正确的阴影路径可能就比较难了,这样子的话你可以考虑用绘图软件预先生成一个阴影背景图。

收起阅读 »

iOS - 图像IO 三

文件格式    图片加载性能取决于加载大图的时间和解压小图时间的权衡。很多苹果的文档都说PNG是iOS所有图片加载的最好格式。但这是极度误导的过时信息了。    PNG图片使用的无...
继续阅读 »

文件格式

    图片加载性能取决于加载大图的时间和解压小图时间的权衡。很多苹果的文档都说PNG是iOS所有图片加载的最好格式。但这是极度误导的过时信息了。

    PNG图片使用的无损压缩算法可以比使用JPEG的图片做到更快地解压,但是由于闪存访问的原因,这些加载的时间并没有什么区别。

    清单14.6展示了标准的应用程序加载不同尺寸图片所需要时间的一些代码。为了保证实验的准确性,我们会测量每张图片的加载和绘制时间来确保考虑到解压性能的因素。另外每隔一秒重复加载和绘制图片,这样就可以取到平均时间,使得结果更加准确。

清单14.6

#import "ViewController.h"

static NSString *const ImageFolder = @"Coast Photos";

@interface ViewController ()

@property (nonatomic, copy) NSArray *items;
@property (nonatomic, weak) IBOutlet UITableView *tableView;

@end

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad];
//set up image names
self.items = @[@"2048x1536", @"1024x768", @"512x384", @"256x192", @"128x96", @"64x48", @"32x24"];
}

- (CFTimeInterval)loadImageForOneSec:(NSString *)path
{
//create drawing context to use for decompression
UIGraphicsBeginImageContext(CGSizeMake(1, 1));
//start timing
NSInteger imagesLoaded = 0;
CFTimeInterval endTime = 0;
CFTimeInterval startTime = CFAbsoluteTimeGetCurrent();
while (endTime - startTime < 1) {
//load image
UIImage *image = [UIImage imageWithContentsOfFile:path];
//decompress image by drawing it
[image drawAtPoint:CGPointZero];
//update totals
imagesLoaded ++;
endTime = CFAbsoluteTimeGetCurrent();
}
//close context
UIGraphicsEndImageContext();
//calculate time per image
return (endTime - startTime) / imagesLoaded;
}

- (void)loadImageAtIndex:(NSUInteger)index
{
//load on background thread so as not to
//prevent the UI from updating between runs dispatch_async(
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
//setup
NSString *fileName = self.items[index];
NSString *pngPath = [[NSBundle mainBundle] pathForResource:filename
ofType:@"png"
inDirectory:ImageFolder];
NSString *jpgPath = [[NSBundle mainBundle] pathForResource:filename
ofType:@"jpg"
inDirectory:ImageFolder];
//load
NSInteger pngTime = [self loadImageForOneSec:pngPath] * 1000;
NSInteger jpgTime = [self loadImageForOneSec:jpgPath] * 1000;
//updated UI on main thread
dispatch_async(dispatch_get_main_queue(), ^{
//find table cell and update
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
cell.detailTextLabel.text = [NSString stringWithFormat:@"PNG: ims JPG: ims", pngTime, jpgTime];
});
});
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [self.items count];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
//dequeue cell
UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:@"Cell"];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle: UITableViewCellStyleValue1 reuseIdentifier:@"Cell"];
}
//set up cell
NSString *imageName = self.items[indexPath.row];
cell.textLabel.text = imageName;
cell.detailTextLabel.text = @"Loading...";
//load image
[self loadImageAtIndex:indexPath.row];
return cell;
}

@end

    PNG和JPEG压缩算法作用于两种不同的图片类型:JPEG对于噪点大的图片效果很好;但是PNG更适合于扁平颜色,锋利的线条或者一些渐变色的图片。为了让测评的基准更加公平,我们用一些不同的图片来做实验:一张照片和一张彩虹色的渐变。JPEG版本的图片都用默认的Photoshop60%“高质量”设置编码。结果见图片14.5。

图14.5

图14.5 不同类型图片的相对加载性能

    如结果所示,相对于不友好的PNG图片,相同像素的JPEG图片总是比PNG加载更快,除非一些非常小的图片、但对于友好的PNG图片,一些中大尺寸的图效果还是很好的。

    所以对于之前的图片传送器程序来说,JPEG会是个不错的选择。如果用JPEG的话,一些多线程和缓存策略都没必要了。

    但JPEG图片并不是所有情况都适用。如果图片需要一些透明效果,或者压缩之后细节损耗很多,那就该考虑用别的格式了。苹果在iOS系统中对PNG和JPEG都做了一些优化,所以普通情况下都应该用这种格式。也就是说在一些特殊的情况下才应该使用别的格式。

混合图片

    对于包含透明的图片来说,最好是使用压缩透明通道的PNG图片和压缩RGB部分的JPEG图片混合起来加载。这就对任何格式都适用了,而且无论从质量还是文件尺寸还是加载性能来说都和PNG和JPEG的图片相近。相关分别加载颜色和遮罩图片并在运行时合成的代码见14.7。

清单14.7 从PNG遮罩和JPEG创建的混合图片

#import "ViewController.h"

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIImageView *imageView;

@end

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad];
//load color image
UIImage *image = [UIImage imageNamed:@"Snowman.jpg"];
//load mask image
UIImage *mask = [UIImage imageNamed:@"SnowmanMask.png"];
//convert mask to correct format
CGColorSpaceRef graySpace = CGColorSpaceCreateDeviceGray();
CGImageRef maskRef = CGImageCreateCopyWithColorSpace(mask.CGImage, graySpace);
CGColorSpaceRelease(graySpace);
//combine images
CGImageRef resultRef = CGImageCreateWithMask(image.CGImage, maskRef);
UIImage *result = [UIImage imageWithCGImage:resultRef];
CGImageRelease(resultRef);
CGImageRelease(maskRef);
//display result
self.imageView.image = result;
}

@end

    对每张图片都使用两个独立的文件确实有些累赘。JPNG的库(https://github.com/nicklockwood/JPNG)对这个技术提供了一个开源的可以复用的实现,并且添加了直接使用+imageNamed:+imageWithContentsOfFile:方法的支持。

JPEG 2000

    除了JPEG和PNG之外iOS还支持别的一些格式,例如TIFF和GIF,但是由于他们质量压缩得更厉害,性能比JPEG和PNG糟糕的多,所以大多数情况并不用考虑。

    但是iOS之后,苹果低调添加了对JPEG 2000图片格式的支持,所以大多数人并不知道。它甚至并不被Xcode很好的支持 - JPEG 2000图片都没在Interface Builder中显示。

    但是JPEG 2000图片在(设备和模拟器)运行时会有效,而且比JPEG质量更好,同样也对透明通道有很好的支持。但是JPEG 2000图片在加载和显示图片方面明显要比PNG和JPEG慢得多,所以对图片大小比运行效率更敏感的时候,使用它是一个不错的选择。

    但仍然要对JPEG 2000保持关注,因为在后续iOS版本说不定就对它的性能做提升,但是在现阶段,混合图片对更小尺寸和质量的文件性能会更好。

PVRTC

    当前市场的每个iOS设备都使用了Imagination Technologies PowerVR图像芯片作为GPU。PowerVR芯片支持一种叫做PVRTC(PowerVR Texture Compression)的标准图片压缩。

    和iOS上可用的大多数图片格式不同,PVRTC不用提前解压就可以被直接绘制到屏幕上。这意味着在加载图片之后不需要有解压操作,所以内存中的图片比其他图片格式大大减少了(这取决于压缩设置,大概只有1/60那么大)。

    但是PVRTC仍然有一些弊端:

  • 尽管加载的时候消耗了更少的RAM,PVRTC文件比JPEG要大,有时候甚至比PNG还要大(这取决于具体内容),因为压缩算法是针对于性能,而不是文件尺寸。

  • PVRTC必须要是二维正方形,如果源图片不满足这些要求,那必须要在转换成PVRTC的时候强制拉伸或者填充空白空间。

  • 质量并不是很好,尤其是透明图片。通常看起来更像严重压缩的JPEG文件。

  • PVRTC不能用Core Graphics绘制,也不能在普通的UIImageView显示,也不能直接用作图层的内容。你必须要用作OpenGL纹理加载PVRTC图片,然后映射到一对三角板来在CAEAGLLayer或者GLKView中显示。

  • 创建一个OpenGL纹理来绘制PVRTC图片的开销相当昂贵。除非你想把所有图片绘制到一个相同的上下文,不然这完全不能发挥PVRTC的优势。

  • PVRTC使用了一个不对称的压缩算法。尽管它几乎立即解压,但是压缩过程相当漫长。在一个现代快速的桌面Mac电脑上,它甚至要消耗一分钟甚至更多来生成一个PVRTC大图。因此在iOS设备上最好不要实时生成。

    如果你愿意使用OpehGL,而且即使提前生成图片也能忍受得了,那么PVRTC将会提供相对于别的可用格式来说非常高效的加载性能。比如,可以在主线程1/60秒之内加载并显示一张2048×2048的PVRTC图片(这已经足够大来填充一个视网膜屏幕的iPad了),这就避免了很多使用线程或者缓存等等复杂的技术难度。

    Xcode包含了一些命令行工具例如texturetool来生成PVRTC图片,但是用起来很不方便(它存在于Xcode应用程序束中),而且很受限制。一个更好的方案就是使用Imagination Technologies PVRTexTool,可以从http://www.imgtec.com/powervr/insider/sdkdownloads免费获得。

    安装了PVRTexTool之后,就可以使用如下命令在终端中把一个合适大小的PNG图片转换成PVRTC文件:

/Applications/Imagination/PowerVR/GraphicsSDK/PVRTexTool/CL/OSX_x86/PVRTexToolCL -i {input_file_name}.png -o {output_file_name}.pvr -legacypvr -p -f PVRTC1_4 -q pvrtcbest

    清单14.8的代码展示了加载和显示PVRTC图片的步骤(第6章CAEAGLLayer例子代码改动而来)。

清单14.8 加载和显示PVRTC图片

#import "ViewController.h" 
#import
#import

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *glView;
@property (nonatomic, strong) EAGLContext *glContext;
@property (nonatomic, strong) CAEAGLLayer *glLayer;
@property (nonatomic, assign) GLuint framebuffer;
@property (nonatomic, assign) GLuint colorRenderbuffer;
@property (nonatomic, assign) GLint framebufferWidth;
@property (nonatomic, assign) GLint framebufferHeight;
@property (nonatomic, strong) GLKBaseEffect *effect;
@property (nonatomic, strong) GLKTextureInfo *textureInfo;

@end

@implementation ViewController

- (void)setUpBuffers
{
//set up frame buffer
glGenFramebuffers(1, &_framebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, _framebuffer);
//set up color render buffer
glGenRenderbuffers(1, &_colorRenderbuffer);
glBindRenderbuffer(GL_RENDERBUFFER, _colorRenderbuffer);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _colorRenderbuffer);
[self.glContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.glLayer];
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &_framebufferWidth);
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &_framebufferHeight);
//check success
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
NSLog(@"Failed to make complete framebuffer object: %i", glCheckFramebufferStatus(GL_FRAMEBUFFER));
}
}

- (void)tearDownBuffers
{
if (_framebuffer) {
//delete framebuffer
glDeleteFramebuffers(1, &_framebuffer);
_framebuffer = 0;
}
if (_colorRenderbuffer) {
//delete color render buffer
glDeleteRenderbuffers(1, &_colorRenderbuffer);
_colorRenderbuffer = 0;
}
}

- (void)drawFrame
{
//bind framebuffer & set viewport
glBindFramebuffer(GL_FRAMEBUFFER, _framebuffer);
glViewport(0, 0, _framebufferWidth, _framebufferHeight);
//bind shader program
[self.effect prepareToDraw];
//clear the screen
glClear(GL_COLOR_BUFFER_BIT);
glClearColor(0.0, 0.0, 0.0, 0.0);
//set up vertices
GLfloat vertices[] = {
-1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f
};
//set up colors
GLfloat texCoords[] = {
0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f
};
//draw triangle
glEnableVertexAttribArray(GLKVertexAttribPosition);
glEnableVertexAttribArray(GLKVertexAttribTexCoord0);
glVertexAttribPointer(GLKVertexAttribPosition, 2, GL_FLOAT, GL_FALSE, 0, vertices);
glVertexAttribPointer(GLKVertexAttribTexCoord0, 2, GL_FLOAT, GL_FALSE, 0, texCoords);
glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
//present render buffer
glBindRenderbuffer(GL_RENDERBUFFER, _colorRenderbuffer);
[self.glContext presentRenderbuffer:GL_RENDERBUFFER];
}

- (void)viewDidLoad
{
[super viewDidLoad];
//set up context
self.glContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
[EAGLContext setCurrentContext:self.glContext];
//set up layer
self.glLayer = [CAEAGLLayer layer];
self.glLayer.frame = self.glView.bounds;
self.glLayer.opaque = NO;
[self.glView.layer addSublayer:self.glLayer];
self.glLayer.drawableProperties = @{kEAGLDrawablePropertyRetainedBacking: @NO, kEAGLDrawablePropertyColorFormat: kEAGLColorFormatRGBA8};
//load texture
glActiveTexture(GL_TEXTURE0);
NSString *imageFile = [[NSBundle mainBundle] pathForResource:@"Snowman" ofType:@"pvr"];
self.textureInfo = [GLKTextureLoader textureWithContentsOfFile:imageFile options:nil error:NULL];
//create texture
GLKEffectPropertyTexture *texture = [[GLKEffectPropertyTexture alloc] init];
texture.enabled = YES;
texture.envMode = GLKTextureEnvModeDecal;
texture.name = self.textureInfo.name;
//set up base effect
self.effect = [[GLKBaseEffect alloc] init];
self.effect.texture2d0.name = texture.name;
//set up buffers
[self setUpBuffers];
//draw frame
[self drawFrame];
}

- (void)viewDidUnload
{
[self tearDownBuffers];
[super viewDidUnload];
}

- (void)dealloc
{
[self tearDownBuffers];
[EAGLContext setCurrentContext:nil];
}

@end

    如你所见,非常不容易,如果你对在常规应用中使用PVRTC图片很感兴趣的话(例如基于OpenGL的游戏),可以参考一下GLView的库(https://github.com/nicklockwood/GLView),它提供了一个简单的GLImageView类,重新实现了UIImageView的各种功能,但同时提供了PVRTC图片,而不需要你写任何OpenGL代码。


总结

    在这章中,我们研究了和图片加载解压相关的性能问题,并延展了一系列解决方案。

    在第15章“图层性能”中,我们将讨论和图层渲染和组合相关的性能问题。

收起阅读 »

iOS - 图像IO 二

缓存    如果有很多张图片要显示,最好不要提前把所有都加载进来,而是应该当移出屏幕之后立刻销毁。通过选择性的缓存,你就可以避免来回滚动时图片重复性的加载了。    缓存其实很简单...
继续阅读 »

缓存

    如果有很多张图片要显示,最好不要提前把所有都加载进来,而是应该当移出屏幕之后立刻销毁。通过选择性的缓存,你就可以避免来回滚动时图片重复性的加载了。

    缓存其实很简单:就是存储昂贵计算后的结果(或者是从闪存或者网络加载的文件)在内存中,以便后续使用,这样访问起来很快。问题在于缓存本质上是一个权衡过程 - 为了提升性能而消耗了内存,但是由于内存是一个非常宝贵的资源,所以不能把所有东西都做缓存。

    何时将何物做缓存(做多久)并不总是很明显。幸运的是,大多情况下,iOS都为我们做好了图片的缓存。

+imageNamed:方法

    之前我们提到使用[UIImage imageNamed:]加载图片有个好处在于可以立刻解压图片而不用等到绘制的时候。但是[UIImage imageNamed:]方法有另一个非常显著的好处:它在内存中自动缓存了解压后的图片,即使你自己没有保留对它的任何引用。

    对于iOS应用那些主要的图片(例如图标,按钮和背景图片),使用[UIImage imageNamed:]加载图片是最简单最有效的方式。在nib文件中引用的图片同样也是这个机制,所以你很多时候都在隐式的使用它。

    但是[UIImage imageNamed:]并不适用任何情况。它为用户界面做了优化,但是并不是对应用程序需要显示的所有类型的图片都适用。有些时候你还是要实现自己的缓存机制,原因如下:

  • [UIImage imageNamed:]方法仅仅适用于在应用程序资源束目录下的图片,但是大多数应用的许多图片都要从网络或者是用户的相机中获取,所以[UIImage imageNamed:]就没法用了。

  • [UIImage imageNamed:]缓存用来存储应用界面的图片(按钮,背景等等)。如果对照片这种大图也用这种缓存,那么iOS系统就很可能会移除这些图片来节省内存。那么在切换页面时性能就会下降,因为这些图片都需要重新加载。对传送器的图片使用一个单独的缓存机制就可以把它和应用图片的生命周期解耦。

  • [UIImage imageNamed:]缓存机制并不是公开的,所以你不能很好地控制它。例如,你没法做到检测图片是否在加载之前就做了缓存,不能够设置缓存大小,当图片没用的时候也不能把它从缓存中移除。

自定义缓存

    构建一个所谓的缓存系统非常困难。菲尔 卡尔顿曾经说过:“在计算机科学中只有两件难事:缓存和命名”。

    如果要写自己的图片缓存的话,那该如何实现呢?让我们来看看要涉及哪些方面:

  • 选择一个合适的缓存键 - 缓存键用来做图片的唯一标识。如果实时创建图片,通常不太好生成一个字符串来区分别的图片。在我们的图片传送带例子中就很简单,我们可以用图片的文件名或者表格索引。

  • 提前缓存 - 如果生成和加载数据的代价很大,你可能想当第一次需要用到的时候再去加载和缓存。提前加载的逻辑是应用内在就有的,但是在我们的例子中,这也非常好实现,因为对于一个给定的位置和滚动方向,我们就可以精确地判断出哪一张图片将会出现。

  • 缓存失效 - 如果图片文件发生了变化,怎样才能通知到缓存更新呢?这是个非常困难的问题(就像菲尔 卡尔顿提到的),但是幸运的是当从程序资源加载静态图片的时候并不需要考虑这些。对用户提供的图片来说(可能会被修改或者覆盖),一个比较好的方式就是当图片缓存的时候打上一个时间戳以便当文件更新的时候作比较。

  • 缓存回收 - 当内存不够的时候,如何判断哪些缓存需要清空呢?这就需要到你写一个合适的算法了。幸运的是,对缓存回收的问题,苹果提供了一个叫做NSCache通用的解决方案

NSCache

    NSCacheNSDictionary类似。你可以通过-setObject:forKey:-object:forKey:方法分别来插入,检索。和字典不同的是,NSCache在系统低内存的时候自动丢弃存储的对象。

    NSCache用来判断何时丢弃对象的算法并没有在文档中给出,但是你可以使用-setCountLimit:方法设置缓存大小,以及-setObject:forKey:cost:来对每个存储的对象指定消耗的值来提供一些暗示。

    指定消耗数值可以用来指定相对的重建成本。如果对大图指定一个大的消耗值,那么缓存就知道这些物体的存储更加昂贵,于是当有大的性能问题的时候才会丢弃这些物体。你也可以用-setTotalCostLimit:方法来指定全体缓存的尺寸。

    NSCache是一个普遍的缓存解决方案,我们创建一个比传送器案例更好的自定义的缓存类。(例如,我们可以基于不同的缓存图片索引和当前中间索引来判断哪些图片需要首先被释放)。但是NSCache对我们当前的缓存需求来说已经足够了;没必要过早做优化。

    使用图片缓存和提前加载的实现来扩展之前的传送器案例,然后来看看是否效果更好(见清单14.5)。

清单14.5 添加缓存

#import "ViewController.h"

@interface ViewController()

@property (nonatomic, copy) NSArray *imagePaths;
@property (nonatomic, weak) IBOutlet UICollectionView *collectionView;

@end

@implementation ViewController

- (void)viewDidLoad
{
//set up data
self.imagePaths = [[NSBundle mainBundle] pathsForResourcesOfType:@"png" inDirectory:@"Vacation Photos"];
//register cell class
[self.collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"Cell"];
}

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return [self.imagePaths count];
}

- (UIImage *)loadImageAtIndex:(NSUInteger)index
{
//set up cache
static NSCache *cache = nil;
if (!cache) {
cache = [[NSCache alloc] init];
}
//if already cached, return immediately
UIImage *image = [cache objectForKey:@(index)];
if (image) {
return [image isKindOfClass:[NSNull class]]? nil: image;
}
//set placeholder to avoid reloading image multiple times
[cache setObject:[NSNull null] forKey:@(index)];
//switch to background thread
dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
//load image
NSString *imagePath = self.imagePaths[index];
UIImage *image = [UIImage imageWithContentsOfFile:imagePath];
//redraw image using device context
UIGraphicsBeginImageContextWithOptions(image.size, YES, 0);
[image drawAtPoint:CGPointZero];
image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
//set image for correct image view
dispatch_async(dispatch_get_main_queue(), ^{ //cache the image
[cache setObject:image forKey:@(index)];
//display the image
NSIndexPath *indexPath = [NSIndexPath indexPathForItem: index inSection:0]; UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:indexPath];
UIImageView *imageView = [cell.contentView.subviews lastObject];
imageView.image = image;
});
});
//not loaded yet
return nil;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
//dequeue cell
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath];
//add image view
UIImageView *imageView = [cell.contentView.subviews lastObject];
if (!imageView) {
imageView = [[UIImageView alloc] initWithFrame:cell.contentView.bounds];
imageView.contentMode = UIViewContentModeScaleAspectFit;
[cell.contentView addSubview:imageView];
}
//set or load image for this index
imageView.image = [self loadImageAtIndex:indexPath.item];
//preload image for previous and next index
if (indexPath.item < [self.imagePaths count] - 1) {
[self loadImageAtIndex:indexPath.item + 1]; }
if (indexPath.item > 0) {
[self loadImageAtIndex:indexPath.item - 1]; }
return cell;
}

@end

    果然效果更好了!当滚动的时候虽然还有一些图片进入的延迟,但是已经非常罕见了。缓存意味着我们做了更少的加载。这里提前加载逻辑非常粗暴,其实可以把滑动速度和方向也考虑进来,但这已经比之前没做缓存的版本好很多了。

收起阅读 »

用 JavaScript 做数独

最近看到老婆天天在手机上玩数独,突然想起 N 年前刷 LeetCode 的时候,有个类似的算法题(37.解数独),是不是可以把这个算法进行可视化。 说干就干,经过一个小时的实践,最终效果如下: 怎么解数独 解数独之前,我们先了解一下数独的规则: 数字 1-...
继续阅读 »

最近看到老婆天天在手机上玩数独,突然想起 N 年前刷 LeetCode 的时候,有个类似的算法题(37.解数独),是不是可以把这个算法进行可视化。


说干就干,经过一个小时的实践,最终效果如下:



怎么解数独


解数独之前,我们先了解一下数独的规则:



  1. 数字 1-9 在每一行只能出现一次。

  2. 数字 1-9 在每一列只能出现一次。

  3. 数字 1-9 在每一个以粗实线分隔的九宫格( 3x3 )内只能出现一次。



接下来,我们要做的就是在每个格子里面填一个数字,然后判断这个数字是否违反规定。


填第一个格子


首先,在第一个格子填 1,发现在第一列里面已经存在一个 1,此时就需要擦掉前面填的数字 1,然后在格子里填上 2,发现数字在行、列、九宫格内均无重复。那么这个格子就填成功了。



填第二个格子


下面看第二个格子,和前面一样,先试试填 1,发现在行、列、九宫格内的数字均无重复,那这个格子也填成功了。



填第三个格子


下面看看第三个格子,由于前面两个格子,我们已经填过数字 12,所以,我们直接从数字 3 开始填。填 3 后,发现在第一行里面已经存在一个 3,然后在格子里填上 4,发现数字 4 在行和九宫格内均出现重复,依旧不成功,然后尝试填上数字 5,终于没有了重复数字,表示填充成功。



一直填,直到填到第九个格子


照这个思路,一直填到第九个格子,这个时候,会发现,最后一个数字 9 在九宫格内冲突了。而 9 已经是最后一个数字了,这里没办法填其他数字了,只能返回上一个格子,把第七个格子的数字从 8 换到 9,发现在九宫格内依然冲突。


此时需要替换上上个格子的数字(第六个格子)。直到没有冲突为止,所以在这个过程中,不仅要往后填数字,还要回过头看看前面的数字有没有问题,不停地尝试。



综上所述


解数独就是一个不断尝试的过程,每个格子把数字 1-9 都尝试一遍,如果出现冲突就擦掉这个数字,直到所有的格子都填完。



通过代码来实现


把上面的解法反映到代码上,就需要通过 递归 + 回溯 的思路来实现。


在写代码之前,先看看怎么把数独表示出来,这里参考 leetcode 上的题目:37. 解数独



前面的这个题目,可以使用一个二维数组来表示。最外层数组内一共有 9 个数组,表示数独的 9 行,内部的每个数组内 9 字符分别对应数组的列,未填充的空格通过字符('.' )来表示。


const sudoku = [
['.', '.', '.', '4', '.', '.', '.', '3', '.'],
['7', '.', '4', '8', '.', '.', '1', '.', '2'],
['.', '.', '.', '2', '3', '.', '4', '.', '9'],
['.', '4', '.', '5', '.', '9', '.', '8', '.'],
['5', '.', '.', '.', '.', '.', '9', '1', '3'],
['1', '.', '.', '.', '8', '.', '2', '.', '4'],
['.', '.', '.', '.', '.', '.', '3', '4', '5'],
['.', '5', '1', '9', '4', '.', '7', '2', '.'],
['4', '7', '3', '.', '5', '.', '.', '9', '1'],
]

知道如何表示数组后,我们再来写代码。


const sudoku = [……]
// 方法接受行、列两个参数,用于定位数独的格子
function solve(row, col) {
if (col >= 9) {
// 超过第九列,表示这一行已经结束了,需要另起一行
col = 0
row += 1
if (row >= 9) {
// 另起一行后,超过第九行,则整个数独已经做完
return true
}
}
if (sudoku[row][col] !== '.') {
// 如果该格子已经填过了,填后面的格子
return solve(row, col + 1)
}
// 尝试在该格子中填入数字 1-9
for (let num = 1; num <= 9; num++) {
if (!isValid(row, col, num)) {
// 如果是无效数字,跳过该数字
continue
}
// 填入数字
sudoku[row][col] = num.toString()
// 继续填后面的格子
if (solve(row, col + 1)) {
// 如果一直到最后都没问题,则这个格子的数字没问题
return true
}
// 如果出现了问题,solve 返回了 false
// 说明这个地方要重填
sudoku[row][col] = '.' // 擦除数字
}
// 数字 1-9 都填失败了,说明前面的数字有问题
// 返回 FALSE,进行回溯,前面数字要进行重填
return false
}

上面的代码只是实现了递归、回溯的部分,还有一个 isValid 方法没有实现。该方法主要就是按照数独的规则进行一次校验。


const sudoku = [……]
function isValid(row, col, num) {
// 判断行里是否重复
for (let i = 0; i < 9; i++) {
if (sudoku[row][i] === num) {
return false
}
}
// 判断列里是否重复
for (let i = 0; i < 9; i++) {
if (sudoku[i][col] === num) {
return false
}
}
// 判断九宫格里是否重复
const startRow = parseInt(row / 3) * 3
const startCol = parseInt(col / 3) * 3
for (let i = startRow; i < startRow + 3; i++) {
for (let j = startCol; j < startCol + 3; j++) {
if (sudoku[i][j] === num) {
return false
}
}
}
return true
}

通过上面的代码,我们就能解出一个数独了。


const sudoku = [
['.', '.', '.', '4', '.', '.', '.', '3', '.'],
['7', '.', '4', '8', '.', '.', '1', '.', '2'],
['.', '.', '.', '2', '3', '.', '4', '.', '9'],
['.', '4', '.', '5', '.', '9', '.', '8', '.'],
['5', '.', '.', '.', '.', '.', '9', '1', '3'],
['1', '.', '.', '.', '8', '.', '2', '.', '4'],
['.', '.', '.', '.', '.', '.', '3', '4', '5'],
['.', '5', '1', '9', '4', '.', '7', '2', '.'],
['4', '7', '3', '.', '5', '.', '.', '9', '1']
]
function isValid(row, col, num) {……}
function solve(row, col) {……}
solve(0, 0) // 从第一个格子开始解
console.log(sudoku) // 输出结果

输出结果


动态展示做题过程


有了上面的理论知识,我们就可以把这个做题的过程套到 react 中,动态的展示做题的过程,也就是文章最开始的 Gif 中的那个样子。


这里直接使用 create-react-app 脚手架快速启动一个项目。


npx create-react-app sudoku
cd sudoku

打开 App.jsx ,开始写代码。


import React from 'react';
import './App.css';

class App extends React.Component {
state = {
// 在 state 中配置一个数独二维数组
sudoku: [
['.', '.', '.', '4', '.', '.', '.', '3', '.'],
['7', '.', '4', '8', '.', '.', '1', '.', '2'],
['.', '.', '.', '2', '3', '.', '4', '.', '9'],
['.', '4', '.', '5', '.', '9', '.', '8', '.'],
['5', '.', '.', '.', '.', '.', '9', '1', '3'],
['1', '.', '.', '.', '8', '.', '2', '.', '4'],
['.', '.', '.', '.', '.', '.', '3', '4', '5'],
['.', '5', '1', '9', '4', '.', '7', '2', '.'],
['4', '7', '3', '.', '5', '.', '.', '9', '1']
]
}

// TODO:解数独
solveSudoku = async () => {
const { sudoku } = this.state
}

render() {
const { sudoku } = this.state
return (
<div className="container">
<div className="wrapper">
{/* 遍历二维数组,生成九宫格 */}
{sudoku.map((list, row) => (
{/* div.row 对应数独的行 */}
<div className="row" key={`row-${row}`}>
{list.map((item, col) => (
{/* span 对应数独的每个格子 */}
<span key={`box-${col}`}>{ item !== '.' && item }</span>
))}
</div>
))}
<button onClick={this.solveSudoku}>开始做题</button>
</div>
</div>
);
}
}

九宫格样式


给每个格子加上一个虚线的边框,先让它有一点九宫格的样子。


.row {
display: flex;
direction: row;
/* 行内元素居中 */
justify-content: center;
align-content: center;
}
.row span {
/* 每个格子宽高一致 */
width: 30px;
min-height: 30px;
line-height: 30px;
text-align: center;
/* 设置虚线边框 */
border: 1px dashed #999;
}

可以得到一个这样的图形:



接下来,需要给外边框和每个九宫格加上实线的边框,具体代码如下:


/* 第 1 行顶部加上实现边框 */
.row:nth-child(1) span {
border-top: 3px solid #333;
}
/* 第 3、6、9 行底部加上实现边框 */
.row:nth-child(3n) span {
border-bottom: 3px solid #333;
}
/* 第 1 列左边加上实现边框 */
.row span:first-child {
border-left: 3px solid #333;
}

/* 第 3、6、9 列右边加上实现边框 */
.row span:nth-child(3n) {
border-right: 3px solid #333;
}

这里会发现第三、六列的右边边框和第四、七列的左边边框会有点重叠,第三、六行的底部边框和第四、七行的顶部边框也会有这个问题,所以,我们还需要将第四、七列的左边边框和第三、六行的底部边框进行隐藏。



.row:nth-child(3n + 1) span {
border-top: none;
}
.row span:nth-child(3n + 1) {
border-left: none;
}

做题逻辑


样式写好后,就可以继续完善做题的逻辑了。


class App extends React.Component {
state = {
// 在 state 中配置一个数独二维数组
sudoku: [……]
}

solveSudoku = async () => {
const { sudoku } = this.state
// 判断填入的数字是否有效,参考上面的代码,这里不再重复
const isValid = (row, col, num) => {
……
}
// 递归+回溯的方式进行解题
const solve = async (row, col) => {
if (col >= 9) {
col = 0
row += 1
if (row >= 9) return true
}
if (sudoku[row][col] !== '.') {
return solve(row, col + 1)
}
for (let num = 1; num <= 9; num++) {
if (!isValid(row, col, num)) {
continue
}

sudoku[row][col] = num.toString()
this.setState({ sudoku }) // 填了格子之后,需要同步到 state

if (solve(row, col + 1)) {
return true
}

sudoku[row][col] = '.'
this.setState({ sudoku }) // 填了格子之后,需要同步到 state
}
return false
}
// 进行解题
solve(0, 0)
}

render() {
const { sudoku } = this.state
return (……)
}
}

对比之前的逻辑,这里只是在对数独的二维数组填空后,调用了 this.setStatesudoku 同步到了 state 中。


function solve(row, col) {   ……   sudoku[row][col] = num.toString()+  this.setState({ sudoku })	 ……   sudoku[row][col] = '.'+  this.setState({ sudoku }) // 填了格子之后,需要同步到 state}

在调用 solveSudoku 后,发现并没有出现动态的效果,而是直接一步到位的将结果同步到了视图中。



这是因为 setState 是一个伪异步调用,在一个事件任务中,所有的 setState 都会被合并成一次,需要看到动态的做题过程,我们需要将每一次 setState 操作放到该事件流之外,也就是放到 setTimeout 中。更多关于 setState 异步的问题,可以参考我之前的文章:React 中 setState 是一个宏任务还是微任务?


solveSudoku = async () => {
const { sudoku } = this.state
// 判断填入的数字是否有效,参考上面的代码,这里不再重复
const isValid = (row, col, num) => {
……
}
// 脱离事件流,调用 setState
const setSudoku = async (row, col, value) => {
sudoku[row][col] = value
return new Promise(resolve => {
setTimeout(() => {
this.setState({
sudoku
}, () => resolve())
})
})
}
// 递归+回溯的方式进行解题
const solve = async (row, col) => {
……
for (let num = 1; num <= 9; num++) {
if (!isValid(row, col, num)) {
continue
}

await setSudoku(row, col, num.toString())

if (await solve(row, col + 1)) {
return true
}

await setSudoku(row, col, '.')
}
return false
}
// 进行解题
solve(0, 0)
}

最后效果如下:



作者:Shenfq
链接:https://juejin.cn/post/7004616375591239711

收起阅读 »

Retorfit + 协程机制 + MVVM

协程是一种解决方案,是一种解决嵌套,并发、弱化线程概念的方案。能让多个任务之间更好的协作,能够以同步的方式编排代码完成异步工作,将异步代码写的像同步代码一样直观。 重点 协程的本质是方法的挂起与恢复:return + callback 协程是什么: ...
继续阅读 »

协程是一种解决方案,是一种解决嵌套,并发、弱化线程概念的方案。能让多个任务之间更好的协作,能够以同步的方式编排代码完成异步工作,将异步代码写的像同步代码一样直观。




重点
协程的本质是方法的挂起恢复:return + callback





  • 协程是什么:


协程是可以由程序自行控制挂起、恢复的程序
协程可以实现多任务的协作执行
协程可以用来解决异步任务控制流的灵活转移



  • 协程的作用:


协程可以让异步代码同步化
协程可以降低异步程序的设计复杂度
协程本身不能让代码异步,只是让异步代码更简单控制流程




总结一句话:协程就是将程序进行挂起和恢复,解决异步回调让异步代码同步化。



什么是协程


Kotlin 协程的标准库:kotlinx-coroutines-core 它是协程的框架层的,而kotlin本身带有协程的基础层,要是用协程框架层封装需要引入依赖库,才能使用协程的框架层


 implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2'
复制代码

常见的异步回调: 在enqueue中创建回调实例接受回调


retrofit.create(GitHubService::class.java).listRepos("octocat")
.enqueue(object : Callback<List<Repo>> {
override fun onResponse(call: Call<List<Repo>>, response: Response<List<Repo>>) {
println("retrofit:" + response.body()?.size)
}

override fun onFailure(call: Call<List<Repo>>, t: Throwable) {
TODO("Not yet implemented")
}
})

经过协程改造的异步程序:



  1. 在函数前面加上suspend 定义为挂起函数,在retrofit 2.6以上的版本就已经支持协程了


    /**
* kotlin 协程
*/
@GET("users/{user}/repos")
suspend fun listReposKx(@Path("user") user: String?): List<Repo>


  1. 协程的方式请求网络,将异步代码写成同步代码更加简单化


code.png
执行结果:



协程函数的挂起需要有一个CoroutineScope空间,函数挂起并不会影响CoroutineScope独立空间之外的代码执行,非阻塞式的挂起
注意:通过suspend修饰的挂起函数只能被挂起函数协程调用。



image.png


协程运行:遇到挂起函数listReposKx在线程2中执行,执行完毕后返回结果继续执行。挂起恢复
image.png



协程和线程的区别:
线程Thread:指操作系统的线程,内核线程
协程Coroutines: 指语言实现的协程,运行在内核线程之上的



协程的基本要素



Kotlin的协程分为两层:



  • 基础设施层:标准库的协程API,主要对协程提供了概念和语义上最基本的支持

  • 业务框架层: 协程的上层框架支持



在上述代码中GlobalScope.launch其实是业务框架层的实现。如果抛去框架层的封装,kotlin提供的基础层是如何实现协程的?这就需要了解协程的基本要素
code.png


如下代码:就是kotlin协程的基础层API包括:suspend挂起函数ContinuationcreateCoroutine创建协程startCoroutine启动协程CoroutinContext协程上下文。这五个就是协程的基本要素
code.png


image.png


挂起函数suspend



挂起点: suspend修饰的函数,只能在其他挂起函数或者协程中调用。
挂起函数调用时包含了协程的“挂起”的语义,而挂起函数返回时则包含了协程的“恢复”语义。



挂起和恢复主要通过:Continuation来实现,如下代码在service中添加的suspend函数反编译后的结果,在挂起函数中多了一个参数Continuation
code.png
其实在上述的createCoroutine中传递了一个Continuation实例,而Continuation主要的作用就是执行挂起函数“恢复”后的代码并且得到挂起函数的返回值作为参数返回


如何将异步回调通过挂起函数的改造呢?其实就是retrofit中是如何处理suspend函数的,通过kotlin的基础层API代码如下:通过suspendCoroutine来实现挂起函数,在通过Continuation返回异步回调请求的结果
code.png
Retrofit中的实现其实是一样的原理,Retrofit通过对Call进行了扩展方法处理,在Service API 的suspend方法的基础上又调用了Retrofit自己写的suspend函数
code.png
其实对于常见的框架层的封装例如xxxScope.launch:就是基于kotlin的基础层的API进行实现
code.png


协程上下文



在协程上下文(CoroutineContext)中存在着拦截器ContinuationInterceptor是一类协程上下文元素,可以对协程上下文所在协程的Continuation进行拦截,可以实现切换线程。
协程上下文包含一个 协程调度器 (参见 CoroutineDispatcher)它确定了相关的协程在哪个线程或哪些线程上执行。协程调度器可以将协程限制在一个特定的线程执行,或将它分派到一个线程池,亦或是让它不受限地运行。
所有的协程构建器诸如 launchasync 接收一个可选的 CoroutineContext 参数,它可以被用来显式的为一个新协程或其它上下文元素指定一个调度器。



image.png


协程框架在Android上的应用



Kotlin 的框架层基于基础层进行的封装:kotlinx-coroutines



Kotlin 提供了协程框架的基础库以及协程Android库


//kotlin 标准库-提供了协程的基础层
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
def coroutines_version = "1.4.2"
//kotlin协程依赖库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
//协程Android库,提供AndroidUI调度器
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"

和协程相关的一些扩展库
image.png


image.png



官方文档中都有讲解这里不在复述了,重点讲解协程在Android中的应用。




  • Job: 协程的启动模式

  • 调度器:协程的线程调度(将协程在特有的线程或者线程池中执行)

  • 作用域:协程的作用域(在Android中常见的:lifecycleScope viewmodelScope)

  • Channel:“热”数据流,并发安全的通信机制(不去订阅也会发数据)

  • Flow:“冷”数据流,协程的响应式API(和RxJava类似,只有订阅了才会发送数据)

  • Select:可以对多个挂起事件进行等待


协程简化Dialog


创建Dialog的挂起函数给Context扩展alert方法如下代码:



通过suspendCancellableCoroutine 创建挂起函数,将异步流程同步化



code.png
通过协程来调用挂起函数:



lifecycleScope 和Activity/Fragment的生命周期绑定,可以直接拿到弹窗点击的结果进行处理



        val show_dialog = findViewById<Button>(R.id.show_dialog)
show_dialog.setOnClickListener {
lifecycleScope.launch {
//Activity生命周期的协程
val myCheck = alert("警告", "Do You Want is?")
Log.e("TAG", "onCreate: $myCheck")
}
}

简化Handler的调用



Handler.post的异步调用



code.png


调用方式如下:


        lifecycleScope.launch {
val handler = Handler(Looper.getMainLooper())
val h1 = handler.run { "test"}
Log.e("TAG", "onCreate: $h1") // test
val h2 = handler.runDelay(1000) { "delay" }
Log.e("TAG", "onCreate: $h2") // delay
}


协程:可以将所有的异步调用简化为同步调用的解决方案



Job 启动模式


code.png
image.pngimage.png


image.pngimage.png


调度器


image.pngimage.png


Retrofit+协程配合LiveData的封装原理



协程只是为了解决异步调用问题而存在的,Retrofit中内部已经将异步问题通过协程解决了,协程更多的是通过和LiveData和ViewModel结合再次简化封装部分业务逻辑。



需要引入的依赖库:



在解析josn的converter,kotlin推荐的moshi,可以调研调研。



    //retrofit
api rootProject.depsLibs.retrofit
api rootProject.depsLibs.logging_interceptor
api rootProject.depsLibs.converter_gson

//协程相关
def coroutines_version = "1.4.2"
//kotlin协程依赖库
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
//协程Android库,提供AndroidUI调度器
api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
api "androidx.core:core-ktx:1.3.2"
api "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1"
api "androidx.lifecycle:lifecycle-livedata-core-ktx:2.3.1"

网络请求的生命周期管理由下面依赖库进行管理



lifecycleScope 来定义Activity/Fragment的生命周期管理的协程运行网络请求的挂起函数或者通过viewModelScope 的生命周期协程来管理网络请求的挂起函数(推荐)。
注意:Lifecycle 和 ViewModel的是不同的,ViewModel可以当屏幕发生旋转后继续存在。
关于Lifecycle和ViewModel的
本质
讲解看这里:
ViewModel 如何对视图状态管理
LiveData 如何安全观察数据



    api "androidx.lifecycle:lifecycle-runtime-ktx:2.3.1"
api "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1"

框架层设计


NetWorkManager 是网络框架的入口类,API设计:
code.png


网络请求返回数据结构设计:



**ResultData **作为最终返回的数据结构类.**ResponseBean**作为JSON的基础数据结构



code.png


通过Kotlin的密封类,用于区分网络状态,具体设计ApiResonse 主要包括网络异常状态空数据状态成功状态、业务错误状态。(对于code的业务错误状态.需要根据上述的错误处理器进行设置)
code.png
ApiResponse 通过静态方法进行构建不同状态的返回数据



有两种create构建方法,第一个是捕捉到的网络请求系统异常,第二个create首先判断是否是成功的状态码,如果不是返回ApiAppErrorResponse业务状态异常,data 为空表示返回空数据返回空数据下的状态,否则返回成功下的状态



code.png


核心类:RequestAction 通过kotlin的DSL包装网络请求动作




  • api: 传递的就是Retrofit的Service中定义的suspend 挂起函数

  • loadCache : 传递的就是加载缓存数据的函数

  • saveCache:传递的是实现缓存数据的函数



注意:这几个关键的方法都是传递DSL包装的函数


code.png
例如这样的调用方式:代码看起来会非常舒服


    @GET("users/{user}/repos")
suspend fun getJson2(@Path("user") user: String): ResponseBean<List<Repo>>

private val netApi = NetWorkManager.instance.create(Service::class.java)

val requestLiveData = viewModelScope.requestLiveData<List<Repo>> {
//请求网络
api { netApi.getJson2(name) }

//加载数据库缓存
loadCache {
//数据库请求,将结果返回给liveData中
_databaseLiveData
}

//将数据保存到数据库
saveCache {
//向数据库中保存数据
Log.e("TAG", "getRepo: ${it.size}")
}
}

下面就是通过协程来进行整合Retrofit+LiveData



CoroutineScope 是所有协程作用域的父类,对其进行扩展一个函数requestLiveData,liveData 是扩展依赖库提供的使其可以在LiveDataScope中运行挂起函数 emit 以及api.invoke都是挂起函数



code.png


异常处理设计


统一异常类:统一使用ResponseThrowable作为网络请求的所有异常类
code.png


IExceptionHandler 作为将异常转换为ResponseThrowable的实现,appSuccessCode 用来定义业务层的成功码,非成功码下的都是业务异常,其他情况为系统异常


code.png


IAdapterHandler 用来处理 IExceptionHandler 转换的ResponseThrowable异常


code.png


NetManager 的API



  1. 初始化设置


code.png



  1. 全局下异常处理的实现类如下,通过转换后的ResponseThrowable中的code判断处于哪个异常情况


code.png



  1. 将异常转换为ResponseThrowable



框架层默认实现了:DefaultExceptionHandler 如果不设置addExceptionHandler会使用默认的异常转换类



code.png



  1. MVVM 架构模式下在ViewModel中实现网络请求,外部通过repoLiveData监听数据,这里_repoLiveData 使用MediaorLiveData 只能监听数据变化不能改变数据,为了防止外部改变数据导致不可预期的错误


code.png



  1. View层实现监听网络数据



监听到的数据都是在框架层定义好的ResultData直接拿到状态等信息,注意在Error状态下所有的异常在框架层已经全部转换为了ResponseThrowable通过自定义异常中的code来判断具体的异常情况



code.png


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

Android正确的保活方案,不要掉进保活需求死循环陷进

在开始前,还是给大家简单介绍一下,以前出现过的一些黑科技: 大概在6年前Github中出现过一个叫MarsDaemon,这个库通过双进程守护的方式实现保活,一时间风头无两。好景不长,进入 Android 8.0时代之后,这个库就废掉了。 最近2年Github上...
继续阅读 »

在开始前,还是给大家简单介绍一下,以前出现过的一些黑科技:


大概在6年前Github中出现过一个叫MarsDaemon,这个库通过双进程守护的方式实现保活,一时间风头无两。好景不长,进入 Android 8.0时代之后,这个库就废掉了。


最近2年Github上面出来一个Leoric 感兴趣的可以去看一下源码,谁敢用在生产环境呢,也就自己玩玩的才会用吧(不能因为保活而导致手机卡巴斯基),我没有试过这个,我想说的是:黑科技能黑的了一时,能黑的了一世吗?


没有规矩,不成方圆,要提升产品的存活率,最终还是要落到产品本身上面来,尊重用户,提升用户体验才是正道。


以前我也是深受保活需求的压迫,最近发现QQ群里有人又提到了如何保活,那么我们就来说一说,如何来正确保活App?




Android 8.0之后: 加强了应用后台限制,当时测试过一组数据:



应用处于前台,启动一个前台Service,里面使用JobScheduler启动定时任务(30秒触发一次),
此时手机锁屏,前10分钟内,定时任务都是正常执行;

大概在12分钟左右,发现应用进程就被kill掉了,解锁屏幕,app也不在前台了;



各大国产手机厂商底层都经过自己魔改,自家都有自己的一套自启动管理,小米手机更乱(当时有个神隐模式的概念,那也是杀后台高手),只能说当时Android手机各种性能方面都不足,各家都会有自己的一套省电模式,以此来达到省电和提高手机性能,Android 系统变得越来越完善,但是厂商定制的自启动、省电模式还在,所以我们要做保活。


1.Android 8.0之前-常用的保活方案



1.开启一个前台Service

2.Android 6.0+ 忽略电池优化开关(稍后会有代码)

3.无障碍服务(只针对有用这个功能的app,如支付宝语音增强提醒用了它)





2.Android 8.0之后-常用的保活方案



1.开启一个前台Service(可以加上,单独启用的话无法满足保活需求)

2.Android 6.0+ 忽略电池优化开关(稍后会有代码)

3.无障碍服务(只针对有用这个功能的app,如支付宝语音增强提醒用了它)

4.应用自启动权限(最简单的方案是针对不同系统提供教程图片-让用户自己去打开)

5.多任务列表窗口加锁(提供GIF教程图片-让用户自己去打开)

6.多任务列表窗口隐藏App(仅针对有这方面需求的App)

7.应用后台高耗电(仅针对Vivo手机)



3.保活方案实现步骤


(1). 前台Service


//前台服务
class ForegroundCoreService : Service() {
override fun onBind(intent: Intent?): IBinder? = null
private var mForegroundNF:ForegroundNF by lazy {
ForegroundNF(this)
}
override fun onCreate() {
super.onCreate()
mForegroundNF.startForegroundNotification()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if(null == intent){
//服务被系统kill掉之后重启进来的
return START_NOT_STICKY
}
mForegroundNF.startForegroundNotification()
return super.onStartCommand(intent, flags, startId)
}
override fun onDestroy() {
mForegroundNF.stopForegroundNotification()
super.onDestroy()
}
}

//初始化前台通知,停止前台通知
class ForegroundNF(private val service: ForegroundCoreService) : ContextWrapper(service) {
companion object {
private const val START_ID = 101
private const val CHANNEL_ID = "app_foreground_service"
private const val CHANNEL_NAME = "前台保活服务"
}
private var mNotificationManager: NotificationManager? = null

private var mCompatBuilder:NotificationCompat.Builder?=null

private val compatBuilder: NotificationCompat.Builder?
get() {
if (mCompatBuilder == null) {
val notificationIntent = Intent(this, MainActivity::class.java)
notificationIntent.action = Intent.ACTION_MAIN
notificationIntent.addCategory(Intent.CATEGORY_LAUNCHER)
notificationIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
//动作意图
val pendingIntent = PendingIntent.getActivity(
this, (Math.random() * 10 + 10).toInt(),
notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT
)
val notificationBuilder: NotificationCompat.Builder = NotificationCompat.Builder(this,CHANNEL_ID)
//标题
notificationBuilder.setContentTitle(getString(R.string.notification_content))
//通知内容
notificationBuilder.setContentText(getString(R.string.notification_sub_content))
//状态栏显示的小图标
notificationBuilder.setSmallIcon(R.mipmap.ic_coolback_launcher)
//通知内容打开的意图
notificationBuilder.setContentIntent(pendingIntent)
mCompatBuilder = notificationBuilder
}
return mCompatBuilder
}

init {
createNotificationChannel()
}

//创建通知渠道
private fun createNotificationChannel() {
mNotificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
//针对8.0+系统
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
CHANNEL_NAME,
NotificationManager.IMPORTANCE_LOW
)
channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
channel.setShowBadge(false)
mNotificationManager?.createNotificationChannel(channel)
}
}

//开启前台通知
fun startForegroundNotification() {
service.startForeground(START_ID, compatBuilder?.build())
}

//停止前台服务并清除通知
fun stopForegroundNotification() {
mNotificationManager?.cancelAll()
service.stopForeground(true)
}
}

(2).忽略电池优化(Android 6.0+)


1.我们需要在AndroidManifest.xml中声明一下权限


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

2.通过Intent来请求忽略电池优化的权限(需要引导用户点击)


//在Activity的onCreate中注册ActivityResult,一定要在onCreate中注册
//监听onActivityForResult回调
mIgnoreBatteryResultContract = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult ->
//查询是否开启成功
if(queryBatteryOptimizeStatus()){
//忽略电池优化开启成功
}else{
//开启失败
}
}

通过Intent打开忽略电池优化弹框:


val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)
intent.data = Uri.parse("package:$packageName")
//启动忽略电池优化,会弹出一个系统的弹框,我们在上面的
launchActivityResult(intent)

查询是否成功开启忽略电池优化开关:


fun Context.queryBatteryOptimizeStatus():Boolean{
val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager?
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
powerManager?.isIgnoringBatteryOptimizations(packageName)?:false
} else {
true
}
}

(3).无障碍服务


看官方文档:创建自己的无障碍服务

它也是一个Service,它的优先级比较高,提供界面增强功能,初衷是帮助视觉障碍的用户或者是可能暂时无法与设备进行全面互动的用户完成操作。

可以做很多事情,使用了此Service,在6.0+不需要申请悬浮窗权限,直接使用WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY 挺方便的

(仅针对有需要此服务的app,可以开启增强后台保活)


(4).自启动权限(即:白名单管理列表页面)


是系统给用户自己去打开“自启动权限”开关的入口,我们需要针对不同的手机厂商和系统版本,弹出提示引导用户是否前去打开“自启动权限”

有的手机厂商叫:白名单管理,有的叫:自启动权限,两个是一个概念;

点击查看跳转到『手机自启动设置页面』完整代码


(需要注意:如果是代码控制跳转,无法保证永远可以调整,系统升级可能就给你屏蔽了,
最简单的方法是:显示一个如何找到自启动页面的引导图,下面以华为手机为例:)



华为手机-自启动管理

(5).多任务列表窗口加锁


可以针对不同手机厂商,显示引导用户,开启App窗口加锁之后,点击清理加速不会导致应用被kill



华为手机窗口加锁-教程图

(6).多任务列表窗口隐藏App窗口


刚刚上面多任务窗口加锁完,再提示用户去App里面把隐藏App窗口开关打开,这样用户就不会多任务列表里面把App窗口给手抖划掉


多任务窗口中『隐藏App窗口』,可以用如下代码控制:

(这个也只是针对有这方面需求App提供的一种增强方案罢了:因为隐藏了窗口,用户就不会去想他,不会去手痒去划掉它)


//在多任务列表页面隐藏App窗口
fun hideAppWindow(context: Context,isHide:Boolean){
try {
val activityManager: ActivityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
//控制App的窗口是否在多任务列表显示
activityManager.appTasks[0].setExcludeFromRecents(isHide)
}catch (e:Exception){
.....
}
}

(7).应用后台高耗电(Vivo手机独有)


开启的入口:“设置”>“电池”>“后台高耗电”>“找到xxxApp打开开关”



vivo允许后台高耗电



最后还是奉劝那些,仍然执着于找寻黑科技的开发者,醒醒吧,太阳晒屁股了。


如果说你的App用户群体不是普通用户,是专门给一些玩机大神们用的,都可以root手机的话,那么直接 move 到系统目录 priv/system/app 即可, 即使被用户强杀也会自动重新拉起。



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

Flutter实现"剑气"加载?️

效果图知识点Animation【动效】Clipper/Canvas【路径裁剪/画布】Matrix4【矩阵转化】剑气形状我们仔细看一道剑气,它的形状是一轮非常细小的弯弯的月牙;在Flutter中,我们可以通过Clipper路径来裁剪出来,或者也可以通过canva...
继续阅读 »

效果图

剑气加载.gif

知识点

  • Animation【动效】
  • Clipper/Canvas【路径裁剪/画布】
  • Matrix4【矩阵转化】

剑气形状

我们仔细看一道剑气,它的形状是一轮非常细小的弯弯的月牙;在Flutter中,我们可以通过Clipper路径来裁剪出来,或者也可以通过canvas绘制出来。

  1. 先看canvas如何进行绘制的
class MyPainter extends CustomPainter {
Color paintColor;

MyPainter(this.paintColor);

Paint _paint = Paint()
..strokeCap = StrokeCap.round
..isAntiAlias = true
..strokeJoin = StrokeJoin.bevel
..strokeWidth = 1.0;

@override
void paint(Canvas canvas, Size size) {
_paint..color = this.paintColor;
Path path = new Path();
// 获取视图的大小
double w = size.width;
double h = size.height;
// 月牙上边界的高度
double topH = h * 0.92;
// 以区域中点开始绘制
path.moveTo(0, h / 2);
// 贝塞尔曲线连接path
path.cubicTo(0, topH * 3 / 4, w / 4, topH, w / 2, topH);
path.cubicTo((3 * w) / 4, topH, w, topH * 3 / 4, w, h / 2);
path.cubicTo(w, h * 3 / 4, 3 * w / 4, h, w / 2, h);
path.cubicTo(w / 4, h, 0, h * 3 / 4, 0, h / 2);

canvas.drawPath(path, _paint);
}

@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false; // 一次性画好,不需要更新,返回false
}
  1. Clipper也上代码,跟canvas两种选其一即可,我用的是canvas
class SwordPath extends CustomClipper<Path> {
@override
getClip(Size size) {
print(size);
// 获取视图的大小
double w = size.width;
double h = size.height;
// 月牙上边界的高度
double topH = h * 0.92;
Path path = new Path();
// 以区域中点开始绘制
path.moveTo(0, h / 2);
// 贝塞尔曲线连接path
path.cubicTo(0, topH * 3 / 4, w / 4, topH, w / 2, topH);
path.cubicTo((3 * w) / 4, topH, w, topH * 3 / 4, w, h / 2);
path.cubicTo(w, h * 3 / 4, 3 * w / 4, h, w / 2, h);
path.cubicTo(w / 4, h, 0, h * 3 / 4, 0, h / 2);
return path;
}

@override
bool shouldReclip(covariant CustomClipper oldClipper) => false;
}
  1. 生成月牙控件
CustomPaint(
painter: MyPainter(widget.loadColor),
size: Size(200, 200),
),

让剑气旋转起来

我们需要剑气一直不停的循环转动,所以需要用到动画,让剑气围绕中心的转动起来。注意这里只是单纯的平面旋转,也就是我们说的2D变换。这里我们用到的是Transform.rotate控件,通过animation.value传入旋转的角度,从而实现360度的旋转。

class _SwordLoadingState extends State<SwordLoading>
with TickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
double angle = 0;

@override
void initState() {
_controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 800));
// pi * 2:360°旋转
_animation = Tween(begin: 0.0, end: pi * 2).animate(_controller);
_controller.repeat(); // 循环播放动画
super.initState();
}

@override
Widget build(BuildContext context) {
return Transform.rotate(
alignment: Alignment.center,
angle: _animation.value,
child: CustomPaint(
painter: MyPainter(widget.loadColor),
size: Size(widget.size, widget.size),
),
);
}
}

转起来啦!

让剑气有角度的、更犀利的转动

  • 我们仔细看单独一条剑气,其实是在一个三维的模型中,把与Z轴垂直的剑气 向Y轴、X轴进行了一定角度的偏移。
  • 相当于在这个3D空间内,剑气不在某一个平面了,而是斜在这个空间内,然后 再绕着圆心去旋转。
  • 而观者的视图,永远与Z轴垂直【或者说:X轴和Y轴共同组成的平面上】,所以就会产生剑气 从外到里进行旋转 的感觉。

下图纯手工绘制,不要笑我~~~

纯手工绘制

不要笑我

综上,可以确定这个过程是一个3D的变换,很明显我们Transform.rotate这种2D的widget已经不满足需求了,这个时候Matrix4大佬上场了,我们通过Matrix4.identity()..rotate的方法,传入我们的3D转化,在通过rotateZ进行旋转,简直完美。代码如下

 AnimatedBuilder(
animation: _animation,
builder: (context, _) => Transform(
transform: Matrix4.identity()
..rotate(v.Vector3(0, -8, 12), pi)
..rotateZ(_animation.value),
alignment: Alignment.center,
child: CustomPaint(
painter: MyPainter(widget.loadColor),
size: Size(widget.size, widget.size),
),
),
),

这里多说一句,要完成矩阵变换,Matrix4必不可少,可以着重学习下。

让剑气一起动起来

完成一个剑气的旋转之后,我们回到预览效果,无非就是3个剑气堆叠在一起,通过偏移角度去区分。Flutter堆叠效果直接用Stack实现,完整代码如下:

import 'package:flutter/material.dart';
import 'dart:math';
import 'package:vector_math/vector_math_64.dart' as v;

class SwordLoading extends StatefulWidget {
const SwordLoading({Key? key, this.loadColor = Colors.black, this.size = 88})
: super(key: key);

final Color loadColor;
final double size;

@override
_SwordLoadingState createState() => _SwordLoadingState();
}

class _SwordLoadingState extends State<SwordLoading>
with TickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
double angle = 0;

@override
void initState() {
_controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 800));
_animation = Tween(begin: 0.0, end: pi * 2).animate(_controller);
_controller.repeat();
super.initState();
}

@override
Widget build(BuildContext context) {
return Stack(
children: [
AnimatedBuilder(
animation: _animation,
builder: (context, _) => Transform(
transform: Matrix4.identity()
..rotate(v.Vector3(0, -8, 12), pi)
..rotateZ(_animation.value),
alignment: Alignment.center,
child: CustomPaint(
painter: MyPainter(widget.loadColor),
size: Size(widget.size, widget.size),
),
),
),
AnimatedBuilder(
animation: _animation,
builder: (context, _) => Transform(
transform: Matrix4.identity()
..rotate(v.Vector3(-12, 8, 8), pi)
..rotateZ(_animation.value),
alignment: Alignment.center,
child: CustomPaint(
painter: MyPainter(widget.loadColor),
size: Size(widget.size, widget.size),
),
),
),
AnimatedBuilder(
animation: _animation,
builder: (context, _) => Transform(
transform: Matrix4.identity()
..rotate(v.Vector3(-8, -8, 6), pi)
..rotateZ(_animation.value),
alignment: Alignment.center,
child: CustomPaint(
painter: MyPainter(widget.loadColor),
size: Size(widget.size, widget.size),
),
),
),
],
);
}
}

class MyPainter extends CustomPainter {
Color paintColor;

MyPainter(this.paintColor);

Paint _paint = Paint()
..strokeCap = StrokeCap.round
..isAntiAlias = true
..strokeJoin = StrokeJoin.bevel
..strokeWidth = 1.0;

@override
void paint(Canvas canvas, Size size) {
_paint..color = this.paintColor;
Path path = new Path();
// 获取视图的大小
double w = size.width;
double h = size.height;
// 月牙上边界的高度
double topH = h * 0.92;
// 以区域中点开始绘制
path.moveTo(0, h / 2);
// 贝塞尔曲线连接path
path.cubicTo(0, topH * 3 / 4, w / 4, topH, w / 2, topH);
path.cubicTo((3 * w) / 4, topH, w, topH * 3 / 4, w, h / 2);
path.cubicTo(w, h * 3 / 4, 3 * w / 4, h, w / 2, h);
path.cubicTo(w / 4, h, 0, h * 3 / 4, 0, h / 2);

canvas.drawPath(path, _paint);
}

@override
bool shouldRepaint(covariant CustomPainter oldDelegate) =>
false; // 一次性画好,不需要更新,返回false
}

业务端调用

SwordLoading(loadColor: Colors.black,size: 128),

写在最后

花了我整个周六下午的时间,很开心用Flutter实现了加载动画,说说感受吧。

  1. 在编写的过程中,对比html+css的方式,Flutter的实现难度其实更大,而且剑气必须使用canvas绘制出来。
  2. 如果你也懂前端,你可以深刻体会声明式和命令式UI在编写布局和动画所带来的强烈差异,从而加深Flutter万物皆对象的思想。*【因为万物皆对象,所以所有控件和动画,都是可以显示声明的对象,而不是像前端那样通过解析xml命令来显示】
  3. 2D/3D变换,我建议Flutter学者们,一定要深入学习,这种空间思维对我们实现特效是不可获取的能力。

好了,小弟班门弄斧,希望能一起学习进步!!!


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

收起阅读 »

常见问题之webView 内存泄露

WebView内存泄露的原因:webView内部的一些线程持有activity对象,导致activity无法释放。继而内存泄漏。现象正常使用都会有内存泄露反复进出多次后:内存没有下降。 网上查了一些解决办法,主要有两种:解决方法一1.在Activit...
继续阅读 »

WebView内存泄露的原因:

webView内部的一些线程持有activity对象,导致activity无法释放。继而内存泄漏。

现象

正常使用都会有内存泄露

image.png

反复进出多次后:
内存没有下降。 image.png

网上查了一些解决办法,主要有两种:

解决方法一

1.在Activity中手动new WebView,然后将Activity通过弱引用的方式传进去。并且在onDestroy里将webview先从父容器中移除webview,然后再销毁webview(调用webView的各种clear)

image.png

image.png

页面反复进出以后:

屁用没有

image.png

解决方法二

2.新启一个进程,单独进程跑webView。AIDL通信。
简化的代码以后:

AIDL接口:
image.png

AIDL的服务: image.png

设置服务隐士启动:
image.png

客户端显示webView的Activity:
image.png

将Activity设置成单独进程:
image.png

在Activity的onDestroy,解绑Service和结束进程
image.png

页面反复进出以后:
没有占用内存,完美。 image.png

补充
如果想要webView跟Activity有更多的数据交互,可以用AIDL->Service(数据交互)->Activity(主进程,Handler、回调等方式)

收起阅读 »

ConstraintLayout2.0一篇写不完之KeyCycles的秘密

KeyCycle与KeyFrame类似,但是又比KeyFrame复杂,复杂在于KeyFrame只是单帧,而KeyCycle则是在KeyFrame的基础上,增加了周期性的处理,所以,KeyCycle的核心就是周期,KeyCycle决定了在Scene中所有需要重复...
继续阅读 »

KeyCycle与KeyFrame类似,但是又比KeyFrame复杂,复杂在于KeyFrame只是单帧,而KeyCycle则是在KeyFrame的基础上,增加了周期性的处理,所以,KeyCycle的核心就是周期,KeyCycle决定了在Scene中所有需要重复处理的部分操作,它的核心API如下所示。

  • framePosition:作为一个KeyFrame,KeyCycle必须知道在场景的哪一点上进行操作
  • motionTarget:指定的View ID
  • wavePeriod:周期数量
  • waveOffset:起始位置的偏移
  • waveShape:Cycle的波形

image-20210827101705951

MotionLayout提供了CycleEditor来帮助开发者编辑KeyCycle,下载地址如下:

github.com/googlesampl…

直接执行即可,点击file中的parse,就可以将编辑区域的xml转换为波形。

java -jar CycleEditor.jar

分割Scene

在创建KeyCycle之前的第一件事,就是通过使用不同的framePositions把你的Scene分成多部分组合的Partial Scene,接下来就可以通过使用wavePeriod来指定你想要的每个部分的周期数,以及waveShape来指定具体的波形。

1*cdtzWO2JKu6VvmN9Ew2kvw

wavePeriod是KeyCycle最难理解的一部分,要掌握wavePeriod的定义,就必须先了解Partial Scene,Partial Scene指的是当前指定点的前一个点和后一个点,总共三个点构成的区域,这点非常重要。

在某个framePosition的KeyCycle中指定wavePeriod,其实就是指在这个Partial Scene中,有几个周期的波形来填满这个区域。

但是这里问题又来了,每个framePosition都被周围的framePosition有关,那么wavePeriod不是被重复计算了吗?

没错。。。所以在整个Partial Scene中的wavePeriod是由Partial Scene中所有framePosition的的wavePeriod之和确定的。很绕是不是,是就对了。

这也是为什么KeyCycle有个单独的生成工具的原因,结合KeyCycleEditor,还是比较能理解的。

wavePeriod已经很绕了,但是绕的还在后面。

我们再来看看KeyCycle中指定的具体属性值的含义。

例如,我们在KeyCycle中指定rotation为20,代码如下所示。

<KeyCycle 
motion:framePosition="0"
motion:target="@+id/button"
motion:wavePeriod="0"
motion:waveOffset="0"
motion:waveShape="sin"
android:rotation="20"/>

这个rotation为20是什么意思?你以为是当然framePosition的属性值为20吗?太年轻了。。。

其实这个属性值与View在当前framePosition的属性值,并没有直接联系。。。

是不是很奇怪,的确如此,那么这玩意儿到底是干嘛的呢???

这里我们需要转换一下思路,那就是KeyCycle里面设置的一切东西,都是为了画出「波形图」,所以,这些参数的设置,就是为了修改波形图的具体形状。

<KeyFrameSet>

<KeyCycle
motion:framePosition="0"
motion:target="@+id/button"
motion:wavePeriod="0"
motion:waveOffset="0"
motion:waveShape="sin"
android:rotation="0"/>

<KeyCycle
motion:framePosition="50"
motion:target="@+id/button"
motion:wavePeriod="1"
motion:waveOffset="0"
motion:waveShape="sin"
android:rotation="10"/>

<KeyCycle
motion:framePosition="100"
motion:target="@+id/button"
motion:wavePeriod="0"
motion:waveOffset="0"
motion:waveShape="sin"
android:rotation="30"/>

</KeyFrameSet>

这样一个KeyCycle最后形成的波形图就是这样。

image-20210827151332911

由此可以发现,每个framePosition的属性值,就是为了画出波形图的波峰。

在这个的基础上,waveOffset就好理解了,它的作用就是给framePosition的当前value增加一个初始值,这个初始值同样是为了修改波形。

要干嘛

你说KeyCycle这玩意儿整这么复杂,到底有什么用呢??

我们有了KeyFrame,可以用来添加中间态关键帧,那么还要KeyCycle干嘛呢?

说到这来,就不能不提下Monotonic Spline(单调采样)了,通常的关键帧插值算法都是使用的单调采样,但是这样无法做到曲线的圆滑过渡,就像下图中的绿色曲线,这样四个点使用单调采样,就变成了下面这样的曲线,过渡会非常生硬。

image-20210827154425111

那么为了让曲线圆滑过渡,KeyCycle使用的是Typical Spline(特征采样),就如上图中的紫色曲线,四个点被圆滑的连接了起来。

如果仅仅是为了让曲线能圆滑过渡,那么你就太小看KeyCycle了,不得不说老外做的这些东西,总能在一些你觉得无关紧要的地方,做的非常深入。

KeyCycle的核心在于波形,而波是什么呢?

image-20210827155302534

上面这张图表达了sin和cos的几何含义,也是sin和cos的来源。

说句不像傅里叶变换的话,我们可以将一个View的曲线运动,拆解成多个不同波形运动的叠加。

例如我们对一个View的translationX同时设置sin和cos的KeyCycle,最终形成的运动轨迹,就是一个圆形!

所以,由此及彼,我们可以复合多个属性的同时,通过不同的波形叠加,实现任何你想要的运动轨迹!这TM就牛逼了啊,简直就是傅里叶变换在Android动画中的实现了。

在CycleEditor中,有一些自带的Demo,可以让你充分的了解这个思想,例如下面这个例子。

image-20210827160218065

太复杂了是吗?

CustomWave shape in keyCycle

CL2.1之后,motion:waveShape除了之前定义的sin、cos、bounce这些预设曲线外,你还可以设置自定义的波形曲线,定义方式如下所示。

<KeyCycle motion:waveShape=”spline(0.0, 1.0, -1.0, 0)” />

这就有点牛逼了,本来就很复杂了,这下还来了自定义曲线,再见。

KeyCycle确实比较强大,但是也非常复杂,强烈建议大家使用CycleEditor来学习,KeyCycle这种东西,就像核武器一样,可以不用,但是不能没有。

收起阅读 »

Jetpack App Startup如何使用及原理分析

1.App Startup是什么?来自 Google官方App Startup文档: App Startup 库提供了一种在应用程序启动时初始化组件的简单、高效的方法。Libary开发人员和App开发人员都可以使用App Startup来简化启动顺序...
继续阅读 »

1.App Startup是什么?

来自 Google官方App Startup文档: App Startup 库提供了一种在应用程序启动时初始化组件的简单、高效的方法。
Libary开发人员和App开发人员都可以使用App Startup来简化启动顺序并明确设置初始化顺序。

2.简单回顾一下ContentProvider

内容提供者ContentProvider 玩法有很多种:可以进程间通信(数据共享)、ContentResolver#registerContentObserver观察uri变化(可以解析uri来处理各种命令) 等等
ContentProvider通过Binder实现进程间通信,使用起来比AIDL要简单,因为系统已经帮我们进行了封装(这里就不扩散介绍Binder,东西太多)

(1).注册

我们需要在AndroidManifest.xml中注册我们的ContentProvider

//AndroidManifest.xml
<provider
android:authorities="..."
android:name="..."/>

上面有两个属性,我们来看一下是干什么的?

  • android:authorities: 一个或多个URI授权方的列表,多个授权方需要使用分号( ; )分隔开。
  • android:name: 完整的ContentProvider类名,如:(com.xxx.xxx.xxxProvider)

(2).实现一个CustomProvider

class CustomProvider: ContentProvider() {
override fun onCreate(): Boolean {
// 返回true表示初始化成功
// 返回false表示初始化失败
}
override fun query(uri: Uri, projection: Array<out String>?,selection: String?,selectionArgs: Array<out String>?,sortOrder: String?): Cursor? {
//查询数据返回Cursor,如果是别的玩法,返回null也可以
}
override fun getType(uri: Uri): String? {
//没有类型的话可以直接返回null
}
override fun insert(uri: Uri, values: ContentValues?): Uri? {
//插入数据,返回uri,如果是别的玩法,返回null也可以
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
//删除数据,返回行数,如果是别的玩法,返回-1也可以
}
override fun update(uri: Uri,values: ContentValues?,selection: String?,selectionArgs: Array<out String>?): Int {
//更新数据,返回行数,如果是别的玩法,返回-1也可以
}
}

(3).ContentProvider在哪初始化的?

我们从ActivityThread的main函数开始跟踪:

//frameworks/base/core/java/android/app/ActivityThread.java
public static void main(String[] args) {
....
ActivityThread thread = new ActivityThread();
thread.attach(false, startSeq);
....
}

我们来看一下ActivityThread的attach方法

private void attach(boolean system, long startSeq) {
....
final IActivityManager mgr = ActivityManager.getService();
try {
//通过IBinder进程间通讯调用AMS
mgr.attachApplication(mAppThread, startSeq);
} catch (RemoteException ex) {
.....
}
.....
}

打开ActivityManagerService查看attachApplication方法里面会执行什么

//com.android.server.am.ActivityManagerService
public final void attachApplication(IApplicationThread thread, long startSeq) {
....
synchronized (this) {
....
attachApplicationLocked(thread, callingPid, callingUid, startSeq);
....
}
}

private boolean attachApplicationLocked(@NonNull IApplicationThread thread,
int pid, int callingUid, long startSeq) {
......
try {
......
if (instr2 != null) {
thread.bindApplication(processName, appInfo, providerList,
instr2.mClass,
profilerInfo, instr2.mArguments,
instr2.mWatcher,....);
} else {
thread.bindApplication(processName, appInfo, providerList, null, profilerInfo,
null, null, null, testMode,....);
}
......
} catch (Exception e) {
......
}
......
}

看到这里我们发现,调用了ActivityThread里面的bindApplication:

//android.app.ActivityThread.ApplicationThread#bindApplication

public final void bindApplication(String processName, ApplicationInfo appInfo,........){
.....
AppBindData data = new AppBindData();
data.processName = processName;
data.appInfo = appInfo;
//ContentProvider列表
data.providers = providerList.getList();
.....
sendMessage(H.BIND_APPLICATION, data);
}

看到这里,仿佛此刻看到了光,BIND_APPLICATION这条消息内部会执行到ActivityThread#handleBindApplication

//android.app.ActivityThread.ApplicationThread#handleBindApplication

private void handleBindApplication(AppBindData data) {
.....
Application app;
.....
try {
//触发LoadedApk调用makeApplication
app = data.info.makeApplication(data.restrictedBackupMode, null);
.....
mInitialApplication = app;
if (!data.restrictedBackupMode) {
if (!ArrayUtils.isEmpty(data.providers)) {
//安装ContentProvider入口
installContentProviders(app, data.providers);
}
}
....
try {
//调用Application#onCreate方法
mInstrumentation.callApplicationOnCreate(app);
} catch (Exception e) {
....
}
} finally {
.....
}
.....
}

handleBindApplication步骤如下:
(1). 执行LoadedApk调用makeApplication
(2). 执行installContentProviders遍历providers列表,实例化ContentProvider
(3). mInstrumentation.callApplicationOnCreate触发Application#onCreate回调

//android.app.ActivityThread#installContentProviders
private void installContentProviders(
Context context, List<ProviderInfo> providers) {
final ArrayList<ContentProviderHolder> results = new ArrayList<>();
for (ProviderInfo cpi : providers) {
....
//真正初始化ContentProvider的地方
ContentProviderHolder cph = installProvider(context, null, cpi,
false /*noisy*/, true /*noReleaseNeeded*/, true /*stable*/);
if (cph != null) {
cph.noReleaseNeeded = true;
results.add(cph);
}
}
try {
//AMS内部会执行mHandler.removeMessages(CONTENT_PROVIDER_PUBLISH_TIMEOUT_MSG, r)
ActivityManager.getService().publishContentProviders(
getApplicationThread(), results);
} catch (RemoteException ex) {
....
}
}

installContentProviders步骤如下:
(1). 遍历providers列表,执行installProvider来实例化ContentProvider
(2). AMS调用publishContentProviders清除掉超时的消息,否则会导致应用被kill

//android.app.ActivityThread#installProvider
private ContentProviderHolder installProvider(Context context,
ContentProviderHolder holder, ProviderInfo info,
boolean noisy, boolean noReleaseNeeded, boolean stable) {
ContentProvider localProvider = null;
IContentProvider provider;
if (holder == null || holder.provider == null) {
....
Context c = null;
ApplicationInfo ai = info.applicationInfo;
//初始化Context
if (context.getPackageName().equals(ai.packageName)) {
c = context;
} else if (mInitialApplication != null &&
mInitialApplication.getPackageName().equals(ai.packageName)) {
c = mInitialApplication;
} else {
try {
c = context.createPackageContext(ai.packageName,
Context.CONTEXT_INCLUDE_CODE);
} catch (PackageManager.NameNotFoundException e) {
// Ignore
}
}
....
try {
final java.lang.ClassLoader cl = c.getClassLoader();
//ContentProvider和Application的加载器是同一个
LoadedApk packageInfo = peekPackageInfo(ai.packageName, true);
if (packageInfo == null) {
// System startup case.
packageInfo = getSystemContext().mPackageInfo;
}
//通过ClassLoader初始化ContentProvider
localProvider = packageInfo.getAppFactory()
.instantiateProvider(cl, info.name);
provider = localProvider.getIContentProvider();
if (provider == null) {
return null;
}
//ContentProvider实例化成功之后,回调ContentProvider#onCreate
localProvider.attachInfo(c, info);
} catch (java.lang.Exception e) {
return null;
}
} else {
provider = holder.provider;
}
....
return retHolder;
}

installProvider步骤如下:
(1). 获取Context
(2). 通过peekPackageInfo获取LoadedApk,然后通过ClassLoader来实例化ContentProvider
(3). ContentProvider实例化成功之后,attachInfo会触发ContentProvider#onCreate回调

从上面的代码分析之后得出结论:ContentProvider的onCreate会比Application的onCreate先执行

(4).ContentResolver

如果我们需要访问内容提供者里面数据的话,可以使用App的Context中的ContentResolver对象与ContentProvider进行通信。
ContentProvider对象从客户端接收数据请求、执行请求的操作并返回结果。ContentResolver方法可以提供基本的“CRUD”功能。

A.在哪初始化

//frameworks/base/core/java/android/app/LoadedApk.java

public Application makeApplication(boolean forceDefaultAppClass,
Instrumentation instrumentation) {
....
Application app = null;
....
try {
....
//初始化ContextImpl,并初始化一个ApplicationContentResolver「即ContentResolver」
ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
....
//执行进程中application对象的实例化,并执行Application#attach方法同时触发attachBaseContext回调
//更多细节,感兴趣的,可以自己看源码查看更多内容
app = mActivityThread.mInstrumentation.newApplication(
cl, appClass, appContext);
//设置applicationContext
appContext.setOuterContext(app);
} catch (Exception e) {
...
}
....
mApplication = app;
if (instrumentation != null) {
try {
//触发application的onCreate方法执行
instrumentation.callApplicationOnCreate(app);
} catch (Exception e) {
....
}
}
....
return app;
}

makeApplication简单逻辑如下:
(1).初始化ContextImpl同时初始化ContentResolver
(2).使用ActivityThread中的Instrumentation执行里面的newApplication:
初始化Application并调用attach方法并触发attachBaseContext回调
(3).调用Application的onCreate方法

B.监听数据变化

我们可以使用ContentResolver#registerContentObserver来观察数据是否发生变化。
我们来看一下简单的demo:

//注册内容观察者
contentResolver.registerContentObserver(Uri.parse("....."),true,xxxxCallback)

//不是和数据库进行操作交互的话,自己写的一些功能玩法的uri可以像下面这样
//uri => content://xxxx.service.status
//我们registerContentObserver观察这个原始的uri
//别的进程或者app通过:contentResolver.notifyChange(uri+"/lastPathSegment")
//可以通过下面的方式去解析uri来做一些操作,可玩性也比较好
private val xxxxCallback: ContentObserver = object : ContentObserver(Handler(Looper.getMainLooper())) {
override fun onChange(selfChange: Boolean, uri: Uri?) {
super.onChange(selfChange, uri)
val lastPathSegment = uri?.lastPathSegment
//有些玩法可以自己定义,比如我们可以拿这个lastPathSegment来实现一些命令操作
.....
}
}

C.如何CURD

//查询数据
ContentResolver#query(android.net.Uri, java.lang.String[], java.lang.String, java.lang.String[], java.lang.String)

//插入一条数据
ContentResolver#insert(android.net.Uri, android.content.ContentValues)

//删除数据
ContentResolver#delete(android.net.Uri, java.lang.String, java.lang.String[])

//更新数据
ContentResolver#update(android.net.Uri, android.content.ContentValues, java.lang.String, java.lang.String[])

3.App Startup使用

(1).依赖

dependencies {
implementation("androidx.startup:startup-runtime:<新版本>")
}

(2).AndroidManifest.xml配置

App Startup是用InitializationProvider来发现和调用你的组件,所以这里我们需要定义<provider/>

<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data android:name="xxx.xxx.xxxInitializer"
android:value="androidx.startup" />
</provider>

使用tools:node="merge"合并属性,解决任何冲突的条目,合并到一起。
使用<meta-data/>,这使得我们自定义的Initializer可以被发现。
如果<provider/>里面增加tools:node="remove"禁用所有组件的自动初始化。
如果需要禁用单个组件自动初始化,可以在<meta-data/>里面增加:tools:node="remove"

(3).自定义Initializer

class MyInitializer :Initializer<Boolean>{
override fun create(context: Context): Boolean {
//可以在此处进行SDK初始化
....
return true
}
override fun dependencies(): List<Class<out Initializer<*>>> {
//没有依赖直接返回emptyList()即可
//如果有依赖,是针对实现了Initializer的类
return emptyList()
}
}

举个例子:
一、Leakcanary目前还没有集成App Startup,那么我们怎么使用呢?
首先我们去AndroidManifest.xml找到Leakcanary的<provider/>

WX20210902-105232@2x.png

然后我们在AndroidManifest.xml找到这两个<provider/>,添加tools:node="remove"

<provider
android:name="leakcanary.internal.PlumberInstaller"
android:authorities="写你自己的包名.plumber-installer"
android:enabled="@bool/leak_canary_plumber_auto_install"
android:exported="false"
tools:node="remove"/>
<provider
android:name="leakcanary.internal.AppWatcherInstaller$MainProcess"
android:authorities="写你自己的包名.leakcanary-installer"
android:enabled="@bool/leak_canary_watcher_auto_install"
android:exported="false"
tools:node="remove"/>

这个时候我们就可以在我们的App Startup中来控制初始化,愉快的玩耍了

class MyInitializer :Initializer<Boolean>{
override fun create(context: Context): Boolean {
val application = context.applicationContext as Application
AndroidLeakFixes.applyFixes(application)
AppWatcher.manualInstall(application)
....
return true
}
override fun dependencies(): List<Class<out Initializer<*>>> {
return emptyList()
}
}

二、WorkManager目前也没有集成App Startup,那么我们自己来扩展一下它实现Initializer,然后再看一下dependencies()的用处

class MyWorkManagerInitializer:Initializer<WorkManager> {
override fun create(context: Context): WorkManager {
//集成了WorkManager之后,可以在AndroidManifest.xml中找到默认的provider,可以看到内部实现
WorkManager.initialize(context, Configuration.Builder().build())
return WorkManager.getInstance(context)
}
override fun dependencies(): List<Class<out Initializer<*>>> {
return emptyList()
}
}

我们还需要把WorkManager默认的provider删除掉

<provider
android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="你自己的包名.workmanager-init"
android:directBootAware="false"
android:exported="false"
android:multiprocess="true"
tools:targetApi="n"
tools:node="remove"/>

在我们的MyInitializer中依赖MyWorkManagerInitializer

class MyInitializer :Initializer<Boolean>{
override fun create(context: Context): Boolean {
//先触发依赖的执行,执行完,我们这个create才会被执行到,表示依赖已经完成,可以正常使用了
return true
}
//先执行
override fun dependencies(): List<Class<out Initializer<*>>> {
return listOf(MyWorkManagerInitializer::class.java)
}
}

玩法使用如下: (1). 我们可以自己定义扩展的XXXInitializer来替换第三方Libary的ContentProvider,然后在自定义的Initializer的dependencies()中依赖扩展的XXXInitializer
(2). 不自定义扩展,我们知道第三方Libary的ContentProvider内部初始化的代码,可以写在自定义的Initializer的onCreate()中也可以。

4.原理分析

我们在上面提到AndroidManifest.xml中使用了InitializationProvider,它就是一个ContentProvider

//androidx.startup.InitializationProvider

public class InitializationProvider extends ContentProvider {
@Override
public final boolean onCreate() {
Context context = getContext();
if (context != null) {
//初始化解析
AppInitializer.getInstance(context).discoverAndInitialize();
} else {
throw new StartupException("Context cannot be null");
}
return true;
}
....
}

InitializationProvider#onCreate
获取内容提供者信息,解析meta-data数据,初始化Initializer并执行Initializer#create调用

//androidx.startup.AppInitializer

void discoverAndInitialize() {
try {
Trace.beginSection(SECTION_NAME);
ComponentName provider = new ComponentName(mContext.getPackageName(),
InitializationProvider.class.getName());
//通过PackageManager.getProviderInfo获取内容提供者信息
ProviderInfo providerInfo = mContext.getPackageManager()
.getProviderInfo(provider, GET_META_DATA);
//获取元数据
Bundle metadata = providerInfo.metaData;
//用于比较元数据里面的数据是不是androidx.startup
String startup = mContext.getString(R.string.androidx_startup);
if (metadata != null) {
Set<Class<?>> initializing = new HashSet<>();
Set<String> keys = metadata.keySet();
//遍历元数据
for (String key : keys) {
String value = metadata.getString(key, null);
if (startup.equals(value)) { //只初始化匹配成功的
Class<?> clazz = Class.forName(key);
if (Initializer.class.isAssignableFrom(clazz)) {
Class<? extends Initializer<?>> component =
(Class<? extends Initializer<?>>) clazz;
mDiscovered.add(component);
if (StartupLogger.DEBUG) {
StartupLogger.i(String.format("Discovered %s", key));
}
//初始化Initializer
doInitialize(component, initializing);
}
}
}
}
} catch (PackageManager.NameNotFoundException | ClassNotFoundException exception) {
throw new StartupException(exception);
} finally {
Trace.endSection();
}
}

discoverAndInitialize()逻辑如下:
(1). 通过PackageManager.getProviderInfo获取内容提供者信息 (2). 遍历元数据key列表,比较value是否等于androidx.startup (3). 条件匹配成功,执行doInitialize(component, initializing)

//androidx.startup.AppInitializer#doInitialize

<T> T doInitialize(
@NonNull Class<? extends Initializer<?>> component,
@NonNull Set<Class<?>> initializing) {
synchronized (sLock) {
try {
....
if (initializing.contains(component)) {
//没有初始化完成的抛异常
throw new IllegalStateException(....);
}
Object result;
//缓存里面是否已经有初始化过的数据
if (!mInitialized.containsKey(component)) {
//记录正在进行初始化
initializing.add(component);
try {
//实例化Initializer
Object instance = component.getDeclaredConstructor().newInstance();
Initializer<?> initializer = (Initializer<?>) instance;
//调用initializer.dependencies()
List<Class<? extends Initializer<?>>> dependencies =
initializer.dependencies();

//如果dependencies不为空
if (!dependencies.isEmpty()) {
for (Class<? extends Initializer<?>> clazz : dependencies) {
if (!mInitialized.containsKey(clazz)) {
//递归调用
doInitialize(clazz, initializing);
}
}
}
//回调Initializer#create
result = initializer.create(mContext);
//Initializer初始化完,移除临时添加的component
initializing.remove(component);
//添加到缓存中
mInitialized.put(component, result);
} catch (Throwable throwable) {
throw new StartupException(throwable);
}
} else {
//从缓存中获取
result = mInitialized.get(component);
}
return (T) result;
} finally {
...
}
}
}

doInitialize步骤如下:
(1). 正在初始化的HashSet集合(initializing)中如果包含component,需要抛出异常。
(2). 缓存的Map集合mInitialized中没有component: 先把component记录到正在初始化的HashSet集合(initializing),实例化Initializer。
检查initializer.dependencies()集合是否有数据,如果有数据,需要递归调用doInitialize
如果没有数据直接执行Initializer#create方法的调用,然后从正在初始化的HashSet集合(initializing)移除component,再把component添加到缓存的Map集合mInitialized中。
(3). 缓存的Map集合mInitialized中有component,直接从Map集合中get返回,不用重复初始化。
(4). 从上面代码分析可以看出来:Initializer中dependencies()优先执行,最后再执行Initializer#create

5.总结 + 耗时对比

App Startup VS 多个ContentProvider 耗时对比,默认对比数值是参考什么都不集成的原始apk

我们用三款设备进行测试冷启动比较(大概的一个数据值,仅供参考):

测试设备1: Huawei Mate20-HarmonyOS 2.0.0 001.png


测试设备2: Nexus 6-Android 7.1 002.png


测试设备3: Nokia N1平板-Android 5.1.1 003.png


结论如下:
App Startup使用的好处有:统一管理,简化启动顺序并明确设置初始化顺序。
如果每个Libary库开发者都自己搞一个ContentProvider来初始化,不方便管理。
设计初衷应该是为了收拢ContentProvider,实际上对启动优化的帮助不是特别大。
抛开ContentProvider中初始化数据,如果大家在项目中大多数的时候会使用线程池异步加载初始化一些数据,这个时候App Startup就没有必要使用了。

我们从测试的数据对比可以看出来:
针对少量的SDK使用少量ContentProvider的时候,如果使用App Startup不一定能缩短应用启动时间。
但是在多个不同性能的设备上(尤其是低端机),App Startup启动时间会比使用多个ContentProvider有些许缩短时间。

看完上面的内容,我想大家应该知道App Startup什么时候用,怎么用,为什么要用。
这样我写这篇文章的目的就达到了。

收起阅读 »

Jetpack生命周期管理 -Lifecycle实战及源码分析

上次我们聊了 Android 触摸事件传递机制,这次我们来聊聊 Jetpack。具体地说是聊聊他的生命周期管理组件 LifeCycle,因为JetPack这个官方库还蛮大。这里不会再讲 Jetpack的前世今生,以及他的作用什么的。这里我们主要讲讲 LifeC...
继续阅读 »

上次我们聊了 Android 触摸事件传递机制,这次我们来聊聊 Jetpack。具体地说是聊聊他的生命周期管理组件 LifeCycle,因为JetPack这个官方库还蛮大。这里不会再讲 Jetpack的前世今生,以及他的作用什么的。这里我们主要讲讲 LifeCycle的基本使用,以及用LifeCycle改进一下上次我们讲到的 MVP 的例子。

然后从源码角度分析一下 LifeCycle是如何帮助 Activity 或 Fragment管理生命周期的。后续会继续推出分析 Jetpack其他组件的文章。

我们知道,我们在用某些模块进行数据加载的时候,往往需要去监听 Activity或 Fragment的生命周期。再根据生命周期的变化去调整数据加载或回调的策略。

不使用组件来管理的话,一般我们可以在 Activity或 Fragment的生命周期回调方法里手动去调用模块的生命周期方法。这样页面的生命周期回调方法里可能就会出现大量这样的刻板代码,也不好管理。LifeCycle就用来解决这样的一些问题。

1、使用

使用 LifeCycle不用添加依赖了,因为已经内置了。如果要使用 Viewmodel和 Livedata则需要加依赖,这两个后续会有文章分析。

首先,既然是生命周期的监听,那就会有观察者和被观察者。被观察者需要实现的接口是 LifecycleOwner,也就是生命周期拥有者(Activity 或 Fragment)。这两者都实现了 LifecycleOwner接口,我们可以看一下 Activity 的父类 ComponentActivity:

public class ComponentActivity extends androidx.core.app.ComponentActivity implements
LifecycleOwner,
...
...
{

生命周期观察者需要实现的接口是 LifecycleObserver。下面我们就结合上次 MVP架构的例子,给 Presenter添加 LifeCycle生命周期监听方法。

首先让 BasePresenter实现观察者接口:

// 实现 LifecycleObserver
public class BasePresenter implements LifecycleObserver {
private V mView;
public void attach(V iView) {
this.mView = iView;
}
public void detach() {
this.mView = null;
}
public V getView() {
return mView;
}
}

然后我们给 BasePresenter的实现类 Presenter添加几个生命周期的方法,并加上**@OnLifecycleEvent**注解:

//  Presenter.java

// onCreate
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
public void onCreate(){
Log.d(TAG, "-----------LifecycleObserver -- onCreate");
}
// onStart
@OnLifecycleEvent(Lifecycle.Event.ON_START)
public void onStart(){
Log.d(TAG, "-----------LifecycleObserver -- onStart");
}
// onPause
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
public void onPause(){
Log.d(TAG, "-----------LifecycleObserver -- onPause");
}
// onStop
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
public void onStop(){
Log.d(TAG, "-----------LifecycleObserver -- onStop");
}
// onDestroy
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
public void onDestroy(){
Log.d(TAG, "-----------LifecycleObserver -- onDestroy");
}

然后在 BaseMvpActivity初始化时添加观察者:

// BaseMvpActivity.java

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView();
initView();
initData();
mPresenter = createP();
//mPresenter.attach(this);
// 注释 1, 添加观察者
getLifecycle().addObserver(mPresenter);
}
@Override
protected void onDestroy() {
super.onDestroy();
// mPresenter.detach();
// 移除观察者
getLifecycle().removeObserver(mPresenter);
}

上次例子使用了模板设计模式,所以现在生命周期观察者的添加和移除也放在模板里了。 然后打开 Activity后退出,看打印结果:

com.ethan.mvpapplication D/Presenter: -----------LifecycleObserver -- onCreate
com.ethan.mvpapplication D/Presenter: -----------LifecycleObserver -- onStart
com.ethan.mvpapplication D/Presenter: -----------LifecycleObserver -- onPause
com.ethan.mvpapplication D/Presenter: -----------LifecycleObserver -- onStop
com.ethan.mvpapplication D/Presenter: -----------LifecycleObserver -- onDestroy

Demo: MVP

2、源码分析

下面我们来分析一下 Lifecycle生命周期监听的原理。简单粗暴,直接点进上面注释 1的getLifecycle()方法,看看干了啥:

//  ComponentActivity.java

private final LifecycleRegistry mLifecycleRegistry = new LifecycleRegistry(this);

@Override
public Lifecycle getLifecycle() {
return mLifecycleRegistry;
}

返回的是一个 LifecycleRegistry 对象,我们可以点进去看。LifecycleRegistry 是抽象类 Lifecycle的实现类:

public abstract class Lifecycle {
@MainThread
public abstract void addObserver(@NonNull LifecycleObserver observer);
@MainThread
public abstract void removeObserver(@NonNull LifecycleObserver observer);
@MainThread
@NonNull
public abstract androidx.lifecycle.Lifecycle.State getCurrentState();
public enum Event {
ON_CREATE,
ON_START,
ON_RESUME,
ON_PAUSE,
ON_STOP,
ON_DESTROY,
ON_ANY
}
public enum State {
DESTROYED,
INITIALIZED,
CREATED,
STARTED,
RESUMED;
public boolean isAtLeast(@NonNull androidx.lifecycle.Lifecycle.State state) {
return compareTo(state) >= 0;
}
}
}

上面抽象类 Lifecycle 不仅包含了添加和移除观察者的方法,还包含了 Event 和 State 两个枚举。我们可以看到,Event 这个枚举包含了 Activity最主要的几个生命周期的方法。

下面我们继续看 LifeCycle是怎么监听生命周期的:

  public class ComponentActivity extends androidx.core.app.ComponentActivity implements
LifecycleOwner,
......{

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
......
// 注释 2, ComponentActivity注入 ReportFragment
ReportFragment.injectIfNeededIn(this);
......
}
......

@Override
public androidx.lifecycle.Lifecycle getLifecycle() {
return mLifecycleRegistry;
}
}

按理说可以在上面的 ComponentActivity的各个生命周期回调中调用观察者的对应方法,但我们可以看到 ComponentActivity 这个类里并没有这样做。而是在上面注释 2处将当前Activity对象注入 ReportFragment中,我们看看ReportFragment干了啥:

 public class ReportFragment extends Fragment {
private static final String REPORT_FRAGMENT_TAG = "androidx.lifecycle"
+ ".LifecycleDispatcher.report_fragment_tag";

public static void injectIfNeededIn(Activity activity) {
android.app.FragmentManager manager = activity.getFragmentManager();
if (manager.findFragmentByTag(REPORT_FRAGMENT_TAG) == null) {
// 注释 3, 创建ReportFragment 添加到 Activity,使得生命周期与之同步
manager.beginTransaction().add(new androidx.lifecycle.ReportFragment(), REPORT_FRAGMENT_TAG).commit();
manager.executePendingTransactions();
}
}
static androidx.lifecycle.ReportFragment get(Activity activity) {
return (androidx.lifecycle.ReportFragment) activity.getFragmentManager().findFragmentByTag(
REPORT_FRAGMENT_TAG);
}
// 分发生命周期回调事件
private void dispatchCreate(androidx.lifecycle.ReportFragment.ActivityInitializationListener listener) {
if (listener != null) {
listener.onCreate();
}
}
......
......
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
dispatchCreate(mProcessListener);
dispatch(androidx.lifecycle.Lifecycle.Event.ON_CREATE);
}

@Override
public void onStart() {
super.onStart();
// 注释 4, 开始调用观察者的生命周期
dispatchStart(mProcessListener);
dispatch(androidx.lifecycle.Lifecycle.Event.ON_START);
}
......
......
private void dispatch(androidx.lifecycle.Lifecycle.Event event) {
Activity activity = getActivity();
if (activity instanceof LifecycleRegistryOwner) {
((LifecycleRegistryOwner) activity).getLifecycle().handleLifecycleEvent(event);
return;
}

if (activity instanceof LifecycleOwner) {
androidx.lifecycle.Lifecycle lifecycle = ((LifecycleOwner) activity).getLifecycle();
if (lifecycle instanceof LifecycleRegistry) {
// 注释 5,生命周期事件分发
((LifecycleRegistry) lifecycle).handleLifecycleEvent(event);
}
}
}
}

上面注释 3处我们可以看到,这里创建了一个 Fragment,然后将 Fragment添加到 Activity中。这样的话,新创建的这个 Fragment和 Activity就可以同步生命周期。之后,在上面注释 4的地方,Fragment的各种生命周期的方法里就可以调用观察者(LifecycleObserver)的相关的生命周期方法了。

也就是说,ComponentActivity 创建了一个 ReportFragment ,并把生命周期回调的事务交给了Fragment。这sao操作是不是很熟悉?没错!我们之前分析过,Glide 也是这么管理生命周期的。

上面注释 5,我们再看看生命周期事件分发,看看观察者方法最终调用的地方:

  // LifecycleRegistry.java
static class ObserverWithState {
androidx.lifecycle.Lifecycle.State mState;
LifecycleEventObserver mLifecycleObserver;
ObserverWithState(LifecycleObserver observer, androidx.lifecycle.Lifecycle.State initialState) {
// 获取 ReflectiveGenericLifecycleObserver对象
mLifecycleObserver = Lifecycling.lifecycleEventObserver(observer);
mState = initialState;
}

void dispatchEvent(LifecycleOwner owner, androidx.lifecycle.Lifecycle.Event event) {
......
// 生命周期回调事件分发
mLifecycleObserver.onStateChanged(owner, event);
}
}

// Lifecycling.java
static LifecycleEventObserver lifecycleEventObserver(Object object) {
.....
// 返回 ReflectiveGenericLifecycleObserver对象
return new androidx.lifecycle.ReflectiveGenericLifecycleObserver(object);
}

// ReflectiveGenericLifecycleObserver.java
class ReflectiveGenericLifecycleObserver implements LifecycleEventObserver {
private final Object mWrapped;
private final ClassesInfoCache.CallbackInfo mInfo;
ReflectiveGenericLifecycleObserver(Object wrapped) {
mWrapped = wrapped;
mInfo = ClassesInfoCache.sInstance.getInfo(mWrapped.getClass());
}

@Override
public void onStateChanged(LifecycleOwner source, androidx.lifecycle.Lifecycle.Event event) {
// 生命周期回调
mInfo.invokeCallbacks(source, event, mWrapped);
}
}

// ClassesInfoCache.java
private ClassesInfoCache.CallbackInfo createInfo(Class klass, @Nullable Method[] declaredMethods) {
Class superclass = klass.getSuperclass();
for (Method method : methods) {
// 反射遍历观察者的各个方法,将带 @OnLifecycleEvent注解的方法保存在
// Map对象中,方便生命周期变化时调用
OnLifecycleEvent annotation = method.getAnnotation(OnLifecycleEvent.class);
if (annotation == null) { continue; }
Class[] params = method.getParameterTypes();
......
androidx.lifecycle.Lifecycle.Event event = annotation.value();
......
ClassesInfoCache.MethodReference methodReference = new ClassesInfoCache.MethodReference(callType, method);
verifyAndPutHandler(handlerToEvent, methodReference, event, klass);
mCallbackMap.put(klass, info);
mHasLifecycleMethods.put(klass, hasLifecycleMethods);
}
......
return info;
}
static class CallbackInfo {
final Map> mEventToHandlers;
final Map mHandlerToEvent;

CallbackInfo(Map handlerToEvent) {
mHandlerToEvent = handlerToEvent;
mEventToHandlers = new HashMap<>();
for (Map.Entry entry : handlerToEvent.entrySet()) {
androidx.lifecycle.Lifecycle.Event event = entry.getValue();
List methodReferences = mEventToHandlers.get(event);
if (methodReferences == null) {
methodReferences = new ArrayList<>();
mEventToHandlers.put(event, methodReferences);
}
methodReferences.add(entry.getKey());
}
}

@SuppressWarnings("ConstantConditions")
void invokeCallbacks(LifecycleOwner source, androidx.lifecycle.Lifecycle.Event event, Object target) {
invokeMethodsForEvent(mEventToHandlers.get(event), source, event, target);
invokeMethodsForEvent(mEventToHandlers.get(androidx.lifecycle.Lifecycle.Event.ON_ANY), source, event,
target);
}

private static void invokeMethodsForEvent(List handlers,
LifecycleOwner source, androidx.lifecycle.Lifecycle.Event event, Object mWrapped) {
if (handlers != null) {
for (int i = handlers.size() - 1; i >= 0; i--) {
// 调用 MethodReference的 invokeCallback方法
handlers.get(i).invokeCallback(source, event, mWrapped);
}
}
}
}

// ClassesInfoCache.MethodReference
static class MethodReference {
final int mCallType;
final Method mMethod;
.......
void invokeCallback(LifecycleOwner source, androidx.lifecycle.Lifecycle.Event event, Object target) {
//noinspection TryWithIdenticalCatches
try {
switch (mCallType) {
case CALL_TYPE_NO_ARG:
mMethod.invoke(target);
break;
case CALL_TYPE_PROVIDER:
mMethod.invoke(target, source);
break;
case CALL_TYPE_PROVIDER_WITH_EVENT:
mMethod.invoke(target, source, event);
break;
}
} catch (InvocationTargetException e) {
throw new RuntimeException("Failed to call observer method", e.getCause());
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}

时序图就不画了,大概写一下调用流程吧:

LifecycleRegistry.java

  • --> handleLifecycleEvent();
  • --> moveToState(next);
  • --> sync();
  • --> forwardPass(lifecycleOwner);
  • --> LifecycleRegistry.ObserverWithState --> dispatchEvent(owner, event);

ReflectiveGenericLifecycleObserver .java

  • --> onStateChanged()

ClassesInfoCache.java

  • --> CallbackInfo createInfo(); (反射将观察者带注解的方法保存)

MethodReference.java

  • -->invokeCallback(source, event, mWrapped);
  • --> mMethod.invoke(target); (反射调用观察者的生命周期的方法)

经过层层调用和包装,注册时最终会用反射将观察者带**@OnLifecycleEvent注解的方法保存在MethodReference中,并放入HashMap。当 Activity生命周期改变时,随着层层调用,最终保存在集合里的观察者LifecycleObserver**生命周期相关方法会被调用。

这波sao操作是不是又似曾相识?没错!EventBus也是这么管理订阅和发送事件的。


收起阅读 »

View的绘制流程 硬件渲染

负责硬件渲染的主体对象ThreadedRenderer在整个绘制流程中做了哪几个步骤。1.enableHardwareAcceleration 实例化ThreadedRenderer2.initialize 初始化3.updateSurface 更新Surfa...
继续阅读 »

负责硬件渲染的主体对象ThreadedRenderer在整个绘制流程中做了哪几个步骤。

  • 1.enableHardwareAcceleration 实例化ThreadedRenderer
  • 2.initialize 初始化
  • 3.updateSurface 更新Surface
  • 4.setup 启动ThreadedRenderer设置阴影等参数
  • 5.如果需要 执行invalidateRoot 判断是否需要从根部开始遍历查找无效的元素
  • 6.draw 开始硬件渲染进行View层级绘制
  • 7.updateDisplayListIfDirty 更新硬件渲染中的脏区
  • 8.destroy 销毁硬件渲染对象

在硬件渲染的过程中,有一个很核心的对象RenderNode,作为每一个View绘制的节点对象。

当每一次进行准备进行绘制的时候,都会雷打不动执行如下三个步骤:

  • 1.RenderNode.start 生成一个新的DisplayListCanvas
  • 2.DisplayListCanvas 上进行绘制 如调用Drawable的draw方法,把DisplayListCanvas作为参数
  • 3.RenderNode.end 完成RenderNode的操作

重要对象

  • 1.ThreadedRenderer 管理所有的硬件渲染对象,也是ViewRootImpl进行硬件渲染的入口对象。
  • 2.RenderNode 每一个View都会携带的对象,当打开了硬件渲染的时候,将会根据判断,把相关的渲染逻辑移动到RenderNode中。
  • 3.DisplayListCanvas 每一个RenderNode真正开始绘制自己的内容之前,需要通过RenderNode生成一个DisplayListCanvas,所有的绘制的行为都会在DisplayListCanvas中绘制,最后DisplayListCanvas会保存会RenderNode中。

在Java层中面向Framework中,只有这么多,下面是一一映射的简图。

image.png

能看到实际上RenderNode也会跟着View 树的构建同时一起构建整个显示层级。也是因此ThreadedRender也能以RenderNode为线索构建出一套和软件渲染一样的渲染流程。

让我继续介绍一下,在硬件渲染中native层的核心对象。

  • 1.RootRenderNode 所有RenderNode的根部RenderNode,一切的View层级结构遍历都从这个RenderNode开始。类似View中DecorView的职责。但是DecorView并非和RootRenderNode对应,而是拥有自己的RenderNode。
  • 2.RenderNode 对应于Java层的native对象
  • 3.RenderThread 硬件渲染线程,所有的渲染任务都会在该线程中使用硬件渲染线程的Looper进行。
  • 4.CanvasContext 是所有的渲染的上下文,它将持用PipeLine渲染管道
  • 5.PipeLine 如OpenGLPipeLine,SkiaOpenGLPipeLine,VulkanPipeLine渲染管道。而这个渲染管道将会根据Android系统的配置,执行真正的渲染行为
  • 6.DrawFrameTask 是整个ThreadedRender中真正开始执行渲染的对象
  • 7.RenderNodeProxy ThreadedRender的对应native层的入口。它将全局的作为RootRenderNode,CanvasContext,以及RenderThread门面(门面设计模式)。

如下是一个思维导图:

image.png

ThreadedRenderer 实例化

当发现mSurfaceHolder为空的时候会调用如下函数:

                if (mSurfaceHolder == null) {
enableHardwareAcceleration(attrs);
....
}

而这个方法则调用如下的方法对ThreadedRenderer进行创建:

 mAttachInfo.mThreadedRenderer = ThreadedRenderer.create(mContext, translucent,
attrs.getTitle().toString());
    public static boolean isAvailable() {
if (sSupportsOpenGL != null) {
return sSupportsOpenGL.booleanValue();
}
if (SystemProperties.getInt("ro.kernel.qemu", 0) == 0) {
sSupportsOpenGL = true;
return true;
}
int qemu_gles = SystemProperties.getInt("qemu.gles", -1);
if (qemu_gles == -1) {
return false;
}
sSupportsOpenGL = qemu_gles > 0;
return sSupportsOpenGL.booleanValue();
}


public static ThreadedRenderer create(Context context, boolean translucent, String name) {
ThreadedRenderer renderer = null;
if (isAvailable()) {
renderer = new ThreadedRenderer(context, translucent, name);
}
return renderer;
}

能不能创建的了ThreadedRenderer则决定于全局配置。如果ro.kernel.qemu的配置为0,说明支持OpenGL 则可以直接返回true。如果qemu.gles为-1说明不支持OpenGL es返回false,只能使用软件渲染。如果设置了qemu.gles并大于0,才能打开硬件渲染。

ThreadedRenderer构造函数

    ThreadedRenderer(Context context, boolean translucent, String name) {
...

long rootNodePtr = nCreateRootRenderNode();
mRootNode = RenderNode.adopt(rootNodePtr);
mRootNode.setClipToBounds(false);
mIsOpaque = !translucent;
mNativeProxy = nCreateProxy(translucent, rootNodePtr);
nSetName(mNativeProxy, name);

ProcessInitializer.sInstance.init(context, mNativeProxy);

loadSystemProperties();
}

我们能看到ThreadedRenderer在初始化,做了三件事情:

  • 1.nCreateRootRenderNode 创建native层的RootRenderNode,也就是所有RenderNode的根。类似DecorView的角色,是所有View的父布局,我们把整个View层次看成一个树,那么这里是根节点。
  • 2.RenderNode.adopt 根据native的 RootRenderNode创建Java层的根部RenderNode。
  • 3.nCreateProxy 创建RenderNode的代理者,nSetName给该代理者赋予名字。
  • 4.ProcessInitializer的初始化graphicsstats服务
  • 5.loadSystemProperties 读取系统给硬件渲染器设置的属性。

关键是看1-3点中ThreadRenderer都做了什么。

nCreateRootRenderNode

static jlong android_view_ThreadedRenderer_createRootRenderNode(JNIEnv* env, jobject clazz) {
RootRenderNode* node = new RootRenderNode(env);
node->incStrong(0);
node->setName("RootRenderNode");
return reinterpret_cast<jlong>(node);
}

能看到这里是直接实例化一个RootRenderNode对象,并把指针的地址直接返回。

class RootRenderNode : public RenderNode, ErrorHandler {
public:
explicit RootRenderNode(JNIEnv* env) : RenderNode() {
mLooper = Looper::getForThread();
env->GetJavaVM(&mVm);
}
}

能看到RootRenderNode继承了RenderNode对象,并且保存一个JavaVM也就是我们所说的Java虚拟机对象,一个java进程全局只有一个。同时通过getForThread方法,获取ThreadLocal中的Looper对象。这里实际上拿的就是UI线程的Looper。

native层RenderNode 的实例化
RenderNode::RenderNode()
: mDirtyPropertyFields(0)
, mNeedsDisplayListSync(false)
, mDisplayList(nullptr)
, mStagingDisplayList(nullptr)
, mAnimatorManager(*this)
, mParentCount(0) {}

在这个构造函数有一个mDisplayList十分重要,记住之后会频繁出现。接着来看看RenderNode的头文件:

class RenderNode : public VirtualLightRefBase {
friend class TestUtils; // allow TestUtils to access syncDisplayList / syncProperties
friend class FrameBuilder;


...

private:
...
} /* namespace uirenderer */
} /* namespace android */

实际上我把几个重要的对象留下来:

  • 1.mDisplayList 实际上就是RenderNode中持有的所有的子RenderNode对象
  • 2.mStagingDisplayList 这个一般是一个View遍历完后保存下来的DisplayList,之后会在绘制行为之前转化为mDisplayList
  • 3.RenderProperties mProperties 是指RenderNode的宽高等信息的存储对象
  • 4.OffscreenBuffer mProperties RenderNode真正的渲染内存对象。

RenderNode.adopt

    public static RenderNode adopt(long nativePtr) {
return new RenderNode(nativePtr);
}

能看到很简单,就是包裹一个native层的RenderNode返回一个Java层对应的对象开放Java层的操作API。

nCreateProxy

static jlong android_view_ThreadedRenderer_createProxy(JNIEnv* env, jobject clazz,
jboolean translucent, jlong rootRenderNodePtr) {
RootRenderNode* rootRenderNode = reinterpret_cast<RootRenderNode*>(rootRenderNodePtr);
ContextFactoryImpl factory(rootRenderNode);
return (jlong) new RenderProxy(translucent, rootRenderNode, &factory);
}

能看到这个过程生成了两个对象:

  • 1.ContextFactoryImpl 动画上下文工厂
class ContextFactoryImpl : public IContextFactory {
public:
explicit ContextFactoryImpl(RootRenderNode* rootNode) : mRootNode(rootNode) {}

virtual AnimationContext* createAnimationContext(renderthread::TimeLord& clock) {
return new AnimationContextBridge(clock, mRootNode);
}

private:
RootRenderNode* mRootNode;
};

这个对象实际上让RenderProxy持有一个创建动画上下文的工厂。RenderProxy可以通过ContextFactoryImpl为每一个RenderNode创建一个动画执行对象的上下文AnimationContextBridge。

  • 2.RenderProxy 一个 根RenderNode的代理对象。这个代理对象将作为所有绘制开始遍历入口。

RenderProxy 根RenderNode的代理对象的创建

RenderProxy::RenderProxy(bool translucent, RenderNode* rootRenderNode,
IContextFactory* contextFactory)
: mRenderThread(RenderThread::getInstance()), mContext(nullptr) {
mContext = mRenderThread.queue().runSync([&]() -> CanvasContext* {
return CanvasContext::create(mRenderThread, translucent, rootRenderNode, contextFactory);
});
mDrawFrameTask.setContext(&mRenderThread, mContext, rootRenderNode);
}
  • 1.RenderThread 硬件渲染线程,所有的硬件渲染命令都需要经过这个线程排队执行。初始化方法如下:
RenderThread::getInstance()
  • 2.CanvasContext 一个硬件Canvas的上下文,一般来说就在这个上下文决定了使用OpenGL es还是其他的渲染管道。初始化方法如下:
CanvasContext::create(mRenderThread, translucent, rootRenderNode, contextFactory);
  • 2.DrawFrameTask 每一帧绘制的任务对象。

我们依次看看他们初始化都做了什么。

RenderThread的初始化和运行机制

RenderThread& RenderThread::getInstance() {
static RenderThread* sInstance = new RenderThread();
gHasRenderThreadInstance = true;
return *sInstance;
}

能看到其实就是简单的调用RenderThread的构造函数进行实例化,并且返回对象的指针。

RenderThread是一个线程对象。先来看看其头文件继承的对象:

class RenderThread : private ThreadBase {
PREVENT_COPY_AND_ASSIGN(RenderThread);

public:
// Sets a callback that fires before any RenderThread setup has occured.
ANDROID_API static void setOnStartHook(void (*onStartHook)());

WorkQueue& queue() { return ThreadBase::queue(); }
...
}

其中RenderThread的中进行排队处理的任务队列实际上是来自ThreadBase的WorkQueue对象。

class ThreadBase : protected Thread {
PREVENT_COPY_AND_ASSIGN(ThreadBase);

public:
ThreadBase()
: Thread(false)
, mLooper(new Looper(false))
, mQueue([this]() { mLooper->wake(); }, mLock) {}

WorkQueue& queue() { return mQueue; }

void requestExit() {
Thread::requestExit();
mLooper->wake();
}

void start(const char* name = "ThreadBase") { Thread::run(name); }
...
}

ThreadBase则是继承于Thread对象。当调用start方法时候其实就是调用Thread的run方法启动线程。

另一个更加关键的对象,就是实例化一个Looper对象到WorkQueue中。而直接实例化Looper实际上就是新建一个Looper。但是这个Looper并没有获取当先线程的Looper,这个Looper做什么的呢?下文就会揭晓。

WorkQueue把一个Looper的方法指针设置到其中,其作用可能是完成了某一件任务后唤醒Looper继续工作。

RenderThread::RenderThread()
: ThreadBase()
, mVsyncSource(nullptr)
, mVsyncRequested(false)
, mFrameCallbackTaskPending(false)
, mRenderState(nullptr)
, mEglManager(nullptr)
, mVkManager(nullptr) {
Properties::load();
start("RenderThread");
}
  • 1.先从Properties读取一些全局配置,进行一些如debug的配置。
  • 2.start启动当前的线程

而start方法会启动Thread的run方法。而run方法最终会走到threadLoop方法中,至于是怎么走进来的,之后有机会会解剖虚拟机的源码线程篇章进行讲解。

RenderThread::threadLoop

bool RenderThread::threadLoop() {
setpriority(PRIO_PROCESS, 0, PRIORITY_DISPLAY);
if (gOnStartHook) {
gOnStartHook();
}
initThreadLocals();

while (true) {
waitForWork();
processQueue();

if (mPendingRegistrationFrameCallbacks.size() && !mFrameCallbackTaskPending) {
drainDisplayEventQueue();
mFrameCallbacks.insert(mPendingRegistrationFrameCallbacks.begin(),
mPendingRegistrationFrameCallbacks.end());
mPendingRegistrationFrameCallbacks.clear();
requestVsync();
}

if (!mFrameCallbackTaskPending && !mVsyncRequested && mFrameCallbacks.size()) {
requestVsync();
}
}

return false;
}

在threadloop中关键的步骤有如下四个:

  • 1.initThreadLocals 初始化线程本地变量
  • 2.waitForWork 等待RenderThread的渲染工作
  • 3.processQueue 执行保存在WorkQueue的渲染工作
  • 4.mPendingRegistrationFrameCallbacks大于0或者mFrameCallbacks大于0;并且mFrameCallbackTaskPending为false,则会调用requestVsync,打开SF进程的EventThread的阻塞让监听返回。mFrameCallbackTaskPending这个方法代表Vsync信号来了并且执行则mFrameCallbackTaskPending为true。
initThreadLocals
void RenderThread::initThreadLocals() {
mDisplayInfo = DeviceInfo::queryDisplayInfo();
nsecs_t frameIntervalNanos = static_cast<nsecs_t>(1000000000 / mDisplayInfo.fps);
mTimeLord.setFrameInterval(frameIntervalNanos);
initializeDisplayEventReceiver();
mEglManager = new EglManager(*this);
mRenderState = new RenderState(*this);
mVkManager = new VulkanManager(*this);
mCacheManager = new CacheManager(mDisplayInfo);
}

在这个过程中创建了几个核心对象:

  • 1.EglManager 当使用OpenGL 相关的管道的时候,将会通过EglManager对OpenGL进行上下文等操作。
  • 2.VulkanManager 当使用Vulkan 的渲染管道,将会使用VulkanManager进行操作(Vulkan 是新一代的3d硬件显卡渲染api,比起OpenGL更加轻量化,性能更佳)
  • 3.RenderState 渲染状态,内有OpenGL和Vulkan的管道,需要渲染的Layer等。

另一个核心的方法就是initializeDisplayEventReceiver,这个方法为WorkQueue的Looper注册了监听:

void RenderThread::initializeDisplayEventReceiver() {
LOG_ALWAYS_FATAL_IF(mVsyncSource, "Initializing a second DisplayEventReceiver?");

if (!Properties::isolatedProcess) {
auto receiver = std::make_unique<DisplayEventReceiver>();
status_t status = receiver->initCheck();

mLooper->addFd(receiver->getFd(), 0, Looper::EVENT_INPUT,
RenderThread::displayEventReceiverCallback, this);
mVsyncSource = new DisplayEventReceiverWrapper(std::move(receiver));
} else {
mVsyncSource = new DummyVsyncSource(this);
}
}

能看到在这个Looper中注册了对DisplayEventReceiver的监听,也就是Vsync信号的监听,回调方法为displayEventReceiverCallback。

我们暂时先对RenderThread的initializeDisplayEventReceiver方法探索到这里,我们稍后继续看看回调后的逻辑。

waitForWork 对Looper监听的对象进行阻塞等待
    void waitForWork() {
nsecs_t nextWakeup;
{
std::unique_lock lock{mLock};
nextWakeup = mQueue.nextWakeup(lock);
}
int timeout = -1;
if (nextWakeup < std::numeric_limits<nsecs_t>::max()) {
timeout = ns2ms(nextWakeup - WorkQueue::clock::now());
if (timeout < 0) timeout = 0;
}
int result = mLooper->pollOnce(timeout);
}

能看到这里的逻辑很简单实际上就是调用Looper的pollOnce方法,阻塞Looper中的循环,直到Vsync的信号到来才会继续往下执行。

processQueue
void processQueue() { mQueue.process(); }

实际上调用的是WorkQueue的process方法。

WorkQueue的process
    void process() {
auto now = clock::now();
std::vector<WorkItem> toProcess;
{
std::unique_lock _lock{mLock};
if (mWorkQueue.empty()) return;
toProcess = std::move(mWorkQueue);
auto moveBack = find_if(std::begin(toProcess), std::end(toProcess),
[&now](WorkItem& item) { return item.runAt > now; });
if (moveBack != std::end(toProcess)) {
mWorkQueue.reserve(std::distance(moveBack, std::end(toProcess)) + 5);
std::move(moveBack, std::end(toProcess), std::back_inserter(mWorkQueue));
toProcess.erase(moveBack, std::end(toProcess));
}
}
for (auto& item : toProcess) {
item.work();
}
}

能看到这个过程中很简单,几乎和Message的loop的逻辑一致。如果Looper的阻塞打开了,则首先找到预计执行时间比当前时刻都大的WorkItem。并且从mWorkQueue移除,最后添加到toProcess中,并且执行每一个WorkItem的work方法。而每一个WorkItem其实就是通过从某一个压入方法添加到mWorkQueue中。

到这里,我们就明白了RenderThread中是如何消费渲染任务的。那么这些渲染任务又是哪里诞生呢?

RenderThread 相应Vsync信号的回调

在RenderThread中的Looper会监听Vsync信号,当信号回调后将会执行下面的回调。

displayEventReceiverCallback

int RenderThread::displayEventReceiverCallback(int fd, int events, void* data) {
if (events & (Looper::EVENT_ERROR | Looper::EVENT_HANGUP)) {
return 0; // remove the callback
}

if (!(events & Looper::EVENT_INPUT)) {
return 1; // keep the callback
}

reinterpret_cast<RenderThread*>(data)->drainDisplayEventQueue();

return 1; // keep the callback
}

能看到这个方法的核心实际上就是调用drainDisplayEventQueue方法,对ui渲染任务队列进行处理。

RenderThread::drainDisplayEventQueue
void RenderThread::drainDisplayEventQueue() {
ATRACE_CALL();
nsecs_t vsyncEvent = mVsyncSource->latestVsyncEvent();
if (vsyncEvent > 0) {
mVsyncRequested = false;
if (mTimeLord.vsyncReceived(vsyncEvent) && !mFrameCallbackTaskPending) {
mFrameCallbackTaskPending = true;
nsecs_t runAt = (vsyncEvent + DISPATCH_FRAME_CALLBACKS_DELAY);
queue().postAt(runAt, [this]() { dispatchFrameCallbacks(); });
}
}
}

能到在这里mVsyncRequested设置为false,且mFrameCallbackTaskPending将会设置为true,并且调用queue的postAt的方法执行ui渲染方法。

实际上就是保存这三对象RenderThread;CanvasContext;RenderNode。

    template <class F>
auto runSync(F&& func) -> decltype(func()) {
std::packaged_task<decltype(func())()> task{std::forward<F>(func)};
post([&task]() { std::invoke(task); });
return task.get_future().get();
};

能看到这个方法实际上也是调用post执行排队执行任务,不同的是,这里使用了线程的Future方式,阻塞了执行,等待CanvasContext的setName工作完毕。


收起阅读 »

iOS - 图像IO 一

图像IO潜伏期值得思考 - 凯文 帕萨特    在第13章“高效绘图”中,我们研究了和Core Graphics绘图相关的性能问题,以及如何修复。和绘图性能相关紧密相关的是图像性能。在这一章中,我们将研究如何优...
继续阅读 »

图像IO

潜伏期值得思考 - 凯文 帕萨特

    在第13章“高效绘图”中,我们研究了和Core Graphics绘图相关的性能问题,以及如何修复。和绘图性能相关紧密相关的是图像性能。在这一章中,我们将研究如何优化从闪存驱动器或者网络中加载和显示图片。

加载和潜伏

    绘图实际消耗的时间通常并不是影响性能的因素。图片消耗很大一部分内存,而且不太可能把需要显示的图片都保留在内存中,所以需要在应用运行的时候周期性地加载和卸载图片。

    图片文件加载的速度被CPU和IO(输入/输出)同时影响。iOS设备中的闪存已经比传统硬盘快很多了,但仍然比RAM慢将近200倍左右,这就需要很小心地管理加载,来避免延迟。

    只要有可能,试着在程序生命周期不易察觉的时候来加载图片,例如启动,或者在屏幕切换的过程中。按下按钮和按钮响应事件之间最大的延迟大概是200ms,这比动画每一帧切换的16ms小得多。你可以在程序首次启动的时候加载图片,但是如果20秒内无法启动程序的话,iOS检测计时器就会终止你的应用(而且如果启动大于2,3秒的话用户就会抱怨了)。

    有些时候,提前加载所有的东西并不明智。比如说包含上千张图片的图片传送带:用户希望能够能够平滑快速翻动图片,所以就不可能提前预加载所有图片;那样会消耗太多的时间和内存。

    有时候图片也需要从远程网络连接中下载,这将会比从磁盘加载要消耗更多的时间,甚至可能由于连接问题而加载失败(在几秒钟尝试之后)。你不能够在主线程中加载网络造成等待,所以需要后台线程。

线程加载

    在第12章“性能调优”我们的联系人列表例子中,图片都非常小,所以可以在主线程同步加载。但是对于大图来说,这样做就不太合适了,因为加载会消耗很长时间,造成滑动的不流畅。滑动动画会在主线程的run loop中更新,所以会有更多运行在渲染服务进程中CPU相关的性能问题。

    清单14.1显示了一个通过UICollectionView实现的基础的图片传送器。图片在主线程中-collectionView:cellForItemAtIndexPath:方法中同步加载(见图14.1)。

清单14.1 使用UICollectionView实现的图片传送器


#import "ViewController.h"

@interface ViewController()

@property (nonatomic, copy) NSArray *imagePaths;
@property (nonatomic, weak) IBOutlet UICollectionView *collectionView;

@end

@implementation ViewController

- (void)viewDidLoad
{
//set up data
self.imagePaths =
[[NSBundle mainBundle] pathsForResourcesOfType:@"png" inDirectory:@"Vacation Photos"];
//register cell class
[self.collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"Cell"];
}

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return [self.imagePaths count];
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView
cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
//dequeue cell
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath];

//add image view
const NSInteger imageTag = 99;
UIImageView *imageView = (UIImageView *)[cell viewWithTag:imageTag];
if (!imageView) {
imageView = [[UIImageView alloc] initWithFrame: cell.contentView.bounds];
imageView.tag = imageTag;
[cell.contentView addSubview:imageView];
}
//set image
NSString *imagePath = self.imagePaths[indexPath.row];
imageView.image = [UIImage imageWithContentsOfFile:imagePath];
return cell;
}

@end

图14.1

图14.1 运行中的图片传送器

    传送器中的图片尺寸为800x600像素的PNG,对iPhone5来说,1/60秒要加载大概700KB左右的图片。当传送器滚动的时候,图片也在实时加载,于是(预期中的)卡动就发生了。时间分析工具(图14.2)显示了很多时间都消耗在了UIImage+imageWithContentsOfFile:方法中了。很明显,图片加载造成了瓶颈。

图14.2

图14.2 时间分析工具展示了CPU瓶颈

    这里提升性能唯一的方式就是在另一个线程中加载图片。这并不能够降低实际的加载时间(可能情况会更糟,因为系统可能要消耗CPU时间来处理加载的图片数据),但是主线程能够有时间做一些别的事情,比如响应用户输入,以及滑动动画。

    为了在后台线程加载图片,我们可以使用GCD或者NSOperationQueue创建自定义线程,或者使用CATiledLayer。为了从远程网络加载图片,我们可以使用异步的NSURLConnection,但是对本地存储的图片,并不十分有效。

GCD和NSOperationQueue

    GCD(Grand Central Dispatch)和NSOperationQueue很类似,都给我们提供了队列闭包块来在线程中按一定顺序来执行。NSOperationQueue有一个Objecive-C接口(而不是使用GCD的全局C函数),同样在操作优先级和依赖关系上提供了很好的粒度控制,但是需要更多地设置代码。

    清单14.2显示了在低优先级的后台队列而不是主线程使用GCD加载图片的-collectionView:cellForItemAtIndexPath:方法,然后当需要加载图片到视图的时候切换到主线程,因为在后台线程访问视图会有安全隐患。

    由于视图在UICollectionView会被循环利用,我们加载图片的时候不能确定是否被不同的索引重新复用。为了避免图片加载到错误的视图中,我们在加载前把单元格打上索引的标签,然后在设置图片的时候检测标签是否发生了改变。

清单14.2 使用GCD加载传送图片

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView
cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
//dequeue cell
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"Cell"
forIndexPath:indexPath];
//add image view
const NSInteger imageTag = 99;
UIImageView *imageView = (UIImageView *)[cell viewWithTag:imageTag];
if (!imageView) {
imageView = [[UIImageView alloc] initWithFrame: cell.contentView.bounds];
imageView.tag = imageTag;
[cell.contentView addSubview:imageView];
}
//tag cell with index and clear current image
cell.tag = indexPath.row;
imageView.image = nil;
//switch to background thread
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
//load image
NSInteger index = indexPath.row;
NSString *imagePath = self.imagePaths[index];
UIImage *image = [UIImage imageWithContentsOfFile:imagePath];
//set image on main thread, but only if index still matches up
dispatch_async(dispatch_get_main_queue(), ^{
if (index == cell.tag) {
imageView.image = image; }
});
});
return cell;
}

    当运行更新后的版本,性能比之前不用线程的版本好多了,但仍然并不完美(图14.3)。

    我们可以看到+imageWithContentsOfFile:方法并不在CPU时间轨迹的最顶部,所以我们的确修复了延迟加载的问题。问题在于我们假设传送器的性能瓶颈在于图片文件的加载,但实际上并不是这样。加载图片数据到内存中只是问题的第一部分。

图14.3

图14.3 使用后台线程加载图片来提升性能

延迟解压

    一旦图片文件被加载就必须要进行解码,解码过程是一个相当复杂的任务,需要消耗非常长的时间。解码后的图片将同样使用相当大的内存。

    用于加载的CPU时间相对于解码来说根据图片格式而不同。对于PNG图片来说,加载会比JPEG更长,因为文件可能更大,但是解码会相对较快,而且Xcode会把PNG图片进行解码优化之后引入工程。JPEG图片更小,加载更快,但是解压的步骤要消耗更长的时间,因为JPEG解压算法比基于zip的PNG算法更加复杂。

    当加载图片的时候,iOS通常会延迟解压图片的时间,直到加载到内存之后。这就会在准备绘制图片的时候影响性能,因为需要在绘制之前进行解压(通常是消耗时间的问题所在)。

    最简单的方法就是使用UIImage+imageNamed:方法避免延时加载。不像+imageWithContentsOfFile:(和其他别的UIImage加载方法),这个方法会在加载图片之后立刻进行解压(就和本章之前我们谈到的好处一样)。问题在于+imageNamed:只对从应用资源束中的图片有效,所以对用户生成的图片内容或者是下载的图片就没法使用了。

    另一种立刻加载图片的方法就是把它设置成图层内容,或者是UIImageViewimage属性。不幸的是,这又需要在主线程执行,所以不会对性能有所提升。

    第三种方式就是绕过UIKit,像下面这样使用ImageIO框架:

NSInteger index = indexPath.row;
NSURL *imageURL = [NSURL fileURLWithPath:self.imagePaths[index]];
NSDictionary *options = @{(__bridge id)kCGImageSourceShouldCache: @YES};
CGImageSourceRef source = CGImageSourceCreateWithURL((__bridge CFURLRef)imageURL, NULL);
CGImageRef imageRef = CGImageSourceCreateImageAtIndex(source, 0,(__bridge CFDictionaryRef)options);
UIImage *image = [UIImage imageWithCGImage:imageRef];
CGImageRelease(imageRef);
CFRelease(source);

    这样就可以使用kCGImageSourceShouldCache来创建图片,强制图片立刻解压,然后在图片的生命周期保留解压后的版本。

    最后一种方式就是使用UIKit加载图片,但是立刻会知道CGContext中去。图片必须要在绘制之前解压,所以就强制了解压的及时性。这样的好处在于绘制图片可以再后台线程(例如加载本身)执行,而不会阻塞UI。

    有两种方式可以为强制解压提前渲染图片:

  • 将图片的一个像素绘制成一个像素大小的CGContext。这样仍然会解压整张图片,但是绘制本身并没有消耗任何时间。这样的好处在于加载的图片并不会在特定的设备上为绘制做优化,所以可以在任何时间点绘制出来。同样iOS也就可以丢弃解压后的图片来节省内存了。

  • 将整张图片绘制到CGContext中,丢弃原始的图片,并且用一个从上下文内容中新的图片来代替。这样比绘制单一像素那样需要更加复杂的计算,但是因此产生的图片将会为绘制做优化,而且由于原始压缩图片被抛弃了,iOS就不能够随时丢弃任何解压后的图片来节省内存了。

    需要注意的是苹果特别推荐了不要使用这些诡计来绕过标准图片解压逻辑(所以也是他们选择用默认处理方式的原因),但是如果你使用很多大图来构建应用,那如果想提升性能,就只能和系统博弈了。

    如果不使用+imageNamed:,那么把整张图片绘制到CGContext可能是最佳的方式了。尽管你可能认为多余的绘制相较别的解压技术而言性能不是很高,但是新创建的图片(在特定的设备上做过优化)可能比原始图片绘制的更快。

    同样,如果想显示图片到比原始尺寸小的容器中,那么一次性在后台线程重新绘制到正确的尺寸会比每次显示的时候都做缩放会更有效(尽管在这个例子中我们加载的图片呈现正确的尺寸,所以不需要多余的优化)。

        如果修改了-collectionView:cellForItemAtIndexPath:方法来重绘图片(清单14.3),你会发现滑动更加平滑。

清单14.3 强制图片解压显示

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView
cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
//dequeue cell
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath];
...
//switch to background thread
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
//load image
NSInteger index = indexPath.row;
NSString *imagePath = self.imagePaths[index];
UIImage *image = [UIImage imageWithContentsOfFile:imagePath];
//redraw image using device context
UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, YES, 0);
[image drawInRect:imageView.bounds];
image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
//set image on main thread, but only if index still matches up
dispatch_async(dispatch_get_main_queue(), ^{
if (index == cell.tag) {
imageView.image = image;
}
});
});
return cell;
}

CATiledLayer

    如第6章“专用图层”中的例子所示,CATiledLayer可以用来异步加载和显示大型图片,而不阻塞用户输入。但是我们同样可以使用CATiledLayerUICollectionView中为每个表格创建分离的CATiledLayer实例加载传动器图片,每个表格仅使用一个图层。

    这样使用CATiledLayer有几个潜在的弊端:

  • CATiledLayer的队列和缓存算法没有暴露出来,所以我们只能祈祷它能匹配我们的需求

  • CATiledLayer需要我们每次重绘图片到CGContext中,即使它已经解压缩,而且和我们单元格尺寸一样(因此可以直接用作图层内容,而不需要重绘)。

    我们来看看这些弊端有没有造成不同:清单14.4显示了使用CATiledLayer对图片传送器的重新实现。

清单14.4 使用CATiledLayer的图片传送器

#import "ViewController.h"
#import

@interface ViewController()

@property (nonatomic, copy) NSArray *imagePaths;
@property (nonatomic, weak) IBOutlet UICollectionView *collectionView;

@end

@implementation ViewController

- (void)viewDidLoad
{
//set up data
self.imagePaths = [[NSBundle mainBundle] pathsForResourcesOfType:@"jpg" inDirectory:@"Vacation Photos"];
[self.collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"Cell"];
}

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return [self.imagePaths count];
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
//dequeue cell
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath];
//add the tiled layer
CATiledLayer *tileLayer = [cell.contentView.layer.sublayers lastObject];
if (!tileLayer) {
tileLayer = [CATiledLayer layer];
tileLayer.frame = cell.bounds;
tileLayer.contentsScale = [UIScreen mainScreen].scale;
tileLayer.tileSize = CGSizeMake(cell.bounds.size.width * [UIScreen mainScreen].scale, cell.bounds.size.height * [UIScreen mainScreen].scale);
tileLayer.delegate = self;
[tileLayer setValue:@(indexPath.row) forKey:@"index"];
[cell.contentView.layer addSublayer:tileLayer];
}
//tag the layer with the correct index and reload
tileLayer.contents = nil;
[tileLayer setValue:@(indexPath.row) forKey:@"index"];
[tileLayer setNeedsDisplay];
return cell;
}

- (void)drawLayer:(CATiledLayer *)layer inContext:(CGContextRef)ctx
{
//get image index
NSInteger index = [[layer valueForKey:@"index"] integerValue];
//load tile image
NSString *imagePath = self.imagePaths[index];
UIImage *tileImage = [UIImage imageWithContentsOfFile:imagePath];
//calculate image rect
CGFloat aspectRatio = tileImage.size.height / tileImage.size.width;
CGRect imageRect = CGRectZero;
imageRect.size.width = layer.bounds.size.width;
imageRect.size.height = layer.bounds.size.height * aspectRatio;
imageRect.origin.y = (layer.bounds.size.height - imageRect.size.height)/2;
//draw tile
UIGraphicsPushContext(ctx);
[tileImage drawInRect:imageRect];
UIGraphicsPopContext();
}

@end

    需要解释几点:

  • CATiledLayertileSize属性单位是像素,而不是点,所以为了保证瓦片和表格尺寸一致,需要乘以屏幕比例因子。

  • -drawLayer:inContext:方法中,我们需要知道图层属于哪一个indexPath以加载正确的图片。这里我们利用了CALayer的KVC来存储和检索任意的值,将图层和索引打标签。

    结果CATiledLayer工作的很好,性能问题解决了,而且和用GCD实现的代码量差不多。仅有一个问题在于图片加载到屏幕上后有一个明显的淡入(图14.4)。

图14.4

图14.4 加载图片之后的淡入

    我们可以调整CATiledLayerfadeDuration属性来调整淡入的速度,或者直接将整个渐变移除,但是这并没有根本性地去除问题:在图片加载到准备绘制的时候总会有一个延迟,这将会导致滑动时候新图片的跳入。这并不是CATiledLayer的问题,使用GCD的版本也有这个问题。

    即使使用上述我们讨论的所有加载图片和缓存的技术,有时候仍然会发现实时加载大图还是有问题。就和13章中提到的那样,iPad上一整个视网膜屏图片分辨率达到了2048x1536,而且会消耗12MB的RAM(未压缩)。第三代iPad的硬件并不能支持1/60秒的帧率加载,解压和显示这种图片。即使用后台线程加载来避免动画卡顿,仍然解决不了问题。

    我们可以在加载的同时显示一个占位图片,但这并没有根本解决问题,我们可以做到更好。

分辨率交换

    视网膜分辨率(根据苹果市场定义)代表了人的肉眼在正常视角距离能够分辨的最小像素尺寸。但是这只能应用于静态像素。当观察一个移动图片时,你的眼睛就会对细节不敏感,于是一个低分辨率的图片和视网膜质量的图片没什么区别了。

    如果需要快速加载和显示移动大图,简单的办法就是欺骗人眼,在移动传送器的时候显示一个小图(或者低分辨率),然后当停止的时候再换成大图。这意味着我们需要对每张图片存储两份不同分辨率的副本,但是幸运的是,由于需要同时支持Retina和非Retina设备,本来这就是普遍要做到的。

    如果从远程源或者用户的相册加载没有可用的低分辨率版本图片,那就可以动态将大图绘制到较小的CGContext,然后存储到某处以备复用。

    为了做到图片交换,我们需要利用UIScrollView的一些实现UIScrollViewDelegate协议的委托方法(和其他类似于UITableViewUICollectionView基于滚动视图的控件一样):

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate;
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView;

    你可以使用这几个方法来检测传送器是否停止滚动,然后加载高分辨率的图片。只要高分辨率图片和低分辨率图片尺寸颜色保持一致,你会很难察觉到替换的过程(确保在同一台机器使用相同的图像程序或者脚本生成这些图片)。

收起阅读 »

iOS - 高效绘图四

异步绘制    UIKit的单线程天性意味着寄宿图通畅要在主线程上更新,这意味着绘制会打断用户交互,甚至让整个app看起来处于无响应状态。我们对此无能为力,但是如果能避免用户等待绘制完成就好多了。  ...
继续阅读 »

异步绘制

    UIKit的单线程天性意味着寄宿图通畅要在主线程上更新,这意味着绘制会打断用户交互,甚至让整个app看起来处于无响应状态。我们对此无能为力,但是如果能避免用户等待绘制完成就好多了。

    针对这个问题,有一些方法可以用到:一些情况下,我们可以推测性地提前在另外一个线程上绘制内容,然后将由此绘出的图片直接设置为图层的内容。这实现起来可能不是很方便,但是在特定情况下是可行的。Core Animation提供了一些选择:CATiledLayerdrawsAsynchronously属性。

CATiledLayer

    我们在第六章简单探索了一下CATiledLayer。除了将图层再次分割成独立更新的小块(类似于脏矩形自动更新的概念),CATiledLayer还有一个有趣的特性:在多个线程中为每个小块同时调用-drawLayer:inContext:方法。这就避免了阻塞用户交互而且能够利用多核心新片来更快地绘制。只有一个小块的CATiledLayer是实现异步更新图片视图的简单方法。

drawsAsynchronously

    iOS 6中,苹果为CALayer引入了这个令人好奇的属性,drawsAsynchronously属性对传入-drawLayer:inContext:的CGContext进行改动,允许CGContext延缓绘制命令的执行以至于不阻塞用户交互。

    它与CATiledLayer使用的异步绘制并不相同。它自己的-drawLayer:inContext:方法只会在主线程调用,但是CGContext并不等待每个绘制命令的结束。相反地,它会将命令加入队列,当方法返回时,在后台线程逐个执行真正的绘制。

    根据苹果的说法。这个特性在需要频繁重绘的视图上效果最好(比如我们的绘图应用,或者诸如UITableViewCell之类的),对那些只绘制一次或很少重绘的图层内容来说没什么太大的帮助。

总结

本章我们主要围绕用Core Graphics软件绘制讨论了一些性能挑战,然后探索了一些改进方法:比如提高绘制性能或者减少需要绘制的数量。

第14章,『图像IO』,我们将讨论图片的载入性能。

收起阅读 »

iOS - 高效绘图三

脏矩形    有时候用CAShapeLayer或者其他矢量图形图层替代Core Graphics并不是那么切实可行。比如我们的绘图应用:我们用线条完美地完成了矢量绘制。但是设想一下如果我们能进一步提高应用的性能,让它就像...
继续阅读 »

脏矩形

    有时候用CAShapeLayer或者其他矢量图形图层替代Core Graphics并不是那么切实可行。比如我们的绘图应用:我们用线条完美地完成了矢量绘制。但是设想一下如果我们能进一步提高应用的性能,让它就像一个黑板一样工作,然后用『粉笔』来绘制线条。模拟粉笔最简单的方法就是用一个『线刷』图片然后将它粘贴到用户手指碰触的地方,但是这个方法用CAShapeLayer没办法实现。

    我们可以给每个『线刷』创建一个独立的图层,但是实现起来有很大的问题。屏幕上允许同时出现图层上线数量大约是几百,那样我们很快就会超出的。这种情况下我们没什么办法,就用Core Graphics吧(除非你想用OpenGL做一些更复杂的事情)。

    我们的『黑板』应用的最初实现见清单13.3,我们更改了之前版本的DrawingView,用一个画刷位置的数组代替UIBezierPath。图13.2是运行结果

清单13.3 简单的类似黑板的应用

#import "DrawingView.h"
#import
#define BRUSH_SIZE 32

@interface DrawingView ()

@property (nonatomic, strong) NSMutableArray *strokes;

@end

@implementation DrawingView

- (void)awakeFromNib
{
//create array
self.strokes = [NSMutableArray array];
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
//get the starting point
CGPoint point = [[touches anyObject] locationInView:self];

//add brush stroke
[self addBrushStrokeAtPoint:point];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
//get the touch point
CGPoint point = [[touches anyObject] locationInView:self];

//add brush stroke
[self addBrushStrokeAtPoint:point];
}

- (void)addBrushStrokeAtPoint:(CGPoint)point
{
//add brush stroke to array
[self.strokes addObject:[NSValue valueWithCGPoint:point]];

//needs redraw
[self setNeedsDisplay];
}

- (void)drawRect:(CGRect)rect
{
//redraw strokes
for (NSValue *value in self.strokes) {
//get point
CGPoint point = [value CGPointValue];

//get brush rect
CGRect brushRect = CGRectMake(point.x - BRUSH_SIZE/2, point.y - BRUSH_SIZE/2, BRUSH_SIZE, BRUSH_SIZE);

//draw brush stroke 
[[UIImage imageNamed:@"Chalk.png"] drawInRect:brushRect];
}
}
@end

图13.2

图13.2 用程序绘制一个简单的『素描』

    这个实现在模拟器上表现还不错,但是在真实设备上就没那么好了。问题在于每次手指移动的时候我们就会重绘之前的线刷,即使场景的大部分并没有改变。我们绘制地越多,就会越慢。随着时间的增加每次重绘需要更多的时间,帧数也会下降(见图13.3),如何提高性能呢?

图13.3

图13.3 帧率和线条质量会随时间下降。

    为了减少不必要的绘制,Mac OS和iOS设备将会把屏幕区分为需要重绘的区域和不需要重绘的区域。那些需要重绘的部分被称作『脏区域』。在实际应用中,鉴于非矩形区域边界裁剪和混合的复杂性,通常会区分出包含指定视图的矩形位置,而这个位置就是『脏矩形』。

    当一个视图被改动过了,TA可能需要重绘。但是很多情况下,只是这个视图的一部分被改变了,所以重绘整个寄宿图就太浪费了。但是Core Animation通常并不了解你的自定义绘图代码,它也不能自己计算出脏区域的位置。然而,你的确可以提供这些信息。

    当你检测到指定视图或图层的指定部分需要被重绘,你直接调用-setNeedsDisplayInRect:来标记它,然后将影响到的矩形作为参数传入。这样就会在一次视图刷新时调用视图的-drawRect:(或图层代理的-drawLayer:inContext:方法)。

    传入-drawLayer:inContext:CGContext参数会自动被裁切以适应对应的矩形。为了确定矩形的尺寸大小,你可以用CGContextGetClipBoundingBox()方法来从上下文获得大小。调用-drawRect()会更简单,因为CGRect会作为参数直接传入。

    你应该将你的绘制工作限制在这个矩形中。任何在此区域之外的绘制都将被自动无视,但是这样CPU花在计算和抛弃上的时间就浪费了,实在是太不值得了。

    相比依赖于Core Graphics为你重绘,裁剪出自己的绘制区域可能会让你避免不必要的操作。那就是说,如果你的裁剪逻辑相当复杂,那还是让Core Graphics来代劳吧,记住:当你能高效完成的时候才这样做。

    清单13.4 展示了一个-addBrushStrokeAtPoint:方法的升级版,它只重绘当前线刷的附近区域。另外也会刷新之前线刷的附近区域,我们也可以用CGRectIntersectsRect()来避免重绘任何旧的线刷以不至于覆盖已更新过的区域。这样做会显著地提高绘制效率(见图13.4)

    清单13.4 用-setNeedsDisplayInRect:来减少不必要的绘制

- (void)addBrushStrokeAtPoint:(CGPoint)point
{
//add brush stroke to array
[self.strokes addObject:[NSValue valueWithCGPoint:point]];

//set dirty rect
[self setNeedsDisplayInRect:[self brushRectForPoint:point]];
}

- (CGRect)brushRectForPoint:(CGPoint)point
{
return CGRectMake(point.x - BRUSH_SIZE/2, point.y - BRUSH_SIZE/2, BRUSH_SIZE, BRUSH_SIZE);
}

- (void)drawRect:(CGRect)rect
{
//redraw strokes
for (NSValue *value in self.strokes) {
//get point
CGPoint point = [value CGPointValue];

//get brush rect
CGRect brushRect = [self brushRectForPoint:point];

//only draw brush stroke if it intersects dirty rect
if (CGRectIntersectsRect(rect, brushRect)) {
//draw brush stroke
[[UIImage imageNamed:@"Chalk.png"] drawInRect:brushRect];
}
}
}

图13.4

图13.4 更好的帧率和顺滑线条

收起阅读 »

Android 中java多线程编程及注意事项

开启线程方式://方式1 public class MyThread extends Thread{ @Override public void run() { super.run(); //do my work...
继续阅读 »

开启线程方式:

//方式1
public class MyThread extends Thread{
@Override
public void run() {
super.run();
//do my work
}
}
new MyThread().start();

//方式2:
public class MyRunnable implements Runnable{
@Override
public void run() {
//do my work
}
}
new Thread(new MyRunnable()).start();

//方式3
class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "result";
}
}
FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
new Thread(futureTask).start();

当然还有一些 HandlerThread、ThreadPool、AsyncTask 等也可以开启新线程,都是基于Thread的封装


线程状态:

image-20210829004506607.png

等待和阻塞的区别:简单理解,等待是线程判断条件不满足,主动调用指令进入的一种状态;而阻塞是被动进入,而且只有synchronized关键字修饰时可能触发。

还一种说法:将IO、sleep、synchronized等进入的线程block状态称为阻塞,他们的共同点是让出cpu,但不释放已经拿到的锁,参考:blog.csdn.net/weixin_3104…


线程安全

不共享数据:ThreadLocal

        ThreadLocal<String> threadLocal = new ThreadLocal<>();

Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
threadLocal.set("线程1数据");
//do some work ...

String data = threadLocal.get();
}
});

Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
threadLocal.set("线程2数据");
//do some work ...

String data = threadLocal.get();
}
});

原理:

没个线程里都有一个threadlocalmap数组, 调用threadlocalset方法时,以threadlocal变量hash值做key,将对于value存入数组中。 image-20210829011508921.png

共享数据:

1.jvm关键字 synchronized, 注:非公平、可重入

    private List<Integer> shareData = new ArrayList<>();

public void trySync(int data) {
synchronized (this) {//锁TestSyncData对象, thread1, thread2 ...
//操作共享数据
shareData.add(data);
}
//do something later ...
}

注意坑点:锁范围,存活周期不一样

    public void trySync1() {
synchronized (this) {//锁TestSyncData对象
}
}
//锁TestSyncData对象
public synchronized void trySync2() {
}

//锁TestSyncData类
public synchronized static void trySync3() {
}

//锁TestSyncData类
public void trySync4() {
synchronized (TestSyncData.class){
}
}

有耗时任务获取锁,后续收尾处理一定要考虑锁释放耗时问题;比如单例对象在主线程释放时,一定要注意锁能否及时拿到。

如果不能确定,考虑将锁降级存活时长,比如用栈内锁,线程安全型bean

synchronized原理详见:blog.csdn.net/weixin_3960…

2.wait-notify 函数,java祖先类Object的成员函数

    public void testWait() {
//1.创建同步对象
Object lock = new Object();

new Thread(new Runnable() {
@Override
public void run() {
//2. do something hard ...
shareData = new ShareData();

//3. 唤醒原线程
synchronized (lock){
lock.notify();
}
//5.some other logic
}
}).start();

//4.原线程等待
synchronized (lock){
try {
lock.wait(45000);
} catch (InterruptedException e) {
//6.线程中断触发
e.printStackTrace();
}
}

if (shareData != null) {
//do something happy ...
}
}

坑点多多:

  1. notify不能定向唤醒,只能随机唤醒一个wait的线程(保证notify的数量>=wait数量),使用的时候一定要保证没有多个线程处于wait状态;如果想定向唤醒,考虑使用 ReentrantLock的Condition

  2. 标记5的地方最好不要做其他逻辑,可能不会执行到,尽量保证notify是耗时任务里的最后逻辑

  3. 标记6的地方注意wait会被线程中断,而跳出同步逻辑,如果需要可以使用抗扰动写法:

            while (shareData == null) {//4 抗线程扰动写法
    synchronized (lock) {
    try {
    lock.wait();
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }
  4. 业务销毁的时候,如果不需要等数据返回,可以直接notifyAll,提前结束线程任务,释放对象

3.基于aqs(队列同步器)接口的一系列同步锁或组件 ReentrantLock、Semaphore、CountDownLantch;通过队列+计数+CAS

    private ReentrantLock lock = new ReentrantLock();
private List<Integer> shareData = new ArrayList<>();

public void testLock() {//非标准
lock.lock();
shareData.add(1);
lock.unlock();
}

坑点:注意手动释放,以免死锁; 同步逻辑会有exception,标准最好用try-catch-finally


线程安全的数据类型:StringBuffer、CopyOnWriteArrayList、concurrentxxx、BlockingQueue、Atomicxxx(CAS)

有些基于锁,有些基于无锁化的CAS(compare and swap),有些两者混合

cas原理参考:cloud.tencent.com/developer/a…

image-20210829040630653.png

坑点:

  1. CopyOnWriteArrayList:先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加删除元素,添加删除完元素之后,再将原容器的引用指向新的容器,整个过程加锁,保证了写的线程安全。然而我们还是出现了多线程的数组越界异常
        //不安全写法
for (int i = 0; i < copyOnWriteArrayList.size(); i++) {
copyOnWriteArrayList.get(i);
}

//安全写法
Iterator iterator = copyOnWriteArrayList.iterator();
while (iterator.hasNext()){
iterator.next();
}
  1. Atomicxxx 注意使用场景

    ABA问题   AtomicStampedReference,AtomicMarkableReference
    开销问题
    只能保证一个共享变量的原子操作 AtomicReference

阻塞队列(BlockingQueue): 生产者与消费者模式里,生产者与消费者解耦,生产者与消费者性能均衡问题

BlockingQueue
ArrayBlockingQueue
数组结构的有界阻塞队列
LinkedBlockingQueue
链表结构的有界阻塞队列
PriorityBlockingQueue
优先级排序无界阻塞队列
DelayQueue
优先级队列无界阻塞队列
应用--过期缓存
SynchronousQueue
不存储元素的阻塞队列
应用--newCachedThreadPool
LinkedTransferQueue
链表结构无界阻塞队列
LinkedBlockingDeque
链表结构双向阻塞队列

有界:定义最大容量 无界:不定义


线程池:线程管理与统一调度

创建:

ThreadPoolExecutor(int corePoolSize,    //核心线程数
int maximumPoolSize, //最大线程数
long keepAliveTime, //空闲线程存活时间
TimeUnit unit,
BlockingQueue<Runnable> workQueue, //阻塞队列
ThreadFactory threadFactory, //线程池工厂
RejectedExecutionHandler handler) //拒绝策略

线程比较稀缺,需合理配置,根据任务特性

cpu密集型:内存中取数据计算 (最大线程数 <=Runtime.getRuntime().availableProcessors()+1) 注:+1 虚拟内存-页缺失

IO密集型:网络通信、读写磁盘 (最大线程数 <= cpu核心*2), 高阻塞低占用

混合型:以上两者,如果两者执行时间相当,拆分线程池;否则视权重大的分配。具体时长可以本地测试或者根据下面的经验表预估后选择 image-20210828234846390-0165731.png

执行:注意任务存放位置顺序 1、2、3、4(重点)

image-20210828225601263.png

拒绝策略(饱和策略):

RejectedExecutionHandler
AbortPolicy
直接抛出异常-默认
CallerRunsPolicy
调用者线程执行
DiscardOldestPolicy
丢弃旧的
DiscardPolicy
直接丢弃

关闭:

awaitTermination(long timeout, TimeUnit unit)//阻塞
shutDown()//中断没有执行任务的线程
shutdownNow()//中断所有线程,协作式处理,只是发出中断信号
isShutdown()//是否关闭
收起阅读 »

自定义View

判断自己有没有掌握这个知识点,就模拟面试,看看你能不能给对方讲清楚1. 坐标系在Android坐标系中,以屏幕左上角作为原点,这个原点向右是X轴的正轴,向下是Y轴正轴。如下所示:除了Android坐标系,还存在View坐标系,View坐标系内部关系如图所示。2...
继续阅读 »

判断自己有没有掌握这个知识点,就模拟面试,看看你能不能给对方讲清楚

1. 坐标系

在Android坐标系中,以屏幕左上角作为原点,这个原点向右是X轴的正轴,向下是Y轴正轴。如下所示:

image.png

除了Android坐标系,还存在View坐标系,View坐标系内部关系如图所示。

image.png

2. 自定义属性

Android系统的控件以android开头的都是系统自带的属性。为了方便配置自定义View的属性,我们也可以自定义属性值。
Android自定义属性可分为以下几步:

  1. 自定义一个View
  2. 编写values/attrs.xml,在其中编写styleable和item等标签元素
  3. 在布局文件中View使用自定义的属性(注意namespace)
  4. 在View的构造方法中通过TypedArray获取

自定义View属性很重要,但是并不复杂,需要的话再查一下就好了

3. View绘制流程

View的绘制基本由measure()、layout()、draw()这个三个函数完成

函数作用相关方法
measure()测量View的宽高measure(),setMeasuredDimension(),onMeasure()
layout()计算当前View以及子View的位置layout(),onLayout(),setFrame()
draw()视图的绘制工作draw(),onDraw()

3.1 MeasureSpec

MeasureSpec是View的内部类,它封装了一个View的尺寸,在onMeasure()当中会根据这个MeasureSpec的值来确定View的宽高。

MeasureSpec的值保存在一个int值当中。一个int值有32位,前两位表示模式mode后30位表示大小size。即MeasureSpecmodesize

MeasureSpec当中一共存在三种modeUNSPECIFIEDEXACTLY
AT_MOST

对于View来说,MeasureSpec的mode和Size有如下意义

模式意义对应
EXACTLY精准模式,View需要一个精确值,这个值即为MeasureSpec当中的Sizematch_parent
AT_MOST最大模式,View的尺寸有一个最大值,View不可以超过MeasureSpec当中的Size值wrap_content
UNSPECIFIED无限制,View对尺寸没有任何限制,View设置为多大就应当为多大一般系统内部使用

3.2 Layout()

layout()过程,对于View来说用来计算View的位置参数,对于ViewGroup来说,除了要测量自身位置,还需要测量子View的位置。

3.3 Draw()

draw流程也就是的View绘制到屏幕上的过程,整个流程的入口在Viewdraw()方法之中,而源码注释也写的很明白,整个过程可以分为6个步骤。

  1. 如果需要,绘制背景。
  2. 有过有必要,保存当前canvas。
  3. 绘制View的内容。
  4. 绘制子View。
  5. 如果有必要,绘制边缘、阴影等效果。
  6. 绘制装饰,如滚动条等等。

使用下方的流程图表示:

image.png

布局过程的自定义:

方式: 重写布局过程的相关方法\

1. 测量过程: onMeasure()
2. 布局过程: onLayout()
复制代码

具体:

1. 重写onMeasure()来修改已有的View的尺寸
2. 重写onMeasure()来全新计算自定义View的尺寸
3. 重写onMeasure()和onLayout()来全新计算自定义 ViewGroup 的内部布局
复制代码
public class SquareImageView extends AppCompatImageView {
private static final String TAG = "SquareImageView";

public SquareImageView(Context context) {
super(context);
}

public SquareImageView(Context context, AttributeSet attrs) {
super(context, attrs);
}



@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 先执行原测量算法
super.onMeasure(widthMeasureSpec, heightMeasureSpec);

// 获取原先的测量结果
int measureWidth = getMeasuredWidth();
int measureHeight = getMeasuredHeight();
Log.d(TAG, "onMeasure11" +
", measureWidth = " + measureWidth +
", measureHeight = " + measureHeight +
"");

// 利用原先的测量结果计算出新尺寸
if (measureWidth > measureHeight) {
measureWidth = measureHeight;
} else {
measureHeight = measureWidth;
}
Log.d(TAG, "onMeasure22" +
", measureWidth = " + measureWidth +
", measureHeight = " + measureHeight +
"");
// 保存计算后的结果
setMeasuredDimension(measureWidth, measureHeight);
}
}
复制代码

重写onMeasure() 修改尺寸

1. 重写 onMeasure() 修改尺寸,并调用super.onMeasure触发原先的测量
2. 用getMeasuredWidth() 和 getMeasuredHeight() 取到之前测得的尺寸,利用这两个尺寸来计算出最终尺寸。
3. 使用 setMeasuredDimension() 保存尺寸
复制代码

收起阅读 »

你真的了解Handler吗?

Handler,一个面试中常问的高频词汇。大家想想这个知识点一般是怎么考察的?请解释一下Handler的原理?不不不,这个问题已经烂大街了,我要是面试官,我会这么问。我们知道在Handler中,存在一个方法叫 sendMessageDelay , 作用是延时发...
继续阅读 »

Handler,一个面试中常问的高频词汇。

大家想想这个知识点一般是怎么考察的?请解释一下Handler的原理?

不不不,这个问题已经烂大街了,我要是面试官,我会这么问。

我们知道在Handler中,存在一个方法叫 sendMessageDelay , 作用是延时发送消息,请解释一下Handler是如何实现延时发送消息的?

Looper.loop是一个死循环,拿不到需要处理的Message就会阻塞,那在UI线程中为什么不会导致ANR?

也请各位读者先自己思考一下这两个问题,换做是你该怎么回答。

Handler

我们先从Handler的定义来认识它,先上谷歌原文:

/**
* A Handler allows you to send and process {@link Message} and Runnable
*/

下面由我这枚英语渣上线,强行翻译一波。

  1. Handler是用来结合线程的消息队列来发送、处理Message对象Runnable对象的工具。每一个Handler实例化之后会关联一个线程和该线程的消息队列。当你创建一个Handler的时候,它就会自动绑定到到所在的线程或线程的消息队列,并陆续把Message/Runnable分发到消息队列,然后在它们出队的时候去执行。

  2. Handler主要有两个用途: (1) 调度在将来某个时候执行的MessageRunnable。(2)把需要在另一个线程执行的操作加入到消息队列中去。

  3. post runnablesend messagehandler时,您可以在消息队列准备就绪后立即处理该事务。也可以延迟一段时间执行,或者指定某个特定时间去执行。


我们先从Handler的构造方法来认识一下它:

public Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async) 

Handler的构造方法有很多个,但最终调用的就是上述构造方法。

老规矩,先上官方解释,再上学渣翻译。

* Use the provided {@link Looper} instead of the default one and take a callback
* interface in which to handle messages. Also set whether the handler
* should be asynchronous.
*
* Handlers are synchronous by default unless this constructor is used to make
* one that is strictly asynchronous.
*
* Asynchronous messages represent interrupts or events that do not require global ordering
* with respect to synchronous messages. Asynchronous messages are not subject to
* the synchronization barriers introduced by conditions such as display vsync.
  1. 使用提供的Looper而不是默认的Looper,并使用回调接口来处理消息。还设置处理程序是否应该是异步的。

  2. 默认情况下,Handler是同步的,除非此构造函数用于生成严格异步的Handler

  3. 异步消息指的是不需要进行全局排序的中断或事件。异步消息不受同步障碍(比如display vsync)的影响。


Handler中的方法主要分为以下两类:

  1. 获取及查询消息,比如 obtainMessage(int what),hasMessages(int what)

  2. 将message或runnable添加/移出消息队列,比如 postAtTime(@NonNull Runnable r, long uptimeMillis),sendEmptyMessageDelayed(int what, long delayMillis)

在这些方法中,我们重点需要关注一下enqueueMessage这个方法。

为什么呢?

无论是 postAtTimesendMessageDelayed还是其他的post、send方法,它们最终都会调到enqueueMessage这个方法里去。

比如:

public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
if (delayMillis < 0) {
delayMillis = 0;
}
return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}

可以看到,sendMessageDelayed方法里将延迟时间转换为消息触发的绝对时间,最终调用的是sendMessageAtTime方法。

public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
MessageQueue queue = mQueue;
if (queue == null) {
RuntimeException e = new RuntimeException(
this + " sendMessageAtTime() called with no mQueue");
Log.w("Looper", e.getMessage(), e);
return false;
}
return enqueueMessage(queue, msg, uptimeMillis);
}

而sendMessageAtTime方法调用了enqueueMessage方法。

private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
long uptimeMillis) {
msg.target = this;
msg.workSourceUid = ThreadLocalWorkSource.getUid();

if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}

enqueueMessage方法直接将message交给了MessageQueue去执行。

Message

在分析MessageQueue之前,我们应该先来认识一下Message这个消息载体类。

老规矩,先从定义看起:

* Defines a message containing a description and arbitrary data object that can be
* sent to a {@link Handler}. This object contains two extra int fields and an
* extra object field that allow you to not do allocations in many cases.
*
* <p>While the constructor of Message is public, the best way to get
* one of these is to call {@link #obtain Message.obtain()} or one of the
* {@link Handler#obtainMessage Handler.obtainMessage()} methods, which will pull
* them from a pool of recycled objects.</p>

下面是渣翻译:

  1. 定义一条包含描述和任意数据对象的消息,该对象可以发送到Handler。此对象包含两个额外的int字段和一个额外的object字段。

  2. 尽管Message的构造方法是public,但获取一个Message的最好的方法是调用Message.obtain或者Handler.obtainMessage方法,这些方法会从可回收的线程池中获取Message对象。


我们来认识一下Message里的字段:

public final class Message implements Parcelable {

}

在Message中,我们需要关注一下Message的回收机制。

先来看下recyclerUnchecked方法:

void recycleUnchecked() {
// Mark the message as in use while it remains in the recycled object pool.
// Clear out all other details.
flags = FLAG_IN_USE;
what = 0;
arg1 = 0;
arg2 = 0;
obj = null;
replyTo = null;
sendingUid = UID_NONE;
workSourceUid = UID_NONE;
when = 0;
target = null;
callback = null;
data = null;

synchronized (sPoolSync) {
if (sPoolSize < MAX_POOL_SIZE) {
next = sPool;
sPool = this;
sPoolSize++;
}
}
}

在这个方法中,有三个关键变量。

  1. sPoolSync :主要是给Message加一个对象锁,不允许多个线程同时访问Message类和recycleUnchecked方法。
  2. sPool:存储我们循环利用Message的单链表。这里sPool只是链表的头节点。
  3. sPoolSize:单链表的链表的长度,即存储的Message对象的个数。

当我们调用recycleUnchecked方法时,首先会将当前Message对象的属性清空。然后判断Message是否已到达缓存的上限(50个),如果没有,将当前的Message对象置于链表的头部。

那么取缓存的操作呢?

我们来看下obtain方法:

public static Message obtain() {
synchronized (sPoolSync) {
if (sPool != null) {
Message m = sPool;
sPool = m.next;
m.next = null;
m.flags = 0; // clear in-use flag
sPoolSize--;
return m;
}
}
return new Message();
}

可以看出,Message会尝试取出sPool链表的第一个元素,并将sPool的头元素往后移动一位。如果sPool链表为空,将会返回一个新的Message对象。

Message里提供obtain方法获取Message对象,使得Message到了重复的利用,减少了每次获取Message时去申请空间的时间。同时,这样也不会永无止境的去创建新对象,减小了Jvm垃圾回收的压力,提高了效率。

MessageQueue

MessageQueue用于保存由Looper发送的消息的列表。消息不会直接添加到消息队列,而是通过Handler对象中关联的Looper里的MessageQueue完成添加的动作。

您可以使用Looper.myQueue()检索当前线程的MessageQueue。

我们先来看看MessageQueue如何实现添加一个Message的操作。

boolean enqueueMessage(Message msg, long when) {
//判断msg是否有target属性以及是否正在使用中
if (msg.target == null) {
throw new IllegalArgumentException("Message must have a target.");
}
if (msg.isInUse()) {
throw new IllegalStateException(msg + " This message is already in use.");
}

synchronized (this) {
if (mQuitting) {
IllegalStateException e = new IllegalStateException(
msg.target + " sending message to a Handler on a dead thread");
Log.w(TAG, e.getMessage(), e);
msg.recycle();
return false;
}


// We can assume mPtr != 0 because mQuitting is false.
if (needWake) {
//唤醒消息
nativeWake(mPtr);
}
}
return true;
}

mMessages是一个按照消息实际触发时间msg.when排序的链表,越往后的越晚触发。enqueueMessage方法根据新插入消息的when,将msg插入到链表中合适的位置。如果是及时消息,还需要唤醒MessageQueue

我们接着来看看nativeWake方法,nativeWake方法的源码位于\frameworks\base\core\jni\android_os_MessageQueue.cpp

static void android_os_MessageQueue_nativeWake(JNIEnv* env, jclass clazz, jlong ptr) {
NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr);
nativeMessageQueue->wake();
}

继续看NativeMessageQueue里的wake函数。

void NativeMessageQueue::wake() {
mLooper->wake();
}

它又转交给了Looper(源码位置/system/core/libutils/Looper.cpp)去处理。

void Looper::wake() {
#if DEBUG_POLL_AND_WAKE
ALOGD("%p ~ wake", this);
#endif

uint64_t inc = 1;
ssize_t nWrite = TEMP_FAILURE_RETRY(write(mWakeEventFd.get(), &inc, sizeof(uint64_t)));
if (nWrite != sizeof(uint64_t)) {
if (errno != EAGAIN) {
LOG_ALWAYS_FATAL("Could not write wake signal to fd %d (returned %zd): %s",
mWakeEventFd.get(), nWrite, strerror(errno));
}
}
}

Looper里的wake函数很简单,它只是向mWakeEventFd里写入了一个 1 值。

上述的mWakeEventFd又是什么呢?

Looper::Looper(bool allowNonCallbacks)
: mAllowNonCallbacks(allowNonCallbacks),
mSendingMessage(false),
mPolling(false),
mEpollRebuildRequired(false),
mNextRequestSeq(0),
mResponseIndex(0),
mNextMessageUptime(LLONG_MAX) {

mWakeEventFd.reset(eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC));

...
}

从Looper的构造函数里可以找到答案,mWakeEventFd本质上是一个eventfd。至于什么是eventfd,这里只能说是eventfd是Linux 2.6提供的一种系统调用,它可以用来实现事件通知,更具体的内容需要各位读者自行查阅资料了。

既然有发送端,那么必然有接收端。接收端在哪呢?

void Looper::awoken() {
#if DEBUG_POLL_AND_WAKE
ALOGD("%p ~ awoken", this);
#endif

uint64_t counter;
TEMP_FAILURE_RETRY(read(mWakeEventFd.get(), &counter, sizeof(uint64_t)));
}

可以看到,awoken函数里的内容很简单,只是做了一个读取的动作,它并不关系读到的具体值是啥。为什么要这样设计呢,我们得结合awoken函数在哪里调用去分析。

awoken函数在LooperpollInner函数里调用。pollInner函数里有一条语句

int eventCount = epoll_wait(mEpollFd.get(), eventItems, EPOLL_MAX_EVENTS, timeoutMillis);

它在这里起到阻塞的作用,如果没有调用nativeWake函数,epoll_wait将一直等待写入事件,直到超时为止。

如此,便回到我们文章一开始提出的问题了。

Looper.loop是一个死循环,拿不到需要处理的Message就会阻塞,那在UI线程中为什么不会导致ANR?

首先,我们需要明确一点,Handler中到底有没有阻塞?

答案是有!!!那它为什么不会导致ANR呢?

这得从ANR产生的原理说起。

ANR的本质也是一个Message,这一点很关键。我们拿前台服务的创建来举例,前台服务创建时,会发送一个 what值为ActivityManagerService.SERVICE_TIMEOUT_MSG的延时20s的Message,如果Service的创建 工作在上述消息的延时时间内完成,则会移除该消息,否则,在Handler正常收到这个消息后,就会进行服务超时处理,即弹出ANR对话框。

为什么不会ANR,现在各位读者清楚了吗?ANR消息本身就是通过Handler去派发的,Handler阻塞与否与ANR并没有必然关系。


我们看了MessageQueue是如何加入一条消息的,接下来,我们来看看它是如何取出一条消息的。

Message next() {
//如果消息循环已退出并已被释放,则return
//如果应用程序在退出后尝试重新启动looper,则可能发生这种情况
final long ptr = mPtr;
if (ptr == 0) {
return null;
}

int pendingIdleHandlerCount = -1; // -1 only during first iteration
int nextPollTimeoutMillis = 0;
for (;;) {
if (nextPollTimeoutMillis != 0) {
//将当前线程中挂起的所有Binder命令刷新到内核驱动程序。
//在执行可能会阻塞很长时间的操作之前调用此函数非常有用,以确保已释放任何挂起的对象引用,
//从而防止进程保留对象的时间超过需要的时间。
Binder.flushPendingCommands();
}

// Run the idle handlers.
// We only ever reach this code block during the first iteration.
for (int i = 0; i < pendingIdleHandlerCount; i++) {
final IdleHandler idler = mPendingIdleHandlers[i];
mPendingIdleHandlers[i] = null; // release the reference to the handler

boolean keep = false;
try {
keep = idler.queueIdle();
} catch (Throwable t) {
Log.wtf(TAG, "IdleHandler threw exception", t);
}

if (!keep) {
synchronized (this) {
mIdleHandlers.remove(idler);
}
}
}

// Reset the idle handler count to 0 so we do not run them again.
pendingIdleHandlerCount = 0;

// While calling an idle handler, a new message could have been delivered
// so go back and look again for a pending message without waiting.
nextPollTimeoutMillis = 0;
}
}

next方法里主要做了三件事,(1)使用nativePollOnce阻塞指定时间,等待下一条消息的执行。 (2)获取下一条消息,并返回此消息。 (3)如果消息队列为空,则执行IdleHandler。

这里有个新名词IdleHandlerIdleHandler是可以在 Looper 事件循环的过程中,当出现空闲的时候,允许我们执行任务的一种机制。 MessageQueue中提供了addIdleHandlerremoveIdleHandler去添加删除IdleHandler


next方法的第一行有个ptr变量,这个ptr变量是什么含义呢?

MessageQueue(boolean quitAllowed) {
mQuitAllowed = quitAllowed;
mPtr = nativeInit();
}

mPtr是一个long型变量,它是在MessageQueue的构造方法中,通过nativeInit方法初始化的。

static jlong android_os_MessageQueue_nativeInit(JNIEnv* env, jclass clazz) {
NativeMessageQueue* nativeMessageQueue = new NativeMessageQueue();
if (!nativeMessageQueue) {
jniThrowRuntimeException(env, "Unable to allocate native queue");
return 0;
}

nativeMessageQueue->incStrong(env);
return reinterpret_cast<jlong>(nativeMessageQueue);
}

可以看到,ptr的本质是对 jni层的NativeMessageQueue对象的指针的引用。


我们重点来看下nativePollOnce方法,探寻一下Handler中的阻塞机制。nativePollOnce方法最终调用的是Looper.cpp中的pollOnce函数。

int Looper::pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData) {
int result = 0;
for (;;) { //一个死循环
while (mResponseIndex < mResponses.size()) {
const Response& response = mResponses.itemAt(mResponseIndex++);
int ident = response.request.ident;
if (ident >= 0) {
int fd = response.request.fd;
int events = response.events;
void* data = response.request.data;
#if DEBUG_POLL_AND_WAKE
ALOGD("%p ~ pollOnce - returning signalled identifier %d: "
"fd=%d, events=0x%x, data=%p",
this, ident, fd, events, data);
#endif
if (outFd != nullptr) *outFd = fd;
if (outEvents != nullptr) *outEvents = events;
if (outData != nullptr) *outData = data;
return ident;
}
}

if (result != 0) {
#if DEBUG_POLL_AND_WAKE
ALOGD("%p ~ pollOnce - returning result %d", this, result);
#endif
if (outFd != nullptr) *outFd = 0;
if (outEvents != nullptr) *outEvents = 0;
if (outData != nullptr) *outData = nullptr;
return result;
}

result = pollInner(timeoutMillis);
}
}

函数里有个关于mResponses的while循环,我们从java层调用的暂时不用管它,它是ndk的handler处理逻辑。我们重点来看pollInner函数。

int Looper::pollInner(int timeoutMillis) {
// 根据下一条消息的到期时间调整超时。
if (timeoutMillis != 0 && mNextMessageUptime != LLONG_MAX) {
nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);
int messageTimeoutMillis = toMillisecondTimeoutDelay(now, mNextMessageUptime);
if (messageTimeoutMillis >= 0
&& (timeoutMillis < 0 || messageTimeoutMillis < timeoutMillis)) {
timeoutMillis = messageTimeoutMillis;
}
}

// 默认触发唤醒事件,POLL_WAKE == -1
int result = POLL_WAKE;
mResponses.clear();
mResponseIndex = 0;

// We are about to idle.
mPolling = true;

struct epoll_event eventItems[EPOLL_MAX_EVENTS];
//等待写入事件,写入事件由awoken函数触发。timeoutMillis为超时时间,0立即返回,-1一直等待
int eventCount = epoll_wait(mEpollFd.get(), eventItems, EPOLL_MAX_EVENTS, timeoutMillis);

// No longer idling.
mPolling = false;

// Acquire lock.
mLock.lock();

...

// Check for poll error.
if (eventCount < 0) {
if (errno == EINTR) {
goto Done;
}
ALOGW("Poll failed with an unexpected error: %s", strerror(errno));
//POLL_ERROR == -4
result = POLL_ERROR;
goto Done;
}

// Check for poll timeout.
if (eventCount == 0) {
//POLL_TIMEOUT == -3,epoll超时会走此分支
result = POLL_TIMEOUT;
goto Done;
}

// Handle all events.
for (int i = 0; i < eventCount; i++) {
int fd = eventItems[i].data.fd;
uint32_t epollEvents = eventItems[i].events;
if (fd == mWakeEventFd.get()) {
if (epollEvents & EPOLLIN) {
//将eventfd里的数值取出,无实际含义,只是为了清空epoll事件和eventfd里的数据
awoken();
} else {
ALOGW("Ignoring unexpected epoll events 0x%x on wake event fd.", epollEvents);
}
} else {
//不会走到此分支,忽略它
ssize_t requestIndex = mRequests.indexOfKey(fd);
if (requestIndex >= 0) {
int events = 0;
if (epollEvents & EPOLLIN) events |= EVENT_INPUT;
if (epollEvents & EPOLLOUT) events |= EVENT_OUTPUT;
if (epollEvents & EPOLLERR) events |= EVENT_ERROR;
if (epollEvents & EPOLLHUP) events |= EVENT_HANGUP;
pushResponse(events, mRequests.valueAt(requestIndex));
} else {
ALOGW("Ignoring unexpected epoll events 0x%x on fd %d that is "
"no longer registered.", epollEvents, fd);
}
}
}
Done: ;

// 中间省略的代码不做探究,和ndk的handler实现有关
...
return result;
}

可以看到,pollInner函数主要的逻辑是使用epoll_wait去读取唤醒事件,它有一个最大的等待时长,其最大等待时长和下一条消息的触发时间有关。

需要注意一下pollInner的返回值result,它有三种状态。进入方法默认为POLL_WAKE,表示触发唤醒事件。 接下来通过对epoll_wait返回值的判断,它可能会变更为另两种状态。epoll_wait返回值为0,表示epoll_wait因超时而结束等待,result值设为POLL_TIMEOUT;epoll_wait返回值为-1,表示epoll_wait因系统中断等原因而结束等待,result值设为POLL_ERROR。但不管result值设为哪一个,都会导致pollOnce退出死循环,然代码流程回到java层的next方法中,去取得下一个Message对象。

因此,nativePollOnce简单意义上的理解,它就是一个阻断器,可以将当前线程阻塞,直到超时或者因需立即执行的新消息入队才结束阻塞。

各位读者,看到这里,大家再回过头去想想文章的第一个问题该怎么回答吧。

Looper

Handler 机制中,我们还剩最后一个一个模块没有分析———— Looper。我们先从官方定义来看起:

* Class used to run a message loop for a thread.  Threads by default do
* not have a message loop associated with them; to create one, call
* {@link #prepare} in the thread that is to run the loop, and then
* {@link #loop} to have it process messages until the loop is stopped.

概括一下:

Looper是一个用于在线程中循环遍历消息的类。默认情况下,线程没有与之关联的消息循环;如果要创建一个,请在运行Looper的线程中调用Looper.prepare(),然后使用Looper.loop()让它处理消息直到循环停止。

上面的定义提到了两个比较关键的方法,我们一个一个来看。

Looper.prepare()

private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}

prepare的方法内容非常简单,创建一个Looper对象,并把它放到sThreadLocal里,其中sThreadLocal是一个ThreadLocal类。

ThreadLocal类又是什么呢?

多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。ThreadLocal是除了加锁这种同步方式之外的一种保证一种规避多线程访问出现线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量,这样就不会存在线程不安全问题。

因此,使用ThreadLocal能够保证不同线程的Looper对象都有一个独立的副本,它们彼此独立,互不干扰。


Looper.looper()

public static void loop() {
//获取当前线程的Looper对象
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
//获取与Looper关联的messagequeue
final MessageQueue queue = me.mQueue;

// Make sure the identity of this thread is that of the local process,
// and keep track of what that identity token actually is.
Binder.clearCallingIdentity();
final long ident = Binder.clearCallingIdentity();

// Allow overriding a threshold with a system prop. e.g.
// adb shell 'setprop log.looper.1000.main.slow 1 && stop && start'
final int thresholdOverride =
SystemProperties.getInt("log.looper."
+ Process.myUid() + "."
+ Thread.currentThread().getName()
+ ".slow", 0);

boolean slowDeliveryDetected = false;

for (;;) {
//进入死循环,不断去从MessageQueue中去拉取Message
Message msg = queue.next(); // next方法我们已经在MessageQueue中做了分析
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}


// Make sure the observer won't change while processing a transaction.
final Observer observer = sObserver;

...

final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0;
final long dispatchEnd;
Object token = null;
if (observer != null) {
token = observer.messageDispatchStarting();
}
long origWorkSource = ThreadLocalWorkSource.setUid(msg.workSourceUid);
try {
//注意这里,msg.target是一个handler对象,这个方法最终调用了handler的dispatchMessage
//去做消息分发
msg.target.dispatchMessage(msg);
if (observer != null) {
observer.messageDispatched(token, msg);
}
dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
} catch (Exception exception) {
if (observer != null) {
observer.dispatchingThrewException(token, msg, exception);
}
throw exception;
} finally {
ThreadLocalWorkSource.restore(origWorkSource);
if (traceTag != 0) {
Trace.traceEnd(traceTag);
}
}

//回收Message,上文中有做过分析
msg.recycleUnchecked();
}
}

loop方法主要的工作是:建立一个死循环,不断的通过调用MessageQueue中的next方法获取下一个消息,并最终通过取得的消息关联的handler去完成消息的分发。

总结

最后,我们再来理一理 HandlerMessageMessageQueueLooper四者的关系和职责。

  • Handler : 消息分发的管理者。负责获取消息、封装消息、派发消息以及处理消息。
  • Message :消息的载体类。
  • MessageQueue :消息的容器。负责按消息的触发时间对消息入队出队,以及在合适的时间唤醒或休眠消息队列。
  • Looper : 消息分发的执行者。负责从消息队列中拉去消息并交给handler去执行。

为了更好的理解它们的关系,拿现实生活中的场景来举个例子:

Handler是快递员,负责收快递,取快递,查快递以及退回快递。

Message是快递包裹,message的target属性就是收件地址,而延时消息就是收件人预约了派送时间,
希望在指定的时间上门派送。

MessageQueue是菜鸟驿站,要对快递进行整理并摆放在合适的位置。

Looper是一个24小时不休息的资本家,他总是不停的在看菜鸟驿站有没有需要派送的快递,一有快递就立马取
出然后压榨快递员去派送。

最后,我们用一张四者之间的流程图来结束整篇文章:

image.png

收起阅读 »

View的绘制流程 onDraw

performTravel的方法走完onMeasure和onLayout流程后会走到下面这段代码段。 if (mFirst) { if (sAlwaysAssignFocus || !isInTouchMode()) { ...
继续阅读 »


performTravel的方法走完onMeasure和onLayout流程后会走到下面这段代码段。

        if (mFirst) {
if (sAlwaysAssignFocus || !isInTouchMode()) {
if (mView != null) {
if (!mView.hasFocus()) {
mView.restoreDefaultFocus();
} else {
...
}
}
} else {

View focused = mView.findFocus();
if (focused instanceof ViewGroup
&& ((ViewGroup) focused).getDescendantFocusability()
== ViewGroup.FOCUS_AFTER_DESCENDANTS) {
focused.restoreDefaultFocus();
}
}
}

final boolean changedVisibility = (viewVisibilityChanged || mFirst) && isViewVisible;
final boolean hasWindowFocus = mAttachInfo.mHasWindowFocus && isViewVisible;
final boolean regainedFocus = hasWindowFocus && mLostWindowFocus;
if (regainedFocus) {
mLostWindowFocus = false;
} else if (!hasWindowFocus && mHadWindowFocus) {
mLostWindowFocus = true;
}

if (changedVisibility || regainedFocus) {

boolean isToast = (mWindowAttributes == null) ? false
: (mWindowAttributes.type == WindowManager.LayoutParams.TYPE_TOAST);
...
}

mFirst = false;
mWillDrawSoon = false;
mNewSurfaceNeeded = false;
mActivityRelaunched = false;
mViewVisibility = viewVisibility;
mHadWindowFocus = hasWindowFocus;

if (hasWindowFocus && !isInLocalFocusMode()) {
final boolean imTarget = WindowManager.LayoutParams
.mayUseInputMethod(mWindowAttributes.flags);
if (imTarget != mLastWasImTarget) {
mLastWasImTarget = imTarget;
InputMethodManager imm = InputMethodManager.peekInstance();
if (imm != null && imTarget) {
imm.onPreWindowFocus(mView, hasWindowFocus);
imm.onPostWindowFocus(mView, mView.findFocus(),
mWindowAttributes.softInputMode,
!mHasHadWindowFocus, mWindowAttributes.flags);
}
}
}

在进入onDraw的流程之前,会先处理焦点。这个过程中可以分为2大步骤:

  • 1.如果是第一次渲染,则说明之前的宽高都是都为0.在requestFocus方法中会有这个判断把整个焦点集中拦截下来:
    private boolean canTakeFocus() {
return ((mViewFlags & VISIBILITY_MASK) == VISIBLE)
&& ((mViewFlags & FOCUSABLE) == FOCUSABLE)
&& ((mViewFlags & ENABLED_MASK) == ENABLED)
&& (sCanFocusZeroSized || !isLayoutValid() || hasSize());
}

而在每一次onMeasure之前,都会尝试集中一次焦点的遍历。其中requestFocusNoSearch方法中,如果没有测量过就会直接返回false。因为每一次更换焦点或者集中焦点都可能伴随着如背景drawable,statelistDrawable等切换。没有测量过就没有必要做这无用功。

因此此时为了弥补之前拒绝焦点的行为,会重新进行一次restoreDefaultFocus的行为进行requestFocus处理。

  • 2.如果存在窗体焦点,同时不是打开了FLAG_LOCAL_FOCUS_MODE标志(这是一种特殊情况,一般打上这个标志位只有在startingWindow的快照中才会有。

则会调用InputMethodManager的onPostWindowFocus方法启动带了android.view.InputMethod这个action的软键盘服务。

onDraw流程

        if ((relayoutResult & WindowManagerGlobal.RELAYOUT_RES_FIRST_TIME) != 0) {
reportNextDraw();
}

boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;

if (!cancelDraw && !newSurface) {
if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
for (int i = 0; i < mPendingTransitions.size(); ++i) {
mPendingTransitions.get(i).startChangingAnimations();
}
mPendingTransitions.clear();
}

performDraw();
} else {
if (isViewVisible) {
scheduleTraversals();
} else if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
for (int i = 0; i < mPendingTransitions.size(); ++i) {
mPendingTransitions.get(i).endChangingAnimations();
}
mPendingTransitions.clear();
}
}

mIsInTraversal = false;
  • 1.判断到如果是第一次调用draw方法,则会调用reportNextDraw。
    private void reportNextDraw() {
if (mReportNextDraw == false) {
drawPending();
}
mReportNextDraw = true;
}

void drawPending() {
mDrawsNeededToReport++;
}

能看到实际上就是设置mReportNextDraw为true。我们回顾一下前两个流程mReportNextDraw参与了标志位的判断。在执行onMeasure和onLayout有两个大前提,一个是mStop为false,一个是mReportNextDraw为true。只要满足其一就会执行。

这么做的目的只有一个,保证调用一次onDraw方法。为什么会这样呢?performDraw是整个Draw流程的入口。然而在这个入口,必须要保证cancelDraw为false以及newSurface为false。

注意,如果是第一次渲染因为会添加进新的Surface,此时newSurface为true。所以会走到下面的分之,如果串口可见则调用scheduleTraversals执行下一次Loop的绘制流程。否则判断是否有需要执行的LayoutTransitions layout动画就执行了。

因此第一次是不会走到onDraw,是从第二次Looper之后View的绘制流程才会执行onDraw。

ViewRootImpl performDraw

    private void performDraw() {
if (mAttachInfo.mDisplayState == Display.STATE_OFF && !mReportNextDraw) {
return;
} else if (mView == null) {
return;
}

final boolean fullRedrawNeeded = mFullRedrawNeeded || mReportNextDraw;
mFullRedrawNeeded = false;

mIsDrawing = true;
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "draw");

boolean usingAsyncReport = false;
if (mReportNextDraw && mAttachInfo.mThreadedRenderer != null
&& mAttachInfo.mThreadedRenderer.isEnabled()) {
usingAsyncReport = true;
mAttachInfo.mThreadedRenderer.setFrameCompleteCallback((long frameNr) -> {
pendingDrawFinished();
});
}

try {
boolean canUseAsync = draw(fullRedrawNeeded);
if (usingAsyncReport && !canUseAsync) {
mAttachInfo.mThreadedRenderer.setFrameCompleteCallback(null);
usingAsyncReport = false;
}
} finally {
mIsDrawing = false;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}

if (mAttachInfo.mPendingAnimatingRenderNodes != null) {
final int count = mAttachInfo.mPendingAnimatingRenderNodes.size();
for (int i = 0; i < count; i++) {
mAttachInfo.mPendingAnimatingRenderNodes.get(i).endAllAnimators();
}
mAttachInfo.mPendingAnimatingRenderNodes.clear();
}

if (mReportNextDraw) {
mReportNextDraw = false;

if (mWindowDrawCountDown != null) {
try {
mWindowDrawCountDown.await();
} catch (InterruptedException e) {
Log.e(mTag, "Window redraw count down interrupted!");
}
mWindowDrawCountDown = null;
}

if (mAttachInfo.mThreadedRenderer != null) {
mAttachInfo.mThreadedRenderer.setStopped(mStopped);
}

if (mSurfaceHolder != null && mSurface.isValid()) {
SurfaceCallbackHelper sch = new SurfaceCallbackHelper(this::postDrawFinished);
SurfaceHolder.Callback callbacks[] = mSurfaceHolder.getCallbacks();

sch.dispatchSurfaceRedrawNeededAsync(mSurfaceHolder, callbacks);
} else if (!usingAsyncReport) {
if (mAttachInfo.mThreadedRenderer != null) {
mAttachInfo.mThreadedRenderer.fence();
}
pendingDrawFinished();
}
}
}

我们把整个流程抽象出来实际上就是可以分为如下几个步骤:
对于软件渲染:

  • 1.调用draw方法,遍历View的层级。
  • 2.如果Surface是生效的,则在SurfaceHolder.Callback的surfaceRedrawNeededAsync回调中调用pendingDrawFinished。
  • 3.如果是强制同步渲染,则会直接调用pendingDrawFinished。

对于硬件渲染:

  • 1.调用draw方法,遍历View的层级。
  • 2.通过监听mThreadedRenderer的setFrameCompleteCallback回调执行pendingDrawFinished方法。

我们先关注软件渲染的流程。也就是draw和pendingDrawFinished。

ViewRootImpl draw

    private boolean draw(boolean fullRedrawNeeded) {
Surface surface = mSurface;
if (!surface.isValid()) {
return false;
}

if (!sFirstDrawComplete) {
synchronized (sFirstDrawHandlers) {
sFirstDrawComplete = true;
final int count = sFirstDrawHandlers.size();
for (int i = 0; i< count; i++) {
mHandler.post(sFirstDrawHandlers.get(i));
}
}
}

scrollToRectOrFocus(null, false);

if (mAttachInfo.mViewScrollChanged) {
mAttachInfo.mViewScrollChanged = false;
mAttachInfo.mTreeObserver.dispatchOnScrollChanged();
}

boolean animating = mScroller != null && mScroller.computeScrollOffset();
final int curScrollY;
if (animating) {
curScrollY = mScroller.getCurrY();
} else {
curScrollY = mScrollY;
}
if (mCurScrollY != curScrollY) {
mCurScrollY = curScrollY;
fullRedrawNeeded = true;
if (mView instanceof RootViewSurfaceTaker) {
((RootViewSurfaceTaker) mView).onRootViewScrollYChanged(mCurScrollY);
}
}

final float appScale = mAttachInfo.mApplicationScale;
final boolean scalingRequired = mAttachInfo.mScalingRequired;

final Rect dirty = mDirty;
if (mSurfaceHolder != null) {
dirty.setEmpty();
if (animating && mScroller != null) {
mScroller.abortAnimation();
}
return false;
}

if (fullRedrawNeeded) {
mAttachInfo.mIgnoreDirtyState = true;
dirty.set(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
}


mAttachInfo.mTreeObserver.dispatchOnDraw();

int xOffset = -mCanvasOffsetX;
int yOffset = -mCanvasOffsetY + curScrollY;
final WindowManager.LayoutParams params = mWindowAttributes;
final Rect surfaceInsets = params != null ? params.surfaceInsets : null;
if (surfaceInsets != null) {
xOffset -= surfaceInsets.left;
yOffset -= surfaceInsets.top;

dirty.offset(surfaceInsets.left, surfaceInsets.right);
}

...

mAttachInfo.mDrawingTime =
mChoreographer.getFrameTimeNanos() / TimeUtils.NANOS_PER_MS;

boolean useAsyncReport = false;
if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) {
if (mAttachInfo.mThreadedRenderer != null && mAttachInfo.mThreadedRenderer.isEnabled()) {
boolean invalidateRoot = accessibilityFocusDirty || mInvalidateRootRequested;
mInvalidateRootRequested = false;

mIsAnimating = false;

if (mHardwareYOffset != yOffset || mHardwareXOffset != xOffset) {
mHardwareYOffset = yOffset;
mHardwareXOffset = xOffset;
invalidateRoot = true;
}

if (invalidateRoot) {
mAttachInfo.mThreadedRenderer.invalidateRoot();
}

dirty.setEmpty();

final boolean updated = updateContentDrawBounds();

if (mReportNextDraw) {
mAttachInfo.mThreadedRenderer.setStopped(false);
}

if (updated) {
requestDrawWindow();
}

useAsyncReport = true;

final FrameDrawingCallback callback = mNextRtFrameCallback;
mNextRtFrameCallback = null;
mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this, callback);
} else {

if (mAttachInfo.mThreadedRenderer != null &&
!mAttachInfo.mThreadedRenderer.isEnabled() &&
mAttachInfo.mThreadedRenderer.isRequested() &&
mSurface.isValid()) {

try {
mAttachInfo.mThreadedRenderer.initializeIfNeeded(
mWidth, mHeight, mAttachInfo, mSurface, surfaceInsets);
} catch (OutOfResourcesException e) {
handleOutOfResourcesException(e);
return false;
}

mFullRedrawNeeded = true;
scheduleTraversals();
return false;
}

if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
scalingRequired, dirty, surfaceInsets)) {
return false;
}
}
}

if (animating) {
mFullRedrawNeeded = true;
scheduleTraversals();
}
return useAsyncReport;
}

大致上完成了如下流程:

  • 1.如果surface无效则直接返回
    1. sFirstDrawHandlers这个存储着runnable静态对象。实际上是在ActivityThread启动后调用attach方法通过addFirstDrawHandler添加进来的目的只是为了启动jit模式。
  • 3.scrollToRectOrFocus 处理滑动区域或者焦点区域。如果发生了滑动则回调TreeObserver.dispatchOnScrollChanged。接下来则通过全局的mScroller通过computeScrollOffset判断是否需要滑动动画。如果需要执行动画,则调用DeocView的onRootViewScrollYChanged,进行Y轴上的动画执行。
  • 4.通过ViewTreeObserver的dispatchOnDraw开始分发draw开始绘制的监听者。
  • 5.判断是否存在surface面上偏移量,有就矫正一次脏区,把偏移量添加上去。

接下来则会进入到硬件渲染和软件渲染的分支。但是进一步进行调用draw的流程有几个前提条件:脏区不为空,需要执行动画,辅助服务发生了焦点变化

  • 6.如果ThreadedRenderer不为空且可用。ThreadedRenderer通过onPreDraw回调到ViewRootImpl,更新mHardwareYOffset,mHardwareXOffset。如果这两个参数发生了变化,则说明整个发生了硬件绘制的区域变化,需要从头遍历一次所有的区域设置为无效区域,mThreadedRenderer.invalidateRoot。

最后调用ThreadedRenderer.draw方法执行硬件渲染绘制。并且设置通过registerRtFrameCallback设置进来的callback设置到ThreadedRenderer中。

  • 7.如果此时ThreadedRenderer不可用但是不为空,说明此时需要对ThreadedRenderer进行初始化,调用scheduleTraversals在下一轮的绘制流程中才进行硬件渲染。
  • 8.如果以上情况都不满足,说明是软件渲染,则调用drawSoftware进行软件渲染。
  • 9.如果不许要draw方法遍历全局的View树,则判断是否需要执行滑动动画,需要则调用scheduleTraversals进入下一轮的绘制。

本文先抛开硬件渲染,来看看软件渲染drawSoftware中做了什么。还有scrollToRectOrFocus滑动中做了什么?

ViewRootImpl scrollToRectOrFocus

    boolean scrollToRectOrFocus(Rect rectangle, boolean immediate) {
final Rect ci = mAttachInfo.mContentInsets;
final Rect vi = mAttachInfo.mVisibleInsets;
int scrollY = 0;
boolean handled = false;

if (vi.left > ci.left || vi.top > ci.top
|| vi.right > ci.right || vi.bottom > ci.bottom) {

final View focus = mView.findFocus();
if (focus == null) {
return false;
}
View lastScrolledFocus = (mLastScrolledFocus != null) ? mLastScrolledFocus.get() : null;
if (focus != lastScrolledFocus) {

rectangle = null;
}

if (focus == lastScrolledFocus && !mScrollMayChange && rectangle == null) {

} else {
mLastScrolledFocus = new WeakReference<View>(focus);
mScrollMayChange = false;
if (focus.getGlobalVisibleRect(mVisRect, null)) {
if (rectangle == null) {
focus.getFocusedRect(mTempRect);
if (mView instanceof ViewGroup) {
((ViewGroup) mView).offsetDescendantRectToMyCoords(
focus, mTempRect);
}
} else {
mTempRect.set(rectangle);
}
if (mTempRect.intersect(mVisRect)) {
if (mTempRect.height() >
(mView.getHeight()-vi.top-vi.bottom)) {
}
else if (mTempRect.top < vi.top) {
scrollY = mTempRect.top - vi.top;
} else if (mTempRect.bottom > (mView.getHeight()-vi.bottom)) {
scrollY = mTempRect.bottom - (mView.getHeight()-vi.bottom);
} else {
scrollY = 0;
}
handled = true;
}
}
}
}

if (scrollY != mScrollY) {
if (!immediate) {
if (mScroller == null) {
mScroller = new Scroller(mView.getContext());
}
mScroller.startScroll(0, mScrollY, 0, scrollY-mScrollY);
} else if (mScroller != null) {
mScroller.abortAnimation();
}
mScrollY = scrollY;
}

return handled;
}

能看到在这个过程中实际上就是处理两个区域mVisibleInsets可见区域以及mContentInsets内容区域。

实际上这个过程就是从根部节点开始寻找焦点,然后整个画面定格在焦点处。因为mVisibleInsets一般是屏幕中出去过扫描区的大小,但是内容区域就不一定了,可能内容会超出屏幕大小,因此会通过mScroller滑动定位。

计算原理如下,分为2个情况:

  • 1.可视区域的顶部比起获得了焦点的view的顶部要低,说明这个view在屏幕外了,需要向下滑动:

scrollY = mTempRect.top - vi.top;

  • 2.如果焦点view的底部比起可视区域要比可视区域的低,说明需要向上滑动,注意滑动之后需要展示view,因此滑动的距离要减去view的高度:

scrollY = mTempRect.bottom - (mView.getHeight()-vi.bottom);
稍微变一下如下:
scrollY = mTempRect.bottom +vi.bottom - mView.getHeight();


收起阅读 »

如何用Rust做AndroidUI渲染

大力智能客户端团队 西豢沝尔 背景 Rust优秀的安全性、媲美C++的性能以及对跨平台编译和外部语言(ffi)的支持使得其成为高性能跨平台库的上佳实现语言。然而,Rust是否可以在逻辑层之上进一步服务于一些通用性的UI渲染?我们大力智能客户端团队针对开...
继续阅读 »

大力智能客户端团队 西豢沝尔



背景


Rust优秀的安全性、媲美C++的性能以及对跨平台编译和外部语言(ffi)的支持使得其成为高性能跨平台库的上佳实现语言。然而,Rust是否可以在逻辑层之上进一步服务于一些通用性的UI渲染?我们大力智能客户端团队针对开源项目rust-windowing( github.com/rust-window… )中几个核心工程进行剖析,并结合在Android系统的接入对此进行探索。


未命名.gif


Rust UI渲染:


Android系统上使用Rust渲染核心围绕ANativeWindow类展开,ANativeWindow位于android ndk中,是egl跨平台EGLNativeWindowType窗口类型在Android架构下的特定实现,因而基于ANativeWindow就可以创建一个EglSurface并通过GLES进行绘制和渲染。另一方面,ANativeWindow可以简单地与Java层的Surface相对应,因而将Android层需要绘制的目标转换为ANativeWindow是使用Rust渲染的关键,这一部分可以通过JNI完成。首先,我们先看一下rust-windowing对UI渲染的支持。


1 软件绘制:


在rust-windowing项目中,android-ndk-rs提供了rust与android ndk之间的胶水层,其中与UI渲染最相关的就是NativeWindow类,NativeWindow在Rust上下文实现了对ANativeWindow的封装,支持通过ffi对ANativeWindow进行操作,达到与在java层使用lockCanvas()和unlockCanvasAndPost()进行绘制相同的效果,基于这些api,我们可以实现在(A)NativeWindow上的指定区域绘制一个长方形:


unsafe fn draw_rect_on_window(nativewindow: &NativeWindow, colors: Vec<u8>, rect: ndk_glue::Rect) {
let height = nativewindow.height();
let width = nativewindow.width();
let color_format = get_color_format();
let format = color_format.0;
let bbp = color_format.1;
nativewindow.set_buffers_geometry(width, height, format);
nativewindow.acquire();
let mut buffer = NativeWindow::generate_epmty_buffer(width, height, width, format);
let locked = nativewindow.lock(&mut buffer, &mut NativeWindow::generate_empty_rect(0, 0, width, height));
if locked < 0 {
nativewindow.release();
return;
}

draw_rect_into_buffer(buffer.bits, colors, rect, width, height);
let result = nativewindow.unlock_and_post();
nativewindow.release();
}

unsafe fn draw_rect_into_buffer(bits: *mut ::std::os::raw::c_void, colors: Vec<u8>, rect: ndk_glue::Rect, window_width: i32, window_height: i32) {
let bbp = colors.len() as u32;
let window_width = window_width as u32;
for i in rect.top+1..=rect.bottom {
for j in rect.left+1..=rect.right {
let cur = (j + (i-1) * window_width - 1) * bbp;
for k in 0..bbp {
*(bits.offset((cur + (k as u32)) as isize) as *mut u8) = colors[k as usize];
}
}
}
}

这样就通过提交一个纯色像素填充的Buffer在指定的位置成功渲染出了一个长方形,不过这种方式本质上是软件绘制,性能欠佳,更好的方式是通过在Rust层封装GL在ANativeWindow上使能硬件绘制。


2 硬件绘制:


2.1 跨平台窗口系统:winit


2.1.1 Window:窗口

窗口系统最主要的目的是提供平台无关的Window抽象,提供一系列通用的基础方法、属性方法、游标相关方法、监控方法。winit以Window类抽象窗口类型并持有平台相关的window实现,通过WindowId唯一识别一个Window用于匹配后续产生的所有窗口事件WindowEvent,最后通过建造者模式对外暴露实例化的能力,支持在Rust侧设置一些平台无关的参数(大小、位置、标题、是否可见等)以及平台相关的特定参数,基本结构如下:


// src/window.rs
pub struct Window {
pub(crate) window: platform_impl::Window,
}

impl Window {
#[inline]
pub fn request_redraw(&self) {
self.window.request_redraw()
}

pub fn inner_position(&self) -> Result<PhysicalPosition<i32>, NotSupportedError> {
self.window.inner_position()
}

pub fn current_monitor(&self) -> Option<MonitorHandle> {
self.window.current_monitor()
}
}

pub struct WindowId(pub(crate) platform_impl::WindowId);

pub struct WindowBuilder {
/// The attributes to use to create the window.
pub window: WindowAttributes,

// Platform-specific configuration.
pub(crate) platform_specific: platform_impl::PlatformSpecificWindowBuilderAttributes,
}

impl WindowBuilder {
#[inline]
pub fn build<T: 'static>(
self,
window_target: &EventLoopWindowTarget<T>,
) -> Result<Window, OsError> {
platform_impl::Window::new(&window_target.p, self.window, self.platform_specific).map(
|window| {
window.request_redraw();
Window { window }
},
)
}
}

在Android平台,winit暂时不支持使用给定的属性构建一个“Window”,大部分方法给出了空实现或者直接panic,仅保留了一些事件循环相关的能力,真正的窗口实现仍然从android-ndk-rs胶水层获得:当前的android-ndk-rs仅针对ANativeActivity进行了适配,通过属性宏代理了unsafe extern "C" fn ANativeActivity_onCreate(...)方法,在获得ANativeActivity指针*activity后,注入自定义的生命周期回调,在onNativeWindowCreated回调中获得ANativeWindow(封装为NativeWindow)作为当前上下文活跃的窗口。当然,android-ndk-rs的能力也支持我们在任意一个ANativeWindow上生成对应的上层窗口。


2.1.2 EventLoop:事件循环 - 上层

事件循环是整个窗口系统行为的驱动,统一响应抛出的系统任务和用户交互并将反馈渲染到窗口上形成闭环,当你需要合理地触发渲染时,最好的方式就是将指令发送给事件循环。winit中,将事件循环封装为EventLoop,使用ControlFlow控制EventLoop如何获取、消费循环中的事件,并对外提供一个EventLoopProxy代理用于作为与用户交互的媒介,支持用户通过proxy向EventLoop发送用户自定义的事件:



Android平台的事件循环建立在ALooper之上,通过android-ndk-rs提供的胶水层注入的回调处理生命周期行为和窗口行为,通过代理InputQueue处理用户手势,同时支持响应用户自定义事件和内部事件。一次典型的循环根据当前first_event的类型分发处理,一次处理一个主要事件;当first_event处理完成后,触发一次MainEventsCleared事件回调给业务方,并判断是否需要触发Resized和RedrawRequested,最后触发RedrawEventsCleared事件标识所有事件处理完毕。


单次循环处理完所有事件后进入控制流,决定下一次处理事件的行为,控制流支持Android epoll多路复用,在必要时唤醒循环处理后续事件,此外,控制流提供了强制执行、强制退出的能力。事实上,android-ndk-rs就是通过添加fd的方式将窗口行为抛到EventLoop中包装成Callback事件处理:



  • 首先,新建一对fdPIPE: [RawFd; 2],并把读端加到ALooper中,指定标识符为NDK_GLUE_LOOPER_EVENT_PIPE_IDENT;

  • 然后,在适当的时机调用向fd写端写入事件;

  • 最后,fd写入后触发ALooper在poll时被唤醒,且得到被唤醒fd的ident为NDK_GLUE_LOOPER_EVENT_PIPE_IDENT,便可以从fd读端读出此前wake()写入的事件并进行相应的处理;


// <--1--> 挂载fd
// ndk-glue/src/lib.rs
lazy_static! {
static ref PIPE: [RawFd; 2] = {
let mut pipe: [RawFd; 2] = Default::default();
unsafe { libc::pipe(pipe.as_mut_ptr()) };
pipe
};
}

{
...
thread::spawn(move || {
let looper = ThreadLooper::prepare();
let foreign = looper.into_foreign();
foreign
.add_fd(
PIPE[0],
NDK_GLUE_LOOPER_EVENT_PIPE_IDENT,
FdEvent::INPUT,
std::ptr::null_mut(),
)
.unwrap();
});
...
}

// <--2--> 向fd写入数据
// ndk-glue/src/lib.rs
unsafe fn wake(_activity: *mut ANativeActivity, event: Event) {
log::trace!("{:?}", event);
let size = std::mem::size_of::<Event>();
let res = libc::write(PIPE[1], &event as *const _ as *const _, size);
assert_eq!(res, size as _);
}

// <--3--> 唤醒事件循环读出事件
// src/platform_impl/android/mod.rs
fn poll(poll: Poll) -> Option<EventSource> {
match poll {
Poll::Event { ident, .. } => match ident {
ndk_glue::NDK_GLUE_LOOPER_EVENT_PIPE_IDENT => Some(EventSource::Callback),
...
},
...
}
}

// ndk-glue/src/lib.rs
pub fn poll_events() -> Option<Event> {
unsafe {
let size = std::mem::size_of::<Event>();
let mut event = Event::Start;
if libc::read(PIPE[0], &mut event as *mut _ as *mut _, size) == size as _ {
Some(event)
} else {
None
}
}
}

2.2 跨平台egl上下文:glutin


我们有了跨平台的OpenGL(ES)用于描述图形对象,也有了跨平台的窗口系统winit封装窗口行为,但是如何理解图形语言并将其渲染到各个平台的窗口上?这就是egl发挥的作用,它实现了OpenGL(ES)和底层窗口系统之间的接口层。在rust-windowing项目中,glutin工程承接了这个职责,以上下文的形式把窗口系统winit和gl关联了起来。



Context是gl的上下文环境,全局可以有多个gl上下文,但是一个线程同时只能有一个活跃的上下文,使用ContextCurrentState区分这一状态。glutin中Context可关联零个或多个Window,当Context与Window相关联时,使用ContextWrapper类,ContextWrapper使得可以方便地在上下文中同时操作gl绘制以及Window渲染。在其上衍生出两个类型:(1)RawContext:Context与Window虽然关联但是分开存储;(2)WindowedContext:同时存放相互关联的一组Context和Window。常见的场景下WindowedContext更加适用,通过ContextBuilder指定所需的gl属性和像素格式就可以构造一个WindowedContext,内部会初始化egl上下文,并基于持有的EglSurfaceType类型的window创建一个eglsurface作为后续gl指令绘制(draw)、回读(read)的作用目标(指定使用该surface上的缓冲)。


2.3 硬件绘制的例子:


基于winit和glutin提供的能力,使用Rust进行渲染的准备工作只需基于特定业务需求去创建一个glutin的Context,通过Context中创建的egl上下文可以调用gl api进行绘制,而window让我们可以掌控渲染流程,在需要的时候(比如基于EventLoop重绘指令或者一个简单的无限循环)下发绘制指令。简单地实现文章开头的三角形demo动画效果如下:


fn render(&mut self, gl: &Gl) {
let time_elapsed = self.startTime.elapsed().as_millis();
let percent = (time_elapsed % 5000) as f32 / 5000f32;
let angle = percent * 2f32 * std::f32::consts::PI;

unsafe {
let vs = gl.CreateShader(gl::VERTEX_SHADER);
gl.ShaderSource(vs, 1, [VS_SRC.as_ptr() as *const _].as_ptr(), std::ptr::null());
gl.CompileShader(vs);
let fs = gl.CreateShader(gl::FRAGMENT_SHADER);
gl.ShaderSource(fs, 1, [FS_SRC.as_ptr() as *const _].as_ptr(), std::ptr::null());
gl.CompileShader(fs);
let program = gl.CreateProgram();
gl.AttachShader(program, vs);
gl.AttachShader(program, fs);
gl.LinkProgram(program);
gl.UseProgram(program);
gl.DeleteShader(vs);
gl.DeleteShader(fs);
let mut vb = std::mem::zeroed();
gl.GenBuffers(1, &mut vb);
gl.BindBuffer(gl::ARRAY_BUFFER, vb);
let vertex = [
SIDE_LEN * (BASE_V_LEFT+angle).cos(), SIDE_LEN * (BASE_V_LEFT+angle).sin(), 0.0, 0.4, 0.0,
SIDE_LEN * (BASE_V_TOP+angle).cos(), SIDE_LEN * (BASE_V_TOP+angle).sin(), 0.0, 0.4, 0.0,
SIDE_LEN * (BASE_V_RIGHT+angle).cos(), SIDE_LEN * (BASE_V_RIGHT+angle).sin(), 0.0, 0.4, 0.0,
];

gl.BufferData(
gl::ARRAY_BUFFER,
(vertex.len() * std::mem::size_of::<f32>()) as gl::types::GLsizeiptr,
vertex.as_ptr() as *const _,
gl::STATIC_DRAW,
);

if gl.BindVertexArray.is_loaded() {
let mut vao = std::mem::zeroed();
gl.GenVertexArrays(1, &mut vao);
gl.BindVertexArray(vao);
}

let pos_attrib = gl.GetAttribLocation(program, b"position\0".as_ptr() as *const _);
let color_attrib = gl.GetAttribLocation(program, b"color\0".as_ptr() as *const _);
gl.VertexAttribPointer(
pos_attrib as gl::types::GLuint,
2,
gl::FLOAT,
0,
5 * std::mem::size_of::<f32>() as gl::types::GLsizei,
std::ptr::null(),
);

gl.VertexAttribPointer(
color_attrib as gl::types::GLuint,
3,
gl::FLOAT,
0,
5 * std::mem::size_of::<f32>() as gl::types::GLsizei,
(2 * std::mem::size_of::<f32>()) as *const () as *const _,
);

gl.EnableVertexAttribArray(pos_attrib as gl::types::GLuint);
gl.EnableVertexAttribArray(color_attrib as gl::types::GLuint);
gl.ClearColor(1.3 * (percent-0.5).abs(), 0., 1.3 * (0.5 - percent).abs(), 1.0);
gl.Clear(gl::COLOR_BUFFER_BIT);
gl.DrawArrays(gl::TRIANGLES, 0, 3);
}
}

3 Android - Rust JNI开发


以上Rust UI渲染部分完全运行在Rust上下文中(包括对c++的封装),而实际渲染场景下很难完全脱离Android层进行UI的渲染或不与Activity等容器进行交互。所幸Rust UI渲染主要基于(A)NativeWindow,而Android Surface在c++的对应类实现了ANativeWindow,ndk也提供了ANativeWindow_fromSurface方法从一个surface获得ANativeWindow对象,因而我们可以通过JNI的方式使用Rust在Android层的Surface上进行UI渲染:


// Android

surface_view.holder.addCallback(object : SurfaceHolder.Callback2 {

override fun surfaceCreated(p0: SurfaceHolder) {
RustUtils.drawColorTriangle(surface, Color.RED)
}

override fun surfaceChanged(p0: SurfaceHolder, p1: Int, p2: Int, p3: Int) {}

override fun surfaceDestroyed(p0: SurfaceHolder) {}

override fun surfaceRedrawNeeded(p0: SurfaceHolder) {}

})

// Rust
pub unsafe extern fn Java_com_example_rust_1demo_RustUtils_drawColorTriangle__Landroid_view_Surface_2I(env: *mut JNIEnv, _: JClass, surface: jobject, color: jint) -> jboolean {
println!("call Java_com_example_rust_1demo_RustUtils_drawColor__Landroid_view_Surface_2I");
ndk_glue::set_native_window(NativeWindow::from_surface(env, surface));
runner::start();
0
}

需要注意,由于EventLoop是基于ALooper的封装,调用Rust实现渲染时需要确保调用在有Looper的线程(比如HandlerThread中),或者在Rust渲染前初始化时为当前线程准备ALooper。


总结



使用Rust在Android上进行UI渲染的可行性已经得证,但是它的性能表现究竟如何?未来又将在哪些业务上落地?这些仍待进一步探索。

收起阅读 »

带倒计时RecyclerView的设计心路历程

需求 目前有这样一个需求: 1 需要一个页面,展示多个条目 2 每个条目有独立的倒计时,倒计时结束后就删除此条目 3 每个条目上有删除按钮,点击可以删除该条目 4 列表上的条目类型是多样的 可行性分析 首先肯定是可以做的: ...
继续阅读 »

需求



目前有这样一个需求:



  • 1 需要一个页面,展示多个条目

  • 2 每个条目有独立的倒计时,倒计时结束后就删除此条目

  • 3 每个条目上有删除按钮,点击可以删除该条目

  • 4 列表上的条目类型是多样的


可行性分析


首先肯定是可以做的:



  • 1 用一个RecyclerView来实现

  • 2 每个item里面添加一个倒计时控件,注意倒计时是在item对应的数据里面,不是UI里面

  • 3 添加删除按钮,点击就删除对应的数据,并且停止数据对应的倒计时,同时更新适配器

  • 4 使用getViewType()来实现多个item类型


三流程序员看到这里已经去写代码了...


二流以上程序员接着往下看。


需求分析


首先,第1条没问题。


第2条,需要在item对应的数据里面添加一个倒计时组件,这听着就不对,倒计时组件明明是用来更新UI的,应该是UI持有,现在让数据持有,那不就等价于数据间接持有了UI吗,长生命周期持有短生命周期了,不行。而且,数据多的时候,比如10w条数据,就有10w个倒计时组件,cpu不吃不喝也忙不过来(cpu:wqnmlgb)!这明显属于量变引起质变的问题,为了避免这个问题,我们需要将倒计时组件常量化,也就是: 只有常数个倒计时,从而让倒计时组件的个数,不受数据数量的影响。


那么,我们怎么定义这个常量呢?


我们考虑到倒计时是用来更新UI的,那么屏幕内可见的item有多少个,就创建多少个 倒计时组件 不就行了吗,反正屏幕外的,别人也看不见,所以我们可以让ViewHolder持有倒计时组件,而且正好可以利用RecyclerView对ViewHolder的复用机制。


但是,如果让ViewHolder持有,当ViewHolder滑出屏幕外,就会回收,那么倒计时就终止了,此时就无法触发倒计时结束的删除操作,因为即使在屏幕外,只要触发了倒计时的删除数据,我们屏幕内的数据就会向上滑动一个位置,是可以感知的,所以,如果滑出屏幕后,倒计时终止了,就无法触发删除,那么我们可能等了很久,也没发现屏幕内的数据向上滑动,明显是不对的。


程序是为用户服务的,根据以上分析,我们只站在用户角度来考虑:



  • case1 如果倒计时放在数据内,用户可以感知到删除操作,因为有滑动,但是数据多了明显会感觉到卡顿,因为有很多倒计时

  • case2 如果倒计时放在ViewHolder内,用户无法感知到删除操作,因为滑出屏幕倒计时就终止了,但是数据多了不会感觉到卡顿


此乃死锁,无法解决!那么就需要退一步来改下需求了。既然无法完美解决用户的问题,我们就来改变用户的习惯,我们让:倒计时结束后,不再删除item,只是置灰


为什么这么改呢?因为针对case1,我们没法解决,只能从case2入手,而case2的问题就是: 用户无法感知到删除操作,那我就不删除了,这样你也不用感知了,只置灰即可。


好,第二条解决。


第3条,没啥问题,直接remove(index),然后调用adapter.notifyItemRemoved()完事。


第4条,也没啥问题,可以if-else/switch-case,根据不同的type返回不同的ViewHolder。但是可以写的更好点,就是使用工厂模式


设计


可行性分析和需求分析完了后,我们就开始进行概要设计了



  • 1 我们需要创建个RecyclerView。

  • 2 我们需要在ViewHolder里面添加一个倒计时组件,这里我们使用Handler就足够,并且我们需要在进入屏幕时,开启倒计时,在滑出屏幕后,停止倒计时来省cpu。

  • 3 删除数据就不废话了,这都不会的话,回炉重造吧。

  • 4 使用工厂模式,来根据不同的ViewType创建不同的ViewHolder。


这里面有几点需要注意:



  • 1 ViewHolder进入屏幕会触发onBindViewHolder(),滑出屏幕会触发onViewRecycled()。

  • 2 工厂模式要使用多工厂,这样可以降低耦合,有新的ViewType时,只添加就行,可以做到OCP原则。

  • 3 我们可以提前加载工厂,使用map缓存,就跟工厂模式的实现思想里面最后的源码类似,Android源码也是提前加载工厂的。


好,分析结束,开始撸码。


编码


首先,我们先定义数据实体:


// 注意这里的terminalTime,指的指终止时间,不是时间差,是一个时间值。
// type可以理解为ViewType,当然中间有对应关系的
data class BaseItemBean(val id: Long, var terminalTime: Long, val type: Int)

很简单的一行代码,是个Bean对象,直接上data class即可。


然后,我们来定义两个ViewHolder,因为有相同布局,我们可以直接用继承关系:


// 基础ViewHolder
open inner class BaseVH(itemView: View) : RecyclerView.ViewHolder(itemView) {

// 展示倒计时
private val tvTimer = itemView.findViewById<TextView>(R.id.tv_time)
// 删除按钮
private val btnDelete = itemView.findViewById<TextView>(R.id.btn_delete)

init {
btnDelete.setOnClickListener {
onItemDeleteClick?.invoke(adapterPosition)
}
}

/**
* 剩余倒计时
*/
private var delay = 0L

private val timerRunnable = Runnable {
// 这里打印日志,来印证我们只跑了 "屏幕内可展示item数量的 倒计时"
Log.d(TAG, "run: ${hashCode()}")
delay -= 1000
updateTimerState()
}

// 开始倒计时
private fun startTimer() {
timerHandler.postDelayed(timerRunnable, 1000)
}

// 结束倒计时
private fun endTimer() {
timerHandler.removeCallbacks(timerRunnable)
}

// 检测倒计时 并 更新状态
private fun updateTimerState() {
if (delay <= 0) {
// 倒计时结束了
tvTimer.text = "已结束"
itemView.setBackgroundColor(Color.GRAY)
endTimer()
} else {
// 继续倒计时
tvTimer.text = "${delay / 1000}S"
itemView.setBackgroundColor(Color.parseColor("#FFBB86FC"))
startTimer()
}
}

/**
* 进入屏幕时: 填充数据,这里声明为open,让子类重写
*/
open fun display(bean: BaseItemBean) {
Log.d(TAG, "display: $adapterPosition")

// 使用 终止时间 - 当前时间,计算倒计时还有多少秒
delay = bean.terminalTime - System.currentTimeMillis()

// 检测并更新timer状态
updateTimerState()
}

/**
* 滑出屏幕时: 移除倒计时
*/
fun onRecycled() {
Log.d(TAG, "onRecycled: $adapterPosition")

// 终止计时
endTimer()
}
}

在基础ViewHolder里,我们添加了倒计时套件,并且在进入屏幕时,计算并开始倒计时,滑出屏幕后,就终止倒计时,下次滑入屏幕,重新计算delay时间差,再倒计时。


然后看另一个ViewHolder:


// 继承自BaseViewHolder,因为有公共的倒计时套件
inner class OnSaleVH(itemView: View) : BaseVH(itemView) {
// 添加了一个名字
private val tvName = itemView.findViewById<TextView>(R.id.tv_name)

override fun display(bean: BaseItemBean) {
super.display(bean)
// 添加名字
tvName.text = "${bean.id} 在售"
}
}

接下来我们来看创建ViewHolder的工厂:


/**
* 定义抽象工厂
*/
abstract class VHFactory {
abstract fun createVH(context: Context, parent: ViewGroup): BaseVH
}

/**
* BaseViewHolder工厂
*/
inner class BaseVHFactory : VHFactory() {
override fun createVH(context: Context, parent: ViewGroup): BaseVH {
return BaseVH(LayoutInflater.from(context).inflate(R.layout.item_base, parent, false))
}
}

/**
* OnSaleVH工厂
*/
inner class OnSaleVHFactory : VHFactory() {
override fun createVH(context: Context, parent: ViewGroup): BaseVH {
return OnSaleVH(LayoutInflater.from(context).inflate(R.layout.item_on_sale, parent, false))
}
}

很简单,接下来,我们来看Adapter:


class Adapter(private val datas: List<BaseItemBean>) : RecyclerView.Adapter<Adapter.BaseVH>() {

private val TAG = "Adapter"

/**
* 点击item的事件
*/
var onItemDeleteClick: ((position: Int) -> Unit)? = null

/**
* ViewHolder的工厂
*/
private val vhs = SparseArray<VHFactory>()

/**
* 用来执行倒计时
*/
private val timerHandler = Handler(Looper.getMainLooper())

/**
* 初始化工厂
*/
init {
vhs.put(ItemType.ITEM_BASE, BaseVHFactory())
vhs.put(ItemType.ITEM_ON_SALE, OnSaleVHFactory())
}

// 直接从工厂map中获取对应的工厂调用createVH()方法即可
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseVH = vhs.get(viewType).createVH(parent.context, parent)

// 滑入屏幕内调用,直接使用hoder.display()展示数据
override fun onBindViewHolder(holder: BaseVH, position: Int) = holder.display(datas[position])

override fun getItemCount(): Int = datas.size

// ViewHolder滑出屏幕调用,进行回收
override fun onViewRecycled(holder: BaseVH) = holder.onRecycled()

/**
* 根据数据类型返回ViewType
*/
override fun getItemViewType(position: Int): Int = datas[position].type
}

代码也很easy,就是使用工厂模式来返回不同的ViewHolder。


写代码的心路历程:



  • 1 因为有多个ViewType,肯定有多个ViewHolder,ViewType和ViewHolder是映射关系

  • 2 可以用if-else,可以用switch-case,但是这样扩展性差

  • 3 所以用多工厂来实现

  • 4 这样需要创建工厂,每次onCreateViewHolder()都要创建吗?不行,那就缓存起来。

  • 5 缓存需要知道哪个工厂创建哪个ViewHolder,而ViewHolder和ViewType对应,所以可以让工厂和ViewType对应,那就创建一个Map。

  • 6 ViewType是Integer类型的,那就可以用更加省内存的SparseArray(),原因可以看这里

  • 7 于是,我们就有了上述代码。


我们定义的ViewType(都是int类型的,因为int的匹配速度快):


object ItemType {
const val ITEM_BASE = 0x001
const val ITEM_ON_SALE = 0x002
}

接下来我们就可以在Activity中使用了:


class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)

binding.recyclerView.layoutManager = LinearLayoutManager(this)

// 添加测试数据
val beans = ArrayList<BaseItemBean>()
for (i in 0..100) {
// 计算终止时间,这里都是当前时间 + i乘以10s
val terminalTime = System.currentTimeMillis() + i * 10_000
// 这里手动计算了ViewType (i%2)+1
beans.add(BaseItemBean(i.toLong(), terminalTime, (i % 2) + 1))
}

val adapter = Adapter(beans)
adapter.onItemDeleteClick = { position ->
// 点击就删除
beans.removeAt(position)
adapter.notifyItemRemoved(position)
}
binding.recyclerView.adapter = adapter

}
}

效果如下:


收起阅读 »

在 Flutter 中探索 StreamBuilder

原文 medium.com/flutterdevs… 正文 异步交互可能需要一个理想的机会来进行总结。偶尔,在周期结束之前可能会发出一些值。在 Dart 中,您可以创建一个返回 Stream 的容量,该容量可以在异步进程处于活动状态时发射一些值...
继续阅读 »


原文



medium.com/flutterdevs…



正文


异步交互可能需要一个理想的机会来进行总结。偶尔,在周期结束之前可能会发出一些值。在 Dart 中,您可以创建一个返回 Stream 的容量,该容量可以在异步进程处于活动状态时发射一些值。假设您需要根据一个 Stream 的快照在 Flutter 中构造一个小部件,那么有一个名为 StreamBuilder 的小部件。


在这个博客中,我们将探索 Flutter 中的 StreamBuilder。我们还将实现一个演示程序,并向您展示如何在您的 Flutter 应用程序中使用 StreamBuilder。


介绍:


StreamBuilder 可以监听公开的流,并返回小部件和捕获获得的流信息的快照。造溪者提出了两个论点。



A stream


构建器,它可以将流中的多个组件更改为小部件



Stream 像一条线。当您从一端输入值而从另一端输入侦听器时,侦听器将获得该值。一个流可以有多个侦听器,这些侦听器的负载可以获得流水线,流水线将获得等价值。如何在流上放置值是通过使用流控制器实现的。流构建器是一个小部件,它可以将用户定义的对象更改为流。


建造者:



要使用 StreamBuilder,需要调用下面的构造函数:



const StreamBuilder({
Key? key,
Stream<T>? stream,
T? initialData,
required AsyncWidgetBuilder<T> builder,
})

实际上,您需要创建一个 Stream 并将其作为流争用传递。然后,在这一点上,您需要传递一个 AsyncWidgetBuilder,该 AsyncWidgetBuilder 可用于构造依赖于 Stream 快照的小部件。


参数:



下面是 StreamBuilderare 的一些参数:




  • Key? key: 小部件的键,用于控制小部件如何被另一个小部件取代

  • Stream<T>? stream: 一个流,其快照可以通过生成器函数获得

  • T? initialData: 将利用这些数据制作初始快照

  • required AsyncWidgetBuilder<T> builder: 生成过程由此生成器使用


如何实现 dart 文件中的代码:


你需要分别在你的代码中实现它:



让我们创建一个流:



下面的函数返回一个每秒生成一个数字的 Stream。你需要使用 async * 关键字来创建一个流。若要发出值,可以使用 yield 关键字后跟要发出的值。


Stream<int> generateNumbers = (() async* {
await Future<void>.delayed(Duration(seconds: 2));

for (int i = 1; i <= 10; i++) {
await Future<void>.delayed(Duration(seconds: 1));
yield i;
}
})();


From that point onward, pass it as the stream argument


从那一点开始,把它作为流参数传递下去



StreamBuilder<int>(
stream: generateNumbers,
// other arguments
)


让我们创建一个 AsyncWidgetBuilder



构造函数期望您传递一个类型为 AsyncWidgetBuilder 的命名争用构建器。这是一个有两个参数的函数,它们的类型都是 BuildContext 和 AsyncSnapshot < t > 。后续的边界(包含当前快照)可以用来确定应该呈现的内容。


要创建这个函数,首先需要了解 AsyncSnapshot。AsyncSnapshot 是使用异步计算的最新通信的不变描述。在这种独特的情况下,它解决了与 Stream 的最新通信。可以通过 AsyncSnapshot 属性获取流的最新快照。您可能需要使用的属性之一是 connectionState,这个枚举将当前关联状态转换为异步计算,在这种特殊情况下,这种异步计算就是 Steam。


StreamBuilder<int>(
stream: generateNumbers,
builder: (
BuildContext context,
AsyncSnapshot<int> snapshot,
) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
} else if (snapshot.connectionState == ConnectionState.active
|| snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
return const Text('Error');
} else if (snapshot.hasData) {
return Text(
snapshot.data.toString(),
style: const TextStyle(color: Colors._red_, fontSize: 40)
);
} else {
return const Text('Empty data');
}
} else {
return Text('State: ${snapshot.connectionState}');
}
},
),

AsyncSnapshot 还有一个名为 hasError 的属性,可用于检查快照是否包含非空错误值。如果异步活动的最新结果失败,hasError 值将有效。为了获取信息,首先,您可以通过获取其 hasData 属性来检查快照是否包含信息,如果 Stream 有效地释放了任何非空值,那么 hasData 属性将是有效的。然后,在这一点上,您可以从 AsyncSnapshot 的数据属性获取信息。


由于上面属性的值,您可以计算出应该在屏幕上呈现什么。在下面的代码中,当 connectionState 值正在等待时,将显示一个 CircularProgressIndicator。当 connectionState 更改为 active 或 done 时,可以检查快照是否有错误或信息。建造函数称为 Flutter 管道的检测。因此,它将获得一个与时间相关的快照子组。这意味着,如果在实际上相似的时间里,Stream 发出了一些值,那么一部分值可能没有传递给构建器。



枚举有一些可能的值:




  • > none: 无: 不与任何异步计算关联。如果流为空,则可能发生

  • > waiting: 等待: 与异步计算关联并等待协作。在这个上下文中,它暗示流还没有完成

  • > active: 活跃的: 与活动的异步计算相关联。例如,如果一个 Stream 已经返回了任何值,但此时还没有结束

  • > done: > 完成: 与结束的异步计算相关联。在这个上下文中,它暗示流已经完成



设置初始数据:



您可以选择传递一个 worth 作为 initialData 参数,这个参数将被利用,直到 Stream 发出 a。如果传递的值不为空,那么当 connectionState 在等待时,hasData 属性在任何事件中首先都将为 true


StreamBuilder<int>(
initialData: 0,
// other arguments
)

要在 connectionState 等待时显示初始数据,应该调整 if snapshot.connectionState = = connectionState.waiting,然后调整上面代码中的块。


if (snapshot.connectionState == ConnectionState.waiting) {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
Visibility(
visible: snapshot.hasData,
child: Text(
snapshot.data.toString(),
style: const TextStyle(color: Colors._black_, fontSize: 24),
),
),
],
);
}

当我们运行应用程序,我们应该得到屏幕的输出像下面的屏幕视频。



Code File:


密码档案:


import 'package:flutter/material.dart';
import 'package:flutter_steambuilder_demo/splash_screen.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {

@override
Widget build(BuildContext context) {
return MaterialApp(
home: Splash(),
debugShowCheckedModeBanner: false,
);
}
}

Stream<int> generateNumbers = (() async* {
await Future<void>.delayed(Duration(seconds: 2));

for (int i = 1; i <= 10; i++) {
await Future<void>.delayed(Duration(seconds: 1));
yield i;
}
})();

class StreamBuilderDemo extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _StreamBuilderDemoState ();
}
}

class _StreamBuilderDemoState extends State<StreamBuilderDemo> {

@override
initState() {
super.initState();
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: const Text('Flutter StreamBuilder Demo'),
),
body: SizedBox(
width: double._infinity_,
child: Center(
child: StreamBuilder<int>(
stream: generateNumbers,
initialData: 0,
builder: (
BuildContext context,
AsyncSnapshot<int> snapshot,
) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
Visibility(
visible: snapshot.hasData,
child: Text(
snapshot.data.toString(),
style: const TextStyle(color: Colors._black_, fontSize: 24),
),
),
],
);
} else if (snapshot.connectionState == ConnectionState.active
|| snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
return const Text('Error');
} else if (snapshot.hasData) {
return Text(
snapshot.data.toString(),
style: const TextStyle(color: Colors._red_, fontSize: 40)
);
} else {
return const Text('Empty data');
}
} else {
return Text('State: ${snapshot.connectionState}');
}
},
),
),
),
);
}
}

结语:


在本文中,我已经简单介绍了 StreamBuilder 的基本结构; 您可以根据自己的选择修改这段代码。这是我对 StreamBuilder On User Interaction 的一个小小介绍,它正在使用 Flutter 工作。

收起阅读 »

iOS 高效绘图 二

异步绘制    UIKit的单线程天性意味着寄宿图通畅要在主线程上更新,这意味着绘制会打断用户交互,甚至让整个app看起来处于无响应状态。我们对此无能为力,但是如果能避免用户等待绘制完成就好多了。  ...
继续阅读 »

异步绘制

    UIKit的单线程天性意味着寄宿图通畅要在主线程上更新,这意味着绘制会打断用户交互,甚至让整个app看起来处于无响应状态。我们对此无能为力,但是如果能避免用户等待绘制完成就好多了。

    针对这个问题,有一些方法可以用到:一些情况下,我们可以推测性地提前在另外一个线程上绘制内容,然后将由此绘出的图片直接设置为图层的内容。这实现起来可能不是很方便,但是在特定情况下是可行的。Core Animation提供了一些选择:CATiledLayerdrawsAsynchronously属性。

CATiledLayer

    我们在第六章简单探索了一下CATiledLayer。除了将图层再次分割成独立更新的小块(类似于脏矩形自动更新的概念),CATiledLayer还有一个有趣的特性:在多个线程中为每个小块同时调用-drawLayer:inContext:方法。这就避免了阻塞用户交互而且能够利用多核心新片来更快地绘制。只有一个小块的CATiledLayer是实现异步更新图片视图的简单方法。

drawsAsynchronously

    iOS 6中,苹果为CALayer引入了这个令人好奇的属性,drawsAsynchronously属性对传入-drawLayer:inContext:的CGContext进行改动,允许CGContext延缓绘制命令的执行以至于不阻塞用户交互。

    它与CATiledLayer使用的异步绘制并不相同。它自己的-drawLayer:inContext:方法只会在主线程调用,但是CGContext并不等待每个绘制命令的结束。相反地,它会将命令加入队列,当方法返回时,在后台线程逐个执行真正的绘制。

    根据苹果的说法。这个特性在需要频繁重绘的视图上效果最好(比如我们的绘图应用,或者诸如UITableViewCell之类的),对那些只绘制一次或很少重绘的图层内容来说没什么太大的帮助。

总结

本章我们主要围绕用Core Graphics软件绘制讨论了一些性能挑战,然后探索了一些改进方法:比如提高绘制性能或者减少需要绘制的数量。

第14章,『图像IO』,我们将讨论图片的载入性能。

收起阅读 »

iOS 高效绘图 一

高效绘图不必要的效率考虑往往是性能问题的万恶之源。 ——William Allan Wulf    在第12章『速度的曲率』我们学习如何用Instruments来诊断Core Animation性能问题。在构建一个iOS...
继续阅读 »

高效绘图

不必要的效率考虑往往是性能问题的万恶之源。 ——William Allan Wulf

    在第12章『速度的曲率』我们学习如何用Instruments来诊断Core Animation性能问题。在构建一个iOS app的时候会遇到很多潜在的性能陷阱,但是在本章我们将着眼于有关绘制的性能问题。

软件绘图

    术语绘图通常在Core Animation的上下文中指代软件绘图(意即:不由GPU协助的绘图)。在iOS中,软件绘图通常是由Core Graphics框架完成来完成。但是,在一些必要的情况下,相比Core Animation和OpenGL,Core Graphics要慢了不少。

    软件绘图不仅效率低,还会消耗可观的内存。CALayer只需要一些与自己相关的内存:只有它的寄宿图会消耗一定的内存空间。即使直接赋给contents属性一张图片,也不需要增加额外的照片存储大小。如果相同的一张图片被多个图层作为contents属性,那么他们将会共用同一块内存,而不是复制内存块。

    但是一旦你实现了CALayerDelegate协议中的-drawLayer:inContext:方法或者UIView中的-drawRect:方法(其实就是前者的包装方法),图层就创建了一个绘制上下文,这个上下文需要的大小的内存可从这个算式得出:图层宽*图层高*4字节,宽高的单位均为像素。对于一个在Retina iPad上的全屏图层来说,这个内存量就是 2048*1526*4字节,相当于12MB内存,图层每次重绘的时候都需要重新抹掉内存然后重新分配。

    软件绘图的代价昂贵,除非绝对必要,你应该避免重绘你的视图。提高绘制性能的秘诀就在于尽量避免去绘制。


矢量图形

    我们用Core Graphics来绘图的一个通常原因就是只是用图片或是图层效果不能轻易地绘制出矢量图形。矢量绘图包含一下这些:

  • 任意多边形(不仅仅是一个矩形)
  • 斜线或曲线
  • 文本
  • 渐变

    举个例子,清单13.1 展示了一个基本的画线应用。这个应用将用户的触摸手势转换成一个UIBezierPath上的点,然后绘制成视图。我们在一个UIView子类DrawingView中实现了所有的绘制逻辑,这个情况下我们没有用上view controller。但是如果你喜欢你可以在view controller中实现触摸事件处理。图13.1是代码运行结果。

清单13.1 用Core Graphics实现一个简单的绘图应用

#import "DrawingView.h"

@interface DrawingView ()

@property (nonatomic, strong) UIBezierPath *path;

@end

@implementation DrawingView

- (void)awakeFromNib
{
//create a mutable path
self.path = [[UIBezierPath alloc] init];
self.path.lineJoinStyle = kCGLineJoinRound;
self.path.lineCapStyle = kCGLineCapRound;

self.path.lineWidth = 5;
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
//get the starting point
CGPoint point = [[touches anyObject] locationInView:self];

//move the path drawing cursor to the starting point
[self.path moveToPoint:point];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
//get the current point
CGPoint point = [[touches anyObject] locationInView:self];

//add a new line segment to our path
[self.path addLineToPoint:point];

//redraw the view
[self setNeedsDisplay];
}

- (void)drawRect:(CGRect)rect
{
//draw path
[[UIColor clearColor] setFill];
[[UIColor redColor] setStroke];
[self.path stroke];
}
@end

图13.1

图13.1 用Core Graphics做一个简单的『素描』

    这样实现的问题在于,我们画得越多,程序就会越慢。因为每次移动手指的时候都会重绘整个贝塞尔路径(UIBezierPath),随着路径越来越复杂,每次重绘的工作就会增加,直接导致了帧数的下降。看来我们需要一个更好的方法了。

    Core Animation为这些图形类型的绘制提供了专门的类,并给他们提供硬件支持(第六章『专有图层』有详细提到)。CAShapeLayer可以绘制多边形,直线和曲线。CATextLayer可以绘制文本。CAGradientLayer用来绘制渐变。这些总体上都比Core Graphics更快,同时他们也避免了创造一个寄宿图。

    如果稍微将之前的代码变动一下,用CAShapeLayer替代Core Graphics,性能就会得到提高(见清单13.2).虽然随着路径复杂性的增加,绘制性能依然会下降,但是只有当非常非常浮躁的绘制时才会感到明显的帧率差异。

清单13.2 用CAShapeLayer重新实现绘图应用

#import "DrawingView.h"
#import

@interface DrawingView ()

@property (nonatomic, strong) UIBezierPath *path;

@end

@implementation DrawingView

+ (Class)layerClass
{
//this makes our view create a CAShapeLayer
//instead of a CALayer for its backing layer
return [CAShapeLayer class];
}

- (void)awakeFromNib
{
//create a mutable path
self.path = [[UIBezierPath alloc] init];

//configure the layer
CAShapeLayer *shapeLayer = (CAShapeLayer *)self.layer;
shapeLayer.strokeColor = [UIColor redColor].CGColor;
shapeLayer.fillColor = [UIColor clearColor].CGColor;
shapeLayer.lineJoin = kCALineJoinRound;
shapeLayer.lineCap = kCALineCapRound;
shapeLayer.lineWidth = 5;
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
//get the starting point
CGPoint point = [[touches anyObject] locationInView:self];

//move the path drawing cursor to the starting point
[self.path moveToPoint:point];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
//get the current point
CGPoint point = [[touches anyObject] locationInView:self];

//add a new line segment to our path
[self.path addLineToPoint:point];

//update the layer with a copy of the path
((CAShapeLayer *)self.layer).path = self.path.CGPath;
}
@end

脏矩形

    有时候用CAShapeLayer或者其他矢量图形图层替代Core Graphics并不是那么切实可行。比如我们的绘图应用:我们用线条完美地完成了矢量绘制。但是设想一下如果我们能进一步提高应用的性能,让它就像一个黑板一样工作,然后用『粉笔』来绘制线条。模拟粉笔最简单的方法就是用一个『线刷』图片然后将它粘贴到用户手指碰触的地方,但是这个方法用CAShapeLayer没办法实现。

    我们可以给每个『线刷』创建一个独立的图层,但是实现起来有很大的问题。屏幕上允许同时出现图层上线数量大约是几百,那样我们很快就会超出的。这种情况下我们没什么办法,就用Core Graphics吧(除非你想用OpenGL做一些更复杂的事情)。

    我们的『黑板』应用的最初实现见清单13.3,我们更改了之前版本的DrawingView,用一个画刷位置的数组代替UIBezierPath。图13.2是运行结果

清单13.3 简单的类似黑板的应用

#import "DrawingView.h"
#import
#define BRUSH_SIZE 32

@interface DrawingView ()

@property (nonatomic, strong) NSMutableArray *strokes;

@end

@implementation DrawingView

- (void)awakeFromNib
{
//create array
self.strokes = [NSMutableArray array];
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
//get the starting point
CGPoint point = [[touches anyObject] locationInView:self];

//add brush stroke
[self addBrushStrokeAtPoint:point];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
//get the touch point
CGPoint point = [[touches anyObject] locationInView:self];

//add brush stroke
[self addBrushStrokeAtPoint:point];
}

- (void)addBrushStrokeAtPoint:(CGPoint)point
{
//add brush stroke to array
[self.strokes addObject:[NSValue valueWithCGPoint:point]];

//needs redraw
[self setNeedsDisplay];
}

- (void)drawRect:(CGRect)rect
{
//redraw strokes
for (NSValue *value in self.strokes) {
//get point
CGPoint point = [value CGPointValue];

//get brush rect
CGRect brushRect = CGRectMake(point.x - BRUSH_SIZE/2, point.y - BRUSH_SIZE/2, BRUSH_SIZE, BRUSH_SIZE);

//draw brush stroke 
[[UIImage imageNamed:@"Chalk.png"] drawInRect:brushRect];
}
}
@end

图13.2

图13.2 用程序绘制一个简单的『素描』

    这个实现在模拟器上表现还不错,但是在真实设备上就没那么好了。问题在于每次手指移动的时候我们就会重绘之前的线刷,即使场景的大部分并没有改变。我们绘制地越多,就会越慢。随着时间的增加每次重绘需要更多的时间,帧数也会下降(见图13.3),如何提高性能呢?

图13.3

图13.3 帧率和线条质量会随时间下降。

    为了减少不必要的绘制,Mac OS和iOS设备将会把屏幕区分为需要重绘的区域和不需要重绘的区域。那些需要重绘的部分被称作『脏区域』。在实际应用中,鉴于非矩形区域边界裁剪和混合的复杂性,通常会区分出包含指定视图的矩形位置,而这个位置就是『脏矩形』。

    当一个视图被改动过了,TA可能需要重绘。但是很多情况下,只是这个视图的一部分被改变了,所以重绘整个寄宿图就太浪费了。但是Core Animation通常并不了解你的自定义绘图代码,它也不能自己计算出脏区域的位置。然而,你的确可以提供这些信息。

    当你检测到指定视图或图层的指定部分需要被重绘,你直接调用-setNeedsDisplayInRect:来标记它,然后将影响到的矩形作为参数传入。这样就会在一次视图刷新时调用视图的-drawRect:(或图层代理的-drawLayer:inContext:方法)。

    传入-drawLayer:inContext:CGContext参数会自动被裁切以适应对应的矩形。为了确定矩形的尺寸大小,你可以用CGContextGetClipBoundingBox()方法来从上下文获得大小。调用-drawRect()会更简单,因为CGRect会作为参数直接传入。

    你应该将你的绘制工作限制在这个矩形中。任何在此区域之外的绘制都将被自动无视,但是这样CPU花在计算和抛弃上的时间就浪费了,实在是太不值得了。

    相比依赖于Core Graphics为你重绘,裁剪出自己的绘制区域可能会让你避免不必要的操作。那就是说,如果你的裁剪逻辑相当复杂,那还是让Core Graphics来代劳吧,记住:当你能高效完成的时候才这样做。

    清单13.4 展示了一个-addBrushStrokeAtPoint:方法的升级版,它只重绘当前线刷的附近区域。另外也会刷新之前线刷的附近区域,我们也可以用CGRectIntersectsRect()来避免重绘任何旧的线刷以不至于覆盖已更新过的区域。这样做会显著地提高绘制效率(见图13.4)

    清单13.4 用-setNeedsDisplayInRect:来减少不必要的绘制

- (void)addBrushStrokeAtPoint:(CGPoint)point
{
//add brush stroke to array
[self.strokes addObject:[NSValue valueWithCGPoint:point]];

//set dirty rect
[self setNeedsDisplayInRect:[self brushRectForPoint:point]];
}

- (CGRect)brushRectForPoint:(CGPoint)point
{
return CGRectMake(point.x - BRUSH_SIZE/2, point.y - BRUSH_SIZE/2, BRUSH_SIZE, BRUSH_SIZE);
}

- (void)drawRect:(CGRect)rect
{
//redraw strokes
for (NSValue *value in self.strokes) {
//get point
CGPoint point = [value CGPointValue];

//get brush rect
CGRect brushRect = [self brushRectForPoint:point];

//only draw brush stroke if it intersects dirty rect
if (CGRectIntersectsRect(rect, brushRect)) {
//draw brush stroke
[[UIImage imageNamed:@"Chalk.png"] drawInRect:brushRect];
}
}
}

图13.4

图13.4 更好的帧率和顺滑线条

收起阅读 »

iOS 性能调优 三

Instruments    Instruments是Xcode套件中没有被充分利用的一个工具。很多iOS开发者从没用过Instruments,或者只是用Leaks工具检测循环引用。实际上有很多Instruments工具...
继续阅读 »

Instruments

    Instruments是Xcode套件中没有被充分利用的一个工具。很多iOS开发者从没用过Instruments,或者只是用Leaks工具检测循环引用。实际上有很多Instruments工具,包括为动画性能调优的东西。

    你可以通过在菜单中选择Profile选项来打开Instruments(在这之前,记住要把目标设置成iOS设备,而不是模拟器)。然后将会显示出图12.1(如果没有看到所有选项,你可能设置成了模拟器选项)。

图12.1

图12.1 Instruments工具选项窗口

    就像之前提到的那样,你应该始终将程序设置成发布选项。幸运的是,配置文件默认就是发布选项,所以你不需要在分析的时候调整编译策略。

我们将讨论如下几个工具:

  • 时间分析器 - 用来测量被方法/函数打断的CPU使用情况。

  • Core Animation - 用来调试各种Core Animation性能问题。

  • OpenGL ES驱动 - 用来调试GPU性能问题。这个工具在编写Open GL代码的时候很有用,但有时也用来处理Core Animation的工作。

    Instruments的一个很棒的功能在于它可以创建我们自定义的工具集。除了你初始选择的工具之外,如果在Instruments中打开Library窗口,你可以拖拽别的工具到左侧边栏。我们将创建以上我们提到的三个工具,然后就可以并行使用了(见图12.2)。

图12.2

图12.2 添加额外的工具到Instruments侧边栏

时间分析器

    时间分析器工具用来检测CPU的使用情况。它可以告诉我们程序中的哪个方法正在消耗大量的CPU时间。使用大量的CPU并不一定是个问题 - 你可能期望动画路径对CPU非常依赖,因为动画往往是iOS设备中最苛刻的任务。

    但是如果你有性能问题,查看CPU时间对于判断性能是不是和CPU相关,以及定位到函数都很有帮助(见图12.3)。

图12.3

图12.3 时间分析器工具

    时间分析器有一些选项来帮助我们定位到我们关心的的方法。可以使用左侧的复选框来打开。其中最有用的是如下几点:

  • 通过线程分离 - 这可以通过执行的线程进行分组。如果代码被多线程分离的话,那么就可以判断到底是哪个线程造成了问题。

  • 隐藏系统库 - 可以隐藏所有苹果的框架代码,来帮助我们寻找哪一段代码造成了性能瓶颈。由于我们不能优化框架方法,所以这对定位到我们能实际修复的代码很有用。

  • 只显示Obj-C代码 - 隐藏除了Objective-C之外的所有代码。大多数内部的Core Animation代码都是用C或者C++函数,所以这对我们集中精力到我们代码中显式调用的方法就很有用。

Core Animation

    Core Animation工具用来监测Core Animation性能。它给我们提供了周期性的FPS,并且考虑到了发生在程序之外的动画(见图12.4)。

图12.4

图12.4 使用可视化调试选项的Core Animation工具

    Core Animation工具也提供了一系列复选框选项来帮助调试渲染瓶颈:

  • Color Blended Layers - 这个选项基于渲染程度对屏幕中的混合区域进行绿到红的高亮(也就是多个半透明图层的叠加)。由于重绘的原因,混合对GPU性能会有影响,同时也是滑动或者动画帧率下降的罪魁祸首之一。

  • ColorHitsGreenandMissesRed - 当使用shouldRasterizep属性的时候,耗时的图层绘制会被缓存,然后当做一个简单的扁平图片呈现。当缓存再生的时候这个选项就用红色对栅格化图层进行了高亮。如果缓存频繁再生的话,就意味着栅格化可能会有负面的性能影响了(更多关于使用shouldRasterize的细节见第15章“图层性能”)。

  • Color Copied Images - 有时候寄宿图片的生成意味着Core Animation被强制生成一些图片,然后发送到渲染服务器,而不是简单的指向原始指针。这个选项把这些图片渲染成蓝色。复制图片对内存和CPU使用来说都是一项非常昂贵的操作,所以应该尽可能的避免。

  • Color Immediately - 通常Core Animation Instruments以每毫秒10次的频率更新图层调试颜色。对某些效果来说,这显然太慢了。这个选项就可以用来设置每帧都更新(可能会影响到渲染性能,而且会导致帧率测量不准,所以不要一直都设置它)。

  • Color Misaligned Images - 这里会高亮那些被缩放或者拉伸以及没有正确对齐到像素边界的图片(也就是非整型坐标)。这些中的大多数通常都会导致图片的不正常缩放,如果把一张大图当缩略图显示,或者不正确地模糊图像,那么这个选项将会帮你识别出问题所在。

  • Color Offscreen-Rendered Yellow - 这里会把那些需要离屏渲染的图层高亮成黄色。这些图层很可能需要用shadowPath或者shouldRasterize来优化。

  • Color OpenGL Fast Path Blue - 这个选项会对任何直接使用OpenGL绘制的图层进行高亮。如果仅仅使用UIKit或者Core Animation的API,那么不会有任何效果。如果使用GLKView或者CAEAGLLayer,那如果不显示蓝色块的话就意味着你正在强制CPU渲染额外的纹理,而不是绘制到屏幕。

  • Flash Updated Regions - 这个选项会对重绘的内容高亮成黄色(也就是任何在软件层面使用Core Graphics绘制的图层)。这种绘图的速度很慢。如果频繁发生这种情况的话,这意味着有一个隐藏的bug或者说通过增加缓存或者使用替代方案会有提升性能的空间。

    这些高亮图层的选项同样在iOS模拟器的调试菜单也可用(图12.5)。我们之前说过用模拟器测试性能并不好,但如果你能通过这些高亮选项识别出性能问题出在什么地方的话,那么使用iOS模拟器来验证问题是否解决也是比真机测试更有效的。

图12.5

图12.5 iOS模拟器中Core Animation可视化调试选项

OpenGL ES驱动

    OpenGL ES驱动工具可以帮你测量GPU的利用率,同样也是一个很好的来判断和GPU相关动画性能的指示器。它同样也提供了类似Core Animation那样显示FPS的工具(图12.6)。

图12.6

图12.6 OpenGL ES驱动工具

侧栏的邮编是一系列有用的工具。其中和Core Animation性能最相关的是如下几点:

  • Renderer Utilization - 如果这个值超过了~50%,就意味着你的动画可能对帧率有所限制,很可能因为离屏渲染或者是重绘导致的过度混合。

  • Tiler Utilization - 如果这个值超过了~50%,就意味着你的动画可能限制于几何结构方面,也就是在屏幕上有太多的图层占用了。

一个可用的案例

    现在我们已经对Instruments中动画性能工具非常熟悉了,那么可以用它在现实中解决一些实际问题。

    我们创建一个简单的显示模拟联系人姓名和头像列表的应用。注意即使把头像图片存在应用本地,为了使应用看起来更真实,我们分别实时加载图片,而不是用–imageNamed:预加载。同样添加一些图层阴影来使得列表显示得更真实。清单12.1展示了最初版本的实现。

清单12.1 使用假数据的一个简单联系人列表

#import "ViewController.h"
#import

@interface ViewController ()

@property (nonatomic, strong) NSArray *items;
@property (nonatomic, weak) IBOutlet UITableView *tableView;

@end

@implementation ViewController

- (NSString *)randomName
{
NSArray *first = @[@"Alice", @"Bob", @"Bill", @"Charles", @"Dan", @"Dave", @"Ethan", @"Frank"];
NSArray *last = @[@"Appleseed", @"Bandicoot", @"Caravan", @"Dabble", @"Ernest", @"Fortune"];
NSUInteger index1 = (rand()/(double)INT_MAX) * [first count];
NSUInteger index2 = (rand()/(double)INT_MAX) * [last count];
return [NSString stringWithFormat:@"%@ %@", first[index1], last[index2]];
}

- (NSString *)randomAvatar
{
NSArray *images = @[@"Snowman", @"Igloo", @"Cone", @"Spaceship", @"Anchor", @"Key"];
NSUInteger index = (rand()/(double)INT_MAX) * [images count];
return images[index];
}

- (void)viewDidLoad
{
[super viewDidLoad];
//set up data
NSMutableArray *array = [NSMutableArray array];
for (int i = 0; i < 1000; i++) {
//add name
[array addObject:@{@"name": [self randomName], @"image": [self randomAvatar]}];
}
self.items = array;
//register cell class
[self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"Cell"];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [self.items count];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
//dequeue cell
UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath];
//load image
NSDictionary *item = self.items[indexPath.row];
NSString *filePath = [[NSBundle mainBundle] pathForResource:item[@"image"] ofType:@"png"];
//set image and text
cell.imageView.image = [UIImage imageWithContentsOfFile:filePath];
cell.textLabel.text = item[@"name"];
//set image shadow
cell.imageView.layer.shadowOffset = CGSizeMake(0, 5);
cell.imageView.layer.shadowOpacity = 0.75;
cell.clipsToBounds = YES;
//set text shadow
cell.textLabel.backgroundColor = [UIColor clearColor];
cell.textLabel.layer.shadowOffset = CGSizeMake(0, 2);
cell.textLabel.layer.shadowOpacity = 0.5;
return cell;
}

@end

    当快速滑动的时候就会非常卡(见图12.7的FPS计数器)。

图12.7

图12.7 滑动帧率降到15FPS

    仅凭直觉,我们猜测性能瓶颈应该在图片加载。我们实时从闪存加载图片,而且没有缓存,所以很可能是这个原因。我们可以用一些很赞的代码修复,然后使用GCD异步加载图片,然后缓存。。。等一下,在开始编码之前,测试一下假设是否成立。首先用我们的三个Instruments工具分析一下程序来定位问题。我们推测问题可能和图片加载相关,所以用Time Profiler工具来试试(图12.8)。

图12.8

图12.8 用The timing profile分析联系人列表

    -tableView:cellForRowAtIndexPath:中的CPU时间总利用率只有~28%(也就是加载头像图片的地方),非常低。于是建议是CPU/IO并不是真正的限制因素。然后看看是不是GPU的问题:在OpenGL ES Driver工具中检测GPU利用率(图12.9)。

图12.9

图12.9 OpenGL ES Driver工具显示的GPU利用率

    渲染服务利用率的值达到51%和63%。看起来GPU需要做很多工作来渲染联系人列表。

    为什么GPU利用率这么高呢?我们来用Core Animation调试工具选项来检查屏幕。首先打开Color Blended Layers(图12.10)。

图12.10

图12.10 使用Color Blended Layers选项调试程序

    屏幕中所有红色的部分都意味着字符标签视图的高级别混合,这很正常,因为我们把背景设置成了透明色来显示阴影效果。这就解释了为什么渲染利用率这么高了。

    那么离屏绘制呢?打开Core Animation工具的Color Offscreen - Rendered Yellow选项(图12.11)。

图12.11

图12.11 Color Offscreen–Rendered Yellow选项

    所有的表格单元内容都在离屏绘制。这一定是因为我们给图片和标签视图添加的阴影效果。在代码中禁用阴影,然后看下性能是否有提高(图12.12)。

图12.12

图12.12 禁用阴影之后运行程序接近60FPS

    问题解决了。干掉阴影之后,滑动很流畅。但是我们的联系人列表看起来没有之前好了。那如何保持阴影效果而且不会影响性能呢?

    好吧,每一行的字符和头像在每一帧刷新的时候并不需要变,所以看起来UITableViewCell的图层非常适合做缓存。我们可以使用shouldRasterize来缓存图层内容。这将会让图层离屏之后渲染一次然后把结果保存起来,直到下次利用的时候去更新(见清单12.2)。

清单12.2 使用shouldRasterize提高性能

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
//dequeue cell
UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:@"Cell"
forIndexPath:indexPath];
...
//set text shadow
cell.textLabel.backgroundColor = [UIColor clearColor];
cell.textLabel.layer.shadowOffset = CGSizeMake(0, 2);
cell.textLabel.layer.shadowOpacity = 0.5;
//rasterize
cell.layer.shouldRasterize = YES;
cell.layer.rasterizationScale = [UIScreen mainScreen].scale;
return cell;
}

    我们仍然离屏绘制图层内容,但是由于显式地禁用了栅格化,Core Animation就对绘图缓存了结果,于是对提高了性能。我们可以验证缓存是否有效,在Core Animation工具中点击Color Hits Green and Misses Red选项(图12.13)。

图12.13

图12.13 Color Hits Green and Misses Red验证了缓存有效

    结果和预期一致 - 大部分都是绿色,只有当滑动到屏幕上的时候会闪烁成红色。因此,现在帧率更加平滑了。

    所以我们最初的设想是错的。图片的加载并不是真正的瓶颈所在,而且试图把它置于一个复杂的多线程加载和缓存的实现都将是徒劳。所以在动手修复之前验证问题所在是个很好的习惯!

总结

    在这章中,我们学习了Core Animation是如何渲染,以及我们可能出现的瓶颈所在。你同样学习了如何使用Instruments来检测和修复性能问题。

    在下三章中,我们将对每个普通程序的性能陷阱进行详细讨论,然后学习如何修复。


收起阅读 »

带着问题学习Android中View的measure测量

在进行研究measure原理之前,我们先带着这三个问题来想想。因为我是遇到这三个问题才开始研究measure的源码,所以我也把下面的三个问题当做引子。调用measure(int widthMeasureSpec, int heightMeasureSpec)方...
继续阅读 »

在进行研究measure原理之前,我们先带着这三个问题来想想。因为我是遇到这三个问题才开始研究measure的源码,所以我也把下面的三个问题当做引子。

调用measure(int widthMeasureSpec, int heightMeasureSpec)方法传递的参数是什么? 为什么调用measure方法View控件没有进行测量? 如何强制view进行测量? 在进行研究之前,我们先来看一个简单的布局,

        <Button
android:id="@+id/btn_click"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="点击"
android:onClick="start"
/>

<LinearLayout
android:id="@+id/linear"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#FF0000">

<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="点击"/>

</LinearLayout>

看效果图:

image.png

根据布局文件,我们并没有设置边距属性,为什么显示的效果的Button跟下面的没有对齐。这就是。在实际开发中,我们细心点会发现,对于Button控件,我们选中它的时候显示的区域比它展现的区域大。

如果我们给Button控件添加背景色:

        <Button
android:id="@+id/btn_click"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="点击"
android:background="#FF0000"
android:onClick="start"
/>

可以看到Button的背景色和LinearLayout的背景色无缝连接在一起,同时我们观察下面的那个点击的Button,发现它的周围区域实际是存在的,是白色与我们的背景色重叠起来了。这就引入了我们的一个重要概念:控件边界布局和视觉编辑布局。我们在真机上打开【显示布局边界】,在设置——>开发者选项——>显示布局边界。 我们看下效果图。

注:蓝色 为控件的布局边界;粉红色为视觉边界 这就涉及到我们的一个ViewGroup属性:android:layoutMode

说的通俗一点,clipBounds就是默认值,不处理一些控件之间的“留白”,opticalBounds消除控件之间的留白。

我们抽出LinearLayout的布局来说:

        <LinearLayout
android:id="@+id/linear"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#fff000"
android:layoutMode="clipBounds">

<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="点击"/>

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="测试的"
/>

</LinearLayout>

我们修改属性android:layoutMode=”opticalBounds”,效果图:

image.png

通过对比发现就是一个清除的效果。

MeasureSpec 我们分析第一个问题,onMeasure()方法里传的是什么?传的就是MeasureSpec变量。它是View的一个内部类。源码设计非常简单精悍。

一个MeasureSpec封装了父布局传递给子布局的布局要求,每个MeasureSpec代表了一组宽度和高度的要求。一个MeasureSpec由大小和模式组成。由32位组成,头8位为模式,后24位封装大小。它有三种模式:UNSPECIFIED(未指定),父元素部队自元素施加任何束缚,子元素可以得到任意想要的大小;EXACTLY(完全),父元素决定自元素的确切大小,子元素将被限定在给定的边界里而忽略它本身大小;AT_MOST(至多),子元素至多达到指定大小的值。它常用的三个函数:

static int getMode(int measureSpec):根据提供的测量值(格式)提取模式(上述三个模式之一) static int getSize(int measureSpec):根据提供的测量值(格式)提取大小值(这个大小也就是我们通常所说的大小) static int makeMeasureSpec(int size,int mode):根据提供的大小值和模式创建一个测量值(格式) Mode的取值:

MeasureSpec.AT_MOST,即十进制2,该值表示View最大可以取其父ViewGroup给其指定的尺寸,例如现在有个Int值widthMeasureSpec,ViewGroup将其传递给了View的measure方法,如果widthMeasureSpec中的mode值是AT_MOST,size是300,那么表示View能取的最大的宽度是300。

MeasureSpec.EXACTLY,即十进制1,该值表示View必须使用其父ViewGroup指定的尺寸,还是以widthMeasureSpec为例,如果其mode值是EXACTLY,控件大小就是它老子的大小

MeasureSpec.UNSPECIFIED,即十进制0,该值表示View的父ViewGroup没有给View在尺寸上设置限制条件,这种情况下View可以忽略measureSpec中的size,View可以取自己想要的值作为量算的尺寸。

我们常看到measure(0,0)或者measure(1,1)之类的,这就是传入的测量模式。

measure()方法 下面就开始分析measure方法。

       public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
//判断当前view的LayoutMode是否为opticalbounds
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {//判断当前view的ParentView的LayoutMode是否为opticalbounds
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);
}

// 根据我们传入的widthMeasureSpec和heightMeasureSpec计算key值,我们在mMeasureCache中存储我们view的信息
long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
//如果mMeasureCache为null,则进行new一个对象
if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);

//mOldWidthMeasureSpec和mOldHeightMeasureSpec分别表示上次对View进行量算时的widthMeasureSpec和heightMeasureSpec
//执行View的measure方法时,View总是先检查一下是不是真的有必要费很大力气去做真正的量算工作
//mPrivateFlags是一个Int类型的值,其记录了View的各种状态位
//如果(mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT,
//那么表示当前View需要强制进行layout(比如执行了View的forceLayout方法),所以这种情况下要尝试进行量算
//如果新传入的widthMeasureSpec/heightMeasureSpec与上次量算时的mOldWidthMeasureSpec/mOldHeightMeasureSpec不等,
//那么也就是说该View的父ViewGroup对该View的尺寸的限制情况有变化,这种情况下要尝试进行量算
if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
widthMeasureSpec != mOldWidthMeasureSpec ||
heightMeasureSpec != mOldHeightMeasureSpec) {

//通过运算,重置mPrivateFlags值,即View的测量状态
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
//解决布局中的Rtl问题
resolveRtlPropertiesIfNeeded();
//判断当前View是否是强制进行测量,如果是则将cacheIndex=-1,反之从mMeasureCache中获取
//对应的index,即从缓存中读取存储的大小。
int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
mMeasureCache.indexOfKey(key);
//根据cacheIndex的大小判断是否需要重新测量,或者根据布尔变量sIgnoreMeasureCache进行判断。
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// 重新测量,则调用我们重写的onMeasure()方法进行测量,然后重置View的状态
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
// 通过我们计算的cacheIndex值,从缓存中读取我们的测量值。
long value = mMeasureCache.valueAt(cacheIndex);
// 通过setMeasuredDimension()方法设置我们的测量值,然后重置View的状态
setMeasuredDimension((int) (value >> 32), (int) value);
mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}

// 如果View的状态没有改变,则会抛出异常“我们没有调用”setMeasuredDimension()“方法,一般出现在我们重写onMeasure方法,
//但是没有调用setMeasuredDimension方法导致的。
if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
throw new IllegalStateException("onMeasure() did not set the"
+ " measured dimension by calling"
+ " setMeasuredDimension()");
}

mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
}

mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;
//将最新的widthMeasureSpec和heightMeasureSpec进行存储到mMeasureCache
mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
(long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}

在上面的代码中,注释还算详细,仔细看应该能知道测量的流程。 (1)、测量首先判断控件的模式,通过调用isLayoutModeOptical方法进行判断。

        public static boolean isLayoutModeOptical(Object o) {
return o instanceof ViewGroup && ((ViewGroup) o).isLayoutModeOptical();
}

//ViewGroup的isLayoutModeOptical方法
boolean isLayoutModeOptical() {
return mLayoutMode == LAYOUT_MODE_OPTICAL_BOUNDS;
}

这个方法就是判断view是否为ViewGroup类型,然后判断layoutMode设定是否为opticalBounds。如果是,则对传入的widthMeasureSpec、heightMeasureSpec进行重新计算封装,通过上面的试验,我们看到了设定的区别,所以需要重新计算封装。

(2)、判断当前view是否强制重新计算,或者传入进来的MeasureSpec是否和上次不同。这两种情况满足一种则进行测量运算。 (3)、系统还不满足,又判断是否为强制测量,如果为强制测量或者忽略缓存,则调用我们重写的onMeasure()方法进行测量,反之,从mMeasureCache缓存中读取上次的测量数据。

为什么调用measure()方法控件没有进行重新测量?

通过前面的源码分析,是不是对结果知道一二,View也不是因为我们调用了measure方法就进行真真切切的重新测量,首先,它会判断我们是否是强制测量或者测量模式发生了改变没有,这个是必要条件,如果这里都不满足就不会进入执行到我们的onMeasure方法,之后还要判断我们是否强制重新测量,不然取缓存的值,只样实际上还没有达到我们的测量。 注:Android不同版本对应的measure方法源码可能有所不同。

说到这里,measure的源码是分析了,我们在往深入的想,我们如果在我们的自定义View时没有对onMeasure()方法进行重写,那么系统调用的onMeasure()方法是怎么实现的呢?不错,我们就瞧一瞧View中默认的onMeasure()方法是怎么实现的。

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

这里面涉及到三个方法:

  • getDefaultSize
  • getSuggestedMinimumWidth
  • getSuggestedMinimumHeight

稍微思考下,我们也知道肯定是设置一个默认值的,我们看下后两个函数的源码:

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

protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}

都是进行判断backgroud是否为空,如果为空,返回view最小的高度或宽度,如果不为空,返回与backgroud的最小宽高中的最大值。可能你会疑惑,view的最小宽度或高度是怎么来的?这个就要回归到我们的View构造函数。

    case R.styleable.View_minWidth:
mMinWidth = a.getDimensionPixelSize(attr, 0);
break;
case R.styleable.View_minHeight:
mMinHeight = a.getDimensionPixelSize(attr, 0);
break;

可以从这里获取,当然我们也可以进行设定:

    public void setMinimumWidth(int minWidth) {
mMinWidth = minWidth;
requestLayout();
}


public void setMinimumHeight(int minHeight) {
mMinHeight = minHeight;
requestLayout();
}

我们接着看看看getDefaultSize()的源码:

    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;
}

在getDefaultSize中,传入进来我们获取的最小值,然后根据我们设定的MeasureSpec获取对应size和mode,然后判断mode,如果为MeasureSpec.UNSPECIFIED就将size赋值我们获取的最小大小。模式为MeasureSpec.AT_MOST、MeasureSpec.EXACTLY时,赋值为我们从MeasureSpec获取的大小。这也证实了自定义控件时,我们没有重写onMeasure方法,同时给控件设置wrap_content属性,控件显示的效果是match_parent的效果。

说到这里measure流程的大概也基本搞明白了。 我们来看第三个问题,如何强制一个view进行重绘? 根据上面的分析,我们强制重绘就得清除缓存mMeasureCache缓存中的数据。这里就得提及forceLayout()方法,看下这个方法的源码:

    public void forceLayout() {
if (mMeasureCache != null) mMeasureCache.clear();
mPrivateFlags |= PFLAG_FORCE_LAYOUT;
mPrivateFlags |= PFLAG_INVALIDATED;
}

这个方法中就是清除缓存mMeasureCache中的缓存数据,然后改变View的mPrivateFlags属性值。这里又得说起requestLayout()函数,用于请求重新布局。


public void requestLayout() {
if (mMeasureCache != null) mMeasureCache.clear();

if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
// Only trigger request-during-layout logic if this is the view requesting it,
// not the views in its parent hierarchy
ViewRootImpl viewRoot = getViewRootImpl();
if (viewRoot != null && viewRoot.isInLayout()) {
if (!viewRoot.requestLayoutDuringLayout(this)) {
return;
}
}
mAttachInfo.mViewRequestingLayout = this;
}

mPrivateFlags |= PFLAG_FORCE_LAYOUT;
mPrivateFlags |= PFLAG_INVALIDATED;

if (mParent != null && !mParent.isLayoutRequested()) {
mParent.requestLayout();
}
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
mAttachInfo.mViewRequestingLayout = null;
}
}

这样就可以完成View的强制测量。在实际的开发中,我们在对自定义View进行测量的时候,只需要重写onMeasure()方法即可,在onMeasure()方法中指定我们要求的控件大小,除非我们在刷新控件的时候需要我们去考虑一些方法的实现,探究源码让我们知道了为什么是这样,不至于迷惘。

收起阅读 »

kotlin协程最佳实践-android官网

协程最佳实践 android官网地址 这些实践可以让你的程序在使用协程的时候更加的易扩展和易测试 1.注入调度器 不要在创建一个协程的时候或者调用withContext,硬编码来指定调度器 比如这样的 class NewsRepository { ...
继续阅读 »

协程最佳实践 android官网地址


这些实践可以让你的程序在使用协程的时候更加的易扩展和易测试


1.注入调度器


不要在创建一个协程的时候或者调用withContext,硬编码来指定调度器 比如这样的


class NewsRepository {
// DO NOT use Dispatchers.Default directly, inject it instead
suspend fun loadNews() = withContext(Dispatchers.Default) { /* ... */ }
}

而应该进行注入


class NewsRepository(
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
) {
suspend fun loadNews() = withContext(defaultDispatcher) { /* ... */ }
}

原因:依赖注入的模式可以让你在测试的时候容易更换调度器 详细参考Android中简易的协程


2. 挂起函数在实现的时候,应该保证对主线程是安全的


比如这样的:


suspend fun fetchLatesNews():ListArtical{
withContext(Dispatchers.IO){
}
}

主线程调用的时候


suspend operator fun updateContent(){
val news = fetchLatesNews()
}

这样可以保证你的App是易扩展的,类挂起方法调用的时候,不需要担心线程是在哪个环境调度的,由具体实现类中的方法来确保线程调度的安全


3. viewModle 应该去创建一个协程


viewModle更应该去创建一个协程,而不是去暴露一个suspend方法。 比如应该是这样:


//示例代码,viewModle内去创建一个协程
class LastestNewsViewModel{
//内部维护了一个可观察的带状态的数据
private val _uiState = MutableStateFlow<LatestNewsUiState>(LatestNewsUiState.Loading)
val uiState:StateFlow<LatestNewsUiState> = _uiState

//重点来了,这里不是一个suspend方法
fun loadNews(){
viewModleScope.lanuch{
val lastestNewsWithAuthors = getLatestNewsWithAuthors()
_uiState.valule = LastestNewUiState.Success(lastestNewsWithAuthors)
}
}

}


而不是这样的


class LastestNewsViewModel():ViewModel{
//这种是直接返回了一个suspend方法
suspend fun loadNews() = getLatestNewsWithAuthors()
}


除非不需要调用知道数据流的状态,而只需要发射一个单独的数据。(个人理解,是保持viewModle中的定义,维护一个可观察的带状态的数据,而不是直接扔原始数据出来)


4.不要暴露可修改的参数类型


应该对其他类暴露不可修改的的类型,这样所有可变类型数据的变更都集中在一个类里,如果有问题的时候,更容易调试(也是迪米特原则) 比如应该是这样的


class LastestNewsViewModel : ViewModel{
//可修改类型
private val _uiState = _MutalbeStateFlwow(LastestNewsViewModel.Loading)
//对外暴露不可修改类型数据(对外不提供修改功能)
val uiState : StateFlow<LatestNewsUiState> = _uiState
}

5. 数据和业务层应该暴露挂起函数 或 Flow


数据层和业务层通常需要暴露方法,去执行一次性的调用或者需要持续接收数据的变化,这时候应该提供为一次性调用提供挂起函数 或者 提供Flow来帮忙观察数据的变化操作 比如这样的:


class ExampleRepository{
//为一次性的调用提供 suspend方法
suspend fun makeNetworkRequest(){}

//为一需要观察的数据提供Flow对象
fun getExamples():Flow<Example>{}
}

最佳的实践可以使调用者通常是业务层,能够控制业务的执行和生命周期的运转,并且在需要的时候可以取消任务


6. 在业务和数据层创建协程


在数据和业务层需要创建协程的原因可能有不几的原因,下边是一些可能的选项



  • 如果协程的任务是相关的,且只在用户在当前界面时才显示,那么它需要关联调用者的生命周期,这个调用者通常就是ViewModel,在这种 情况下, 应该使用coroutineScope 和 supervisorScope


示例代码:


class GetAllBooksAndAuthorsUseCase(
private val booksRepository:BooksRepository,
private val authorsRepository:AuthorsRepository,
private val defaultDispatcher:CoroutineDispatcher = Dispatchers.Default
){
suspend fun getBookdAndAuthors():BookAndAuthors{
//平行的情况需要等待结果,书籍列表和作者列表需要同时准备好之后再返回
return coroutineScope{
val books = async(defaultDispatcher){
booksRepository.getAllBooks()
}
val authors = async(defaultDispatcher){
authorsRepository.getallAuthors()
}
//准备好数据之后再返回
BookAndAuthors(books.await(),authors.await())
}
}
}


  • 如果这个任务是在App开启期间需要执行,这个任务也不绑定到某一个具体的界面,这时候任务是需要在超出调用者的生命周期的,这种场景下,需要用到


external 的 CoroutineScope ,详细可参考 协程设计模式之任务不应该被取消


参考示例代码:


class ArticalesRepository(
private val articlesDataSource: ArticlesDataSource,
private val externalScope:CoroutineScope,
private val defaultDispatcher:CoroutineDispatcher = Dispatchers.Default
){
//这个场景是这样的,即使我们离开的屏幕,也希望这个预订操作是能够被完整执行的,那么这任务斋要在外部域开启一个新的协程里来完成这wh
suspend fun bookmarkArtical(artical:Article){
externalScope.lanuch(defaultDispatcher){
articlesDataSource.bookmarkArticle(article)
}.join() //等待协程执行完毕
}
}


说明: 外部域需要被一个比当前界面的生命周期更长的一个类来创建,比如说 Application或者是一个navigatin grah的ViewModel


7. 避免使用GlobalScope全局作用域


就像最佳实践里边的注入调度器,如果用了GlobalScope,那就是在类里边使用硬编码,可能会有以下几个负面影响



  • 硬编码。

  • 难以测试


8. 协程需要可以被取消


取消操作也是一种协程的操作,意思是说当协程被取消的时候,协程并没有直接被取消,除非它在 挂起 或者 有取消操作,如果你的协程是在操作一个阻塞的操作,需要确保协程是中途可以被取消的。 举个例子,如果你正在读取多个文件,需要在读取每个文件之前,检查下协程是否已经被取消了,一个检查协程是否被取消的方法就是 调用 ensureActivite方法,(或者还有isActive可用) 参考示例代码:


    someScope.lanuch{
ensureActive()//检查协程是否已经被取消
readFile(file)
}

更多详细的描述信息可以参考 取消协程


9. 协程的异常处理


如果协程抛出的异常处理不当,可能会导致你的App崩溃。如果异常出现了,就在协程里就捕获好异常并进行处理


参考示例代码:


class LoginViewModel(
private val loginRepository:LoginRepository
):ViewModel(){
fun login(username:String,token:String){
viewModleScope.lanuch{
try{
loginRepository.login(username,token)
//通知界面登录成功
}catch(error:Throwable){
//通知view 登录操作失败
}
}
}
}

更多协程异常的处理,或者其他场景需要用到CoroutineExceptionHandler,可以参考 协程异常处理



																			        	收起阅读 »
								        											

影响性能的 Kotlin 代码(一)

Kotlin 高级函数的特性不仅让代码可读性更强,更加简洁,而且还提高了生产效率,但是简洁的背后是有代价的,隐藏着不能被忽视的成本,特别是在低端机上,这种成本会被放大,因此我们需要去研究 kotlin 语法糖背后的魔法,选择合适的语法糖,尽量避免这些坑。 L...
继续阅读 »

Kotlin 高级函数的特性不仅让代码可读性更强,更加简洁,而且还提高了生产效率,但是简洁的背后是有代价的,隐藏着不能被忽视的成本,特别是在低端机上,这种成本会被放大,因此我们需要去研究 kotlin 语法糖背后的魔法,选择合适的语法糖,尽量避免这些坑。


Lambda 表达式


Lambda 表达式语法简洁,避免了冗长的函数声明,代码如下。


fun requestData(type: Int, call: (code: Int, type: Int) -> Unit) {
call(200, type)
}

Lambda 表达式语法虽然简洁,但是隐藏着两个性能问题。



  • 每次调用 Lambda 表达式,都会创建一个对象



图中标记 1 所示的地方,涉及一个字节码类型的知识点。



















标识符 含义
I 基本类型 int
L 对象类型,以分号结尾,如 Lkotlin/jvm/functions/Function2;

Lambda 表达式 call: (code: Int, type: Int) -> Unit 作为函数参数,传递到函数中,Lambda 表达式会继承 kotlin/jvm/functions/Function2 , 每次调用都会创建一个 Function2 对象,如图中标记 2 所示的地方。



  • Lambda 表达式隐含自动装箱和拆箱过程



正如你所见 lambda 表达式存在装箱和拆箱的开销,会将 int 转成 Integer,之后进行一系列操作,最后会将 Integer 转成 int


如果想要避免 Lambda 表达式函数对象的创建及装箱拆箱开销,可以使用 inline 内联函数,直接执行 lambda 表达式函数体。


Inline 修饰符


Inline 内联函数的作用:提升运行效率,调用被 inline 修饰符标记的函数,会把函数内的代码放到调用的地方。


如果阅读过 Koin 源码的朋友,应该会发现 inline 都是和 lambda 表达式和 reified 修饰符配套在一起使用的,如果只使用 inline 修饰符标记普通函数,Android Studio 也会给一个大大大的警告。



编译器建议我们在含有 lambda 表达式作为形参的函数中使用内联,既然 Inline 修饰符可以提升运行效率,为什么编译器会给我们一个警告? 这是为了防止 inline 操作符滥用而带来的性能损失。


inline 修饰符适用于以下情况



  • inline 修饰符适用于把函数作为另一个函数的参数,例如高阶函数? filter、map、joinToString 或者一些独立的函数 repeat

  • inline 操作符适合和 reified 操作符结合在一起使用

  • 如果函数体很短,使用 inline 操作符可以提高效率


Kotlin 遍历数组


这一小节主要介绍 Kotlin 数组,一起来看一下遍历数组都有几种方式。



  • 通过 forEach 遍历数组

  • 通过区间表达式遍历数组(..downTountil)

  • 通过 indices 遍历数组

  • 通过 withIndex 遍历数组


通过 forEach 遍历数组


先来看看通过 forEach 遍历数组,和其他的遍历数组的方式,有什么不同。


array.forEach { value ->

}

反编译后:

Integer[] var5 = array;
int var6 = array.length;
for(int var7 = 0; var7 < var6; ++var7) {
Object element$iv = var5[var7];
int value = ((Number)element$iv).intValue();
boolean var10 = false;
}

正如你所见通过 forEach 遍历数组的方式,会创建额外的对象,并且存在装箱/拆箱开销,会占用更多的内存。


通过区间表达式遍历数组


在 Kotlin 中区间表达式有三种 ..downTountil



  • .. 关键字,表示左闭右闭区间

  • downTo 关键字,实现降序循环

  • until 关键字,表示左闭右开区间


.. 、downTo 、until


for (value in 0..size - 1) {
// case 1
}

for (value in size downTo 0) {
// case 2
}

for (value in 0 until size) {
// case 3
}

反编译后

// case 1
if (value <= var4) {
while(value != var4) {
++value;
}
}

// case 2
for(boolean var5 = false; value >= 0; --value) {
}

// case 3
for(var4 = size; value < var4; ++value) {
}

如上所示 区间表达式 ( ..downTountil) 除了创建一些临时变量之外,不会创建额外的对象,但是区间表达式 和 step 关键字结合起来一起使用,就会存在内存问题。


区间表达式 和 step 关键字


step 操作的 ..downTountil, 编译之后如下所示。


for (value in 0..size - 1 step 2) {
// case 1
}

for (value in 0 downTo size step 2) {
// case 2
}

反编译后:

// case 1
var10000 = RangesKt.step((IntProgression)(new IntRange(var6, size - 1)), 2);
while(value != var4) {
value += var5;
}

// case 2
var10000 = RangesKt.step(RangesKt.downTo(0, size), 2);
while(value != var4) {
value += var5;
}

step 操作的 ..downTountil 除了创建一些临时变量之外,还会创建 IntRangeIntProgression 对象,会占用更多的内存。


通过 indices 遍历数组


indices 通过索引的方式遍历数组,每次遍历的时候通过索引获取数组里面的元素,如下所示。


for (index in array.indices) {
}

反编译后:

for(int var4 = array.length; var3 < var4; ++var3) {
}

通过 indices 遍历数组, 编译之后的代码 ,除了创建了一些临时变量,并没有创建额外的对象。


通过 withIndex 遍历数组


withIndexindices 遍历数组的方式相似,通过 withIndex 遍历数组,不仅可以获取的数组索引,同时还可以获取到每一个元素。


for ((index, value) in array.withIndex()) {

}

反编译后:

Integer[] var5 = array;
int var6 = array.length;
for(int var3 = 0; var3 < var6; ++var3) {
int value = var5[var3];
}

正如你所看到的,通过 withIndex 方式遍历数组,虽然不会创建额外的对象,但是存在装箱/拆箱的开销


总结:



  • 通过 forEach 遍历数组的方式,会创建额外的对象,占用内存,并且存在装箱 / 拆箱开销

  • 通过 indices 和区间表达式 ( ..downTountil) 都不会创建额外的对象

  • 区间表达式 和 step 关键字结合一起使用, 会有创建额外的对象的开销,占用更多的内存

  • 通过 withIndex 方式遍历数组,不会创建额外的对象,但是存在装箱/拆箱的开销


尽量少使用 toLowerCase 和 toUpperCase 方法



这一小节内容,在我之前的文章中分享过,但是这也是很多小伙伴,遇到最多的问题,所以单独拿出来在分析一次



当我们比较两个字符串,需要忽略大小写的时候,通常的写法是调用 toLowerCase() 方法或者 toUpperCase() 方法转换成大写或者小写,然后在进行比较,但是这样的话有一个不好的地方,每次调用 toLowerCase() 方法或者 toUpperCase() 方法会创建一个新的字符串,然后在进行比较。


调用 toLowerCase() 方法


fun main(args: Array<String>) {
// use toLowerCase()
val oldName = "Hi dHL"
val newName = "hi Dhl"
val result = oldName.toLowerCase() == newName.toLowerCase()

// or use toUpperCase()
// val result = oldName.toUpperCase() == newName.toUpperCase()
}

toLowerCase() 编译之后的 Java 代码



如上图所示首先会生成一个新的字符串,然后在进行字符串比较,那么 toUpperCase() 方法也是一样的如下图所示。


toUpperCase() 编译之后的 Java 代码



这里有一个更好的解决方案,使用 equals 方法来比较两个字符串,添加可选参数 ignoreCase 来忽略大小写,这样就不需要分配任何新的字符串来进行比较了。


fun main(args: Array<String>) {
val oldName = "hi DHL"
val newName = "hi dhl"
val result = oldName.equals(newName, ignoreCase = true)
}

equals 编译之后的 Java 代码



使用 equals 方法并没有创建额外的对象,如果遇到需要比较字符串的时候,可以使用这种方法,减少额外的对象创建。


by lazy


by lazy 作用是懒加载,保证首次访问的时候才初始化 lambda 表达式中的代码, by lazy 有三种模式。



  • LazyThreadSafetyMode.NONE 仅仅在单线程

  • LazyThreadSafetyMode.SYNCHRONIZED 在多线程中使用

  • LazyThreadSafetyMode.PUBLICATION 不常用


LazyThreadSafetyMode.SYNCHRONIZED 是默认的模式,多线程中使用,可以保证线程安全,但是会有 double check + lock 性能开销,代码如下图所示。



如果是在主线程中使用,和初始化相关的逻辑,建议使用 LazyThreadSafetyMode.NONE 模式,减少不必要的开销。


仓库 KtKit 是用 Kotlin 语言编写的小巧而实用的工具库,包含了项目中常用的一系列工具, 正在逐渐完善中,如果你有兴趣,想邀请你和我一起来完善这个库。


收起阅读 »