注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

Android输入系统之 的创建与启动

今天趁着在公司摸鱼的时间,来更新一篇文章。 上一篇文章 InputManagerService的创建与启动 分析了 IMS 的创建与启动,这其中就伴随着 InputReader 的创建与启动,本文就着重分析这两点内容。 本文所涉及的文件路径如下 fram...
继续阅读 »

今天趁着在公司摸鱼的时间,来更新一篇文章。


上一篇文章 InputManagerService的创建与启动 分析了 IMS 的创建与启动,这其中就伴随着 InputReader 的创建与启动,本文就着重分析这两点内容。


本文所涉及的文件路径如下


frameworks/native/services/inputflinger/InputManager.cpp frameworks/native/services/inputflinger/reader/EventHub.cpp frameworks/native/services/inputflinger/reader/InputReader.cpp frameworks/base/services/core/jni/com_android_server_input_InputManagerService.cpp frameworks/native/services/inputflinger/InputThread.cpp


InputManagerService的创建与启动 可知,创建 InputReader 的代码如下


InputManager::InputManager(
const sp<InputReaderPolicyInterface>& readerPolicy,
const sp<InputDispatcherPolicyInterface>& dispatcherPolicy) {

// ...

mReader = createInputReader(readerPolicy, mClassifier);
}


sp<InputReaderInterface> createInputReader(const sp<InputReaderPolicyInterface>& policy,
const sp<InputListenerInterface>& listener)
{
// InputReader从EventHub中读取数据
// policy 实现类是 NativeInputManager
// listener的实现类其实是 InputClassifier
return new InputReader(std::make_unique<EventHub>(), policy, listener);
}

创建 InputReader 需要三个参数。


第一个参数的类型是 EventHub。正如名字所示,它是输入事件的中心,InputReader 会从 EventHub 中读取事件。这个事件分两类,一个是输入设备的输入事件,另一个是 EventHub 合成事件,用于表明设备的挂载与卸载。


第二个参数的类型为 InputReaderPolicyInterface,由 InputManagerService的创建与启动 可知,它的实现类是 JNI 层的 NativeInputManager。


第三个参数的类型为 InputListenerInterface, 由 InputManagerService的创建与启动 可知,它的实现类是 InputClassifier。InputReader 会的加工后的事件发送给 InputClassifier,而 InputClassifier 会针对触摸事件进行分类,再发送到 InputDispatcher。


为止防止大家有所健忘,我把上一篇文章中,关于事件的流程图,再展示下


graph TD
EventHub --> InputReader
InputReader --> NativeInputManager
InputReader --> InputClassifer
InputClassifer --> InputDispatcher
InputDispatcher --> NativeInputManager
NativeInputManager --> InputManagerService

创建EventHub


创建 InputReader 首先需要一个 EventHub 对象,因此我们首先得看下 EventHub 的创建过程


EventHub::EventHub(void)
: mBuiltInKeyboardId(NO_BUILT_IN_KEYBOARD),
mNextDeviceId(1),
mControllerNumbers(),
mOpeningDevices(nullptr),
mClosingDevices(nullptr),
mNeedToSendFinishedDeviceScan(false),
mNeedToReopenDevices(false),
mNeedToScanDevices(true),
mPendingEventCount(0),
mPendingEventIndex(0),
mPendingINotify(false) {
ensureProcessCanBlockSuspend();

// 创建epoll
mEpollFd = epoll_create1(EPOLL_CLOEXEC);

// 初始化inotify
mINotifyFd = inotify_init();
// 1. 使用inotify监听/dev/input目录下文件的创建与删除事件
mInputWd = inotify_add_watch(mINotifyFd, DEVICE_PATH, IN_DELETE | IN_CREATE);
// ...

struct epoll_event eventItem = {};
eventItem.events = EPOLLIN | EPOLLWAKEUP;
eventItem.data.fd = mINotifyFd;
// 2. epoll监听inotify可读事件
int result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mINotifyFd, &eventItem);

// 3. 创建两个管道
int wakeFds[2];
result = pipe(wakeFds);

mWakeReadPipeFd = wakeFds[0];
mWakeWritePipeFd = wakeFds[1];

result = fcntl(mWakeReadPipeFd, F_SETFL, O_NONBLOCK);

result = fcntl(mWakeWritePipeFd, F_SETFL, O_NONBLOCK);

eventItem.data.fd = mWakeReadPipeFd;
// 4. epoll监听mWakeReadPipeFd可读事件
result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeReadPipeFd, &eventItem);
}

第一步和第二步,初始化 inotify 监听 /dev/input/ 目录下的文件的创建和删除事件,然后使用 epoll 管理这个 inotify。


为何要使用 inotify 监听 /dev/input/ 目录呢,因为当输入设备挂载和卸载时,内核会相应地在这个目录下创建和删除设备文件,因此监听这个目录可获知当前有哪些输入设备,然后才能监听这些设备的输入事件。


第三步和第四步,创建了两个管道,其中一个管道也被 epoll 管理起来,这个管道是用来唤醒 InputReader 线程。例如当配置发生改变时,这个管道会被用来唤醒 InputReader 线程来处理配置的改变。



另一个管道用于做什么呢?



现在 epoll 已经管理了两个文件描述符,mINotifyFd 和 mWakeReadPipeFd。但是现在并没有启动 epoll 来监听它们的可读事件,这是因为 InputReader 还没有准备好,让我们继续往下看。



本文不想浪费篇幅去介绍 Linux inotify 和 epoll 机制,这两个机制并不复杂,请大家自己去了解。



创建 InputReader


EventHub 已经创建完毕,现在来看下创建 InputReader 的过程


InputReader::InputReader(std::shared_ptr<EventHubInterface> eventHub,
const sp<InputReaderPolicyInterface>& policy,
const sp<InputListenerInterface>& listener)
: mContext(this), // ContextImpl mContext 是一个关于 InputReader 的环境
mEventHub(eventHub),
mPolicy(policy), // 由 NativeInputManager实现
mGlobalMetaState(0),
mGeneration(1),
mNextInputDeviceId(END_RESERVED_ID),
mDisableVirtualKeysTimeout(LLONG_MIN),
mNextTimeout(LLONG_MAX),
mConfigurationChangesToRefresh(0) {
// 1. 创建QueuedInputListener对象
// 事件的转换都是通过 InputListenerInterface 接口
// QueuedInputListener 是一个继承并实现了 InputListenerInterface 接口的代理类
// QueuedInputListener 把事件加入队列,并推迟发送事件直到调用它的flush()函数
mQueuedListener = new QueuedInputListener(listener);

{ // acquire lock
AutoMutex _l(mLock);
// 2. 更新配置,保存到mConfig中
refreshConfigurationLocked(0);
// 根据已经映射的设备,更新 mGlobalMetaState 的值
// 由于目前还没有映射设备,所以mGlobalMetaState值为0
updateGlobalMetaStateLocked();
} // release lock
}

InputReader 构造函数看似平平无奇,实际上有许多值得注意的地方。


首先注意 mContext 变量,它的类型是 ContextImpl,这是一个表示 InputReader 的环境,由于 ContextImpl 是 InputReader 的友元类,因此透过 ContextImpl 可以访问 InputReader 的私有数据。


那么这个 mContext 变量被谁所用呢? InputReader 会为物理输入设备建立一个映射类 InputDevice,这个 InputDevice 就会保存这个 mContext 变量,InputDevice 会通过 mContext,从 InputReader 中获取全局的设备状态以及参数。


InputReader 构造函数使用了一个 InputClassifier 接口对象,由 InputManagerService的创建与启动 可知,InputListenerInterfac 接口的实现类是 InputClassifier。 实际上,InputListenerInterface 接口是专为传递事件设计的。因此只要你看到哪个类实现 ( 在c++中叫继承 ) 了 InputListenerInterface 这个接口,那么它肯定是传递事件中的一环。


mQueuedListener 变量的类型是 QueuedInputListener ,恰好这个类也实现了 InputListenerInterface 接口,那么它肯定也传递事件。然而 QueuedInputListener 只是一个代理类,InputReader 会把事件存储到 QueuedInputListener 的队列中,然后直到 QueuedInputListener::flush() 函数被调用,QueuedInputListener 才把队列中的事件发送出去。发送给谁呢,就是 InputClassifier。


那么现在我们来总结下,事件通过 InputListenerInterface 接口传递的关系图


graph TD
InputReader --> |InputListenerInterface|QueuedInputListener
QueuedInputListener --> |InputListenerInterface|InputClassifier
InputClassifier --> |InputListenerInterface|InputDispatcher

最后,我们来一件挺烦琐的小事,InputReader 读取配置,它调用的是如下代码


// 注意,参数 changes 值为0
void InputReader::refreshConfigurationLocked(uint32_t changes) {
// 从NativeInputManager中获取配置,保存到mConfig中
mPolicy->getReaderConfiguration(&mConfig);
// EventHub保存排除的输入设备
mEventHub->setExcludedDevices(mConfig.excludedDeviceNames);

if (changes) {
// ...
}
}

mPolicy 的实现类是 JNI 层 NativeInputManager,由 InputManagerService的创建与启动 可知, NativeInputManager 只是一个桥梁作用,那么它肯定是向上层的 InputManagerService 获取配置,是不是这样呢,来验证下。


void NativeInputManager::getReaderConfiguration(InputReaderConfiguration* outConfig) {
ATRACE_CALL();
JNIEnv* env = jniEnv();

// 0
jint virtualKeyQuietTime = env->CallIntMethod(mServiceObj,
gServiceClassInfo.getVirtualKeyQuietTimeMillis);
if (!checkAndClearExceptionFromCallback(env, "getVirtualKeyQuietTimeMillis")) {
outConfig->virtualKeyQuietTime = milliseconds_to_nanoseconds(virtualKeyQuietTime);
}

outConfig->excludedDeviceNames.clear();
// 如下两个文件定义了排除的设备
// /system/etc/excluded-input-devices.xml
// /vendor/etc/excluded-input-devices.xml
jobjectArray excludedDeviceNames = jobjectArray(env->CallStaticObjectMethod(
gServiceClassInfo.clazz, gServiceClassInfo.getExcludedDeviceNames));
if (!checkAndClearExceptionFromCallback(env, "getExcludedDeviceNames") && excludedDeviceNames) {
jsize length = env->GetArrayLength(excludedDeviceNames);
for (jsize i = 0; i < length; i++) {
std::string deviceName = getStringElementFromJavaArray(env, excludedDeviceNames, i);
outConfig->excludedDeviceNames.push_back(deviceName);
}
env->DeleteLocalRef(excludedDeviceNames);
}

// Associations between input ports and display ports
// The java method packs the information in the following manner:
// Original data: [{'inputPort1': '1'}, {'inputPort2': '2'}]
// Received data: ['inputPort1', '1', 'inputPort2', '2']
// So we unpack accordingly here.
// 输入端口和显示端口绑定的关系,一种是静态绑定,来自于/vendor/etc/input-port-associations.xml
// 而另一种是来自于运行时的动态绑定,并且动态绑定可以覆盖静态绑定。
outConfig->portAssociations.clear();
jobjectArray portAssociations = jobjectArray(env->CallObjectMethod(mServiceObj,
gServiceClassInfo.getInputPortAssociations));
if (!checkAndClearExceptionFromCallback(env, "getInputPortAssociations") && portAssociations) {
jsize length = env->GetArrayLength(portAssociations);
for (jsize i = 0; i < length / 2; i++) {
std::string inputPort = getStringElementFromJavaArray(env, portAssociations, 2 * i);
std::string displayPortStr =
getStringElementFromJavaArray(env, portAssociations, 2 * i + 1);
uint8_t displayPort;
// Should already have been validated earlier, but do it here for safety.
bool success = ParseUint(displayPortStr, &displayPort);
if (!success) {
ALOGE("Could not parse entry in port configuration file, received: %s",
displayPortStr.c_str());
continue;
}
outConfig->portAssociations.insert({inputPort, displayPort});
}
env->DeleteLocalRef(portAssociations);
}

// 下面这些都与悬浮点击有关系,如果触摸屏支持悬浮点击,可以研究下这些参数
jint hoverTapTimeout = env->CallIntMethod(mServiceObj,
gServiceClassInfo.getHoverTapTimeout);
if (!checkAndClearExceptionFromCallback(env, "getHoverTapTimeout")) {
jint doubleTapTimeout = env->CallIntMethod(mServiceObj,
gServiceClassInfo.getDoubleTapTimeout);
if (!checkAndClearExceptionFromCallback(env, "getDoubleTapTimeout")) {
jint longPressTimeout = env->CallIntMethod(mServiceObj,
gServiceClassInfo.getLongPressTimeout);
if (!checkAndClearExceptionFromCallback(env, "getLongPressTimeout")) {
outConfig->pointerGestureTapInterval = milliseconds_to_nanoseconds(hoverTapTimeout);

// We must ensure that the tap-drag interval is significantly shorter than
// the long-press timeout because the tap is held down for the entire duration
// of the double-tap timeout.
jint tapDragInterval = max(min(longPressTimeout - 100,
doubleTapTimeout), hoverTapTimeout);
outConfig->pointerGestureTapDragInterval =
milliseconds_to_nanoseconds(tapDragInterval);
}
}
}

// 悬浮移动距离
jint hoverTapSlop = env->CallIntMethod(mServiceObj,
gServiceClassInfo.getHoverTapSlop);
if (!checkAndClearExceptionFromCallback(env, "getHoverTapSlop")) {
outConfig->pointerGestureTapSlop = hoverTapSlop;
}

// 如下mLocked的相关参数是在 NativeInputManager 的构造函数中初始化的
// 但是这些参数都是可以通过 InputManagerService 改变的
{ // acquire lock
AutoMutex _l(mLock);

outConfig->pointerVelocityControlParameters.scale = exp2f(mLocked.pointerSpeed
* POINTER_SPEED_EXPONENT);
outConfig->pointerGesturesEnabled = mLocked.pointerGesturesEnabled;

outConfig->showTouches = mLocked.showTouches;

outConfig->pointerCapture = mLocked.pointerCapture;

outConfig->setDisplayViewports(mLocked.viewports);

outConfig->defaultPointerDisplayId = mLocked.pointerDisplayId;

outConfig->disabledDevices = mLocked.disabledInputDevices;
} // release lock
}

从这里可以看出,InputReader 获取配置的方式,是通过 JNI 层的 NativeInputManager 向 Java 层的 InputManagerService 获取的。


但是这些配置并不是不变的,当Java层改变这些配置后,会通过 JNI 层的 NativeInputManager 通知 InputReader ( 注意,不是InputReader线程 ),然后通过 EventHub::wake() 函数通过管道唤醒 InputReader 线程来处理配置改变。这个过程可以在阅读完本文后,自行分析。


启动 InputReader


现在 InputReader 已经创建完毕,让我们继续看下它的启动过程。


InputManagerService的创建与启动 可知,启动 InputReader 的代码如下


status_t InputReader::start() {
if (mThread) {
return ALREADY_EXISTS;
}
// 创建线程并启动
mThread = std::make_unique<InputThread>(
"InputReader", [this]() { loopOnce(); }, [this]() { mEventHub->wake(); });
return OK;
}

InputThread 封装了 c++ 的 Thread 类


class InputThreadImpl : public Thread {
public:
explicit InputThreadImpl(std::function<void()> loop)
: Thread(/* canCallJava */ true), mThreadLoop(loop) {
}

~InputThreadImpl() {}

private:
std::function<void()> mThreadLoop;

bool threadLoop() override {
mThreadLoop();
return true;
}
};

当 InputThread 对象创建的时候,会启动一个线程


InputThread::InputThread(std::string name, std::function<void()> loop, std::function<void()> wake)
: mName(name), mThreadWake(wake) {
mThread = new InputThreadImpl(loop);
mThread->run(mName.c_str(), ANDROID_PRIORITY_URGENT_DISPLAY);
}

线程会循环调用 loopOnce() 函数,也就是 InputThread 构造函数的第二个参数,它的实现函数是 InputReader::loopOnce() 函数。


我们注意到 InputThread 构造函数还有第三个参数,它是在 InputThread 析构函数调用的。


InputThread::~InputThread() {
mThread->requestExit();
// mThreadWake 就是构造函数中的第三个参数
if (mThreadWake) {
mThreadWake();
}
mThread->requestExitAndWait();
}

那么什么时候会调用 InputThread 的析构函数呢,我觉得应该是 system_server 进程挂掉的时候,此时会调用 EventHub::wake() 来唤醒 InputReader 线程,从而退出 InputReader 线程。而这个唤醒的方式,就是使用刚才在 EventHub 中创建的一个管道。


现在来分析下 InputReader::loopOnce() 函数,这里就是 InputReader 线程所做的事


void InputReader::loopOnce() {
int32_t oldGeneration;
int32_t timeoutMillis;
bool inputDevicesChanged = false;
std::vector<InputDeviceInfo> inputDevices;

// 1. 处理配置改变
{ // acquire lock
AutoMutex _l(mLock);
oldGeneration = mGeneration;
timeoutMillis = -1;
uint32_t changes = mConfigurationChangesToRefresh;
if (changes) {
mConfigurationChangesToRefresh = 0;
timeoutMillis = 0;
refreshConfigurationLocked(changes);
} else if (mNextTimeout != LLONG_MAX) { // mNextTimeout 也属于配置
nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);
timeoutMillis = toMillisecondTimeoutDelay(now, mNextTimeout);
}
} // release lock

// 2. 读取事件
size_t count = mEventHub->getEvents(timeoutMillis, mEventBuffer, EVENT_BUFFER_SIZE);

// 3. 处理事件
{ // acquire lock
AutoMutex _l(mLock);
mReaderIsAliveCondition.broadcast();

// 如果读到事件,就处理
if (count) {
processEventsLocked(mEventBuffer, count);
}

// 处理超时情况
if (mNextTimeout != LLONG_MAX) {
// ...
}

// mGeneration 表明输入设备改变
if (oldGeneration != mGeneration) {
inputDevicesChanged = true;
// 对inputDevices填充inputDeviceInfo,而这个InputDeviceInfo是从InputDevice中获取
getInputDevicesLocked(inputDevices);
}
} // release lock

// 4. 通知设备改变
if (inputDevicesChanged) {
// mPolicy实现类为NativeInputManager
mPolicy->notifyInputDevicesChanged(inputDevices);
}

// 5. 事件发送给 InputClassifier。
mQueuedListener->flush();
}

我第一次看到这段代码时,头皮发麻,InputReader 做了这么多事情,我该怎么分析呢?不要紧,让我来梳理下思路。


首先看第一步,这一步是处理配置改变。前面我们谈论过这个话题,当配置发生改变时,一般都通过 Java 层的 InputManagerService 发送信息给 JNI 层的 NativeInputManager ,然后再通知 InputReader (注意不是InputReader线程),InputReader 会通过 EventHub::wake() 函数来唤醒 InputReader 线程来处理配置改变。这就是第一步做的事件。鉴于篇幅原因,这个过程就不分析了。


第二步,从 EventHub 获取数据。这个获取数据的过程其实分三种情况。



  1. 第一种情况,发生在系统首次启动,并且没有输入事件发生,例如手指没有在触摸屏上滑动。EventHub 会扫描输入设备,并建立与输入设备相应的数据结构,然后创建多个 EventHub 自己合成的事件,最后把这些事件返回给 InputReader 线程。为何要扫描设备,前面已经说过,是为了监听输入设备事件。


  2. 第二种情况,发生在系统启动完毕,然后有输入事件,例如手指在触摸屏上滑动。EventHub 会把 /dev/input/ 目录下的设备文件中的原始数据,包装成一个事件,发送给 InputReader 线程处理。


  3. 第三种情况,系统在运行的过程中,发生设备的挂载和卸载,EventHub 也会像第一种情况一样,合成自己的事件,并发送给 InputReader 线程处理。其实第一种情况和第三种情况下,InputReader 线程对事件的处理是类似的。因此后面的文章并不会分析这种情况。



第三步,获取完事件后,就处理这些事件。


第四步,通知监听者,设备发生改变。谁是监听者呢,就是上层的 InputManagerService。


第五步,把InpuReader加工好的事件发送给 InputClassifier。


事件发送关系图


经过本文的分析,我们可以得出一张事件发送关系图,以及各个组件如何通信的关系图


graph TD
EventHub --> |EventHub::getEvent|InputReader
InputReader --> |InputReaderPolicyInterface|NativeInputManager
InputReader --> |InputListenerInterface|InputClassifer
InputClassifer --> |InputListenerInterface|InputDispatcher
InputDispatcher --> |InputDispatcherPolicyInterface|NativeInputManager
NativeInputManager --> |mServiceObj|InputManagerService

结束


简简单单的 InputReader 的创建与启动就分析完了,而本文仅仅是描述了一个轮廓,但是我就问你复杂不复杂?复杂吧,不过没关系,只要我们理清思路,我们就一步一步来。那么下篇文章,我们来分析系统启动时,EventHub 是如何扫描设备并发送合成事件,以及 InputReader 线程是如何处理这些合成事件。


收起阅读 »

Android OpenGL ES 实现抖音传送带特效

抖音 APP 真是个好东西,不过也容易上瘾,老实说你的抖音是不是反复卸载又反复安装了,后来我也发现我的几个 leader 都不刷抖音,这令我挺吃惊的。 我刷抖音主要是为了看新闻,听一些大 V 讲历史,研究抖音的一些算法特效,最重要的是抖音提供了一个年轻人的视...
继续阅读 »

抖音 APP 真是个好东西,不过也容易上瘾,老实说你的抖音是不是反复卸载又反复安装了,后来我也发现我的几个 leader 都不刷抖音,这令我挺吃惊的。


我刷抖音主要是为了看新闻,听一些大 V 讲历史,研究抖音的一些算法特效,最重要的是抖音提供了一个年轻人的视角去观察世界。另外,自己感兴趣的内容看多了,反而训练抖音推送更多类似的优质内容,大家可以反向利用抖音的这一特点。


至于我的 leader 老是强调刷抖音不好,对此我并不完全认同。


实现抖音传送带特效 传送带


抖音传送带特效原理


抖音传送带特效推出已经很长一段时间了,前面也实现了下,最近把它整理出来了,如果你有仔细观测传送带特效,就会发现它的实现原理其实很简单。


传送带原理.png


通过仔细观察抖音的传送带特效,你可以发现左侧是不停地更新预览画面,右侧看起来就是一小格一小格的竖条状图像区域不断地向右移动,一直移动到右侧边界位置。


预览的时候每次拷贝一小块预览区域的图像送到传送带,这就形成了源源不断地向右传送的效果。


原理图进行了简化处理, 实际上右侧的竖条图像更多,效果会更流畅,每来一帧预览图像,首先拷贝更新左侧预览画面,然后从最右侧的竖条图像区域开始拷贝图像(想一想为什么?)。


例如将区域 2 的像素拷贝到区域 3 ,然后将区域 1 的像素拷贝到区域 2,以此类推,最后将来源区域的像素拷贝到区域 0 。


这样就形成了不断传送的效果,最后将拷贝好的图像更新到纹理,利用 OpenGL 渲染到屏幕上。


抖音传送带特效实现


抖音传送带特效实现


上节原理分析时,将图像区域从左侧到右侧拷贝并不高效,可能会导致一些性能问题,好在 Android 相机出图都是横向的(旋转了 90 或 270 度),这样图像区域上下拷贝效率高了很多,最后渲染的时候再将图像旋转回来。


Android 相机出图是 YUV 格式的,这里为了拷贝处理方便,先使用 OpenCV 将 YUV 图像转换为 RGBA 格式,当然为了追求性能直接使用 YUV 格式的图像问题也不大。


cv::Mat mati420 = cv::Mat(pImage->height * 3 / 2, pImage->width, CV_8UC1, pImage->ppPlane[0]);
cv::Mat matRgba = cv::Mat(m_SrcImage.height, m_SrcImage.width, CV_8UC4, m_SrcImage.ppPlane[0]);
cv::cvtColor(mati420, matRgba, CV_YUV2RGBA_I420);

用到的着色器程序就是简单的贴图:


#version 300 es
layout(location = 0) in vec4 a_position;
layout(location = 1) in vec2 a_texCoord;
uniform mat4 u_MVPMatrix;
out vec2 v_texCoord;
void main()
{
gl_Position = u_MVPMatrix * a_position;
v_texCoord = a_texCoord;
}

#version 300 es
precision mediump float;
in vec2 v_texCoord;
layout(location = 0) out vec4 outColor;
uniform sampler2D u_texture;

void main()
{
outColor = texture(u_texture, v_texCoord);
}

传送带的核心就是图像拷贝操作:


memcpy(m_RenderImage.ppPlane[0], m_SrcImage.ppPlane[0], m_RenderImage.width * m_RenderImage.height * 4 / 2); //左侧预览区域像素拷贝

int bannerHeight = m_RenderImage.height / 2 / m_bannerNum;//一个 banner 的高(小竖条)
int bannerPixelsBufSize = m_RenderImage.width * bannerHeight * 4;//一个 banner 占用的图像内存

uint8 *pBuf = m_RenderImage.ppPlane[0] + m_RenderImage.width * m_RenderImage.height * 4 / 2; //传送带分界线

//从最右侧的竖条图像区域开始拷贝图像
for (int i = m_bannerNum - 1; i >= 1; --i) {
memcpy(pBuf + i*bannerPixelsBufSize, pBuf + (i - 1)*bannerPixelsBufSize, bannerPixelsBufSize);
}

//将来源区域的像素拷贝到竖条图像区域 0
memcpy(pBuf, pBuf - bannerPixelsBufSize, bannerPixelsBufSize);

渲染操作:


glUseProgram (m_ProgramObj);

glBindVertexArray(m_VaoId);

glUniformMatrix4fv(m_MVPMatLoc, 1, GL_FALSE, &m_MVPMatrix[0][0]);

//图像拷贝,传送带拷贝
memcpy(m_RenderImage.ppPlane[0], m_SrcImage.ppPlane[0], m_RenderImage.width * m_RenderImage.height * 4 / 2);
int bannerHeight = m_RenderImage.height / 2 / m_bannerNum;
int bannerPixelsBufSize = m_RenderImage.width * bannerHeight * 4;

uint8 *pBuf = m_RenderImage.ppPlane[0] + m_RenderImage.width * m_RenderImage.height * 4 / 2; //传送带分界线

for (int i = m_bannerNum - 1; i >= 1; --i) {
memcpy(pBuf + i*bannerPixelsBufSize, pBuf + (i - 1)*bannerPixelsBufSize, bannerPixelsBufSize);
}
memcpy(pBuf, pBuf - bannerPixelsBufSize, bannerPixelsBufSize);

//更新纹理
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, m_TextureId);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, m_RenderImage.width, m_RenderImage.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, m_RenderImage.ppPlane[0]);
glBindTexture(GL_TEXTURE_2D, GL_NONE);

glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, m_TextureId);
GLUtils::setInt(m_ProgramObj, "u_texture", 0);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, (const void *)0);
glBindVertexArray(GL_NONE);

详细实现代码见项目:github.com/githubhaoha…

收起阅读 »

自动化检测 Android APP 非 SDK 接口使用,防止非预期异常发生!

背景 从 Android 9(API 级别 28)开始,Android 平台对应用能使用的非 SDK 接口实施了限制,只要应用引用非 SDK 接口或尝试使用反射或 JNI 来获取其句柄,这些限制就适用,这些限制旨在帮助提升用户体验和开发者体验,为用户降低应用...
继续阅读 »

背景


从 Android 9(API 级别 28)开始,Android 平台对应用能使用的非 SDK 接口实施了限制,只要应用引用非 SDK 接口或尝试使用反射或 JNI 来获取其句柄,这些限制就适用,这些限制旨在帮助提升用户体验和开发者体验,为用户降低应用发生崩溃的风险,同时为开发者降低紧急发布的风险。


区分 SDK 接口和非 SDK 接口


一般而言,公共 SDK 接口是在 Android 框架软件包索引中记录的那些接口,非 SDK 接口的处理是 API 抽象出来的实现细节,因此这些接口可能会在不另行通知的情况下随时发生更改。 


为了避免发生崩溃和意外行为,应用应仅使用 SDK 中经过正式记录的类,这也意味着当您的应用通过反射等机制与类互动时,不应访问 SDK 中未列出的方法或字段。


非 SDK API 名单


为最大程度地降低非 SDK 使用限制对开发工作流的影响,Google 将非 SDK 接口分成了几个名单,这些名单界定了非 SDK 接口使用限制的严格程度(取决于应用的目标 API 级别):



  • greylist 无限制,可以正常使用

  • blacklist 无论什么版本的手机系统,使用这些api,系统将会抛出异常

  • greylist-max-o 受限制的灰名单,APP运行在 版本<=8.0的系统里 可以正常访问,targetSDK>8.0且运行在>8.0的手机会抛出异常

  • greylist-max-p 受限制的灰名单,APP运行在 版本<=9.0的系统里 可以正常访问,targetSDK>9.0且运行在>9.0的手机会抛出异常

  • greylist-max-q 受限制的灰名单,受限制的灰名单。APP运行在 版本<=10.0的系统里 可以正常访问,targetSDK>10.0且运行在>10.0的手机会抛出异常


测试你的应用是否使用了非 SDK 接口


这里我们通过veridex工具进行测试,veridex 工具会扫描 APK 的整个代码库(包括所有第三方库),并报告发现的所有使用非 SDK 接口的行为。


不过veridex 工具存在以下局限性:



  • 它无法检测到通过 JNI 实现的调用

  • 它只能检测到一部分通过反射实现的调用

  • 它对非活动代码路径的分析仅限于 API 级别的检查

  • 它只能在支持 SSE4.2 和 POPCNT 指令的机器上运行


我们以Mac系统为例,首先我们需要下载veridex 工具:android.googlesource.com/platform/pr…


然后解压缩 appcompat.tar.gz 文件的内容,在解压缩的文件夹中,找到 veridex-mac.zip 文件并将其解压缩,转到解压缩的文件夹,然后运行下面的命令,其中 /path-from-root/your-app.apk 是你要测试的 APK 的路径,从系统的根目录开始:


./appcompat.sh --dex-file=/path-from-root/your-app.apk

文件夹中的hiddenapi-flags.csv文件是需要根据targetAPI版本来更新的,不同的版本会有不同的检查清单,具体可参考:


https://developer.android.google.cn/distribute/best-practices/develop/restrictions-non-sdk-interfaces#determine-list

报告


生成的报告如下图,我们主要关注红框部分的内容就可以了,如果存在blacklist的接口一定是需要修复的:


图片

收起阅读 »

【Flutter 组件集录】SizedBox

一、认识 SizedBox 组件 源码中对 SizedBox 的介绍为:一个指定尺寸的盒子。那 SizedBox 为什么可以限定尺寸?背后区域限定的原理又是什么? 本文通过 SizedBox 来一窥布局约束奥秘的冰山一角。 1.SizedBox 基...
继续阅读 »
一、认识 SizedBox 组件

源码中对 SizedBox 的介绍为:一个指定尺寸的盒子。那 SizedBox 为什么可以限定尺寸?背后区域限定的原理又是什么? 本文通过 SizedBox 来一窥布局约束奥秘的冰山一角。





1.SizedBox 基本信息

下面是 SizedBox 组件类的定义构造方法,可以看出它继承自 SingleChildRenderObjectWidget。可接受一个子组件,和区域的宽高。





2.SizedBox 的使用

如下,是一个 100*50SizedBox ,通过 ColoredBox 涂上蓝色,效果如下:



SizedBox(
width: 100,
height: 50,
child: ColoredBox(
color: Colors.blue.withAlpha(88)
),
),



3.区域分析

乍一看,不就是一个组件提供宽高来设置尺寸吗,似乎并没有什么好延伸的。但你有没有想过,为什么 SizedBox 有权力决定尺寸大小?它决定的区域一定有效吗?在分析之前,先了解一些前置知识:


任何组件的占位区域绘制内容最终都取决于 RenderObject 。而并非所有的组件都和 RenderObject 有关,只有 RenderObjectWidget 负责维护 RenderObject 。像 StatelessWidgetStatefulWidget 这种都是基于已有组件进行组合,往深层去看,他们都是基于某些 RenderObjectWidget 实现。


关于布局, RenderObject 有一个非常重要的属性: Constraints 类型的 constraints ,表示自身受到的区域约束限制。而 RenderBox 作为 RenderObject 的子类,拓展出了 size 的概念,绝大多数组件维护的渲染对象都是在 RenderBox 基础上进行拓展的。


下面来打开组件树,一起来看一下:



上面的 SizedBox 组件,它维护的 RenderObjectRenderConstrainedBox ,自身的约束为 [w(0,800) - h(0,600)] ,也就说明该渲染对象的大小必须在这此区间内。然后它会给子组件施加一个额外的约束 [w(100,100) - h(50,50)]


这样对于 ColoredBox 对应的渲染对象 _RenderColoredBox ,由于父级施加的额外约束,自身的约束也就变成 [w(100,100) - h(50,50)] 。也就说明该渲染对象的大小必须在这此区间内,即 _RenderColoredBox 的尺寸被限定为 (100,50)


_RenderColoredBoxsize 确定后,RenderConstrainedBox 会根据自身的约束和子节点的尺寸来确定自身的尺寸。这就是 SizedBox 的工作原理。




4、约束测试

为了更好地说明约束的作用,这里进行一下测试,在之前的案例的 SizedBox 外层通过 ConstrainedBox 组件添加添加一个 [w(20,20) - h(20,20)] 的强制约束。可以看出即使 SizedBox 设置了固定的宽高,但是在外层的约束之下,会优先满足父级约束。


[推论1] SizedBox 的最终尺寸会受到父级约束的影响,并非一定为指定值。


ConstrainedBox(
constraints: BoxConstraints(
minWidth: 20,
maxWidth: 20,
maxHeight: 20,
minHeight: 20,
),
child: SizedBox(
width: 100,
height: 50,
child: ColoredBox(color: Colors.blue.withAlpha(88)),
),
);



我们再来看一下此时的组件树:
可以看出 SizedBox 维护的 RenderConstrainedBox 本身的约束区域为 [w(20,20) - h(20,20)] ,为子节点施加的额外约束为 [w(100,100) - h(50,50)] 。在 ColoredBox 维护的 _RenderColoredBox 中,约束区域为 [w(20,20) - h(20,20)] ,这也就觉得了其尺寸为 (20,20)



这样可以看出,渲染对象对子节点施加的额外约束 ,并不会完全作用于子节点。还会根据自身的约束情况,来确定子组件的最终约束。




三、SizedBox 的源码分析


SizedBox 继承自 SingleChildRenderObjectWidget ,就说明它需要维护一个 RenderObject 来实现功能。





在前面我们通过组件树可以看出,它维护的渲染对象是 RenderConstrainedBox 。从源码中可以看出, RenderConstrainedBox 构造时需要传入一个约束对象 BoxConstraints 。这里通过 BoxConstraints.tightFor 构造使用 widthheight 创建一个紧约束。



通过源码可以看出,这个构造的约束为: [w(width,width) - h(height,height)],也就是固定宽高约束。





SizedBox 除了普通构造之外,还有三个命名构造。如果已经了解上面的用法,那这三个也非常简单,都逃离不了对宽高的初始化。比如 .expand 会创建一个无限的约束,这样由于 推论1 ,其约束的尺寸就可以在父级的约束下,尽可能的大 。 .shrink 就是一个 [w(0,0) - h(0,0)]的限制,同理,会在父级的约束下,尽可能的小。



至于 RenderConstrainedBox 渲染对象的实现,将在后面的 ConstrainedBox 一文中进行介绍,毕竟 RenderConstrainedBox 的本命是 ConstrainedBox 。通过本文,你应该对 SizedBox 有了更深的认识,对布局约束、尺寸确定也认识了九牛一毛 。那本文到这里就结束了,谢谢观看,明天见~

收起阅读 »

iOS 专用图层 二

6.2 CATextLayer用户界面是无法从一个单独的图片里面构建的。一个设计良好的图标能够很好地表现一个按钮或控件的意图,不过你迟早都要需要一个不错的老式风格的文本标签。如果你想在一个图层里面显示文字,完全可以借助图层代理直接将字符串使用Core Grap...
继续阅读 »

6.2 CATextLayer

用户界面是无法从一个单独的图片里面构建的。一个设计良好的图标能够很好地表现一个按钮或控件的意图,不过你迟早都要需要一个不错的老式风格的文本标签。

如果你想在一个图层里面显示文字,完全可以借助图层代理直接将字符串使用Core Graphics写入图层的内容(这就是UILabel的精髓)。如果越过寄宿于图层的视图,直接在图层上操作,那其实相当繁琐。你要为每一个显示文字的图层创建一个能像图层代理一样工作的类,还要逻辑上判断哪个图层需要显示哪个字符串,更别提还要记录不同的字体,颜色等一系列乱七八糟的东西。

万幸的是这些都是不必要的,Core Animation提供了一个CALayer的子类CATextLayer,它以图层的形式包含了UILabel几乎所有的绘制特性,并且额外提供了一些新的特性。

同样,CATextLayer也要比UILabel渲染得快得多。很少有人知道在iOS 6及之前的版本,UILabel其实是通过WebKit来实现绘制的,这样就造成了当有很多文字的时候就会有极大的性能压力。而CATextLayer使用了Core text,并且渲染得非常快。

让我们来尝试用CATextLayer来显示一些文字。清单6.2的代码实现了这一功能,结果如图6.2所示。

清单6.2 用CATextLayer来实现一个UILabel

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *labelView;

@end

@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];

//create a text layer
CATextLayer *textLayer = [CATextLayer layer];
textLayer.frame = self.labelView.bounds;
[self.labelView.layer addSublayer:textLayer];

//set text attributes
textLayer.foregroundColor = [UIColor blackColor].CGColor;
textLayer.alignmentMode = kCAAlignmentJustified;
textLayer.wrapped = YES;

//choose a font
UIFont *font = [UIFont systemFontOfSize:15];

//set layer font
CFStringRef fontName = (__bridge CFStringRef)font.fontName;
CGFontRef fontRef = CGFontCreateWithFontName(fontName);
textLayer.font = fontRef;
textLayer.fontSize = font.pointSize;
CGFontRelease(fontRef);

//choose some text
NSString *text = @"Lorem ipsum dolor sit amet, consectetur adipiscing \ elit. Quisque massa arcu, eleifend vel varius in, facilisis pulvinar \ leo. Nunc quis nunc at mauris pharetra condimentum ut ac neque. Nunc elementum, libero ut porttitor dictum, diam odio congue lacus, vel \ fringilla sapien diam at purus. Etiam suscipit pretium nunc sit amet \ lobortis";

//set layer text
textLayer.string = text;
}
@end

图6.2

图6.2 用CATextLayer来显示一个纯文本标签

如果你仔细看这个文本,你会发现一个奇怪的地方:这些文本有一些像素化了。这是因为并没有以Retina的方式渲染,第二章提到了这个contentScale属性,用来决定图层内容应该以怎样的分辨率来渲染。contentsScale并不关心屏幕的拉伸因素而总是默认为1.0。如果我们想以Retina的质量来显示文字,我们就得手动地设置CATextLayercontentsScale属性,如下:

textLayer.contentsScale = [UIScreen mainScreen].scale;

这样就解决了这个问题(如图6.3)

图6.3

图6.3 设置contentsScale来匹配屏幕

CATextLayerfont属性不是一个UIFont类型,而是一个CFTypeRef类型。这样可以根据你的具体需要来决定字体属性应该是用CGFontRef类型还是CTFontRef类型(Core Text字体)。同时字体大小也是用fontSize属性单独设置的,因为CTFontRefCGFontRef并不像UIFont一样包含点大小。这个例子会告诉你如何将UIFont转换成CGFontRef

另外,CATextLayerstring属性并不是你想象的NSString类型,而是id类型。这样你既可以用NSString也可以用NSAttributedString来指定文本了(注意,NSAttributedString并不是NSString的子类)。属性化字符串是iOS用来渲染字体风格的机制,它以特定的方式来决定指定范围内的字符串的原始信息,比如字体,颜色,字重,斜体等。

富文本

iOS 6中,Apple给UILabel和其他UIKit文本视图添加了直接的属性化字符串的支持,应该说这是一个很方便的特性。不过事实上从iOS3.2开始CATextLayer就已经支持属性化字符串了。这样的话,如果你想要支持更低版本的iOS系统,CATextLayer无疑是你向界面中增加富文本的好办法,而且也不用去跟复杂的Core Text打交道,也省了用UIWebView的麻烦。

让我们编辑一下示例使用到NSAttributedString(见清单6.3).iOS 6及以上我们可以用新的NSTextAttributeName实例来设置我们的字符串属性,但是练习的目的是为了演示在iOS 5及以下,所以我们用了Core Text,也就是说你需要把Core Text framework添加到你的项目中。否则,编译器是无法识别属性常量的。

图6.4是代码运行结果(注意那个红色的下划线文本)

清单6.3 用NSAttributedString实现一个富文本标签。

#import "DrawingView.h"
#import
#import

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *labelView;

@end

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad];

//create a text layer
CATextLayer *textLayer = [CATextLayer layer];
textLayer.frame = self.labelView.bounds;
textLayer.contentsScale = [UIScreen mainScreen].scale;
[self.labelView.layer addSublayer:textLayer];

//set text attributes
textLayer.alignmentMode = kCAAlignmentJustified;
textLayer.wrapped = YES;

//choose a font
UIFont *font = [UIFont systemFontOfSize:15];

//choose some text
NSString *text = @"Lorem ipsum dolor sit amet, consectetur adipiscing \ elit. Quisque massa arcu, eleifend vel varius in, facilisis pulvinar \ leo. Nunc quis nunc at mauris pharetra condimentum ut ac neque. Nunc \ elementum, libero ut porttitor dictum, diam odio congue lacus, vel \ fringilla sapien diam at purus. Etiam suscipit pretium nunc sit amet \ lobortis";

//create attributed string
NSMutableAttributedString *string = nil;
string = [[NSMutableAttributedString alloc] initWithString:text];

//convert UIFont to a CTFont
CFStringRef fontName = (__bridge CFStringRef)font.fontName;
CGFloat fontSize = font.pointSize;
CTFontRef fontRef = CTFontCreateWithName(fontName, fontSize, NULL);

//set text attributes
NSDictionary *attribs = @{
(__bridge id)kCTForegroundColorAttributeName:(__bridge id)[UIColor blackColor].CGColor,
(__bridge id)kCTFontAttributeName: (__bridge id)fontRef
};

[string setAttributes:attribs range:NSMakeRange(0, [text length])];
attribs = @{
(__bridge id)kCTForegroundColorAttributeName: (__bridge id)[UIColor redColor].CGColor,
(__bridge id)kCTUnderlineStyleAttributeName: @(kCTUnderlineStyleSingle),
(__bridge id)kCTFontAttributeName: (__bridge id)fontRef
};
[string setAttributes:attribs range:NSMakeRange(6, 5)];

//release the CTFont we created earlier
CFRelease(fontRef);

//set layer text
textLayer.string = string;
}
@end

图6.4

图6.4 用CATextLayer实现一个富文本标签。

行距和字距

有必要提一下的是,由于绘制的实现机制不同(Core Text和WebKit),用CATextLayer渲染和用UILabel渲染出的文本行距和字距也不是不尽相同的。

二者的差异程度(由使用的字体和字符决定)总的来说挺小,但是如果你想正确的显示普通便签和CATextLayer就一定要记住这一点。

UILabel的替代品

我们已经证实了CATextLayerUILabel有着更好的性能表现,同时还有额外的布局选项并且在iOS 5上支持富文本。但是与一般的标签比较而言会更加繁琐一些。如果我们真的在需求一个UILabel的可用替代品,最好是能够在Interface Builder上创建我们的标签,而且尽可能地像一般的视图一样正常工作。

我们应该继承UILabel,然后添加一个子图层CATextLayer并重写显示文本的方法。但是仍然会有由UILabel-drawRect:方法创建的空寄宿图。而且由于CALayer不支持自动缩放和自动布局,子视图并不是主动跟踪视图边界的大小,所以每次视图大小被更改,我们不得不手动更新子图层的边界。

我们真正想要的是一个用CATextLayer作为宿主图层的UILabel子类,这样就可以随着视图自动调整大小而且也没有冗余的寄宿图啦。

就像我们在第一章『图层树』讨论的一样,每一个UIView都是寄宿在一个CALayer的示例上。这个图层是由视图自动创建和管理的,那我们可以用别的图层类型替代它么?一旦被创建,我们就无法代替这个图层了。但是如果我们继承了UIView,那我们就可以重写+layerClass方法使得在创建的时候能返回一个不同的图层子类。UIView会在初始化的时候调用+layerClass方法,然后用它的返回类型来创建宿主图层。

清单6.4 演示了一个UILabel子类LayerLabelCATextLayer绘制它的问题,而不是调用一般的UILabel使用的较慢的-drawRect:方法。LayerLabel示例既可以用代码实现,也可以在Interface Builder实现,只要把普通的标签拖入视图之中,然后设置它的类是LayerLabel就可以了。

清单6.4 使用CATextLayerUILabel子类:LayerLabel

#import "LayerLabel.h"
#import

@implementation LayerLabel
+ (Class)layerClass
{
//this makes our label create a CATextLayer //instead of a regular CALayer for its backing layer
return [CATextLayer class];
}

- (CATextLayer *)textLayer
{
return (CATextLayer *)self.layer;
}

- (void)setUp
{
//set defaults from UILabel settings
self.text = self.text;
self.textColor = self.textColor;
self.font = self.font;

//we should really derive these from the UILabel settings too
//but that's complicated, so for now we'll just hard-code them
[self textLayer].alignmentMode = kCAAlignmentJustified;

[self textLayer].wrapped = YES;
[self.layer display];
}

- (id)initWithFrame:(CGRect)frame
{
//called when creating label programmatically
if (self = [super initWithFrame:frame]) {
[self setUp];
}
return self;
}

- (void)awakeFromNib
{
//called when creating label using Interface Builder
[self setUp];
}

- (void)setText:(NSString *)text
{
super.text = text;
//set layer text
[self textLayer].string = text;
}

- (void)setTextColor:(UIColor *)textColor
{
super.textColor = textColor;
//set layer text color
[self textLayer].foregroundColor = textColor.CGColor;
}

- (void)setFont:(UIFont *)font
{
super.font = font;
//set layer font
CFStringRef fontName = (__bridge CFStringRef)font.fontName;
CGFontRef fontRef = CGFontCreateWithFontName(fontName);
[self textLayer].font = fontRef;
[self textLayer].fontSize = font.pointSize;

CGFontRelease(fontRef);
}
@end

如果你运行代码,你会发现文本并没有像素化,而我们也没有设置contentsScale属性。把CATextLayer作为宿主图层的另一好处就是视图自动设置了contentsScale属性。

在这个简单的例子中,我们只是实现了UILabel的一部分风格和布局属性,不过稍微再改进一下我们就可以创建一个支持UILabel所有功能甚至更多功能的LayerLabel类(你可以在一些线上的开源项目中找到)。

如果你打算支持iOS 6及以上,基于CATextLayer的标签可能就有有些局限性。但是总得来说,如果想在app里面充分利用CALayer子类,用+layerClass来创建基于不同图层的视图是一个简单可复用的方法。

收起阅读 »

iOS 专用图层 一

专用图层复杂的组织都是专门化的Catharine R. Stimpson到目前为止,我们已经探讨过CALayer类了,同时我们也了解到了一些非常有用的绘图和动画功能。但是Core Animation图层不仅仅能作用于图片和颜色而已。本章就会学习其他的一些图层类...
继续阅读 »

专用图层

复杂的组织都是专门化的

Catharine R. Stimpson

到目前为止,我们已经探讨过CALayer类了,同时我们也了解到了一些非常有用的绘图和动画功能。但是Core Animation图层不仅仅能作用于图片和颜色而已。本章就会学习其他的一些图层类,进一步扩展使用Core Animation绘图的能力。

6.1 CAShapeLayer

在第四章『视觉效果』我们学习到了不使用图片的情况下用CGPath去构造任意形状的阴影。如果我们能用同样的方式创建相同形状的图层就好了。

CAShapeLayer是一个通过矢量图形而不是bitmap来绘制的图层子类。你指定诸如颜色和线宽等属性,用CGPath来定义想要绘制的图形,最后CAShapeLayer就自动渲染出来了。当然,你也可以用Core Graphics直接向原始的CALyer的内容中绘制一个路径,相比直下,使用CAShapeLayer有以下一些优点:

  • 渲染快速。CAShapeLayer使用了硬件加速,绘制同一图形会比用Core Graphics快很多。
  • 高效使用内存。一个CAShapeLayer不需要像普通CALayer一样创建一个寄宿图形,所以无论有多大,都不会占用太多的内存。
  • 不会被图层边界剪裁掉。一个CAShapeLayer可以在边界之外绘制。你的图层路径不会像在使用Core Graphics的普通CALayer一样被剪裁掉(如我们在第二章所见)。
  • 不会出现像素化。当你给CAShapeLayer做3D变换时,它不像一个有寄宿图的普通图层一样变得像素化。

创建一个CGPath

CAShapeLayer可以用来绘制所有能够通过CGPath来表示的形状。这个形状不一定要闭合,图层路径也不一定要不可破,事实上你可以在一个图层上绘制好几个不同的形状。你可以控制一些属性比如lineWith(线宽,用点表示单位),lineCap(线条结尾的样子),和lineJoin(线条之间的结合点的样子);但是在图层层面你只有一次机会设置这些属性。如果你想用不同颜色或风格来绘制多个形状,就不得不为每个形状准备一个图层了。

清单6.1 的代码用一个CAShapeLayer渲染一个简单的火柴人。CAShapeLayer属性是CGPathRef类型,但是我们用UIBezierPath帮助类创建了图层路径,这样我们就不用考虑人工释放CGPath了。图6.1是代码运行的结果。虽然还不是很完美,但是总算知道了大意对吧!

清单6.1 用CAShapeLayer绘制一个火柴人

#import "DrawingView.h"
#import

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *containerView;

@end

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad];
//create path
UIBezierPath *path = [[UIBezierPath alloc] init];
[path moveToPoint:CGPointMake(175, 100)];

[path addArcWithCenter:CGPointMake(150, 100) radius:25 startAngle:0 endAngle:2*M_PI clockwise:YES];
[path moveToPoint:CGPointMake(150, 125)];
[path addLineToPoint:CGPointMake(150, 175)];
[path addLineToPoint:CGPointMake(125, 225)];
[path moveToPoint:CGPointMake(150, 175)];
[path addLineToPoint:CGPointMake(175, 225)];
[path moveToPoint:CGPointMake(100, 150)];
[path addLineToPoint:CGPointMake(200, 150)];

//create shape layer
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.strokeColor = [UIColor redColor].CGColor;
shapeLayer.fillColor = [UIColor clearColor].CGColor;
shapeLayer.lineWidth = 5;
shapeLayer.lineJoin = kCALineJoinRound;
shapeLayer.lineCap = kCALineCapRound;
shapeLayer.path = path.CGPath;
//add it to our view
[self.containerView.layer addSublayer:shapeLayer];
}
@end

图6.1

图6.1 用CAShapeLayer绘制一个简单的火柴人

圆角

第二章里面提到了CAShapeLayer为创建圆角视图提供了一个方法,就是CALayercornerRadius属性(译者注:其实是在第四章提到的)。虽然使用CAShapeLayer类需要更多的工作,但是它有一个优势就是可以单独指定每个角。

我们创建圆角矩形其实就是人工绘制单独的直线和弧度,但是事实上UIBezierPath有自动绘制圆角矩形的构造方法,下面这段代码绘制了一个有三个圆角一个直角的矩形:

//define path parameters
CGRect rect = CGRectMake(50, 50, 100, 100);
CGSize radii = CGSizeMake(20, 20);
UIRectCorner corners = UIRectCornerTopRight | UIRectCornerBottomRight | UIRectCornerBottomLeft;
//create path
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect byRoundingCorners:corners cornerRadii:radii];

我们可以通过这个图层路径绘制一个既有直角又有圆角的视图。如果我们想依照此图形来剪裁视图内容,我们可以把CAShapeLayer作为视图的宿主图层,而不是添加一个子视图(图层蒙板的详细解释见第四章『视觉效果』)。

收起阅读 »

iOS 变化 三

5.3 固体对象现在你懂得了在3D空间的一些图层布局的基础,我们来试着创建一个固态的3D对象(实际上是一个技术上所谓的空洞对象,但它以固态呈现)。我们用六个独立的视图来构建一个立方体的各个面。在这个例子中,我们用Interface Builder来构建立方体的...
继续阅读 »

5.3 固体对象

现在你懂得了在3D空间的一些图层布局的基础,我们来试着创建一个固态的3D对象(实际上是一个技术上所谓的空洞对象,但它以固态呈现)。我们用六个独立的视图来构建一个立方体的各个面。

在这个例子中,我们用Interface Builder来构建立方体的面(图5.19),我们当然可以用代码来写,但是用Interface Builder的好处是可以方便的在每一个面上添加子视图。记住这些面仅仅是包含视图和控件的普通的用户界面元素,它们完全是我们界面交互的部分,并且当把它折成一个立方体之后也不会改变这个性质。

图5.19

图5.19 用Interface Builder对立方体的六个面进行布局

这些面视图并没有放置在主视图当中,而是松散地排列在根nib文件里面。我们并不关心在这个容器中如何摆放它们的位置,因为后续将会用图层的transform对它们进行重新布局,并且用Interface Builder在容器视图之外摆放他们可以让我们容易看清楚它们的内容,如果把它们一个叠着一个都塞进主视图,将会变得很难看。

我们把一个有颜色的UILabel放置在视图内部,是为了清楚的辨别它们之间的关系,并且UIButton被放置在第三个面视图里面,后面会做简单的解释。

具体把视图组织成立方体的代码见清单5.9,结果见图5.20

清单5.9 创建一个立方体

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *containerView;
@property (nonatomic, strong) IBOutletCollection(UIView) NSArray *faces;

@end

@implementation ViewController

- (void)addFace:(NSInteger)index withTransform:(CATransform3D)transform
{
//get the face view and add it to the container
UIView *face = self.faces[index];
[self.containerView addSubview:face];
//center the face view within the container
CGSize containerSize = self.containerView.bounds.size;
face.center = CGPointMake(containerSize.width / 2.0, containerSize.height / 2.0);
// apply the transform
face.layer.transform = transform;
}

- (void)viewDidLoad
{
[super viewDidLoad];
//set up the container sublayer transform
CATransform3D perspective = CATransform3DIdentity;
perspective.m34 = -1.0 / 500.0;
self.containerView.layer.sublayerTransform = perspective;
//add cube face 1
CATransform3D transform = CATransform3DMakeTranslation(0, 0, 100);
[self addFace:0 withTransform:transform];
//add cube face 2
transform = CATransform3DMakeTranslation(100, 0, 0);
transform = CATransform3DRotate(transform, M_PI_2, 0, 1, 0);
[self addFace:1 withTransform:transform];
//add cube face 3
transform = CATransform3DMakeTranslation(0, -100, 0);
transform = CATransform3DRotate(transform, M_PI_2, 1, 0, 0);
[self addFace:2 withTransform:transform];
//add cube face 4
transform = CATransform3DMakeTranslation(0, 100, 0);
transform = CATransform3DRotate(transform, -M_PI_2, 1, 0, 0);
[self addFace:3 withTransform:transform];
//add cube face 5
transform = CATransform3DMakeTranslation(-100, 0, 0);
transform = CATransform3DRotate(transform, -M_PI_2, 0, 1, 0);
[self addFace:4 withTransform:transform];
//add cube face 6
transform = CATransform3DMakeTranslation(0, 0, -100);
transform = CATransform3DRotate(transform, M_PI, 0, 1, 0);
[self addFace:5 withTransform:transform];
}

@end

图5.20

图5.20 正面朝上的立方体

从这个角度看立方体并不是很明显;看起来只是一个方块,为了更好地欣赏它,我们将更换一个不同的视角

旋转这个立方体将会显得很笨重,因为我们要单独对每个面做旋转。另一个简单的方案是通过调整容器视图的sublayerTransform去旋转照相机

添加如下几行去旋转containerView图层的perspective变换矩阵:

perspective = CATransform3DRotate(perspective, -M_PI_4, 1, 0, 0); 
perspective = CATransform3DRotate(perspective, -M_PI_4, 0, 1, 0);

这就对相机(或者相对相机的整个场景,你也可以这么认为)绕Y轴旋转45度,并且绕X轴旋转45度。现在从另一个角度去观察立方体,就能看出它的真实面貌(图5.21)。

图5.21

图5.21 从一个边角观察的立方体

光亮和阴影

现在它看起来更像是一个立方体没错了,但是对每个面之间的连接还是很难分辨。Core Animation可以用3D显示图层,但是它对光线并没有概念。如果想让立方体看起来更加真实,需要自己做一个阴影效果。你可以通过改变每个面的背景颜色或者直接用带光亮效果的图片来调整。

如果需要动态地创建光线效果,你可以根据每个视图的方向应用不同的alpha值做出半透明的阴影图层,但为了计算阴影图层的不透明度,你需要得到每个面的正太向量(垂直于表面的向量),然后根据一个想象的光源计算出两个向量叉乘结果。叉乘代表了光源和图层之间的角度,从而决定了它有多大程度上的光亮。

清单5.10实现了这样一个结果,我们用GLKit框架来做向量的计算(你需要引入GLKit库来运行代码),每个面的CATransform3D都被转换成GLKMatrix4,然后通过GLKMatrix4GetMatrix3函数得出一个3×3的旋转矩阵。这个旋转矩阵指定了图层的方向,然后可以用它来得到正太向量的值。

结果如图5.22所示,试着调整LIGHT_DIRECTIONAMBIENT_LIGHT的值来切换光线效果

清单5.10 对立方体的表面应用动态的光线效果

#import "ViewController.h" 
#import
#import

#define LIGHT_DIRECTION 0, 1, -0.5
#define AMBIENT_LIGHT 0.5

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *containerView;
@property (nonatomic, strong) IBOutletCollection(UIView) NSArray *faces;

@end

@implementation ViewController

- (void)applyLightingToFace:(CALayer *)face
{
//add lighting layer
CALayer *layer = [CALayer layer];
layer.frame = face.bounds;
[face addSublayer:layer];
//convert the face transform to matrix
//(GLKMatrix4 has the same structure as CATransform3D)
//译者注:GLKMatrix4和CATransform3D内存结构一致,但坐标类型有长度区别,所以理论上应该做一次float到CGFloat的转换,感谢[@zihuyishi](https://github.com/zihuyishi)同学~
CATransform3D transform = face.transform;
GLKMatrix4 matrix4 = *(GLKMatrix4 *)&transform;
GLKMatrix3 matrix3 = GLKMatrix4GetMatrix3(matrix4);
//get face normal
GLKVector3 normal = GLKVector3Make(0, 0, 1);
normal = GLKMatrix3MultiplyVector3(matrix3, normal);
normal = GLKVector3Normalize(normal);
//get dot product with light direction
GLKVector3 light = GLKVector3Normalize(GLKVector3Make(LIGHT_DIRECTION));
float dotProduct = GLKVector3DotProduct(light, normal);
//set lighting layer opacity
CGFloat shadow = 1 + dotProduct - AMBIENT_LIGHT;
UIColor *color = [UIColor colorWithWhite:0 alpha:shadow];
layer.backgroundColor = color.CGColor;
}

- (void)addFace:(NSInteger)index withTransform:(CATransform3D)transform
{
//get the face view and add it to the container
UIView *face = self.faces[index];
[self.containerView addSubview:face];
//center the face view within the container
CGSize containerSize = self.containerView.bounds.size;
face.center = CGPointMake(containerSize.width / 2.0, containerSize.height / 2.0);
// apply the transform
face.layer.transform = transform;
//apply lighting
[self applyLightingToFace:face.layer];
}

- (void)viewDidLoad
{
[super viewDidLoad];
//set up the container sublayer transform
CATransform3D perspective = CATransform3DIdentity;
perspective.m34 = -1.0 / 500.0;
perspective = CATransform3DRotate(perspective, -M_PI_4, 1, 0, 0);
perspective = CATransform3DRotate(perspective, -M_PI_4, 0, 1, 0);
self.containerView.layer.sublayerTransform = perspective;
//add cube face 1
CATransform3D transform = CATransform3DMakeTranslation(0, 0, 100);
[self addFace:0 withTransform:transform];
//add cube face 2
transform = CATransform3DMakeTranslation(100, 0, 0);
transform = CATransform3DRotate(transform, M_PI_2, 0, 1, 0);
[self addFace:1 withTransform:transform];
//add cube face 3
transform = CATransform3DMakeTranslation(0, -100, 0);
transform = CATransform3DRotate(transform, M_PI_2, 1, 0, 0);
[self addFace:2 withTransform:transform];
//add cube face 4
transform = CATransform3DMakeTranslation(0, 100, 0);
transform = CATransform3DRotate(transform, -M_PI_2, 1, 0, 0);
[self addFace:3 withTransform:transform];
//add cube face 5
transform = CATransform3DMakeTranslation(-100, 0, 0);
transform = CATransform3DRotate(transform, -M_PI_2, 0, 1, 0);
[self addFace:4 withTransform:transform];
//add cube face 6
transform = CATransform3DMakeTranslation(0, 0, -100);
transform = CATransform3DRotate(transform, M_PI, 0, 1, 0);
[self addFace:5 withTransform:transform];
}

@end

图5.22

图5.22 动态计算光线效果之后的立方体

点击事件

你应该能注意到现在可以在第三个表面的顶部看见按钮了,点击它,什么都没发生,为什么呢?

这并不是因为iOS在3D场景下正确地处理响应事件,实际上是可以做到的。问题在于视图顺序。在第三章中我们简要提到过,点击事件的处理由视图在父视图中的顺序决定的,并不是3D空间中的Z轴顺序。当给立方体添加视图的时候,我们实际上是按照一个顺序添加,所以按照视图/图层顺序来说,4,5,6在3的前面。

即使我们看不见4,5,6的表面(因为被1,2,3遮住了),iOS在事件响应上仍然保持之前的顺序。当试图点击表面3上的按钮,表面4,5,6截断了点击事件(取决于点击的位置),这就和普通的2D布局在按钮上覆盖物体一样。

你也许认为把doubleSided设置成NO可以解决这个问题,因为它不再渲染视图后面的内容,但实际上并不起作用。因为背对相机而隐藏的视图仍然会响应点击事件(这和通过设置hidden属性或者设置alpha为0而隐藏的视图不同,那两种方式将不会响应事件)。所以即使禁止了双面渲染仍然不能解决这个问题(虽然由于性能问题,还是需要把它设置成NO)。

这里有几种正确的方案:把除了表面3的其他视图userInteractionEnabled属性都设置成NO来禁止事件传递。或者简单通过代码把视图3覆盖在视图6上。无论怎样都可以点击按钮了(图5.23)。

图5.23

图5.23 背景视图不再阻碍按钮,我们可以点击它了

总结

这一章涉及了一些2D和3D的变换。你学习了一些矩阵计算的基础,以及如何用Core Animation创建3D场景。你看到了图层背后到底是如何呈现的,并且知道了不能把扁平的图片做成真实的立体效果,最后我们用demo说明了触摸事件的处理,视图中图层添加的层级顺序会比屏幕上显示的顺序更有意义。

第六章我们会研究一些Core Animation提供不同功能的具体的CALayer子类。

收起阅读 »

iOS 变化 二

5.2 3D变换CG的前缀告诉我们,CGAffineTransform类型属于Core Graphics框架,Core Graphics实际上是一个严格意义上的2D绘图API,并且CGAffineTransform仅仅对2D变换有效。在第三章中,我们提到了zP...
继续阅读 »

5.2 3D变换

CG的前缀告诉我们,CGAffineTransform类型属于Core Graphics框架,Core Graphics实际上是一个严格意义上的2D绘图API,并且CGAffineTransform仅仅对2D变换有效。

在第三章中,我们提到了zPosition属性,可以用来让图层靠近或者远离相机(用户视角),transform属性(CATransform3D类型)可以真正做到这点,即让图层在3D空间内移动或者旋转。

CGAffineTransform类似,CATransform3D也是一个矩阵,但是和2x3的矩阵不同,CATransform3D是一个可以在3维空间内做变换的4x4的矩阵(图5.6)。

图5.6

图5.6 对一个3D像素点做CATransform3D矩阵变换

CGAffineTransform矩阵类似,Core Animation提供了一系列的方法用来创建和组合CATransform3D类型的矩阵,和Core Graphics的函数类似,但是3D的平移和旋转多处了一个z参数,并且旋转函数除了angle之外多出了x,y,z三个参数,分别决定了每个坐标轴方向上的旋转:

CATransform3DMakeRotation(CGFloat angle, CGFloat x, CGFloat y, CGFloat z)
CATransform3DMakeScale(CGFloat sx, CGFloat sy, CGFloat sz)
CATransform3DMakeTranslation(Gloat tx, CGFloat ty, CGFloat tz)

你应该对X轴和Y轴比较熟悉了,分别以右和下为正方向(回忆第三章,这是iOS上的标准结构,在Mac OS,Y轴朝上为正方向),Z轴和这两个轴分别垂直,指向视角外为正方向(图5.7)。

图5.7

图5.7 X,Y,Z轴,以及围绕它们旋转的方向

由图所见,绕Z轴的旋转等同于之前二维空间的仿射旋转,但是绕X轴和Y轴的旋转就突破了屏幕的二维空间,并且在用户视角看来发生了倾斜。

举个例子:清单5.4的代码使用了CATransform3DMakeRotation对视图内的图层绕Y轴做了45度角的旋转,我们可以把视图向右倾斜,这样会看得更清晰。

结果见图5.8,但并不像我们期待的那样。

清单5.4 绕Y轴旋转图层

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad];
//rotate the layer 45 degrees along the Y axis
CATransform3D transform = CATransform3DMakeRotation(M_PI_4, 0, 1, 0);
self.layerView.layer.transform = transform;
}

@end

图5.8

图5.8 绕y轴旋转45度的视图

看起来图层并没有被旋转,而是仅仅在水平方向上的一个压缩,是哪里出了问题呢?

其实完全没错,视图看起来更窄实际上是因为我们在用一个斜向的视角看它,而不是透视

透视投影

在真实世界中,当物体远离我们的时候,由于视角的原因看起来会变小,理论上说远离我们的视图的边要比靠近视角的边跟短,但实际上并没有发生,而我们当前的视角是等距离的,也就是在3D变换中任然保持平行,和之前提到的仿射变换类似。

在等距投影中,远处的物体和近处的物体保持同样的缩放比例,这种投影也有它自己的用处(例如建筑绘图,颠倒,和伪3D视频),但当前我们并不需要。

为了做一些修正,我们需要引入投影变换(又称作z变换)来对除了旋转之外的变换矩阵做一些修改,Core Animation并没有给我们提供设置透视变换的函数,因此我们需要手动修改矩阵值,幸运的是,很简单:

CATransform3D的透视效果通过一个矩阵中一个很简单的元素来控制:m34m34(图5.9)用于按比例缩放X和Y的值来计算到底要离视角多远。

图5.9

图5.9 CATransform3Dm34元素,用来做透视

m34的默认值是0,我们可以通过设置m34为-1.0 / d来应用透视效果,d代表了想象中视角相机和屏幕之间的距离,以像素为单位,那应该如何计算这个距离呢?实际上并不需要,大概估算一个就好了。

因为视角相机实际上并不存在,所以可以根据屏幕上的显示效果自由决定它的防止的位置。通常500-1000就已经很好了,但对于特定的图层有时候更小后者更大的值会看起来更舒服,减少距离的值会增强透视效果,所以一个非常微小的值会让它看起来更加失真,然而一个非常大的值会让它基本失去透视效果,对视图应用透视的代码见清单5.5,结果见图5.10。

清单5.5 对变换应用透视效果

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad];
//create a new transform
CATransform3D transform = CATransform3DIdentity;
//apply perspective
transform.m34 = - 1.0 / 500.0;
//rotate by 45 degrees along the Y axis
transform = CATransform3DRotate(transform, M_PI_4, 0, 1, 0);
//apply to layer
self.layerView.layer.transform = transform;
}

@end

图5.10

图5.10 应用透视效果之后再次对图层做旋转

灭点

当在透视角度绘图的时候,远离相机视角的物体将会变小变远,当远离到一个极限距离,它们可能就缩成了一个点,于是所有的物体最后都汇聚消失在同一个点。

在现实中,这个点通常是视图的中心(图5.11),于是为了在应用中创建拟真效果的透视,这个点应该聚在屏幕中点,或者至少是包含所有3D对象的视图中点。

图5.11

图5.11 灭点

Core Animation定义了这个点位于变换图层的anchorPoint(通常位于图层中心,但也有例外,见第三章)。这就是说,当图层发生变换时,这个点永远位于图层变换之前anchorPoint的位置。

当改变一个图层的position,你也改变了它的灭点,做3D变换的时候要时刻记住这一点,当你视图通过调整m34来让它更加有3D效果,应该首先把它放置于屏幕中央,然后通过平移来把它移动到指定位置(而不是直接改变它的position),这样所有的3D图层都共享一个灭点。

sublayerTransform属性

如果有多个视图或者图层,每个都做3D变换,那就需要分别设置相同的m34值,并且确保在变换之前都在屏幕中央共享同一个position,如果用一个函数封装这些操作的确会更加方便,但仍然有限制(例如,你不能在Interface Builder中摆放视图),这里有一个更好的方法。

CALayer有一个属性叫做sublayerTransform。它也是CATransform3D类型,但和对一个图层的变换不同,它影响到所有的子图层。这意味着你可以一次性对包含这些图层的容器做变换,于是所有的子图层都自动继承了这个变换方法。

相较而言,通过在一个地方设置透视变换会很方便,同时它会带来另一个显著的优势:灭点被设置在容器图层的中点,从而不需要再对子图层分别设置了。这意味着你可以随意使用positionframe来放置子图层,而不需要把它们放置在屏幕中点,然后为了保证统一的灭点用变换来做平移。

我们来用一个demo举例说明。这里用Interface Builder并排放置两个视图(图5.12),然后通过设置它们容器视图的透视变换,我们可以保证它们有相同的透视和灭点,代码见清单5.6,结果见图5.13。

图5.12

图5.12 在一个视图容器内并排放置两个视图

清单5.6 应用sublayerTransform

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *containerView;
@property (nonatomic, weak) IBOutlet UIView *layerView1;
@property (nonatomic, weak) IBOutlet UIView *layerView2;

@end

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad];
//apply perspective transform to container
CATransform3D perspective = CATransform3DIdentity;
perspective.m34 = - 1.0 / 500.0;
self.containerView.layer.sublayerTransform = perspective;
//rotate layerView1 by 45 degrees along the Y axis
CATransform3D transform1 = CATransform3DMakeRotation(M_PI_4, 0, 1, 0);
self.layerView1.layer.transform = transform1;
//rotate layerView2 by 45 degrees along the Y axis
CATransform3D transform2 = CATransform3DMakeRotation(-M_PI_4, 0, 1, 0);
self.layerView2.layer.transform = transform2;
}

图5.13

图5.13 通过相同的透视效果分别对视图做变换

背面

我们既然可以在3D场景下旋转图层,那么也可以从背面去观察它。如果我们在清单5.4中把角度修改为M_PI(180度)而不是当前的M_PI_4(45度),那么将会把图层完全旋转一个半圈,于是完全背对了相机视角。

那么从背部看图层是什么样的呢,见图5.14

图5.14

图5.14 视图的背面,一个镜像对称的图片

如你所见,图层是双面绘制的,反面显示的是正面的一个镜像图片。

但这并不是一个很好的特性,因为如果图层包含文本或者其他控件,那用户看到这些内容的镜像图片当然会感到困惑。另外也有可能造成资源的浪费:想象用这些图层形成一个不透明的固态立方体,既然永远都看不见这些图层的背面,那为什么浪费GPU来绘制它们呢?

CALayer有一个叫做doubleSided的属性来控制图层的背面是否要被绘制。这是一个BOOL类型,默认为YES,如果设置为NO,那么当图层正面从相机视角消失的时候,它将不会被绘制。

扁平化图层

如果对包含已经做过变换的图层的图层做反方向的变换将会发什么什么呢?是不是有点困惑?见图5.15

图5.15

图5.15 反方向变换的嵌套图层

注意做了-45度旋转的内部图层是怎样抵消旋转45度的图层,从而恢复正常状态的。

如果内部图层相对外部图层做了相反的变换(这里是绕Z轴的旋转),那么按照逻辑这两个变换将被相互抵消。

验证一下,相应代码见清单5.7,结果见5.16

清单5.7 绕Z轴做相反的旋转变换

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *outerView;
@property (nonatomic, weak) IBOutlet UIView *innerView;

@end

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad];
//rotate the outer layer 45 degrees
CATransform3D outer = CATransform3DMakeRotation(M_PI_4, 0, 0, 1);
self.outerView.layer.transform = outer;
//rotate the inner layer -45 degrees
CATransform3D inner = CATransform3DMakeRotation(-M_PI_4, 0, 0, 1);
self.innerView.layer.transform = inner;
}

@end

图5.16

图5.16 旋转后的视图

运行结果和我们预期的一致。现在在3D情况下再试一次。修改代码,让内外两个视图绕Y轴旋转而不是Z轴,再加上透视效果,以便我们观察。注意不能用sublayerTransform属性,因为内部的图层并不直接是容器图层的子图层,所以这里分别对图层设置透视变换(清单5.8)。

清单5.8 绕Y轴相反的旋转变换

- (void)viewDidLoad
{
[super viewDidLoad];
//rotate the outer layer 45 degrees
CATransform3D outer = CATransform3DIdentity;
outer.m34 = -1.0 / 500.0;
outer = CATransform3DRotate(outer, M_PI_4, 0, 1, 0);
self.outerView.layer.transform = outer;
//rotate the inner layer -45 degrees
CATransform3D inner = CATransform3DIdentity;
inner.m34 = -1.0 / 500.0;
inner = CATransform3DRotate(inner, -M_PI_4, 0, 1, 0);
self.innerView.layer.transform = inner;
}

预期的效果应该如图5.17所示。

图5.17

图5.17 绕Y轴做相反旋转的预期结果。

但其实这并不是我们所看到的,相反,我们看到的结果如图5.18所示。发什么了什么呢?内部的图层仍然向左侧旋转,并且发生了扭曲,但按道理说它应该保持正面朝上,并且显示正常的方块。

这是由于尽管Core Animation图层存在于3D空间之内,但它们并不都存在同一个3D空间。每个图层的3D场景其实是扁平化的,当你从正面观察一个图层,看到的实际上由子图层创建的想象出来的3D场景,但当你倾斜这个图层,你会发现实际上这个3D场景仅仅是被绘制在图层的表面。

图5.18

图5.18 绕Y轴做相反旋转的真实结果

类似的,当你在玩一个3D游戏,实际上仅仅是把屏幕做了一次倾斜,或许在游戏中可以看见有一面墙在你面前,但是倾斜屏幕并不能够看见墙里面的东西。所有场景里面绘制的东西并不会随着你观察它的角度改变而发生变化;图层也是同样的道理。

这使得用Core Animation创建非常复杂的3D场景变得十分困难。你不能够使用图层树去创建一个3D结构的层级关系--在相同场景下的任何3D表面必须和同样的图层保持一致,这是因为每个的父视图都把它的子视图扁平化了。

至少当你用正常的CALayer的时候是这样,CALayer有一个叫做CATransformLayer的子类来解决这个问题。具体在第六章“特殊的图层”中将会具体讨论。

收起阅读 »

Crash 防护系统 -- KVO 防护

通过本文,您将了解到:KVO Crash 的主要原因KVO 防止 Crash 的常见方案我的 KVO 防护实现测试 KVO 防护效果1. KVO Crash 的常见原因KVO(Key Value Observing) 翻译过来就是键值对观察,是 iO...
继续阅读 »

通过本文,您将了解到:

  1. KVO Crash 的主要原因
  2. KVO 防止 Crash 的常见方案
  3. 我的 KVO 防护实现
  4. 测试 KVO 防护效果

1. KVO Crash 的常见原因

KVO(Key Value Observing) 翻译过来就是键值对观察,是 iOS 观察者模式的一种实现。KVO 允许一个对象监听另一个对象特定属性的改变,并在改变时接收到事件。但是 KVO API 的设计,我个人觉得不是很合理。被观察者需要做的工作太多,日常使用时稍不注意就会导致崩溃。

KVO 日常使用造成崩溃的原因通常有以下几个:

  1. KVO 添加次数和移除次数不匹配:
    • 移除了未注册的观察者,导致崩溃。
    • 重复移除多次,移除次数多于添加次数,导致崩溃。
    • 重复添加多次,虽然不会崩溃,但是发生改变时,也同时会被观察多次。
  2. 被观察者提前被释放,被观察者在 dealloc 时仍然注册着 KVO,导致崩溃。
    例如:被观察者是局部变量的情况(iOS 10 及之前会崩溃)。
  3. 添加了观察者,但未实现 observeValueForKeyPath:ofObject:change:context: 方法,导致崩溃。
  4. 添加或者移除时 keypath == nil,导致崩溃。

2. KVO 防止 Crash 常见方案

为了避免上面提到的使用 KVO 造成崩溃的问题,于是出现了很多关于 KVO 的第三方库,比如最出名的就是 FaceBook 开源的第三方库 facebook / KVOController

FBKVOController 对 KVO 机制进行了额外的一层封装,框架不但可以自动帮我们移除观察者,还提供了 block 或者 selector 的方式供我们进行观察处理。不可否认的是,FBKVOController 为我们的开发提供了很大的便利性。但是相对而言,这种方式对项目代码的侵入性比较大,必须考编码规范来强制约束团队人员使用这种方式。

那么有没有一种对项目代码侵入性小,同时还能有效防护 KVO 崩溃的防护机制呢?

网上有很多类似的方案可以参考一下。

方案一:大白健康系统 — iOS APP 运行时 Crash 自动修复系统

  1. 首先为 NSObject 建立一个分类,利用 Method Swizzling,实现自定义的 BMP_addObserver:forKeyPath:options:context:BMP_removeObserver:forKeyPath:BMP_removeObserver:forKeyPath:context:BMPKVO_dealloc方法,用来替换系统原生的添加移除观察者方法的实现。
  2. 然后在观察者和被观察者之间建立一个 KVODelegate 对象,两者之间通过 KVODelegate 对象 建立联系。然后在添加和移除操作时,将 KVO 的相关信息例如 observerkeyPathoptionscontext保存为 KVOInfo 对象,并添加到 KVODelegate 对象 中对应 的 关系哈希表 中,对应原有的添加观察者。
    关系哈希表的数据结构:{keypath : [KVOInfo 对象1, KVOInfo 对象2, ... ]}
  3. 在添加和移除操作的时候,利用 KVODelegate 对象 做转发,把真正的观察者变为 KVODelegate 对象,而当被观察者的特定属性发生了改变,再由 KVODelegate 对象 分发到原有的观察者上。

那么,BayMax 系统是如何避免 KVO 崩溃的呢?

  1. 添加观察者时:通过关系哈希表判断是否重复添加,只添加一次。

  2. 移除观察者时:通过关系哈希表是否已经进行过移除操作,避免多次移除。

  3. 观察键值改变时:同样通过关系哈希表判断,将改变操作分发到原有的观察者上。

    另外,为了避免被观察者提前被释放,被观察者在 dealloc 时仍然注册着 KVO 导致崩溃。BayMax 系统还利用 Method Swizzling 实现了自定义的 dealloc,在系统 dealloc 调用之前,将多余的观察者移除掉。

方案二: ValiantCat / XXShield(第三方框架)

XXShield 实现方案和 BayMax 系统类似。也是利用一个 Proxy 对象用来做转发, 真正的观察者是 Proxy,被观察者出现了通知信息,由 Proxy 做分发。不过不同点是 Proxy 里面保存的内容没有前者多。只保存了 _observed(被观察者) 和关系哈希表,这个关系哈希表中只维护了 keyPath 和 observer 的关系。

关系哈希表的数据结构:{keypath : [observer1, observer2 , ...](NSHashTable)} 。

XXShield 在 dealloc 中也做了类似将多余观察者移除掉的操作,是通过关系数据结构和 _observed ,然后调用原生移除观察者操作实现的。

方案三: JackLee18 / JKCrashProtect(第三方框架)

JKCrashProtect 相对于前两个方案来讲,看上去更加的简洁明了。他的不同点在于没有使用 delegate。而是直接在分类中建立了一个关系哈希表,用来保存 {keypath : [observer1, observer2 , ...](NSHashTable)} 的关系。

添加的时候,如果关系哈希表中与 keyPath 对应的已经有了相关的观察者,就不再进行添加。同样移除观察者的时候,也在哈希表中进行查找,如果存在 observer、keyPath 的信息,就移除掉,否则就不进行移除操作。

不过,这个框架并没有对被观察者在 dealloc 时仍然注册着 KVO ,造成崩溃的情况进行处理。


3. 我的 KVO 防护实现

参考了这几个方法的实现后,分别实现了一下之后,最终还是选择了 方案一、方案二 这两种方案的实现思路。

  1. 我使用了 YSCKVOProxy 对象,在 YSCKVOProxy 对象 中使用 {keypath : [observer1, observer2 , ...](NSHashTable)} 结构的 关系哈希表 进行 observerkeyPath 之间的维护。
  2. 然后利用 YSCKVOProxy 对象 对添加、移除、观察方法进行分发处理。
  3. 在分类中自定义了 dealloc 的实现,移除了多余的观察者。
  • 代码如下所示:
#import "NSObject+KVODefender.h"
#import "NSObject+MethodSwizzling.h"
#import <objc/runtime.h>

// 判断是否是系统类
static inline BOOL IsSystemClass(Class cls){
BOOL isSystem = NO;
NSString *className = NSStringFromClass(cls);
if ([className hasPrefix:@"NS"] || [className hasPrefix:@"__NS"] || [className hasPrefix:@"OS_xpc"]) {
isSystem = YES;
return isSystem;
}
NSBundle *mainBundle = [NSBundle bundleForClass:cls];
if (mainBundle == [NSBundle mainBundle]) {
isSystem = NO;
}else{
isSystem = YES;
}
return isSystem;
}


#pragma mark - YSCKVOProxy 相关

@interface YSCKVOProxy : NSObject

// 获取所有被观察的 keyPaths
- (NSArray *)getAllKeyPaths;

@end

@implementation YSCKVOProxy
{
// 关系数据表结构:{keypath : [observer1, observer2 , ...](NSHashTable)}
@private
NSMutableDictionary<NSString *, NSHashTable<NSObject *> *> *_kvoInfoMap;
}

- (instancetype)init {
self = [super init];
if (self) {
_kvoInfoMap = [NSMutableDictionary dictionary];
}
return self;
}

// 添加 KVO 信息操作, 添加成功返回 YES
- (BOOL)addInfoToMapWithObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(void *)context {

@synchronized (self) {
if (!observer || !keyPath ||
([keyPath isKindOfClass:[NSString class]] && keyPath.length <= 0)) {
return NO;
}

NSHashTable<NSObject *> *info = _kvoInfoMap[keyPath];
if (info.count == 0) {
info = [[NSHashTable alloc] initWithOptions:(NSPointerFunctionsWeakMemory) capacity:0];
[info addObject:observer];

_kvoInfoMap[keyPath] = info;

return YES;
}

if (![info containsObject:observer]) {
[info addObject:observer];
}

return NO;
}
}

// 移除 KVO 信息操作, 添加成功返回 YES
- (BOOL)removeInfoInMapWithObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath {

@synchronized (self) {
if (!observer || !keyPath ||
([keyPath isKindOfClass:[NSString class]] && keyPath.length <= 0)) {
return NO;
}

NSHashTable<NSObject *> *info = _kvoInfoMap[keyPath];

if (info.count == 0) {
return NO;
}

[info removeObject:observer];

if (info.count == 0) {
[_kvoInfoMap removeObjectForKey:keyPath];

return YES;
}

return NO;
}
}

// 添加 KVO 信息操作, 添加成功返回 YES
- (BOOL)removeInfoInMapWithObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
context:(void *)context {
@synchronized (self) {
if (!observer || !keyPath ||
([keyPath isKindOfClass:[NSString class]] && keyPath.length <= 0)) {
return NO;
}

NSHashTable<NSObject *> *info = _kvoInfoMap[keyPath];

if (info.count == 0) {
return NO;
}

[info removeObject:observer];

if (info.count == 0) {
[_kvoInfoMap removeObjectForKey:keyPath];

return YES;
}

return NO;
}
}

// 实际观察者 yscKVOProxy 进行监听,并分发
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context {

NSHashTable<NSObject *> *info = _kvoInfoMap[keyPath];

for (NSObject *observer in info) {
@try {
[observer observeValueForKeyPath:keyPath ofObject:object change:change context:context];
} @catch (NSException *exception) {
NSString *reason = [NSString stringWithFormat:@"KVO Warning : %@",[exception description]];
NSLog(@"%@",reason);
}
}
}

// 获取所有被观察的 keyPaths
- (NSArray *)getAllKeyPaths {
NSArray <NSString *>*keyPaths = _kvoInfoMap.allKeys;
return keyPaths;
}

@end


#pragma mark - NSObject+KVODefender 分类

@implementation NSObject (KVODefender)

+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{

// 拦截 `addObserver:forKeyPath:options:context:` 方法,替换自定义实现
[NSObject yscDefenderSwizzlingInstanceMethod: @selector(addObserver:forKeyPath:options:context:)
withMethod: @selector(ysc_addObserver:forKeyPath:options:context:)
withClass: [NSObject class]];

// 拦截 `removeObserver:forKeyPath:` 方法,替换自定义实现
[NSObject yscDefenderSwizzlingInstanceMethod: @selector(removeObserver:forKeyPath:)
withMethod: @selector(ysc_removeObserver:forKeyPath:)
withClass: [NSObject class]];

// 拦截 `removeObserver:forKeyPath:context:` 方法,替换自定义实现
[NSObject yscDefenderSwizzlingInstanceMethod: @selector(removeObserver:forKeyPath:context:)
withMethod: @selector(ysc_removeObserver:forKeyPath:context:)
withClass: [NSObject class]];

// 拦截 `dealloc` 方法,替换自定义实现
[NSObject yscDefenderSwizzlingInstanceMethod: NSSelectorFromString(@"dealloc")
withMethod: @selector(ysc_kvodealloc)
withClass: [NSObject class]];
});
}

static void *YSCKVOProxyKey = &YSCKVOProxyKey;
static NSString *const KVODefenderValue = @"YSC_KVODefender";
static void *KVODefenderKey = &KVODefenderKey;

// YSCKVOProxy setter 方法
- (void)setYscKVOProxy:(YSCKVOProxy *)yscKVOProxy {
objc_setAssociatedObject(self, YSCKVOProxyKey, yscKVOProxy, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

// YSCKVOProxy getter 方法
- (YSCKVOProxy *)yscKVOProxy {
id yscKVOProxy = objc_getAssociatedObject(self, YSCKVOProxyKey);
if (yscKVOProxy == nil) {
yscKVOProxy = [[YSCKVOProxy alloc] init];
self.yscKVOProxy = yscKVOProxy;
}
return yscKVOProxy;
}

// 自定义 addObserver:forKeyPath:options:context: 实现方法
- (void)ysc_addObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(void *)context {

if (!IsSystemClass(self.class)) {
objc_setAssociatedObject(self, KVODefenderKey, KVODefenderValue, OBJC_ASSOCIATION_RETAIN);
if ([self.yscKVOProxy addInfoToMapWithObserver:observer forKeyPath:keyPath options:options context:context]) {
// 如果添加 KVO 信息操作成功,则调用系统添加方法
[self ysc_addObserver:self.yscKVOProxy forKeyPath:keyPath options:options context:context];
} else {
// 添加 KVO 信息操作失败:重复添加
NSString *className = (NSStringFromClass(self.class) == nil) ? @"" : NSStringFromClass(self.class);
NSString *reason = [NSString stringWithFormat:@"KVO Warning : Repeated additions to the observer:%@ for the key path:'%@' from %@",
observer, keyPath, className];
NSLog(@"%@",reason);
}
} else {
[self ysc_addObserver:observer forKeyPath:keyPath options:options context:context];
}
}

// 自定义 removeObserver:forKeyPath:context: 实现方法
- (void)ysc_removeObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
context:(void *)context {

if (!IsSystemClass(self.class)) {
if ([self.yscKVOProxy removeInfoInMapWithObserver:observer forKeyPath:keyPath context:context]) {
// 如果移除 KVO 信息操作成功,则调用系统移除方法
[self ysc_removeObserver:self.yscKVOProxy forKeyPath:keyPath context:context];
} else {
// 移除 KVO 信息操作失败:移除了未注册的观察者
NSString *className = NSStringFromClass(self.class) == nil ? @"" : NSStringFromClass(self.class);
NSString *reason = [NSString stringWithFormat:@"KVO Warning : Cannot remove an observer %@ for the key path '%@' from %@ , because it is not registered as an observer", observer, keyPath, className];
NSLog(@"%@",reason);
}
} else {
[self ysc_removeObserver:observer forKeyPath:keyPath context:context];
}
}

// 自定义 removeObserver:forKeyPath: 实现方法
- (void)ysc_removeObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath {

if (!IsSystemClass(self.class)) {
if ([self.yscKVOProxy removeInfoInMapWithObserver:observer forKeyPath:keyPath]) {
// 如果移除 KVO 信息操作成功,则调用系统移除方法
[self ysc_removeObserver:self.yscKVOProxy forKeyPath:keyPath];
} else {
// 移除 KVO 信息操作失败:移除了未注册的观察者
NSString *className = NSStringFromClass(self.class) == nil ? @"" : NSStringFromClass(self.class);
NSString *reason = [NSString stringWithFormat:@"KVO Warning : Cannot remove an observer %@ for the key path '%@' from %@ , because it is not registered as an observer", observer, keyPath, className];
NSLog(@"%@",reason);
}
} else {
[self ysc_removeObserver:observer forKeyPath:keyPath];
}

}

// 自定义 dealloc 实现方法
- (void)ysc_kvodealloc {
@autoreleasepool {
if (!IsSystemClass(self.class)) {
NSString *value = (NSString *)objc_getAssociatedObject(self, KVODefenderKey);
if ([value isEqualToString:KVODefenderValue]) {
NSArray *keyPaths = [self.yscKVOProxy getAllKeyPaths];
// 被观察者在 dealloc 时仍然注册着 KVO
if (keyPaths.count > 0) {
NSString *reason = [NSString stringWithFormat:@"KVO Warning : An instance %@ was deallocated while key value observers were still registered with it. The Keypaths is:'%@'", self, [keyPaths componentsJoinedByString:@","]];
NSLog(@"%@",reason);
}

// 移除多余的观察者
for (NSString *keyPath in keyPaths) {
[self ysc_removeObserver:self.yscKVOProxy forKeyPath:keyPath];
}
}
}
}


[self ysc_kvodealloc];
}

@end

4. 测试 KVO 防护效果

这里提供一下相关崩溃的测试代码:


/********************* KVOCrashObject.h 文件 *********************/
#import <Foundation/Foundation.h>

@interface KVOCrashObject : NSObject

@property (nonatomic, copy) NSString *name;

@end

/********************* KVOCrashObject.m 文件 *********************/
#import "KVOCrashObject.h"

@implementation KVOCrashObject

@end

/********************* ViewController.m 文件 *********************/
#import "ViewController.h"
#import "KVOCrashObject.h"

@interface ViewController ()

@property (nonatomic, strong) KVOCrashObject *objc;

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.

self.objc = [[KVOCrashObject alloc] init];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

// 1.1 移除了未注册的观察者,导致崩溃
[self testKVOCrash11];

// 1.2 重复移除多次,移除次数多于添加次数,导致崩溃
// [self testKVOCrash12];

// 1.3 重复添加多次,虽然不会崩溃,但是发生改变时,也同时会被观察多次。
// [self testKVOCrash13];

// 2. 被观察者 dealloc 时仍然注册着 KVO,导致崩溃
// [self testKVOCrash2];

// 3. 观察者没有实现 -observeValueForKeyPath:ofObject:change:context:导致崩溃
// [self testKVOCrash3];

// 4. 添加或者移除时 keypath == nil,导致崩溃。
// [self testKVOCrash4];
}

/**
1.1 移除了未注册的观察者,导致崩溃
*/

- (void)testKVOCrash11 {
// 崩溃日志:Cannot remove an observer XXX for the key path "xxx" from XXX because it is not registered as an observer.
[self.objc removeObserver:self forKeyPath:@"name"];
}

/**
1.2 重复移除多次,移除次数多于添加次数,导致崩溃
*/

- (void)testKVOCrash12 {
// 崩溃日志:Cannot remove an observer XXX for the key path "xxx" from XXX because it is not registered as an observer.
[self.objc addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
self.objc.name = @"0";
[self.objc removeObserver:self forKeyPath:@"name"];
[self.objc removeObserver:self forKeyPath:@"name"];
}

/**
1.3 重复添加多次,虽然不会崩溃,但是发生改变时,也同时会被观察多次。
*/

- (void)testKVOCrash13 {
[self.objc addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
[self.objc addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
self.objc.name = @"0";
}

/**
2. 被观察者 dealloc 时仍然注册着 KVO,导致崩溃
*/

- (void)testKVOCrash2 {
// 崩溃日志:An instance xxx of class xxx was deallocated while key value observers were still registered with it.
// iOS 10 及以下会导致崩溃,iOS 11 之后就不会崩溃了
KVOCrashObject *obj = [[KVOCrashObject alloc] init];
[obj addObserver: self
forKeyPath: @"name"
options: NSKeyValueObservingOptionNew
context: nil];
}

/**
3. 观察者没有实现 -observeValueForKeyPath:ofObject:change:context:导致崩溃
*/

- (void)testKVOCrash3 {
// 崩溃日志:An -observeValueForKeyPath:ofObject:change:context: message was received but not handled.
KVOCrashObject *obj = [[KVOCrashObject alloc] init];

[self addObserver: obj
forKeyPath: @"title"
options: NSKeyValueObservingOptionNew
context: nil];

self.title = @"111";
}

/**
4. 添加或者移除时 keypath == nil,导致崩溃。
*/

- (void)testKVOCrash4 {
// 崩溃日志: -[__NSCFConstantString characterAtIndex:]: Range or index out of bounds
KVOCrashObject *obj = [[KVOCrashObject alloc] init];

[self addObserver: obj
forKeyPath: @""
options: NSKeyValueObservingOptionNew
context: nil];

// [self removeObserver:obj forKeyPath:@""];
}


- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary*)change context:(void *)context {

NSLog(@"object = %@, keyPath = %@", object, keyPath);
}

@end

可以将示例项目 NSObject+KVODefender.m 中的 + (void)load; 方法注释掉或打开进行防护前后的测试。

经测试可以发现,成功的拦截了这几种因为 KVO 使用不当导致的崩溃。



作者:NJKNJK
链接:https://www.jianshu.com/p/0d67bb7b96de


收起阅读 »

『Crash 防护系统』 一 Unrecognized Selector

这个系列将会介绍如何设计一套 APP Crash 防护系统。这套系统采用 AOP(面向切面编程)的设计思想,利用 Objective-C语言的运行时机制,在不侵入原有项目代码的基础之上,通过在 APP 运行时阶段对崩溃因素的的拦截和处理,使得 APP 能够持续...
继续阅读 »
这个系列将会介绍如何设计一套 APP Crash 防护系统。这套系统采用 AOP(面向切面编程)的设计思想,利用 Objective-C语言的运行时机制,在不侵入原有项目代码的基础之上,通过在 APP 运行时阶段对崩溃因素的的拦截和处理,使得 APP 能够持续稳定正常的运行。

通过本文,您将了解到:

  1. Crash 防护系统开篇
  2. 防护原理简介和常见 Crash
  3. Method Swizzling 方法的封装
  4. Unrecognized Selector 防护
    4.1 unrecognized selector sent to instance(找不到对象方法的实现)
    4.2 unrecognized selector sent to class(找不到类方法实现)

1. Crash 防护系统开篇

APP 的崩溃问题,一直以来都是开发过程中重中之重的问题。日常开发阶段的崩溃,发现后还能够立即处理。但是一旦发布上架的版本出现问题,就需要紧急加班修复 BUG,再更新上架新版本了。在这个过程中, 说不定会因为崩溃而导致关键业务中断、用户存留率下降、品牌口碑变差、生命周期价值下降等,最终导致流失用户,影响到公司的发展。

当然,避免崩溃问题的最好办法就是不产生崩溃。在开发的过程中就要尽可能地保证程序的健壮性。但是,人又不是机器,不可能不犯错。不可能存在没有 BUG 的程序。但是如果能够利用一些语言机制和系统方法,设计一套防护系统,使之能够有效的降低 APP 的崩溃率,那么不仅 APP 的稳定性得到了保障,而且最重要的是可以减少不必要的加班。

这套 Crash 防护系统被命名为:『YSCDefender(防卫者)』。Defender 也是路虎旗下最硬派的越野车系。在电影《Tomb Raider》里面,由 Angelina Jolie 饰演的英国女探险家 Lara Croft,所驾驶的就是一台 Defender。Defender 也是我比较喜欢的车之一。

不过呢,这不重要。。。我就是为这个项目起了个花里胡哨的名字,并给这个名字赋予了一些无聊的意义。。。


2. 防护原理简介和常见 Crash

Objective-C 语言是一门动态语言,我们可以利用 Objective-C 语言的 Runtime 运行时机制,对需要 Hook 的类添加 Category(分类),在各个分类的 +(void)load; 中通过 Method Swizzling 拦截容易造成崩溃的系统方法,将系统原有方法与添加的防护方法的 selector(方法选择器) 与 IMP(函数实现指针)进行对调。然后在替换方法中添加防护操作,从而达到避免以及修复崩溃的目的。

通过 Runtime 机制可以避免的常见 Crash :

  1. unrecognized selector sent to instance(找不到对象方法的实现)
  2. unrecognized selector sent to class(找不到类方法实现)
  3. KVO Crash
  4. KVC Crash
  5. NSNotification Crash
  6. NSTimer Crash
  7. Container Crash(集合类操作造成的崩溃,例如数组越界,插入 nil 等)
  8. NSString Crash (字符串类操作造成的崩溃)
  9. Bad Access Crash (野指针)
  10. Threading Crash (非主线程刷 UI)
  11. NSNull Crash

这一篇我们先来讲解下 unrecognized selector sent to instance(找不到对象方法的实现) 和 unrecognized selector sent to class(找不到类方法实现) 造成的崩溃问题。


3. Method Swizzling 方法的封装

由于这几种常见 Crash 的防护都需要用到 Method Swizzling 技术。所以我们可以为 NSObject 新建一个分类,将 Method Swizzling 相关的方法封装起来。

/********************* NSObject+MethodSwizzling.h 文件 *********************/

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface NSObject (MethodSwizzling)

/** 交换两个类方法的实现
* @param originalSelector 原始方法的 SEL
* @param swizzledSelector 交换方法的 SEL
* @param targetClass 类
*/

+ (void)yscDefenderSwizzlingClassMethod:(SEL)originalSelector withMethod:(SEL)swizzledSelector withClass:(Class)targetClass;

/** 交换两个对象方法的实现
* @param originalSelector 原始方法的 SEL
* @param swizzledSelector 交换方法的 SEL
* @param targetClass 类
*/

+ (void)yscDefenderSwizzlingInstanceMethod:(SEL)originalSelector withMethod:(SEL)swizzledSelector withClass:(Class)targetClass;

@end

/********************* NSObject+MethodSwizzling.m 文件 *********************/

#import "NSObject+MethodSwizzling.h"
#import <objc/runtime.h>

@implementation NSObject (MethodSwizzling)

// 交换两个类方法的实现
+ (void)yscDefenderSwizzlingClassMethod:(SEL)originalSelector withMethod:(SEL)swizzledSelector withClass:(Class)targetClass {
swizzlingClassMethod(targetClass, originalSelector, swizzledSelector);
}

// 交换两个对象方法的实现
+ (void)yscDefenderSwizzlingInstanceMethod:(SEL)originalSelector withMethod:(SEL)swizzledSelector withClass:(Class)targetClass {
swizzlingInstanceMethod(targetClass, originalSelector, swizzledSelector);
}

// 交换两个类方法的实现 C 函数
void swizzlingClassMethod(Class class, SEL originalSelector, SEL swizzledSelector) {

Method originalMethod = class_getClassMethod(class, originalSelector);
Method swizzledMethod = class_getClassMethod(class, swizzledSelector);

BOOL didAddMethod = class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));

if (didAddMethod) {
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}

// 交换两个对象方法的实现 C 函数
void swizzlingInstanceMethod(Class class, SEL originalSelector, SEL swizzledSelector) {
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

BOOL didAddMethod = class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));

if (didAddMethod) {
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}

@end

4. Unrecognized Selector 防护

4.1 unrecognized selector sent to instance(找不到对象方法的实现)

如果被调用的对象方法没有实现,那么程序在运行中调用该方法时,就会因为找不到对应的方法实现,从而导致 APP 崩溃。比如下面这样的代码:


UIButton *testButton = [[UIButton alloc] init];
[testButton performSelector:@selector(someMethod:)];

testButton 是一个 UIButton 对象,而 UIButton 类中并没有实现 someMethod: 方法。所以向 testButoon 对象发送 someMethod: 方法,就会导致 testButoon 对象无法找到对应的方法实现,最终导致 APP 的崩溃。

那么有办法解决这类因为找不到方法的实现而导致程序崩溃的方法吗?

消息转发机制中三大步骤:消息动态解析消息接受者重定向消息重定向。通过这三大步骤,可以让我们在程序找不到调用方法崩溃之前,拦截方法调用。

大致流程如下:

  1. 消息动态解析:Objective-C 运行时会调用 +resolveInstanceMethod: 或者 +resolveClassMethod:,让你有机会提供一个函数实现。我们可以通过重写这两个方法,添加其他函数实现,并返回 YES, 那运行时系统就会重新启动一次消息发送的过程。若返回 NO 或者没有添加其他函数实现,则进入下一步。
  2. 消息接受者重定向:如果当前对象实现了 forwardingTargetForSelector:,Runtime 就会调用这个方法,允许我们将消息的接受者转发给其他对象。如果这一步方法返回 nil,则进入下一步。
  3. 消息重定向:Runtime 系统利用 methodSignatureForSelector: 方法获取函数的参数和返回值类型。
    • 如果 methodSignatureForSelector: 返回了一个 NSMethodSignature 对象(函数签名),Runtime 系统就会创建一个 NSInvocation 对象,并通过 forwardInvocation: 消息通知当前对象,给予此次消息发送最后一次寻找 IMP 的机会。
    • 如果 methodSignatureForSelector: 返回 nil。则 Runtime 系统会发出 doesNotRecognizeSelector:消息,程序也就崩溃了。

[图片上传失败...(image-5cdd82-1618276584627)]

这里我们选择第二步(消息接受者重定向)来进行拦截。因为 -forwardingTargetForSelector 方法可以将消息转发给一个对象,开销较小,并且被重写的概率较低,适合重写。

具体步骤如下:

  1. 给 NSObject 添加一个分类,在分类中实现一个自定义的 -ysc_forwardingTargetForSelector: 方法;
  2. 利用 Method Swizzling 将 -forwardingTargetForSelector: 和 -ysc_forwardingTargetForSelector: 进行方法交换。
  3. 在自定义的方法中,先判断当前对象是否已经实现了消息接受者重定向和消息重定向。如果都没有实现,就动态创建一个目标类,给目标类动态添加一个方法。
  4. 把消息转发给动态生成类的实例对象,由目标类动态创建的方法实现,这样 APP 就不会崩溃了。

实现代码如下:


#import "NSObject+SelectorDefender.h"
#import "NSObject+MethodSwizzling.h"
#import <objc/runtime.h>

@implementation NSObject (SelectorDefender)

+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{

// 拦截 `-forwardingTargetForSelector:` 方法,替换自定义实现
[NSObject yscDefenderSwizzlingInstanceMethod:@selector(forwardingTargetForSelector:)
withMethod:@selector(ysc_forwardingTargetForSelector:)
withClass:[NSObject class]];

});
}

// 自定义实现 `-ysc_forwardingTargetForSelector:` 方法
- (id)ysc_forwardingTargetForSelector:(SEL)aSelector {

SEL forwarding_sel = @selector(forwardingTargetForSelector:);

// 获取 NSObject 的消息转发方法
Method root_forwarding_method = class_getInstanceMethod([NSObject class], forwarding_sel);
// 获取 当前类 的消息转发方法
Method current_forwarding_method = class_getInstanceMethod([self class], forwarding_sel);

// 判断当前类本身是否实现第二步:消息接受者重定向
BOOL realize = method_getImplementation(current_forwarding_method) != method_getImplementation(root_forwarding_method);

// 如果没有实现第二步:消息接受者重定向
if (!realize) {
// 判断有没有实现第三步:消息重定向
SEL methodSignature_sel = @selector(methodSignatureForSelector:);
Method root_methodSignature_method = class_getInstanceMethod([NSObject class], methodSignature_sel);

Method current_methodSignature_method = class_getInstanceMethod([self class], methodSignature_sel);
realize = method_getImplementation(current_methodSignature_method) != method_getImplementation(root_methodSignature_method);

// 如果没有实现第三步:消息重定向
if (!realize) {
// 创建一个新类
NSString *errClassName = NSStringFromClass([self class]);
NSString *errSel = NSStringFromSelector(aSelector);
NSLog(@"出问题的类,出问题的对象方法 == %@ %@", errClassName, errSel);

NSString *className = @"CrachClass";
Class cls = NSClassFromString(className);

// 如果类不存在 动态创建一个类
if (!cls) {
Class superClsss = [NSObject class];
cls = objc_allocateClassPair(superClsss, className.UTF8String, 0);
// 注册类
objc_registerClassPair(cls);
}
// 如果类没有对应的方法,则动态添加一个
if (!class_getInstanceMethod(NSClassFromString(className), aSelector)) {
class_addMethod(cls, aSelector, (IMP)Crash, "@@:@");
}
// 把消息转发到当前动态生成类的实例对象上
return [[cls alloc] init];
}
}
return [self ysc_forwardingTargetForSelector:aSelector];
}

// 动态添加的方法实现
static int Crash(id slf, SEL selector) {
return 0;
}

@end

4.2 unrecognized selector sent to class(找不到类方法实现)

同对象方法一样,如果被调用的类方法没有实现,那么同样也会导致 APP 崩溃。

例如,有这样一个类,声明了一个 + (id)aClassFunc; 的类方法, 但是并没有实现,就像下边的 YSCObject 这样。

/********************* YSCObject.h 文件 *********************/
#import <Foundation/Foundation.h>

@interface YSCObject : NSObject

+ (id)aClassFunc;

@end

/********************* YSCObject.m 文件 *********************/
#import "YSCObject.h"

@implementation YSCObject

@end

如果我们直接调用 [YSCObject aClassFunc]; 就会导致崩溃。

找不到类方法实现的解决方法和之前类似,我们可以利用 Method Swizzling 将 +forwardingTargetForSelector:和 +ysc_forwardingTargetForSelector: 进行方法交换。


#import "NSObject+SelectorDefender.h"
#import "NSObject+MethodSwizzling.h"
#import <objc/runtime.h>

@implementation NSObject (SelectorDefender)

+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{

// 拦截 `+forwardingTargetForSelector:` 方法,替换自定义实现
[NSObject yscDefenderSwizzlingClassMethod:@selector(forwardingTargetForSelector:)
withMethod:@selector(ysc_forwardingTargetForSelector:)
withClass:[NSObject class]];
});
}

// 自定义实现 `+ysc_forwardingTargetForSelector:` 方法
+ (id)ysc_forwardingTargetForSelector:(SEL)aSelector {
SEL forwarding_sel = @selector(forwardingTargetForSelector:);

// 获取 NSObject 的消息转发方法
Method root_forwarding_method = class_getClassMethod([NSObject class], forwarding_sel);
// 获取 当前类 的消息转发方法
Method current_forwarding_method = class_getClassMethod([self class], forwarding_sel);

// 判断当前类本身是否实现第二步:消息接受者重定向
BOOL realize = method_getImplementation(current_forwarding_method) != method_getImplementation(root_forwarding_method);

// 如果没有实现第二步:消息接受者重定向
if (!realize) {
// 判断有没有实现第三步:消息重定向
SEL methodSignature_sel = @selector(methodSignatureForSelector:);
Method root_methodSignature_method = class_getClassMethod([NSObject class], methodSignature_sel);

Method current_methodSignature_method = class_getClassMethod([self class], methodSignature_sel);
realize = method_getImplementation(current_methodSignature_method) != method_getImplementation(root_methodSignature_method);

// 如果没有实现第三步:消息重定向
if (!realize) {
// 创建一个新类
NSString *errClassName = NSStringFromClass([self class]);
NSString *errSel = NSStringFromSelector(aSelector);
NSLog(@"出问题的类,出问题的类方法 == %@ %@", errClassName, errSel);

NSString *className = @"CrachClass";
Class cls = NSClassFromString(className);

// 如果类不存在 动态创建一个类
if (!cls) {
Class superClsss = [NSObject class];
cls = objc_allocateClassPair(superClsss, className.UTF8String, 0);
// 注册类
objc_registerClassPair(cls);
}
// 如果类没有对应的方法,则动态添加一个
if (!class_getInstanceMethod(NSClassFromString(className), aSelector)) {
class_addMethod(cls, aSelector, (IMP)Crash, "@@:@");
}
// 把消息转发到当前动态生成类的实例对象上
return [[cls alloc] init];
}
}
return [self ysc_forwardingTargetForSelector:aSelector];
}

// 动态添加的方法实现
static int Crash(id slf, SEL selector) {
return 0;
}

@end

将 4.1 和 4.2 结合起来就可以拦截所有未实现的类方法和对象方法了


作者:NJKNJK
链接:https://www.jianshu.com/p/bd8a2594b788





收起阅读 »

Crash拦截器 - 让unrecognized selector消失

在本文中,我们将了解到如下内容:基础的消息转发流程unrecognized selector 拦截建议快速转发(Fast Forwarding)拦截unrecognized selector常规转发(Normal Forwarding)拦截unrecogniz...
继续阅读 »

在本文中,我们将了解到如下内容:

  1. 基础的消息转发流程
  2. unrecognized selector 拦截建议
  3. 快速转发(Fast Forwarding)拦截unrecognized selector
  4. 常规转发(Normal Forwarding)拦截unrecognized selector

前言

我们在第一天学习Objective-C这一门语言的时候,就被告知这是一门动态语言。
C这样的编译语言,在编译阶段就确定了所有函数的调用链,如果函数没有被实现,编译就根本不过了。而基于动态语言的特性,在编译期间,我们无法确认程序在运行时要调用哪一个函数,某一个未被实现的函数是否会在运行时被实现。
这样就可能会出现运行时发现调用的函数根本不存在的尴尬,这也就是我们收到unrecognized selector sent to XXX这样的崩溃的原因了(动态语言也有让人心累的地方,手动叹气)。

这篇文章要讨论的就是如果遇到了这种尴尬情况的时候,我们该如何避免我们最最最讨厌的崩溃(是的,所有的崩溃都是最最最让人讨厌的)。

消息转发流程

我们知道在我们调用某一个方法之后,最终调用的是objc_msgSend()这样一个方法,发送消息(selector)给消息接收者(receiver)。这个方法会根据OC的消息发送机制在receiver中查找selector。如果没有查找到,就会出现上述的运行时调用了未实现的函数的尴尬局面了。

不过为了缓解这种尴尬,我们还有机会来挣扎。这挣扎机会就是消息转发流程

消息转发流程包含以下3个步骤:

  1. 动态方法解析:resolveInstanceMethod:resolveClassMethod:
  2. 消息转发
    • 快速转发:forwardingTargetForSelector:
    • 常规转发:methodSignatureForSelector:forwardInvocation:

消息转发流程是以动态方法解析消息快速转发消息常规转发这样的顺序来执行的。如果其中任意一个步骤能使消息被执行,那么就不会出现unrecognized selector sent to XXX的崩溃

动态方法解析

resolveInstanceMethod:这个方法的作用是动态地为selector提供一个实例方法的实现。而resolveClassMethod:则是提供一个类方法的实现。

所以我们可以在这两个方法中,为对象添加方法的实现,再返回YES告诉已经为selector添加了实现。这样就会重新在对象上查找方法,找到我们新添加的方法后就直接调用。从而避免掉unrecognized selector sent to XXX

需要注意的是: 这两个方法会响应respondsToSelector:instancesRespondToSelector:

消息快速转发

forwardingTargetForSelector:的作用是将消息转发给其它对象去处理。
我们可以在这个方法中,返回一个对象,让这个对象来响应消息。

需要注意的是: 如果在这个方法中返回selfnil,则表示没有可响应的目标。

消息常规转发

forwardInvocation:的作用也是将消息转发给其它对象。不过与 消息快速转发 不同的是该方法需要手动的创建一个NSInvocation对象,并手动地将新消息发送给新的接收者。

很显然,这种方式会比 消息快速转发 付出更大的消耗。

如何选择拦截方案的建议

对于以上的三个步骤,我们该选择哪一个步骤来进行拦截呢?

  • 动态方法解析 - 不建议
    1. 这个方法会为类添加本身不存在的方法,绝大多数情况下,这个方法时冗余的。
    2. respondsToSelector:instancesRespondToSelector:这两个方法都会调用到resolveInstanceMethod:,那么在我们需要使用这两个方法进行判断的时候,就会出现我们不想看到的情况。
  • 消息快速转发 - 推荐
    会拦截掉已经通过消息常规转发实现的消息转发,但是可以通过判断避开对NSObject子类的消息常规转发的拦截。
  • 消息常规转发 - 推荐
    这一步不会对原有的消息转发机制产生影响,缺点是更大的性能开销。

快速转发拦截方案

我们可以创建一个例如:crashPreventor的类,在forwardingTargetForSelector:中为crashPreventor添加selector,最后返回crashPreventor的实例。从而让crashPreventor的实例响应这个selector。具体代码如下:


@implementation
NSObject (SelectorPreventor)

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation"
- (id)forwardingTargetForSelector:(SEL)aSelector{
Class rootClass = NSObject.class;
Class currentClass = self.class;
return [self.class actionForwardingTargetForSelector:aSelector rootClass:rootClass currentClass:currentClass];
}

+ (id)forwardingTargetForSelector:(SEL)aSelector {
Class rootClass = objc_getMetaClass(class_getName(NSObject.class));
Class currentClass = objc_getMetaClass(class_getName(self.class));
return [self actionForwardingTargetForSelector:aSelector rootClass:rootClass currentClass:currentClass];
}

+ (id)actionForwardingTargetForSelector:(SEL)aSelector rootClass:(Class)rootClass currentClass:(Class)currentClass {
// 过滤掉内部对象
NSString *className = NSStringFromClass(currentClass);
if ([className hasPrefix:@"_"]) {
return nil;
}

SEL methodSignatureSelector = @selector(methodSignatureForSelector:);
IMP rootMethodSignatureMethod = class_getMethodImplementation(rootClass, methodSignatureSelector);
IMP currentMethodSignatureMethod = class_getMethodImplementation(currentClass, methodSignatureSelector);
if (rootMethodSignatureMethod != currentMethodSignatureMethod) {
return nil;
}

NSString * selectorName = NSStringFromSelector(aSelector);

// 上报异常
// unrecognized selector sent to class XXX
// unrecognized selector sent to instance XXX
NSLog(@"unrecognized selector crash:%@:%@", className, selectorName);

// 创建crashPreventor类
NSString *targetClassName = @"crashPreventor";
Class cls = NSClassFromString(targetClassName);
if (!cls) {
// 如果要注册类,则必须要先判断class是否已经存在,否则会产生崩溃
// 如果不注册类,则可以重复创建class
cls = objc_allocateClassPair(NSObject.class, targetClassName.UTF8String, 0);
objc_registerClassPair(cls);
}

// 如果类没有对应的方法,则动态添加一个
if (!class_getInstanceMethod(cls, aSelector)) {
Method method = class_getInstanceMethod(currentClass, @selector(crashPreventor));
class_addMethod(cls, aSelector, method_getImplementation(method), method_getTypeEncoding(method));
}

return [cls new];
}

#pragma clang diagnostic pop

- (id)crashPreventor {
return nil;
}

@end

这里有几个点需要提一下:

  1. - (id)forwardingTargetForSelector:(SEL)aSelector;+ (id)forwardingTargetForSelector:(SEL)aSelector;都要在NSObject的分类中重写。前者对应实例方法,后者对应类方法。
  2. 过滤掉一些系统内部对象,否则在启动的时候就会有一些奇怪的异常被捕获到。
  3. 我们需要判断当前类是否实现了methodSignatureForSelector:方法,如果实现了该方法,就认为当前类已经实现了自己的消息转发机制,我们不对其进行拦截。
  4. 细心的我们肯定有注意到,不管是类方法还是实例方法,我们都是向crashPreventor中添加实例方法。这是因为,我们的响应对象时crashPreventor实例,而selector不区分实例方法还是类方法。我们这么处理最终对方法执行来说不会有什么差别。

常规转发拦截方案

实现比较简单,我们直接上代码:

@implementation NSObject (SelectorPreventor)

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation"
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
return [NSMethodSignature signatureWithObjCTypes:"@"];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
NSLog(@"forwardInvocation------");
}

+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
return [NSMethodSignature signatureWithObjCTypes:"@"];
}

+ (void)forwardInvocation:(NSInvocation *)anInvocation {
NSLog(@"forwardInvocation------");
}

#pragma clang diagnostic pop

@end

同样的,类方法和实例方法我们都需要重写。
methodSignatureForSelector:中我们返回一个返回值为voidNSMethodSignature,在forwardInvocation:中我们不做任何事情。这样将性能消耗减到最小。

以上:我们可以选择其中一种方式来实现我们对unrecognized selector的拦截,跟unrecognized selector彻底说拜拜啦(手动微笑)。



作者:一纸苍白
链接:https://www.jianshu.com/p/90b04882c595
收起阅读 »

自定义KVO(四)

四、KVOController上面Hook系统kvo相关方法的方式侵入太严重了,我们要做的其实只是需要对自己的调用负责而已,可以通过中间类来完成。这块有很多第三方框架,其中Facebook提供的KVOController是很优秀的一个框架。在这篇文章中将对这个...
继续阅读 »


四、KVOController

上面Hook系统kvo相关方法的方式侵入太严重了,我们要做的其实只是需要对自己的调用负责而已,可以通过中间类来完成。这块有很多第三方框架,其中Facebook提供的KVOController是很优秀的一个框架。在这篇文章中将对这个库进行简单分析。

4.1 KVOController 的使用

#import <KVOController/KVOController.h>


- (void)viewDidLoad {
[super viewDidLoad];

self.KVOController = [FBKVOController controllerWithObserver:self];
[self.KVOController observe:self.obj keyPath:@"name" options:NSKeyValueObservingOptionNew block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
NSLog(@"change:%@",change);
}];

[self.KVOController observe:self.obj keyPath:@"name" options:NSKeyValueObservingOptionNew block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
NSLog(@"change:%@",change);
}];

[self.KVOController observe:self.obj keyPath:@"nickName" options:NSKeyValueObservingOptionNew action:@selector(hp_NickNameChange:object:)];
}

- (void)hp_NickNameChange:(NSDictionary *)change object:(id)object {
NSLog(@"change:%@ object:%@",change,object);
}

输出:

change:{
FBKVONotificationKeyPathKey = name;
kind = 1;
new = HP111;
}
change:{
kind = 1;
new = cat111;
} object:<HPObject: 0x6000022c91d0>
  • vc持有FBKVOController实例KVOController。在NSObject+FBKVOController.h的关联属性。
  • 通过FBKVOController实例进行注册。注册方式提供了多种。
  • 对于重复添加会进行判断直接返回。
  • 会自动进行移除操作。

4.2 KVOController 实现分析

KVOController主要是使用了中介者模式,官方kvo使用麻烦的点在于使用需要三部曲。KVOController核心就是将三部曲进行了底层封装,上层只需要关心业务逻辑。

FBKVOController会进行注册、移除以及回调的处理(回调包括blockaction以及兼容系统的observe回调)。是对外暴露的交互类。使用FBKVOController分为两步:

  1. 使用 controllerWithObserver 初始化FBKVOController实例。
  2. 使用observe:进行注册。

4.2.1 FBKVOController 初始化

controllerWithObserver
controllerWithObserver最终会调用到initWithObserver中:

- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved
{
self = [super init];
if (nil != self) {
_observer = observer;
NSPointerFunctionsOptions keyOptions = retainObserved ? NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPointerPersonality : NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality;
_objectInfosMap = [[NSMapTable alloc] initWithKeyOptions:keyOptions valueOptions:NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality capacity:0];
pthread_mutex_init(&_lock, NULL);
}
return self;
}

  • _observer是观察者,FBKVOController的属性。
@property (nullable, nonatomic, weak, readonly) id observer;

weak类型,因为FBKVOController本身被观察者持有了。

  • _objectInfosMap根据retainObserved进行NSMapTable内存管理初始化配置,FBKVOController的成员变量。其中保存的是一个被观察者对应多个_FBKVOInfo(也就是被观察对象对应多个keyPath):
  NSMapTable<id, NSMutableSet<_FBKVOInfo *> *> *_objectInfosMap;

这里_FBKVOInfo是放在NSMutableSet中的,说明是去重的。

4.2.2 FBKVOController 注册

由于各个observe方式的原理差不多,这里只分析block的形式。

- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block
{
NSAssert(0 != keyPath.length && NULL != block, @"missing required parameters observe:%@ keyPath:%@ block:%p", object, keyPath, block);
if (nil == object || 0 == keyPath.length || NULL == block) {
return;
}

// create info
_FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block];

// observe object with info
[self _observe:object info:info];
}
  • 首先一些条件容错判断。
  • 构造_FBKVOInfo。保存FBKVOControllerkeyPathoptions以及block
  • 调用_observe:(id)object info:(_FBKVOInfo *)info

4.2.2.1 _FBKVOInfo

@implementation _FBKVOInfo
{
@public
__weak FBKVOController *_controller;
NSString *_keyPath;
NSKeyValueObservingOptions _options;
SEL _action;
void *_context;
FBKVONotificationBlock _block;
_FBKVOInfoState _state;
}
  • _FBKVOInfo中保存了相关数据信息。

并且重写了isEqualhash方法:

- (NSUInteger)hash
{
return [_keyPath hash];
}

- (BOOL)isEqual:(id)object
{
if (nil == object) {
return NO;
}
if (self == object) {
return YES;
}
if (![object isKindOfClass:[self class]]) {
return NO;
}
return [_keyPath isEqualToString:((_FBKVOInfo *)object)->_keyPath];
}

说明只要_keyPath相同就认为是同一对象。

4.2.2.2 _observe: info:

- (void)_observe:(id)object info:(_FBKVOInfo *)info
{
// lock
pthread_mutex_lock(&_lock);

//从TableMap中获取 object(被观察者) 对应的 set
NSMutableSet *infos = [_objectInfosMap objectForKey:object];

// check for info existence
//判断对应的keypath info 是否存在
_FBKVOInfo *existingInfo = [infos member:info];
if (nil != existingInfo) {
//存在直接返回,这里就相当于对于同一个观察者排除了相同的keypath
// observation info already exists; do not observe it again

// unlock and return
pthread_mutex_unlock(&_lock);
return;
}

// lazilly create set of infos
//TableMap数据为空进行创建设置
if (nil == infos) {
infos = [NSMutableSet set];
//<被观察者 - keypaths info>
[_objectInfosMap setObject:infos forKey:object];
}

// add info and oberve
//keypaths info添加 keypath info
[infos addObject:info];

// unlock prior to callout
pthread_mutex_unlock(&_lock);
//注册
[[_FBKVOSharedController sharedController] observe:object info:info];
}
  • 首先判断kayPath是否已经被注册了,注册了直接返回,这里也就进行了去重处理。
  • 将构造的_FBKVOInfo信息添加进_objectInfosMap中。
  • 调用_FBKVOSharedController进行真正的注册。

member:说明
member会调用到_FBKVOInfo中的hash以及isEqual进行判断对象是否存在,也就是判断keyPath对应的对象是否存在。








官方API说明:







源码实现:

+ (NSUInteger)hash {
return _objc_rootHash(self);
}

- (NSUInteger)hash {
return _objc_rootHash(self);
}

+ (BOOL)isEqual:(id)obj {
return obj == (id)self;
}

- (BOOL)isEqual:(id)obj {
return obj == self;
}

uintptr_t
_objc_rootHash(id obj)
{
return (uintptr_t)obj;
}
  • hash默认实现将对象地址转换为uintptr_t类型返回。
  • isEqual:直接判断地址是否相同。
  • member:根据汇编可以看到大概逻辑是先计算参数的hash,然后集合中的元素调用isEqual参数是hash值。

4.2.2.3 _unobserve:info:

- (void)_unobserve:(id)object info:(_FBKVOInfo *)info
{
// lock
pthread_mutex_lock(&_lock);

// get observation infos
NSMutableSet *infos = [_objectInfosMap objectForKey:object];

// lookup registered info instance
_FBKVOInfo *registeredInfo = [infos member:info];

if (nil != registeredInfo) {
[infos removeObject:registeredInfo];

// remove no longer used infos
if (0 == infos.count) {
[_objectInfosMap removeObjectForKey:object];
}
}

// unlock
pthread_mutex_unlock(&_lock);

// unobserve
[[_FBKVOSharedController sharedController] unobserve:object info:registeredInfo];
}

- (void)_unobserve:(id)object
{
// lock
pthread_mutex_lock(&_lock);

NSMutableSet *infos = [_objectInfosMap objectForKey:object];

// remove infos
[_objectInfosMap removeObjectForKey:object];

// unlock
pthread_mutex_unlock(&_lock);

// unobserve
[[_FBKVOSharedController sharedController] unobserve:object infos:infos];
}

- (void)_unobserveAll
{
// lock
pthread_mutex_lock(&_lock);

NSMapTable *objectInfoMaps = [_objectInfosMap copy];

// clear table and map
[_objectInfosMap removeAllObjects];

// unlock
pthread_mutex_unlock(&_lock);

_FBKVOSharedController *shareController = [_FBKVOSharedController sharedController];

for (id object in objectInfoMaps) {
// unobserve each registered object and infos
NSSet *infos = [objectInfoMaps objectForKey:object];
[shareController unobserve:object infos:infos];
}
}

  • _unobserve提供了3个方法进行移除。分别对应keyPathobserverd(被观察对象)、observer(观察者)。
  • 最终都是通过_FBKVOSharedControllerunobserve进行移除。

4.2.3 _FBKVOSharedController

[[_FBKVOSharedController sharedController] observe:object info:info];

4.2.3.1 sharedController

_FBKVOSharedController是个单例,有成员变量_infos:

 NSHashTable<_FBKVOInfo *> *_infos;
不设计FBKVOController为单例是因为它被观察者持有,它是单例观察者就无法释放了。这里_infos存储的是所有类的_FBKVOInfo信息

- (instancetype)init
{
self = [super init];
if (nil != self) {
NSHashTable *infos = [NSHashTable alloc];
#ifdef __IPHONE_OS_VERSION_MIN_REQUIRED
_infos = [infos initWithOptions:NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];
#elif defined(__MAC_OS_X_VERSION_MIN_REQUIRED)
if ([NSHashTable respondsToSelector:@selector(weakObjectsHashTable)]) {
_infos = [infos initWithOptions:NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];
} else {
// silence deprecated warnings
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
_infos = [infos initWithOptions:NSPointerFunctionsZeroingWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];
#pragma clang diagnostic pop
}

#endif
pthread_mutex_init(&_mutex, NULL);
}
return self;
}

  • infos的初始化是weak的,也就是它不影响_FBKVOInfo的引用计数。

4.2.3.2 observe: info:

- (void)observe:(id)object info:(nullable _FBKVOInfo *)info
{
if (nil == info) {
return;
}

// register info
pthread_mutex_lock(&_mutex);
[_infos addObject:info];
pthread_mutex_unlock(&_mutex);

// add observer
//被观察者调用官方kvo进行注册,context 传递的是 _FBKVOInfo 信息。
[object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info];

if (info->_state == _FBKVOInfoStateInitial) {
//状态变为Observing
info->_state = _FBKVOInfoStateObserving;
} else if (info->_state == _FBKVOInfoStateNotObserving) {
// this could happen when `NSKeyValueObservingOptionInitial` is one of the NSKeyValueObservingOptions,
// and the observer is unregistered within the callback block.
// at this time the object has been registered as an observer (in Foundation KVO),
// so we can safely unobserve it.
//当状态变为不在观察时移除
[object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
}
}
  • 首先自己持有了传进来的info信息。
  • observe: info:中调用系统kvo方法观察注册。context传递的是_FBKVOInfo信息。
  • 对于系统而言观察者是_FBKVOSharedController

4.2.3.3 unobserve: info:

- (void)unobserve:(id)object info:(nullable _FBKVOInfo *)info
{
if (nil == info) {
return;
}

// unregister info
pthread_mutex_lock(&_mutex);
[_infos removeObject:info];
pthread_mutex_unlock(&_mutex);

// remove observer
if (info->_state == _FBKVOInfoStateObserving) {
[object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
}
info->_state = _FBKVOInfoStateNotObserving;
}
  • 调用系统的removeObserver移除观察。

4.2.3.4 observeValueForKeyPath

既然是在4.2.3_FBKVOSharedController中进行的注册,那么系统的回调observeValueForKeyPath必然由它实现:

- (void)observeValueForKeyPath:(nullable NSString *)keyPath
ofObject:(nullable id)object
change:(nullable NSDictionary<NSString *, id> *)change
context:(nullable void *)context
{
NSAssert(context, @"missing context keyPath:%@ object:%@ change:%@", keyPath, object, change);

_FBKVOInfo *info;

{
// lookup context in registered infos, taking out a strong reference only if it exists
pthread_mutex_lock(&_mutex);
info = [_infos member:(__bridge id)context];
pthread_mutex_unlock(&_mutex);
}

if (nil != info) {

// take strong reference to controller
FBKVOController *controller = info->_controller;
if (nil != controller) {

// take strong reference to observer
//观察者
id observer = controller.observer;
if (nil != observer) {

// dispatch custom block or action, fall back to default action
if (info->_block) {
NSDictionary<NSString *, id> *changeWithKeyPath = change;
// add the keyPath to the change dictionary for clarity when mulitple keyPaths are being observed
if (keyPath) {
//将keypath加入字典中
NSMutableDictionary<NSString *, id> *mChange = [NSMutableDictionary dictionaryWithObject:keyPath forKey:FBKVONotificationKeyPathKey];
[mChange addEntriesFromDictionary:change];
changeWithKeyPath = [mChange copy];
}
info->_block(observer, object, changeWithKeyPath);
} else if (info->_action) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[observer performSelector:info->_action withObject:change withObject:object];
#pragma clang diagnostic pop
} else {
[observer observeValueForKeyPath:keyPath ofObject:object change:change context:info->_context];
}
}
}
}
}
  • info中获取观察者,info信息是context传递过来的。
  • _FBKVOInfo存在的情况下根据类型(blockaction、系统原始回调)进行了回调。block回调的过程中添加了keyPath

4.2.4 自动移除观察者

FBKVOControllerdealloc中调用了unobserveAll进行移除:

- (void)dealloc
{
[self unobserveAll];
pthread_mutex_destroy(&_lock);
}

由于FBKVOController的实例是被观察者持有的,所以当观察者dealloc的时候FBKVOController实例也就dealloc了。在这里调用就相当于在观察者dealloc中调用了移除。

FBKVOController流程




五、通过gnustep探索

kvokvc相关的代码苹果并没有开源,对于它们的探索可以通过gnustep查看原理,gnustep中有一些苹果早期底层的实现。



5.1 addObserver


  • setup()中是对一些表的初始化。
  • replacementForClass创建并注册kvo类。
  • 创建GSKVOInfo信息加入Map中。然后进行isa替换。
  • 重写setter方法。





  • 根据是否开启自动回调决定是否调用willChangeValueForKey以及didChangeValueForKey

didChangeValueForKey




最终调用了notifyForKey发送通知。

notifyForKey:ofInstance:prior:



收起阅读 »

自定义KVO(三)

三、系统kvo容错处理在上面自定义kvo中处理了自动移除观察者逻辑,以及将回调使用block实现。在实际使用系统kvo的时候有以下问题:1.多次添加同一观察者会进行多次回调。2.某个属性没有被观察,在dealloc中移除会造成crash。3.多次移除观察者也会...
继续阅读 »

三、系统kvo容错处理

在上面自定义kvo中处理了自动移除观察者逻辑,以及将回调使用block实现。在实际使用系统kvo的时候有以下问题:
1.多次添加同一观察者会进行多次回调。
2.某个属性没有被观察,在dealloc中移除会造成crash
3.多次移除观察者也会造成crash
4.不移除观察者有可能造成crash。(观察者释放后被观察者调用回调)

那么要避免就要在添加和移除以及dealloc过程中做容错处理。

NSObject(NSKeyValueObservingCustomization)中发现了observationInfo


/*
Take or return a pointer that identifies information about all of the observers that are registered with the receiver, the options that were used at registration-time, etc.
The default implementation of these methods store observation info in a global dictionary keyed by the receivers' pointers. For improved performance, you can override these methods to store the opaque data pointer in an instance variable.
Overrides of these methods must not attempt to send Objective-C messages to the passed-in observation info, including -retain and -release.
*/

@property (nullable) void *observationInfo NS_RETURNS_INNER_POINTER;
根据注释可以看到默认情况下observationInfo中保存了所有观察者信息。
那么observationInfo保存在哪里呢?直接代码验证下:


NSLog(@"observed before %@",self.obj.observationInfo);
NSLog(@"observe before %@",self.observationInfo);
[self.obj addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
NSLog(@"observe after %@",self.observationInfo);
NSLog(@"observed after %@",self.obj.observationInfo);
输出

observed before (null)
observe before (null)
observe after (null)
observed after <NSKeyValueObservationInfo 0x60000100c700> (
<NSKeyValueObservance 0x600001ee0c90: Observer: 0x7fd6eb112cb0, Key path: name, Options: <New: YES, Old: NO, Prior: NO> Context: 0x0, Property: 0x600001ee1050>
)

可以看到在注册后存入到了被观察者中,类型是NSKeyValueObservationInfo,它是一个私有类。NSKeyValueObservationInfo中保存的是NSKeyValueObservance
NSKeyValueObservationInfo保存了NSKeyValueObservance集合,NSKeyValueObservance中保存了观察者注册的时候的信息。
既然是在Foundation框架中,那么dump一下这个动态库的头文件(越狱手机使用classdump-dyld导出头文件)。

NSKeyValueObservationInfo头文件:

@class NSArray;
@interface NSKeyValueObservationInfo : NSObject {
NSArray* _observances;
unsigned long long _cachedHash;
BOOL _cachedIsShareable;
}
@property (nonatomic,readonly) BOOL containsOnlyInternalObservationHelpers;
-(void)dealloc;
-(unsigned long long)hash;
-(id)_initWithObservances:(id*)arg1 count:(unsigned long long)arg2 hashValue:(unsigned long long)arg3 ;
-(id)description;
-(BOOL)containsOnlyInternalObservationHelpers;
-(BOOL)isEqual:(id)arg1 ;
-(id)_copyByAddingObservance:(id)arg1 ;
@end

NSKeyValueObservance头文件:

@class NSObject, NSKeyValueProperty;
@interface NSKeyValueObservance : NSObject {
NSObject* _observer;
NSKeyValueProperty* _property;
void* _context;
NSObject* _originalObservable;
unsigned _options : 6;
unsigned _cachedIsShareable : 1;
unsigned _isInternalObservationHelper : 1;
}
-(id)_initWithObserver:(id)arg1 property:(id)arg2 options:(unsigned long long)arg3 context:(void*)arg4 originalObservable:(id)arg5 ;
-(unsigned long long)hash;
-(id)description;
-(BOOL)isEqual:(id)arg1 ;
-(void)observeValueForKeyPath:(id)arg1 ofObject:(id)arg2 change:(id)arg3 context:(void*)arg4 ;
@end
那么基本可以确定_observances中保存的是NSKeyValueObservance
代码验证:



它的定义如下:

@class NSKeyValueContainerClass, NSString;
@interface NSKeyValueProperty : NSObject <NSCopying> {
NSKeyValueContainerClass* _containerClass;
NSString* _keyPath;
}
-(Class)isaForAutonotifying;
-(id)_initWithContainerClass:(id)arg1 keyPath:(id)arg2 propertiesBeingInitialized:(CFSetRef)arg3 ;
-(id)dependentValueKeyOrKeysIsASet:(BOOL*)arg1 ;
-(void)object:(id)arg1 withObservance:(id)arg2 didChangeValueForKeyOrKeys:(id)arg3 recurse:(BOOL)arg4 forwardingValues:(SCD_Struct_NS48)arg5 ;
-(BOOL)object:(id)arg1 withObservance:(id)arg2 willChangeValueForKeyOrKeys:(id)arg3 recurse:(BOOL)arg4 forwardingValues:(SCD_Struct_NS48*)arg5 ;
-(void)object:(id)arg1 didAddObservance:(id)arg2 recurse:(BOOL)arg3 ;
-(id)restOfKeyPathIfContainedByValueForKeyPath:(id)arg1 ;
-(void)object:(id)arg1 didRemoveObservance:(id)arg2 recurse:(BOOL)arg3 ;
-(BOOL)matchesWithoutOperatorComponentsKeyPath:(id)arg1 ;
-(id)copyWithZone:(NSZone*)arg1 ;
-(id)keyPath;
-(void)dealloc;
-(id)keyPathIfAffectedByValueForKey:(id)arg1 exactMatch:(BOOL*)arg2 ;
-(id)keyPathIfAffectedByValueForMemberOfKeys:(id)arg1 ;
@end
  • _observer存储在NSKeyValueObservance 中。
  • _keyPath存储在NSKeyValueObservance_propertyNSKeyValueProperty)中。

3.1Hook注册和移除方法

要对系统方法进行容错处理那么最好的办法就是Hook了,直接对添加和移除的3个方法进行Hook处理:

+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self hp_methodSwizzleWithClass:self oriSEL:@selector(addObserver:forKeyPath:options:context:) swizzledSEL:@selector(hp_addObserver:forKeyPath:options:context:) isClassMethod:NO];
[self hp_methodSwizzleWithClass:self oriSEL:@selector(removeObserver:forKeyPath:context:) swizzledSEL:@selector(hp_removeObserver:forKeyPath:context:)isClassMethod:NO];
[self hp_methodSwizzleWithClass:self oriSEL:@selector(removeObserver:forKeyPath:) swizzledSEL:@selector(hp_removeObserver:forKeyPath:)isClassMethod:NO];
});
}
  • 由于removeObserver:forKeyPath:底层调用的不是removeObserver:forKeyPath:context:所以两个方法都要Hook

那么核心逻辑就是怎么判断observer对应的keyPath是否存在。由于observationInfo存储的是私有类,那么直接通过kvc获取值:

- (BOOL)keyPathIsExist:(NSString *)sarchKeyPath observer:(id)observer {
BOOL findKey = NO;
id info = self.observationInfo;
if (info) {
NSArray *observances = [info valueForKeyPath:@"_observances"];
for (id observance in observances) {
id tempObserver = [observance valueForKey:@"_observer"];
if (tempObserver == observer) {
NSString *keyPath = [observance valueForKeyPath:@"_property._keyPath"];
if ([keyPath isEqualToString:sarchKeyPath]) {
findKey = YES;
break;
}
}
}
}
return findKey;
}

Hook的具体实现:

- (void)hp_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context {
if ([self keyPathIsExist:keyPath observer:observer]) {//observer 观察者已经添加了对应key的观察,再次添加不做处理。
return;
}
[self hp_addObserver:observer forKeyPath:keyPath options:options context:context];
}

- (void)hp_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context {
if ([self keyPathIsExist:keyPath observer:observer]) {//key存在才移除
[self hp_removeObserver:observer forKeyPath:keyPath context:context];
}
}

- (void)hp_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {
if ([self keyPathIsExist:keyPath observer:observer]) {//key存在才移除
[self hp_removeObserver:observer forKeyPath:keyPath];
}
}

  • 这样就解决了重复添加和移除的问题。

3.2 自动移除观察者

3.1中解决了重复添加和移除的问题,还有一个问题是dealloc的时候自动移除。这块思路与自定义kvo相同,通过Hook观察者的的dealloc实现。

- (void)hp_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context {
if ([self keyPathIsExist:keyPath observer:observer]) {//observer 观察者已经添加了对应key的观察,再次添加不做处理。
return;
}
NSString *className = NSStringFromClass([self class]);
NSString *newClassName = [NSString stringWithFormat:@"NSKVONotifying_%@",className];
Class newClass = NSClassFromString(newClassName);
if (!newClass) {//类不存在的时候进行 hook 观察者 dealloc
//hook dealloc
[[observer class] hp_methodSwizzleWithClass:[observer class] oriSEL:NSSelectorFromString(@"dealloc") swizzledSEL:@selector(hp_dealloc) isClassMethod:NO];
}
[self hp_addObserver:observer forKeyPath:keyPath options:options context:context];
}

- (void)hp_dealloc {
[self hp_removeSelfAllObserverd];
[self hp_dealloc];
}
  • kvo子类已经存在的时候证明已经hook过了。

deallocself.observationInfo是获取不到信息的,因为observationInfo是存储在被观察者中的。所以还需要自己存储信息。
修改如下:

static NSString *const kHPSafeKVOObserverdAssiociateKey = @"HPSafeKVOObserverdAssiociateKey";

@interface HPSafeKVOObservedInfo : NSObject

@property (nonatomic, weak) id observerd;
@property (nonatomic, copy) NSString *keyPath;
@property (nonatomic, strong) id context;

@end

@implementation HPSafeKVOObservedInfo

- (instancetype)initWitObserverd:(NSObject *)observerd forKeyPath:(NSString *)keyPath context:(nullable void *)context {
if (self=[super init]) {
_observerd = observerd;
_keyPath = keyPath;
_context = (__bridge id)(context);
}
return self;
}

@end

- (void)hp_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context {
if ([self keyPathIsExist:keyPath observer:observer]) {//observer 观察者已经添加了对应key的观察,再次添加不做处理。
return;
}
NSString *className = NSStringFromClass([self class]);
NSString *newClassName = [NSString stringWithFormat:@"NSKVONotifying_%@",className];
Class newClass = NSClassFromString(newClassName);
if (!newClass) {//类不存在的时候进行 hook 观察者 dealloc
//hook dealloc
[[observer class] hp_methodSwizzleWithClass:[observer class] oriSEL:NSSelectorFromString(@"dealloc") swizzledSEL:@selector(hp_dealloc) isClassMethod:NO];
}

//保存被观察者信息
HPSafeKVOObservedInfo *kvoObservedInfo = [[HPSafeKVOObservedInfo alloc] initWitObserverd:self forKeyPath:keyPath context:context];
NSMutableArray *observerdArray = objc_getAssociatedObject(observer, (__bridge const void * _Nonnull)(kHPSafeKVOObserverdAssiociateKey));
if (!observerdArray) {
observerdArray = [NSMutableArray arrayWithCapacity:1];
}
[observerdArray addObject:kvoObservedInfo];
objc_setAssociatedObject(observer, (__bridge const void * _Nonnull)(kHPSafeKVOObserverdAssiociateKey), observerdArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

//调用原始方法
[self hp_addObserver:observer forKeyPath:keyPath options:options context:context];
}

hp_dealloc中主动调用移除方法:

- (void)hp_dealloc {
[self hp_removeSelfAllObserverd];
[self hp_dealloc];
}

- (void)hp_removeSelfAllObserverd {
NSMutableArray *observerdArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kHPSafeKVOObserverdAssiociateKey));
for (HPSafeKVOObservedInfo *info in observerdArray) {
if (info.observerd) {
//调用系统方法,已经hook了,走hook逻辑。
if (info.context) {
[info.observerd removeObserver:self forKeyPath:info.keyPath context:(__bridge void * _Nullable)(info.context)];
} else {
[info.observerd removeObserver:self forKeyPath:info.keyPath];
}
}
}
}

这样在dealloc的时候就主动清空了已经释放掉的observer

3.3 问题处理

上面这样处理后在退出页面的时候发生了crash(非必现),堆栈如下:



UIScreen 观察了 CADisplay 的 cloned。但是在释放的过程中UIScreen却没有调用到Hookhp_dealloc中,对应的汇编实现:

int -[UIScreen dealloc](int arg0) {
[r0 _invalidate];
__UIScreenWriteDisplayConfiguration(r0, 0x0, 0x0);
r0 = [[&stack[0] super] dealloc];
return r0;
}

int -[UIScreen _invalidate](int arg0) {
var_10 = r20;
stack[-24] = r19;
r31 = r31 + 0xffffffffffffffe0;
saved_fp = r29;
stack[-8] = r30;
r19 = arg0;
*(int16_t *)(arg0 + 0xb0) = *(int16_t *)(arg0 + 0xb0) & 0xffffffffffffffcf;
r0 = [NSNotificationCenter defaultCenter];
r0 = [r0 retain];
[r0 removeObserver:r19];
[r0 release];
if ([r19 _isCarScreen] == 0x0) goto loc_7495b4;

loc_749570:
r0 = __UIInternalPreferenceUsesDefault_751e78(0x19080b0, @"ApplySceneUserInterfaceStyleToCarScreen", 0xec7178);
if (((*(int8_t *)0x19080b4 & 0x1) == 0x0) || (r0 != 0x0)) goto loc_74959c;

loc_7495b4:
[r19 _endObservingBacklightLevelNotifications];
[r19 _setSoftwareDimmingWindow:0x0];
r0 = *(r19 + 0x90);
r0 = [r0 _setScreen:0x0];
return r0;

loc_74959c:
CFNotificationCenterRemoveObserver(CFNotificationCenterGetDarwinNotifyCenter(), r19, @"CarPlayUserInterfaceStyleDidChangeNotification", 0x0);
goto loc_7495b4;
}
那么意味着是否没有替换成功?
_UIScreenWriteDisplayConfiguration中确实先移除后添加:



是否进行注册是通过rax控制的。也就是__UIScreenIsCapturedValueOverride.isCapturedValue控制的。
经过测试只要在系统自动调用UIScreen initialize之前调用一个UIScreen相关方法就不走kvo设置逻辑了,比如:

[UIScreen mainScreen]
//[UIScreen class]

目前不清楚原因。所以处理这个问题有两个思路:

  1. + load进行方法交换的时候先调用[UIScreen class]
  1. 在注册的时候对系统类或者自己的类进行过滤。
  • 2.1只排除UIScreen
if ([observer isKindOfClass:[UIScreen class]]) {
[self hp_addObserver:observer forKeyPath:keyPath options:options context:context];
return;
}
  • 2.2排除系统类
NSString *className = NSStringFromClass([observer class]);
if ([className hasPrefix:@"NS"] || [className hasPrefix:@"UI"]) { //排除某些系统类。
[self hp_addObserver:observer forKeyPath:keyPath options:options context:context];
return;
}
  • 2.3 只处理自己的类
NSString *className = NSStringFromClass([observer class]);
if (![className hasPrefix:@"HP"]) { //排除某些系统类。
[self hp_addObserver:observer forKeyPath:keyPath options:options context:context];
return;
}


收起阅读 »

自定义KVO(二)

2.2.2 优化Hook逻辑上面在+ load中Hook dealloc方法是在NSObject分类中处理的,那么意味着所有的类的dealloc方法都被Hook了。显然这么做是不合理的。逻辑就是仅对需要的类进行Hook dealloc方法,所以将Hook延迟到...
继续阅读 »

2.2.2 优化Hook逻辑

上面在+ loadHook dealloc方法是在NSObject分类中处理的,那么意味着所有的类的dealloc方法都被Hook了。显然这么做是不合理的。
逻辑就是仅对需要的类进行Hook dealloc方法,所以将Hook延迟到addObserver中:

- (void)hp_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(HPKVOBlock)block {
……
//hook dealloc
[[observer class] hp_methodSwizzleWithClass:[observer class] oriSEL:NSSelectorFromString(@"dealloc") swizzledSEL:@selector(hp_dealloc) isClassMethod:NO];
}

但是只应该对dealloc hook一次,否则又交换回来了。要么做标记,要么在创建kvo子类的时候进行hook。显然在创建子类的时候更合适。修改逻辑如下:
//申请类-注册类-添加方法
- (Class)_creatKVOClassWithKeyPath:(NSString *)keyPath observer:(NSObject *)observer {
//这里重写class后kvo子类也返回的是父类的名字
NSString *superClassName = NSStringFromClass([self class]);
NSString *newClassName = [NSString stringWithFormat:@"%@%@",kHPBlockKVOClassPrefix,superClassName];
Class newClass = NSClassFromString(newClassName);
//类是否存在
if (!newClass) {//不存在需要创建类
//1:申请类 父类、新类名称、额外空间
newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
//2:注册类
objc_registerClassPair(newClass);
//3:添加class方法,class返回父类信息 这里是`-class`
SEL classSEL = NSSelectorFromString(@"class");
Method classMethod = class_getInstanceMethod([self class], classSEL);
const char *classTypes = method_getTypeEncoding(classMethod);
class_addMethod(newClass, classSEL, (IMP)_hp_class, classTypes);

//hook dealloc
[[observer class] hp_methodSwizzleWithClass:[observer class] oriSEL:NSSelectorFromString(@"dealloc") swizzledSEL:@selector(hp_dealloc) isClassMethod:NO];
}
//4:添加setter方法
SEL setterSEL = NSSelectorFromString(_setterForGetter(keyPath));
Method setterMethod = class_getInstanceMethod([self class], setterSEL);
const char *setterTypes = method_getTypeEncoding(setterMethod);
class_addMethod(newClass, setterSEL, (IMP)_hp_setter, setterTypes);

return newClass;
}

完整实现代码:

typedef void(^HPKVOBlock)(id observer,NSString *keyPath,id oldValue,id newValue);

@interface NSObject (HP_KVO_Block)

- (void)hp_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(HPKVOBlock)block;

- (void)hp_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

@end

#import "NSObject+HP_KVO_Block.h"
#import <objc/runtime.h>
#import <objc/message.h>
#import "HPKVOInfo.h"

static NSString *const kHPBlockKVOClassPrefix = @"HPKVONotifying_";
static NSString *const kHPBlockKVOAssiociateKey = @"HPKVOAssiociateKey";

static NSString *const kHPBlockKVOObserverdAssiociateKey = @"HPKVOObserverdAssiociateKey";

@interface HPKVOBlockInfo : NSObject

@property (nonatomic, weak) id observer;
@property (nonatomic, copy) NSString *keyPath;
@property (nonatomic, copy) HPKVOBlock handleBlock;

@end

@implementation HPKVOBlockInfo

- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handleBlock:(HPKVOBlock)block {
if (self=[super init]) {
_observer = observer;
_keyPath = keyPath;
_handleBlock = block;
}
return self;
}

@end

@interface HPKVOObservedInfo : NSObject

@property (nonatomic, weak) id observerd;
@property (nonatomic, copy) NSString *keyPath;

@end

@implementation HPKVOObservedInfo

- (instancetype)initWitObserverd:(NSObject *)observerd forKeyPath:(NSString *)keyPath {
if (self=[super init]) {
_observerd = observerd;
_keyPath = keyPath;
}
return self;
}

@end

@implementation NSObject (HP_KVO_Block)


- (void)hp_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(HPKVOBlock)block {
//1.参数判断 以及 setter检查
if (!observer || !keyPath) return;
BOOL result = [self _handleSetterMethodFromKeyPath:keyPath];
if (!result) return;

//2.isa_swizzle 申请类-注册类-添加方法
Class newClass = [self _creatKVOClassWithKeyPath:keyPath observer:observer];

//3.isa 指向子类
object_setClass(self, newClass);
//4.setter逻辑处理
//保存观察者信息-数组
HPKVOBlockInfo *kvoInfo = [[HPKVOBlockInfo alloc] initWitObserver:observer forKeyPath:keyPath handleBlock:block];
NSMutableArray *observerArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kHPBlockKVOAssiociateKey));
if (!observerArray) {
observerArray = [NSMutableArray arrayWithCapacity:1];
}
[observerArray addObject:kvoInfo];
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kHPBlockKVOAssiociateKey), observerArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

//保存被观察者信息
HPKVOObservedInfo *kvoObservedInfo = [[HPKVOObservedInfo alloc] initWitObserverd:self forKeyPath:keyPath];
NSMutableArray *observerdArray = objc_getAssociatedObject(observer, (__bridge const void * _Nonnull)(kHPBlockKVOObserverdAssiociateKey));
if (!observerdArray) {
observerdArray = [NSMutableArray arrayWithCapacity:1];
}
[observerdArray addObject:kvoObservedInfo];
objc_setAssociatedObject(observer, (__bridge const void * _Nonnull)(kHPBlockKVOObserverdAssiociateKey), observerdArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (void)hp_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {
NSMutableArray *observerArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kHPBlockKVOAssiociateKey));
if (observerArray.count <= 0) {
return;
}

NSMutableArray *tempArray = [observerArray mutableCopy];
for (HPKVOInfo *info in tempArray) {
if ([info.keyPath isEqualToString:keyPath]) {
if (info.observer) {
if (info.observer == observer) {
[observerArray removeObject:info];
}
} else {
[observerArray removeObject:info];
}
}
}
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kHPBlockKVOAssiociateKey), observerArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
//已经全部移除了
if (observerArray.count <= 0) {
//isa指回给父类
Class superClass = [self class];
object_setClass(self, superClass);
}
}

- (BOOL)_handleSetterMethodFromKeyPath:(NSString *)keyPath {
SEL setterSeletor = NSSelectorFromString(_setterForGetter(keyPath));
Method setterMethod = class_getInstanceMethod(object_getClass(self), setterSeletor);
NSAssert(setterMethod, @"%@ setter is not exist",keyPath);
return setterMethod ? YES : NO;
}

// 从get方法获取set方法的名称 key -> setKey
static NSString *_setterForGetter(NSString *getter) {
if (getter.length <= 0) { return nil;}
NSString *firstString = [[getter substringToIndex:1] uppercaseString];
NSString *otherString = [getter substringFromIndex:1];
return [NSString stringWithFormat:@"set%@%@:",firstString,otherString];
}

//申请类-注册类-添加方法
- (Class)_creatKVOClassWithKeyPath:(NSString *)keyPath observer:(NSObject *)observer {
//这里重写class后kvo子类也返回的是父类的名字
NSString *superClassName = NSStringFromClass([self class]);
NSString *newClassName = [NSString stringWithFormat:@"%@%@",kHPBlockKVOClassPrefix,superClassName];
Class newClass = NSClassFromString(newClassName);
//类是否存在
if (!newClass) {//不存在需要创建类
//1:申请类 父类、新类名称、额外空间
newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
//2:注册类
objc_registerClassPair(newClass);
//3:添加class方法,class返回父类信息 这里是`-class`
SEL classSEL = NSSelectorFromString(@"class");
Method classMethod = class_getInstanceMethod([self class], classSEL);
const char *classTypes = method_getTypeEncoding(classMethod);
class_addMethod(newClass, classSEL, (IMP)_hp_class, classTypes);

//hook dealloc
[[observer class] hp_methodSwizzleWithClass:[observer class] oriSEL:NSSelectorFromString(@"dealloc") swizzledSEL:@selector(hp_dealloc) isClassMethod:NO];
}
//4:添加setter方法
SEL setterSEL = NSSelectorFromString(_setterForGetter(keyPath));
Method setterMethod = class_getInstanceMethod([self class], setterSEL);
const char *setterTypes = method_getTypeEncoding(setterMethod);
class_addMethod(newClass, setterSEL, (IMP)_hp_setter, setterTypes);

return newClass;
}

//返回父类信息
Class _hp_class(id self,SEL _cmd) {
return class_getSuperclass(object_getClass(self));
}

static void _hp_setter(id self,SEL _cmd,id newValue) {
//自动开关判断,省略
//保存旧值
NSString *keyPath = _getterForSetter(NSStringFromSelector(_cmd));
id oldValue = [self valueForKey:keyPath];
//1.调用父类的setter(也可以通过performSelector调用)
void (*hp_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper;
struct objc_super super_struct = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self)),
};
hp_msgSendSuper(&super_struct,_cmd,newValue);

//2.通知观察者
NSMutableArray *observerArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kHPBlockKVOAssiociateKey));
for (HPKVOBlockInfo *info in observerArray) {//循环调用,可能添加多次。
if ([info.keyPath isEqualToString:keyPath] && info.handleBlock && info.observer) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
info.handleBlock(info.observer, keyPath, oldValue, newValue);
});
}
}
}

//获取getter
static NSString *_getterForSetter(NSString *setter){
if (setter.length <= 0 || ![setter hasPrefix:@"set"] || ![setter hasSuffix:@":"]) { return nil;}
NSRange range = NSMakeRange(3, setter.length - 4);
NSString *getter = [setter substringWithRange:range];
NSString *firstString = [[getter substringToIndex:1] lowercaseString];
return [getter stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstString];
}

+ (void)hp_methodSwizzleWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL isClassMethod:(BOOL)isClassMethod {
if (!cls) {
NSLog(@"class is nil");
return;
}
if (!swizzledSEL) {
NSLog(@"swizzledSEL is nil");
return;
}
//类/元类
Class swizzleClass = isClassMethod ? object_getClass(cls) : cls;
Method oriMethod = class_getInstanceMethod(swizzleClass, oriSEL);
Method swiMethod = class_getInstanceMethod(swizzleClass, swizzledSEL);
if (!oriMethod) {//原始方法没有实现
// 在oriMethod为nil时,替换后将swizzledSEL复制一个空实现
class_addMethod(swizzleClass, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
//添加一个空的实现
method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){
NSLog(@"imp default null implementation");
}));
}
//自己没有则会添加成功,自己有添加失败
BOOL success = class_addMethod(swizzleClass, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(oriMethod));
if (success) {//自己没有方法添加一个,添加成功则证明自己没有。
class_replaceMethod(swizzleClass, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
} else { //自己有直接进行交换
method_exchangeImplementations(oriMethod, swiMethod);
}
}

- (void)hp_dealloc {
NSMutableArray *observerdArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kHPBlockKVOObserverdAssiociateKey));
for (HPKVOObservedInfo *info in observerdArray) {
if (info.observerd) {
[info.observerd hp_removeObserver:self forKeyPath:info.keyPath];
}
}
[self hp_dealloc];
}

@end


收起阅读 »

自定义KVO(一)

kvo1.1 hp_addObserver由于只有属性才有效,所以先进行容错处理。1.1.2 isa_swizzle动态生成子类static NSString *const kHPKVOClassPrefix = @"HPKVONotifying_"; //...
继续阅读 »

实现一个简单的kvo

@interface NSObject (HP_KVO)

- (void)hp_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- (void)hp_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;
- (void)hp_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

@end

1.1 hp_addObserver

1.1.1参数检查

- (void)hp_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context {
//1.参数判断 以及 setter检查
if (!observer || !keyPath) return;
BOOL result = [self handleSetterMethodFromKeyPath:keyPath];
if (!result) return;
}

- (BOOL)handleSetterMethodFromKeyPath:(NSString *)keyPath {
SEL setterSeletor = NSSelectorFromString(setterForGetter(keyPath));
Method setterMethod = class_getInstanceMethod(object_getClass(self), setterSeletor);
NSAssert(setterMethod, @"%@ setter is not exist",keyPath);
return setterMethod ? YES : NO;
}

// 从get方法获取set方法的名称 key -> setKey
static NSString *setterForGetter(NSString *getter) {
if (getter.length <= 0) { return nil;}
NSString *firstString = [[getter substringToIndex:1] uppercaseString];
NSString *otherString = [getter substringFromIndex:1];
return [NSString stringWithFormat:@"set%@%@:",firstString,otherString];
}

由于只有属性才有效,所以先进行容错处理。

1.1.2 isa_swizzle动态生成子类

static NSString *const kHPKVOClassPrefix = @"HPKVONotifying_";

//申请类-注册类-添加方法
- (Class)creatKVOClassWithKeyPath:(NSString *)keyPath {
//这里重写class后kvo子类也返回的是父类的名字
NSString *superClassName = NSStringFromClass([self class]);
NSString *newClassName = [NSString stringWithFormat:@"%@%@",kHPKVOClassPrefix,superClassName];
Class newClass = NSClassFromString(newClassName);
//类是否存在
if (!newClass) {//不存在需要创建类
//1:申请类 父类、新类名称、额外空间
newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
//2:注册类
objc_registerClassPair(newClass);
//3:添加class方法,class返回父类信息 这里是`-class`
SEL classSEL = NSSelectorFromString(@"class");
Method classMethod = class_getInstanceMethod([self class], classSEL);
const char *classTypes = method_getTypeEncoding(classMethod);
class_addMethod(newClass, classSEL, (IMP)hp_class, classTypes);
}
//4:添加setter方法
SEL setterSEL = NSSelectorFromString(setterForGetter(keyPath));
Method setterMethod = class_getInstanceMethod([self class], setterSEL);
const char *setterTypes = method_getTypeEncoding(setterMethod);
class_addMethod(newClass, setterSEL, (IMP)hp_setter, setterTypes);

return newClass;
}

//返回父类信息
Class hp_class(id self,SEL _cmd){
return class_getSuperclass(object_getClass(self));
}

static void hp_setter(id self,SEL _cmd,id newValue){

}
  • 根据类名字拼接kvo类名字,判断是否已经存在。(superClassName由于class重写了,即使二次进入也获取到的是父类的名字)。
  • newClass不存在则调用objc_allocateClassPair创建kvo子类。并且重写- class方法。
  • 添加对应的setter方法。

当然也可以写+class,写入元类中。在objc_allocateClassPair后元类就存在了:




1.1.3 isa 指向子类

object_setClass(self, newClass);
  • 直接调用object_setClass设置objisa为新创建的kvo子类。

object_setClass源码:


Class object_setClass(id obj, Class cls)
{
if (!obj) return nil;
if (!cls->isFuture() && !cls->isInitialized()) {
lookUpImpOrNilTryCache(nil, @selector(initialize), cls, LOOKUP_INITIALIZE);
}

return obj->changeIsa(cls);
}

源码中就是修改对象的isa指向。


1.1.4 setter逻辑

在进行了上面逻辑的处理后,这个时候调用如下代码:

[self.obj hp_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
self.obj.name = @"HP";

会进入hp_setter函数。目前从HPObjectsetterName替换到了HPKVONotifying_ HPObjecthp_setter函数中。

hp_setter主要逻辑分两部分:调用父类方法以及发送通知

static void hp_setter(id self,SEL _cmd,id newValue) {
//自动开关判断,省略
//1.调用父类的setter(也可以通过performSelector调用)
void (*hp_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper;
struct objc_super super_struct = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self)),
};
hp_msgSendSuper(&super_struct,_cmd,newValue);

//2.通知观察者
[observer hp_observeValueForKeyPath:getterForSetter(_cmd) ofObject:self change:@{} context:NULL];
}
  • 调用父类方法可以通过objc_msgSendSuper实现。
  • 通知观察者keypath可以通过_cmd转换获取,objectselfchange也可以获取到,context可以先不传。那么核心就是observer的获取。

通知观察者
首先想到的是用属性存储observer,那么有个问题在类已经创建后就无法添加了。所以关联属性明显更合适。在hp_addObserver中添加关联对象:

static NSString *const kHPKVOAssiociateKey = @"HPKVOAssiociateKey";

objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kHPKVOAssiociateKey), observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

通知逻辑实现:

//2.通知观察者
id observer = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kHPKVOAssiociateKey));
[observer hp_observeValueForKeyPath:getterForSetter(NSStringFromSelector(_cmd)) ofObject:self change:@{@"kind":@1,@"new":newValue} context:NULL];

//获取getter
static NSString *getterForSetter(NSString *setter){
if (setter.length <= 0 || ![setter hasPrefix:@"set"] || ![setter hasSuffix:@":"]) { return nil;}
NSRange range = NSMakeRange(3, setter.length - 4);
NSString *getter = [setter substringWithRange:range];
NSString *firstString = [[getter substringToIndex:1] lowercaseString];
return [getter stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstString];
}

这个时候在hp_observeValueForKeyPath中就有回调了:

change:{
kind = 1;
new = HP;
}

1.1.5 观察者信息保存

上面的逻辑虽然简单实现了,但是存在一个严重问题,观察多个属性的时候以及新旧值都要观察以及传递了context的情况就无效了。
那么就需要保存观察者相关的信息,创建一个新类HPKVOInfo实现如下:

typedef NS_OPTIONS(NSUInteger, HPKeyValueObservingOptions) {
HPKeyValueObservingOptionNew = 0x01,
HPKeyValueObservingOptionOld = 0x02,
};

@interface HPKVOInfo : NSObject

@property (nonatomic, weak) id observer;
@property (nonatomic, copy) NSString *keyPath;
@property (nonatomic, assign) HPKeyValueObservingOptions options;
@property (nonatomic, strong) id context;

- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(HPKeyValueObservingOptions)options context:(nullable void *)context;

@end

@implementation HPKVOInfo

- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(HPKeyValueObservingOptions)options context:(nullable void *)context {
self = [super init];
if (self) {
self.observer = observer;
self.keyPath = keyPath;
self.options = options;
self.context = (__bridge id _Nonnull)(context);
}
return self;
}

@end

hp_addObserver中信息保存修改如下:

//保存观察者信息-数组
HPKVOInfo *kvoInfo = [[HPKVOInfo alloc] initWitObserver:observer forKeyPath:keyPath options:options context:context];
NSMutableArray *observerArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kHPKVOAssiociateKey));
if (!observerArray) {
observerArray = [NSMutableArray arrayWithCapacity:1];
}
[observerArray addObject:kvoInfo];
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kHPKVOAssiociateKey), observerArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

hp_setter逻辑修改如下:

static void hp_setter(id self,SEL _cmd,id newValue) {
//自动开关判断,省略
//保存旧值
NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
id oldValue = [self valueForKey:keyPath];
//1.调用父类的setter(也可以通过performSelector调用)
void (*hp_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper;
struct objc_super super_struct = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self)),
};
hp_msgSendSuper(&super_struct,_cmd,newValue);

//2.通知观察者
NSMutableArray *observerArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kHPKVOAssiociateKey));
for (HPKVOInfo *info in observerArray) {//循环调用,可能添加多次。
if ([info.keyPath isEqualToString:keyPath]) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSMutableDictionary<NSKeyValueChangeKey,id> *change = [NSMutableDictionary dictionaryWithCapacity:1];
//对新旧值进行处理
if (info.options & HPKeyValueObservingOptionNew) {
[change setObject:newValue forKey:NSKeyValueChangeNewKey];
}
if (info.options & HPKeyValueObservingOptionOld) {
if (oldValue) {
[change setObject:oldValue forKey:NSKeyValueChangeOldKey];
} else {
[change setObject:@"" forKey:NSKeyValueChangeOldKey];
}
}
[change setObject:@1 forKey:@"kind"];
//消息发送给观察者
[info.observer hp_observeValueForKeyPath:keyPath ofObject:self change:change context:(__bridge void * _Nullable)(info.context)];
});
}
}
}
  • 在调用父类之前先获取旧值。
  • 取出关联对象数组数据,循环判断调用hp_observeValueForKeyPath通知观察者。

这个时候观察多个属性以及多次观察就都没问题了。

1.2 hp_removeObserver

观察者对象是保存在关联对象中,所以在移除的时候也需要删除关联对象,并且当没有观察者时就要回复isa指向了。

- (void)hp_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {
[self hp_removeObserver:observer forKeyPath:keyPath context:NULL];
}

- (void)hp_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(void *)context {
NSMutableArray *observerArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kHPKVOAssiociateKey));
if (observerArray.count <= 0) {
return;
}

NSMutableArray *tempArray = [observerArray mutableCopy];
for (HPKVOInfo *info in tempArray) {
if ([info.keyPath isEqualToString:keyPath]) {
if (info.observer) {
if (info.observer == observer) {
if (context != NULL) {
if (info.context == context) {
[observerArray removeObject:info];
}
} else {
[observerArray removeObject:info];
}
}
} else {
if (context != NULL) {
if (info.context == context) {
[observerArray removeObject:info];
}
} else {
[observerArray removeObject:info];
}
}
}
}
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kHPKVOAssiociateKey), observerArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
//已经全部移除了
if (observerArray.count <= 0) {
//isa指回给父类
Class superClass = [self class];
object_setClass(self, superClass);
}
}
  • 通过keyPath以及observercontext确定要移除的关联对象数据。
  • 当关联对象中没有数据的时候isa进行指回。

完整代码如下:

@interface NSObject (HP_KVO)

- (void)hp_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(HPKeyValueObservingOptions)options context:(nullable void *)context;
- (void)hp_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;
- (void)hp_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

- (void)hp_observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;

@end

#import "NSObject+HP_KVO.h"
#import
#import

static NSString *const kHPKVOClassPrefix = @"HPKVONotifying_";
static NSString *const kHPKVOAssiociateKey = @"HPKVOAssiociateKey";

@implementation NSObject (HP_KVO)

- (void)hp_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(HPKeyValueObservingOptions)options context:(nullable void *)context {
//1.参数判断 以及 setter检查
if (!observer || !keyPath) return;
BOOL result = [self handleSetterMethodFromKeyPath:keyPath];
if (!result) return;

//2.isa_swizzle 申请类-注册类-添加方法
Class newClass = [self creatKVOClassWithKeyPath:keyPath];

//3.isa 指向子类
object_setClass(self, newClass);
//4.setter逻辑处理
//保存观察者信息-数组
HPKVOInfo *kvoInfo = [[HPKVOInfo alloc] initWitObserver:observer forKeyPath:keyPath options:options context:context];
NSMutableArray *observerArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kHPKVOAssiociateKey));
if (!observerArray) {
observerArray = [NSMutableArray arrayWithCapacity:1];
}
[observerArray addObject:kvoInfo];
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kHPKVOAssiociateKey), observerArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (void)hp_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {
[self hp_removeObserver:observer forKeyPath:keyPath context:NULL];
}

- (void)hp_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(void *)context {
NSMutableArray *observerArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kHPKVOAssiociateKey));
if (observerArray.count <= 0) {
return;
}

NSMutableArray *tempArray = [observerArray mutableCopy];
for (HPKVOInfo *info in tempArray) {
if ([info.keyPath isEqualToString:keyPath]) {
if (info.observer) {
if (info.observer == observer) {
if (context != NULL) {
if (info.context == context) {
[observerArray removeObject:info];
}
} else {
[observerArray removeObject:info];
}
}
} else {
if (context != NULL) {
if (info.context == context) {
[observerArray removeObject:info];
}
} else {
[observerArray removeObject:info];
}
}
}
}
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kHPKVOAssiociateKey), observerArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
//已经全部移除了
if (observerArray.count <= 0) {
//isa指回给父类
Class superClass = [self class];
object_setClass(self, superClass);
}
}

- (BOOL)handleSetterMethodFromKeyPath:(NSString *)keyPath {
SEL setterSeletor = NSSelectorFromString(setterForGetter(keyPath));
Method setterMethod = class_getInstanceMethod(object_getClass(self), setterSeletor);
NSAssert(setterMethod, @"%@ setter is not exist",keyPath);
return setterMethod ? YES : NO;
}

// 从get方法获取set方法的名称 key -> setKey
static NSString *setterForGetter(NSString *getter) {
if (getter.length <= 0) { return nil;}
NSString *firstString = [[getter substringToIndex:1] uppercaseString];
NSString *otherString = [getter substringFromIndex:1];
return [NSString stringWithFormat:@"set%@%@:",firstString,otherString];
}

//申请类-注册类-添加方法
- (Class)creatKVOClassWithKeyPath:(NSString *)keyPath {
//这里重写class后kvo子类也返回的是父类的名字
NSString *superClassName = NSStringFromClass([self class]);
NSString *newClassName = [NSString stringWithFormat:@"%@%@",kHPKVOClassPrefix,superClassName];
Class newClass = NSClassFromString(newClassName);
//类是否存在
if (!newClass) {//不存在需要创建类
//1:申请类 父类、新类名称、额外空间
newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
//2:注册类
objc_registerClassPair(newClass);
//3:添加class方法,class返回父类信息 这里是`-class`
SEL classSEL = NSSelectorFromString(@"class");
Method classMethod = class_getInstanceMethod([self class], classSEL);
const char *classTypes = method_getTypeEncoding(classMethod);
class_addMethod(newClass, classSEL, (IMP)hp_class, classTypes);
}
//4:添加setter方法
SEL setterSEL = NSSelectorFromString(setterForGetter(keyPath));
Method setterMethod = class_getInstanceMethod([self class], setterSEL);
const char *setterTypes = method_getTypeEncoding(setterMethod);
class_addMethod(newClass, setterSEL, (IMP)hp_setter, setterTypes);

return newClass;
}

//返回父类信息
Class hp_class(id self,SEL _cmd) {
return class_getSuperclass(object_getClass(self));
}

static void hp_setter(id self,SEL _cmd,id newValue) {
//自动开关判断,省略
//保存旧值
NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
id oldValue = [self valueForKey:keyPath];
//1.调用父类的setter(也可以通过performSelector调用)
void (*hp_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper;
struct objc_super super_struct = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self)),
};
hp_msgSendSuper(&super_struct,_cmd,newValue);

//2.通知观察者
NSMutableArray *observerArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kHPKVOAssiociateKey));
for (HPKVOInfo *info in observerArray) {//循环调用,可能添加多次。
if ([info.keyPath isEqualToString:keyPath]) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSMutableDictionary<NSKeyValueChangeKey,id> *change = [NSMutableDictionary dictionaryWithCapacity:1];
//对新旧值进行处理
if (info.options & HPKeyValueObservingOptionNew) {
[change setObject:newValue forKey:NSKeyValueChangeNewKey];
}
if (info.options & HPKeyValueObservingOptionOld) {
if (oldValue) {
[change setObject:oldValue forKey:NSKeyValueChangeOldKey];
} else {
[change setObject:@"" forKey:NSKeyValueChangeOldKey];
}
}
[change setObject:@1 forKey:@"kind"];
//消息发送给观察者
[info.observer hp_observeValueForKeyPath:keyPath ofObject:self change:change context:(__bridge void * _Nullable)(info.context)];
});
}
}
}

//获取getter
static NSString *getterForSetter(NSString *setter){
if (setter.length <= 0 || ![setter hasPrefix:@"set"] || ![setter hasSuffix:@":"]) { return nil;}
NSRange range = NSMakeRange(3, setter.length - 4);
NSString *getter = [setter substringWithRange:range];
NSString *firstString = [[getter substringToIndex:1] lowercaseString];
return [getter stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstString];
}

@end

二、kvo函数式编程

上面的自定义自定义kvo与系统的kvo实现都有一个问题,都需要三步曲。代码是分离的可读性并不好。

2.1 注册与回调绑定

可以定义一个block用来处理回调,这样就不需要回调方法了,注册和回调就可以在一起处理了。
直接修改注册方法为block实现:

typedef void(^HPKVOBlock)(id observer,NSString *keyPath,id oldValue,id newValue);

- (void)hp_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(HPKVOBlock)block {
……
//保存观察者信息-数组
HPKVOBlockInfo *kvoInfo = [[HPKVOBlockInfo alloc] initWitObserver:observer forKeyPath:keyPath handleBlock:block];
……
}
  • block实现也保存在HPKVOBlockInfo中,这样在回调的时候直接执行block实现就可以了。

修改回调逻辑:

NSMutableArray *observerArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kHPBlockKVOAssiociateKey));
for (HPKVOBlockInfo *info in observerArray) {//循环调用,可能添加多次。
if ([info.keyPath isEqualToString:keyPath] && info.handleBlock) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
info.handleBlock(info.observer, keyPath, oldValue, newValue);
});
}
}

  • 在回调的时候直接将新值与旧值一起返回。

注册调用逻辑:

[self.obj hp_addObserver:self forKeyPath:@"name" block:^(id  _Nonnull observer, NSString * _Nonnull keyPath, id  _Nonnull oldValue, id  _Nonnull newValue) {
NSLog(@"block: oldValue:%@,newValue:%@",oldValue,newValue);
}];

这样就替换了回调函数为block实现了,注册和回调逻辑在一起了。

2.2 kvo自动销毁

上面虽然实现了注册和回调绑定,但是在观察者dealloc的时候仍然需要remove
那么怎么能自动释放不需要主动调用呢?

removeObserver的过程中主要做了两件事,移除关联对象数组中的数据以及指回isa。关联对象不移除的后果是会继续调用回调,那么在调用的时候判断下observer存不存在来处理是否回调就可以了。核心就在指回isa了。

2.2.1 Hook dealloc

首先想到的就是Hook dealloc方法:

+ (void)hp_methodSwizzleWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL isClassMethod:(BOOL)isClassMethod {
if (!cls) {
NSLog(@"class is nil");
return;
}
if (!swizzledSEL) {
NSLog(@"swizzledSEL is nil");
return;
}
//类/元类
Class swizzleClass = isClassMethod ? object_getClass(cls) : cls;
Method oriMethod = class_getInstanceMethod(swizzleClass, oriSEL);
Method swiMethod = class_getInstanceMethod(swizzleClass, swizzledSEL);
if (!oriMethod) {//原始方法没有实现
// 在oriMethod为nil时,替换后将swizzledSEL复制一个空实现
class_addMethod(swizzleClass, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
//添加一个空的实现
method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){
NSLog(@"imp default null implementation");
}));
}
//自己没有则会添加成功,自己有添加失败
BOOL success = class_addMethod(swizzleClass, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(oriMethod));
if (success) {//自己没有方法添加一个,添加成功则证明自己没有。
class_replaceMethod(swizzleClass, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
} else { //自己有直接进行交换
method_exchangeImplementations(oriMethod, swiMethod);
}
}

+ (void)load {
[self hp_methodSwizzleWithClass:[self class] oriSEL:NSSelectorFromString(@"dealloc") swizzledSEL:@selector(hp_dealloc) isClassMethod:NO];
}

- (void)hp_dealloc {
// [self.obj hp_removeObserver:self forKeyPath:@""];
[self hp_dealloc];
}
hp_dealloc中调用hp_removeObserver移除观察者。这里有个问题是被观察者和keypath从哪里来?这里相当于是观察者的dealloc中调用。所以可以通过在注册的时候对观察者添加关联对象保存被观察者和keyPath

static NSString *const kHPBlockKVOObserverdAssiociateKey = @"HPKVOObserverdAssiociateKey";

@interface HPKVOObservedInfo : NSObject

@property (nonatomic, weak) id observerd;
@property (nonatomic, copy) NSString *keyPath;

@end

@implementation HPKVOObservedInfo

- (instancetype)initWitObserverd:(NSObject *)observerd forKeyPath:(NSString *)keyPath {
if (self=[super init]) {
_observerd = observerd;
_keyPath = keyPath;
}
return self;
}

@end


- (void)hp_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(HPKVOBlock)block {
……
//保存被观察者信息
HPKVOObservedInfo *kvoObservedInfo = [[HPKVOObservedInfo alloc] initWitObserverd:self forKeyPath:keyPath];
NSMutableArray *observerdArray = objc_getAssociatedObject(observer, (__bridge const void * _Nonnull)(kHPBlockKVOObserverdAssiociateKey));
if (!observerdArray) {
observerdArray = [NSMutableArray arrayWithCapacity:1];
}
[observerdArray addObject:kvoObservedInfo];
objc_setAssociatedObject(observer, (__bridge const void * _Nonnull)(kHPBlockKVOObserverdAssiociateKey), observerdArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

  • kvoObservedInfo中保存的是self也就是被观察者。
  • 关联对象关联在observer也就是观察者身上。

这个时候在dealloc中遍历对其进行移除:

- (void)hp_dealloc {
NSMutableArray *observerdArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kHPBlockKVOObserverdAssiociateKey));
for (HPKVOObservedInfo *info in observerdArray) {
if (info.observerd) {
[info.observerd hp_removeObserver:self forKeyPath:info.keyPath];
}
}
[self hp_dealloc];
}

当然这里的方法执行只针对被观察者没有释放的情况,释放了observerd就不存在了不需要调用remove逻辑了。

篇幅有限 下片继续



作者:HotPotCat
链接:https://www.jianshu.com/p/a57d0d98cc21








收起阅读 »

Android 11源码分析:从Activity的setContent方法看渲染流初识Window

在上一篇的分析中,我们已经知道DecorView以下的部分弄的很明白了,但是对于DecorView是如何显示在我们的屏幕上还是不太清楚。所以接着分析DecorView与PhoneWindow与Activity具体是如何建立联系的。 我们先弄清楚两个问题:Dec...
继续阅读 »

在上一篇的分析中,我们已经知道DecorView以下的部分弄的很明白了,但是对于DecorView是如何显示在我们的屏幕上还是不太清楚。所以接着分析DecorView与PhoneWindow与Activity具体是如何建立联系的。 我们先弄清楚两个问题:

  1. DecorView何时绘制到屏幕中
  2. DecorView如何绘制到屏幕中(addView,removeView,upDateViewLayout)
  3. Activity是如何得到Touch事件的

DecorView何时绘制到屏幕中

Activity的启动流程中提到执行Activity的performLaunchActivity方法执行的时,先是通过反射创建了Activity,然后会调用Activity的attach方法,在上一篇我们知道attach方法做了PhoneWindow的初始化操作,再然后才是执行生命周期onCreate,而setContent方法又是在onCreate执行后,看起来一切都很合理。

先创建一个Activity,然后再为这个Activity创建一个PhoneWindow,执行onCreate,最后再把我们写的XML设置进去。

一切都准备就绪,但是我们到目前还是看不到界面,因为我们知道,Activity真正可见是在执行onResume的时候。所以对于DecorView何时绘制到屏幕中问题,其实答案已经出来了,接下来需要去代码里进行验证。

看过我之前文章的人知道找Activity不同生命周期具体执行代码要去TransactionExecutor里看performLifecycleSequence方法。

Activity的onRemune具体执行是在Activity的handleResumeActivity方法中,我们去看看这里是怎么处理PhoneWindow的。

android.app.ActivityThread

@Override
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
String reason) {
......
// TODO Push resumeArgs into the activity for consideration
performResumeActivity内会触发Activity的onResume生命周期
final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
......
r.window = r.activity.getWindow();
//获取到当前PhoneWindow的DecorView
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
......
关键代码
wm.addView(decor, l);
......
}

关键代码只有简单的一句wm.addView(decor, l);

找出WindowManagerImpl

这个wm是ViewManager类型,然后获取他的方法是Activity的WindowManager。先来搞清楚一下WindowManager和ViewManager是什么关系。

假装分析一波,能用ViewManager去接收WindowManager,说明WindowManager要么是ViewManager的子类,要么是实现了ViewManager接口。而且会使用wm.addView。就说明这个addView方法ViewManager里被定义了,在WindowManager中没有,否则不会这么去写。

其实也不用想这么多,各自点进去看看就知道了。

android.view.ViewManager

public interface ViewManager
{
public void addView(View view, ViewGroup.LayoutParams params);
public void updateViewLayout(View view, ViewGroup.LayoutParams params);
public void removeView(View view);
}

android.view.WindowManager
@SystemService(Context.WINDOW_SERVICE)
public interface WindowManager extends ViewManager {
......
}

ViewManager是个接口,里面虽然只有三个方法,但是我们却非常熟悉,这三个方法的重要性就不用多说了。

让我没想到的是WindowManager居然也是个接口......那addView具体的实现在哪呢。只能去getWindowManager具体找找了。

android.app.Activity
......
private WindowManager mWindowManager;
......
public WindowManager getWindowManager() {
return mWindowManager;
}

而的赋值操作在attach里

   final void attach(......){
mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setWindowControllerCallback(mWindowControllerCallback);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
......
给PhoneWindow设置一个WindowManager
mWindow.setWindowManager((WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
mToken, mComponent.flattenToString(),
(info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
if (mParent != null) {
mWindow.setContainer(mParent.getWindow());
}
取出WindowManager来赋值给mWindowManager
mWindowManager = mWindow.getWindowManager();
}

所以想知道mWindowManager到底是啥还得看mWindow.getWindowManager。而mWindow.getWindowManager返回的又是上面的一行mWindow.setWindowManager,所以去setWindowManager看看吧

android.view.Window

......
private WindowManager mWindowManager;
......
public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
boolean hardwareAccelerated) {
mAppToken = appToken;
mAppName = appName;
mHardwareAccelerated = hardwareAccelerated;
if (wm == null) {
wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
}
关键代码
mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
}
......
public WindowManager getWindowManager() {
return mWindowManager;
}

真相浮出水面

mWindowManager的真实对象是WindowManagerImpl

addView

这一小节我们来看看Window的添加过程

找了这么久我们就是为了弄明白wm.addView(decor, l);的wm到底是个啥。下面去看看addView到底把我们的DecorView怎么了。

android.view.WindowManagerImpl

public final class WindowManagerImpl implements WindowManager {
@UnsupportedAppUsage
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
......
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplayNoVerify(), mParentWindow,
mContext.getUserId());
}
......
}

WindowManagerImpl居然又是个空壳,大部分操作都给了单例类WindowManagerGlobal去处理,

android.view.WindowManagerGlobal

private final ArrayList<View> mViews = new ArrayList<View>();

private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();

private final ArrayList<WindowManager.LayoutParams> mParams =
new ArrayList<WindowManager.LayoutParams>();
private final ArraySet<View> mDyingViews = new ArraySet<View>();

public void addView(View view, ViewGroup.LayoutParams params,Display display,
Window parentWindow, int userId) {
......
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
进行参数检查
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
if (display == null) {
throw new IllegalArgumentException("display must not be null");
}
if (!(params instanceof WindowManager.LayoutParams)) {
throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
}

final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
如果是子Windowh还需要进行布局参数的调整
if (parentWindow != null) {
parentWindow.adjustLayoutParamsForSubWindow(wparams);
}
......
创建ViewRootImpl
root = new ViewRootImpl(view.getContext(), display);

view.setLayoutParams(wparams);

mViews.add(view);
mRoots.add(root);
mParams.add(wparams);

// do this last because it fires off messages to start doing things
try {
//把DecorView交给ViewRootImpl
root.setView(view, wparams, panelParentView, userId);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
}
}

首先我们看到WindowManagerGlobal维护了4个集合

  • mViews :存储了所有Window所对应的View

  • mRoots :存储的是所以Window所对应的ViewRootImpl

  • mParams:存储的是所有Window所对应的布局参数

  • mDyingViews:存储的是那些正在被删除的View

在addView中后面的关键代码创建了ViewRootImpl,并将我们的DecorView交给了它。

android.view.ViewRootImpl

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
int userId) {
synchronized (this) {
if (mView == null) {
mView = view;
}
......
触发一次屏幕刷新
requestLayout();
try {
将最后的操作给mWindowSession
res = mWindowSession.addToDisplayAsUser(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), userId, mTmpFrame,
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mDisplayCutout, inputChannel,
mTempInsets, mTempControls);
setFrame(mTmpFrame);
} ......
// Set up the input pipeline.
输入事件处理
CharSequence counterSuffix = attrs.getTitle();
mSyntheticInputStage = new SyntheticInputStage();
InputStage viewPostImeStage = new ViewPostImeInputStage(mSyntheticInputStage);
InputStage nativePostImeStage = new NativePostImeInputStage(viewPostImeStage,
"aq:native-post-ime:" + counterSuffix);
InputStage earlyPostImeStage = new EarlyPostImeInputStage(nativePostImeStage);
InputStage imeStage = new ImeInputStage(earlyPostImeStage,
"aq:ime:" + counterSuffix);
InputStage viewPreImeStage = new ViewPreImeInputStage(imeStage);
InputStage nativePreImeStage = new NativePreImeInputStage(viewPreImeStage,
"aq:native-pre-ime:" + counterSuffix);
......
}
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}

setView方法先是做了个刷新布局的操作,内部执行的scheduleTraversals() 就是View绘制的入口,调用此方法后 ViewRootImpl 所关联的 View 也执行 measure - layout - draw 操作,确保在 View 被添加到 Window 上显示到屏幕之前,已经完成测量和绘制操作。至此由ViewRootImpl完成了添加View到Window的操作。 然后将调用mWindowSession的addToDisplayAsUser方法来完成Window的添加过程,内部真实执行的地方在WMS(WindowManagerService)

View的事件如何反馈到Activity

对于后面的输入事件处理的代码我也不是很明白,但是之前好像在哪里看过,所有对于利用管道和系统底层通信的机制有点印象。 这一块的简单的理解就是:这里是设置了一系列的输入通道。因为一个触屏事件的发生是肯定是由屏幕发起,再经过驱动层一系列的计算处理最后通过 Socket 跨进程通知 Android Framework 层。 其实看到这一块我一开始是不打算继续深入了,因为的也知道我的能力差不多就这了,但是抱着好奇的心点了进去想看看ViewPostImeInputStage是个啥。结果却有了意想不到的收获。

android.view.ViewRootImpl
final class ViewPostImeInputStage extends InputStage {
public ViewPostImeInputStage(InputStage next) {
super(next);
}

@Override
protected int onProcess(QueuedInputEvent q) {
if (q.mEvent instanceof KeyEvent) {
return processKeyEvent(q);
} else {
final int source = q.mEvent.getSource();
if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
只看这里,其实下面的两个都是类似
return processPointerEvent(q);
} else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
return processTrackballEvent(q);
} else {
return processGenericMotionEvent(q);
}
}
}
}
private int processPointerEvent(QueuedInputEvent q) {
final MotionEvent event = (MotionEvent)q.mEvent;

mAttachInfo.mUnbufferedDispatchRequested = false;
mAttachInfo.mHandlingPointerEvent = true;
boolean handled = mView.dispatchPointerEvent(event);
......
return handled ? FINISH_HANDLED : FORWARD;
}

注意一下参数的传递就知道这个mView其实就是一开始我们DecorView。但是dispatchPointerEvent是在View定义的。

android.view.View
public final boolean dispatchPointerEvent(MotionEvent event) {
if (event.isTouchEvent()) {
return dispatchTouchEvent(event);
} else {
return dispatchGenericMotionEvent(event);
}
}

返回了dispatchTouchEvent和dispatchGenericMotionEvent的执行。所以去DecorView看看

com.android.internal.policy.DecorView

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
final Window.Callback cb = mWindow.getCallback();
return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
}

@Override
public boolean dispatchTrackballEvent(MotionEvent ev) {
final Window.Callback cb = mWindow.getCallback();
return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
? cb.dispatchTrackballEvent(ev) : super.dispatchTrackballEvent(ev);
}

@Override
public boolean dispatchGenericMotionEvent(MotionEvent ev) {
final Window.Callback cb = mWindow.getCallback();
return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
? cb.dispatchGenericMotionEvent(ev) : super.dispatchGenericMotionEvent(ev);
}

我们发现不管是哪个方法,最后都是从mWindow获取一个回调cb,然后把事回调出去。不知道读者对之前在Activity的attach里的时候的代码 mWindow.setCallback(this);还有没有印象,方法接收的参数类型是Window内部的一个接口Callback而我们的Activity实现了这个接口。所以mWindow.setCallback就是把Activity设置了进去,所以这个cb就是我们的Activity,所以DecorView的事件,就这样传递给了Activity

removeView

android.view.WindowManagerGlobal

@UnsupportedAppUsage
public void removeView(View view, boolean immediate) {
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}

synchronized (mLock) {
找到需要删除View的索引
int index = findViewLocked(view, true);
View curView = mRoots.get(index).getView();
真的执行删除操作
removeViewLocked(index, immediate);
if (curView == view) {
return;
}

throw new IllegalStateException("Calling with view " + view
+ " but the ViewAncestor is attached to " + curView);
}
}

Window的删除过程页是在WindowManagerGlobal中。先是找到需要删除View的索引,然后传递到removeViewLocked方法里。

   private void removeViewLocked(int index, boolean immediate) {
ViewRootImpl root = mRoots.get(index);
View view = root.getView();

if (root != null) {
root.getImeFocusController().onWindowDismissed();
}
boolean deferred = root.die(immediate);
if (view != null) {
view.assignParent(null);
if (deferred) {
mDyingViews.add(view);
}
}
}

removeViewLocked内部也是通过ViewRootImpl來完成刪除操作的。 在removeView方法里我们就看到了mRoots.get(index).getView(),里面又有View view = root.getView();ViewRootImpl的getView返回的View其实就是上面setView方法里,传进来的我们的顶层视图DecorView。

看看ViewRootImpl的die方法

Params:immediate – True, do now if not in traversal. False, put on queue and do later.
Returns:True, request has been queued. False, request has been completed.
boolean die(boolean immediate) {
// Make sure we do execute immediately if we are in the middle of a traversal or the damage
// done by dispatchDetachedFromWindow will cause havoc on return.
if (immediate && !mIsInTraversal) {
doDie();
return false;
}

if (!mIsDrawing) {
destroyHardwareRenderer();
} else {
Log.e(mTag, "Attempting to destroy the window while drawing!\n" +
" window=" + this + ", title=" + mWindowAttributes.getTitle());
}
mHandler.sendEmptyMessage(MSG_DIE);
return true;
}

参数immediates表示是否立即删除,返回false表示删除已完成,返回true表示加入待删除的队列里。看removeViewLocked的最后,将返回true加入了待删除的列表mDyingViews就明白了。 如果是需要立即删除则执行doDie,如果是是异步删除,则发送个消息,ViewRootImpl内部Handle接收消息的处理还是执行doDie。

现在看来doDie才是真正执行删除操作的地方。

void doDie() {
checkThread();
if (LOCAL_LOGV) Log.v(mTag, "DIE in " + this + " of " + mSurface);
synchronized (this) {
if (mRemoved) {
return;
}
mRemoved = true;
if (mAdded) {
关键代码
dispatchDetachedFromWindow();
}
......
全局单例WindowManagerGlobal也执行对应方法
WindowManagerGlobal.getInstance().doRemoveView(this);
}
void dispatchDetachedFromWindow() {
内部完成视图的移除
mView.dispatchDetachedFromWindow();
try {
内部通过WMA完成Window的删除
mWindowSession.remove(mWindow);
} catch (RemoteException e) {}
}

在dispatchDetachedFromWindow方法内部通过View的dispatchDetachedFromWindow方法完成View的删除,同时在通过mWindowSession来完成Window的删除

updateViewLayout

android.view.WindowManagerGlobal
public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
if (!(params instanceof WindowManager.LayoutParams)) {
throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
}

final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
更新View参数
view.setLayoutParams(wparams);

synchronized (mLock) {
int index = findViewLocked(view, true);
ViewRootImpl root = mRoots.get(index);
mParams.remove(index);
mParams.add(index, wparams);
更新ViewRootImpl参数
root.setLayoutParams(wparams, false);
}
}

updateViewLayout内部做的事情比较简单,先是更新View的参数,然后更新ViewRootImpl的参数。

  • setLayoutParams内部会触发scheduleTraversals来对View重新布局,scheduleTraversals一旦触发,就会执行relayoutWindow方法,触发WMS来更新Window视图。我就不贴代码了,太多了,附上调用路径吧。
  • scheduleTraversals->mTraversalRunnable(TraversalRunnable)->doTraversal->performTraversals->relayoutWindow->内部执行mWindowSession.relayout通知WMS更新Window

总结

  1. DecorView在Activity的handleResumeActivity方法执行通过wm.addView完成操作。执行时机是在onResume后面一点点
  2. DecorView存在于PhoneWindow中,DecorView的添加删除更新曹组是由ViewRootImpl负责,而PhoneWindow的添加删除更新操作由WMS负责
  3. PhoneWindow的事件通过Callback接口回调给Activity
收起阅读 »

[Android翻译]解除对WindowManager的束缚

原文地址:medium.com/androiddeve…原文作者:medium.com/@pmaggi发布时间:2021年8月20日 - 6分钟阅读为可折叠设备和大屏幕设备优化应用程序Android的屏幕尺寸正在迅速变化,随着平板电脑和可折叠设备的不断普及,了...
继续阅读 »

原文地址:medium.com/androiddeve…

原文作者:medium.com/@pmaggi

发布时间:2021年8月20日 - 6分钟阅读

为可折叠设备和大屏幕设备优化应用程序

Android的屏幕尺寸正在迅速变化,随着平板电脑和可折叠设备的不断普及,了解你的应用程序的窗口尺寸和状态对于开发一个响应式的UI至关重要。Jetpack WindowManager现在处于测试阶段,它是一个库和API,提供类似于Android框架WindowManager的功能,包括对响应式UI的支持、检测屏幕变化的回调适配器以及窗口测试API。但Jetpack WindowManager还提供了对新型设备的支持,如可折叠设备和Chrome OS等窗口环境。

新的WindowManager APIs包括以下内容。

  • WindowLayoutInfo:包含了一个窗口的显示特征,例如窗口是否包含了折叠或铰链
  • FoldingFeature:使你能够监测可折叠设备的折叠状态,以确定设备的姿势
  • WindowMetrics:提供当前窗口的指标或整体显示的指标

Jetpack WindowManager与安卓系统没有捆绑,允许更快地迭代API,以快速支持快速发展的设备市场,并使应用程序开发人员能够采用库的更新,而不必等待最新的安卓版本。

现在该库已经进入测试阶段,我们鼓励所有的开发者采用Jetpack WindowManager,它具有设备无关的API,测试API,以及带来WindowMetrics,使你能够轻松应对窗口尺寸的变化。逐步过渡到测试版意味着你可以对你所采用的API有信心,使你可以完全专注于在这些设备上建立令人兴奋的体验。Jetpack WindowManager支持低至API 14的功能检测。

该库

Jetpack WindowManager是一个现代的、以Kotlin为首的库,它支持新的设备形态因素,并提供 "类似AppCompat "的功能,以构建具有响应式用户界面的应用程序。

折叠状态

这个库所提供的最明显的功能是对可折叠设备的支持。当设备的折叠状态发生变化时,应用程序可以接收事件,允许更新用户界面以支持新的用户互动。

1.gif

三星Galaxy Z Fold2上的Google Duo

请看这个Google Duo案例研究,它介绍了如何为可折叠设备添加支持。

有两种可能的折叠状态:平面半开放。对于FLAT,你可以认为表面是完全平坦地打开的,尽管在某些情况下它可能被铰链分割。对于HALF_OPENED,窗口至少有两个逻辑区域。下面,我们有图片说明每种状态的可能性。

image.png

折叠状态。平坦和半开放

当应用程序处于活动状态时,应用程序可以通过收集Kotlin流的事件来接收关于折叠状态变化的信息。 为了开始和停止事件收集,我们可以使用一个生命周期范围,正如 repeatOnLifeCycle API设计故事博文和下面的代码示例中所解释的。

lifecycleScope.launch(Dispatchers.Main) {
// The block passed to repeatOnLifecycle is executed when the lifecycle
// is at least STARTED and is cancelled when the lifecycle is STOPPED.
// It automatically restarts the block when the lifecycle is STARTED again.
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
// Safely collects from windowInfoRepository when the lifecycle is STARTED
// and stops collection when the lifecycle is STOPPED.
windowInfoRepository.windowLayoutInfo
.collect { newLayoutInfo ->
updateStateLog(newLayoutInfo)
updateCurrentState(newLayoutInfo)
}
}
}

然后,应用程序可以使用收到的WindowLayoutInfo对象中的可用信息,在应用程序对用户可见时更新其布局。

FoldingFeature包括铰链方向和折叠功能是否创建两个逻辑屏幕区域(isSeparating属性)等信息。我们可以使用这些值来检查设备是否处于桌面模式(半开,铰链水平)。

image.png

设备处于TableTop模式

private fun isTableTopMode(foldFeature: FoldingFeature) =
foldFeature.isSeparating &&
foldFeature.orientation == FoldingFeature.Orientation.HORIZONTAL

或处于书本模式(半开,铰链垂直)。

image.png

设备在书本模式下

private fun isBookMode(foldFeature: FoldingFeature) =
foldFeature.isSeparating &&
foldFeature.orientation == FoldingFeature.Orientation.VERTICAL

你可以在《可折叠设备上的桌面模式》一文中看到一个例子,说明如何为一个媒体播放器应用程序做到这一点。

注意:在主/UI线程上收集这些事件很重要,以避免UI和处理这些事件之间的同步问题。

对响应式UI的支持

由于安卓系统中的屏幕尺寸变化非常频繁,因此开始设计完全自适应和响应式的UI非常重要。WindowManager库中包含的另一个功能是能够检索当前和最大的窗口度量信息。这与API 30中包含的框架WindowMetrics API提供的信息类似,但它向后兼容到API 14。

Jetpack WindowManager提供了两种检索WindowMetrics信息的方式,作为流事件流或通过WindowMetricsCalculator类同步进行。

当在视图中写代码时,异步的API可能太难处理(比如onMeasure),就使用WindowMetricsCalculator。

val windowMetrics = 
WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(activity)

另一个用例是在测试中(见下面的测试)。

对于应用程序UI的更高层次的处理,使用WindowInfoRepository#currentWindowMetrics来获得库的通知,当有一个窗口大小的变化时,独立于这个变化是否触发了配置的变化。

下面是一个如何根据你的可用区域的大小来切换你的布局的例子。

// Create a new coroutine since repeatOnLifecycle is a suspend function
lifecycleScope.launch(Dispatchers.Main) {
// The block passed to repeatOnLifecycle is executed when the lifecycle
// is at least STARTED and is cancelled when the lifecycle is STOPPED.
// It automatically restarts the block when the lifecycle is STARTED again.
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
// Safely collect from currentWindowMetrics when the lifecycle is STARTED
// and stops collection when the lifecycle is STOPPED
windowInfoRepository.currentWindowMetrics
.collect { windowMetrics ->
val currentBounds = windowMetrics.bounds
Log.i(TAG, "New bounds: {$currentBounds}")
// We can update the layout if needed from here
}
}
}

回调适配器 要在Java编程语言中使用这个库,或者使用回调接口,请在你的应用程序中包含androidx.window:window-java依赖项。该工件提供了WindowInfoRepositoryCallbackAdapter,你可以用它来注册(和取消注册)一个回调,以接收设备姿态和窗口度量信息的更新。

public class SplitLayoutActivity extends AppCompatActivity {

private WindowInfoRepositoryCallbackAdapter windowInfoRepository;
private ActivitySplitLayoutBinding binding;
private final LayoutStateChangeCallback layoutStateChangeCallback =
new LayoutStateChangeCallback();

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

binding = ActivitySplitLayoutBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());

windowInfoRepository =
new WindowInfoRepositoryCallbackAdapter(WindowInfoRepository.getOrCreate(this));
}

@Override
protected void onStart() {
super.onStart();
windowInfoRepository.addWindowLayoutInfoListener(Runnable::run, layoutStateChangeCallback);
}

@Override
protected void onStop() {
super.onStop();
windowInfoRepository.removeWindowLayoutInfoListener(layoutStateChangeCallback);
}

class LayoutStateChangeCallback implements Consumer<WindowLayoutInfo> {
@Override
public void accept(WindowLayoutInfo windowLayoutInfo) {
binding.splitLayout.updateWindowLayout(windowLayoutInfo);
}
}
}

测试

我们从开发者那里听说,更强大的测试API对于维持长期支持至关重要。让我们来谈谈如何在正常设备上测试可折叠的姿势。

到目前为止,我们已经看到Jetpack WindowManager库在设备姿态发生变化时通知你的应用程序,这样你就可以修改应用程序的布局。

该库在androidx.window:window-testing中提供了WindowLayoutInfoPublisherRule,它使你可以在测试FoldingFeature的支持下发布WindowInfoLayout。

import androidx.window.testing.layout.FoldingFeature
import androidx.window.testing.layout.WindowLayoutInfoPublisherRule

我们可以用它来创建一个假的FoldingFeature,在我们的测试中使用。

val feature = FoldingFeature(
activity = activity,
center = center,
size = 0,
orientation = VERTICAL,
state = HALF_OPENED
)
val expected =
WindowLayoutInfo.Builder().setDisplayFeatures(listOf(feature)).build()

publisherRule.overrideWindowLayoutInfo(expected)

然后使用WindowLayoutInfoPublisherRule来发布它。

val publisherRule = WindowLayoutInfoPublisherRule()

publisherRule.overrideWindowLayoutInfo(expected)

最后一步是使用可用的Espresso匹配器检查我们正在测试的活动的布局是否符合预期。

下面是一个测试发布FoldingFeature的例子,它在屏幕中心有一个HALF_OPENED的垂直铰链。

@Test
fun testDeviceOpen_Vertical(): Unit = testScope.runBlockingTest {
activityRule.scenario.onActivity { activity ->
val feature = FoldingFeature(
activity = activity,
orientation = VERTICAL,
state = HALF_OPENED
)
val expected =
WindowLayoutInfo.Builder().setDisplayFeatures(listOf(feature)).build()

val value = testScope.async {
activity.windowInfoRepository().windowLayoutInfo.first()
}
publisherRule.overrideWindowLayoutInfo(expected)
runBlockingTest {
Assert.assertEquals(
expected,
value.await()
)
}
}

// Checks that start_layout is on the left of end_layout with a vertical folding feature.
// This requires to run the test on a big enough screen to fit both views on screen
onView(withId(R.id.start_layout))
.check(isCompletelyLeftOf(withId(R.id.end_layout)))
}

请看它的运行情况。代码样本

GitHub上的一个最新样本显示了如何使用Jetpack WindowManager库来检索显示姿势信息,从WindowLayoutInfo流中收集信息或通过WindowInfoRepositoryCallbackAdapter注册一个回调。

该样本还包括一些测试,可以在任何设备或模拟器上运行。

在你的应用程序中采用WindowManager

可折叠和双屏设备不再是实验性的或未来主义的--大的显示区域和额外的姿势具有被证实的用户价值,而且现在有更多的设备可以供你的用户使用。可折叠设备和双屏设备代表了智能手机的自然进化。对于安卓开发者来说,他们提供了进入一个正在增长的高端市场的机会,这也得益于设备制造商的重新关注。

我们去年推出了Jetpack WindowManager alpha01。从那时起,该库有了稳定的发展,针对早期的反馈有了一些很大的改进。该库现在已经接受了Android的Kotlin优先理念,从回调驱动的模型过渡到coroutines和flow。随着WindowManager现在处于测试阶段,该API已经稳定,我们强烈建议采用。 而更新并不限于此。我们计划为该库添加更多的功能,并将其发展成一个用于系统UI的非捆绑式AppCompat,使开发者能够在所有的Android设备上轻松实现现代的、响应式的UI。


收起阅读 »

Android组件化开发笔记

Modularization什么是组件化组件化就是将一个app拆分成不同的组件,每一个组件都是一个独立的module。组件化的意义组件化能降低耦合性,而耦合性低就能提高维护性。于此同时由于组件间是独立的,所以组件与组件间耦合性低,所以我们在团队开发的时候可以以...
继续阅读 »

Modularization

什么是组件化

组件化就是将一个app拆分成不同的组件,每一个组件都是一个独立的module。

组件化的意义

组件化能降低耦合性,而耦合性低就能提高维护性。

于此同时由于组件间是独立的,所以组件与组件间耦合性低,所以我们在团队开发的时候可以以组件为分割单位,这样就能提高开发效率。

如何进行组件化

组件化是依靠gradle实现的。所以不会gradle的得去学学基础语法。当然也可以不学,只是说看别人写的代码看的半懂不懂的。

第一步创建Module

起点是一个全新的Project

image-20210818192909793

创建Module有好几种方法。

  • 右击new Module

    image-20210818193028977

    这个得注意位置哦,不然new出来全在app包下不是很好,通常我们的module是和app平级的。也就是说在大project下面。

    在我的这个demo中就是Modularization下面

  • 点击File new一个Module

    image-20210818193107528

这里我创建了3个module,一个lib

image-20210818193618429

注意命名,module是module_模块名,lib是lib__库名称.

简单区分以下module和lib,lib就是不显示页面的模块,module就是一个页面模块的集合。

将版本信息配置到一个gradle文件中

Tips:
fileName :app_versions.gradle

//applicationIds
def appIds = [:]
appIds.module_main = "com.example.module_main"
appIds.module_one = "com.example.module_one"
appIds.module_two = "com.example.module_two"
appIds.app = "com.example.modularization"
ext.appIds = appIds

将该gradle文件配置到project的build.gradle中

// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
apply from: 'app_versions.gradle'
ext{
app_configs = "$rootDir/app_config.gradle"
}
repositories {
google()
mavenCentral()
}
dependencies {
classpath "com.android.tools.build:gradle:7.0.0"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.20"

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

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

配置gradle的编译插件

先在gradle.properties,加入一个变量判断是否是发行状态

组件化中有两种状态,一种是debug状态,一个是发行状态,

  • debug状态也就是开发阶段,这个阶段每一个模块都是一个独立的app,可以独立运行
  • release发行状态,这个状态下只有app模块可以独立运行,其他的模块都是lib,依托于app模块。

image-20210818194002652

然后创建了一个app_config.gradle文件

image-20210818194518257

编写代码使得module_XX能在lib和app中切换状态。

这里有一点需要注意我们在gradle.propergies虽然写了一个isRelease的bool变量但是其实gradle这里获取的是一个string,得用toBoolean()进行转化。

image-20210818194838453

if (isRelease.toBoolean()){
if (project.name != 'app') apply plugin: 'com.android.library'
else apply plugin: 'com.andorid.application'
}
else{
if (project.name.matches('module_.+') || project.name == 'app') apply plugin: 'com.android.application'
else if (project.name.matches('lib_.+')) apply plugin: 'com.android.library'
}

然后加入必要的依赖

apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'

配置Manifest文件

android{
`````

sourceSets{
main{
if (isRelease.toBoolean()){
manifest.srcFile "src/main/AndroidManifest.xml"
}else {
if (project.name.matches('module_.+')){
manifest.srcFile "src/main/manifest/AndroidManifest.xml"
}else if (project.name.matches('lib_.+') || project.name == 'app'){
manifest.srcFile "src/main/AndroidManifest.xml"
}
}
}
}


`````
}

然后对module的manifest文件进行一点变动

在main文件夹下创建manifest文件夹,然后把debug状态的manifest的文件放进去。

image-20210818200716961

debug状态下的文件(这个状态下编译的文件是apk所以需要配置启动页和一些application的选项)

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.module_main">


<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Modularization">

<activity
android:name=".MainMainActivity"
android:exported="true">

<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>

release状态下的manifest文件(这个状态下的编译文件是aar文件所以只需要注册一个activity,其余的都不需要)

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.module_main">


<application>
<activity
android:name=".MainMainActivity"
android:exported="true" />

</application>

</manifest>

其余的module_XX,模块也是按照这样改。

配置applicationId

由于application才有applicationId,所以lib是没有applicationId的,而module在application和module之间疯狂切换,说以是有必要进行设置的。

if (isRelease.toBoolean()) {
applicationId "com.example.modularization"
}else {
if (project.name.matches('module_.+') || project.name == 'app') {
applicationId appIds[project.name]
}
}

之后在project的build.gradle中加点变量方便其gradle文件访问app_config.gradle文件(注意要加等号‘=’)

buildscript {
ext{
app_config = "$rootDir/app_config.gradle"
}
repositories {
google()
mavenCentral()
}
dependencies {
classpath "com.android.tools.build:gradle:7.0.0"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.20"

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

module引用app_config.gradle

之前

plugins {
id 'com.android.application'
id 'kotlin-android'
}

android {
compileSdk 30

defaultConfig {
applicationId "com.example.module_main"
minSdk 21
targetSdk 30
versionCode 1
versionName "1.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}

dependencies {

implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}

之后

apply from: app_configs

dependencies {

implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}

其余的lib和module都是这样

把android闭包下的一些属性进行抽离


def android_versions = [:]
android_versions.sdk_version = 30
android_versions.min_version = 21
android_versions.target_version = 30
android_versions.version_code = 1
android_versions.version_name = "1.0"

def kotlin_options = [:]
kotlin_options.jvm_target = '1.8'
android_versions.kotlin_options = kotlin_options

ext.android_versions = android_versions

app_config.gradle的内容

if (isRelease.toBoolean()){
if (project.name != 'app') apply plugin: 'com.android.library'
else apply plugin: 'com.andorid.application'
}
else{
if (project.name.matches('module_.+') || project.name == 'app') apply plugin: 'com.android.application'
else if (project.name.matches('lib_.+')) apply plugin: 'com.android.library'
}

apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'

android {
compileSdk android_versions.sdk_version

defaultConfig {
if (isRelease.toBoolean()) {
applicationId "com.example.modularization"
}else {
if (project.name.matches('module_.+') || project.name == 'app') {
applicationId appIds[project.name]
}
}

minSdk android_versions.min_version
targetSdk android_versions.target_version
versionCode android_versions.version_code
versionName android_versions.version_name

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

sourceSets{
main{
if (isRelease.toBoolean()){
manifest.srcFile "src/main/AndroidManifest.xml"
}else {
if (project.name.matches('module_.+')){
manifest.srcFile "src/main/manifest/AndroidManifest.xml"
}else if (project.name.matches('lib_.+') || project.name == 'app'){
manifest.srcFile "src/main/AndroidManifest.xml"
}
}
}
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = android_versions.kotlin_options.jvm_target
}
}
收起阅读 »

iOS 变化 一

变换很不幸,没人能告诉你母体是什么,你只能自己体会 -- 骇客帝国在第四章“可视效果”中,我们研究了一些增强图层和它的内容显示效果的一些技术,在这一章中,我们将要研究可以用来对图层旋转,摆放或者扭曲的CGAffineTransform,以及可以将扁平...
继续阅读 »

变换

很不幸,没人能告诉你母体是什么,你只能自己体会 -- 骇客帝国

在第四章“可视效果”中,我们研究了一些增强图层和它的内容显示效果的一些技术,在这一章中,我们将要研究可以用来对图层旋转,摆放或者扭曲的CGAffineTransform,以及可以将扁平物体转换成三维空间对象的CATransform3D(而不是仅仅对圆角矩形添加下沉阴影)。

5.1仿射变换

在第三章“图层几何学”中,我们使用了UIViewtransform属性旋转了钟的指针,但并没有解释背后运作的原理,实际上UIViewtransform属性是一个CGAffineTransform类型,用于在二维空间做旋转,缩放和平移。CGAffineTransform是一个可以和二维空间向量(例如CGPoint)做乘法的3X2的矩阵(见图5.1)。

图5.1

图5.1 用矩阵表示的CGAffineTransformCGPoint

CGPoint的每一列和CGAffineTransform矩阵的每一行对应元素相乘再求和,就形成了一个新的CGPoint类型的结果。要解释一下图中显示的灰色元素,为了能让矩阵做乘法,左边矩阵的列数一定要和右边矩阵的行数个数相同,所以要给矩阵填充一些标志值,使得既可以让矩阵做乘法,又不改变运算结果,并且没必要存储这些添加的值,因为它们的值不会发生变化,但是要用来做运算。

因此,通常会用3×3(而不是2×3)的矩阵来做二维变换,你可能会见到3行2列格式的矩阵,这是所谓的以列为主的格式,图5.1所示的是以行为主的格式,只要能保持一致,用哪种格式都无所谓。

当对图层应用变换矩阵,图层矩形内的每一个点都被相应地做变换,从而形成一个新的四边形的形状。CGAffineTransform中的“仿射”的意思是无论变换矩阵用什么值,图层中平行的两条线在变换之后任然保持平行,CGAffineTransform可以做出任意符合上述标注的变换,图5.2显示了一些仿射的和非仿射的变换:

图5.2

创建一个CGAffineTransform

对矩阵数学做一个全面的阐述就超出本书的讨论范围了,不过如果你对矩阵完全不熟悉的话,矩阵变换可能会使你感到畏惧。幸运的是,Core Graphics提供了一系列函数,对完全没有数学基础的开发者也能够简单地做一些变换。如下几个函数都创建了一个CGAffineTransform实例:

CGAffineTransformMakeRotation(CGFloat angle)
CGAffineTransformMakeScale(CGFloat sx, CGFloat sy)
CGAffineTransformMakeTranslation(CGFloat tx, CGFloat ty)

旋转和缩放变换都可以很好解释--分别旋转或者缩放一个向量的值。平移变换是指每个点都移动了向量指定的x或者y值--所以如果向量代表了一个点,那它就平移了这个点的距离。

我们用一个很简单的项目来做个demo,把一个原始视图旋转45度角度(图5.3)

图5.3

图5.3 使用仿射变换旋转45度角之后的视图

UIView可以通过设置transform属性做变换,但实际上它只是封装了内部图层的变换。

CALayer同样也有一个transform属性,但它的类型是CATransform3D,而不是CGAffineTransform,本章后续将会详细解释。CALayer对应于UIViewtransform属性叫做affineTransform,清单5.1的例子就是使用affineTransform对图层做了45度顺时针旋转。

清单5.1 使用affineTransform对图层旋转45度

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *layerView;

@end

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad];
//rotate the layer 45 degrees
CGAffineTransform transform = CGAffineTransformMakeRotation(M_PI_4);
self.layerView.layer.affineTransform = transform;
}

@end

注意我们使用的旋转常量是M_PI_4,而不是你想象的45,因为iOS的变换函数使用弧度而不是角度作为单位。弧度用数学常量pi的倍数表示,一个pi代表180度,所以四分之一的pi就是45度。

C的数学函数库(iOS会自动引入)提供了pi的一些简便的换算,M_PI_4于是就是pi的四分之一,如果对换算不太清楚的话,可以用如下的宏做换算:

#define RADIANS_TO_DEGREES(x) ((x)/M_PI*180.0)

混合变换

Core Graphics提供了一系列的函数可以在一个变换的基础上做更深层次的变换,如果做一个既要缩放又要旋转的变换,这就会非常有用了。例如下面几个函数:

CGAffineTransformRotate(CGAffineTransform t, CGFloat angle)
CGAffineTransformScale(CGAffineTransform t, CGFloat sx, CGFloat sy)
CGAffineTransformTranslate(CGAffineTransform t, CGFloat tx, CGFloat ty)

当操纵一个变换的时候,初始生成一个什么都不做的变换很重要--也就是创建一个CGAffineTransform类型的空值,矩阵论中称作单位矩阵,Core Graphics同样也提供了一个方便的常量:

CGAffineTransformIdentity

最后,如果需要混合两个已经存在的变换矩阵,就可以使用如下方法,在两个变换的基础上创建一个新的变换:

CGAffineTransformConcat(CGAffineTransform t1, CGAffineTransform t2);

我们来用这些函数组合一个更加复杂的变换,先缩小50%,再旋转30度,最后向右移动200个像素(清单5.2)。图5.4显示了图层变换最后的结果。

清单5.2 使用若干方法创建一个复合变换

- (void)viewDidLoad
{
[super viewDidLoad];
//create a new transform
CGAffineTransform transform = CGAffineTransformIdentity;
//scale by 50%
transform = CGAffineTransformScale(transform, 0.5, 0.5);
//rotate by 30 degrees
transform = CGAffineTransformRotate(transform, M_PI / 180.0 * 30.0);
//translate by 200 points
transform = CGAffineTransformTranslate(transform, 200, 0);
//apply transform to layer
self.layerView.layer.affineTransform = transform;
}

图5.4

图5.4 顺序应用多个仿射变换之后的结果

图5.4中有些需要注意的地方:图片向右边发生了平移,但并没有指定距离那么远(200像素),另外它还有点向下发生了平移。原因在于当你按顺序做了变换,上一个变换的结果将会影响之后的变换,所以200像素的向右平移同样也被旋转了30度,缩小了50%,所以它实际上是斜向移动了100像素。

这意味着变换的顺序会影响最终的结果,也就是说旋转之后的平移和平移之后的旋转结果可能不同。

#define DEGREES_TO_RADIANS(x) ((x)/180.0*M_PI)


收起阅读 »

iOS 视觉效果 四

4.6 组透明    UIView有一个叫做alpha的属性来确定视图的透明度。CALayer有一个等同的属性叫做opacity,这两个属性都是影响子层级的。也就是说,如果你给一个图层设置了opacity属性,那它的子图...
继续阅读 »

4.6 组透明

    UIView有一个叫做alpha的属性来确定视图的透明度。CALayer有一个等同的属性叫做opacity,这两个属性都是影响子层级的。也就是说,如果你给一个图层设置了opacity属性,那它的子图层都会受此影响。

    iOS常见的做法是把一个控件的alpha值设置为0.5(50%)以使其看上去呈现为不可用状态。对于独立的视图来说还不错,但是当一个控件有子视图的时候就有点奇怪了,图4.20展示了一个内嵌了UILabel的自定义UIButton;左边是一个不透明的按钮,右边是50%透明度的相同按钮。我们可以注意到,里面的标签的轮廓跟按钮的背景很不搭调。

图4.20

图4.20 右边的渐隐按钮中,里面的标签清晰可见

    这是由透明度的混合叠加造成的,当你显示一个50%透明度的图层时,图层的每个像素都会一半显示自己的颜色,另一半显示图层下面的颜色。这是正常的透明度的表现。但是如果图层包含一个同样显示50%透明的子图层时,你所看到的视图,50%来自子视图,25%来了图层本身的颜色,另外的25%则来自背景色。

    在我们的示例中,按钮和表情都是白色背景。虽然他们都是50%的可见度,但是合起来的可见度是75%,所以标签所在的区域看上去就没有周围的部分那么透明。所以看上去子视图就高亮了,使得这个显示效果都糟透了。

    理想状况下,当你设置了一个图层的透明度,你希望它包含的整个图层树像一个整体一样的透明效果。你可以通过设置Info.plist文件中的UIViewGroupOpacity为YES来达到这个效果,但是这个设置会影响到这个应用,整个app可能会受到不良影响。如果UIViewGroupOpacity并未设置,iOS 6和以前的版本会默认为NO(也许以后的版本会有一些改变)。

    另一个方法就是,你可以设置CALayer的一个叫做shouldRasterize属性(见清单4.7)来实现组透明的效果,如果它被设置为YES,在应用透明度之前,图层及其子图层都会被整合成一个整体的图片,这样就没有透明度混合的问题了(如图4.21)。

    为了启用shouldRasterize属性,我们设置了图层的rasterizationScale属性。默认情况下,所有图层拉伸都是1.0, 所以如果你使用了shouldRasterize属性,你就要确保你设置了rasterizationScale属性去匹配屏幕,以防止出现Retina屏幕像素化的问题。

    当shouldRasterizeUIViewGroupOpacity一起的时候,性能问题就出现了(我们在第12章『速度』和第15章『图层性能』将做出介绍),但是性能碰撞都本地化了(译者注:这句话需要再翻译)。

清单4.7 使用shouldRasterize属性解决组透明问题

@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView;
@end

@implementation ViewController

- (UIButton *)customButton
{
//create button
CGRect frame = CGRectMake(0, 0, 150, 50);
UIButton *button = [[UIButton alloc] initWithFrame:frame];
button.backgroundColor = [UIColor whiteColor];
button.layer.cornerRadius = 10;

//add label
frame = CGRectMake(20, 10, 110, 30);
UILabel *label = [[UILabel alloc] initWithFrame:frame];
label.text = @"Hello World";
label.textAlignment = NSTextAlignmentCenter;
[button addSubview:label];
return button;
}

- (void)viewDidLoad
{
[super viewDidLoad];

//create opaque button
UIButton *button1 = [self customButton];
button1.center = CGPointMake(50, 150);
[self.containerView addSubview:button1];

//create translucent button
UIButton *button2 = [self customButton];

button2.center = CGPointMake(250, 150);
button2.alpha = 0.5;
[self.containerView addSubview:button2];

//enable rasterization for the translucent button
button2.layer.shouldRasterize = YES;
button2.layer.rasterizationScale = [UIScreen mainScreen].scale;
}
@end

图4.12

图4.21 修正后的图

总结

    这一章介绍了一些可以通过代码应用到图层上的视觉效果,比如圆角,阴影和蒙板。我们也了解了拉伸过滤器和组透明。

在第五章,『变换』中,我们将会研究图层变化和3D转换

收起阅读 »

iOS 视觉效果 三

4.4 图层蒙板    通过masksToBounds属性,我们可以沿边界裁剪图形;通过cornerRadius属性,我们还可以设定一个圆角。但是有时候你希望展现的内容不是在一个矩形或圆角矩形。比如,你想展示一个有星形框...
继续阅读 »

4.4 图层蒙板

    通过masksToBounds属性,我们可以沿边界裁剪图形;通过cornerRadius属性,我们还可以设定一个圆角。但是有时候你希望展现的内容不是在一个矩形或圆角矩形。比如,你想展示一个有星形框架的图片,又或者想让一些古卷文字慢慢渐变成背景色,而不是一个突兀的边界。

    使用一个32位有alpha通道的png图片通常是创建一个无矩形视图最方便的方法,你可以给它指定一个透明蒙板来实现。但是这个方法不能让你以编码的方式动态地生成蒙板,也不能让子图层或子视图裁剪成同样的形状。

    CALayer有一个属性叫做mask可以解决这个问题。这个属性本身就是个CALayer类型,有和其他图层一样的绘制和布局属性。它类似于一个子图层,相对于父图层(即拥有该属性的图层)布局,但是它却不是一个普通的子图层。不同于那些绘制在父图层中的子图层,mask图层定义了父图层的部分可见区域。

    mask图层的Color属性是无关紧要的,真正重要的是图层的轮廓。mask属性就像是一个饼干切割机,mask图层实心的部分会被保留下来,其他的则会被抛弃。(如图4.12)

    如果mask图层比父图层要小,只有在mask图层里面的内容才是它关心的,除此以外的一切都会被隐藏起来。

图4.12

图4.12 把图片和蒙板图层作用在一起的效果

    我们将代码演示一下这个过程,创建一个简单的项目,通过图层的mask属性来作用于图片之上。为了简便一些,我们用Interface Builder来创建一个包含UIImageView的图片图层。这样我们就只要代码实现蒙板图层了。清单4.5是最终的代码,图4.13是运行后的结果。

清单4.5 应用蒙板图层

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIImageView *imageView;
@end

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad];

//create mask layer
CALayer *maskLayer = [CALayer layer];
maskLayer.frame = self.layerView.bounds;
UIImage *maskImage = [UIImage imageNamed:@"Cone.png"];
maskLayer.contents = (__bridge id)maskImage.CGImage;

//apply mask to image layer
self.imageView.layer.mask = maskLayer;
}
@end

图4.13

图4.13 使用了mask之后的UIImageView

    CALayer蒙板图层真正厉害的地方在于蒙板图不局限于静态图。任何有图层构成的都可以作为mask属性,这意味着你的蒙板可以通过代码甚至是动画实时生成。

4.5拉伸过滤

    最后我们再来谈谈minificationFiltermagnificationFilter属性。总得来讲,当我们视图显示一个图片的时候,都应该正确地显示这个图片(意即:以正确的比例和正确的1:1像素显示在屏幕上)。原因如下:

  • 能够显示最好的画质,像素既没有被压缩也没有被拉伸。
  • 能更好的使用内存,因为这就是所有你要存储的东西。
  • 最好的性能表现,CPU不需要为此额外的计算。

    不过有时候,显示一个非真实大小的图片确实是我们需要的效果。比如说一个头像或是图片的缩略图,再比如说一个可以被拖拽和伸缩的大图。这些情况下,为同一图片的不同大小存储不同的图片显得又不切实际。

    当图片需要显示不同的大小的时候,有一种叫做拉伸过滤的算法就起到作用了。它作用于原图的像素上并根据需要生成新的像素显示在屏幕上。

    事实上,重绘图片大小也没有一个统一的通用算法。这取决于需要拉伸的内容,放大或是缩小的需求等这些因素。CALayer为此提供了三种拉伸过滤方法,他们是:

  • kCAFilterLinear
  • kCAFilterNearest
  • kCAFilterTrilinear

    minification(缩小图片)和magnification(放大图片)默认的过滤器都是kCAFilterLinear,这个过滤器采用双线性滤波算法,它在大多数情况下都表现良好。双线性滤波算法通过对多个像素取样最终生成新的值,得到一个平滑的表现不错的拉伸。但是当放大倍数比较大的时候图片就模糊不清了。

    kCAFilterTrilinearkCAFilterLinear非常相似,大部分情况下二者都看不出来有什么差别。但是,较双线性滤波算法而言,三线性滤波算法存储了多个大小情况下的图片(也叫多重贴图),并三维取样,同时结合大图和小图的存储进而得到最后的结果。

    这个方法的好处在于算法能够从一系列已经接近于最终大小的图片中得到想要的结果,也就是说不要对很多像素同步取样。这不仅提高了性能,也避免了小概率因舍入错误引起的取样失灵的问题

图4.14

图4.14 对于大图来说,双线性滤波和三线性滤波表现得更出色

    kCAFilterNearest是一种比较武断的方法。从名字不难看出,这个算法(也叫最近过滤)就是取样最近的单像素点而不管其他的颜色。这样做非常快,也不会使图片模糊。但是,最明显的效果就是,会使得压缩图片更糟,图片放大之后也显得块状或是马赛克严重。

图4.15

图4.15 对于没有斜线的小图来说,最近过滤算法要好很多

    总的来说,对于比较小的图或者是差异特别明显,极少斜线的大图,最近过滤算法会保留这种差异明显的特质以呈现更好的结果。但是对于大多数的图尤其是有很多斜线或是曲线轮廓的图片来说,最近过滤算法会导致更差的结果。换句话说,线性过滤保留了形状,最近过滤则保留了像素的差异。

    让我们来实验一下。我们对第三章的时钟项目改动一下,用LCD风格的数字方式显示。我们用简单的像素字体(一种用像素构成字符的字体,而非矢量图形)创造数字显示方式,用图片存储起来,而且用第二章介绍过的拼合技术来显示(如图4.16)。

图4.16

图4.16 一个简单的运用拼合技术显示的LCD数字风格的像素字体

    我们在Interface Builder中放置了六个视图,小时、分钟、秒钟各两个,图4.17显示了这六个视图是如何在Interface Builder中放置的。如果每个都用一个淡出的outlets对象就会显得太多了,所以我们就用了一个IBOutletCollection对象把他们和控制器联系起来,这样我们就可以以数组的方式访问视图了。清单4.6是代码实现。

清单4.6 显示一个LCD风格的时钟

@interface ViewController ()

@property (nonatomic, strong) IBOutletCollection(UIView) NSArray *digitViews;
@property (nonatomic, weak) NSTimer *timer;

@end

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad]; //get spritesheet image
UIImage *digits = [UIImage imageNamed:@"Digits.png"];

//set up digit views
for (UIView *view in self.digitViews) {
//set contents
view.layer.contents = (__bridge id)digits.CGImage;
view.layer.contentsRect = CGRectMake(0, 0, 0.1, 1.0);
view.layer.contentsGravity = kCAGravityResizeAspect;
}

//start timer
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(tick) userInfo:nil repeats:YES];

//set initial clock time
[self tick];
}

- (void)setDigit:(NSInteger)digit forView:(UIView *)view
{
//adjust contentsRect to select correct digit
view.layer.contentsRect = CGRectMake(digit * 0.1, 0, 0.1, 1.0);
}

- (void)tick
{
//convert time to hours, minutes and seconds
NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier: NSGregorianCalendar];
NSUInteger units = NSHourCalendarUnit | NSMinuteCalendarUnit | NSSecondCalendarUnit;

NSDateComponents *components = [calendar components:units fromDate:[NSDate date]];

//set hours
[self setDigit:components.hour / 10 forView:self.digitViews[0]];
[self setDigit:components.hour % 10 forView:self.digitViews[1]];

//set minutes
[self setDigit:components.minute / 10 forView:self.digitViews[2]];
[self setDigit:components.minute % 10 forView:self.digitViews[3]];

//set seconds
[self setDigit:components.second / 10 forView:self.digitViews[4]];
[self setDigit:components.second % 10 forView:self.digitViews[5]];
}
@end

如图4.18,这样做的确起了效果,但是图片看起来模糊了。看起来默认的kCAFilterLinear选项让我们失望了。

图4.18

图4.18 一个模糊的时钟,由默认的kCAFilterLinear引起

    为了能像图4.19中那样,我们需要在for循环中加入如下代码:

view.layer.magnificationFilter = kCAFilterNearest;

图4.19

图4.19 设置了最近过滤之后的清晰显示


收起阅读 »

iOS 视觉效果 二

4.2 图层边框  &nbp; CALayer另外两个非常有用属性就是borderWidth和borderColor。二者共同定义了图层边的绘制样式。这条线(也被称作stroke)沿着图层的bounds绘制,同时也包含图层...
继续阅读 »

4.2 图层边框

  &nbp; CALayer另外两个非常有用属性就是borderWidthborderColor。二者共同定义了图层边的绘制样式。这条线(也被称作stroke)沿着图层的bounds绘制,同时也包含图层的角。

  &nbp; borderWidth是以点为单位的定义边框粗细的浮点数,默认为0.borderColor定义了边框的颜色,默认为黑色。

  &nbp; borderColor是CGColorRef类型,而不是UIColor,所以它不是Cocoa的内置对象。不过呢,你肯定也清楚图层引用了borderColor,虽然属性声明并不能证明这一点。CGColorRef在引用/释放时候的行为表现得与NSObject极其相似。但是Objective-C语法并不支持这一做法,所以CGColorRef属性即便是强引用也只能通过assign关键字来声明。

  &nbp; 边框是绘制在图层边界里面的,而且在所有子内容之前,也在子图层之前。如果我们在之前的示例中(清单4.2)加入图层的边框,你就能看到到底是怎么一回事了(如图4.3).

清单4.2 加上边框

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad];

//set the corner radius on our layers
self.layerView1.layer.cornerRadius = 20.0f;
self.layerView2.layer.cornerRadius = 20.0f;

//add a border to our layers
self.layerView1.layer.borderWidth = 5.0f;
self.layerView2.layer.borderWidth = 5.0f;

//enable clipping on the second layer
self.layerView2.layer.masksToBounds = YES;
}

@end

图4.3

图4.3 给图层增加一个边框

  &nbp; 仔细观察会发现边框并不会把寄宿图或子图层的形状计算进来,如果图层的子图层超过了边界,或者是寄宿图在透明区域有一个透明蒙板,边框仍然会沿着图层的边界绘制出来(如图4.4).

图4.4

图4.4 边框是跟随图层的边界变化的,而不是图层里面的内容


4.3 阴影

    iOS的另一个常见特性呢,就是阴影。阴影往往可以达到图层深度暗示的效果。也能够用来强调正在显示的图层和优先级(比如说一个在其他视图之前的弹出框),不过有时候他们只是单纯的装饰目的。

    给shadowOpacity属性一个大于默认值(也就是0)的值,阴影就可以显示在任意图层之下。shadowOpacity是一个必须在0.0(不可见)和1.0(完全不透明)之间的浮点数。如果设置为1.0,将会显示一个有轻微模糊的黑色阴影稍微在图层之上。若要改动阴影的表现,你可以使用CALayer的另外三个属性:shadowColorshadowOffsetshadowRadius

    显而易见,shadowColor属性控制着阴影的颜色,和borderColorbackgroundColor一样,它的类型也是CGColorRef。阴影默认是黑色,大多数时候你需要的阴影也是黑色的(其他颜色的阴影看起来是不是有一点点奇怪。。)。

    shadowOffset属性控制着阴影的方向和距离。它是一个CGSize的值,宽度控制这阴影横向的位移,高度控制着纵向的位移。shadowOffset的默认值是 {0, -3},意即阴影相对于Y轴有3个点的向上位移。

    为什么要默认向上的阴影呢?尽管Core Animation是从图层套装演变而来(可以认为是为iOS创建的私有动画框架),但是呢,它却是在Mac OS上面世的,前面有提到,二者的Y轴是颠倒的。这就导致了默认的3个点位移的阴影是向上的。在Mac上,shadowOffset的默认值是阴影向下的,这样你就能理解为什么iOS上的阴影方向是向上的了(如图4.5).

图4.5

图4.5 在iOS(左)和Mac OS(右)上shadowOffset的表现。

    苹果更倾向于用户界面的阴影应该是垂直向下的,所以在iOS把阴影宽度设为0,然后高度设为一个正值不失为一个做法。

    shadowRadius属性控制着阴影的模糊度,当它的值是0的时候,阴影就和视图一样有一个非常确定的边界线。当值越来越大的时候,边界线看上去就会越来越模糊和自然。苹果自家的应用设计更偏向于自然的阴影,所以一个非零值再合适不过了。

    通常来讲,如果你想让视图或控件非常醒目独立于背景之外(比如弹出框遮罩层),你就应该给shadowRadius设置一个稍大的值。阴影越模糊,图层的深度看上去就会更明显(如图4.6).

图4.6

阴影裁剪

&nbps;   和图层边框不同,图层的阴影继承自内容的外形,而不是根据边界和角半径来确定。为了计算出阴影的形状,Core Animation会将寄宿图(包括子视图,如果有的话)考虑在内,然后通过这些来完美搭配图层形状从而创建一个阴影(见图4.7)。

图4.7

图4.7 阴影是根据寄宿图的轮廓来确定的

&nbps;   当阴影和裁剪扯上关系的时候就有一个头疼的限制:阴影通常就是在Layer的边界之外,如果你开启了masksToBounds属性,所有从图层中突出来的内容都会被才剪掉。如果我们在我们之前的边框示例项目中增加图层的阴影属性时,你就会发现问题所在(见图4.8).

图4.8

图4.8 maskToBounds属性裁剪掉了阴影和内容

&nbps;   从技术角度来说,这个结果是可以是可以理解的,但确实又不是我们想要的效果。如果你想沿着内容裁切,你需要用到两个图层:一个只画阴影的空的外图层,和一个用masksToBounds裁剪内容的内图层。

&nbps;   如果我们把之前项目的右边用单独的视图把裁剪的视图包起来,我们就可以解决这个问题(如图4.9).

图4.9

图4.9 右边,用额外的阴影转换视图包裹被裁剪的视图

&nbps;   我们只把阴影用在最外层的视图上,内层视图进行裁剪。清单4.3是代码实现,图4.10是运行结果。

清单4.3 用一个额外的视图来解决阴影裁切的问题

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *layerView1;
@property (nonatomic, weak) IBOutlet UIView *layerView2;
@property (nonatomic, weak) IBOutlet UIView *shadowView;

@end

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad];

//set the corner radius on our layers
self.layerView1.layer.cornerRadius = 20.0f;
self.layerView2.layer.cornerRadius = 20.0f;

//add a border to our layers
self.layerView1.layer.borderWidth = 5.0f;
self.layerView2.layer.borderWidth = 5.0f;

//add a shadow to layerView1
self.layerView1.layer.shadowOpacity = 0.5f;
self.layerView1.layer.shadowOffset = CGSizeMake(0.0f, 5.0f);
self.layerView1.layer.shadowRadius = 5.0f;

//add same shadow to shadowView (not layerView2)
self.shadowView.layer.shadowOpacity = 0.5f;
self.shadowView.layer.shadowOffset = CGSizeMake(0.0f, 5.0f);
self.shadowView.layer.shadowRadius = 5.0f;

//enable clipping on the second layer
self.layerView2.layer.masksToBounds = YES;
}

@end

图4.10

图4.10 右边视图,不受裁切阴影的阴影视图。

shadowPath属性

    我们已经知道图层阴影并不总是方的,而是从图层内容的形状继承而来。这看上去不错,但是实时计算阴影也是一个非常消耗资源的,尤其是图层有多个子图层,每个图层还有一个有透明效果的寄宿图的时候。

    如果你事先知道你的阴影形状会是什么样子的,你可以通过指定一个shadowPath来提高性能。shadowPath是一个CGPathRef类型(一个指向CGPath的指针)。CGPath是一个Core Graphics对象,用来指定任意的一个矢量图形。我们可以通过这个属性单独于图层形状之外指定阴影的形状。

图4.11 展示了同一寄宿图的不同阴影设定。如你所见,我们使用的图形很简单,但是它的阴影可以是你想要的任何形状。清单4.4是代码实现。

图4.11

图4.11 用shadowPath指定任意阴影形状

清单4.4 创建简单的阴影形状

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *layerView1;
@property (nonatomic, weak) IBOutlet UIView *layerView2;
@end

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad];

//enable layer shadows
self.layerView1.layer.shadowOpacity = 0.5f;
self.layerView2.layer.shadowOpacity = 0.5f;

//create a square shadow
CGMutablePathRef squarePath = CGPathCreateMutable();
CGPathAddRect(squarePath, NULL, self.layerView1.bounds);
self.layerView1.layer.shadowPath = squarePath; CGPathRelease(squarePath);

//create a circular shadow
CGMutablePathRef circlePath = CGPathCreateMutable();
CGPathAddEllipseInRect(circlePath, NULL, self.layerView2.bounds);
self.layerView2.layer.shadowPath = circlePath; CGPathRelease(circlePath);
}
@end

    如果是一个矩形或者是圆,用CGPath会相当简单明了。但是如果是更加复杂一点的图形,UIBezierPath类会更合适,它是一个由UIKit提供的在CGPath基础上的Objective-C包装类。

图4.6 大一些的阴影位移和角半径会增加图层的深度即视感

收起阅读 »

Android数据库高手秘籍,如何在Kotlin中更好地使用LitePal

前言 自从 LitePal 在 2.0.0 版本中全面支持了 Kotlin 之后,我也一直在思考如何让 LitePal 更好地融入和适配 Kotlin 语言,而不仅仅停留在简单的支持层面。 Kotlin 确实是一门非常出色的语言,里面有许多优秀的特性是在 ...
继续阅读 »

前言


自从 LitePal 在 2.0.0 版本中全面支持了 Kotlin 之后,我也一直在思考如何让 LitePal 更好地融入和适配 Kotlin 语言,而不仅仅停留在简单的支持层面。


Kotlin 确实是一门非常出色的语言,里面有许多优秀的特性是在 Java 中无法实现的。因此,在 LitePal 全面支持了 Kotlin 之后,我觉得如果我还视这些优秀特性而不见的话,就有些太暴殄天物了。所以在最新的 LitePal 3.0.0 版本里面,我准备让 LitePal 更加充分地利用 Kotlin 的一些语言特性,从而让我们的开发更加轻松。


本篇文章除了介绍 LitePal 3.0.0 版本的升级内容之外,还会讲解一些 Kotlin 方面的高级知识。


首先还是来看如何升级。


升级的方式


为什么这次的版本号跨度如此之大,直接从 2.0 升到了 3.0 呢?因为这次 LitePal 在结构上面有了一个质的变化。


为了更好地兼容 Kotlin 语言,LitePal 现在不再只是一个库了,而是变成了两个库,根据你使用的语言不同,需要引入的库也不同。如果你使用的是 Java,那么就在 build.gradle 中引入如下配置:


dependencies {
implementation 'org.litepal.android:java:3.0.0'
}


而如果你使用的是 Kotlin,那么就在 build.gradle 中引入如下配置:


dependencies {
implementation 'org.litepal.android:kotlin:3.0.0'
}


好了,接下来我们就一起看一看 LitePal 3.0.0 版本到底变更了哪些东西。


不得不说,其实 LitePal 的泛型设计一直都不是很友好,尤其在异步查询的时候格外难受,比如我们看下如下代码:


在异步查询的onFinish()回调中,我们直接得到的并不是查询的对象,而是一个泛型 T 对象,还需要再经过一次强制转型才能得到真正想要查询的对象。


如果你觉得这还不算难受的话,那么再来看看下面这个例子:



可以看到,这次查询返回的是一个List<T>,我们必须要对整个 List 进行强制转型。不仅要多写一行代码,关键是开发工具还会给出一个很丑的警告。


这样的设计无论如何都算不上友好。


这里非常感谢 xiazunyang 这位朋友在 GitHub 上提出的这个 Issue(github.com/LitePalFram… 3.0.0 版本在泛型方面的优化很大程度上是基于他的建议。


那么我们现在来看看,到了 LitePal 3.0.0 版本,同样的功能可以怎么写:


LitePal.findAsync(Song.class, 1).listen(new FindCallback<Song>() {
@Override
public void onFinish(Song song) {

}
});


可以看到,这里在FindCallback接口上声明了泛型类型为Song,那么在onFinish()方法回调中的参数就可以直接指定为Song类型了,从而避免了一次强制类型转换。


那么同样地,在查询多条数据的时候就可以这样写:


LitePal.where("duration > ?", "100").findAsync(Song.class).listen(new FindMultiCallback<Song>() {
@Override
public void onFinish(List<Song> list) {

}
});


这次就清爽多了吧,在onFinish()回调方法中,我们直接拿到的就是一个List<Song>集合,而不会再出现那个丑丑的警告了。


而如果这段代码使用 Kotlin 来编写的话,将会更加的精简:


LitePal.where("duration > ?", "100").findAsync(Song::class.java).listen { list ->

}


得益于 Kotlin 出色的 lambda 机制,我们的代码可以得到进一步精简。在上述代码中,行尾的list参数就是查询出来的List<Song>集合了。


那么关于泛型优化的讲解就到这里,下面我们来看另一个主题,监听数据库的创建和升级。


没错,LitePal 3.0.0 版本新增了监听数据库的创建和升级功能。


加入这个功能是因为 JakeHao 这位朋友在 GitHub 上提了一个 Issue(github.com/LitePalFram…


)


要实现这个功能肯定要添加新的接口了,而我对于添加新接口保持着一种比较谨慎的态度,因为要考虑到接口的易用性和对整体框架的影响。


LitePal 的每一个接口我都要尽量将它设计得简单好用,因此大家应该也可以猜到了,监听数据库创建和升级这个功能会非常容易,只需要简单几行代码就可以了实现了:


LitePal.registerDatabaseListener(new DatabaseListener() {
@Override
public void onCreate() {
}

@Override
public void onUpgrade(int oldVersion, int newVersion) {
}
});


需要注意的是,registerDatabaseListener()方法一定要确保在任何其他数据库操作之前调用,然后当数据库创建的时候,onCreate()方法就会得到回调,当数据库升级的时候onUpgrade()方法就会得到回调,并且告诉通过参数告诉你之前的老版本号,以及升级之后的新版本号。


Kotlin 版的代码也是类似的,但是由于这个接口有两个回调方法,因此用不了 Kotlin 的单抽象方法 (SAM) 这种语法糖,只能使用实现接口的匿名对象这种写法:


LitePal.registerDatabaseListener(object : DatabaseListener {
override fun onCreate() {
}

override fun onUpgrade(oldVersion: Int, newVersion: Int) {
}
})


这样我们就将监听数据库创建和升级这部分内容也快速介绍完了,接下来即将进入到本篇文章的重头戏内容。


从上述文章中我们都可以看出,Kotlin 版的代码普遍都是比 Java 代码要更简约的,Google 给出的官方统计是,使用 Kotlin 开发可以减少大约 25% 以上的代码。


但是处处讲究简约的 Kotlin,却在有一处用法上让我着实很难受。比如使用 Java 查询 song 表中 id 为 1 的这条记录是这样写的:


Song song = LitePal.find(Song.class, 1);


而同样的功能在 Kotlin 中却需要这样写:


val song = LitePal.find(Song::class.java, 1)


由于 LitePal 必须知道要查询哪个表当中的数据,因此一定要传递一个 Class 参数给 LitePal 才行。在 Java 中我们只需要传入Song.class即可,但是在 Kotlin 中的写法却变成了Song::class.java,反而比 Java 代码更长了,有没有觉得很难受?


当然,很多人写着写着也就习惯了,这并不是什么大问题。但是随着我深入学习 Kotlin 之后,我发现 Kotlin 提供了一个相当强大的机制可以优化这个问题,这个机制叫作泛型实化。接下来我会对泛型实化的概念和用法做个详细的讲解。


要理解泛型实化,首先你需要知道泛型擦除的概念。


不管是 Java 还是 Kotlin,只要是基于 JVM 的语言,泛型基本都是通过类型擦除来实现的。也就是说泛型对于类型的约束只在编译时期存在,运行时期是无法直接对泛型的类型进行检查的。例如,我们创建一个List<String>集合,虽然在编译时期只能向集合中添加字符串类型的元素,但是在运行时期 JVM 却并不能知道它本来只打算包含哪种类型的元素,只能识别出来它是个List


Java 的泛型擦除机制,使得我们不可能使用if (a instanceof T),或者是T.class这样的语法。


而 Kotlin 也是基于 JVM 的语言,因此 Kotlin 的泛型在运行时也是会被擦除的。但是 Kotlin 中提供了一个内联函数的概念,内联函数中的代码会在编译的时候自动被替换到调用它的地方,这就使得原有方法调用时的形参声明和实参传递,在编译之后直接变成了同一个方法内的变量调用。这样的话也就不存在什么泛型擦除的问题了,因为 Kotlin 在编译之后会直接使用实参替代内联方法中泛型部分的代码。


简单点来说,就是 Kotlin 是允许将内联方法中的泛型进行实化的。


泛型实化


那么具体该怎么写才能将泛型实化呢?首先,该方法必须是内联方法才行,也就是要用inline关键字来修饰该方法。其次,在声明泛型的地方还必须加上reified关键字来表示该泛型要进行实化。示例代码如下所示:


inline fun <reified T> instanceOf(value: Any) {

}


上述方法中的泛型 T 就是一个被实化的泛型,因为它满足了内联函数和reified关键字这两个前提条件。那么借助泛型实化,我们到底可以实现什么样的效果呢?从方法名上就可以看出来了,这里我们借助泛型来实现一个 instanceOf 的效果,代码如下所示:


inline fun <reified T> instanceOf(value: Any) = value is T


虽然只有一行代码,但是这里实现了一个 Java 中完全不可能实现的功能 —— 判断参数的类型是不是属于泛型的类型。这就是泛型实化不可思议的地方。


那么我们如何使用这个方法呢?在 Kotlin 中可以这么写:


val result1 = instanceOf<String>("hello")
val result2 = instanceOf<String>(123)



可以看到,第一行代码指定的泛型是String,参数是字符串"hello",因此最后的结果是true。而第二行代码指定泛型是String,参数却是数字123,因此最后的结果是false


除了可以做类型判断之外,我们还可以直接获取到泛型的 Class 类型。看一下下面的代码:


inline fun <reified T> genericClass() = T::class.java


这段代码就更加不可思议了,genericClass()方法直接返回了当前指定泛型的 class 类型。T.class这样的语法在 Java 中是不可能的,而在 Kotlin 中借助泛型实化功能就可以使用T::class.java这样的语法了。


然后我们就可以这样调用:


val result1 = genericClass<String>()
val result2 = genericClass<Int>()



可以看到,我们如果指定了泛型String,那么最终就可以得到java.lang.String的 Class,如果指定了泛型Int,最终就可以得到java.lang.Integer的 Class。


关于 Kotlin 泛型实化这部分的讲解就到这里,现在我们重新回到 LitePal 上面。讲了这么多泛型实化方面的内容,那么 LitePal 到底如何才能利用这个特性进行优化呢?


回顾一下,刚才我们查询 song 表中 id 为 1 的这条记录是这样写的:


val song = LitePal.find(Song::class.java, 1)


这里需要传入Song::class.java是因为要告知 LitePal 去查询 song 这张表中的数据。而通过刚才泛型实化部分的讲解,我们知道 Kotlin 中是可以使用T::class.java这样的语法的,因此我在 LitePal 3.0.0 中扩展了这部分特性,允许通过指定泛型来声明查询哪张表中的内容。于是代码就可以优化成这个样子了:


val song = LitePal.find<Song>(1)


怎么样,有没有觉得代码瞬间清爽了很多?看起来比 Java 版的查询还要更加简约。


另外得益于 Kotlin 出色的类型推导机制,我们还可以将代码改为如下写法:


val song: Song? = LitePal.find(1)


这两种写法效果是一模一样的,因为如果我在song变量的后面声明了Song?类型,那么find()方法就可以自动推导出泛型类型,从而不需要再手动进行<Song>的泛型指定了。


除了find()方法之外,我还对 LitePal 中几乎全部的公有 API 都进行了优化,只要是原来需要传递 Class 参数的接口,我都增加了一个通过指定泛型来替代 Class 参数的扩展方法。注意,这里我使用的是扩展方法,而不是修改了原有方法,这样的话两种写法你都可以使用,全凭自己的喜好,如果是直接修改原有方法,那么项目升级之后就可能会造成大面积报错了,这是谁都不想看到的。


那么这里我再向大家演示另外几种 CRUD 操作优化之后的用法吧,比如我想使用 where 条件查询的时候就可以这样写:


val list = LitePal.where("duration > ?", "100").find<Song>()


这里在最后的 find() 方法中指定了泛型<Song>,得到的结果会是一个List<Song>集合。


想要删除 song 表中 id 为 1 的这条数据可以这么写:


LitePal.delete<Song>(1)


想要统计 song 表中的记录数量可以这么写:


val count = LitePal.count<Song>()


其他一些方法的优化也都是类似的,相信大家完全可以举一反三,就不再一一演示了。


这样我们就将 LitePal 新版本中的主要功能都介绍完了。当然,除了这些新功能之外,我还修复了一些已知的 bug,提升了整体框架的稳定性,如果这些正是你所需要的话,那就赶快升级吧。


收起阅读 »

从精准化测试看ASM在Android中的强势插入-JaCoco初探

ASM
在Java技术栈上,基本上提到覆盖率,大家就会想到JaCoco「Java Code Coverage的缩写」,几乎所有的覆盖率项目,都是使用JaCoco,可想而知它的影响力有多大,我们在Android项目中,也集成了JaCoco,官网文档如下。 docs.g...
继续阅读 »

在Java技术栈上,基本上提到覆盖率,大家就会想到JaCoco「Java Code Coverage的缩写」,几乎所有的覆盖率项目,都是使用JaCoco,可想而知它的影响力有多大,我们在Android项目中,也集成了JaCoco,官网文档如下。


docs.gradle.org/current/use…


但是这里的JaCoco是与单元测试配合使用的,与一般的业务测试场景不太一样,所以,我们需要自己依赖JaCoco来做改造。


初探


官网镇楼


http://www.eclemma.org/jacoco/


从官网上就能看出这是一个极具历史感的项目。最后生成的覆盖率文件,是在 源代码的基础上,用颜色标记不同的执行状态。


image-20210716171811946


在上面这张图中,绿色代表已执行, 红色代表未执行, 黄色代表执行了一部分,这样就可以算出代码的覆盖率数据。


使用全量报表


JaCoco默认的插桩方式是全部插桩,在Android项目中,要使用JaCoco的全量报表功能非常简单,因为JaCoco插件已经集成在Gradle中了,所以我们只需要开启JaCoco即可。


首先,在根目录gradle文件中加入JaCoco的依赖


classpath "org.jacoco:org.jacoco.core:0.8.4"

然后在App的gradle文件中增加插件的依赖。


apply plugin: 'jacoco'

并在android标签中,增加开关。


testCoverageEnabled = true

接下来引入JaCoco的Report模块,同时exclude掉core,因为其在gradle中已经有依赖了。


implementation('org.jacoco:org.jacoco.report:0.8.4') {
exclude group: 'org.jacoco', module: 'org.jacoco.core'
}

创建生成Report的Task


def coverageSourceDirs = ['../xxxx/src/main/java']

task jacocoTestReport(type: JacocoReport) {
group = "Reporting"
description = "Generate Jacoco coverage reports after running tests."
reports {
xml.enabled = true
html.enabled = true
}
classDirectories.setFrom(fileTree(
dir: './build/intermediates/javac/xxxxx',
excludes: ['**/R*.class']))
sourceDirectories.setFrom(files(coverageSourceDirs))
executionData.setFrom(files("$buildDir/outputs/code-coverage/connected/coverage.exec"))
doFirst {new File("$buildDir/intermediates/javac/masterDebug/classes/com/qidian/QDReader").eachFileRecurse { file ->
if (file.name.contains('$$')) {
file.renameTo(file.path.replace('$$', '$'))
}
}
}
}

在项目中合适的地方来调用这两个方法,分别用来创建JaCoco的Exec文件和写入Exec文件。


private void createExecFile() {
String DEFAULT_COVERAGE_FILE_PATH = "/mnt/sdcard/" + getPackageName();
String DEFAULT_COVERAGE_FILE = DEFAULT_COVERAGE_FILE_PATH + "/coverage.ec";
File file_path = new File(DEFAULT_COVERAGE_FILE_PATH);
File file = new File(DEFAULT_COVERAGE_FILE);
Log.d(TAG, "file_path = " + file_path);
if (!file.exists()) {
try {
file_path.mkdirs();
file.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
}

private void writeExecFile() {
OutputStream out = null;
try {
out = new FileOutputStream("/mnt/sdcard/" + getPackageName() + "/coverage.ec", true);
Object agent = Class.forName("org.jacoco.agent.rt.RT")
.getMethod("getAgent")
.invoke(null);
out.write((byte[]) agent.getClass().getMethod("getExecutionData", boolean.class)
.invoke(agent, false));
} catch (Exception e) {
Log.d(TAG, e.toString(), e);
e.printStackTrace();
} finally {
if (out != null) {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

在创建Exec文件后,进行测试,然后写入Exec文件,等测试完毕后,把生成的Exec文件通过ADB pull到本地,再执行jacocoTestReport这个Task即可生成全量的JaCoco覆盖率报告。


花了这么长时间写了这么多,其实并没什么卵用,只是让大家看下如何来使用JaCoco的标准用法。


JaCoco插桩原理


JaCoco在Android上只能使用Offline mode,它的实现机制其实很简单,我们反编译一下它插入的代码。


image-20210617135224018


可以发现,实际上JaCoco就是用一个Boolean数组来标记每句可执行代码,只要执行过相应的语句,当前位就被标记为True,这个标记,官方称之为「探针」(Probe)。


JaCoco对代码的修改主要体现在下面几个地方:



  • 在Class中增加




    j


    a


    c


    o


    c


    o


    D


    a


    t


    a


    属性和



    jacocoData属性和


    jacocoInit方法

  • 在Method中增加了$jacocoInit数字并初始化

  • 增加了对数组的修改


当然,这只是JaCoco最基本的原理,实际的实现细节会更加复杂,例如条件、选择语句、方法函数的探针插入等等,这里不详细深入讨论,感兴趣的朋友可以参考JaCoco的源码:


github.com/jacoco/jaco…


性能影响


由于JaCoco只是插入一个探针数组,所以对代码执行的性能开销影响不大,但是由于插入大量的探针代码,所以代码体积会增大不少,一般情况下,Android会在测试包中做插入,而在正式包中去除插入逻辑。



当然,借助JaCoco还能玩一些骚操作,比如发到线上,实时统计代码中有哪些代码从未执行过,用于发现潜在的垃圾代码。



探针插桩策略


JaCoco的核心逻辑就是要决定,到底在哪插入探针代码。官网文档上对插桩策略写的比较清楚,涉及到字节码的一些原理,所以这里就不深入讲解了,感兴趣的朋友可以通过下面的链接查看。


http://www.jacoco.org/jacoco/trun…


关键代码类


JaCoco对代码的探针插入分析,主要是利用了下面这些计数器:



  • 指令计数器(CounterImpl)

  • 行计数器(LineImpl)

  • 方法计算节点(MethodCoverageImpl)

  • 类计算节点(ClassCoverageImpl)

  • Package计算节点(PackageCoverageImpl)

  • Module计算节点(BundleCoverageImpl)


这里面包含了JaCoco的覆盖率数据。


JaCoco的使用其实非常简单,原理也很简单,但要做的好,稳定运行这么多年没有Bug,还是很难的,所以现在市面上做覆盖率的很多软件都逐渐被历史所淘汰了,而剩下的就是经历过时间检验的真金。

收起阅读 »

JetpackSplashscreen解析助力新生代IT农民工事半功倍

公众号:ByteCode,致力于分享最新技术原创文章,涉及 Kotlin、Jetpack、译文、系统源码、 LeetCode / 剑指 Offer / 多线程 / 国内外大厂算法题 等等一系列文章。 Jetpack 家族迎来了一位新的成员 Core Sp...
继续阅读 »

公众号:ByteCode,致力于分享最新技术原创文章,涉及 Kotlin、Jetpack、译文、系统源码、 LeetCode / 剑指 Offer / 多线程 / 国内外大厂算法题 等等一系列文章。



Jetpack 家族迎来了一位新的成员 Core Splashscreen,所以我也要重新开始写 Jetpack 系列文章了,在这之前写过一系列 Jetpack 文章以及配套的实战应用,包含 App StartupPaging3HiltDataStoreViewBinding 等等实战项目,点击下方链接前去查看。



而今天这篇文章主要介绍 Google 新库 Core Splashscreen ,众所周知在 Android 12 中增加了一个改善用户体验的功能 SplashScreen API,它可为所有应用添加启动画面。包括启动时进入应用的启动动画,以及退出动画。


通过这篇文章你将学习到以下内容



  • Core Splashscreen 解决了什么问题?

  • Core Splashscreen 工作原理?

  • 针对不同的场景,如何在项目中使用 Core Splashscreen?

  • Core Splashscreen 源码分析?


Core Splashscreen 实战项目地址,可以前往 GitHub 查看示例项目 Splashscreen。 github.com/hi-dhl/Andr…


Core Splashscreen


Core Splashscreen 解决了什么问题?


在 Android 启动过程中会出现白屏 / 黑屏,为了改善这一体验,因此添加启动画面,从而改善视觉上的体验,为了实现这一功能,市面上也有很多实现方法,都有各自的优缺点,因此并不能保证在所有设备上都能够流畅的运行。


其次有的时候需要从本地磁盘或者网络异步加载数据,等待数据加载完之后,才会去渲染 View, 大多数时候,希望将数据加载提前,尽量保证用户进入到首页之后,看到数据,减少用户的等待时间。


在 Android 12 上新增的 SplashScreen API,可以解决这一系列问题,但是缺点是仅限于 Android 12。


Core Splashscreen 因此而诞生了,为 Android 12 新增的 SplashScreen API 提供了向后兼容,可以在 Android 5.0 (API 21) ~ Android 12 (API 31)所有的 API 上使用。来看一下 Google 提供的动画效果。



Core Splashscreen 工作原理


Core Splashscreen 为 Android 12 新增的 SplashScreen API 提供了向后兼容,但是仅仅在以下情况下才会显示启动画面:



  • 冷启动:用户打开 APP 时 APP 进程尚未运行

  • 温启动:APP 进程正在运行,但是 Activity 尚未创建


启动动画只有在以上情况才会显示,但是在热启动期间是不会显示启动画面。



  • 热启动:APP 进程正在运行,Activity 也已经创建,也就说用户按下 Home 键退到后台,直到 Activity 被销毁之前,是不会显示启动画面


如何使用 Core Splashscreen


因为 Core Splashscreen 兼容了 Android 12 新增的 SplashScreen API, 因此需要将 compileSdkVersion 更新到 31 及其以上。



如果你的 SDK 还没有更新到 Android 12, 请先更新。SDK Manager -> 选择 Android 12



android {
compileSdkVersion 31
}

在模块级别的 build.gradle 文件中添加以下依赖。


implementation 'androidx.core:core-splashscreen:1.0.0-alpha01'

当添加完依赖之后就可以开始使用 Core Splashscreen,只需要三步即可实现显示启动画面。


1. 在 res/values/themes.xml 文件下添加新的主题 Theme.AppSplashScreen


<style name="Theme.AppSplashScreen" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">@color/purple_200</item>
<item name="windowSplashScreenAnimatedIcon">@mipmap/ic_launcher</item>
<item name="postSplashScreenTheme">@style/Theme.AppTheme</item>
</style>

<!-- Base application theme. -->
<style name="Theme.AppTheme" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- 添加 APP 默认主题 -->
</style>


  • android:windowSplashScreenBackground : 设置背景颜色

  • windowSplashScreenAnimatedIcon : 设置显示在屏幕中间的图标, 如果是通过 AnimationDrawableAnimatedVectorDrawable 创建的对象,可呈现动画效果,则会在页面显示的时候,播放动画

  • postSplashScreenTheme : 设置显示动画不可见时,使用 APP 的默认主题


2. 在 application 节点中,设置上一步添加主题 Theme.AppSplashScreen


<application
android:theme="@style/Theme.AppSplashScreen">
</application>

3. 在调用 setContentView() 方法之前调用 installSplashScreen()


class MainActivity : AppCompatActivity() {
private val binding: ActivityMainBinding by viewbind()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
installSplashScreen()
with(binding) {
// init view
}
}
}

调用 installSplashScreen() 方法主要将 Activity 与我们添加的主题相关联。这一步完成之后,就可以在 APP 启动过程中,看到刚才设置的图标或者动画了。


扩展功能


让启动动画持久一点


默认情况下当应用绘制第一帧后,启动画面会立即关闭,但是有的时候需要从本地磁盘或者网络异步加载数据,这个时候,希望启动画面能够等到数据加载完回来才结束。可以通过以下方法实现。


splashScreen.setKeepVisibleCondition { !appReady }

// 模拟从本地磁盘或者网络异步加载数据的耗时操作
Handler(Looper.getMainLooper())
.postDelayed({ appReady = true }, 3000)

调用以上方法,可以让应用暂停绘制第一帧这样启动画面就不会结束,当数据加载完之后,通过更新变量 appReady 来控制是否结束启动画面。


实现退出动画


当然我们也可以添加启动画面的退出动画,即从启动画面优雅的回到应用主界面。


splashScreen.setOnExitAnimationListener { splashScreenViewProvider ->
......
// 自定义退出动画
val translationY = ObjectAnimator.ofFloat(......)
translationY.doOnEnd { splashScreenViewProvider.remove() }
translationY.start()
}

效果可以前往 GitHub 查看示例项目 Splashscreen。


GitHub 示例项目:https://github.com/hi-dhl/AndroidX-Jetpack-Practice


Core Splashscreen 源码解析


Core Splashscreen 源码很简单,总共就只有两个类。



  • SplashScreen :主要为实现 SplashScreen API 提供了向后兼容性,用于将 Activity 与主题相关联。

  • SplashScreenViewProvider : 用于控制退出动画(启动画面 -> 应用主界面),当退出动画结束时需要手动调用 SplashScreenViewProvider#remove() 方法


初始化 SplashScreen


通过调用 SplashScreen#installSplashScreen() 方法来进行初始化,将 Activity 与添加的主题相关联。 androidx/core/splashscreen/SplashScreen.kt


public companion object {
@JvmStatic
public fun Activity.installSplashScreen(): SplashScreen {
val splashScreen = SplashScreen(this)
splashScreen.install()
return splashScreen
}
}

private fun install() {
impl.install()
}

最终都是通过调用 impl.install() 方法来进行初始化,一起来看看成员变量 impl 是如何初始化的。


private val impl = when {
SDK_INT >= 31 -> Impl31(activity)
SDK_INT == 30 && PREVIEW_SDK_INT > 0 -> Impl31(activity)
SDK_INT >= 23 -> Impl23(activity)
else -> Impl(activity)
}

到这里我们知道了 Google 为了向后兼容,针对于不同版本的系统,分别对应有不同的实现类。最终都是调用 install() 方法来进行初始化的,在 install() 方法内通过解析我们添加的主题,最后通过 activity.setTheme() 方法,将添加的主题和 Activity 关联在一起。


如何让启动动画持久一点


在代码中,我们通过调用 SplashScreen#setKeepVisibleCondition() 方法,让启动动画持久一点,等待数据加完之后,才结束启动动画。一起来看看这个方法。 androidx/core/splashscreen/SplashScreen.kt


public fun setKeepVisibleCondition(condition: KeepOnScreenCondition) {
// impl:针对于不同版本的系统,分别对应有不同的实现类
impl.setKeepVisibleCondition(condition)
}

open fun setKeepVisibleCondition(keepOnScreenCondition: KeepOnScreenCondition) {
......
observer.addOnPreDrawListener(object : OnPreDrawListener {
override fun onPreDraw(): Boolean {
if (splashScreenWaitPredicate.shouldKeepOnScreen()) {
return false
}
contentView.viewTreeObserver.removeOnPreDrawListener(this)
// 当开始绘制时,会调用 dispatchOnExitAnimation 方法,结束启动动画
mSplashScreenViewProvider?.let(::dispatchOnExitAnimation)
return true
}
})
}

最后通过 ViewTreeObserver 来监听视图的变化,当视图将要开始绘制时,会回调 OnPreDrawListener#onPreDraw() 方法。最后调用 dispatchOnExitAnimation 方法,结束启动动画。


实现退出动画


最后一起来看一下,源码中是如何实现退出动画,即从启动画面优雅的回到应用主界面,源码中只是提供了一个 OnExitAnimationListener 接口,将退出动画交给了开发者去实现,一起来看一下SplashScreen#setOnExitAnimationListener() 方法。 androidx/core/splashscreen/SplashScreen.kt


Android 12 以上


override fun setOnExitAnimationListener(
exitAnimationListener: OnExitAnimationListener
) {
activity.splashScreen.setOnExitAnimationListener {
val splashScreenViewProvider = SplashScreenViewProvider(it, activity)
exitAnimationListener.onSplashScreenExit(splashScreenViewProvider)
}
}

在 Android 12 中是通过系统源码提供的接口 activity.splashScreen.setOnExitAnimationListener ,回调对外暴露的接口 OnExitAnimationListener 让开发者去实现退出动画的效果。


Android 12 以下


open fun setOnExitAnimationListener(exitAnimationListener: OnExitAnimationListener) {
animationListener = exitAnimationListener
val splashScreenViewProvider = SplashScreenViewProvider(activity)
......
splashScreenViewProvider.view.addOnLayoutChangeListener(
object : OnLayoutChangeListener {
override fun onLayoutChange(......) {
......
dispatchOnExitAnimation(splashScreenViewProvider)
}
})
}

fun dispatchOnExitAnimation(splashScreenViewProvider: SplashScreenViewProvider) {
......
splashScreenViewProvider.view.postOnAnimation {
finalListener.onSplashScreenExit(splashScreenViewProvider)
}
}

通过向屏幕中显示的 View 添加 addOnLayoutChangeListener 方法,来监听布局的变化,当布局会发生改变时,会回调 onLayoutChange 方法,最后通过回调对外暴露的接口 OnExitAnimationListener 让开发者去实现退出动画。


不过这里需要注意的是,最后都需要调用 SplashScreenViewProvider#remove() 方法在合适的时机移除动画,可以在退出动画结束时,调用这个方法。


总结


本文从不同的角度分别分析了 Core Splashscreen。如何在项目中使用 Core Splashscreen,可以前往 GitHub 查看示例项目 Splashscreen。


仓库地址:https://github.com/hi-dhl/AndroidX-Jetpack-Practice


另外 KtKit 是用 Kotlin 语言编写的小巧而实用工具库,包含了项目中常用的一系列工具,我添加了许多新的功能,包含了很多 Kotlin 技巧。文章分析可前往查看 为数不多的人知道的 Kotlin 技巧以及解析(三)


监听 EditText


将 Flow 通过 lifecycleScope 将 EditText 与 Activity / Fragment 的生命周期绑定在一起,在 Activity / Fragment 生命周期结束时,会结束 flow , flow 结束时会断开它们之间的引用,有效的避免内存泄漏。


......
// 监听 TextWatcher#onTextChanged 的回调函数
editText.textChange(lifecycleScope) {
Log.e(TAG, "textChange = $it")
}

// 监听 TextWatcher#beforeTextChanged 的回调函数
editText.textChangeWithbefore(lifecycleScope) {
Log.e(TAG, "textChangeWithbefore = $it")
}

// 监听 TextWatcher#afterTextChanged 的回调函数
editText.textChangeWithAfter(lifecycleScope) {
Log.e(TAG, "textChangeWithbefore = $it")
}
......

监听蜂窝网络变化


lifecycleScope.launch {
listenCellular().collect {
Log.e(TAG, "listenNetwork = $it")
}
}

监听 wifi 网络的变化


lifecycleScope.launch {
listenWifi().collect {
Log.e(TAG, "listenNetwork = $it")
}
}

监听蓝牙网络的变化


lifecycleScope.launch {
listenNetworkFlow().collect {
Log.e(TAG, "listenNetwork = $it")
}
}

更多 API 使用方式点击这里前往查看:



如果这个仓库对你有帮助,请在仓库右上角帮我 star 一下,非常感谢你的支持,同时也欢迎你提交 PR ??????




如果有帮助 点个赞 就是对我最大的鼓励


代码不止,文章不停


欢迎关注公众号:ByteCode,持续分享最新的技术










收起阅读 »

Flow操作符shareIn和stateIn使用须知

Flow.shareIn 与 Flow.stateIn 操作符可以将冷流转换为热流: 它们可以将来自上游冷数据流的信息广播给多个收集者。这两个操作符通常用于提升性能: 在没有收集者时加入缓冲;或者干脆作为一种缓存机制使用。 注意 : 冷流 是按需创建的...
继续阅读 »

Flow.shareInFlow.stateIn 操作符可以将冷流转换为热流: 它们可以将来自上游冷数据流的信息广播给多个收集者。这两个操作符通常用于提升性能: 在没有收集者时加入缓冲;或者干脆作为一种缓存机制使用。



注意 : 冷流 是按需创建的,并且会在它们被观察时发送数据;热流 则总是活跃,无论是否被观察,它们都能发送数据。



本文将会通过示例帮您熟悉 shareIn 与 stateIn 操作符。您将学到如何针对特定用例配置它们,并避免可能遇到的常见陷阱。


底层数据流生产者


继续使用我 之前文章 中使用过的例子——使用底层数据流生产者发出位置更新。它是一个使用 callbackFlow 实现的 冷流。每个新的收集者都会触发数据流的生产者代码块,同时也会将新的回调加入到 FusedLocationProviderClient。


class LocationDataSource(
private val locationClient: FusedLocationProviderClient
) {
val locationsSource: Flow<Location> = callbackFlow<Location> {
val callback = object : LocationCallback() {
override fun onLocationResult(result: LocationResult?) {
result ?: return
try { offer(result.lastLocation) } catch(e: Exception) {}
}
}
requestLocationUpdates(createLocationRequest(), callback, Looper.getMainLooper())
.addOnFailureListener { e ->
close(e) // in case of exception, close the Flow
}
// 在 Flow 结束收集时进行清理
awaitClose {
removeLocationUpdates(callback)
}
}
}

让我们看看在不同的用例下如何使用 shareIn 与 stateIn 优化 locationsSource 数据流。


shareIn 还是 stateIn?


我们要讨论的第一个话题是 shareInstateIn 之间的区别。shareIn 操作符返回的是 SharedFlowstateIn 返回的是 StateFlow



注意 : 要了解有关 StateFlowSharedFlow 的更多信息,可以查看 我们的文档



StateFlow 是 SharedFlow 的一种特殊配置,旨在优化分享状态: 最后被发送的项目会重新发送给新的收集者,并且这些项目会使用 Any.equals 进行合并。您可以在 StateFlow 文档 中查看更多相关信息。


两者之间的最主要区别,在于 StateFlow 接口允许您通过读取 value 属性同步访问其最后发出的值。而这不是 SharedFlow 的使用方式。


提升性能


通过共享所有收集者要观察的同一数据流实例 (而不是按需创建同一个数据流的新实例),这些 API 可以为我们提升性能。


在下面的例子中,LocationRepository 消费了 LocationDataSource 暴露的 locationsSource 数据流,同时使用了 shareIn 操作符,从而让每个对用户位置信息感兴趣的收集者都从同一数据流实例中收集数据。这里只创建了一个 locationsSource 数据流实例并由所有收集者共享:


class LocationRepository(
private val locationDataSource: LocationDataSource,
private val externalScope: CoroutineScope
) {
val locations: Flow<Location> =
locationDataSource.locationsSource.shareIn(externalScope, WhileSubscribed())
}

WhileSubscribed 共享策略用于在没有收集者时取消上游数据流。这样一来,我们便能在没有程序对位置更新感兴趣时避免资源的浪费。



Android 应用小提醒! 在大部分情况下,您可以使用 WhileSubscribed(5000),当最后一个收集者消失后再保持上游数据流活跃状态 5 秒钟。这样在某些特定情况 (如配置改变) 下可以避免重启上游数据流。当上游数据流的创建成本很高,或者在 ViewModel 中使用这些操作符时,这一技巧尤其有用。



缓冲事件


在下面的例子中,我们的需求有所改变。现在要求我们保持监听位置更新,同时要在应用从后台返回前台时在屏幕上显示最后的 10 个位置:


class LocationRepository(
private val locationDataSource: LocationDataSource,
private val externalScope: CoroutineScope
) {
val locations: Flow<Location> =
locationDataSource.locationsSource
.shareIn(externalScope, SharingStarted.Eagerly, replay = 10)
}

我们将参数 replay 的值设置为 10,来让最后发出的 10 个项目保持在内存中,同时在每次有收集者观察数据流时重新发送这些项目。为了保持内部数据流始终处于活跃状态并发送位置更新,我们使用了共享策略 SharingStarted.Eagerly,这样就算没有收集者,也能一直监听更新。


缓存数据


我们的需求再次发生变化,这次我们不再需要应用处于后台时 持续 监听位置更新。不过,我们需要缓存最后发送的项目,让用户在获取当前位置时能在屏幕上看到一些数据 (即使数据是旧的)。针对这种情况,我们可以使用 stateIn 操作符。


class LocationRepository(
private val locationDataSource: LocationDataSource,
private val externalScope: CoroutineScope
) {
val locations: Flow<Location> =
locationDataSource.locationsSource.stateIn(externalScope, WhileSubscribed(), EmptyLocation)
}

Flow.stateIn 可以缓存最后发送的项目,并重放给新的收集者。


注意!不要在每个函数调用时创建新的实例


切勿 在调用某个函数调用返回时,使用 shareIn 或 stateIn 创建新的数据流。这样会在每次函数调用时创建一个新的 SharedFlow 或 StateFlow,而它们将会一直保持在内存中,直到作用域被取消或者在没有任何引用时被垃圾回收。


class UserRepository(
private val userLocalDataSource: UserLocalDataSource,
private val externalScope: CoroutineScope
) {
// 不要像这样在函数中使用 shareIn 或 stateIn
// 这将在每次调用时创建新的 SharedFlow 或 StateFlow,而它们将不会被复用。
fun getUser(): Flow<User> =
userLocalDataSource.getUser()
.shareIn(externalScope, WhileSubscribed())

// 可以在属性中使用 shareIn 或 stateIn
val user: Flow<User> =
userLocalDataSource.getUser().shareIn(externalScope, WhileSubscribed())
}

需要入参的数据流


需要入参 (如 userId) 的数据流无法简单地使用 shareInstateIn 共享。以开源项目——Google I/O 的 Android 应用 iosched 为例,您可以在 源码中 看到,从 Firestore 获取用户事件的数据流是通过 callbackFlow 实现的。由于其接收 userId 作为参数,因此无法简单使用 shareInstateIn 操作符对其进行复用。


class UserRepository(
private val userEventsDataSource: FirestoreUserEventDataSource
) {
// 新的收集者会在 Firestore 中注册为新的回调。
// 由于这一函数依赖一个 `userId`,所以在这个函数中
// 数据流无法通过调用 shareIn 或 stateIn 进行复用.
// 这样会导致每次调用函数时,都会创建新的 SharedFlow 或 StateFlow
fun getUserEvents(userId: String): Flow<UserEventsResult> =
userLocalDataSource.getObservableUserEvents(userId)
}

如何优化这一用例取决于您应用的需求:



  • 您是否允许同时从多个用户接收事件?如果答案是肯定的,您可能需要为 SharedFlowStateFlow 实例创建一个 map,并在 subscriptionCount 为 0 时移除引用并退出上游数据流。

  • 如果您只允许一个用户,并且收集者需要更新为观察新的用户,您可以向一个所有收集者共用的 SharedFlowStateFlow 发送事件更新,并将公共数据流作为类中的变量。


shareInstateIn 操作符可以与冷流一同使用来提升性能,您可以使用它们在没有收集者时添加缓冲,或者直接将其作为缓存机制使用。小心使用它们,不要在每次函数调用时都创建新的数据流实例——这样会导致资源的浪费及预料之外的问题!


收起阅读 »

【Gradle7.0】依赖统一管理的全新方式,了解一下~

前言 随着项目的不断发展,项目中的依赖也越来越多,有时可能会有几百个,这个时候对项目依赖做一个统一的管理很有必要,我们一般会有以下需求: 项目依赖统一管理,在单独文件中配置 不同Module中的依赖版本号统一 不同项目中的依赖版本号统一 ...
继续阅读 »

前言


随着项目的不断发展,项目中的依赖也越来越多,有时可能会有几百个,这个时候对项目依赖做一个统一的管理很有必要,我们一般会有以下需求:



  1. 项目依赖统一管理,在单独文件中配置

  2. 不同Module中的依赖版本号统一

  3. 不同项目中的依赖版本号统一


针对这些需求,目前其实已经有了一些方案:



  1. 使用循环优化Gradle依赖管理

  2. 使用buildSrc管理Gradle依赖

  3. 使用includeBuild统一配置依赖版本


上面的方案支持在不同Module间统一版本号,同时如果需要在项目间共享,也可以做成Gradle插件发布到远端,已经基本可以满足我们的需求
不过Gradle7.0推出了一个新的特性,使用Catalog统一依赖版本,它支持以下特性:



  1. 对所有module可见,可统一管理所有module的依赖

  2. 支持声明依赖bundles,即总是一起使用的依赖可以组合在一起

  3. 支持版本号与依赖名分离,可以在多个依赖间共享版本号

  4. 支持在单独的libs.versions.toml文件中配置依赖

  5. 支持在项目间共享依赖


使用Version Catalog


注意,Catalog仍然是一个孵化中的特性,如需使用,需要在settings.gradle中添加以下内容:


enableFeaturePreview('VERSION_CATALOGS')

从命名上也可以看出,Version Catalog其实就是一个版本的目录,我们可以从目录中选出我们需要的依赖使用
我们可以通过如下方式使用Catalog中声明的依赖


dependencies {
implementation(libs.retrofit)
implementation(libs.groovy.core)
}

在这种情况下,libs是一个目录,retrofit表示该目录中可用的依赖项。 与直接在构建脚本中声明依赖项相比,Version Catalog具有许多优点:



  • 对于每个catalog,Gradle都会生成类型安全的访问器,以便你在IDE中可以自动补全.(注:目前在build.gradle中还不能自动补全,可能是指kts或者开发中?)

  • 声明在catalog中的依赖对所有module可见,当修改版本号时,可以统一管理统一修改

  • catalog支持声明一个依赖bundles,即一些总是一起使用的依赖的组合

  • catalog支持版本号与依赖名分离,可以在多个依赖间共享版本号


声明Version Catalog


Version Catalog可以在settings.gradle(.kts)文件中声明。


dependencyResolutionManagement {
versionCatalogs {
libs {
alias('retrofit').to('com.squareup.retrofit2:retrofit:2.9.0')
alias('groovy-core').to('org.codehaus.groovy:groovy:3.0.5')
alias('groovy-json').to('org.codehaus.groovy:groovy-json:3.0.5')
alias('groovy-nio').to('org.codehaus.groovy:groovy-nio:3.0.5')
alias('commons-lang3').to('org.apache.commons', 'commons-lang3').version {
strictly '[3.8, 4.0['
prefer '3.9'
}
}
}
}

别名必须由一系列以破折号(-,推荐)、下划线 (_) 或点 (.) 分隔的标识符组成。
标识符本身必须由ascii字符组成,最好是小写,最后是数字。


值得注意的是,groovy-core会被映射成libs.groovy.core
如果你想避免映射可以使用大小写来区分,比如groovyCore会被处理成libs.groovyCore


具有相同版本号的依赖


在上面的示例中,我们可以看到三个groovy依赖具有相同的版本号,我们可以把它们统一起来


dependencyResolutionManagement {
versionCatalogs {
libs {
version('groovy', '3.0.5')
version('compilesdk', '30')
version('targetsdk', '30')
alias('groovy-core').to('org.codehaus.groovy', 'groovy').versionRef('groovy')
alias('groovy-json').to('org.codehaus.groovy', 'groovy-json').versionRef('groovy')
alias('groovy-nio').to('org.codehaus.groovy', 'groovy-nio').versionRef('groovy')
alias('commons-lang3').to('org.apache.commons', 'commons-lang3').version {
strictly '[3.8, 4.0['
prefer '3.9'
}
}
}
}

除了在依赖中,我们同样可以在build.gradle中获取版本,比如可以用来指定compileSdk


android {
compileSdk libs.versions.compilesdk.get().toInteger()


defaultConfig {
applicationId "com.zj.gradlecatalog"
minSdk 21
targetSdk libs.versions.targetsdk.get().toInteger()
}
}

如上,可以使用catalog统一compileSdk,targetSdk,minSdk的版本号


依赖bundles


因为在不同的项目中经常系统地一起使用某些依赖项,所以Catalog提供了bundle(依赖包)的概念。依赖包基本上是几个依赖项打包的别名。
例如,你可以这样使用一个依赖包,而不是像上面那样声明 3 个单独的依赖项:


dependencies {
implementation libs.bundles.groovy
}

groovy依赖包声明如下:


dependencyResolutionManagement {
versionCatalogs {
libs {
version('groovy', '3.0.5')
version('checkstyle', '8.37')
alias('groovy-core').to('org.codehaus.groovy', 'groovy').versionRef('groovy')
alias('groovy-json').to('org.codehaus.groovy', 'groovy-json').versionRef('groovy')
alias('groovy-nio').to('org.codehaus.groovy', 'groovy-nio').versionRef('groovy')
alias('commons-lang3').to('org.apache.commons', 'commons-lang3').version {
strictly '[3.8, 4.0['
prefer '3.9'
}
bundle('groovy', ['groovy-core', 'groovy-json', 'groovy-nio'])
}
}
}

如上所示:添加groovy依赖包等同于添加依赖包下的所有依赖项


插件版本


除了Library之外,Catalog还支持声明插件版本。
因为library由它们的groupartifactversion表示,但Gradle插件仅由它们的idversion标识。
因此,插件需要单独声明:


dependencyResolutionManagement {
versionCatalogs {
libs {
alias('jmh').toPluginId('me.champeau.jmh').version('0.6.5')
}
}
}

然后可以在plugins块下面使用


plugins {
id 'java-library'
id 'checkstyle'
// 使用声明的插件
alias(libs.plugins.jmh)
}

在单独文件中配置Catalog


除了在settings.gradle中声明Catalog外,也可以通过一个单独的文件来配置Catalog
如果在根构建的gradle目录中找到了libs.versions.toml文件,则将使用该文件的内容自动声明一个Catalog


TOML文件主要由4个部分组成:



  • [versions] 部分用于声明可以被依赖项引用的版本

  • [libraries] 部分用于声明Library的别名

  • [bundles] 部分用于声明依赖包

  • [plugins] 部分用于声明插件


如下所示:


[versions]
groovy = "3.0.5"
checkstyle = "8.37"
compilesdk = "30"
targetsdk = "30"

[libraries]
retrofit = "com.squareup.retrofit2:retrofit:2.9.0"
groovy-core = { module = "org.codehaus.groovy:groovy", version.ref = "groovy" }
groovy-json = { module = "org.codehaus.groovy:groovy-json", version.ref = "groovy" }
groovy-nio = { module = "org.codehaus.groovy:groovy-nio", version.ref = "groovy" }
commons-lang3 = { group = "org.apache.commons", name = "commons-lang3", version = { strictly = "[3.8, 4.0[", prefer="3.9" } }

[bundles]
groovy = ["groovy-core", "groovy-json", "groovy-nio"]

[plugins]
jmh = { id = "me.champeau.jmh", version = "0.6.5" }

如上所示,依赖可以定义成一个字符串,也可以将moduleversion分离开来
其中versions可以定义成一个字符串,也可以定义成一个范围,详情可参见rich-version


[versions]
my-lib = { strictly = "[1.0, 2.0[", prefer = "1.2" }

在项目间共享Catalog


Catalog不仅可以在项目内统一管理依赖,同样可以实现在项目间共享
例如我们需要在团队内制定一个依赖规范,不同组的不同项目需要共享这些依赖,这是个很常见的需求


通过文件共享


Catalog支持通过从Toml文件引入依赖,这就让我们可以通过指定文件路径来实现共享依赖
如下所示,我们在settins.gradle中配置如下:


dependencyResolutionManagement {
versionCatalogs {
libs {
from(files("../gradle/libs.versions.toml"))
}
}
}

此技术可用于声明来自不同文件的多个目录:


dependencyResolutionManagement {
versionCatalogs {
// 声明一个'testLibs'目录, 从'test-libs.versions.toml'文件中
testLibs {
from(files('gradle/test-libs.versions.toml'))
}
}
}

发布插件实现共享


虽然从本地文件导入Catalog很方便,但它并没有解决在组织或外部消费者中共享Catalog的问题。
我们还可能通过Catalog插件来发布目录,这样用户直接引入这个插件即可


Gradle提供了一个Catalog插件,它提供了声明然后发布Catalog的能力。


1. 首先引入两个插件


plugins {
id 'version-catalog'
id 'maven-publish'
}

然后,此插件将公开可用于声明目录的catalog扩展


2. 定义目录


上面引入插件后,即可使用catalog扩展定义目录


catalog {
// 定义目录
versionCatalog {
from files('../libs.versions.toml')
}
}

然后可以通过maven-publish插件来发布目录


3. 发布目录


publishing {
publications {
maven(MavenPublication) {
groupId = 'com.zj.catalog'
artifactId = 'catalog'
version = '1.0.0'
from components.versionCatalog
}
}
}

我们定义好groupId,artifactId,version,from就可以发布了
我们这里发布到mavenLocal,你也可以根据需要配置发布到自己的maven
以上发布的所有代码可见:Catalog发布相关代码


4. 使用目录


因为我们已经发布到了mavenLocal,在仓库中引入mavenLocal就可以使用插件了


# settings.gradle
dependencyResolutionManagement {
//...
repositories {
mavenLocal()
//...
}
}

enableFeaturePreview('VERSION_CATALOGS')
dependencyResolutionManagement {
versionCatalogs {
libs {
from("com.zj.catalog:catalog:1.0.0")
// 我们也可以重写覆盖catalog中的groovy版本
version("groovy", "3.0.6")
}
}
}

如上就成功引入了插件,就可以使用catalog中的依赖了
这样就完成了依赖的项目间共享,以上使用的所有代码可见:Catalog使用相关代码


总结


项目间共享依赖是比较常见的需求,虽然我们也可以通过自定义插件实现,但还是不够方便
Gradle官方终于推出了Catalog,让我们可以方便地实现依赖的共享,Catalog主要具有以下特性:



  1. 对所有module可见,可统一管理所有module的依赖

  2. 支持声明依赖bundles,即总是一起使用的依赖可以组合在一起

  3. 支持版本号与依赖名分离,可以在多个依赖间共享版本号

  4. 支持在单独的libs.versions.toml文件中配置依赖

  5. 支持在项目间共享依赖


本文所有相关代码


Catalog发布相关代码
Catalog使用相关代码


参考资料



Sharing dependency versions between projects

收起阅读 »

Android超简单实现验证码倒计时,页面关闭不中断,杀掉进程也不中断

在日常开发中,获取验证码是一个常见的功能,通常验证码倒计时的实现思路都是使用CountDownTimer来实现,但是存在一个问题就是当页面关闭之后重新进入页面,倒计时是不会继续进行的,如果后端验证码接口做了时间限制,那么我们再次请求的时候就会报错,用户体验不好...
继续阅读 »

在日常开发中,获取验证码是一个常见的功能,通常验证码倒计时的实现思路都是使用CountDownTimer来实现,但是存在一个问题就是当页面关闭之后重新进入页面,倒计时是不会继续进行的,如果后端验证码接口做了时间限制,那么我们再次请求的时候就会报错,用户体验不好。 作为一名CV工程师,在一番百度之后,唯一找到的一个实现方案还需要花钱下载,哎。。。无奈只能自己想了。

实现思路

其实实现思路很简单,使用CountDownTimer进行倒计时,在开始倒计时的时候,把 当前时间+倒计时总时间 持久化存储,再次打开页面的时候判断一下当前时间是否在倒计时时间的范围内,如果在就继续倒计时。

效果如下

在这里插入图片描述

倒计时工具类

持久化存储我使用了腾讯的MMKV,当前你也可以使用SharedPreference 如果你使用的也是MMKV,别忘了在Application中初始化

package com.lzk.jetpacktest.code

import android.os.CountDownTimer
import com.tencent.mmkv.MMKV

/**
* @Author: LiaoZhongKai
* @Date: 2021/8/17 16:38
* @Description: 验证码倒计时工具类
*/
object CodeCountDownUtil {
private const val KEY_TIME = "TotalTimeMills"
private val mMkv: MMKV = MMKV.mmkvWithID("CodeCountDownUtil")
private var mTotalTimeMills = 0L
private var mListener: OnCountDownListener? = null

private val mCountDownTimer: CountDownTimer
get() {
return object : CountDownTimer(getCountTimeMills(),1000){
override fun onTick(millisUntilFinished: Long) {
mListener?.onTick(millisUntilFinished/1000)
}

override fun onFinish() {
mListener?.onFinish()
}

}
}

/**
* 开始倒计时
* [totalTimeMills] 倒计时时间毫秒值
*/
fun start(totalTimeMills: Long){
mTotalTimeMills = totalTimeMills+System.currentTimeMillis()
mMkv.encode(KEY_TIME, mTotalTimeMills)
mCountDownTimer.start()
}

/**
* 继续倒计时
*/
fun continueCount(){
mCountDownTimer.start()
}

/**
* 取消倒计时
*/
fun cancel(){
mCountDownTimer.cancel()
}

/**
* 获取当前的倒计时时间毫秒值
*/
fun getCountTimeMills(): Long{
mTotalTimeMills = mMkv.decodeLong(KEY_TIME)
return mTotalTimeMills - System.currentTimeMillis()
}

/**
* 当前时间是否在倒计时范围内
* @return true: 倒计时正在进行 false:倒计时未进行
*/
fun isCounting(): Boolean{
mTotalTimeMills = mMkv.decodeLong(KEY_TIME)
return System.currentTimeMillis() < mTotalTimeMills
}

/**
* 设置监听器
*/
fun setOnCountDownListener(listener: OnCountDownListener){
mListener = listener
}

interface OnCountDownListener{
/**
* 倒计时
* [seconds] 倒计时剩余时间,秒为单位
*/
fun onTick(seconds: Long)

/**
* 倒计时结束
*/
fun onFinish()
}
}

使用示例

package com.lzk.jetpacktest.code

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import androidx.databinding.DataBindingUtil
import com.lzk.jetpacktest.R
import com.lzk.jetpacktest.databinding.ActivityVerificationCodeBinding

/**
* @Author: LiaoZhongKai
* @Date: 2021/8/18 9:22
* @Description: 验证码页面
*/
class VerificationCodeActivity : AppCompatActivity() {

companion object{
//验证码倒计时总时间
//60秒
private const val COUNT_DOWN_TIME = 60*1000L
}

private lateinit var mBinding: ActivityVerificationCodeBinding

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mBinding = DataBindingUtil.setContentView(this,R.layout.activity_verification_code)
initView()
initEvent()
}

//千万不要忘记在onDestroy中取消倒计时
//千万不要忘记在onDestroy中取消倒计时
//千万不要忘记在onDestroy中取消倒计时
override fun onDestroy() {
super.onDestroy()
CodeCountDownUtil.cancel()
}

private fun initView(){
//如果当前时间仍在倒计时范围内,则显示倒计时
if (CodeCountDownUtil.isCounting()){
CodeCountDownUtil.continueCount()
mBinding.btn.isEnabled = false
}else{//否则显示获取验证码
mBinding.btn.text = "获取验证码"
}
}

private fun initEvent(){
//设置监听器
CodeCountDownUtil.setOnCountDownListener(object : CodeCountDownUtil.OnCountDownListener{
override fun onTick(seconds: Long) {
mBinding.btn.text = "$seconds 秒后重新获取"
}

override fun onFinish() {
mBinding.btn.isEnabled = true
mBinding.btn.text = "获取验证码"
}

})

//获取验证码按钮
mBinding.btn.setOnClickListener {
//模拟验证码发送成功
Toast.makeText(this,"验证码已发送",Toast.LENGTH_SHORT).show()
//按照正常流程,以下代码应该在验证码发送成功之后再调用
//开始倒计时
CodeCountDownUtil.start(COUNT_DOWN_TIME)
mBinding.btn.isEnabled = false
}
}

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


<data>

</data>

<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".code.VerificationCodeActivity">


<Button
android:id="@+id/btn"
android:layout_width="150dp"
android:layout_height="50dp"
android:layout_gravity="center"
android:text="获取验证码" />

</FrameLayout>
</layout>
收起阅读 »

四大组件之Service|Android开发系列

概述  Service 是一种可在后台执行长时间运行操作而不提供界面的应用组件,与Activity不同,Activity是现实图形用户界面的,而Service的运行是不可见的。服务可由其他应用组件启动,即使切换到其他的应用,Service仍将在后台继...
继续阅读 »

概述

  Service 是一种可在后台执行长时间运行操作而不提供界面的应用组件,与Activity不同,Activity是现实图形用户界面的,而Service的运行是不可见的。服务可由其他应用组件启动,即使切换到其他的应用,Service仍将在后台继续运行。此外,组件可通过绑定到服务与之进行交互,甚至是执行进程间通信 (IPC)。例如,服务可在后台处理网络事务、播放音乐,执行文件 I/O 或与内容提供程序进行交互。

Sercvvice类型

前台Service

  前台服务执行一些用户能注意到的操作。例如,音频应用会使用前台服务来播放音频曲目。前台服务必须显示Notification。即使用户停止与应用的交互,前台服务仍会继续运行。

后台Service

  后台服务执行用户不会直接注意到的操作。例如,如果应用使用某个服务来压缩其存储空间,则此服务通常是后台服务。

注意:如果您的应用面向 API 级别 26 或更高版本,当应用本身未在前台运行时,系统会对运行后台服务施加限制。在诸如此类的大多数情况下,您的应用应改为使用计划作业

绑定Service

   绑定Service方式核心在于bindService()绑定服务会提供客户端-服务器接口,以便组件与服务进行交互、发送请求、接收结果,甚至是利用进程间通信 (IPC) 跨进程执行这些操作。仅当与另一个应用组件绑定时,绑定服务才会运行。多个组件可同时绑定到该服务,但全部取消绑定后,该服务即会被销毁。

Service生命周期

image.png

public class MyService extends Service {

/**
* 首次创建服务时,系统会(在调用 onStartCommand() 或 onBind() 之前)调用此方法来执行一次性设置程序。如果服务已在运行,则不会调用此方法。
*/
@Override
public void onCreate() {
super.onCreate();
}

/**
* 当另一个组件(如 Activity)请求启动服务时,系统会通过调用 startService() 来调用此方法。执行此方法时,服务即会启动并可在后台无限期运行。
* 如果您实现此方法,则在服务工作完成后,您需负责通过调用 stopSelf() 或 stopService() 来停止服务。(如果您只想提供绑定,则无需实现此方法。)
* @param intent
* @param flags
* @param startId
* @return
*/
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return super.onStartCommand(intent, flags, startId);
}

/**
* 当另一个组件想要与服务绑定(例如执行 RPC)时,系统会通过调用 bindService() 来调用此方法。在此方法的实现中,您必须通过返回 IBinder 提供一个接口,
* 以供客户端用来与服务进行通信。请务必实现此方法;但是,如果您并不希望允许绑定,则应返回 null。
* @param intent
* @return
*/
@Override
public IBinder onBind(Intent intent) {
return null;
}

/**
* 取消绑定时调用
* @param intent
* @return
*/
@Override
public boolean onUnbind(Intent intent) {
return super.onUnbind(intent);
}

/**
* 当不再使用服务且准备将其销毁时,系统会调用此方法。服务应通过实现此方法来清理任何资源,如线程、注册的侦听器、接收器等。这是服务接收的最后一个调用。
*/
@Override
public void onDestroy() {
super.onDestroy();
}
}

创建启动和绑定Service

   常见的需要定义一个Service的子类,重写其生命周期方法,当创建一个Service后,必须将这个Service在AndroidManifest.xml中进行注册。

<service
android:name=".MyService"
android:enabled="true"
android:exported="true" />
public class MainActivity extends AppCompatActivity implements View.OnClickListener {

private Button mStartService;
private Button mBindService;
private Button mUnBindService;
private Button mStopService;
private ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {

}

@Override
public void onServiceDisconnected(ComponentName name) {

}
};

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mStartService = findViewById(R.id.start_service);
mBindService = findViewById(R.id.onBind_service);
mUnBindService = findViewById(R.id.unbind_service);
mStopService = findViewById(R.id.stop_service);
mStartService.setOnClickListener(this);
mBindService.setOnClickListener(this);
mUnBindService.setOnClickListener(this);
mStopService.setOnClickListener(this);
}

@Override
public void onClick(View v) {
switch (v.getId()){
case R.id.start_service:
Intent startIntent = new Intent(this,MyService.class);
startService(startIntent);
break;
case R.id.onBind_service:
Intent bindIntent = new Intent(this,MyService.class);
bindService(bindIntent, serviceConnection,BIND_AUTO_CREATE);
break;
case R.id.unbind_service:
unbindService(serviceConnection);
break;
case R.id.stop_service:
Intent stopIntent = new Intent();
stopService(stopIntent);
break;
}
}
}
  • 如果服务存在,binderService方法只能是onBind方法被调用,而unBindService方法只能被onUnbind被调用
收起阅读 »

Android Binder 学习笔记-未完结

读者须知:在跟随大佬学习的历程中,不断地跟着源码查看,主要参阅的博客我会在文章开头着重声明一遍。由于自己能力有限,在总结过程中发现很多东西没有想象的简单,该文章暂时处于烂尾模式....重启计划暂定于一年后。 Binder 真的太深了(个人感觉,短时间内没办法总...
继续阅读 »

读者须知:

在跟随大佬学习的历程中,不断地跟着源码查看,主要参阅的博客我会在文章开头着重声明一遍。由于自己能力有限,在总结过程中发现很多东西没有想象的简单,该文章暂时处于烂尾模式....

重启计划暂定于一年后。 Binder 真的太深了(个人感觉,短时间内没办法总结好)

如果有看到该篇文章想学Binder知识的同学,可参考以下三篇文章

Binder设计原理

写给 Android 应用工程师的 Binder 原理剖析

Binder源码解析

当然在学习图中为了能看到那个部分比喻,也重新温习了 互联网协议 和 DNS 原理 DNS原理 和 互联网协议

Binder学习笔记 1

前言

Android系统中,每个应用程序是由Android的Activity,Service,Broadcast,ContentProvider这四组件的中一个或多个组合而成,这四组件所涉及的多进程间的通信底层都是依赖于Binder IPC机制。 此处借用大佬的一段话来诠释Binder Binder设计原理

Android作为一个开放式,拥有众多开发者的的平台,应用程序的来源广泛,确保智能终端的安全是非常重要的。终端用户不希望从网上下载的程序在不知情的情况下偷窥隐私数据,连接无线网络,长期操作底层设备导致电池很快耗尽等等。传统IPC没有任何安全措施,完全依赖上层协议来确保。首先传统IPC的接收方无法获得对方进程可靠的UID/PID(用户ID/进程ID),从而无法鉴别对方身份。Android为每个安装好的应用程序分配了自己的UID,故进程的UID是鉴别进程身份的重要标志。使用传统IPC只能由用户在数据包里填入UID/PID,但这样不可靠,容易被恶意程序利用。可靠的身份标记只有由IPC机制本身在内核中添加。其次传统IPC访问接入点是开放的,无法建立私有通道。比如命名管道的名称,system V的键值,socket的ip地址或文件名都是开放的,只要知道这些接入点的程序都可以和对端建立连接,不管怎样都无法阻止恶意程序通过猜测接收方地址获得连接。

基于以上原因,Android需要建立一套新的IPC机制来满足系统对通信方式,传输性能和安全性的要求,这就是Binder。Binder基于Client-Server通信模式,传输过程只需一次拷贝,为发送发添加UID/PID身份,既支持实名Binder也支持匿名Binder,安全性高。

Binder使用Client-Server通信方式:一个进程作为Server提供诸如视频/音频解码,视频捕获,地址本查询,网络连接等服务;多个进程作为Client向Server发起服务请求,获得所需要的服务。要想实现Client-Server通信据必须实现以下两点:一是server必须有确定的访问接入点或者说地址来接受Client的请求,并且Client可以通过某种途径获知Server的地址;二是制定Command-Reply协议来传输数据。例如在网络通信中Server的访问接入点就是Server主机的IP地址+端口号,传输协议为TCP协议。对Binder而言,Binder可以看成Server提供的实现某个特定服务的访问接入点, Client通过这个‘地址’向Server发送请求来使用该服务;对Client而言,Binder可以看成是通向Server的管道入口,要想和某个Server通信首先必须建立这个管道并获得管道入口。

与其它IPC不同,Binder使用了面向对象的思想来描述作为访问接入点的Binder及其在Client中的入口:Binder是一个实体位于Server中的对象,该对象提供了一套方法用以实现对服务的请求,就象类的成员函数。遍布于client中的入口可以看成指向这个binder对象的‘指针’,一旦获得了这个‘指针’就可以调用该对象的方法访问server。在Client看来,通过Binder‘指针’调用其提供的方法和通过指针调用其它任何本地对象的方法并无区别,尽管前者的实体位于远端Server中,而后者实体位于本地内存中。‘指针’是C++的术语,而更通常的说法是引用,即Client通过Binder的引用访问Server。而软件领域另一个术语‘句柄’也可以用来表述Binder在Client中的存在方式。从通信的角度看,Client中的Binder也可以看作是Server Binder的‘代理’,在本地代表远端Server为Client提供服务。

IPC 简单概要理解

进程间通信(inter-process communication或interprocess communication,简写IPC)是指两个或两个以上进程(或线程)之间进行数据或信号交互的技术方案。

11.webp

每个Android的进程,只能运行在自己进程所拥有的虚拟地址空间。举例 对应一个4GB的虚拟地址空间,其中3GB是用户空间,1GB是内核空间,当然内核空间的大小是可以通过参数配置调整的。对于用户空间,不同进程之间彼此是不能共享的,而内核空间却是可共享的。Client进程向Server进程通信,恰恰是利用 进程间可共享的内核内存空间 来完成底层通信工作的,Client端与Server端进程往往采用ioctl等方法跟内核空间的驱动进行交互。

Binder 原理

sdk ver : Android 6.0

Gityuan 博客学习地址 Binder 部分为本人你学习其博客的笔记与总结。

22.webp

Binder通信采用C/S架构,从组件视角来说,包含Client、Server、ServiceManager以及binder驱动,其中ServiceManager用于管理系统中的各种服务。架构图如上所示。

可以看出无论是注册服务和获取服务的过程都需要ServiceManager,需要注意的是此处的Service Manager是指Native层的ServiceManager(C++)。ServiceManager是整个Binder通信机制的管家,是Android进程间通信机制Binder的守护进程,来负责 查询 和 注册服务 。和DNS类似,ServiceManager的作用是将字符形式的Binder名字转化成Client中对该Binder的引用,使得Client能够通过Binder名字获得对Server中Binder实体的引用。

图中Client/Server/ServiceManage之间的相互通信都是基于Binder机制。既然基于Binder机制通信,那么同样也是C/S架构,则图中的3大步骤都有相应的Client端与Server端。

  1. 注册服务(addService):Server进程要先注册Service到ServiceManager。该过程:Server是客户端,ServiceManager是服务端。
  2. 获取服务(getService):Client进程使用某个Service前,须先向ServiceManager中获取相应的Service。该过程:Client是客户端,ServiceManager是服务端。
  3. 使用服务:Client根据得到的Service信息建立与Service所在的Server进程通信的通路,然后就可以直接与Service交互。该过程:client是客户端,server是服务端。

上图中的 Client,Server,Service Manager 之间交互都是虚线表示,是由于它们彼此之间不是直接交互的,而是都通过与Binder驱动进行交互的,从而实现IPC通信方式。其中Binder驱动位于内核空间,Client,Server,Service Manager位于用户空间。Binder驱动和Service Manager可以看做是Android平台的基础架构,而Client和Server是Android的应用层,开发人员只需自定义实现client、Server端,借助Android的基本平台架构便可以直接进行IPC通信。

Binder 学习笔记 2

Binder 中 ServerManage启动

SMgr是一个进程,Server是另一个进程,Server向SMgr注册Binder必然会涉及进程间通信。当前实现的是进程间通信却又要用到进程间通信,这就好象蛋可以孵出鸡前提却是要找只鸡来孵蛋。Binder的实现比较巧妙:预先创造一只鸡来孵蛋:ServiceManager和其它进程同样采用Binder通信,ServiceManager是Server端,有自己的Binder对象(实体),其它进程都是Client,需要通过这个Binder的引用来实现Binder的注册,查询和获取。ServiceManager提供的Binder比较特殊,它没有名字也不需要注册,当一个进程使用BINDER_SET_CONTEXT_MGR命令将自己注册成ServiceManager时Binder驱动会自动为它创建Binder实体(这就是那只预先造好的鸡)。其次这个Binder的引用在所有Client中都固定为0而无须通过其它手段获得。也就是说,一个Server若要向ServiceManager注册自己Binder就必需通过0这个引用号和ServiceManager的Binder通信。

↓↓↓↓↓↓ ServerManage 启动流程图

33.webp

ServiceManager是Binder IPC通信过程中的守护进程,本身也是一个Binder服务,但并没有采用libbinder中的多线程模型来与Binder驱动通信,而是自行编写了binder.c直接和Binder驱动来通信,并且只有一个循环binder_loop来进行读取和处理事务,接下来我们通过源码看下 ServiceManager怎么玩的。

通过查看官网 得知ServiceManager 是由init进程通过解析init.rc文件而创建的,其所对应的可执行程序/system/bin/servicemanager,所对应的源文件是service_manager.c。通过 service_manager.c 中的main() 函数为入口 查看整个启动 ServiceManager的流程。

主要方法都在 main函数中出现,后续是对部分方法的深入查看

// 主方法
int main(int argc, char **argv) {
struct binder_state *bs;
//第一步 打开binder驱动,申请128k字节大小的内存空间
bs = binder_open(128*1024);
...

//第二步 成为上下文管理者
if (binder_become_context_manager(bs)) {
return -1;
}

//selinux权限 判断进程 是否有权利注册或者查看
selinux_enabled = is_selinux_enabled();
sehandle = selinux_android_service_context_handle();
selinux_status_open(true);

if (selinux_enabled > 0) {
if (sehandle == NULL) {
abort(); //无法获取sehandle
}
if (getcon(&service_manager_context) != 0) {
abort(); //无法获取service_manager上下文
}
}
...

//第三步 进入无限循环,处理client端发来的请求。svcmgr_handler 主要提供服务注册和查找
binder_loop(bs, svcmgr_handler);
return 0;
}

第一步 打开binder驱动

对应上图中的 第一步到第四步

  1. 先调用open()打开binder设备,open()方法经过系统调用,进入Binder驱动,然后调用方法binder_open(),该方法会在Binder驱动层创建一个binder_proc对象,再将binder_proc对象赋值给fd->private_data,同时放入全局链表binder_procs。

  2. 再通过ioctl()检验当前binder版本与Binder驱动层的版本是否一致。

  3. 调用mmap()进行内存映射,同理mmap()方法经过系统调用,对应于Binder驱动层的binder_mmap()方法,该方法会在Binder驱动层创建Binder_buffer对象,并放入当前binder_proc的proc->buffers链表。

第二步 注册成为binder服务的大管家

对应上图中的 第五到 第七步

根据 main() 函数中的 第二步代码 引用链为 binder_become_context_manager(struct binder_state) -> binder_ioctl(bs->fd, BINDER_SET_CONTEXT_MGR, 0) -> binder_ioctl_set_ctx_mgr(struct file *filp)

在 binder_ioctl_set_ctx_mgr()方法中 创建了全局的单例binder_node对象binder_context_mgr_node,并将binder_context_mgr_node的强弱引用各加1.部分代码块如下:

static int binder_ioctl_set_ctx_mgr(struct file *filp)
{
int ret = 0;
struct binder_proc *proc = filp->private_data;
kuid_t curr_euid = current_euid();
//保证只创建一次mgr_node对象
if (binder_context_mgr_node != NULL) {
ret = -EBUSY;
goto out;
}
if (uid_valid(binder_context_mgr_uid)) {
...
} else {
//设置当前线程euid作为Service Manager的uid
binder_context_mgr_uid = curr_euid;
}
//创建ServiceManager实体 终于找到了
binder_context_mgr_node = binder_new_node(proc, 0, 0);
...
binder_context_mgr_node->local_weak_refs++;
binder_context_mgr_node->local_strong_refs++;
binder_context_mgr_node->has_strong_ref = 1;
binder_context_mgr_node->has_weak_ref = 1;
out:
return ret;
}

通过跟踪 我们发现了 binder_new_node () 代码块如下:


static struct binder_node *binder_new_node(struct binder_proc *proc,
binder_uintptr_t ptr,
binder_uintptr_t cookie)
{
struct rb_node **p = &proc->nodes.rb_node;
struct rb_node *parent = NULL;
struct binder_node *node;
//首次进来为空
while (*p) {
parent = *p;
node = rb_entry(parent, struct binder_node, rb_node);

if (ptr < node->ptr)
p = &(*p)->rb_left;
else if (ptr > node->ptr)
p = &(*p)->rb_right;
else
return NULL;
}

//给新创建的binder_node 分配内核空间
node = kzalloc(sizeof(*node), GFP_KERNEL);
if (node == NULL)
return NULL;
binder_stats_created(BINDER_STAT_NODE);
// 将新创建的node对象添加到proc红黑树;
rb_link_node(&node->rb_node, parent, p);
rb_insert_color(&node->rb_node, &proc->nodes);
node->debug_id = ++binder_last_id;
node->proc = proc;
node->ptr = ptr;
node->cookie = cookie;
node->work.type = BINDER_WORK_NODE; //设置binder_work的type
INIT_LIST_HEAD(&node->work.entry);
INIT_LIST_HEAD(&node->async_todo);
return node;
}

在Binder驱动层创建binder_node结构体对象,并将当前binder_proc加入到binder_node的node->proc。并创建binder_node的async_todo和binder_work两个队列。

第三步 无限循环,处理client端发来的请求

对应上图中的 第九到 第十三步

void binder_loop(struct binder_state *bs, binder_handler func) {
int res;
struct binder_write_read bwr;
uint32_t readbuf[32];
bwr.write_size = 0;
bwr.write_consumed = 0;
bwr.write_buffer = 0;
readbuf[0] = BC_ENTER_LOOPER;

//将BC_ENTER_LOOPER命令发送给binder驱动,让Service Manager进入循环
binder_write(bs, readbuf, sizeof(uint32_t));
for (;;) {
bwr.read_size = sizeof(readbuf);
bwr.read_consumed = 0;
bwr.read_buffer = (uintptr_t) readbuf;
//进入循环,不断地binder读写过程
res = ioctl(bs->fd, BINDER_WRITE_READ, &bwr);
if (res < 0) {
break;
}
// 解析binder信息
res = binder_parse(bs, 0, (uintptr_t) readbuf, bwr.read_consumed, func);
if (res == 0) {
break;
}
if (res < 0) {
break;
}
}
}

进入循环读写操作,由main()方法传递过来的参数func指向svcmgr_handler。

binder_write通过ioctl()将BC_ENTER_LOOPER命令发送给binder驱动,此时bwr只有write_buffer有数据,进入binder_thread_write()方法。 接下来进入for循环,执行ioctl(),此时bwr只有read_buffer有数据,那么进入binder_thread_read()方法。

我们通过 binder_write() -> ioctl(bs->fd, BINDER_WRITE_READ, &bwr) -> binder_ioctl_write_read() -> binder_thread_write()

binder_thread_write 方法

static int binder_thread_write(struct binder_proc *proc, struct binder_thread *thread, binder_uintptr_t binder_buffer, size_t size, binder_size_t *consumed) {
uint32_t cmd;
void __user *buffer = (void __user *)(uintptr_t)binder_buffer;
void __user *ptr = buffer + *consumed;
void __user *end = buffer + size;

while (ptr < end && thread->return_error == BR_OK) {
get_user(cmd, (uint32_t __user *)ptr); //获取命令
switch (cmd) {
case BC_ENTER_LOOPER:
//设置该线程的looper状态
thread->looper |= BINDER_LOOPER_STATE_ENTERED;
break;
case ...;
}
}
}

从bwr.write_buffer拿出cmd数据,此处为BC_ENTER_LOOPER. 可见上层本次调用binder_write()方法,主要是完成设置当前线程的looper状态为BINDER_LOOPER_STATE_ENTERED。

第四部分 Binder消息的处理


int binder_parse(struct binder_state *bs, struct binder_io *bio,
uintptr_t ptr, size_t size, binder_handler func)
{
int r = 1;
uintptr_t end = ptr + (uintptr_t) size;

while (ptr < end) {
uint32_t cmd = *(uint32_t *) ptr;
ptr += sizeof(uint32_t);
switch(cmd) {
case BR_NOOP: //无操作,退出循环
break;
case BR_TRANSACTION_COMPLETE:
break;
case BR_INCREFS:
case BR_ACQUIRE:
case BR_RELEASE:
case BR_DECREFS:
ptr += sizeof(struct binder_ptr_cookie);
break;
case BR_TRANSACTION: {
struct binder_transaction_data *txn = (struct binder_transaction_data *) ptr;
...
binder_dump_txn(txn);
if (func) {
unsigned rdata[256/4];
struct binder_io msg;
struct binder_io reply;
int res;
// 对 binder_io 进行初始化设置
bio_init(&reply, rdata, sizeof(rdata), 4);
//从txn解析并复制给binder_io信息
bio_init_from_txn(&msg, txn);
//
res = func(bs, txn, &msg, &reply);
// 向 binder 驱动通信
binder_send_reply(bs, &reply, txn->data.ptr.buffer, res);
}
ptr += sizeof(*txn);
break;
}
case BR_REPLY: {
struct binder_transaction_data *txn = (struct binder_transaction_data *) ptr;
...
binder_dump_txn(txn);
if (bio) {
bio_init_from_txn(bio, txn);
bio = 0;
}
ptr += sizeof(*txn);
r = 0;
break;
}
case BR_DEAD_BINDER: {
struct binder_death *death = (struct binder_death *)(uintptr_t) *(binder_uintptr_t *)ptr;
ptr += sizeof(binder_uintptr_t);
// binder死亡消息
death->func(bs, death->ptr);
break;
}
case BR_FAILED_REPLY:
r = -1;
break;
case BR_DEAD_REPLY:
r = -1;
break;
default:
return -1;
}
}
return r;
}

binder_parse方法,先调用svcmgr_handler(),再然后执行binder_send_reply过程。该方法会调用binder_write进入binder驱动后,将BC_FREE_BUFFER和BC_REPLY命令协议发送给Binder驱动,向client端发送reply. 其中data的数据区中保存的是TYPE为HANDLE.

ServiceManager启动总结

  • 打开binder驱动,并调用binder.c 文件中的mmap()方法分配128k的内存映射空间:binder_open();
  • 通知binder驱动使其成为守护进程:binder_become_context_manager();通过binder驱动中的binder_new_node()方法 创建 ServiceManager 对象实体类
  • 验证selinux权限,判断进程是否有权注册或查看指定服务;
  • 进入循环状态,等待Client端的请求:binder_loop()。
  • 注册服务的过程,根据服务名称,但同一个服务已注册,重新注册前会先移除之前的注册信息;
  • 死亡通知: 当binder所在进程死亡后,会调用binder_release方法,然后调用binder_node_release.这个过程便会发出死亡通知的回调.

--------------------------------------------------- 章节分割线 ---------------------------------------------------

Binder 的一些理解

Binder优点

通过看完上文的 Binder驱动中的工作,我们不妨找一下Binder为何能成为Android 进程交互中流砥柱。

暂且撇开Binder,考虑一下传统的IPC方式中,数据是怎样从发送端到达接收端的呢?通常的做法是,发送方将准备好的数据存放在缓存区中,调用API通过系统调用进入内核中。内核服务程序在内核空间分配内存,将数据从发送方缓存区复制到内核缓存区中。接收方读数据时也要提供一块缓存区,内核将数据从内核缓存区拷贝到接收方提供的缓存区中并唤醒接收线程,完成一次数据发送。这种存储-转发机制有两个缺陷:首先是效率低下,需要做两次拷贝:用户空间->内核空间->用户空间。Linux使用copy_from_user()和copy_to_user()实现这两个跨空间拷贝,在此过程中如果使用了高端内存(high memory),这种拷贝需要临时建立/取消页面映射,造成性能损失。其次是接收数据的缓存要由接收方提供,可接收方不知道到底要多大的缓存才够用,只能开辟尽量大的空间或先调用API接收消息头获得消息体大小,再开辟适当的空间接收消息体。两种做法都有不足,不是浪费空间就是浪费时间。

Binder采用一种全新策略:由Binder驱动负责管理数据接收缓存。我们注意到Binder驱动实现了mmap()系统调用,这对字符设备是比较特殊的,因为mmap()通常用在有物理存储介质的文件系统上,而象Binder这样没有物理介质,纯粹用来通信的字符设备没必要支持mmap()。Binder驱动当然不是为了在物理介质和用户空间做映射,而是用来创建数据接收的缓存空间。先看mmap()是如何使用的:

fd = open("/dev/binder", O_RDWR);

mmap(NULL, MAP_SIZE, PROT_READ, MAP_PRIVATE, fd, 0);

这样Binder的接收方就有了一片大小为MAP_SIZE的接收缓存区。mmap()的返回值是内存映射在用户空间的地址,不过这段空间是由驱动管理,用户不必也不能直接访问(映射类型为PROT_READ,只读映射)。

接收缓存区映射好后就可以做为缓存池接收和存放数据了。前面说过,接收数据包的结构为binder_transaction_data,但这只是消息头,真正的有效负荷位于data.buffer所指向的内存中。这片内存不需要接收方提供,恰恰是来自mmap()映射的这片缓存池。在数据从发送方向接收方拷贝时,驱动会根据发送数据包的大小,使用最佳匹配算法从缓存池中找到一块大小合适的空间,将数据从发送缓存区复制过来。要注意的是,存放binder_transaction_data结构本身以及表4中所有消息的内存空间还是得由接收者提供,但这些数据大小固定,数量也不多,不会给接收方造成不便。映射的缓存池要足够大,因为接收方的线程池可能会同时处理多条并发的交互,每条交互都需要从缓存池中获取目的存储区,一旦缓存池耗竭将产生导致无法预期的后果。

有分配必然有释放。接收方在处理完数据包后,就要通知驱动释放data.buffer所指向的内存区。在介绍Binder协议时已经提到,这是由命令BC_FREE_BUFFER完成的。

通过上面介绍可以看到,驱动为接收方分担了最为繁琐的任务:分配/释放大小不等,难以预测的有效负荷缓存区,而接收方只需要提供缓存来存放大小固定,最大空间可以预测的消息头即可。在效率上,由于mmap()分配的内存是映射在接收方用户空间里的,所有总体效果就相当于对有效负荷数据做了一次从发送方用户空间到接收方用户空间的直接数据拷贝,省去了内核中暂存这个步骤,提升了一倍的性能。顺便再提一点,Linux内核实际上没有从一个用户空间到另一个用户空间直接拷贝的函数,需要先用copy_from_user()拷贝到内核空间,再用copy_to_user()拷贝到另一个用户空间。为了实现用户空间到用户空间的拷贝,mmap()分配的内存除了映射进了接收方进程里,还映射进了内核空间。所以调用copy_from_user()将数据拷贝进内核空间也相当于拷贝进了接收方的用户空间,这就是Binder只需一次拷贝的‘秘密’

Binder 中的线程管理

Binder通信实际上是位于不同进程中的线程之间的通信。假如进程S是Server端,提供Binder实体,线程T1从Client进程C1中通过Binder的引用向进程S发送请求。S为了处理这个请求需要启动线程T2,而此时线程T1处于接收返回数据的等待状态。T2处理完请求就会将处理结果返回给T1,T1被唤醒得到处理结果。在这过程中,T2仿佛T1在进程S中的代理,代表T1执行远程任务,而给T1的感觉就是象穿越到S中执行一段代码又回到了C1。为了使这种穿越更加真实,驱动会将T1的一些属性赋给T2,特别是T1的优先级nice,这样T2会使用和T1类似的时间完成任务。很多资料会用‘线程迁移’来形容这种现象,容易让人产生误解。一来线程根本不可能在进程之间跳来跳去,二来T2除了和T1优先级一样,其它没有相同之处,包括身份,打开文件,栈大小,信号处理,私有数据等。

对于Server进程S,可能会有许多Client同时发起请求,为了提高效率往往开辟线程池并发处理收到的请求。怎样使用线程池实现并发处理呢?这和具体的IPC机制有关。拿socket举例,Server端的socket设置为侦听模式,有一个专门的线程使用该socket侦听来自Client的连接请求,即阻塞在accept()上。这个socket就象一只会生蛋的鸡,一旦收到来自Client的请求就会生一个蛋 – 创建新socket并从accept()返回。侦听线程从线程池中启动一个工作线程并将刚下的蛋交给该线程。后续业务处理就由该线程完成并通过这个单与Client实现交互。

可是对于Binder来说,既没有侦听模式也不会下蛋,怎样管理线程池呢?一种简单的做法是,先创建一堆线程,每个线程都用BINDER_WRITE_READ命令读Binder。这些线程会阻塞在驱动为该Binder设置的等待队列上,一旦有来自Client的数据驱动会从队列中唤醒一个线程来处理。这样做简单直观,省去了线程池,但一开始就创建一堆线程有点浪费资源。于是Binder协议引入了专门命令或消息帮助用户管理线程池,包括:

· INDER_SET_MAX_THREADS // 设置最大线程数

· BC_REGISTER_LOOP // 注册

· BC_ENTER_LOOP // 进入

· BC_EXIT_LOOP // 退出

· BR_SPAWN_LOOPER // 通知线程即将不够使用,创建指令

首先要管理线程池就要知道池子有多大,应用程序通过INDER_SET_MAX_THREADS告诉驱动最多可以创建几个线程。以后每个线程在创建,进入主循环,退出主循环时都要分别使用BC_REGISTER_LOOP,BC_ENTER_LOOP,BC_EXIT_LOOP告知驱动,以便驱动收集和记录当前线程池的状态。每当驱动接收完数据包返回读Binder的线程时,都要检查一下是不是已经没有闲置线程了。如果是,而且线程总数不会超出线程池最大线程数,就会在当前读出的数据包后面再追加一条BR_SPAWN_LOOPER消息,告诉用户线程即将不够用了,请再启动一些,否则下一个请求可能不能及时响应。新线程一启动又会通过BC_xxx_LOOP告知驱动更新状态。这样只要线程没有耗尽,总是有空闲线程在等待队列中随时待命,及时处理请求。

关于工作线程的启动,Binder驱动还做了一点小小的优化。当进程P1的线程T1向进程P2发送请求时,驱动会先查看一下线程T1是否也正在处理来自P2某个线程请求但尚未完成(没有发送回复)。这种情况通常发生在两个进程都有Binder实体并互相对发时请求时。假如驱动在进程P2中发现了这样的线程,比如说T2,就会要求T2来处理T1的这次请求。因为T2既然向T1发送了请求尚未得到返回包,说明T2肯定(或将会)阻塞在读取返回包的状态。这时候可以让T2顺便做点事情,总比等在那里闲着好。而且如果T2不是线程池中的线程还可以为线程池分担部分工作,减少线程池使用率。

↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ 以下为未完成篇幅

Binder 学习笔记 5

Binder 中服务注册流程

服务注册过程(addService)核心功能:在服务所在进程创建binder_node,在servicemanager进程创建binder_ref。 其中binder_ref的desc再同一个进程内是唯一的:

  • 每个进程binder_proc所记录的binder_ref的handle值是从1开始递增的;
  • 所有进程binder_proc所记录的handle=0的binder_ref都指向service manager;
  • 同一个服务的binder_node在不同进程的binder_ref的handle值可以不同;

Media服务注册的过程涉及到MediaPlayerService(作为Client进程)和Service Manager(作为Service进程),通信流程图如下所示:

444.webp

过程分析:

  1. MediaPlayerService进程调用ioctl()向Binder驱动发送IPC数据,该过程可以理解成一个事务binder_transaction(记为T1),执行当前操作的线程binder_thread(记为thread1),则T1->from_parent=NULL,T1->from = thread1,thread1->transaction_stack=T1。其中IPC数据内容包含:
  • Binder协议为BC_TRANSACTION;
  • Handle等于0;
  • RPC代码为ADD_SERVICE;
  • RPC数据为”media.player”。
  1. Binder驱动收到该Binder请求,生成BR_TRANSACTION命令,选择目标处理该请求的线程,即ServiceManager的binder线程(记为thread2),则 T1->to_parent = NULL,T1->to_thread = thread2。并将整个binder_transaction数据(记为T2)插入到目标线程的todo队列;

  2. Service Manager的线程thread2收到T2后,调用服务注册函数将服务”media.player”注册到服务目录中。当服务注册完成后,生成IPC应答数据(BC_REPLY),T2->form_parent = T1,T2->from = thread2, thread2->transaction_stack = T2。

  3. Binder驱动收到该Binder应答请求,生成BR_REPLY命令,T2->to_parent = T1,T2->to_thread = thread1, thread1->transaction_stack = T2。 在MediaPlayerService收到该命令后,知道服务注册完成便可以正常使用。

整个过程中,BC_TRANSACTION和BR_TRANSACTION过程是一个完整的事务过程;BC_REPLY和BR_REPLY是一个完整的事务过程。

收起阅读 »

熟悉又陌生的Handler

熟悉又陌生的Handler-3nativeInit:在上文中,关于Handler三件套的创建流程,第一个涉及到的JNI调用就是MessageQueue的nativeInit方法。MessageQueue(boolean quitAllowed) { m...
继续阅读 »


熟悉又陌生的Handler-3

nativeInit:

在上文中,关于Handler三件套的创建流程,第一个涉及到的JNI调用就是MessageQueue的nativeInit方法。

MessageQueue(boolean quitAllowed) {
mQuitAllowed = quitAllowed;
mPtr = nativeInit();
}

具体实现在framework/base/core/jni/android_os_MessageQueue.cpp中:

static jlong android_os_MessageQueue_nativeInit(JNIEnv* env, jclass clazz) {
// 首先创建NativeMessageQueue对象,该对象持有Native侧的Looper对象
NativeMessageQueue* nativeMessageQueue = new NativeMessageQueue();
//如果创建失败,直接异常
if (!nativeMessageQueue) {
jniThrowRuntimeException(env, "Unable to allocate native queue");
return 0;
}
// 增加NativeMessageQueue的引用计数
nativeMessageQueue->incStrong(env);
// 返回nativeMessageQueue这个指针给Java层
return reinterpret_cast<jlong>(nativeMessageQueue);
}

在NativeMessageQueue的构造函数中:

NativeMessageQueue::NativeMessageQueue() :
mPollEnv(NULL), mPollObj(NULL), mExceptionObj(NULL) {
// 通过TLS(Thread Local Storage线程局部存储)获取当前线程的Native的Looper对象
mLooper = Looper::getForThread();
if (mLooper == NULL) {
// 如果没有,那么会去创建一个Native的Looper对象
mLooper = new Looper(false);
// 并将创建的Looper对象保存到TLS中
Looper::setForThread(mLooper);
}
}

需要注意这里的Looper对象和Java层的Looper对象并没有什么直接关联。在其构造方法中,最关联的两件事:

Looper::Looper(bool allowNonCallbacks)
: mAllowNonCallbacks(allowNonCallbacks),
mSendingMessage(false),
mPolling(false),
mEpollRebuildRequired(false),
mNextRequestSeq(0),
mResponseIndex(0),
mNextMessageUptime(LLONG_MAX) {
// 初始化表示唤醒事件的文件描述符mWakeEventFd
// eventfd这个系统调用用于创建或者打开一个eventfd的文件,类似于文件的open操作
// 这里传入的初始值为0,然后设置的标志位为
// EFD_CLOEXEC:FD_CLOEXEC,简单说就是fork子进程时不继承,对于多线程的程序设上这个值不会有错的。
// EFD_NONBLOCK:
// 文件会被设置成O_NONBLOCK(非阻塞IO,读取不到数据的时候或写入缓冲区满了会return -1),
// 而不是阻塞调用,一般要设置。
mWakeEventFd.reset(eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC));
AutoMutex _l(mLock);
// 重新创建当前Looper的Epoll事件
rebuildEpollLocked();
}

rebuildEpollLocked实现如下:

void Looper::rebuildEpollLocked() {
// 如果当前Looper已经有了EpollFd,即已经有了旧的epoll实例,那么先重置一下
if (mEpollFd >= 0) {
mEpollFd.reset();
}
// 创建新的Epoll实例
mEpollFd.reset(epoll_create1(EPOLL_CLOEXEC));
struct epoll_event eventItem;
// 初始化eventItem占用的内存空间
memset(& eventItem, 0, sizeof(epoll_event)); // zero out unused members of data field union
// EPOLLIN :表示对应的文件描述符可以读
eventItem.events = EPOLLIN;
eventItem.data.fd = mWakeEventFd.get();
// 调用epoll_ctl操作mEpollFd对应的Epoll实例,将mWakeEventFd(唤醒事件)
// 添加到mEpoll对应的epoll实例上
int result = epoll_ctl(mEpollFd.get(), EPOLL_CTL_ADD, mWakeEventFd.get(), &eventItem);
for (size_t i = 0; i < mRequests.size(); i++) {
const Request& request = mRequests.valueAt(i);
struct epoll_event eventItem;
request.initEventItem(&eventItem);
// 将Request也一并添加到epoll实例上
int epollResult = epoll_ctl(mEpollFd.get(), EPOLL_CTL_ADD, request.fd, &eventItem);
if (epollResult < 0) {
ALOGE("Error adding epoll events for fd %d while rebuilding epoll set: %s",
request.fd, strerror(errno));
}
}
}

epoll_event结构体如下:

struct epoll_event {
uint32_t events;
epoll_data_t data;
}

typedef union epoll_data {
void* ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;

events成员变量:可以是以下几个宏的集合:

  • EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
  • EPOLLOUT:表示对应的文件描述符可以写;
  • EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
  • EPOLLERR:表示对应的文件描述符发生错误;
  • EPOLLHUP:表示对应的文件描述符被挂断;
  • EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
  • EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里。

既然Java层的MessageQueue在创建的时候,有创建Native层的MessageQueue,那么同样的Java层MQ在销毁的时候,也会触发NativeMessageQueue的销毁,Native层MQ的销毁比较简单,实质上就是Native层一个对象的清除:

  1. 移除对象的引用关系。
  2. delete调用清除对象的内存空间。

nativeWake:

从上文分析中,我们知道,在Java层当我们调用MessageQueue.enqueueMessage的时候,在Java层觉得需要唤醒消息队列的时候,会调用nativeWake这个native方法:

static void android_os_MessageQueue_nativeWake(JNIEnv* env, jclass clazz, jlong ptr) {
NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr);
// 调用NativeMessageQueue的wake方法。
nativeMessageQueue->wake();
}

NativeMessageQueue的wake方法就是调Native Looper的wake方法:

void Looper::wake() {
uint64_t inc = 1;
// 向管道mWakeEventFd写入字符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));
}
}
}

TEMP_FAILURE_RETRY这个宏表达式的作用是,对传入的表达式求值,当传入的表达式求值为-1,则表示失败,当表达式返回值-1且设置了错误码为EINITR(4),那么他会一直重试,直到成功。

nativePollOnce:

从上文分析中,我们知道,在Java层消息队列处理Message之前,会先调用nativePollOnce,处理Native层的消息:

nativePollOnce(ptr, nextPollTimeoutMillis);

ptr是之前在Native层创建的MessageQueue的“指针”,nextPollTimeoutMillis表示下一条消息要被取出的时间。

在android_os_MessageQueue.cpp中,nativePollOnce实现如下:

static void android_os_MessageQueue_nativePollOnce(JNIEnv* env, jobject obj,
jlong ptr, jint timeoutMillis) {
// 将Java层传递过来的mPtr转换为NativeMessageQueue指针
NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr);
// 调用nativeMessageQueue的pollOnce方法
nativeMessageQueue->pollOnce(env, obj, timeoutMillis);
}

pollOnce:

void NativeMessageQueue::pollOnce(JNIEnv* env, jobject pollObj, int timeoutMillis) {
// ...
// 调用到Looper的pollOnce方法
mLooper->pollOnce(timeoutMillis);
// ...
}

Looper的pollOnce最终实现在system/core/libutils/Looper.cpp:

int Looper::pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData) {
int result = 0;
for (;;) {
// 首先处理Native层的Response
while (mResponseIndex < mResponses.size()) {
const Response& response = mResponses.itemAt(mResponseIndex++)
// 当ident>=0的时候,表示没有callback
int ident = response.request.ident;
if (ident >= 0) {
int fd = response.request.fd;
int events = response.events;
void* data = response.request.data;
if (outFd != nullptr) *outFd = fd;
if (outEvents != nullptr) *outEvents = events;
if (outData != nullptr) *outData = data;
return ident;
}
}
// 如果有result,那么就退出了
if (result != 0) {
if (outFd != nullptr) *outFd = 0;
if (outEvents != nullptr) *outEvents = 0;
if (outData != nullptr) *outData = nullptr;
return result;
}
// 调用pollInner
result = pollInner(timeoutMillis);
}
}

Response和Request的结构体如下:

    struct Request {
// request关联的文件描述符
int fd;
// requestId,当为POLL_CALLBACK(-2)的时候,表示有callback
int ident;
int events;
int seq;
// request的处理回调
sp<LooperCallback> callback;
void* data;
void initEventItem(struct epoll_event* eventItem) const;
};
struct Response {
int events;
Request request;
};

Looper::pollInner的实现如下,内部首先是调用epoll_wait这个阻塞方法,获取Epoll事件发生的数量,然后根据这个数量,

int Looper::pollInner(int timeoutMillis) {
// Adjust the timeout based on when the next message is due.
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.
int result = POLL_WAKE;
// 上面的分析已知,在pollInner被调用之前,mResponses已经都被处理完了
mResponses.clear();
mResponseIndex = 0;
// 即将开始epoll轮询。
mPolling = true;
struct epoll_event eventItems[EPOLL_MAX_EVENTS];
// 等待epoll_wait系统调用返回,返回timeoutMillis时间内文件描述符mEpollFd上发生的epoll事件数量
int eventCount = epoll_wait(mEpollFd.get(), eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
// 轮询结束,下面开始处理收到的事件。
mPolling = false;
// Acquire lock.
mLock.lock();

// 比如发生了什么异常,需要重新创建Epoll机制
if (mEpollRebuildRequired) {
mEpollRebuildRequired = false;
rebuildEpollLocked();
goto Done;
}

// 当epoll事件个数小于0的时候,即认为发生了异常,跳转到Done处继续执行
if (eventCount < 0) {
if (errno == EINTR) {
goto Done;
}
result = POLL_ERROR;
goto Done;
}

// 当epoll事件等于0的时候,表示轮询超时了,直接跳转到Done处继续执行
if (eventCount == 0) {
result = POLL_TIMEOUT;
goto Done;
}
// 开始循环遍历,处理所有的event
for (int i = 0; i < eventCount; i++) {
// 获取一个事件的FD
int fd = eventItems[i].data.fd;
uint32_t epollEvents = eventItems[i].events;
// 如果是唤醒事件
if (fd == mWakeEventFd.get()) {
if (epollEvents & EPOLLIN) {
// 此时已经被唤醒了,读取并清空管道中的数据
awoken();
} else {
ALOGW("Ignoring unexpected epoll events 0x%x on wake event fd.", epollEvents);
}
} else {
// 通过文件描述符,找到对应的Request索引
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;
// 将对应的request封装成Response对象并push到mRequests这和Vector中
pushResponse(events, mRequests.valueAt(requestIndex));
} else {
ALOGW("Ignoring unexpected epoll events 0x%x on fd %d that is "
"no longer registered.", epollEvents, fd);
}
}
}
Done: ;
// Response事件都处理完了,接下来处理Native的Message事件。
mNextMessageUptime = LLONG_MAX;
// mMessageEnvelopes是一个Vector,MessageEnvelopes如其名消息信封
// 封装了Message和MessageHandler对象
// Message表示消息,MessageHandler定义了一个handleMessage方法
// 通过调用Looper::sendMessageXX可以发送一条Native Message
while (mMessageEnvelopes.size() != 0) {
nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);
const MessageEnvelope& messageEnvelope = mMessageEnvelopes.itemAt(0);
if (messageEnvelope.uptime <= now) {
// 取出来的一条消息到了可以被处理的时间,那么就移除并执行
// 对应MessageHandler的handleMessage方法。
{ // obtain handler
sp<MessageHandler> handler = messageEnvelope.handler;
Message message = messageEnvelope.message;
mMessageEnvelopes.removeAt(0);
mSendingMessage = true;
mLock.unlock();
handler->handleMessage(message);
} // release handler
mLock.lock();
mSendingMessage = false;
result = POLL_CALLBACK;
} else {
// 而如果队列头部的消息尚未到达需要被处理的时间
// 那么队列需要挂起到这个头部消息能被处理时候为止
mNextMessageUptime = messageEnvelope.uptime;
break;
}
}
// Release lock.
mLock.unlock();
// 接着处理上面push进来的mResponses,即Request
for (size_t i = 0; i < mResponses.size(); i++) {
Response& response = mResponses.editItemAt(i);
// 有callback
if (response.request.ident == POLL_CALLBACK) {
int fd = response.request.fd;
int events = response.events;
void* data = response.request.data;
// 执行callback
int callbackResult = response.request.callback->handleEvent(fd, events, data)
if (callbackResult == 0) {
removeFd(fd, response.request.seq);
}
// 清除callback的引用
response.request.callback.clear();
result = POLL_CALLBACK;
}
}
return result;
}

整个Native层Looper机制的重中之重就是Looper::pollInner中的epoll_wait这个系统调用,这个调用在消息队列没有工作需要处理的时候,会阻塞当前线程,释放系统资源,也就是说,Looper的死循环机制并不会一直占用系统资源,在没有任务需要处理的时候,主线程是阻塞状态的,因此并不会造成资源占用过高。

或者更通俗易懂的,我们看到的Looper.loop开启了一个死循环,这个死循环的的确确就是一个死循环。但是特别之处在于,不同于我们写一个无限循环,CPU会一直执行,然后导致资源占用激增,Looper.loop这个死循环,在没有消息需要处理的时候,就会阻塞停止,不再往epoll_wait后面执行。

而我们感受不到主线程停止,是因为,我们写的一个代码,执行都是被动的,我们在子线程post一个message,MessageQueue接收消息,主线程的Looper.loop执行代码,执行完代码后取下一条Message,没有Message,主线程继续阻塞。我们写的代码执行了,当然是感受不到主线程阻塞的。

小结:

通过对Native的pollOnce的分析,我们知道Android的消息处理机制实际上纵跨Java层和Native层的。

Java层和Native层,通过上面的一些JNI调用以及mPtr这个关键指针,将Java层的MessageQueue和Native的MessageQueue进行关联,这样在Java层的消息机制进行运转的时候,Native层的消息机制也能一起运转。

消息处理的流程是先处理Native的Message,然后再处理Native的Request,最后才会是pollOnce结束之后,处理Java层的Message,所以有时候Java层的消息并不多但是响应时间比较慢可能是Native层的消息机制导致的。

收起阅读 »

解析android源码中dex文件的几个关键函数

dex简介dex文件作为android的的主要格式,它是可以直接在Dalvik虚拟机中加载运行的文件。 dex 文件可以分为3个模块,头文件(header)、索引区(xxxx_ids)、数据区(data)。 我们在进行对android加固和脱壳的时候都需要进行...
继续阅读 »

dex简介

dex文件作为android的的主要格式,它是可以直接在Dalvik虚拟机中加载运行的文件。 dex 文件可以分为3个模块,头文件(header)、索引区(xxxx_ids)、数据区(data)。 我们在进行对android加固和脱壳的时候都需要进行和dex文件格式打交道。 它在系统的定义是定义在/art/runtime/dex_file文件中的。下面对dex文件格式的几个关键函数进行分析。

CheckMagicAndVersion 函数

//判断dex文件中魔法值dex后面所跟的版本
bool DexFile::CheckMagicAndVersion(std::string* error_msg) const {
//dex文件魔法值是存在在头文件header中的,所有通过header就可以获取魔法值
if (!IsMagicValid(header_->magic_)) {
std::ostringstream oss;
oss << "Unrecognized magic number in " << GetLocation() << ":"
<< " " << header_->magic_[0]
<< " " << header_->magic_[1]
<< " " << header_->magic_[2]
<< " " << header_->magic_[3];
*error_msg = oss.str();
return false;
}
//判断获取的魔法值是否有效
if (!IsVersionValid(header_->magic_)) {
std::ostringstream oss;
oss << "Unrecognized version number in " << GetLocation() << ":"
<< " " << header_->magic_[4]
<< " " << header_->magic_[5]
<< " " << header_->magic_[6]
<< " " << header_->magic_[7];
*error_msg = oss.str();
return false;
}
return true;
}

DexFile函数

//获取解析dex文件格式
DexFile::DexFile(const byte* base, size_t size,
const std::string& location,
uint32_t location_checksum,
MemMap* mem_map)

: begin_(base),
size_(size),
location_(location),
location_checksum_(location_checksum),
mem_map_(mem_map),
header_(reinterpret_cast<const Header*>(base)),
string_ids_(reinterpret_cast<const StringId*>(base + header_->string_ids_off_)),
type_ids_(reinterpret_cast<const TypeId*>(base + header_->type_ids_off_)),
field_ids_(reinterpret_cast<const FieldId*>(base + header_->field_ids_off_)),
method_ids_(reinterpret_cast<const MethodId*>(base + header_->method_ids_off_)),
proto_ids_(reinterpret_cast<const ProtoId*>(base + header_->proto_ids_off_)),
class_defs_(reinterpret_cast<const ClassDef*>(base + header_->class_defs_off_)),
find_class_def_misses_(0),
class_def_index_(nullptr),
build_class_def_index_mutex_("DexFile index creation mutex") {
CHECK(begin_ != NULL) << GetLocation();
CHECK_GT(size_, 0U) << GetLocation();
}

FindClassDef 函数

//查找dex文件中的class_ids数据
const DexFile::ClassDef* DexFile::FindClassDef(uint16_t type_idx) const {
//获取class的数量
size_t num_class_defs = NumClassDefs();
for (size_t i = 0; i < num_class_defs; ++i) {
const ClassDef& class_def = GetClassDef(i);
if (class_def.class_idx_ == type_idx) {
return &class_def;
}
}
return NULL;
}

FindFieldId 函数

//查找dex文件中的fileId
const DexFile::FieldId* DexFile::FindFieldId(const DexFile::TypeId& declaring_klass,
const DexFile::StringId& name,
const DexFile::TypeId& type) const {
// Binary search MethodIds knowing that they are sorted by class_idx, name_idx then proto_idx
const uint16_t class_idx = GetIndexForTypeId(declaring_klass);
const uint32_t name_idx = GetIndexForStringId(name);
const uint16_t type_idx = GetIndexForTypeId(type);
int32_t lo = 0;
int32_t hi = NumFieldIds() - 1;
while (hi >= lo) {
int32_t mid = (hi + lo) / 2;
//获取FileId数据
const DexFile::FieldId& field = GetFieldId(mid);
if (class_idx > field.class_idx_) {
lo = mid + 1;
} else if (class_idx < field.class_idx_) {
hi = mid - 1;
} else {
if (name_idx > field.name_idx_) {
lo = mid + 1;
} else if (name_idx < field.name_idx_) {
hi = mid - 1;
} else {
if (type_idx > field.type_idx_) {
lo = mid + 1;
} else if (type_idx < field.type_idx_) {
hi = mid - 1;
} else {
//成功返回获取到的fileId数据
return &field;
}
}
}
}
return NULL;
}

FindMethodId 函数

该函数多所对应的是dex文件中的Method table数据

//查找dex文件中的FindMethodId数据
const DexFile::MethodId* DexFile::FindMethodId(const DexFile::TypeId& declaring_klass,
const DexFile::StringId& name,
const DexFile::ProtoId& signature) const {
// Binary search MethodIds knowing that they are sorted by class_idx, name_idx then proto_idx
const uint16_t class_idx = GetIndexForTypeId(declaring_klass);
const uint32_t name_idx = GetIndexForStringId(name);
const uint16_t proto_idx = GetIndexForProtoId(signature);
int32_t lo = 0;
//获取dex文件中的所有MethodIds的数量
int32_t hi = NumMethodIds() - 1;
//当数量大于0
while (hi >= lo) {
//折半的方式进去一个个获取
int32_t mid = (hi + lo) / 2;
const DexFile::MethodId& method = GetMethodId(mid);
if (class_idx > method.class_idx_) {
lo = mid + 1;
} else if (class_idx < method.class_idx_) {
hi = mid - 1;
} else {
if (name_idx > method.name_idx_) {
lo = mid + 1;
} else if (name_idx < method.name_idx_) {
hi = mid - 1;
} else {
if (proto_idx > method.proto_idx_) {
lo = mid + 1;
} else if (proto_idx < method.proto_idx_) {
hi = mid - 1;
} else {
return &method;
}
}
}
}
return NULL;
}

FindStringId 函数

该函数对应的是在dex文件中的String table上

//查找dex文件中的StringId数据
const DexFile::StringId* DexFile::FindStringId(const char* string) const {
int32_t lo = 0;
//获取dex文件中的所有StringId是数量
int32_t hi = NumStringIds() - 1;
while (hi >= lo) {
int32_t mid = (hi + lo) / 2;
const DexFile::StringId& str_id = GetStringId(mid);
const char* str = GetStringData(str_id);
int compare = CompareModifiedUtf8ToModifiedUtf8AsUtf16CodePointValues(string, str);
if (compare > 0) {
lo = mid + 1;
} else if (compare < 0) {
hi = mid - 1;
} else {
return &str_id;
}
}
return NULL;
}

收起阅读 »

iOS 视觉效果 一

视觉效果嗯,圆和椭圆还不错,但如果是带圆角的矩形呢?我们现在能做到那样了么?史蒂芬·乔布斯    我们在第三章『图层几何学』中讨论了图层的frame,第二章『寄宿图』则讨论了图层的寄宿图。但是图层不仅仅可以是图片或是颜色...
继续阅读 »

视觉效果

嗯,圆和椭圆还不错,但如果是带圆角的矩形呢?

我们现在能做到那样了么?

史蒂芬·乔布斯

    我们在第三章『图层几何学』中讨论了图层的frame,第二章『寄宿图』则讨论了图层的寄宿图。但是图层不仅仅可以是图片或是颜色的容器;还有一系列内建的特性使得创造美丽优雅的令人深刻的界面元素成为可能。在这一章,我们将会探索一些能够通过使用CALayer属性实现的视觉效果。

4.1 圆角

    圆角矩形是iOS的一个标志性审美特性。这在iOS的每一个地方都得到了体现,不论是主屏幕图标,还是警告弹框,甚至是文本框。按照这流行程度,你可能会认为一定有不借助Photoshop就能轻易创建圆角举行的方法。恭喜你,猜对了。

    CALayer有一个叫做conrnerRadius的属性控制着图层角的曲率。它是一个浮点数,默认为0(为0的时候就是直角),但是你可以把它设置成任意值。默认情况下,这个曲率值只影响背景颜色而不影响背景图片或是子图层。不过,如果把masksToBounds设置成YES的话,图层里面的所有东西都会被截取。

    我们可以通过一个简单的项目来演示这个效果。在Interface Builder中,我们放置一些视图,他们有一些子视图。而且这些子视图有一些超出了边界(如图4.1)。你可能无法看到他们超出了边界,因为在编辑界面的时候,超出的部分总是被Interface Builder裁切掉了。不过,你相信我就好了 :)

图4.1

图4.1 两个白色的大视图,他们都包含了小一些的红色视图。

    然后在代码中,我们设置角的半径为20个点,并裁剪掉第一个视图的超出部分(见清单4.1)。技术上来说,这些属性都可以在Interface Builder的探测板中分别通过『用户定义运行时属性』和勾选『裁剪子视图』(Clip Subviews)选择框来直接设置属性的值。不过,在这个示例中,代码能够表示得更清楚。图4.2是运行代码的结果

清单4.1 设置cornerRadiusmasksToBounds

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *layerView1;
@property (nonatomic, weak) IBOutlet UIView *layerView2;

@end

@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];

//set the corner radius on our layers
self.layerView1.layer.cornerRadius = 20.0f;
self.layerView2.layer.cornerRadius = 20.0f;

//enable clipping on the second layer
self.layerView2.layer.masksToBounds = YES;
}
@end

图4.2

    右图中,红色的子视图沿角半径被裁剪了

    如你所见,右边的子视图沿边界被裁剪了。

    单独控制每个层的圆角曲率也不是不可能的。如果想创建有些圆角有些直角的图层或视图时,你可能需要一些不同的方法。比如使用一个图层蒙板(本章稍后会讲到)或者是CAShapeLayer(见第六章『专用图层』)。


4.2 图层边框

  &nbp; CALayer另外两个非常有用属性就是borderWidthborderColor。二者共同定义了图层边的绘制样式。这条线(也被称作stroke)沿着图层的bounds绘制,同时也包含图层的角。

  &nbp; borderWidth是以点为单位的定义边框粗细的浮点数,默认为0.borderColor定义了边框的颜色,默认为黑色。

  &nbp; borderColor是CGColorRef类型,而不是UIColor,所以它不是Cocoa的内置对象。不过呢,你肯定也清楚图层引用了borderColor,虽然属性声明并不能证明这一点。CGColorRef在引用/释放时候的行为表现得与NSObject极其相似。但是Objective-C语法并不支持这一做法,所以CGColorRef属性即便是强引用也只能通过assign关键字来声明。

  &nbp; 边框是绘制在图层边界里面的,而且在所有子内容之前,也在子图层之前。如果我们在之前的示例中(清单4.2)加入图层的边框,你就能看到到底是怎么一回事了(如图4.3).

清单4.2 加上边框

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad];

//set the corner radius on our layers
self.layerView1.layer.cornerRadius = 20.0f;
self.layerView2.layer.cornerRadius = 20.0f;

//add a border to our layers
self.layerView1.layer.borderWidth = 5.0f;
self.layerView2.layer.borderWidth = 5.0f;

//enable clipping on the second layer
self.layerView2.layer.masksToBounds = YES;
}

@end

图4.3

图4.3 给图层增加一个边框

  &nbp; 仔细观察会发现边框并不会把寄宿图或子图层的形状计算进来,如果图层的子图层超过了边界,或者是寄宿图在透明区域有一个透明蒙板,边框仍然会沿着图层的边界绘制出来(如图4.4).

图4.4

图4.4 边框是跟随图层的边界变化的,而不是图层里面的内容

收起阅读 »

iOS 图形几何学 三

3.5 自动布局    你可能用过UIViewAutoresizingMask类型的一些常量,应用于当父视图改变尺寸的时候,相应UIView的frame也跟着更新的场景(通常用于横竖屏切换)。  &n...
继续阅读 »

3.5 自动布局

    你可能用过UIViewAutoresizingMask类型的一些常量,应用于当父视图改变尺寸的时候,相应UIViewframe也跟着更新的场景(通常用于横竖屏切换)。

    在iOS6中,苹果介绍了自动排版机制,它和自动调整不同,并且更加复杂。

    在Mac OS平台,CALayer有一个叫做layoutManager的属性可以通过CALayoutManager协议和CAConstraintLayoutManager类来实现自动排版的机制。但由于某些原因,这在iOS上并不适用。

    当使用视图的时候,可以充分利用UIView类接口暴露出来的UIViewAutoresizingMaskNSLayoutConstraintAPI,但如果想随意控制CALayer的布局,就需要手工操作。最简单的方法就是使用CALayerDelegate如下函数:

- (void)layoutSublayersOfLayer:(CALayer *)layer;

    当图层的bounds发生改变,或者图层的-setNeedsLayout方法被调用的时候,这个函数将会被执行。这使得你可以手动地重新摆放或者重新调整子图层的大小,但是不能像UIViewautoresizingMaskconstraints属性做到自适应屏幕旋转。

    这也是为什么最好使用视图而不是单独的图层来构建应用程序的另一个重要原因之一。


总结

    本章涉及了CALayer的集合结构,包括它的framepositionbounds,介绍了三维空间内图层的概念,以及如何在独立的图层内响应事件,最后简单说明了在iOS平台,Core Animation对自动调整和自动布局支持的缺乏。

    在第四章“视觉效果”当中,我们接着介绍一些图层外表的特性。

收起阅读 »

iOS 图形几何学 二

3.3 坐标系和视图一样,图层在图层树当中也是相对于父图层按层级关系放置,一个图层的position依赖于它父图层的bounds,如果父图层发生了移动,它的所有子图层也会跟着移动。这样对于放置图层会更加方便,因为你可以通过移动根图层来将它的子图层作为一个整体来...
继续阅读 »

3.3 坐标系

和视图一样,图层在图层树当中也是相对于父图层按层级关系放置,一个图层的position依赖于它父图层的bounds,如果父图层发生了移动,它的所有子图层也会跟着移动。

这样对于放置图层会更加方便,因为你可以通过移动根图层来将它的子图层作为一个整体来移动,但是有时候你需要知道一个图层的绝对位置,或者是相对于另一个图层的位置,而不是它当前父图层的位置。

CALayer给不同坐标系之间的图层转换提供了一些工具类方法:

- (CGPoint)convertPoint:(CGPoint)point fromLayer:(CALayer *)layer;
- (CGPoint)convertPoint:(CGPoint)point toLayer:(CALayer *)layer;
- (CGRect)convertRect:(CGRect)rect fromLayer:(CALayer *)layer;
- (CGRect)convertRect:(CGRect)rect toLayer:(CALayer *)layer;

这些方法可以把定义在一个图层坐标系下的点或者矩形转换成另一个图层坐标系下的点或者矩形.

翻转的几何结构

    常规说来,在iOS上,一个图层的position位于父图层的左上角,但是在Mac OS上,通常是位于左下角。Core Animation可以通过geometryFlipped属性来适配这两种情况,它决定了一个图层的坐标是否相对于父图层垂直翻转,是一个BOOL类型。在iOS上通过设置它为YES意味着它的子图层将会被垂直翻转,也就是将会沿着底部排版而不是通常的顶部(它的所有子图层也同理,除非把它们的geometryFlipped属性也设为YES)。

Z坐标轴

    和UIView严格的二维坐标系不同,CALayer存在于一个三维空间当中。除了我们已经讨论过的positionanchorPoint属性之外,CALayer还有另外两个属性,zPositionanchorPointZ,二者都是在Z轴上描述图层位置的浮点类型。

    注意这里并没有更的属性来描述由宽和高做成的bounds了,图层是一个完全扁平的对象,你可以把它们想象成类似于一页二维的坚硬的纸片,用胶水粘成一个空洞,就像三维结构的折纸一样。

    zPosition属性在大多数情况下其实并不常用。在第五章,我们将会涉及CATransform3D,你会知道如何在三维空间移动和旋转图层,除了做变换之外,zPosition最实用的功能就是改变图层的显示顺序了。

    通常,图层是根据它们子图层的sublayers出现的顺序来类绘制的,这就是所谓的画家的算法--就像一个画家在墙上作画--后被绘制上的图层将会遮盖住之前的图层,但是通过增加图层的zPosition,就可以把图层向相机方向前置,于是它就在所有其他图层的前面了(或者至少是小于它的zPosition值的图层的前面)。

    这里所谓的“相机”实际上是相对于用户是视角,这里和iPhone背后的内置相机没任何关系。

图3.8显示了在Interface Builder内的一对视图,正如你所见,首先出现在视图层级绿色的视图被绘制在红色视图的后面。

图3.8

图3.8 在视图层级中绿色视图被绘制在红色视图的后面

    我们希望在真实的应用中也能显示出绘图的顺序,同样地,如果我们提高绿色视图的zPosition(清单3.3),我们会发现顺序就反了(图3.9)。其实并不需要增加太多,视图都非常地薄,所以给zPosition提高一个像素就可以让绿色视图前置,当然0.1或者0.0001也能够做到,但是最好不要这样,因为浮点类型四舍五入的计算可能会造成一些不便的麻烦。

清单3.3

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *greenView;
@property (nonatomic, weak) IBOutlet UIView *redView;

@end

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad];

//move the green view zPosition nearer to the camera
self.greenView.layer.zPosition = 1.0f;
}
@end

图3.9

图3.9 绿色视图被绘制在红色视图的前面


3.4 Hit Testing

第一章“图层树”证实了最好使用图层相关视图,而不是创建独立的图层关系。其中一个原因就是要处理额外复杂的触摸事件。

CALayer并不关心任何响应链事件,所以不能直接处理触摸事件或者手势。但是它有一系列的方法帮你处理事件:-containsPoint:-hitTest:

-containsPoint:接受一个在本图层坐标系下的CGPoint,如果这个点在图层frame范围内就返回YES。如清单3.4所示第一章的项目的另一个合适的版本,也就是使用-containsPoint:方法来判断到底是白色还是蓝色的图层被触摸了 (图3.10)。这需要把触摸坐标转换成每个图层坐标系下的坐标,结果很不方便。

清单3.4 使用containsPoint判断被点击的图层

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *layerView;
@property (nonatomic, weak) CALayer *blueLayer;

@end

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad];
//create sublayer
self.blueLayer = [CALayer layer];
self.blueLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f);
self.blueLayer.backgroundColor = [UIColor blueColor].CGColor;
//add it to our view
[self.layerView.layer addSublayer:self.blueLayer];
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
//get touch position relative to main view
CGPoint point = [[touches anyObject] locationInView:self.view];
//convert point to the white layer's coordinates
point = [self.layerView.layer convertPoint:point fromLayer:self.view.layer];
//get layer using containsPoint:
if ([self.layerView.layer containsPoint:point]) {
//convert point to blueLayer’s coordinates
point = [self.blueLayer convertPoint:point fromLayer:self.layerView.layer];
if ([self.blueLayer containsPoint:point]) {
[[[UIAlertView alloc] initWithTitle:@"Inside Blue Layer"
message:nil
delegate:nil
cancelButtonTitle:@"OK"
otherButtonTitles:nil] show];
} else {
[[[UIAlertView alloc] initWithTitle:@"Inside White Layer"
message:nil
delegate:nil
cancelButtonTitle:@"OK"
otherButtonTitles:nil] show];
}
}
}

@end

图3.10

图3.10 点击图层被正确标识

-hitTest:方法同样接受一个CGPoint类型参数,而不是BOOL类型,它返回图层本身,或者包含这个坐标点的叶子节点图层。这意味着不再需要像使用-containsPoint:那样,人工地在每个子图层变换或者测试点击的坐标。如果这个点在最外面图层的范围之外,则返回nil。具体使用-hitTest:方法被点击图层的代码如清单3.5所示。

清单3.5 使用hitTest判断被点击的图层

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
//get touch position
CGPoint point = [[touches anyObject] locationInView:self.view];
//get touched layer
CALayer *layer = [self.layerView.layer hitTest:point];
//get layer using hitTest
if (layer == self.blueLayer) {
[[[UIAlertView alloc] initWithTitle:@"Inside Blue Layer"
message:nil
delegate:nil
cancelButtonTitle:@"OK"
otherButtonTitles:nil] show];
} else if (layer == self.layerView.layer) {
[[[UIAlertView alloc] initWithTitle:@"Inside White Layer"
message:nil
delegate:nil
cancelButtonTitle:@"OK"
otherButtonTitles:nil] show];
}
}

注意当调用图层的-hitTest:方法时,测算的顺序严格依赖于图层树当中的图层顺序(和UIView处理事件类似)。之前提到的zPosition属性可以明显改变屏幕上图层的顺序,但不能改变事件传递的顺序。

这意味着如果改变了图层的z轴顺序,你会发现将不能够检测到最前方的视图点击事件,这是因为被另一个图层遮盖住了,虽然它的zPosition值较小,但是在图层树中的顺序靠前。我们将在第五章详细讨论这个问题。

收起阅读 »

iOS 图形几何学 一

3.1 布局    UIView有三个比较重要的布局属性:frame,bounds和center,CALayer对应地叫做frame,bounds和position。为了能清楚区分,图层用了“position”,视图用了...
继续阅读 »

3.1 布局

    UIView有三个比较重要的布局属性:frameboundscenterCALayer对应地叫做frameboundsposition。为了能清楚区分,图层用了“position”,视图用了“center”,但是他们都代表同样的值。

    frame代表了图层的外部坐标(也就是在父图层上占据的空间),bounds是内部坐标({0, 0}通常是图层的左上角),centerposition都代表了相对于父图层anchorPoint所在的位置。anchorPoint的属性将会在后续介绍到,现在把它想成图层的中心点就好了。图3.1显示了这些属性是如何相互依赖的。

图3.1

图3.1 UIViewCALayer的坐标系

    视图的frameboundscenter属性仅仅是存取方法,当操纵视图的frame,实际上是在改变位于视图下方CALayerframe,不能够独立于图层之外改变视图的frame

    对于视图或者图层来说,frame并不是一个非常清晰的属性,它其实是一个虚拟属性,是根据boundspositiontransform计算而来,所以当其中任何一个值发生改变,frame都会变化。相反,改变frame的值同样会影响到他们当中的值

    记住当对图层做变换的时候,比如旋转或者缩放,frame实际上代表了覆盖在图层旋转之后的整个轴对齐的矩形区域,也就是说frame的宽高可能和bounds的宽高不再一致了(图3.2)

图3.2

图3.2 旋转一个视图或者图层之后的frame属性


3.2锚点

    之前提到过,视图的center属性和图层的position属性都指定了anchorPoint相对于父图层的位置。图层的anchorPoint通过position来控制它的frame的位置,你可以认为anchorPoint是用来移动图层的把柄

    默认来说,anchorPoint位于图层的中点,所以图层的将会以这个点为中心放置。anchorPoint属性并没有被UIView接口暴露出来,这也是视图的position属性被叫做“center”的原因。但是图层的anchorPoint可以被移动,比如你可以把它置于图层frame的左上角,于是图层的内容将会向右下角的position方向移动(图3.3),而不是居中了。

图3.3

图3.3 改变anchorPoint的效果

    和第二章提到的contentsRectcontentsCenter属性类似,anchorPoint单位坐标来描述,也就是图层的相对坐标,图层左上角是{0, 0},右下角是{1, 1},因此默认坐标是{0.5, 0.5}。anchorPoint可以通过指定x和y值小于0或者大于1,使它放置在图层范围之外。

    注意在图3.3中,当改变了anchorPointposition属性保持固定的值并没有发生改变,但是frame却移动了。

    那在什么场合需要改变anchorPoint呢?既然我们可以随意改变图层位置,那改变anchorPoint不会造成困惑么?为了举例说明,我们来举一个实用的例子,创建一个模拟闹钟的项目。

    钟面和钟表由四张图片组成(图3.4),为了简单说明,我们还是用传统的方式来装载和加载图片,使用四个UIImageView实例(当然你也可以用正常的视图,设置他们图层的contents图片)。

图3.4

图3.4 组成钟面和钟表的四张图片

    闹钟的组件通过IB来排列(图3.5),这些图片视图嵌套在一个容器视图之内,并且自动调整和自动布局都被禁用了。这是因为自动调整会影响到视图的frame,而根据图3.2的演示,当视图旋转的时候,frame是会发生改变的,这将会导致一些布局上的失灵。

    我们用NSTimer来更新闹钟,使用视图的transform属性来旋转钟表(如果你对这个属性不太熟悉,不要着急,我们将会在第5章“变换”当中详细说明),具体代码见清单3.1

图3.5

图3.5 在Interface Builder中布局闹钟视图

清单3.1 Clock

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIImageView *hourHand;
@property (nonatomic, weak) IBOutlet UIImageView *minuteHand;
@property (nonatomic, weak) IBOutlet UIImageView *secondHand;
@property (nonatomic, weak) NSTimer *timer;

@end

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad];
//start timer
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(tick) userInfo:nil repeats:YES];

//set initial hand positions
[self tick];
}

- (void)tick
{
//convert time to hours, minutes and seconds
NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
NSUInteger units = NSHourCalendarUnit | NSMinuteCalendarUnit | NSSecondCalendarUnit;
NSDateComponents *components = [calendar components:units fromDate:[NSDate date]];
CGFloat hoursAngle = (components.hour / 12.0) * M_PI * 2.0;
//calculate hour hand angle //calculate minute hand angle
CGFloat minsAngle = (components.minute / 60.0) * M_PI * 2.0;
//calculate second hand angle
CGFloat secsAngle = (components.second / 60.0) * M_PI * 2.0;
//rotate hands
self.hourHand.transform = CGAffineTransformMakeRotation(hoursAngle);
self.minuteHand.transform = CGAffineTransformMakeRotation(minsAngle);
self.secondHand.transform = CGAffineTransformMakeRotation(secsAngle);
}

@end

    运行项目,看起来有点奇怪(图3.6),因为钟表的图片在围绕着中心旋转,这并不是我们期待的一个支点。

图3.6

图3.6 钟面,和不对齐的钟指针

    你也许会认为可以在Interface Builder当中调整指针图片的位置来解决,但其实并不能达到目的,因为如果不放在钟面中间的话,同样不能正确的旋转。

    也许在图片末尾添加一个透明空间也是个解决方案,但这样会让图片变大,也会消耗更多的内存,这样并不优雅。

    更好的方案是使用anchorPoint属性,我们来在-viewDidLoad方法中添加几行代码来给每个钟指针的anchorPoint做一些平移(清单3.2),图3.7显示了正确的结果。

清单3.2

- (void)viewDidLoad 
{
[super viewDidLoad];
// adjust anchor points

self.secondHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f);
self.minuteHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f);
self.hourHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f);


// start timer
}

图3.7

图3.7 钟面,和正确对齐的钟指针

收起阅读 »

Android Java 虚拟机

1. 概述 1.1 Java 虚拟机家族1.1.1 HotSpot VMOracle JDK 和 OpenJDK 中自带的虚拟机,最主流和使用范围最广的 Java 虚拟机。1.1.2 J9 VM1.1.3 Zing VM1.2 Java 虚拟机执行流...
继续阅读 »

1. 概述

image.png image.png

1.1 Java 虚拟机家族

1.1.1 HotSpot VM

Oracle JDK 和 OpenJDK 中自带的虚拟机,最主流和使用范围最广的 Java 虚拟机。

1.1.2 J9 VM

1.1.3 Zing VM

1.2 Java 虚拟机执行流程

当执行一个 Java 程序时,它的执行流程图如下:

  • 编译时环境
  • 运行时环境

image.png

2. Java 虚拟机结构

image.png

2.1 Class 文件格式

image.png

Class 文件格式: image.png

2.2 类的生命周期

image.png

image.png

2.3 类加载系统

  • Bootstrap ClassLoader(引导类加载器)
  • Extensions ClassLoader(拓展类加载器)
  • Application ClassLoader(应用程序类加载器)/ System ClassLoader(系统类加载器)

image.png

image.png

2.4 运行时数据区域

这些数据区域分别为程序计数器、Java 虚拟机栈、本地方法栈、Java 堆和方法区。

2.4.1 程序计数器

image.png

image.png

2.4.2 Java 虚拟机栈

image.png

2.4.3 本地方法栈

image.png

2.4.4 Java 堆

image.png

image.png

2.4.5 方法区

image.png

2.4.6 运行时常量池

image.png

3. 对象的创建

当虚拟机接收到一个 new 指令时,它会做如下的操作:

image.png

image.png

4. 对象的堆内存布局

image.png

image.png

5. oop-klass 模型

image.png

image.png

image.png

image.png

6. 垃圾标记算法

image.png

6.1 Java 中的引用

  • 强引用
  • 软引用
  • 弱引用
  • 虚引用

image.png

image.png

6.2 引用计数法

image.png

image.png

image.png

缺点:引用计数算法没有解决对象之间相互循环引用的问题。

6.3 根搜索算法

image.png

image.png

优点:解决了已经死亡的对象因为相互引用而不能被回收。

7. Java 对象在虚拟机中的生命周期

image.png

image.png

8. 垃圾收集算法

8.1 标记-清除算法

image.png

image.png

8.2 复制算法

image.png

image.png

8.3 标记-压缩算法

image.png

image.png

8.4 分代收集算法

image.png

8.4.1 分代收集

image.png

image.png

收起阅读 »

Android 多返回栈技术详解

用户通过系统返回按钮导航回去的一组页面,在开发中被称为返回栈 (back stack)。多返回栈即一堆 "返回栈",对多返回栈的支持是在 Navigation 2.4.0-alpha01 和 Fragment 1.4.0-alpha01 中开始的。本文将为您展...
继续阅读 »

用户通过系统返回按钮导航回去的一组页面,在开发中被称为返回栈 (back stack)。多返回栈即一堆 "返回栈",对多返回栈的支持是在 Navigation 2.4.0-alpha01  Fragment 1.4.0-alpha01 中开始的。本文将为您展开多返回栈的技术详解。

系统返回按钮的乐趣

无论您在使用 Android 全新的 手势导航 还是传统的导航栏,用户的 "返回" 操作是 Android 用户体验中关键的一环,把握好返回功能的设计可以使应用更加贴近整个生态系统。

在最简单的应用场景中,系统返回按钮仅仅 finish 您的 Activity。在过去您可能需要覆写 Activity 的 onBackPressed() 方法来自定义返回操作,而在 2021 年您无需再这样操作。我们已经在 OnBackPressedDispatcher 中提供了 针对自定义返回导航的 API。实际上这与 FragmentManager 和 NavController 中 已经 添加的 API 相同。

这意味着当您使用 Fragments 或 Navigation 时,它们会通过 OnBackPressedDispatcher 来确保您调用了它们返回栈的 API,系统的返回按钮会将您推入返回栈的页面逐层返回。

多返回栈不会改变这个基本逻辑。系统的返回按钮仍然是一个单向指令 —— "返回"。这对多返回栈 API 的实现机制有深远影响。

Fragment 中的多返回栈

在 surface 层级,对于 多返回栈的支持 貌似很直接,但其实需要额外解释一下 "Fragment 返回栈" 到底是什么。FragmentManager 的返回栈其实包含的不是 Fragment,而是由 Fragment 事务组成的。更准确地说,是由那些调用了 addToBackStack(String name) API 的事务组成的。

这就意味着当您调用 commit() 提交了一个调用过 addToBackStack() 方法的 Fragment 事务时,FragmentManager 会执行所有您在事务中所指定的操作 (比如 替换操作),从而将每个 Fragment 转换为预期的状态。然后 FragmentManager 会将该事务作为它返回栈的一部分。

当您调用 popBackStack() 方法时 (无论是直接调用,还是通过系统返回键以 FragmentManager 内部机制调用),Fragment 返回栈的最上层事务会从栈中弹出 -- 比如新添加的 Fragment 会被移除,隐藏的 Fragment 会显示。这会使得 FragmentManager 恢复到最初提交 Fragment 事务之前的状态。

作者注: 这里有一个非常重要的事情需要大家注意,在同一个 FragmentManager 中绝对不应该将含有 addToBackStack() 的事务和不含的事务混在一起: 返回栈的事务无法察觉返回栈之外的 Fragment 事务的修改 —— 当您从堆栈弹出一个非常不确定的元素时,这些事务从下层替换出来的时候会撤销之前未添加到返回栈的修改。

也就是说 popBackStack() 变成了销毁操作: 任何已添加的 Fragment 在事务被弹出的时候都会丢失它的状态。换言之,您会失去视图的状态,任何所保存的实例状态 (Saved Instance State),并且任何绑定到该 Fragment 的 ViewModel 实例都会被清除。这也是该 API 和新的 saveBackStack() 方法之间的主要区别。saveBackStack() 可以实现弹出事务所实现的返回效果,此外它还可以确保视图状态、已保存的实例状态,以及 ViewModel 实例能够在销毁时被保存。这使得 restoreBackStack() API 后续可以通过已保存的状态重建这些事务和它们的 Fragment,并且高效 "重现" 已保存的全部细节。太神奇了!

而实现这个目的必须要解决大量技术上的问题。

排除 Fragment 在技术上的障碍

虽然 Fragment 总是会保存 Fragment 的视图状态,但是 Fragment 的 onSaveInstanceState() 方法只有在 Activity 的 onSaveInstanceState() 被调用时才会被调用。为了能够保证调用 saveBackStack() 时 SavedInstanceState 会被保存,我们  需要在 Fragment 生命周期切换 的正确时机注入对 onSaveInstanceState() 的调用。我们不能调用得太早 (您的 Fragment 不应该在 STARTED 状态下保存状态),也不能调用得太晚 (您需要在 Fragment 被销毁之前保存状态)。

这样的前提条件就开启了需要 解决 FragmentManager 转换到对应状态的问题,以此来保障有一个地方能够将 Fragment 转换为所需状态,并且处理可重入行为和 Fragment 内部的状态转换。

在 Fragment 的重构工作进行了 6 个月,进行了 35 次修改时,发现 Postponed Fragment 功能已经严重损坏,这一问题使得被推迟的事务处于一个中间状态 —— 既没有被提交也并不是未被提交。之后的 65 个修改和 5 个月的时间里,我们几乎重写了 FragmentManager 管理状态、延迟状态切换和动画的内部代码,具体请参见我们之前的文章《全新的 Fragment: 使用新的状态管理器》。

Fragment 中值得期待的地方

随着技术问题的逐步解决,包括更加可靠和更易理解的 FragmentManager,我们新增加了两个 API: saveBackStack() 和 restoreBackStack()

如果您不使用这些新增 API,则一切照旧: 单个 FragmentManager 返回栈和之前的功能相同。现有的 addToBackStack() 保持不变 —— 您可以将 name 赋值为 null 或者任意 name。然而,当您使用多返回栈时,name 的作用就非常重要了: 在您调用 saveBackStack() 和之后的 restoreBackStack() 方法时,它将作为 Fragment 事务的唯一的 key。

举个例子,会更容易理解。比如您已经添加了一个初始的 Fragment 到 Activity,然后提交了两个事务,每个事务中包含一个单独的 replace 操作:

// 这是用户看到的初始的 Fragment
fragmentManager.commit {
setReorderingAllowed(true)
replace(R.id.fragment_container)
}
// 然后,响应用户操作,我们在返回栈中增加了两个事务
fragmentManager.commit {
setReorderingAllowed(true)
replace(R.id.fragment_container)
addToBackStack(“profile”)
}
fragmentManager.commit {
setReorderingAllowed(true)
replace(R.id.fragment_container)
addToBackStack(“edit_profile”)
}

也就是说我们的 FragmentManager 会变成这样:

△ 提交三次之后的 FragmentManager 的状态

△ 提交三次之后的 FragmentManager 的状态

比如说我们希望将 profile 页换出返回栈,然后切换到通知 Fragment。这就需要调用 saveBackStack() 并且紧跟一个新的事务:

fragmentManager.saveBackStack("profile")
fragmentManager.commit {
setReorderingAllowed(true)
replace(R.id.fragment_container)
addToBackStack("notifications")
}

现在我们添加 ProfileFragment 的事务和添加 EditProfileFragment 的事务都保存在 "profile" 关键字下。这些 Fragment 已经完全将状态保存,并且 FragmentManager 会随同事务状态一起保持它们的状态。很重要的一点: 这些 Fragment 的实例并不在内存中或者在 FragmentManager 中 —— 存在的仅仅只有状态 (以及任何以 ViewModel 实例形式存在的非配置状态)。

△ 我们保存 profile 返回栈并且添加一个新的 commit 后的 FragmentManager 状态

△ 我们保存 profile 返回栈并且添加一个新的 commit 后的 FragmentManager 状态

替换回来非常简单: 我们可以在 "notifications" 事务中同样调用 saveBackStack() 操作,然后调用 restoreBackStack():

fragmentManager.saveBackStack(“notifications”)
fragmentManager.restoreBackStack(“profile”)

这两个堆栈项高效地交换了位置:

△ 交换堆栈项后的 FragmentManager 状

△ 交换堆栈项后的 FragmentManager 状态

维持一个单独且活跃的返回栈并且将事务在其中交换,这保证了当返回按钮被点击时,FragmentManager 和系统的其他部分可以保持一致的响应。实际上,整个逻辑并未改变,同之前一样,仍然弹出 Fragment 返回栈的最后一个事务。

这些 API 都特意按照最小化设计,尽管它们会产生潜在的影响。这使得开发者可以基于这些接口设计自己的结构,而无需通过任何非常规的方式保存 Fragment 的视图状态、已保存的实例状态、非配置的状态。

当然了,如果您不希望在这些 API 之上构建您的框架,那么可以使用我们所提供的框架进行开发。

使用 Navigation 将多返回栈适配到任意屏幕类型

Navigation Component 最初 是作为通用运行时组件进行开发的,其中不涉及 View、Fragment、Composable 或者其他屏幕显示相关类型及您可能会在 Activity 中实现的 "目的地界面"。然而,NavHost 接口 的实现中需要考虑这些内容,通过它添加一个或者多个 Navigator 实例时,这些实例 确实 清楚如何与特定类型的目的地进行交互。

这也就意味着与 Fragment 的交互逻辑全部封装在了 navigation-fragment 开发库和它其中的 FragmentNavigator 与 DialogFragmentNavigator 中。类似的,与 Composable 的交互逻辑被封装在完全独立的 navigation-compose 开发库和它的 ComposeNavigator 中。这里的抽象设计意味着如果您希望仅仅通过 Composable 构建您的应用,那么当您使用 Navigation Compose 时无需任何涉及到 Fragment 的依赖。

该级别的分离意味着 Navigation 中有两个层次来实现多返回栈:

  • 保存独立的 NavBackStackEntry 实例状态,这些实例组成了 NavController 返回栈。这是属于 NavController 的职责。
  • 保存 Navigator 针对每个 NavBackStackEntry 的特定状态 (比如与 FragmentNavigator 目的地相关联的 Fragment)。这是属于 Navigator 的职责。

仍需特别注意那些 尚未 更新的 Navigator,它们无法支持保存自身状态。底层的 Navigator API 已经整体重写来支持状态保存 (您需要覆写新增的 navigate() 和 popBackStack() API 的重载方法,而不是覆写之前的版本),即使 Navigator 并未更新,NavController 仍会保存 NavBackStackEntry 的状态 (在 Jetpack 世界中向后兼容是非常重要的)。

备注: 通过绑定 TestNavigatorState 使其成为一个 mini-NavController 可以实现在新的 Navigator API 上更轻松、独立地测试您自定义的 Navigator

如果您仅仅在应用中使用 Navigation,那么 Navigator 这个层面更多的是实现细节,而不是您需要直接与之交互的内容。可以这么说,我们已经完成了将 FragmentNavigator 和 ComposeNavigator 迁移到新的 Navigator API 的工作,使其能够正确地保存和恢复它们的状态,在这个层面上您无需再做任何额外工作。

在 Navigation 中启用多返回栈

如果您正在使用 NavigationUI,它是用于连接您的 NavController 到 Material 视图组件的一系列专用助手,您会发现对于菜单项、BottomNavigationView (现在叫 NavigationRailView) 和 NavigationView,多返回栈是 默认启用 的。这就意味着结合 navigation-fragment 和 navigation-ui 使用就可以。

NavigationUI API 是基于 Navigation 的其他公共 API 构建的,确保您可以准确地为自定义组件构建您自己的版本。保证您可以构建所需的自定义组件。启用保存和恢复返回栈的 API 也不例外,在 Navigation XML 中通过 NavOptions 上的新 API,也就是 navOptions Kotlin DSL,以及 popBackStack() 的重载方法可以帮助您指定 pop 操作保存状态或者指定 navigate 操作来恢复之前已保存的状态。

比如,在 Compose 中,任何全局的导航模式 (无论是底部导航栏、导航边栏、抽屉式导航栏或者任何您能想到的形式) 都可以使用我们在与 底部导航栏集成 所介绍的相同的技术,并且结合 saveState 和 restoreState 属性一起调用 navigate():

onClick = {
navController.navigate(screen.route) {
// 当用户选择子项时在返回栈中弹出到导航图中的起始目的地
// 来避免太过臃肿的目的地堆栈
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}

// 当重复选择相同项时避免相同目的地的多重拷贝
launchSingleTop = true
// 当重复选择之前已经选择的项时恢复状态
restoreState = true
}
}

保存状态,锁定用户

对用户来说,最令人沮丧的事情之一便是丢失之前的状态。这也是为什么 Fragment 用一整页来讲解 保存与 Fragment 相关的状态,而且也是我非常乐于更新每个层级来支持多返回栈的原因之一:

  • Fragments (比如完全不使用 Navigation Component): 通过使用新的 FragmentManager API,也就是 saveBackStack 和 restoreBackStack

  • 核心的 Navigation 运行时: 添加可选的新的 NavOptions 方法用于 restoreState(恢复状态) 和 saveState (保存状态) 以及新的 popBackStack() 的重载方法,它同样可以传入一个布尔型的 saveState 参数 (默认是 false)。

  • 通过 Fragment 实现 Navigation: FragmentNavigator 现在利用新的 NavigatorAPI,通过使用 Navigation 运行时 API 将 Navigation 运行时 API 转换为 Fragment API。

  • NavigationUI: 每当它们弹出返回栈时,onNavDestinationSelected()、NavigationBarView.setupWithNavController() 和 NavigationView.setupWithNavController() 现在默认使用 restoreState 和 saveState 这两个新的 NavOption。也就意味着 当升级到 Navigation 2.4.0-alpha01 或者更高版本后,任何使用 NavigationUI API 的应用无需修改代码即可实现多返回栈

如果您希望了解 更多使用该 API 的示例,请参考 NavigationAdvancedSample (它是最新更新的,且不包含任何用于支持多返回栈的 NavigationExtensions 代码)。

收起阅读 »

Android 面试准备进行曲-Java基础篇

虚拟机 基础jvm 参考文章JVM内存管理JVM执行Java程序的过程:Java源代码文件(.java)会被Java编译器编译为字节码文件(.class),然后JVM中的类加载器加载各个类的字节码文件,加载完毕之后,交由JVM执行引擎执行。运行时数据区被分为&...
继续阅读 »

虚拟机 基础

jvm 参考文章

JVM内存管理

JVM执行Java程序的过程:Java源代码文件(.java)会被Java编译器编译为字节码文件(.class),然后JVM中的类加载器加载各个类的字节码文件,加载完毕之后,交由JVM执行引擎执行。

1.webp

运行时数据区被分为 线程私有数据区 和 线程共享数据区 两大类:

线程私有数据区包含:程序计数器、虚拟机栈、本地方法栈 线程共享数据区包含:Java堆、方法区(内部包含常量池)

线程私有数据区包含:

  • 程序计数器:是当前线程所执行的字节码的行号指示器
  • 虚拟机栈:是Java方法执行的内存模型
  • 本地方法栈:是虚拟机使用到的Native方法服务

线程共享数据区包含:

  • Java堆:用于存放几乎所有的对象实例和数组;是垃圾收集器管理的主要区域,也被称做“GC堆”;是Java虚拟机所管理的内存中最大的一块

  • 方法区:用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据;Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放

Java堆和栈的区别

  • 堆内存 用于存储Java中的对象和数组,当我们new一个对象或者创建一个数组的时候,就会在堆内存中开辟一段空间给它,用于存放。特点: 先进先出,后进后出。堆可以动态地分配内存大小,由于要在运行时动态分配内存,存取速度较慢。

  • 栈内存

主要是用来执行程序用的,比如:基本类型的变量和对象的引用变量。特点:先进后出,后进先出,存取速度比堆要快,仅次于寄存器,栈数据可以共享,但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性

垃圾回收机制/ 回收算法

判定对象可回收有两种方法:

  • 引用计数算法:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。然而在主流的Java虚拟机里未选用引用计数算法来管理内存,主要原因是它难以解决对象之间相互循环引用的问题,所以出现了另一种对象存活判定算法。

  • 可达性分析法:通过一系列被称为『GC Roots』的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。其中可作为GC Roots的对象:虚拟机栈中引用的对象,主要是指栈帧中的本地变量、本地方法栈中Native方法引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象

回收算法

分代收集算法:是当前商业虚拟机都采用的一种算法,根据对象存活周期的不同,将Java堆划分为新生代和老年代,并根据各个年代的特点采用最适当的收集算法。

  • 新生代:多数对象死去,少量存活。使用『复制算法』,只需复制少量存活对象即可。

    • 复制算法:把可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用尽后,把还存活着的对象『复制』到另外一块上面,再将这一块内存空间一次清理掉。
  • 老年代:对象存活率高。使用『标记—清理算法』或者『标记—整理算法』,只需标记较少的回收对象即可。

    • 标记-清除算法:首先『标记』出所有需要回收的对象,然后统一『清除』所有被标记的对象。
    • 标记-整理算法:首先『标记』出所有需要回收的对象,然后进行『整理』,使得存活的对象都向一端移动,最后直接清理掉端边界以外的内存。

参考文章

Java基础

Java 引用类型

  • 强引用(StrongReference):具有强引用的对象不会被GC;即便内存空间不足,JVM宁愿抛出OutOfMemoryError使程序异常终止,也不会随意回收具有强引用的对象。

  • 软引用(SoftReference):具有软引用的对象,会在内存空间不足的时候被GC;软引用常用来实现内存敏感的高速缓存。

  • 弱引用(WeakReference):被弱引用关联的对象,无论当前内存是否足够都会被GC;强度比软引用更弱,常用于描述非必需对象;常用于解决内存泄漏的问题(Handle 中Context 部分)

  • 虚引用(PhantomReference):仅持有虚引用的对象,在任何时候都可能被GC;常用于跟踪对象被GC回收的活动;必须和引用队列 (ReferenceQueue)联合使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。

类加载机制

类加载机制:是虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,形成可被虚拟机直接使用的Java类型的过程。另外,类型的加载、连接和初始化过程都是在程序运行期完成的,从而通过牺牲一些性能开销来换取Java程序的高度灵活性。主要阶段:

  • 加载(Loading):通过类的全限定名来获取定义此类的二进制字节流;将该二进制字节流所代表的静态存储结构转化为方法区的运行时数据结构,该数据存储数据结构由虚拟机实现自行定义;在内存中生成一个代表这个类的java.lang.Class对象,它将作为程序访问方法区中的这些类型数据的外部接口

  • 验证(Verification):确保Class文件的字节流中包含的信息符合当前虚拟机的要求,包括文件格式验证、元数据验证、字节码验证和符号引用验证

  • 准备(Preparation):为类变量分配内存,因为这里的变量是由方法区分配内存的,所以仅包括类变量而不包括实例变量,后者将会在对象实例化时随着对象一起分配在Java堆中;设置类变量初始值,通常情况下零值

  • 解析(Resolution):虚拟机将常量池内的符号引用替换为直接引用的过程

  • 初始化(Initialization):是类加载过程的最后一步,会开始真正执行类中定义的Java字节码。而之前的类加载过程中,除了在『加载』阶段用户应用程序可通过自定义类加载器参与之外,其余阶段均由虚拟机主导和控制

内存模型

22.webp 主内存是所有变量的存储位置,每条线程都有自己的工作内存,用于保存被该线程使用到的变量的主内存副本拷贝。为了获取更好的运行速度,虚拟机可能会让工作内存优先存储于寄存器和高速缓存中。

并发过程中的原子性 时序性

  • 原子性

可直接保证的原子性变量操作有:read、load、assign、use、store和write,因此可认为基本数据类型的访问读写是具备原子性的。

若需要保证更大范围的原子性,可通过更高层次的字节码指令monitorenter和monitorexit来隐式地使用lock和unlock这两个操作,反映到Java代码中就是同步代码块synchronized关键字。

  • 可见性(一个线程修改了共享变量的值,其他线程能够立即得知这个修改)

通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现。

提供三个关键字保证可见性:volatile能保证新值能立即同步到主内存,且每次使用前立即从主内存刷新;synchronized对一个变量执行unlock操作之前可以先把此变量同步回主内存中;被final修饰的字段在构造器中一旦初始化完成且构造器没有把this的引用传递出去,就可以在其他线程中就能看见final字段的值。

  • 有序性(按照指令顺序执行)

如果在本线程内观察,所有的操作都是有序的,指“线程内表现为串行的语义”;如果在一个线程中观察另一个线程,所有的操作都是无序的,指“指令重排序”现象和“工作内存与主内存同步延迟”现象。

提供两个关键字保证有序性:volatile 本身就包含了禁止指令重排序的语义;synchronized保证一个变量在同一个时刻只允许一条线程对其进行lock操作,使得持有同一个锁的两个同步块只能串行地进入。

设计模式 (使用过的)

  • 单一职责原则:一个类只负责一个功能领域中的相应职责

  • 开放封闭原则:对扩展开放,对修改关闭

  • 依赖倒置原则:抽象不应该依赖于细节,细节应当依赖于抽象。换言之,要针对接口编程,而不是针对实现编程

  • 迪米特法则:应该尽量减少对象之间的交互,如果两个对象之间不必彼此直接通信,那么这两个对象就不应当发生任何直接的相互作用,如果其中的一个对象需要调用另一个对象的某一个方法的话,可以通过第三者转发这个调用

  • 合成/聚合复用原则:要尽量使用合成/聚合,尽量不要使用继承

延伸:Android 中源码使用的设计模式,自己使用过的设计模式

View事件分发: 责任链模式 BitmapFactory加载图片: 工厂模式

ListAdapter: 适配器模式
DialogBuilder: 建造者模式 Adapter.notifyDataSetChanged(): 观察者模式 Binder机制: 代理模式

平时经常使用的 设计模式

单例模式

初级版

public class Singleton {
private static Singleton instance;
private Singleton (){}

public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
复制代码

进阶版

public class Singleton {
private volatile static Singleton singleton;//代码1
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {//代码2
synchronized (Singleton.class) {
if (singleton == null) {//代码3
singleton = new Singleton();//代码4
}
}
}
return singleton;
}
}
复制代码

  在多线程中 两个线程可能同时进入代码2, synchronize保证只有一个线程能进入下面的代码,   此时一个线程A进入一个线程B在外等待, 当线程A完成代码3 和代码4之后, 线程B进入synchronized下面的方法, 线程B在代码3的时候判断不过,从而保证了多线程下 单例模式的线程安全,   另外要慎用单例模式,因为单例模式一旦初始化后 只有进程退出才有可能被回收,如果一个对象不经常被使用,尽量不要使用单例,否则为了几次使用,一直让单例存在占用内存

优点:

  • 内存中只存在一个对象,节省了系统资源。
  • 避免对资源的多重占用,例如一个文件操作,由于只有一个实例存在内存中,避免对同一资源文件的同时操作。

缺点:

  • 单例对象如果持有Context,那么很容易引发内存泄露。
  • 单例模式一般没有接口,扩展很困难,若要扩展,只能修改代码来实现。

Builder 模式

参考文章

33.webp 具体的产品类

public class Computer {
private String mCPU;
private String mMemory;
private String mHD;

public void setCPU(String CPU) {
mCPU = CPU;
}

public void setMemory(String memory) {
mMemory = memory;
}

public void setHD(String HD) {
mHD = HD;
}
}
复制代码

抽象建造者

public abstract class Builder {
public abstract void buildCPU(String cpu);//组装CPU

public abstract void buildMemory(String memory);//组装内存

public abstract void buildHD(String hd);//组装硬盘

public abstract Computer create();//返回组装好的电脑
}
复制代码

创建者实现类

public class ConcreteBuilder extends Builder {
//创建产品实例
private Computer mComputer = new Computer();

@Override
public void buildCPU(String cpu) {//组装CPU
mComputer.setCPU(cpu);
}

@Override
public void buildMemory(String memory) {//组装内存
mComputer.setMemory(memory);
}

@Override
public void buildHD(String hd) {//组装硬盘
mComputer.setHD(hd);
}

@Override
public Computer create() {//返回组装好的电脑
return mComputer;
}
}
复制代码

调用者

public class Director {
private Builder mBuild = null;

public Director(Builder build) {
this.mBuild = build;
}

//指挥装机人员组装电脑
public void Construct(String cpu, String memory, String hd) {
mBuild.buildCPU(cpu);
mBuild.buildMemory(memory);
mBuild.buildHD(hd);
}
}
复制代码

调用


Director direcror = new Director(new ConcreteBuilder());//创建指挥者实例,并分配相应的建造者,(老板分配任务)
direcror.Construct("i7-6700", "三星DDR4", "希捷1T");//组装电脑
复制代码

Builder 模式 优缺点

优点

  • 封装性良好,隐藏内部构建细节。
  • 易于解耦,将产品本身与产品创建过程进行解耦,可以使用相同的创建过程来得到不同的产品。也就说细节依赖抽象。
  • 易于扩展,具体的建造者类之间相互独立,增加新的具体建造者无需修改原有类库的代码。
  • 易于精确控制对象的创建,由于具体的建造者是独立的,因此可以对建造过程逐步细化,而不对其他的模块产生任何影响。

缺点

  • 产生多余的Build对象以及Dirextor类。
  • 建造者模式所创建的产品一般具有较多的共同点,其组成部分相似;如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制

源码中使用的 比如: Dialog.Builder

工厂模式

44.webp

抽象产品类

    public abstract class Product {
public abstract void show();
}
复制代码

具体产品类

    public class ProductA extends Product {
@Override
public void show() {
System.out.println("product A");
}
}
//具体产品类B
public class ProductB extends Product {
@Override
public void show() {
System.out.println("product B");
}
}
复制代码

创建抽象工厂类

	 //抽象工厂类
public abstract class Factory {
public abstract Product create();
}
复制代码

创建具体工厂类,继承抽象工厂类

	public class FactoryA extends Factory {
@Override
public Product create() {
return new ProductA();//创建ProductA
}
}
//具体工厂类B
public class FactoryB extends Factory {
@Override
public Product create() {
return new ProductB();//创建ProductB
}
}
复制代码

测试代码

		Factory factoryA = new FactoryA();
Product productA = factoryA.create();
productA.show();
//产品B
Factory factoryB = new FactoryB();
Product productB = factoryB.create();
productB.show();
复制代码

优点:

  • 符合开放封闭原则。新增产品时,只需增加相应的具体产品类和相应的工厂子类即可。
  • 符合单一职责原则。每个具体工厂类只负责创建对应的产品。

缺点:

  • 增加新产品时,还需增加相应的工厂类,系统类的个数将成对增加,增加了系统的复杂度和性能开销。

源码中 使用的 比如 ThreadFactory

观察者模式

参考文章

含义: 定义对象间的一种一个对多的依赖关系,当一个对象的状态发送改变时,所以依赖于它的对象都得到通知并被自动更新。

55.webp

备注:

  • Subject(抽象主题):又叫抽象被观察者,把所有观察者对象的引用保存到一个集合里,每个主题都可以有任何数量的观察者。抽象主题提供一个接口,可以增加和删除观察者对象。

  • ConcreteSubject(具体主题):又叫具体被观察者,将有关状态存入具体观察者对象;在具体主题内部状态改变时,给所有登记过的观察者发出通知。

  • Observer (抽象观察者):为所有的具体观察者定义一个接口,在得到主题通知时更新自己。

  • ConcrereObserver(具体观察者):实现抽象观察者定义的更新接口,当得到主题更改通知时更新自身的状态。

代码实现

抽象观察者

	public interface Observer {//抽象观察者
public void update(String message);//更新方法
}
复制代码

具体观察者

public class Boy implements Observer {

private String name;//名字
public Boy(String name) {
this.name = name;
}
@Override
public void update(String message) {//男孩的具体反应
System.out.println(name + ",收到了信息:" + message+"屁颠颠的去取快递.");
}
}
复制代码

创建抽象主题

	public interface  Observable {//抽象被观察者
void add(Observer observer);//添加观察者

void remove(Observer observer);//删除观察者

void notify(String message);//通知观察者
}
复制代码

创建具体主题

	public class Postman implements  Observable{//快递员

private List<Observer> personList = new ArrayList<Observer>();//保存收件人(观察者)的信息
@Override
public void add(Observer observer) {//添加收件人
personList.add(observer);
}

@Override
public void remove(Observer observer) {//移除收件人
personList.remove(observer);

}

@Override
public void notify(String message) {//逐一通知收件人(观察者)
for (Observer observer : personList) {
observer.update(message);
}
}
}
复制代码

测试代码

	Observable postman=new Postman();

Observer boy1=new Boy("路飞");
Observer boy2=new Boy("乔巴");
postman.notify("快递到了,请下楼领取.");
复制代码

优点:

  • 解除观察者与主题之间的耦合。让耦合的双方都依赖于抽象,而不是依赖具体。从而使得各自的变化都不会影响另一边的变化。
  • 易于扩展,对同一主题新增观察者时无需修改原有代码。

缺点:

  • 使用观察者模式时需要考虑一下开发效率和运行效率的问题,程序中包括一个被观察者、多个观察者,开发、调试等内容会比较复杂,而且在Java中消息的通知一般是顺序执行,那么一个观察者卡顿,会影响整体的执行效率,在这种情况下,一般会采用异步实现。
  • 可能会引起多余的数据通知。

Android 源码中的观察者模式:Listener

其他设计模式

由于本人涉猎较少,有些只能说简单了解。这里分享一个 不错的 系列博客,感谢前人栽树。

设计模式系列教程 !!!

源码设计

接口和抽象类

  • 抽象类可以提供成员方法的实现细节,而接口中只能存在 public 抽象方法,没有实现,(JDK8以后可以有)
  • 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是 public static final 类型的;
  • 接口的成员变量只能是静态常量,没有构造函数,也没有代码块,但抽象类都可以有。
  • 一个类只能继承一个抽象类,而一个类却可以实现多个接口;

抽象类访问速度比接口速度要快,因为接口需要时间去寻找在类中具体实现的方法;

  • 如果你往抽象类中添加新的方法,你可以给它提供默认的实现。因此你不需要改变你现在的代码。如果你往接口中添加方法,那么你必须改变实现该接口的类。

所以:抽象类强调的是重用,接口强调的是解耦。

收起阅读 »

Android 面试准备进行曲-Android基础进阶

View相关View的绘制流程自定义控件: 1、组合控件。这种自定义控件不需要我们自己绘制,而是使用原生控件组合成的新控件。如标题栏。 2、继承原有的控件。这种自定义控件在原生控件提供的方法外,可以自己添加一些方法。如制作圆角,圆形图片。 3、完全自定义控件:...
继续阅读 »

View相关

View的绘制流程

自定义控件: 1、组合控件。这种自定义控件不需要我们自己绘制,而是使用原生控件组合成的新控件。如标题栏。 2、继承原有的控件。这种自定义控件在原生控件提供的方法外,可以自己添加一些方法。如制作圆角,圆形图片。 3、完全自定义控件:这个View上所展现的内容全部都是我们自己绘制出来的。比如说制作水波纹进度条。

View的绘制流程:OnMeasure()——>OnLayout()——>OnDraw()

第一步:OnMeasure():测量视图大小。从顶层父View到子View递归调用measure方法,measure方法又回调OnMeasure。

第二步:OnLayout():确定View位置,进行页面布局。从顶层父View向子View的递归调用view.layout方法的过程,即父View根据上一步measure子View所得到的布局大小和布局参数,将子View放在合适的位置上。

第三步:OnDraw():绘制视图。ViewRoot创建一个Canvas对象,然后调用OnDraw()。

1.webp

View,ViewGroup事件分发

Touch事件分发中只有两个主角:ViewGroup和View。ViewGroup包含onInterceptTouchEvent、dispatchTouchEvent、onTouchEvent三个相关事件。View包含dispatchTouchEvent、onTouchEvent两个相关事件。其中ViewGroup又继承于View。

2.ViewGroup和View组成了一个树状结构,根节点为Activity内部包含的一个ViwGroup。

3.触摸事件由Action_Down、Action_Move、Aciton_UP组成,其中一次完整的触摸事件中,Down和Up都只有一个,Move有若干个,可以为0个。

4.当Acitivty接收到Touch事件时,将遍历子View进行Down事件的分发。ViewGroup的遍历可以看成是递归的。分发的目的是为了找到真正要处理本次完整触摸事件的View,这个View会在onTouchuEvent结果返回true。

5.当某个子View返回true时,会中止Down事件的分发,同时在ViewGroup中记录该子View。接下去的Move和Up事件将由该子View直接进行处理。由于子View是保存在ViewGroup中的,多层ViewGroup的节点结构时,上级ViewGroup保存的会是真实处理事件的View所在的ViewGroup对象:如ViewGroup0-ViewGroup1-TextView的结构中,TextView返回了true,它将被保存在ViewGroup1中,而ViewGroup1也会返回true,被保存在ViewGroup0中。当Move和UP事件来时,会先从ViewGroup0传递至ViewGroup1,再由ViewGroup1传递至TextView。

6.当ViewGroup中所有子View都不捕获Down事件时,将触发ViewGroup自身的onTouch事件。触发的方式是调用super.dispatchTouchEvent函数,即父类View的dispatchTouchEvent方法。在所有子View都不处理的情况下,触发Acitivity的onTouchEvent方法。

7.onInterceptTouchEvent有两个作用:1.拦截Down事件的分发。2.中止Up和Move事件向目标View传递,使得目标View所在的ViewGroup捕获Up和Move事件。

2.webp

view 事件分发流程

3.webp

ViewGroup 时间分发流程

4.webp

整体Activity - ViewGroup - view 分发流程

5.webp

View 事件分发及源码讲解

MeasureSpec 相关知识

MeasureSpec 是一个32位int值,高2位代表SpecMode(测量模式),低30位代表SpecSize( 某种测量模式下的规格大小)。通过宽测量值widthMeasureSpec和高测量值heightMeasureSpec决定View的大小 SpecMode 代表的三种测量模式分别为:

  1. UNSPECIFIED:父容器不对View有任何限制,要多大有多大。常用于系统内部。

  2. EXACTLY(精确模式):父视图为子视图指定一个确切的尺寸SpecSize。对应LyaoutParams中的match_parent或具体数值。

  3. AT_MOST(最大模式):父容器为子视图指定一个最大尺寸SpecSize,View的大小不能大于这个值。对应LayoutParams中的wrap_content。

决定因素:值由子View的布局参数LayoutParams和父容器的MeasureSpec值共同决定。见下图:

6.webp

参考图片 及讲解地址

SurfaceView和View的区别

SurfaceView是从View基类中派生出来的显示类,他和View的区别有:

  • View需要在UI线程对画面进行刷新,而SurfaceView可在子线程进行页面的刷新,View适用于主动更新的情况,View频繁刷新会阻塞主线程,导致界面卡顿

  • SurfaceView在底层已实现双缓冲机制,而View没有,因此SurfaceView更适用于被动更新,需要频繁刷新、刷新时数据处理量很大的页面,而SurfaceView适用于

invalidate()和postInvalidate()的区别

invalidate()与postInvalidate()都用于刷新View,主要区别是invalidate()在主线程中调用,若在子线程中使用需要配合handler;而postInvalidate()可在子线程中直接调用。 我们通过 postInvalidate 如何在子线程中更新的

	// 系统代码
public void postInvalidateDelayed(long delayMilliseconds) {
// We try only with the AttachInfo because there's no point in invalidating
// if we are not attached to our window
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
}
}

复制代码

接下来我们看下

	// 系统代码
public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
mHandler.sendMessageDelayed(msg, delayMilliseconds);
}

复制代码

我们可以看到 postInvalidate它是向主线程发送个Message,然后handleMessage时,调用了invalidate()函数。(系统帮我们 写好了 Handle部分)

Android 动画

Android中的几种动画

帧动画:指通过指定每一帧的图片和播放时间,有序的进行播放而形成动画效果,比如想听的律动条。

补间动画:指通过指定View的初始状态、变化时间、方式,通过一系列的算法去进行图形变换,从而形成动画效果,主要有Alpha、Scale、Translate、Rotate四种效果。注意:只是在视图层实现了动画效果,并没有真正改变View的属性,比如滑动列表,改变标题栏的透明度。

属性动画:在Android3.0的时候才支持,通过不断的改变View的属性,不断的重绘而形成动画效果。相比于视图动画,View的属性是真正改变了。比如view的旋转,放大,缩小。

属性动画和补间动画区别

  • 补间动画仅仅是 Parents View 对子View里面的画布进行操作,新位置并不响应点击事件,原位置响应。
  • 属性动画是通过修改view属性实现动画,新位置响应点击事件

属性动画为何在新位置还能响应事件

ViewGroup 在 getTransformedMotionEvent() 方法中会通过子 View 的 hasIdentityMatrix() 方法来判断子 View 是否应用过位移、缩放、旋转之类的属性动画。如果应用过的话,那还会调用子 View 的 getInverseMatrix() 做「反平移」操作,然后再去判断处理后的触摸点是否在子 View 的边界范围内。

属性动画点击解密

属性动画原理

属性动画要求 动画作用的对象提供该属性的set方法,属性动画根据你传递的该熟悉的初始值和最终值,以动画的效果多次去调用set方法,每次传递给set方法的值都不一样,确切来说是随着时间的推移,所传递的值越来越接近最终值。如果动画的时候没有传递初始值,那么还要提供get方法,因为系统要去拿属性的初始值。

// 系统代码
void setAnimatedValue(Object target) {
if (mProperty != null) {
mProperty.set(target, getAnimatedValue());
}
if (mSetter != null) {
try {
mTmpValueArray[0] = getAnimatedValue();
mSetter.invoke(target, mTmpValueArray);
} catch (InvocationTargetException e) {
Log.e("PropertyValuesHolder", e.toString());
} catch (IllegalAccessException e) {
Log.e("PropertyValuesHolder", e.toString());
}
}

复制代码

属性动画源码解析

Handler 详解

Handler的原理

Android中主线程是不能进行耗时操作的,子线程是不能进行更新UI的。所以就有了handler,它的作用就是实现线程之间的通信。 handler整个流程中,主要有四个对象,handlerMessage,MessageQueue,Looper。当应用创建的时候,就会在主线程中创建handler对象。

对于Message :

在线程之间传递的消息,它的内部持有Handler和Runnable的引用以及消息类型。可以使用what、arg1、arg2字段携带一些整型数据,使用obj字段携带Object对象;其中有一个obtain()方法,该方法的内部是先通过消息池获取消息,没有再创建,实现了对message对象的复用。其内部有一个target引用,就是对Handler对象的引用,在Looper.loop方法中的消息处理就是通过message的target引用来调用Handler的dispatchMessage()方法来实现消息的处理。

对于Message Queue:

指的是消息队列,是通过一个 单链表 的数据结构维护消息列表的,在插入和删除有优势。其中主要包括两个操作:插入和读取,读取操作本身伴随着删除操作。插入操作是enqueueMessage()方法,就是插入一条消息到MessageQueue中;读取操作是next()方法,它是一个无限循环,如果有消息就返回并从单链表中移除;没有消息就一直阻塞(此时主线程会释放CPU进入休眠状态)。

对于Looper:

Looper在消息机制中进行消息循环,像一个泵,不断地从MessageQueue中查看是否有新消息并提取,交给handler处理。Handler机制一定要Looper,在线程中通过Looper.prepare()为当前线程创建一个Looper,并使用Looper.loop()来开启消息的读取。为什么在平常Activity主线程使用时没有使用到Looper呢?因为对于主线程(UI线程),会自动创建一个Looper 驱动消息队列获取消息,所以Looper可以通过getMainLooper获取到主线程的Looper。 通过quit/quitSafely可以退出Looper,区别在于quit会直接退出,quitSafely会把消息队列已有的消息处理完毕后才退出Looper。

对于Handler

Handler可以发送和接收消息。发送消息(就是往MessageQueue里面插入一条Message)通过post方法和send方法,而post方法最终也是通过send方法来发送的,最终就会调用sendMessageAtTime这个方法(内部就是调用MessageQueue的enqueueMessage()方法,往MessageQueue里面插入一条消息),同时也会给msg的target赋值为handler本身,进入MessageQueue中。处理消息就是Looper调用loop()方法进入无限循环,获取到消息后就会调用msg.target(Handler本身)的dispatchMessage()方法,进而调用handlerMessage()方法处理消息。

Handler导致内存泄露问题

一般我们写Handler:

Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
mImageView.setImageBitmap(mBitmap);
}
}
复制代码

当使用内部类(包括匿名类)来创建Handler的时候,Handler对象会隐式地持有一个外部类对象(通常是一个Activity)的引用,而常常在Activity退出后,消息队列还有未被处理完的消息,此时activity依然被handler引用,导致内存无法回收而内存泄露。

在Handler中增加一个对Activity的弱引用(WeakReference):

static class MyHandler extends Handler {
WeakReference mActivityReference;

MyHandler(Activity activity) {
mActivityReference= new WeakReference(activity);
}

@Override
public void handleMessage(Message msg) {
final Activity activity = mActivityReference.get();
if (activity != null) {
mImageView.setImageBitmap(mBitmap);
}
}
}

复制代码

如果在非自定义 Handler 情况下,还可以通过 Activity 生命周期来及时清除消息,从而及时回收 Activity

override fun onDestroy() {
super.onDestroy()
if (mHandler != null){
mHandler.removeCallbacksAndMessages(null)
}
}
复制代码

Handler的post方法原理

mHandler.post(new Runnable()
{
@Override
public void run()
{
Log.e(“TAG”, Thread.currentThread().getName());
mTxt.setText(“yoxi”);
}
});
复制代码

然后run方法中可以写更新UI的代码,其实这个Runnable并没有创建什么线程,而是发送了一条消息,下面看源码:

public final boolean post(Runnable r)
{
return sendMessageDelayed(getPostMessage(r), 0);
}
复制代码

最终和handler.sendMessage一样,调用了sendMessageAtTime,然后调用了enqueueMessage方法,给msg.target赋值为handler,最终加入MessagQueue.

Handler 其他问题

  1. Looper.loop()和MessageQueue.next()取延时消息时,主线程中使用死循环为什么不会卡死?

    答: 在MessageQueue在取消息时,如果是延时消息就会计算得到该延时消息还需要延时多久nextPollTimeoutMillis。然后再继续循环的时候,发现nextPollTimeoutMillis不等于0,就会执行nativePollOnce阻塞线程nextPollTimeoutMillis毫秒,而阻塞了之后被唤醒的时机就是阻塞的时间到了或者又有新的消息添加进来执行enqueueMessage方法调用nativeWake唤醒阻塞线程,再继续执行获取消息的代码,如果有消息就返回,如果还是需要延时就继续和上边一样阻塞。而Android所有的事件要在主线程中改变的都会通过主线程的Handler发送消息处理,所以就完全保证了不会卡死。

    其中nativePollOnce的位置也有考究,刚好在synchronized的外边,所以在阻塞的时候也能保证添加消息是可以执行的,而取消息 时添加消息就需要等待。

  2. MessageQueue是队列吗? 答: MessageQueue不是队列,它内部使用一个Message链表实现消息的存和取。

  3. Handler的postDelay,时间准吗?它用的是system.currentTime吗? 答: 不准,因为looper里边从MessageQueue里取出来的消息执行也是串行的,如果前一个消息是比较耗时的,那么等到执行之前延时的消息时时间难免可能会超过延时的时间。postDelay时用的是System.uptimeMillis,也就是开机时间。

  4. 子线程run方法中如何使用Handler? 答 : 先要使用Lopper.prepare方法,然后使用该looper创建一个Handler,最后调用Looper.loop方法;Looper.loop方法之后就不要执行写代码了,因为是loop是死循环除非退出,所以Handler的创建也必须写在loop之前。

  5. ThreadLocal是如何实现一个线程一个Looper的? 答: Looper的使用最终都需要执行loop方法,而loop方法中去获取的Looper是从sThreadLocal中获取的,所以Looper就需要和sThreadLocal建立关系,在不考虑反射的情况下,就只能通过Looper的prepare方法进行关联,这里边就会引入一个threadLocalMap,该对象又是和thread一一对应,而threadLocal的get方法实际使用的就是threadLocalMap的get方法,而key就是Looper中的静态变量sThreadLocal,value则就是当前looper对象,而prepare方法只能被执行一次,也就保证了一个线程只有一个looper。ThreadLocalMap对key和value的存取和hashMap类似。

  6. 假设先 postDelay 10ms, 再postDelay 1ms,这两个消息会有什么不同的经历。 答: 先传入一个延时为10ms的消息进入MessageQueue中,因为该消息延时,假设当前消息队列中没有消息,则会直接将消息放入队列,因为loop一直在取消息,但是这里有延时就会阻塞10ms,当然这不考虑代码执行的时间;然后延时1ms的消息进入时,会和之前的10ms的消息进行比较,根据延时的大小进行排序插入,延时小的在前边,所以这时候就把1ms的消息放在10ms的前边,然后唤醒,不阻塞,继续执行取消息的操作,发现还是有延时1ms,所以也会继续阻塞1ms,直到阻塞1ms之后或者又有新的消息进入队列唤醒,直到获取到1ms延时消息,在loop中,通过调用handler的dispatchMessage方法,判断消息的callback或者Handler的callback不为null就回调对应的callback,否则就执行handler的handleMessage方法,我们就可以根据情况处理消息了;10ms的延时消息的处理也是一致,延时的时间到了就交给返回给looper,然后给handler处理。

  7. HandlerThread ? 答: HandlerThread是Thread的一个子类,只是内部创建了一个Handler,这个Handler是子线程的handler,其中子线程的looper的创建和管理也提供了方法方便使用。

  8. 你对 Message.obtain() 了解吗? 答: Message.obtain其实是从缓冲的消息池中取出第一个消息来使用,避免消息对象的频繁创建和销毁;消息池其实是使用Message链表结构实现,在消息在loop中被handler分发消费之后会执行回收的操作,将该消息内部数据清空并添加到消息链表最前边。

多线程相关问题

如何创建多线程

  1. 继承Thread类,重写run函数方法
  2. 实现Runnable接口,重写run函数方法
  3. 实现Callable接口,重写call函数方法
  4. HandlerThread
  5. AsyncTask很老的一种= =

多线程间同步问题

  1. volatile关键字,在get和set的场景下是可以的,由于get和set的时候都加了读写内存屏障,在数据可见性上保证数据同步。但是对于++这种非原子性操作,数据会出现不同步

  2. synchronized对代码块或方法加锁,结合wait,notify调度保证数据同步

  3. reentrantLock加锁结合Condition条件设置,在线程调度上保障数据同步

  4. CountDownLatch简化版的条件锁,在线程调度上保障数据同步

  5. cas=compare and swap(set), 在保证操作原子性上,确保数据同步

  6. 参照UI线程更新UI的思路,使用handler把多线程的数据更新都集中在一个线程上,避免多线程出现脏读

  7. 当然如果只是部分变量存在多线程修改的可能性 建议使用 原子类AtomicInteger AtomicBoolean等 这样会更方便一点。

Android 优化

OOM

  1. 根据java的内存模型会出现内存溢出的内存有堆内存、方法区内存、虚拟机栈内存、native方法区内存;一般说的OOM基本都是针对堆内存;

  2. 对于堆内存溢出主的根本原因有两种 (1)app进程内存达到上限 (2)手机可用内存不足,这种情况并不是我们app消耗了很多内存,而是整个手机内存不足

而我们需要解决的主要是 app的内存达到上限

  1. 对于app内存达到上限只有两种情况

(1)申请内存的速度超出gc释放内存的速度 (2)内存出现泄漏,gc无法回收泄漏的内存,导致可用内存越来越少

  1. 对于申请内存速度超出gc释放内存的速度主要有2种情况

(1)往内存中加载超大文件 (2)循环创建大量对象

  1. 一般申请内存的速度超出gc释放内存基本不会出现,内存泄漏才是出现问题的关键所在

导致内存泄漏情况

内存泄漏的根本原因在于生命周期长的对象持有了生命周期短的对象的引用

  1. 资源对象没关闭造成的内存泄漏(如: Cursor、File等)

  2. 全局集合类强引用没清理造成的内存泄漏( static 修饰的集合)

  3. 接收器、监听器注册没取消造成的内存泄漏,如广播,eventsbus

  4. Activity 的 Context 造成的泄漏,可以使用 ApplicationContext

  5. Handler 造成的内存泄漏问题(一般由于 Handler 生命周期比其外部类的生命周期长引起的)

注1:ListView 的 Adapter 中缓存用的 ConvertView ,主要缓存的是 移除屏幕外的View,就算没有复用,暂时 只会 内存溢出,和泄漏还是有区别的。

注2 :Bitmap 对象到底要不要调用 recycle() 释放内存。结论 Android 3.0 以前需要,因为像素数据与对象本身分开存储,像素数据存储在native层;对象存储在java层。 3.0之后 像素数据与Bitmap对象数据一起关联存储在Dalvik堆中。所以,这个时候,就可以考虑用GC来自动回收。所以我们不用的时候直接 将Bitmap对象设置为Null 即可。参考博客地址

我们列举了 大部分常见的 内存泄漏出现的时机,那么我也简要的列举下 常见的避免内存泄漏的方法(仅供参考);

  1. 为应用申请更大内存,把manifest上的largdgeheap设置为true

  2. 减少内存的使用 ①使用优化后的集合对象,比如SpaseArray;

②使用微信的mmkv替代sharedpreference; ③对于经常打log的地方使用StringBuilder来组拼,替代String拼接 ④统一带有缓存的基础库,特别是图片库,如果用了两套不一样的图片加载库就会出现2个图片各自维护一套图片缓存 ⑤给ImageView设置合适尺寸的图片,列表页显示缩略图,查看大图显示原图 ⑥优化业务架构设计,比如省市区数据分批加载,需要加载省就加载省,需要加载市就加载失去,避免一下子加载所有数据

  1. 避免内存泄漏

    编码规范上:

    ①资源对象用完一定要关闭,最好加finally

②静态集合对象用完要清理 ③接收器、监听器使用时候注册和取消成对出现 ④context使用注意生命周期,如果是静态类引用直接用ApplicationContext ⑤使用静态内部类 ⑥结合业务场景,设置软引用,弱引用,确保对象可以在合适的时机回收

建设内存监控体系:

线下监控:

①使用ArtHook检测图片尺寸是否超出imageview自身宽高的2倍

②编码阶段Memery Profile看app的内存使用情况,是否存在内存抖动,内存泄漏,结合Mat分析内存泄漏

线上监控:

①上报app使用期间待机内存、重点模块内存、OOM率

②上报整体及重点模块的GC次数,GC时间

③使用LeakCannery自动化内存泄漏分析

ANR

Android系统中,AMS和WMS会检测App的响应时间,如果App在主线程进行耗时操作,导致用户的操作没得到及时的响应 就会报出 Application Not Response 的问题 即ANR 。

  1. activity 、键盘输入事件和触摸事件超过五秒
  2. 前台广播10秒没有完成 后台60秒
  3. 服务前台20秒 后台200秒

主要的 规避方案

解决笼统一下尽量使用 子线程,避免死锁 的出现,使用子线程来处理耗时操作或阻塞任务。服务内容提供者尽量不要执行太长时间的任务。

收起阅读 »

Activity的启动方法

在 Android 中,界面的跳转通常是通过启动不同的 Activity 来实现的,下面介绍一下 Activity 的启动方法。显式调用显式调用,字面意思即”明显的调用“,我们可以在调用方法中明确的知道我们即将启动的 Activity,显示调用的具体方法如下:...
继续阅读 »

在 Android 中,界面的跳转通常是通过启动不同的 Activity 来实现的,下面介绍一下 Activity 的启动方法。

显式调用

显式调用,字面意思即”明显的调用“,我们可以在调用方法中明确的知道我们即将启动的 Activity,显示调用的具体方法如下:

val intent = Intent(this, SecondActivity::class.java)
// SecondActivity::class.java 相当于Java中的 SecondActivity.class
startActivity(intent)

我们需要构建一个 Intent 对象,第一个参数传入 this 即当前 Activity 的上下文,第二个对象传入 SecondActivity::class.java 作为目标 Activity,这样我们的意图就很明显,即我们想跳转到 SecondActivity 这个界面,我们只需要调用 startActivity 这个函数就可以达到我们的目的了。

隐式调用

隐式调用与显式调用相反,它没有明确的说明要跳转到哪个 Activity,而是通过 action, category [ˈkætəɡəri] 和 data 这三个过滤信息由系统去匹配复合条件的 Activity。

为 Activity 设置过滤信息

如果我们想要通过隐式调用去启动一个 Activity,我们首先要为这个 Activity 设置过滤信息,否则是不能通过隐式调用去启动这个 Activity 的。过滤信息在 AndroidMainfest.xml 文件中注册 Activity 时设置,通过在 标签下配置 的内容,可以指定当前Activity能够响应的action,category和data。

<activity
   android:name=".activity.SplashActivity"
   android:theme="@style/SplashTheme">
   <intent-filter>
       <action android:name="android.intent.action.VIEW" />
       <category android:name="android.intent.category.DEFAULT" />
       <category android:name="android.intent.category.BROWSABLE" />
       <data
           android:host="room.join"
           android:scheme="bjhlliveapp" />
   </intent-filter>
</activity>

当使用隐式调用启动 Activity 时,需要同时匹配 action,category 和 data,否则匹配失败。一个 下的 action,category 和 data 可以有多个,一个 activity 可以有多个 标签,只要成功匹配其中任意一个中的信息即可匹配成功。

下面详细说明一下各种信息的匹配规则。

action

action 是一个字符串 (区分大小写) ,系统已经为我们预定义了一些 action,如 android.intent.action.MAIN 等,同时我们也可以自己定义 action,一个 下可以有多个 action。

当我们使用隐式调用时 Intent 中必须指定 action,当 action 和 activity 的 中任意一个 action 相同 (字符串的值相同) 时,匹配成功。

category

category 和 action 一样是一个字符串,同时系统中有定义的 category,我们也可以自己定义 category,但是如果想让一个 activity 支持隐式调用,那么必须在 中指定 “android.intent.category.DEFAULT” 这个 category。

category 的匹配规则与 action 不同,隐式调用时 Intent 中必须指定 action,但可以没有 category,但是如果指定了 category (可以是一个或多个) ,那么所有指定的 category 都要和 中的 category 相同,否则匹配失败。

用通俗的话来讲,category 你可以不指定 (如果不指定一定可以匹配成功) ,但是一旦你指定了,你就要保证你指定的这些 category 都要和你即将启动的 activity 中某一个 中的category 相同。

为什么不指定反而可以匹配成功呢?因为前面说了如果 activity 支持隐式调用,则一定要有 “android.intent.category.DEFAULT” 这个 category,系统在调用 startActivity 或者 startActivityForResult 的时候会默认为 Intent 加上 “android.intent. category.DEFAULT” 这个category,所以可以匹配成功。

data

data 的语法结构如下:

<data android:scheme="string"
     android:host="string"
     android:port="string"
     android:path="string"
     android:pathPattern="string"
     android:pathPrefix="string"
     android:mimeType="string" />

data 由两部分组成,mimeType 和 URI。mimeType 指媒体类型,比如image/jpeg、audio/mpeg4-generic 和 video/* 等,可以表示图片、文本、视频等不同的媒体格式,而URI中包含的数据就比较多了,下面是URI的结构:

<scheme>://<host>:<port>/[<path>|<pathPrefix>|<pathPattern>]

例如:

content://com.example.project:200/folder/subfolder/etc
http://www.baidu.com:80/search/info

URI 中每个数据的含义如下:

  • Scheme:URI 的模式,比如http、file、content 等,如果 URI 中没有指定scheme,那么整个 URI 的其他参数无效,这也意味着 URI 是无效的。
  • Host:URI 的主机名,比如 http://www.baidu.com,如果 host 未指定,那么整个 URI 中的其他参数无效,这也意味着 URI 是无效的。
  • Port:URI 中的端口号,比如80,仅当 URI 中指定了 scheme 和 host 参数的时候 port 参数才是有意义的。
  • path、pathPattern 和 pathPrefix:这三个参数表述路径信息,其中 path 表示完整的路径信息;pathPattern 也表示完整的路径信息,但是它里面可以包含通配符 “”,“” 表示0个或多个任意字符,需要注意的是,由于正则表达式的规范,如果想表示真实的字符串,那么“*” 要写成“*”, “\”要写成“ \\”;pathPrefix 表示路径的前缀信息。

data 的匹配规则较为复杂,但总的来说就是 Intent 中的 data 必须和 中的一致,如果 中没有,则 Intent 也没有,如果 中有,则 Intent 中必须有切必须和 中相同。

调用方法

val intent = Intent("android.intent.action.CPW")
// intent 构造函数指定 action
intent.addCategory("android.intent.category.DEFAULT")
// addCategory 方法指定category
intent.setDataAndType(Uri.parse("file://abc"), "text/plain")
// setDataAndType 方法指定data,参数为 URI 和 mimeType
startActivity(intent)

以上就是关于 Activity 启动方法的全部内容了!

收起阅读 »

iOS 寄宿图 二

2.2 Custom Drawing    给contents赋CGImage的值不是唯一的设置寄宿图的方法。我们也可以直接用Core Graphics直接绘制寄宿图。能够通过继承UIView并实现-drawRect:方...
继续阅读 »

2.2 Custom Drawing

    给contents赋CGImage的值不是唯一的设置寄宿图的方法。我们也可以直接用Core Graphics直接绘制寄宿图。能够通过继承UIView并实现-drawRect:方法来自定义绘制。

    -drawRect: 方法没有默认的实现,因为对UIView来说,寄宿图并不是必须的,它不在意那到底是单调的颜色还是有一个图片的实例。如果UIView检测到-drawRect: 方法被调用了,它就会为视图分配一个寄宿图,这个寄宿图的像素尺寸等于视图大小乘以 contentsScale的值。

    如果你不需要寄宿图,那就不要创建这个方法了,这会造成CPU资源和内存的浪费,这也是为什么苹果建议:如果没有自定义绘制的任务就不要在子类中写一个空的-drawRect:方法。

    当视图在屏幕上出现的时候 -drawRect:方法就会被自动调用。-drawRect:方法里面的代码利用Core Graphics去绘制一个寄宿图,然后内容就会被缓存起来直到它需要被更新(通常是因为开发者调用了-setNeedsDisplay方法,尽管影响到表现效果的属性值被更改时,一些视图类型会被自动重绘,如bounds属性)。虽然-drawRect:方法是一个UIView方法,事实上都是底层的CALayer安排了重绘工作和保存了因此产生的图片。

    CALayer有一个可选的delegate属性,实现了CALayerDelegate协议,当CALayer需要一个内容特定的信息时,就会从协议中请求。CALayerDelegate是一个非正式协议,其实就是说没有CALayerDelegate @protocol可以让你在类里面引用啦。你只需要调用你想调用的方法,CALayer会帮你做剩下的。(delegate属性被声明为id类型,所有的代理方法都是可选的)。

    当需要被重绘时,CALayer会请求它的代理给他一个寄宿图来显示。它通过调用下面这个方法做到的:

(void)displayLayer:(CALayerCALayer *)layer;

    趁着这个机会,如果代理想直接设置contents属性的话,它就可以这么做,不然没有别的方法可以调用了。如果代理不实现-displayLayer:方法,CALayer就会转而尝试调用下面这个方法:

- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;

    在调用这个方法之前,CALayer创建了一个合适尺寸的空寄宿图(尺寸由boundscontentsScale决定)和一个Core Graphics的绘制上下文环境,为绘制寄宿图做准备,他作为ctx参数传入。

    让我们来继续第一章的项目让它实现CALayerDelegate并做一些绘图工作吧(见清单2.5).图2.12是他的结果

清单2.5 实现CALayerDelegate

@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];

//create sublayer
CALayer *blueLayer = [CALayer layer];
blueLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f);
blueLayer.backgroundColor = [UIColor blueColor].CGColor;

//set controller as layer delegate
blueLayer.delegate = self;

//ensure that layer backing image uses correct scale
blueLayer.contentsScale = [UIScreen mainScreen].scale; //add layer to our view
[self.layerView.layer addSublayer:blueLayer];

//force layer to redraw
[blueLayer display];
}

- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
{
//draw a thick red circle
CGContextSetLineWidth(ctx, 10.0f);
CGContextSetStrokeColorWithColor(ctx, [UIColor redColor].CGColor);
CGContextStrokeEllipseInRect(ctx, layer.bounds);
}
@end

图2.12

图2.12 实现CALayerDelegate来绘制图层

注意一下一些有趣的事情:

  • 我们在blueLayer上显式地调用了-display。不同于UIView,当图层显示在屏幕上时,CALayer不会自动重绘它的内容。它把重绘的决定权交给了开发者。
  • 尽管我们没有用masksToBounds属性,绘制的那个圆仍然沿边界被裁剪了。这是因为当你使用CALayerDelegate绘制寄宿图的时候,并没有对超出边界外的内容提供绘制支持。

    现在你理解了CALayerDelegate,并知道怎么使用它。但是除非你创建了一个单独的图层,你几乎没有机会用到CALayerDelegate协议。因为当UIView创建了它的宿主图层时,它就会自动地把图层的delegate设置为它自己,并提供了一个-displayLayer:的实现,那所有的问题就都没了。

    当使用寄宿了视图的图层的时候,你也不必实现-displayLayer:-drawLayer:inContext:方法来绘制你的寄宿图。通常做法是实现UIView的-drawRect:方法,UIView就会帮你做完剩下的工作,包括在需要重绘的时候调用-display方法。

总结

    本章介绍了寄宿图和一些相关的属性。你学到了如何显示和放置图片, 使用拼合技术来显示, 以及用CALayerDelegate和Core Graphics来绘制图层内容。

    在第三章,"图层几何学"中,我们将会探讨一下图层的几何,观察他们是如何放置和改变相互的尺寸的。


收起阅读 »

iOS 寄宿图 一

寄宿图图片胜过千言万语,界面抵得上千图片 ——Ben Shneiderman    我们在第一章『图层树』中介绍了CALayer类并创建了一个简单的有蓝色背景的图层。背景颜色还好啦,但是如果它仅仅是展现了一个单调的颜色未...
继续阅读 »

寄宿图

图片胜过千言万语,界面抵得上千图片 ——Ben Shneiderman

    我们在第一章『图层树』中介绍了CALayer类并创建了一个简单的有蓝色背景的图层。背景颜色还好啦,但是如果它仅仅是展现了一个单调的颜色未免也太无聊了。事实上CALayer类能够包含一张你喜欢的图片,这一章节我们将来探索CALayer的寄宿图(即图层中包含的图)。

2.1contents属性

CALayer 有一个属性叫做contents,这个属性的类型被定义为id,意味着它可以是任何类型的对象。在这种情况下,你可以给contents属性赋任何值,你的app仍然能够编译通过。但是,在实践中,如果你给contents赋的不是CGImage,那么你得到的图层将是空白的。

contents这个奇怪的表现是由Mac OS的历史原因造成的。它之所以被定义为id类型,是因为在Mac OS系统上,这个属性对CGImage和NSImage类型的值都起作用。如果你试图在iOS平台上将UIImage的值赋给它,只能得到一个空白的图层。一些初识Core Animation的iOS开发者可能会对这个感到困惑。

头疼的不仅仅是我们刚才提到的这个问题。事实上,你真正要赋值的类型应该是CGImageRef,它是一个指向CGImage结构的指针。UIImage有一个CGImage属性,它返回一个"CGImageRef",如果你想把这个值直接赋值给CALayer的contents,那你将会得到一个编译错误。因为CGImageRef并不是一个真正的Cocoa对象,而是一个Core Foundation类型。

尽管Core Foundation类型跟Cocoa对象在运行时貌似很像(被称作toll-free bridging),他们并不是类型兼容的,不过你可以通过bridged关键字转换。如果要给图层的寄宿图赋值,你可以按照以下这个方法:

layer.contents = (__bridge id)image.CGImage;

如果你没有使用ARC(自动引用计数),你就不需要__bridge这部分。但是,你干嘛不用ARC?!

让我们来继续修改我们在第一章新建的工程,以便能够展示一张图片而不仅仅是一个背景色。我们已经用代码的方式建立一个图层,那我们就不需要额外的图层了。那么我们就直接把layerView的宿主图层的contents属性设置成图片。

清单2.1 更新后的代码。

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad]; //load an image
UIImage *image = [UIImage imageNamed:@"Snowman.png"];

//add it directly to our view's layer
self.layerView.layer.contents = (__bridge id)image.CGImage;
}
@end

图表2.1 在UIView的宿主图层中显示一张图片

图2.1

我们用这些简单的代码做了一件很有趣的事情:我们利用CALayer在一个普通的UIView中显示了一张图片。这不是一个UIImageView,它不是我们通常用来展示图片的方法。通过直接操作图层,我们使用了一些新的函数,使得UIView更加有趣了。

contentGravity

你可能已经注意到了我们的雪人看起来有点。。。胖 ==! 我们加载的图片并不刚好是一个方的,为了适应这个视图,它有一点点被拉伸了。在使用UIImageView的时候遇到过同样的问题,解决方法就是把contentMode属性设置成更合适的值,像这样:

view.contentMode = UIViewContentModeScaleAspectFit;

这个方法基本和我们遇到的情况的解决方法已经接近了(你可以试一下 :) ),不过UIView大多数视觉相关的属性比如contentMode,对这些属性的操作其实是对对应图层的操作。

CALayer与contentMode对应的属性叫做contentsGravity,但是它是一个NSString类型,而不是像对应的UIKit部分,那里面的值是枚举。contentsGravity可选的常量值有以下一些:

  • kCAGravityCenter
  • kCAGravityTop
  • kCAGravityBottom
  • kCAGravityLeft
  • kCAGravityRight
  • kCAGravityTopLeft
  • kCAGravityTopRight
  • kCAGravityBottomLeft
  • kCAGravityBottomRight
  • kCAGravityResize
  • kCAGravityResizeAspect
  • kCAGravityResizeAspectFill

cotentMode一样,contentsGravity的目的是为了决定内容在图层的边界中怎么对齐,我们将使用kCAGravityResizeAspect,它的效果等同于UIViewContentModeScaleAspectFit, 同时它还能在图层中等比例拉伸以适应图层的边界。

self.layerView.layer.contentsGravity = kCAGravityResizeAspect;

图2.2 可以看到结果

image

图2.2 正确地设置contentsGravity的值

contentsScale

contentsScale属性定义了寄宿图的像素尺寸和视图大小的比例,默认情况下它是一个值为1.0的浮点数。

contentsScale的目的并不是那么明显。它并不是总会对屏幕上的寄宿图有影响。如果你尝试对我们的例子设置不同的值,你就会发现根本没任何影响。因为contents由于设置了contentsGravity属性,所以它已经被拉伸以适应图层的边界。

如果你只是单纯地想放大图层的contents图片,你可以通过使用图层的transformaffineTransform属性来达到这个目的(见第五章『Transforms』,里面对此有解释),这(指放大)也不是contengsScale的目的所在.

contentsScale属性其实属于支持高分辨率(又称Hi-DPI或Retina)屏幕机制的一部分。它用来判断在绘制图层的时候应该为寄宿图创建的空间大小,和需要显示的图片的拉伸度(假设并没有设置contentsGravity属性)。UIView有一个类似功能但是非常少用到的contentScaleFactor属性。

如果contentsScale设置为1.0,将会以每个点1个像素绘制图片,如果设置为2.0,则会以每个点2个像素绘制图片,这就是我们熟知的Retina屏幕。(如果你对像素和点的概念不是很清楚的话,这个章节的后面部分将会对此做出解释)。

这并不会对我们在使用kCAGravityResizeAspect时产生任何影响,因为它就是拉伸图片以适应图层而已,根本不会考虑到分辨率问题。但是如果我们把contentsGravity设置为kCAGravityCenter(这个值并不会拉伸图片),那将会有很明显的变化(如图2.3)

图2.3

图2.3 用错误的contentsScale属性显示Retina图片

如你所见,我们的雪人不仅有点大还有点像素的颗粒感。那是因为和UIImage不同,CGImage没有拉伸的概念。当我们使用UIImage类去读取我们的雪人图片的时候,他读取了高质量的Retina版本的图片。但是当我们用CGImage来设置我们的图层的内容时,拉伸这个因素在转换的时候就丢失了。不过我们可以通过手动设置contentsScale来修复这个问题(如2.2清单),图2.4是结果

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad]; //load an image
UIImage *image = [UIImage imageNamed:@"Snowman.png"]; //add it directly to our view's layer
self.layerView.layer.contents = (__bridge id)image.CGImage; //center the image
self.layerView.layer.contentsGravity = kCAGravityCenter;

//set the contentsScale to match image
self.layerView.layer.contentsScale = image.scale;
}

@end

图2.4

图2.4 同样的Retina图片设置了正确的contentsScale之后

当用代码的方式来处理寄宿图的时候,一定要记住要手动的设置图层的contentsScale属性,否则,你的图片在Retina设备上就显示得不正确啦。代码如下:

layer.contentsScale = [UIScreen mainScreen].scale;

maskToBounds

现在我们的雪人总算是显示了正确的大小,不过你也许已经发现了另外一些事情:他超出了视图的边界。默认情况下,UIView仍然会绘制超过边界的内容或是子视图,在CALayer下也是这样的。

UIView有一个叫做clipsToBounds的属性可以用来决定是否显示超出边界的内容,CALayer对应的属性叫做masksToBounds,把它设置为YES,雪人就在边界里啦~(如图2.5)

图2.5

图2.5 使用masksToBounds来修建图层内容

contentsRect

CALayer的contentsRect属性允许我们在图层边框里显示寄宿图的一个子域。这涉及到图片是如何显示和拉伸的,所以要比contentsGravity灵活多了

boundsframe不同,contentsRect不是按点来计算的,它使用了单位坐标,单位坐标指定在0到1之间,是一个相对值(像素和点就是绝对值)。所以他们是相对与寄宿图的尺寸的。iOS使用了以下的坐标系统:

  • 点 —— 在iOS和Mac OS中最常见的坐标体系。点就像是虚拟的像素,也被称作逻辑像素。在标准设备上,一个点就是一个像素,但是在Retina设备上,一个点等于2*2个像素。iOS用点作为屏幕的坐标测算体系就是为了在Retina设备和普通设备上能有一致的视觉效果。
  • 像素 —— 物理像素坐标并不会用来屏幕布局,但是仍然与图片有相对关系。UIImage是一个屏幕分辨率解决方案,所以指定点来度量大小。但是一些底层的图片表示如CGImage就会使用像素,所以你要清楚在Retina设备和普通设备上,他们表现出来了不同的大小。
  • 单位 —— 对于与图片大小或是图层边界相关的显示,单位坐标是一个方便的度量方式, 当大小改变的时候,也不需要再次调整。单位坐标在OpenGL这种纹理坐标系统中用得很多,Core Animation中也用到了单位坐标。

默认的contentsRect是{0, 0, 1, 1},这意味着整个寄宿图默认都是可见的,如果我们指定一个小一点的矩形,图片就会被裁剪(如图2.6)

图2.6

图2.6 一个自定义的contentsRect(左)和之前显示的内容(右)

事实上给contentsRect设置一个负数的原点或是大于{1, 1}的尺寸也是可以的。这种情况下,最外面的像素会被拉伸以填充剩下的区域。

contentsRect在app中最有趣的地方在于一个叫做image sprites(图片拼合)的用法。如果你有游戏编程的经验,那么你一定对图片拼合的概念很熟悉,图片能够在屏幕上独立地变更位置。抛开游戏编程不谈,这个技术常用来指代载入拼合的图片,跟移动图片一点关系也没有。

典型地,图片拼合后可以打包整合到一张大图上一次性载入。相比多次载入不同的图片,这样做能够带来很多方面的好处:内存使用,载入时间,渲染性能等等

2D游戏引擎入Cocos2D使用了拼合技术,它使用OpenGL来显示图片。不过我们可以使用拼合在一个普通的UIKit应用中,对!就是使用contentsRect

首先,我们需要一个拼合后的图表 —— 一个包含小一些的拼合图的大图片。如图2.7所示:

图2.7

接下来,我们要在app中载入并显示这些拼合图。规则很简单:像平常一样载入我们的大图,然后把它赋值给四个独立的图层的contents,然后设置每个图层的contentsRect来去掉我们不想显示的部分。

我们的工程中需要一些额外的视图。(为了避免太多代码。我们将使用Interface Builder来拜访他们的位置,如果你愿意还是可以用代码的方式来实现的)。清单2.3有需要的代码,图2.8展示了结果


@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *coneView;
@property (nonatomic, weak) IBOutlet UIView *shipView;
@property (nonatomic, weak) IBOutlet UIView *iglooView;
@property (nonatomic, weak) IBOutlet UIView *anchorView;
@end

@implementation ViewController

- (void)addSpriteImage:(UIImage *)image withContentRect:(CGRect)rect toLayer:(CALayer *)layer //set image
{
layer.contents = (__bridge id)image.CGImage;

//scale contents to fit
layer.contentsGravity = kCAGravityResizeAspect;

//set contentsRect
layer.contentsRect = rect;
}

- (void)viewDidLoad
{
[super viewDidLoad]; //load sprite sheet
UIImage *image = [UIImage imageNamed:@"Sprites.png"];
//set igloo sprite
[self addSpriteImage:image withContentRect:CGRectMake(0, 0, 0.5, 0.5) toLayer:self.iglooView.layer];
//set cone sprite
[self addSpriteImage:image withContentRect:CGRectMake(0.5, 0, 0.5, 0.5) toLayer:self.coneView.layer];
//set anchor sprite
[self addSpriteImage:image withContentRect:CGRectMake(0, 0.5, 0.5, 0.5) toLayer:self.anchorView.layer];
//set spaceship sprite
[self addSpriteImage:image withContentRect:CGRectMake(0.5, 0.5, 0.5, 0.5) toLayer:self.shipView.layer];
}
@end

图2.8

拼合不仅给app提供了一个整洁的载入方式,还有效地提高了载入性能(单张大图比多张小图载入地更快),但是如果有手动安排的话,他们还是有一些不方便的,如果你需要在一个已经创建好的品和图上做一些尺寸上的修改或者其他变动,无疑是比较麻烦的。

Mac上有一些商业软件可以为你自动拼合图片,这些工具自动生成一个包含拼合后的坐标的XML或者plist文件,拼合图片的使用大大简化。这个文件可以和图片一同载入,并给每个拼合的图层设置contentsRect,这样开发者就不用手动写代码来摆放位置了。

这些文件通常在OpenGL游戏中使用,不过呢,你要是有兴趣在一些常见的app中使用拼合技术,那么一个叫做LayerSprites的开源库(https://github.com/nicklockwood/LayerSprites),它能够读取Cocos2D格式中的拼合图并在普通的Core Animation层中显示出来。

contentsCenter

本章我们介绍的最后一个和内容有关的属性是contentsCenter,看名字你可能会以为它可能跟图片的位置有关,不过这名字着实误导了你。contentsCenter其实是一个CGRect,它定义了一个固定的边框和一个在图层上可拉伸的区域。 改变contentsCenter的值并不会影响到寄宿图的显示,除非这个图层的大小改变了,你才看得到效果。

默认情况下,contentsCenter是{0, 0, 1, 1},这意味着如果大小(由conttensGravity决定)改变了,那么寄宿图将会均匀地拉伸开。但是如果我们增加原点的值并减小尺寸。我们会在图片的周围创造一个边框。图2.9展示了contentsCenter设置为{0.25, 0.25, 0.5, 0.5}的效果。

图2.9

图2.9 contentsCenter的例子

这意味着我们可以随意重设尺寸,边框仍然会是连续的。他工作起来的效果和UIImage里的-resizableImageWithCapInsets: 方法效果非常类似,只是它可以运用到任何寄宿图,甚至包括在Core Graphics运行时绘制的图形(本章稍后会讲到)。

图2.10

图2.10 同一图片使用不同的contentsCenter

清单2.4 演示了如何编写这些可拉伸视图。不过,contentsCenter的另一个很酷的特性就是,它可以在Interface Builder里面配置,根本不用写代码。如图2.11

清单2.4 用contentsCenter设置可拉伸视图

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *button1;
@property (nonatomic, weak) IBOutlet UIView *button2;

@end

@implementation ViewController

- (void)addStretchableImage:(UIImage *)image withContentCenter:(CGRect)rect toLayer:(CALayer *)layer
{
//set image
layer.contents = (__bridge id)image.CGImage;

//set contentsCenter
layer.contentsCenter = rect;
}

- (void)viewDidLoad
{
[super viewDidLoad]; //load button image
UIImage *image = [UIImage imageNamed:@"Button.png"];

//set button 1
[self addStretchableImage:image withContentCenter:CGRectMake(0.25, 0.25, 0.5, 0.5) toLayer:self.button1.layer];

//set button 2
[self addStretchableImage:image withContentCenter:CGRectMake(0.25, 0.25, 0.5, 0.5) toLayer:self.button2.layer];
}

@end

图2.11

图2.11 用Interface Builder 探测窗口控制contentsCenter属性

收起阅读 »

iOS 图层树 二

1.3使用图层    首先我们来创建一个简单的项目,来操纵一些layer的属性。打开Xcode,使用Single View Application模板创建一个工程。    在屏幕中...
继续阅读 »

1.3使用图层

    首先我们来创建一个简单的项目,来操纵一些layer的属性。打开Xcode,使用Single View Application模板创建一个工程。

    在屏幕中央创建一个小视图(大约200 X 200的尺寸),当然你可以手工编码,或者使用Interface Builder(随你方便)。确保你的视图控制器要添加一个视图的属性以便可以直接访问它。我们把它称作layerView

    运行项目,应该能在浅灰色屏幕背景中看见一个白色方块(图1.3),如果没看见,可能需要调整一下背景window或者view的颜色

图1.3

图1.3 灰色背景上的一个白色UIView

    这并没有什么令人激动的地方,我们来添加一个色块,在白色方块中间添加一个小的蓝色块。

    我们当然可以简单地在已经存在的UIView上添加一个子视图(随意用代码或者IB),但这不能真正学到任何关于图层的东西。

    于是我们来创建一个CALayer,并且把它作为我们视图相关图层的子图层。尽管UIView类的接口中暴露了图层属性,但是标准的Xcode项目模板并没有包含Core Animation相关头文件。所以如果我们不给项目添加合适的库,是不能够使用任何图层相关的方法或者访问它的属性。所以首先需要添加QuartzCore框架到Build Phases标签(图1.4),然后在vc的.m文件中引入库。

图1.4

图1.4 把QuartzCore库添加到项目

    之后就可以在代码中直接引用CALayer的属性和方法。在清单1.1中,我们用创建了一个CALayer,设置了它的backgroundColor属性,然后添加到layerView背后相关图层的子图层(这段代码的前提是通过IB创建了layerView并做好了连接),图1.5显示了结果。

清单1.1 给视图添加一个蓝色子图层

#import "ViewController.h"
#import
@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *layerView;

@end

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad];
//create sublayer
CALayer *blueLayer = [CALayer layer];
blueLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f);
blueLayer.backgroundColor = [UIColor blueColor].CGColor;
//add it to our view
[self.layerView.layer addSublayer:blueLayer];
}
@end

图1.5

图1.5 白色UIView内部嵌套的蓝色CALayer

    一个视图只有一个相关联的图层(自动创建),同时它也可以支持添加无数多个子图层,从清单1.1可以看出,你可以显示创建一个单独的图层,并且把它直接添加到视图关联图层的子图层。尽管可以这样添加图层,但往往我们只是见简单地处理视图,他们关联的图层并不需要额外地手动添加子图层。

    在Mac OS平台,10.8版本之前,一个显著的性能缺陷就是由于用了视图层级而不是单独在一个视图内使用CALayer树状层级。但是在iOS平台,使用轻量级的UIView类并没有显著的性能影响(当然在Mac OS 10.8之后,NSView的性能同样也得到很大程度的提高)。

    使用图层关联的视图而不是CALayer的好处在于,你能在使用所有CALayer底层特性的同时,也可以使用UIView的高级API(比如自动排版,布局和事件处理)。

    然而,当满足以下条件的时候,你可能更需要使用CALayer而不是UIView:

  • 开发同时可以在Mac OS上运行的跨平台应用
  • 使用多种CALayer的子类(见第六章,“特殊的图层“),并且不想创建额外的UIView去包封装它们所有
  • 做一些对性能特别挑剔的工作,比如对UIView一些可忽略不计的操作都会引起显著的不同(尽管如此,你可能会直接想使用OpenGL绘图)

    但是这些例子都很少见,总的来说,处理视图会比单独处理图层更加方便。

总结

    这一章阐述了图层的树状结构,说明了如何在iOS中由UIView的层级关系形成的一种平行的CALayer层级关系,在后面的实验中,我们创建了自己的CALayer,并把它添加到图层树中。

    在第二章,“图层关联的图片”,我们将要研究一下CALayer关联的图片,以及Core Animation提供的操作显示的一些特性。

收起阅读 »

iOS 图层树 一

1.1图层与视图    如果你曾经在iOS或者Mac OS平台上写过应用程序,你可能会对视图的概念比较熟悉。一个视图就是在屏幕上显示的一个矩形块(比如图片,文字或者视频),它能够拦截类似于鼠标点击或者触摸手势等用户输入。...
继续阅读 »

1.1图层与视图

    如果你曾经在iOS或者Mac OS平台上写过应用程序,你可能会对视图的概念比较熟悉。一个视图就是在屏幕上显示的一个矩形块(比如图片,文字或者视频),它能够拦截类似于鼠标点击或者触摸手势等用户输入。视图在层级关系中可以互相嵌套,一个视图可以管理它的所有子视图的位置。图1.1显示了一种典型的视图层级关系

图1.1

图1.1 一种典型的iOS屏幕(左边)和形成视图的层级关系(右边)

  在iOS当中,所有的视图都从一个叫做UIVIew的基类派生而来,UIView可以处理触摸事件,可以支持基于Core Graphics绘图,可以做仿射变换(例如旋转或者缩放),或者简单的类似于滑动或者渐变的动画。

CALayer

CALayer类在概念上和UIView类似,同样也是一些被层级关系树管理的矩形块,同样也可以包含一些内容(像图片,文本或者背景色),管理子图层的位置。它们有一些方法和属性用来做动画和变换。和UIView最大的不同是CALayer不处理用户的交互。

CALayer并不清楚具体的响应链(iOS通过视图层级关系用来传送触摸事件的机制),于是它并不能够响应事件,即使它提供了一些方法来判断是否一个触点在图层的范围之内(具体见第三章,“图层的几何学”)

平行的层级关系

    每一个UIview都有一个CALayer实例的图层属性,也就是所谓的backing layer,视图的职责就是创建并管理这个图层,以确保当子视图在层级关系中添加或者被移除的时候,他们关联的图层也同样对应在层级关系树当中有相同的操作(见图1.2)。 图1.2 图1.2 图层的树状结构(左边)以及对应的视图层级(右边)

    实际上这些背后关联的图层才是真正用来在屏幕上显示和做动画,UIView仅仅是对它的一个封装,提供了一些iOS类似于处理触摸的具体功能,以及Core Animation底层方法的高级接口。

    但是为什么iOS要基于UIViewCALayer提供两个平行的层级关系呢?为什么不用一个简单的层级来处理所有事情呢?原因在于要做职责分离,这样也能避免很多重复代码。在iOS和Mac OS两个平台上,事件和用户交互有很多地方的不同,基于多点触控的用户界面和基于鼠标键盘有着本质的区别,这就是为什么iOS有UIKit和UIView,但是Mac OS有AppKit和NSView的原因。他们功能上很相似,但是在实现上有着显著的区别。

    绘图,布局和动画,相比之下就是类似Mac笔记本和桌面系列一样应用于iPhone和iPad触屏的概念。把这种功能的逻辑分开并应用到独立的Core Animation框架,苹果就能够在iOS和Mac OS之间共享代码,使得对苹果自己的OS开发团队和第三方开发者去开发两个平台的应用更加便捷。

    实际上,这里并不是两个层级关系,而是四个,每一个都扮演不同的角色,除了视图层级和图层树之外,还存在呈现树渲染树,将在第七章“隐式动画”和第十二章“性能调优”分别讨论。

1.2图层的能力

    如果说CALayerUIView内部实现细节,那我们为什么要全面地了解它呢?苹果当然为我们提供了优美简洁的UIView接口,那么我们是否就没必要直接去处理Core Animation的细节了呢?

    某种意义上说的确是这样,对一些简单的需求来说,我们确实没必要处理CALayer,因为苹果已经通过UIView的高级API间接地使得动画变得很简单。

    但是这种简单会不可避免地带来一些灵活上的缺陷。如果你略微想在底层做一些改变,或者使用一些苹果没有在UIView上实现的接口功能,这时除了介入Core Animation底层之外别无选择。

    我们已经证实了图层不能像视图那样处理触摸事件,那么他能做哪些视图不能做的呢?这里有一些UIView没有暴露出来的CALayer的功能:

  • 阴影,圆角,带颜色的边框
  • 3D变换
  • 非矩形范围
  • 透明遮罩
  • 多级非线性动画

    我们将会在后续章节中探索这些功能,首先我们要关注一下在应用程序当中CALayer是怎样被利用起来的。

收起阅读 »

kotlin 作用域函数

在Kotlin标准库(Standard.kt)中定义了几个作用域函数,其中包含let、run、with、apply和also。这几个函数有一个共同点就是在一个对象的上下文中执行代码块。 当对一个对象调用一个函数并提供一个lambda表达式时,它会形成一...
继续阅读 »

在Kotlin标准库(Standard.kt)中定义了几个作用域函数,其中包含letrunwithapplyalso。这几个函数有一个共同点就是在一个对象的上下文中执行代码块。



当对一个对象调用一个函数并提供一个lambda表达式时,它会形成一个临时作用域。在此作用域中,可以访问该对象而无需其名称。这样的函数称之为作用域函数



这些函数使用起来比较相似,主要区别在于两个方面:



  • 应用上下文对象的方式

  • 返回值


let


public inline fun <T, R> T.let(block: (T) -> R): R 	

let声明为扩展函数,上下文对象作为lambda表达式的参数(默认为it,也可以自定义名称),返回值是lambda表达式的结果。


val result = "a".let {
123
// return@let 123
}
print(result) // 123

上面的代码会输出123,在lambda表达式中可以省略return语句,默认最后一行代码为返回值。


let函数经常用于对象非空执行代码块的情况。例如下面情况使用let是非常方便的。


val str: String? = "Hello" 
//processNonNullString(str) // 编译错误:str 可能为空
val length = str?.let {
println("let() called on $it")
processNonNullString(it) // 编译通过:'it' 在 '?.let { }' 中必不为空
it.length
}

当运行时str不为空才会执行let后面的代码块,相比Java中需要对str进行非空判断就非常便捷了。


run


public inline fun <R> run(block: () -> R): R 

public inline fun <T, R> T.run(block: T.() -> R): R

再标准库中定义了两个run函数,其中第一个run函数可以独立运行一个代码块,并将lambda表达式的返回值作为run函数的返回值。例如:


val hexNumberRegex = run {
val digits = "0-9"
val hexDigits = "A-Fa-f"
val sign = "+-"

Regex("[$sign]?[$digits$hexDigits]+")
}

for (match in hexNumberRegex.findAll("+1234 -FFFF not-a-number")) {
println(match.value)
}

第二个run函数是一个扩展函数,上下文对象作为接收者(this) 来访问,返回值是lambda表达式结果。


val service = MultiportService("https://example.kotlinlang.org", 80)

val result = service.run {
port = 8080
query(prepareRequest() + " to port $port")
}

// 同样的代码如果用 let() 函数来写:
val letResult = service.let {
it.port = 8080
it.query(it.prepareRequest() + " to port ${it.port}")
}

可以看出这个run函数与let类似,区别在于run中可以直接使用上下文对象的属性和方法,而let需要通过it来调用上下文对象的属性和方法。


with


public inline fun <T, R> with(receiver: T, block: T.() -> R): R

with函数是一个非扩展函数,将上下文对象作为参数传递,并接收一个lambda表达式,在lambda表达式内部可以直接引用上下文对象的属性和方法,并将lambda表达式结果作为with函数的返回值。


val numbers = mutableListOf("one", "two", "three")
with(numbers) {
println("'with' is called with argument $this")
println("It contains $size elements")
}

with函数可以理解为“对于这个对象执行以下操作”。在使用with函数时建议使用 with 来调用上下文对象上的函数,而不使用 lambda 表达式结果。


apply


public inline fun <T> T.apply(block: T.() -> Unit): T

apply函数是一个扩展函数,上下文对象 作为接收者(this)来访问。 返回值 是上下文对象本身。


apply 的常见情况是对象配置。这样的调用可以理解为“将以下赋值操作应用于对象”。


val adam = Person("Adam").apply {
age = 32
city = "London"
}
println(adam)

also


public inline fun <T> T.also(block: (T) -> Unit): T

also函数是一个扩展函数,上下文对象作为 lambda 表达式的参数(it)来访问。 返回值是上下文对象本身。


also 对于执行一些将上下文对象作为参数的操作很有用。 对于需要引用对象而不是其属性与函数的操作,或者不想屏蔽来自外部作用域的 this 引用时,请使用 also


当你在代码中看到 also 时,可以将其理解为“并且用该对象执行以下操作”。


val numbers = mutableListOf("one", "two", "three")
numbers
.also { println("The list elements before adding new one: $it") }
.add("four")

总结


对于各个函数之间的区别可以参考下面的表格。根据使用场景选择合适的函数。

















































函数 对象引用 返回值 是否时扩展函数
let it Lambda 表达式结果
run this Lambda 表达式结果
run - Lambda 表达式结果 不是:调用无需上下文对象
with this Lambda 表达式结果 不是:把上下文对象当做参数
apply this 上下文对象
also it 上下文对象

以下是根据预期目的选择作用域函数的简短指南:



  • 对一个非空(non-null)对象执行 lambda 表达式:let

  • 将表达式作为变量引入为局部作用域中:let

  • 对象配置:apply

  • 对象配置并且计算结果:run

  • 在需要表达式的地方运行语句:非扩展的 run

  • 附加效果:also

  • 一个对象的一组函数调用:with


参考


Kotlin语言中文站


收起阅读 »

Recyclerview EditText 引发的问题与解决方案

问题 我使用简单的魔法就让各位大佬的财富值减少了,我们来看看谷歌公司是怎样做到的。 我们知道 Recyclerview 是有复用机制的,一般复用的个数是一个屏幕多一点的数量,比如我这里就是 16 。 默认情况,找到产生问题的原因 也就是我们不做任何...
继续阅读 »



问题


GIF 2021-8-5 7-27-00.gif


我使用简单的魔法就让各位大佬的财富值减少了,我们来看看谷歌公司是怎样做到的。


我们知道 Recyclerview 是有复用机制的,一般复用的个数是一个屏幕多一点的数量,比如我这里就是 16


默认情况,找到产生问题的原因


也就是我们不做任何修改,只看文本监听里面输出内容看看打印的日志,先看监听代码:


input?.addTextChangedListener(object : TextWatcher {
init {
Log.i(TAG, "Holder init: -------------------------------")
}
override fun beforeTextChanged(s: CharSequence?,start: Int,count: Int,after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
Log.i(TAG, "onTextChanged ${d.name}: $s")
}
override fun afterTextChanged(s: Editable?) {}
})

现在我们看初始化的日志:


I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: Holder init: -------------------------------

现在我滑动到底看看对应的日志输出:


I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 杰夫·贝索斯: 618
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 埃隆·马斯克: 602
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 伯纳德·阿尔诺及家族: 595
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 比尔·盖茨: 590
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 马克·扎克伯格: 553
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 沃伦·巴菲特: 530
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 拉里·埃里森: 519
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 拉里·佩奇: 505
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 谢尔盖·布林: 499
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 穆克什·安巴尼: 484
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 阿曼西奥·奥特加: 464
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 弗朗索瓦丝·贝当古·迈耶斯及家族: 464
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 钟睒睒: 454
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 史蒂夫·鲍尔默: 451
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 马化腾: 441
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 杰夫·贝索斯: 418
I/吴敬悦: onTextChanged 艾丽斯·沃尔顿: 418
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 埃隆·马斯克: 392
I/吴敬悦: onTextChanged 吉姆·沃尔顿: 392
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 伯纳德·阿尔诺及家族: 390
I/吴敬悦: onTextChanged 罗伯·沃尔顿: 390
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 比尔·盖茨: 382
I/吴敬悦: onTextChanged 迈克尔·布隆伯格: 382
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 马克·扎克伯格: 377
I/吴敬悦: onTextChanged 黄峥: 377
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 沃伦·巴菲特: 369
I/吴敬悦: onTextChanged 麦肯齐·斯科特: 369
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 拉里·埃里森: 356
I/吴敬悦: onTextChanged 丹尼尔·吉尔伯特: 356
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 拉里·佩奇: 351
I/吴敬悦: onTextChanged 高塔姆·阿达尼及家族: 351
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 谢尔盖·布林: 345
I/吴敬悦: onTextChanged 菲尔·耐特及家族: 345
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 穆克什·安巴尼: 345
I/吴敬悦: onTextChanged 马云: 345
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 阿曼西奥·奥特加: 337
I/吴敬悦: onTextChanged 查尔斯·科赫: 337
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 弗朗索瓦丝·贝当古·迈耶斯及家族: 335
I/吴敬悦: onTextChanged 茱莉亚·科赫及家族: 335
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 卡洛斯·斯利姆·埃卢及家族: 330
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 史蒂夫·鲍尔默: 320
I/吴敬悦: onTextChanged 迈克尔·戴尔: 320
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 马化腾: 317
I/吴敬悦: onTextChanged 柳井正及家族: 317
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 弗朗索瓦·皮诺特及家族: 313
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 杰夫·贝索斯: 313
I/吴敬悦: onTextChanged 艾丽斯·沃尔顿: 313
I/吴敬悦: onTextChanged 大卫·汤姆森及家族: 313
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 埃隆·马斯克: 296
I/吴敬悦: onTextChanged 吉姆·沃尔顿: 296
I/吴敬悦: onTextChanged 贝亚特·海斯特和小卡尔·阿尔布雷希特: 296
I/吴敬悦: Holder init: -------------------------------

可以看到默认情况下是只执行实例化的操作,而文本改变的监听却没有,那我滚动的时候发现文本监听触发了,其实我并没有改变文本,那为啥会这样子呢,当然就是因为复用导致的,由于复用所以原本已经被赋值的还会被赋值,这个时候就会触发文本改变监听,同时由于每一个监听器被多个数据使用,所以这里的财富所对应的名字也是不同的。在滑动过程中我们也发现 TextWatcher 被多次实例化,但又不是跟数据条数所对应。我们知道如果 TextWatcher 的个数跟数据量相同,是不是就可以解决数据乱的问题呢,我们尝试让每一项数据都有独一无二的 TextWatcher


我新建了一个类:


class OwnTextWatcher(private val name: String): TextWatcher {
init {
Log.i(Adapter.TAG, "init name: $name-------------------")
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}

override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
Log.i(Adapter.TAG, "onTextChanged ${name}: $s")
}

override fun afterTextChanged(s: Editable?) {}
}

初始化的日志:


I/吴敬悦: init name: 杰夫·贝索斯-------------------
I/吴敬悦: init name: 埃隆·马斯克-------------------
I/吴敬悦: init name: 伯纳德·阿尔诺及家族-------------------
I/吴敬悦: init name: 比尔·盖茨-------------------
I/吴敬悦: init name: 马克·扎克伯格-------------------
I/吴敬悦: init name: 沃伦·巴菲特-------------------
I/吴敬悦: init name: 拉里·埃里森-------------------
I/吴敬悦: init name: 拉里·佩奇-------------------
I/吴敬悦: init name: 谢尔盖·布林-------------------
I/吴敬悦: init name: 穆克什·安巴尼-------------------
I/吴敬悦: init name: 阿曼西奥·奥特加-------------------
I/吴敬悦: init name: 弗朗索瓦丝·贝当古·迈耶斯及家族-------------------

这是的个数刚好差不多是一屏的数量,再看我滑动的日志输出:


I/吴敬悦: init name: 钟睒睒-------------------
I/吴敬悦: init name: 史蒂夫·鲍尔默-------------------
I/吴敬悦: init name: 马化腾-------------------
I/吴敬悦: init name: 卡洛斯·斯利姆·埃卢及家族-------------------
I/吴敬悦: onTextChanged 杰夫·贝索斯: 618
I/吴敬悦: init name: 艾丽斯·沃尔顿-------------------
I/吴敬悦: onTextChanged 埃隆·马斯克: 602
I/吴敬悦: init name: 吉姆·沃尔顿-------------------
I/吴敬悦: onTextChanged 伯纳德·阿尔诺及家族: 595
I/吴敬悦: init name: 罗伯·沃尔顿-------------------
I/吴敬悦: onTextChanged 比尔·盖茨: 590
I/吴敬悦: init name: 迈克尔·布隆伯格-------------------
I/吴敬悦: onTextChanged 马克·扎克伯格: 553
I/吴敬悦: init name: 黄峥-------------------
I/吴敬悦: onTextChanged 沃伦·巴菲特: 530
I/吴敬悦: init name: 麦肯齐·斯科特-------------------
I/吴敬悦: onTextChanged 拉里·埃里森: 519
I/吴敬悦: init name: 丹尼尔·吉尔伯特-------------------
I/吴敬悦: onTextChanged 拉里·佩奇: 505
I/吴敬悦: init name: 高塔姆·阿达尼及家族-------------------
I/吴敬悦: onTextChanged 谢尔盖·布林: 499
I/吴敬悦: init name: 菲尔·耐特及家族-------------------
I/吴敬悦: init name: 马云-------------------
I/吴敬悦: onTextChanged 阿曼西奥·奥特加: 464
I/吴敬悦: init name: 查尔斯·科赫-------------------
I/吴敬悦: onTextChanged 弗朗索瓦丝·贝当古·迈耶斯及家族: 464
I/吴敬悦: init name: 茱莉亚·科赫及家族-------------------
I/吴敬悦: onTextChanged 钟睒睒: 454
I/吴敬悦: init name: 孙正义-------------------
I/吴敬悦: onTextChanged 史蒂夫·鲍尔默: 451
I/吴敬悦: init name: 迈克尔·戴尔-------------------
I/吴敬悦: onTextChanged 马化腾: 441
I/吴敬悦: init name: 柳井正及家族-------------------
I/吴敬悦: onTextChanged 卡洛斯·斯利姆·埃卢及家族: 423
I/吴敬悦: init name: 弗朗索瓦·皮诺特及家族-------------------
I/吴敬悦: onTextChanged 穆克什·安巴尼: 418
I/吴敬悦: init name: 大卫·汤姆森及家族-------------------
I/吴敬悦: onTextChanged 埃隆·马斯克: 392
I/吴敬悦: onTextChanged 吉姆·沃尔顿: 392
I/吴敬悦: init name: 贝亚特·海斯特和小卡尔·阿尔布雷希特-------------------
I/吴敬悦: onTextChanged 杰夫·贝索斯: 390
I/吴敬悦: onTextChanged 艾丽斯·沃尔顿: 390
I/吴敬悦: init name: 王卫-------------------
I/吴敬悦: onTextChanged 比尔·盖茨: 382
I/吴敬悦: onTextChanged 迈克尔·布隆伯格: 382
I/吴敬悦: init name: 米丽娅姆·阿德尔森-------------------
I/吴敬悦: onTextChanged 马克·扎克伯格: 377
I/吴敬悦: onTextChanged 黄峥: 377
I/吴敬悦: init name: 何享健及家族-------------------
I/吴敬悦: onTextChanged 沃伦·巴菲特: 369
I/吴敬悦: onTextChanged 麦肯齐·斯科特: 369
I/吴敬悦: init name: 迪特尔·施瓦茨-------------------
I/吴敬悦: onTextChanged 拉里·埃里森: 356
I/吴敬悦: onTextChanged 丹尼尔·吉尔伯特: 356
I/吴敬悦: init name: 张一鸣-------------------
I/吴敬悦: onTextChanged 拉里·佩奇: 351
I/吴敬悦: onTextChanged 高塔姆·阿达尼及家族: 351
I/吴敬悦: init name: 乔瓦尼·费列罗-------------------
I/吴敬悦: onTextChanged 谢尔盖·布林: 345
I/吴敬悦: onTextChanged 菲尔·耐特及家族: 345
I/吴敬悦: init name: 阿兰·韦特海默-------------------
I/吴敬悦: onTextChanged 马云: 345
I/吴敬悦: init name: 杰拉德·韦特海默-------------------
I/吴敬悦: onTextChanged 阿曼西奥·奥特加: 337
I/吴敬悦: onTextChanged 查尔斯·科赫: 337
I/吴敬悦: init name: 李嘉诚-------------------
I/吴敬悦: onTextChanged 弗朗索瓦丝·贝当古·迈耶斯及家族: 335
I/吴敬悦: onTextChanged 茱莉亚·科赫及家族: 335
I/吴敬悦: init name: 秦英林-------------------
I/吴敬悦: onTextChanged 钟睒睒: 330
I/吴敬悦: onTextChanged 孙正义: 330
I/吴敬悦: init name: 丁磊-------------------
I/吴敬悦: onTextChanged 史蒂夫·鲍尔默: 320
I/吴敬悦: onTextChanged 迈克尔·戴尔: 320
I/吴敬悦: init name: 莱恩·布拉瓦特尼克-------------------
I/吴敬悦: onTextChanged 马化腾: 317
I/吴敬悦: onTextChanged 柳井正及家族: 317
I/吴敬悦: init name: 李兆基-------------------
I/吴敬悦: onTextChanged 卡洛斯·斯利姆·埃卢及家族: 313
I/吴敬悦: onTextChanged 弗朗索瓦·皮诺特及家族: 313
I/吴敬悦: init name: 杰奎琳·马尔斯-------------------
I/吴敬悦: onTextChanged 穆克什·安巴尼: 313
I/吴敬悦: onTextChanged 大卫·汤姆森及家族: 313
I/吴敬悦: init name: 约翰·马尔斯-------------------
I/吴敬悦: onTextChanged 埃隆·马斯克: 296
I/吴敬悦: onTextChanged 吉姆·沃尔顿: 296
I/吴敬悦: onTextChanged 贝亚特·海斯特和小卡尔·阿尔布雷希特: 296
I/吴敬悦: init name: 杨惠妍及家族-------------------

我核对了初始化的数量,发现跟数据是相同的,说明的确初始化了这么多,那为啥还是有这种现象呢,我们知道的是其实输入框的节点对象并不是跟数据量相同,而是要看复用了多少,其实对于一个手机来说基本上每次初始化相同列表所实例化的是相同或相似的(我没有验证)。既然如此那么即便我们 TextWatcher 的数量是跟数据量相同,但由于本身 EditText 的数量就只有那么几个,要同时保存那么多数量的 TextWatcher 是不现实的,如果真要保存,那么只能是一个 EditText 实例保存了多份 TextWatcher 。我们可以去看一下 addTextChangedListener 的源码:


public void addTextChangedListener(TextWatcher watcher) {
if (mListeners == null) {
mListeners = new ArrayList<TextWatcher>();
}

mListeners.add(watcher);
}

我们发现果然是添加,并不是替换,也就是一个 EditText 是可以对应多个 TextWatcher 的,于是我就想为啥这样设计呢,其实我觉得原因就是有这样的需求,就是有可能一个文本的改变有多处监听,这也是普遍的需求。我们假设如果这个地方只有一个监听,也就是一对一的关系,那么我们这里是不是可以实现我们想要的功能呢,答案的否定的,如果真是这样的话,那么只会有那么几个是有效的,而且当后面的监听把前面的代替以后,前面的压根就不能正常工作。


寻找我们想要的答案


根据前面的分析与理解,我们知道产生这种问题的原因,现在我们的目标就是对症下药。


我们知道总是只有那么几个实例,只要数量多到达到复用的情况,那么就会出现一对多的情况,其实在复用的情况下我们是不希望一对多的,毕竟我们改变一个的时候就是改变一个,既然这样,那么我们可以尝试让监听的数量刚好跟 EditText 的实例数量相同;我们知道每一个 RecyclerView.ViewHolder 实例化都会执行 init ,而且在这里面总是跟 RecyclerView.ViewHolder 的数量相同,所以我们把监听的工作放到这里面进行。但是又会出现一个新的问题,也就是刚才我们说的如果只有一个的话,那么数据的一对一怎么保证呢,我想到一个方法,因为我们知道数据每一次渲染都会执行 onBindViewHolder 这个函数,也就是每一次数据都会在这里改变,那么我使用一个全局的变量保存数据,只要执行了 onBindViewHolder 这个函数,那么就更新数据,这样就解决问题了,下面看代码:


class Adapter: RecyclerView.Adapter<Adapter.Holder>() {
companion object {
const val TAG = "吴敬悦"
private var currentData: Data? = null
}
var list: ArrayList<Data> = arrayListOf()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_test_adapter, parent, false)
return Holder(view)
}

override fun onBindViewHolder(holder: Holder, position: Int) {
holder.bind(list[position], position)
}

override fun getItemCount(): Int = list.size

inner class Holder(view: View): RecyclerView.ViewHolder(view) {
private var text: TextView? = null
private var input: EditText? = null
init {
text = view.findViewById(R.id.titleText)
input = view.findViewById(R.id.input)
input?.addTextChangedListener(object : TextWatcher {
init {
Log.i(TAG, "Holder init: -------------------------------")
}
override fun beforeTextChanged(s: CharSequence?,start: Int,count: Int,after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
Log.i(TAG, "onTextChanged ${currentData?.name}: $s")
}
override fun afterTextChanged(s: Editable?) {}
})
}
fun bind(d: Data, position: Int) {
currentData = d
text?.text = d.name
input?.setText(d.wealth.toString())
}
}
}

下面看一下日志输出,当初始化时:


I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 杰夫·贝索斯: 1770
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 埃隆·马斯克: 1510
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 伯纳德·阿尔诺及家族: 1500
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 比尔·盖茨: 1240
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 马克·扎克伯格: 970
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 沃伦·巴菲特: 960
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 拉里·埃里森: 930
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 拉里·佩奇: 915
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 谢尔盖·布林: 890
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 穆克什·安巴尼: 845
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 阿曼西奥·奥特加: 770
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 弗朗索瓦丝·贝当古·迈耶斯及家族: 736

当我们滑到底的日志:


I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 钟睒睒: 689
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 史蒂夫·鲍尔默: 687
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 马化腾: 658
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 卡洛斯·斯利姆·埃卢及家族: 628
I/吴敬悦: onTextChanged 艾丽斯·沃尔顿: 618
I/吴敬悦: onTextChanged 吉姆·沃尔顿: 602
I/吴敬悦: onTextChanged 罗伯·沃尔顿: 595
I/吴敬悦: onTextChanged 迈克尔·布隆伯格: 590
I/吴敬悦: onTextChanged 黄峥: 553
I/吴敬悦: onTextChanged 麦肯齐·斯科特: 530
I/吴敬悦: onTextChanged 丹尼尔·吉尔伯特: 519
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 高塔姆·阿达尼及家族: 505
I/吴敬悦: onTextChanged 菲尔·耐特及家族: 499
I/吴敬悦: onTextChanged 马云: 484
I/吴敬悦: onTextChanged 查尔斯·科赫: 464
I/吴敬悦: onTextChanged 茱莉亚·科赫及家族: 464
I/吴敬悦: onTextChanged 孙正义: 454
I/吴敬悦: onTextChanged 迈克尔·戴尔: 451
I/吴敬悦: onTextChanged 柳井正及家族: 441
I/吴敬悦: onTextChanged 弗朗索瓦·皮诺特及家族: 423
I/吴敬悦: onTextChanged 大卫·汤姆森及家族: 418
I/吴敬悦: onTextChanged 贝亚特·海斯特和小卡尔·阿尔布雷希特: 392
I/吴敬悦: onTextChanged 王卫: 390
I/吴敬悦: onTextChanged 米丽娅姆·阿德尔森: 382
I/吴敬悦: onTextChanged 何享健及家族: 377
I/吴敬悦: onTextChanged 迪特尔·施瓦茨: 369
I/吴敬悦: onTextChanged 张一鸣: 356
I/吴敬悦: onTextChanged 乔瓦尼·费列罗: 351
I/吴敬悦: onTextChanged 阿兰·韦特海默: 345
I/吴敬悦: onTextChanged 杰拉德·韦特海默: 345
I/吴敬悦: onTextChanged 李嘉诚: 337
I/吴敬悦: onTextChanged 秦英林: 335
I/吴敬悦: onTextChanged 丁磊: 330
I/吴敬悦: onTextChanged 莱恩·布拉瓦特尼克: 320
I/吴敬悦: onTextChanged 李兆基: 317
I/吴敬悦: onTextChanged 杰奎琳·马尔斯: 313
I/吴敬悦: onTextChanged 约翰·马尔斯: 313
I/吴敬悦: onTextChanged 杨惠妍及家族: 296

我们发现达到了我们想要的目标

收起阅读 »

给Android应用设置DeviceOwner权限遇到的问题及解决方案

背景 Android手机品牌和型号众多,特别是国产手机系统时常添加各种中国特色功能,因此其与设备管理员模式的兼容性或多或少存在一些问题,今天专门来讲讲我遇到的一些常见机型兼容性问题。 注意事项 设备管理员模式不需要反复连接电脑设置,只需要配置一次,重启或...
继续阅读 »



背景


Android手机品牌和型号众多,特别是国产手机系统时常添加各种中国特色功能,因此其与设备管理员模式的兼容性或多或少存在一些问题,今天专门来讲讲我遇到的一些常见机型兼容性问题。


注意事项


设备管理员模式不需要反复连接电脑设置,只需要配置一次,重启或升级系统都没有影响。


但是在执行命令之前需要对手机进行一些设置,具体如下:



  • 小米用户需要开启「USB 调试(安全设置)」关闭「MIUI 优化」

  • 所有手机进入「设置 - 帐户」,删除所有的帐户,包括你的 Google、小米、华为、OPPO、vivo等系统帐号(像OPPO和vivo这样安装需要登录账户的可以之后再登录回来)

  • 如果你之前设置过多用户或开启过手机自带的访客模式、应用双开等,也需要一并关闭或删除(之后可以再打开)


常见问题


问题1:提示 “Not allowed to … already several accounts on the device”


说明手机上的账户没有删干净,这时候需要注销手机上的所有账户,包括 Google 账号和系统自带的如小米账号、华为账号、OPPO/vivo账号等,另外索尼手机需要拔掉 SIM 卡重启。


问题2:提示 “Not allowed to … already several users on the device”


说明手机的多用户或应用双开没删干净或者关闭,请删除或关闭所有的多用户、访客模式以及应用双开。


问题3:提示 “Trying to set the device owner, but device owner is already set.”


说明手机已经设置了其他 APP 为设备管理员,一台手机上只能有一个设备管理员。


问题4:MIUI 用户提示 “Neither user xxx nor current process has android.permission.MANAGE_DEVICE_ADMINS”


这个时候需要手动在系统设置- 开发者设置里开启「USB 调试(安全设置)」,如果任然不可以,那么就关闭 MIUI 优化重试。


问题5:尝试完以上步骤还是无法设置DeviceOwner权限


但是在有些机型上即便重置了手机,发现还是设置不了DeviceOwner权限,那就说明这台手机存在隐藏账户或者用户了,这时候我们可以通过adb命令来获取手机账户信息从而查看设置不了权限的原因。


查看手机账户(Account):


adb shell dumpsys account

图片


如果账户数目大于0,则请查看手机账户管理,是否有账户存在,存在的账户要退出或者删除;如果没有看到账户,那可能是隐藏账户,需要重置手机,然后再重新设置权限,如下图是重置手机后的结果:


图片


查看手机用户(User):


adb shell dumpsys user

图片


Android 6.0以后,设置DeviceOwner会检测手机里面user数目,如果大于1个则不能设置DeviceOwner权限。


问题6:手机重置之后仍然无法设置DeviceOwner权限


重置手机的时候需要注意,在系统初始化设置的时候,初始化界面上有一些选项(比如智能助手、智能桌面、用户体验计划等),能不选的就都别勾选,因为勾选了这些选项之后系统就会创建一个隐藏的账户。


问题7:提示:java.lang.IllegalStateException: Unexpected @ProvisioningPreCondition 99


这个问题暂时解决不了,据了解OPPO以及Realme 最新的几款机型已经修改了底层源码,不支持设置DeviceOwner了。


截至发稿,我已经在小米、红米、华为、荣耀、三星、魅族、一加、HTC、努比亚、vivo这几款主流机型上验证过了将近200个机型都是可以正常激活DeviceOwner权限的,另外早期的几款OPPO手机型号也是可以的。

收起阅读 »

Android 状态机源码解析

概述如果流程围绕失误的状态流转,这时候就要用到状态机,状态机描述一个事务,有多种状态,不同的动作作用再状态上导致抓状态的转换,这里面有三个重点状态 : 睡觉,工作,吃饭事件 : 起床,饥饿,疲惫动作 : 比如说闹铃触发了起床事件导致状态 从睡觉->工作(...
继续阅读 »

概述

如果流程围绕失误的状态流转,这时候就要用到状态机,状态机描述一个事务,有多种状态,不同的动作作用再状态上导致抓状态的转换,这里面有三个重点

  • 状态 : 睡觉,工作,吃饭
  • 事件 : 起床,饥饿,疲惫
  • 动作 : 比如说闹铃触发了起床事件导致状态 从睡觉->工作(可以省略)

总体就是,首先触发某个事件,导致了状态的改变, 闹铃触发起床事件,导致状态的改变睡觉-->工作

而Android中提供了状态机,在frameworks层源码frameworks/base/core/java/com/android/internal/util,如果项目中需要使用可以把对应的三个类拷贝到项目中StateMachine.java、State、IState

源码分析

IState

public interface IState {
/**
* Returned by processMessage to indicate the the message was processed.
* 由 processMessage 返回以指示消息已处理。
*/

static final boolean HANDLED = true;
/**
* Returned by processMessage to indicate the the message was NOT processed.
* 由 processMessage 返回以指示消息未被处理。
*/

static final boolean NOT_HANDLED = false;
/**
* Called when a state is entered.
* 进入状态时调用
*
*/

void enter();
/**
* Called when a state is exited.
* 退出一个状态时调用
*/

void exit();
/**
* Called when a message is to be processed by the
* state machine.
*
* This routine is never reentered thus no synchronization
* is needed as only one processMessage method will ever be
* executing within a state machine at any given time. This
* does mean that processing by this routine must be completed
* as expeditiously as possible as no subsequent messages will
* be processed until this routine returns.
*
* @param msg to process
* @return HANDLED if processing has completed and NOT_HANDLED
* if the message wasn't processed.
*/

boolean processMessage(Message msg);
/**
* Name of State for debugging purposes.
*
* @return name of state.返回状态的名字
*/

String getName();
}

状态的接口,定义了基本的方法,State实现了IState,我们自定义的状态需要直接继承State

StateMachine

构造方法

  private void initStateMachine(String name, Looper looper) {
mName = name;
mSmHandler = new SmHandler(looper, this);
}

protected StateMachine(String name) {
mSmThread = new HandlerThread(name);
mSmThread.start();
Looper looper = mSmThread.getLooper();
initStateMachine(name, looper);
}

public StateMachine(String name, Looper looper) {
initStateMachine(name, looper);
}

private void initStateMachine(String name, Looper looper) {
mName = name;
mSmHandler = new SmHandler(looper, this);
}

有三个构造方法,可以外部传入Looper,如果外部不传入就自动 new HandlerThread,最终创建SmHandler,他是StateMachine的内部类,他的角色相当于上面说的动作

addState

  private HashMap<State, StateInfo> mStateInfo =new HashMap<State, StateInfo>();

private final StateInfo addState(State state, State parent) {
if (mDbg) {
Log.d(TAG, "addStateInternal: E state=" + state.getName()
+ ",parent=" + ((parent == null) ? "" : parent.getName()));
}
StateInfo parentStateInfo = null;
if (parent != null) {
parentStateInfo = mStateInfo.get(parent);
if (parentStateInfo == null) {
// Recursively add our parent as it's not been added yet.
parentStateInfo = addState(parent, null);
}
}
StateInfo stateInfo = mStateInfo.get(state);
if (stateInfo == null) {
stateInfo = new StateInfo();
mStateInfo.put(state, stateInfo);
}
// Validate that we aren't adding the same state in two different hierarchies.
if ((stateInfo.parentStateInfo != null) &&
(stateInfo.parentStateInfo != parentStateInfo)) {
throw new RuntimeException("state already added");
}
stateInfo.state = state;
stateInfo.parentStateInfo = parentStateInfo;
stateInfo.active = false;
if (mDbg) Log.d(TAG, "addStateInternal: X stateInfo: " + stateInfo);
return stateInfo;
}


private class StateInfo {
/** The state */
State state;
/** The parent of this state, null if there is no parent */
StateInfo parentStateInfo;
/** True when the state has been entered and on the stack */
boolean active;
/**
* Convert StateInfo to string
*/

@Override
public String toString() {
return "state=" + state.getName() + ",active=" + active
+ ",parent=" + ((parentStateInfo == null) ?
"null" : parentStateInfo.state.getName());
}
}

像状态机添加状态,可以看到最外层使用HashMap存储key=State,value=StateInfo,而StateInfo中储存了当前状态和是否激活,和当前状态的父节点

image.png

假如说目前有六个状态,A->B->C 和 D->E->F,C是B的父节点,B是A的父节点

setInitialState 设置除初始化状态

 public final void setInitialState(State initialState) {
mSmHandler.setInitialState(initialState);
}

private final void setInitialState(State initialState) {
if (mDbg) Log.d(TAG, "setInitialState: initialState=" + initialState.getName());
mInitialState = initialState;
}

假如现在设的初始状态为 C

状态机开始

 public void start() {
// mSmHandler can be null if the state machine has quit.
if (mSmHandler == null) return;
/** Send the complete construction message */
mSmHandler.completeConstruction();
}

private final void completeConstruction() {
//首先拿到状态树的最大深度
int maxDepth = 0;
for (StateInfo si : mStateInfo.values()) {
int depth = 0;
for (StateInfo i = si; i != null; depth++) {
i = i.parentStateInfo;
}
if (maxDepth < depth) {
maxDepth = depth;
}
}
if (mDbg) Log.d(TAG, "completeConstruction: maxDepth=" + maxDepth);
//根据最大深度初始化状态栈,和临时状态栈
mStateStack = new StateInfo[maxDepth];
mTempStateStack = new StateInfo[maxDepth];
setupInitialStateStack();
/** Sending SM_INIT_CMD message to invoke enter methods asynchronously */
//发送初始化消息给Handler
sendMessageAtFrontOfQueue(obtainMessage(SM_INIT_CMD, mSmHandlerObj));
if (mDbg) Log.d(TAG, "completeConstruction: X");
}

//根据初始状态填充mTempStateStack 临时栈
private final void setupInitialStateStack() {
if (mDbg) {
Log.d(TAG, "setupInitialStateStack: E mInitialState="
+ mInitialState.getName());
}
StateInfo curStateInfo = mStateInfo.get(mInitialState);
for (mTempStateStackCount = 0; curStateInfo != null; mTempStateStackCount++) {
mTempStateStack[mTempStateStackCount] = curStateInfo;
curStateInfo = curStateInfo.parentStateInfo;
}
// Empty the StateStack
mStateStackTopIndex = -1;
moveTempStateStackToStateStack();
}

//然后把mTempStateStack翻转填充mStateStack
private final int moveTempStateStackToStateStack() {
int startingIndex = mStateStackTopIndex + 1;
int i = mTempStateStackCount - 1;
int j = startingIndex;
while (i >= 0) {
if (mDbg) Log.d(TAG, "moveTempStackToStateStack: i=" + i + ",j=" + j);
mStateStack[j] = mTempStateStack[i];
j += 1;
i -= 1;
}
mStateStackTopIndex = j - 1;
if (mDbg) {
Log.d(TAG, "moveTempStackToStateStack: X mStateStackTop="
+ mStateStackTopIndex + ",startingIndex=" + startingIndex
+ ",Top=" + mStateStack[mStateStackTopIndex].state.getName());
}
return startingIndex;
}

这里一共做了一下几件事情

  • 计算出状态树的最大深度
  • 根据最大深度初始化俩个数组
  • 然后根据初始的State 填充数组

image.png

此时数组状态,也就是说从mStateStack按照mStateStackTopIndex取出的状态是C

Handler处理初始化

public final void handleMessage(Message msg) {
if (mDbg) Log.d(TAG, "handleMessage: E msg.what=" + msg.what);
/** Save the current message */
mMsg = msg;
if (mIsConstructionCompleted) {
/** Normal path */
processMsg(msg);
} else if (!mIsConstructionCompleted &&
(mMsg.what == SM_INIT_CMD) && (mMsg.obj == mSmHandlerObj)) {
/** Initial one time path. */
//第一次初始化走这里
mIsConstructionCompleted = true;
invokeEnterMethods(0);
} else {
throw new RuntimeException("StateMachine.handleMessage: " +
"The start method not called, received msg: " + msg);
}
//处理状态的切换
performTransitions();
if (mDbg) Log.d(TAG, "handleMessage: X");
}

private final void invokeEnterMethods(int stateStackEnteringIndex) {
for (int i = stateStackEnteringIndex; i <= mStateStackTopIndex; i++) {
if (mDbg) Log.d(TAG, "invokeEnterMethods: " + mStateStack[i].state.getName());
mStateStack[i].state.enter();
mStateStack[i].active = true;
}
}

第一次初始化做了俩件事情

  • 首先把`mIsConstructionCompleted = true;
  • 然后把invokeEnterMethods(0)方法,由于传入的是0,所以把mStateStack中所有的状态都调用mStateStack[i].state.enter();mStateStack[i].active = true;全部激活

如果已经初始化完成了调用processMsg

   private final void processMsg(Message msg) {
//首先从mStateStack取出顶部状态
StateInfo curStateInfo = mStateStack[mStateStackTopIndex];
if (mDbg) {
Log.d(TAG, "processMsg: " + curStateInfo.state.getName());
}
if (isQuit(msg)) {
transitionTo(mQuittingState);
} else {
//调用状态的processMessage方法,如果没处理就调用父节点,如果父节点也不处理,就提跳出循环
while (!curStateInfo.state.processMessage(msg)) {
/**
* Not processed
*/

curStateInfo = curStateInfo.parentStateInfo;
if (curStateInfo == null) {
/**
* No parents left so it's not handled
*/

mSm.unhandledMessage(msg);
break;
}
if (mDbg) {
Log.d(TAG, "processMsg: " + curStateInfo.state.getName());
}
}

}

这个就做了俩件事情

  • 首先从mStateStack取出顶部状态(目前来说就是取出了C)
  • 调用State的processMessage方法,如果没处理就调用父节点,如果父节点也不处理,就提跳出循环

怎么切换状态呢?

 private final void transitionTo(IState destState) {
mDestState = (State) destState;
if (mDbg) Log.d(TAG, "transitionTo: destState=" + mDestState.getName());
}

用这个方法切换状态,参数就是目标状态,我们看到再handleMessage中,除了调用StateprocessMessage方法,还调用了performTransitions来处理状态的切换,看下这个方法

 private void performTransitions() {
/**
* If transitionTo has been called, exit and then enter
* the appropriate states. We loop on this to allow
* enter and exit methods to use transitionTo.
*/

State destState = null;
while (mDestState != null) {
if (mDbg) Log.d(TAG, "handleMessage: new destination call exit");
/**
* Save mDestState locally and set to null
* to know if enter/exit use transitionTo.
*/

destState = mDestState;
mDestState = null;
/**
* Determine the states to exit and enter and return the
* common ancestor state of the enter/exit states. Then
* invoke the exit methods then the enter methods.
*/

StateInfo commonStateInfo = setupTempStateStackWithStatesToEnter(destState);
invokeExitMethods(commonStateInfo);
int stateStackEnteringIndex = moveTempStateStackToStateStack();
invokeEnterMethods(stateStackEnteringIndex);
/**
* Since we have transitioned to a new state we need to have
* any deferred messages moved to the front of the message queue
* so they will be processed before any other messages in the
* message queue.
*/

moveDeferredMessageAtFrontOfQueue();
}

}

假如目标状态为F,先走setupTempStateStackWithStatesToEnter

 private final StateInfo setupTempStateStackWithStatesToEnter(State destState) {
/**
* Search up the parent list of the destination state for an active
* state. Use a do while() loop as the destState must always be entered
* even if it is active. This can happen if we are exiting/entering
* the current state.
*/

mTempStateStackCount = 0;
StateInfo curStateInfo = mStateInfo.get(destState);
do {
mTempStateStack[mTempStateStackCount++] = curStateInfo;
curStateInfo = curStateInfo.parentStateInfo;
} while ((curStateInfo != null) && !curStateInfo.active);
if (mDbg) {
Log.d(TAG, "setupTempStateStackWithStatesToEnter: X mTempStateStackCount="
+ mTempStateStackCount + ",curStateInfo: " + curStateInfo);
}
return curStateInfo;
}

这个就是按照顺序把destState和他的父节点依次填入mTempStateStack,这里返回值为null,因为新的状态都没有被激活过,此时mTemStateStack数据为

image.png

然后调用invokeExitMethods(commonStateInfo);

  private final void invokeExitMethods(StateInfo commonStateInfo) {
while ((mStateStackTopIndex >= 0) &&
(mStateStack[mStateStackTopIndex] != commonStateInfo)) {
State curState = mStateStack[mStateStackTopIndex].state;
if (mDbg) Log.d(TAG, "invokeExitMethods: " + curState.getName());
curState.exit();
mStateStack[mStateStackTopIndex].active = false;
mStateStackTopIndex -= 1;
}
}

这里表示把之前mStateStack数据exit,并且active = false,此时mStateStack状态为

image.png

接下来调用moveTempStateStackToStateStack

 private final int moveTempStateStackToStateStack() {
int startingIndex = mStateStackTopIndex + 1;
int i = mTempStateStackCount - 1;
int j = startingIndex;
while (i >= 0) {
if (mDbg) Log.d(TAG, "moveTempStackToStateStack: i=" + i + ",j=" + j);
mStateStack[j] = mTempStateStack[i];
j += 1;
i -= 1;
}
mStateStackTopIndex = j - 1;
if (mDbg) {
Log.d(TAG, "moveTempStackToStateStack: X mStateStackTop="
+ mStateStackTopIndex + ",startingIndex=" + startingIndex
+ ",Top=" + mStateStack[mStateStackTopIndex].state.getName());
}
return startingIndex;
}

这个就是把mTempStateStack翻转填充mStateStack,此时mStateStack状态为,此时返回值为0

image.png

最后调用

 private final void invokeEnterMethods(int stateStackEnteringIndex) {
for (int i = stateStackEnteringIndex; i <= mStateStackTopIndex; i++) {
if (mDbg) Log.d(TAG, "invokeEnterMethods: " + mStateStack[i].state.getName());
mStateStack[i].state.enter();
mStateStack[i].active = true;
}
}

把mStateStack中的状态激活,此时抓状态就装换完毕了,下次handle处理数据 StateInfo curStateInfo = mStateStack[mStateStackTopIndex]就是拿到的新的状态

这只是讨论其中一种情况切换到了状态F,如果切换到状态B呢?有些区差异,但基本差不多

使用


public class MyStateMachine extends StateMachine {


private static final String TAG = "mmm";

//设置状态改变事件
public static final int MSG_WAKEUP = 1; // 消息:醒
public static final int MSG_TIRED = 2; // 消息:困
public static final int MSG_HUNGRY = 3; // 消息:饿
private static final int MSG_HALTING = 4; // 状态机暂停消息

//创建状态
private State mBoringState = new BoringState();// 默认状态
private State mWorkState = new WorkState(); // 工作
private State mEatState = new EatState(); // 吃
private State mSleepState = new SleepState(); // 睡

/**
* 构造方法
*
* @param name
*/

MyStateMachine(String name) {
super(name);
//加入状态,初始化状态
addState(mBoringState, null);
addState(mSleepState, mBoringState);
addState(mWorkState, mBoringState);
addState(mEatState, mBoringState);

// sleep状态为初始状态
setInitialState(mSleepState);
}

/**
* @return 创建启动person 状态机
*/

public static MyStateMachine makePerson() {
MyStateMachine person = new MyStateMachine("Person");
person.start();
return person;
}


@Override
public void onHalting() {
synchronized (this) {
this.notifyAll();
}
}


/**
* 定义状态:无聊
*/

class BoringState extends State {
@Override
public void enter() {
Log.e(TAG, "############ enter Boring ############");
}

@Override
public void exit() {
Log.e(TAG, "############ exit Boring ############");
}

@Override
public boolean processMessage(Message msg) {
Log.e(TAG, "BoringState processMessage.....");
return true;
}
}

/**
* 定义状态:睡觉
*/

class SleepState extends State {
@Override
public void enter() {
Log.e(TAG, "############ enter Sleep ############");
}

@Override
public void exit() {
Log.e(TAG, "############ exit Sleep ############");
}

@Override
public boolean processMessage(Message msg) {
Log.e(TAG, "SleepState processMessage.....");
switch (msg.what) {
// 收到清醒信号
case MSG_WAKEUP:
Log.e(TAG, "SleepState MSG_WAKEUP");
// 进入工作状态
transitionTo(mWorkState);
//...
//...
//发送饿了信号...
sendMessage(obtainMessage(MSG_HUNGRY));
break;
case MSG_HALTING:
Log.e(TAG, "SleepState MSG_HALTING");

// 转化到暂停状态
transitionToHaltingState();
break;
default:
return false;
}
return true;
}
}


/**
* 定义状态:工作
*/

class WorkState extends State {
@Override
public void enter() {
Log.e(TAG, "############ enter Work ############");
}

@Override
public void exit() {
Log.e(TAG, "############ exit Work ############");
}

@Override
public boolean processMessage(Message msg) {
Log.e(TAG, "WorkState processMessage.....");
switch (msg.what) {
// 收到 饿了 信号
case MSG_HUNGRY:
Log.e(TAG, "WorkState MSG_HUNGRY");
// 吃饭状态
transitionTo(mEatState);
//...
//...
// 发送累了信号...
sendMessage(obtainMessage(MSG_TIRED));
break;
default:
return false;
}
return true;
}
}

/**
* 定义状态:吃
*/

class EatState extends State {
@Override
public void enter() {
Log.e(TAG, "############ enter Eat ############");
}

@Override
public void exit() {
Log.e(TAG, "############ exit Eat ############");
}

@Override
public boolean processMessage(Message msg) {
Log.e(TAG, "EatState processMessage.....");
switch (msg.what) {
// 收到 困了 信号
case MSG_TIRED:
Log.e(TAG, "EatState MSG_TIRED");
// 睡觉
transitionTo(mSleepState);
//...
//...
// 发出结束信号...
sendMessage(obtainMessage(MSG_HALTING));
break;
default:
return false;
}
return true;
}

}
}

调用

 	// 获取 状态机引用
MyStateMachine personStateMachine = MyStateMachine.makePerson();
// 初始状态为SleepState,发送消息MSG_WAKEUP
personStateMachine.sendMessage(MyStateMachine.MSG_WAKEUP);

日志

2021-08-12 18:20:03.137 6035-8981/com.example.myapplication E/mmm: ############ enter Boring ############
2021-08-12 18:20:03.137 6035-8981/com.example.myapplication E/mmm: ############ enter Sleep ############
2021-08-12 18:20:03.137 6035-8981/com.example.myapplication E/mmm: SleepState processMessage.....
2021-08-12 18:20:03.137 6035-8981/com.example.myapplication E/mmm: SleepState MSG_WAKEUP
2021-08-12 18:20:03.137 6035-8981/com.example.myapplication E/mmm: ############ exit Sleep ############
2021-08-12 18:20:03.137 6035-8981/com.example.myapplication E/mmm: ############ enter Work ############
2021-08-12 18:20:03.137 6035-8981/com.example.myapplication E/mmm: WorkState processMessage.....
2021-08-12 18:20:03.137 6035-8981/com.example.myapplication E/mmm: WorkState MSG_HUNGRY
2021-08-12 18:20:03.137 6035-8981/com.example.myapplication E/mmm: ############ exit Work ############
2021-08-12 18:20:03.137 6035-8981/com.example.myapplication E/mmm: ############ enter Eat ############
2021-08-12 18:20:03.137 6035-8981/com.example.myapplication E/mmm: EatState processMessage.....
2021-08-12 18:20:03.137 6035-8981/com.example.myapplication E/mmm: EatState MSG_TIRED
2021-08-12 18:20:03.137 6035-8981/com.example.myapplication E/mmm: ############ exit Eat ############
2021-08-12 18:20:03.137 6035-8981/com.example.myapplication E/mmm: ############ enter Sleep ############
2021-08-12 18:20:03.137 6035-8981/com.example.myapplication E/mmm: SleepState processMessage.....
2021-08-12 18:20:03.137 6035-8981/com.example.myapplication E/mmm: SleepState MSG_HALTING
2021-08-12 18:20:03.137 6035-8981/com.example.myapplication E/mmm: ############ exit Sleep ############
2021-08-12 18:20:03.137 6035-8981/com.example.myapplication E/mmm: ############ exit Boring ############

这里最重要的是要分清楚 状态和事件,首先触发某个事件,导致了状态的改变, 闹铃触发起床事件,导致状态的改变睡觉-->工作

这里首先把所有的状态都加入到了状态机,然后设置初始状态是为Sleep,然后就调用了start

所以开始就会把Sleep和其父节点加入状态栈中,然后调用enter,然后调用personStateMachine.sendMessage(MyStateMachine.MSG_WAKEUP);这里可以这样理解,sendMessage表示都动作,MyStateMachine.MSG_WAKEUP表示事件,然后SleepState接收事件情,触发状态的改变 transitionTo(mWorkState);

也就是说当前状态SleepState只接受接收事件MSG_WAKEUP,如果是其他事件,当前状态不接受,也就不会改变状态,比如当前状态时睡觉,触发事件吃饭,睡觉时不能吃饭,所以是个无效事件

收起阅读 »

Android 多返回栈技术详解

用户通过系统返回按钮导航回去的一组页面,在开发中被称为返回栈 (back stack)。多返回栈即一堆 "返回栈",对多返回栈的支持是在 Navigation 2.4.0-alpha01 和 Fragment 1.4.0-alpha01 中开始的。本文将为您展...
继续阅读 »

用户通过系统返回按钮导航回去的一组页面,在开发中被称为返回栈 (back stack)。多返回栈即一堆 "返回栈",对多返回栈的支持是在 Navigation 2.4.0-alpha01Fragment 1.4.0-alpha01 中开始的。本文将为您展开多返回栈的技术详解。


系统返回按钮的乐趣


无论您在使用 Android 全新的 手势导航 还是传统的导航栏,用户的 "返回" 操作是 Android 用户体验中关键的一环,把握好返回功能的设计可以使应用更加贴近整个生态系统。


在最简单的应用场景中,系统返回按钮仅仅 finish 您的 Activity。在过去您可能需要覆写 Activity 的 onBackPressed() 方法来自定义返回操作,而在 2021 年您无需再这样操作。我们已经在 OnBackPressedDispatcher 中提供了 针对自定义返回导航的 API。实际上这与 FragmentManagerNavController已经 添加的 API 相同。


这意味着当您使用 Fragments 或 Navigation 时,它们会通过 OnBackPressedDispatcher 来确保您调用了它们返回栈的 API,系统的返回按钮会将您推入返回栈的页面逐层返回。


多返回栈不会改变这个基本逻辑。系统的返回按钮仍然是一个单向指令 —— "返回"。这对多返回栈 API 的实现机制有深远影响。


Fragment 中的多返回栈


在 surface 层级,对于 多返回栈的支持 貌似很直接,但其实需要额外解释一下 "Fragment 返回栈" 到底是什么。FragmentManager 的返回栈其实包含的不是 Fragment,而是由 Fragment 事务组成的。更准确地说,是由那些调用了 addToBackStack(String name) API 的事务组成的。


这就意味着当您调用 commit() 提交了一个调用过 addToBackStack() 方法的 Fragment 事务时,FragmentManager 会执行所有您在事务中所指定的操作 (比如 替换操作),从而将每个 Fragment 转换为预期的状态。然后 FragmentManager 会将该事务作为它返回栈的一部分。


当您调用 popBackStack() 方法时 (无论是直接调用,还是通过系统返回键以 FragmentManager 内部机制调用),Fragment 返回栈的最上层事务会从栈中弹出 -- 比如新添加的 Fragment 会被移除,隐藏的 Fragment 会显示。这会使得 FragmentManager 恢复到最初提交 Fragment 事务之前的状态。



作者注: 这里有一个非常重要的事情需要大家注意,在同一个 FragmentManager 中绝对不应该将含有 addToBackStack() 的事务和不含的事务混在一起: 返回栈的事务无法察觉返回栈之外的 Fragment 事务的修改 —— 当您从堆栈弹出一个非常不确定的元素时,这些事务从下层替换出来的时候会撤销之前未添加到返回栈的修改。



也就是说 popBackStack() 变成了销毁操作: 任何已添加的 Fragment 在事务被弹出的时候都会丢失它的状态。换言之,您会失去视图的状态,任何所保存的实例状态 (Saved Instance State),并且任何绑定到该 Fragment 的 ViewModel 实例都会被清除。这也是该 API 和新的 saveBackStack() 方法之间的主要区别。saveBackStack() 可以实现弹出事务所实现的返回效果,此外它还可以确保视图状态、已保存的实例状态,以及 ViewModel 实例能够在销毁时被保存。这使得 restoreBackStack() API 后续可以通过已保存的状态重建这些事务和它们的 Fragment,并且高效 "重现" 已保存的全部细节。太神奇了!


而实现这个目的必须要解决大量技术上的问题。


排除 Fragment 在技术上的障碍


虽然 Fragment 总是会保存 Fragment 的视图状态,但是 Fragment 的 onSaveInstanceState() 方法只有在 Activity 的 onSaveInstanceState() 被调用时才会被调用。为了能够保证调用 saveBackStack() 时 SavedInstanceState 会被保存,我们 需要在 Fragment 生命周期切换 的正确时机注入对 onSaveInstanceState() 的调用。我们不能调用得太早 (您的 Fragment 不应该在 STARTED 状态下保存状态),也不能调用得太晚 (您需要在 Fragment 被销毁之前保存状态)。


这样的前提条件就开启了需要 解决 FragmentManager 转换到对应状态的问题,以此来保障有一个地方能够将 Fragment 转换为所需状态,并且处理可重入行为和 Fragment 内部的状态转换。


在 Fragment 的重构工作进行了 6 个月,进行了 35 次修改时,发现 Postponed Fragment 功能已经严重损坏,这一问题使得被推迟的事务处于一个中间状态 —— 既没有被提交也并不是未被提交。之后的 65 个修改和 5 个月的时间里,我们几乎重写了 FragmentManager 管理状态、延迟状态切换和动画的内部代码,具体请参见我们之前的文章《全新的 Fragment: 使用新的状态管理器》。


Fragment 中值得期待的地方


随着技术问题的逐步解决,包括更加可靠和更易理解的 FragmentManager,我们新增加了两个 API: saveBackStack()restoreBackStack()


如果您不使用这些新增 API,则一切照旧: 单个 FragmentManager 返回栈和之前的功能相同。现有的 addToBackStack() 保持不变 —— 您可以将 name 赋值为 null 或者任意 name。然而,当您使用多返回栈时,name 的作用就非常重要了: 在您调用 saveBackStack() 和之后的 restoreBackStack() 方法时,它将作为 Fragment 事务的唯一的 key。


举个例子,会更容易理解。比如您已经添加了一个初始的 Fragment 到 Activity,然后提交了两个事务,每个事务中包含一个单独的 replace 操作:


// 这是用户看到的初始的 Fragment
fragmentManager.commit {
setReorderingAllowed(true)
replace<HomeFragment>(R.id.fragment_container)
}
// 然后,响应用户操作,我们在返回栈中增加了两个事务
fragmentManager.commit {
setReorderingAllowed(true)
replace<ProfileFragment>(R.id.fragment_container)
addToBackStack(“profile”)
}
fragmentManager.commit {
setReorderingAllowed(true)
replace<EditProfileFragment>(R.id.fragment_container)
addToBackStack(“edit_profile”)
}

也就是说我们的 FragmentManager 会变成这样:


△ 提交三次之后的 FragmentManager 的状态


△ 提交三次之后的 FragmentManager 的状态


比如说我们希望将 profile 页换出返回栈,然后切换到通知 Fragment。这就需要调用 saveBackStack() 并且紧跟一个新的事务:


fragmentManager.saveBackStack("profile")
fragmentManager.commit {
setReorderingAllowed(true)
replace<NotificationsFragment>(R.id.fragment_container)
addToBackStack("notifications")
}

现在我们添加 ProfileFragment 的事务和添加 EditProfileFragment 的事务都保存在 "profile" 关键字下。这些 Fragment 已经完全将状态保存,并且 FragmentManager 会随同事务状态一起保持它们的状态。很重要的一点: 这些 Fragment 的实例并不在内存中或者在 FragmentManager 中 —— 存在的仅仅只有状态 (以及任何以 ViewModel 实例形式存在的非配置状态)。


△ 我们保存 profile 返回栈并且添加一个新的 commit 后的 FragmentManager 状态


△ 我们保存 profile 返回栈并且添加一个新的 commit 后的 FragmentManager 状态


替换回来非常简单: 我们可以在 "notifications" 事务中同样调用 saveBackStack() 操作,然后调用 restoreBackStack():


fragmentManager.saveBackStack(“notifications”)
fragmentManager.restoreBackStack(“profile”)

这两个堆栈项高效地交换了位置:


△ 交换堆栈项后的 FragmentManager 状


△ 交换堆栈项后的 FragmentManager 状态


维持一个单独且活跃的返回栈并且将事务在其中交换,这保证了当返回按钮被点击时,FragmentManager 和系统的其他部分可以保持一致的响应。实际上,整个逻辑并未改变,同之前一样,仍然弹出 Fragment 返回栈的最后一个事务。


这些 API 都特意按照最小化设计,尽管它们会产生潜在的影响。这使得开发者可以基于这些接口设计自己的结构,而无需通过任何非常规的方式保存 Fragment 的视图状态、已保存的实例状态、非配置的状态。


当然了,如果您不希望在这些 API 之上构建您的框架,那么可以使用我们所提供的框架进行开发。


使用 Navigation 将多返回栈适配到任意屏幕类型


Navigation Component 最初 是作为通用运行时组件进行开发的,其中不涉及 View、Fragment、Composable 或者其他屏幕显示相关类型及您可能会在 Activity 中实现的 "目的地界面"。然而,NavHost 接口 的实现中需要考虑这些内容,通过它添加一个或者多个 Navigator 实例时,这些实例 确实 清楚如何与特定类型的目的地进行交互。


这也就意味着与 Fragment 的交互逻辑全部封装在了 navigation-fragment 开发库和它其中的 FragmentNavigatorDialogFragmentNavigator 中。类似的,与 Composable 的交互逻辑被封装在完全独立的 navigation-compose 开发库和它的 ComposeNavigator 中。这里的抽象设计意味着如果您希望仅仅通过 Composable 构建您的应用,那么当您使用 Navigation Compose 时无需任何涉及到 Fragment 的依赖。


该级别的分离意味着 Navigation 中有两个层次来实现多返回栈:



  • 保存独立的 NavBackStackEntry 实例状态,这些实例组成了 NavController 返回栈。这是属于 NavController 的职责。

  • 保存 Navigator 针对每个 NavBackStackEntry 的特定状态 (比如与 FragmentNavigator 目的地相关联的 Fragment)。这是属于 Navigator 的职责。


仍需特别注意那些 尚未 更新的 Navigator,它们无法支持保存自身状态。底层的 Navigator API 已经整体重写来支持状态保存 (您需要覆写新增的 navigate()popBackStack() API 的重载方法,而不是覆写之前的版本),即使 Navigator 并未更新,NavController 仍会保存 NavBackStackEntry 的状态 (在 Jetpack 世界中向后兼容是非常重要的)。



备注: 通过绑定 TestNavigatorState 使其成为一个 mini-NavController 可以实现在新的 Navigator API 上更轻松、独立地测试您自定义的 Navigator



如果您仅仅在应用中使用 Navigation,那么 Navigator 这个层面更多的是实现细节,而不是您需要直接与之交互的内容。可以这么说,我们已经完成了将 FragmentNavigatorComposeNavigator 迁移到新的 Navigator API 的工作,使其能够正确地保存和恢复它们的状态,在这个层面上您无需再做任何额外工作。


在 Navigation 中启用多返回栈


如果您正在使用 NavigationUI,它是用于连接您的 NavController 到 Material 视图组件的一系列专用助手,您会发现对于菜单项、BottomNavigationView (现在叫 NavigationRailView) 和 NavigationView,多返回栈是 默认启用 的。这就意味着结合 navigation-fragmentnavigation-ui 使用就可以。


NavigationUI API 是基于 Navigation 的其他公共 API 构建的,确保您可以准确地为自定义组件构建您自己的版本。保证您可以构建所需的自定义组件。启用保存和恢复返回栈的 API 也不例外,在 Navigation XML 中通过 NavOptions 上的新 API,也就是 navOptions Kotlin DSL,以及 popBackStack() 的重载方法可以帮助您指定 pop 操作保存状态或者指定 navigate 操作来恢复之前已保存的状态。


比如,在 Compose 中,任何全局的导航模式 (无论是底部导航栏、导航边栏、抽屉式导航栏或者任何您能想到的形式) 都可以使用我们在与 底部导航栏集成 所介绍的相同的技术,并且结合 saveStaterestoreState 属性一起调用 navigate():


onClick = {
navController.navigate(screen.route) {
// 当用户选择子项时在返回栈中弹出到导航图中的起始目的地
// 来避免太过臃肿的目的地堆栈
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}

// 当重复选择相同项时避免相同目的地的多重拷贝
launchSingleTop = true
// 当重复选择之前已经选择的项时恢复状态
restoreState = true
}
}

保存状态,锁定用户


对用户来说,最令人沮丧的事情之一便是丢失之前的状态。这也是为什么 Fragment 用一整页来讲解 保存与 Fragment 相关的状态,而且也是我非常乐于更新每个层级来支持多返回栈的原因之一:



  • Fragments (比如完全不使用 Navigation Component): 通过使用新的 FragmentManager API,也就是 saveBackStackrestoreBackStack


  • 核心的 Navigation 运行时: 添加可选的新的 NavOptions 方法用于 restoreState(恢复状态) 和 saveState (保存状态) 以及新的 popBackStack() 的重载方法,它同样可以传入一个布尔型的 saveState 参数 (默认是 false)。


  • 通过 Fragment 实现 Navigation: FragmentNavigator 现在利用新的 NavigatorAPI,通过使用 Navigation 运行时 API 将 Navigation 运行时 API 转换为 Fragment API。


  • NavigationUI: 每当它们弹出返回栈时,onNavDestinationSelected()、NavigationBarView.setupWithNavController()NavigationView.setupWithNavController() 现在默认使用 restoreState 和 saveState 这两个新的 NavOption。也就意味着 当升级到 Navigation 2.4.0-alpha01 或者更高版本后,任何使用 NavigationUI API 的应用无需修改代码即可实现多返回栈



如果您希望了解 更多使用该 API 的示例,请参考 NavigationAdvancedSample (它是最新更新的,且不包含任何用于支持多返回栈的 NavigationExtensions 代码)。


对于 Navigation Compose 的示例,请参考 Tivi。


如果您遇到任何问题,请使用官方的问题追踪页面提交关于 Fragment 或者 Navigation 的 bug,我们会尽快处理。


收起阅读 »