Quantcast
Channel: CSDN博客移动开发推荐文章
Viewing all 5930 articles
Browse latest View live

Android系统源码阅读(13):Input消息的分发过程

$
0
0

Android系统源码阅读(13):Input消息的分发过程

请对照AOSP版本:6.0.1_r50。学校电脑好渣,看源码时卡半天


先回顾一下前两篇文章。在设备没有事件输入的时候,InputReader和InputDispatcher都处于睡眠状态。当输入事件发生,InputReader首先被激活,然后发送读取消息,激活Dispatcher。Dispatcher被激活以后,将消息发送给当前激活窗口的主线程,然后睡眠等待主线程处理完这个事件。主线程被激活后,会处理相应的消息,处理完毕后反馈给Dispatcher,从而Dispatcher可以继续发送消息。

1. InputReader获取事件

回顾一下第11章4.2中,InputReader线程在获取事件以后,会调用processEventsLocked(mEventBuffer, count);处理事件。

这里写图片描述

1.1

这里先根据event的种类进行分门别类的处理。

frameworks/native/services/inputflinger/InputReader.cpp :

void InputReader::processEventsLocked(const RawEvent* rawEvents, size_t count) {
    for (const RawEvent* rawEvent = rawEvents; count;) {
        int32_t type = rawEvent->type;
        size_t batchSize = 1;

        if (type < EventHubInterface::FIRST_SYNTHETIC_EVENT) {
            //如果这里获得是合成事件
            //这里一次要将该输入设备中的一组事件都获取出来
            int32_t deviceId = rawEvent->deviceId;
            while (batchSize < count) {
                if (rawEvent[batchSize].type >= EventHubInterface::FIRST_SYNTHETIC_EVENT
                        || rawEvent[batchSize].deviceId != deviceId) {
                    break;
                }
                batchSize += 1;
            }
            //处理这些事件
            processEventsForDeviceLocked(deviceId, rawEvent, batchSize);
        } else {
            //这些是一些设备状况的事件,没必要将这些消息发送出去,留给自己处理就可以了
            switch (rawEvent->type) {
            case EventHubInterface::DEVICE_ADDED:
                addDeviceLocked(rawEvent->when, rawEvent->deviceId);
                break;
            case EventHubInterface::DEVICE_REMOVED:
                removeDeviceLocked(rawEvent->when, rawEvent->deviceId);
                break;
            case EventHubInterface::FINISHED_DEVICE_SCAN:
                handleConfigurationChangedLocked(rawEvent->when);
                break;
            default:
                ALOG_ASSERT(false); // can't happen
                break;
            }
        }
        count -= batchSize;
        rawEvent += batchSize;
    }
}

1.2

准备将事件交给设备进行处理。

frameworks/native/services/inputflinger/InputReader.cpp :

void InputReader::processEventsForDeviceLocked(int32_t deviceId,
        const RawEvent* rawEvents, size_t count) {  
    //判断设备是否是已知的
    ssize_t deviceIndex = mDevices.indexOfKey(deviceId);
    if (deviceIndex < 0) {
        ALOGW("Discarding event for unknown deviceId %d.", deviceId);
        return;
    }
    //获得设备
    InputDevice* device = mDevices.valueAt(deviceIndex);
    if (device->isIgnored()) {
        //ALOGD("Discarding event for ignored deviceId %d.", deviceId);
        return;
    }
    //交给设备进行处理
    device->process(rawEvents, count);
}

1.3

用device中的mapper去映射传入的事件,然后再处理。

frameworks/native/services/inputflinger/InputReader.cpp :

void InputDevice::process(const RawEvent* rawEvents, size_t count) {
    // Process all of the events in order for each mapper.
    // We cannot simply ask each mapper to process them in bulk because mappers may
    // have side-effects that must be interleaved.  For example, joystick movement events and
    // gamepad button presses are handled by different mappers but they should be dispatched
    // in the order received.
    //一个设备可能有多种类型的event,所有有多个mapper,但是需要保持event的顺序性
    //所以这里采用先循环event,再循环mapper的方式
    size_t numMappers = mMappers.size();
    for (const RawEvent* rawEvent = rawEvents; count--; rawEvent++) {

        if (mDropUntilNextSync) {
            if (rawEvent->type == EV_SYN && rawEvent->code == SYN_REPORT) {
                mDropUntilNextSync = false;
                //..
            } else {
                //..
            }
        } else if (rawEvent->type == EV_SYN && rawEvent->code == SYN_DROPPED) {
            //..
            mDropUntilNextSync = true;
            reset(rawEvent->when);
        } else {
            for (size_t i = 0; i < numMappers; i++) {
                InputMapper* mapper = mMappers[i];
                //用每一种mapper尝试处理event
                mapper->process(rawEvent);
            }
        }
    }
}

1.4

这里InputMapper种类很多,每个事件处理方法各不相同,所以在这里不再详述。其中有如下的InputMapper:

SwitchInputMapper, VibratorInputMapper, KeyboardInputMapper, CursorInputMapper, TouchInputMapper, SingleTouchInputMapper, MultiTouchInputMapper, JoystickInputMapper

这些方法处理到最后,会调用getListener()->notifyXXX(&args),让Dispatcher进行分发,XXX根据不同的Mapper有相应的名字。在创建InputReader时,将InputDispatcher作为参数传入,同时建立了QueuedInputListener来存放这个InputDispatcher,估计是准备将来处理多个InputDispatcher。所以这里getListener获取的就是当初建立的InputDispatcher对象。

1.5

这里虽然已经开始调用InputDispatcher的函数,但是还是在InputReader线程中。这里开始向InputDispatcher的队列中插入事件,并且把InputDispatcher唤醒了。因为notifyXXX函数同样是针对不同的输入有着不同的处理,所以不再详述,截取一段MotionEvent的代码片段。

frameworks/native/services/inputflinger/InputDispatcher.cpp :

   //针对每种event,都会想将其封装成一个EventEntry
   // Just enqueue a new motion event.
   MotionEntry* newEntry = new MotionEntry(args->eventTime,
                args->deviceId, args->source, policyFlags,
                args->action, args->actionButton, args->flags,
                args->metaState, args->buttonState,
                args->edgeFlags, args->xPrecision, args->yPrecision, args->downTime,
                args->displayId,
                args->pointerCount, args->pointerProperties, args->pointerCoords, 0, 0);
  //然后加入队列
  needWake = enqueueInboundEventLocked(newEntry);

  //...
  //唤醒Looper线程
  if (needWake) {
        mLooper->wake();
    }

将一个事件放入队列之后,会根据needWake参数决定是否要唤醒线程。如果要唤醒,则调用Looper的wake函数就可以了,和原来道理一样。在有些时候,有事件添加进去,不一定要唤醒线程,比如线程正在等待应用反馈事件处理完毕的消息。

1.6

实实在在的将这个EventEntry放入队列mInboundQueue中了。

frameworks/native/services/inputflinger/InputDispatcher.cpp :

bool InputDispatcher::enqueueInboundEventLocked(EventEntry* entry) {
    //如果队列空了,需要唤醒
    bool needWake = mInboundQueue.isEmpty();
    //将事件加入队列
    mInboundQueue.enqueueAtTail(entry);
    traceInboundQueueLengthLocked();

    switch (entry->type) {
    //这里会优化App切换的事件,如果上一个App还有事件没处理完,也没反馈事件处理完毕消息
    //则清空之前的事件,切换下一个应用
    case EventEntry::TYPE_KEY: {
        // Optimize app switch latency.
        // If the application takes too long to catch up then we drop all events preceding
        // the app switch key.
        KeyEntry* keyEntry = static_cast<KeyEntry*>(entry);
        if (isAppSwitchKeyEventLocked(keyEntry)) {
            if (keyEntry->action == AKEY_EVENT_ACTION_DOWN) {
                mAppSwitchSawKeyDown = true;
            } else if (keyEntry->action == AKEY_EVENT_ACTION_UP) {
                if (mAppSwitchSawKeyDown) {
#if DEBUG_APP_SWITCH
                    ALOGD("App switch is pending!");
#endif
                    mAppSwitchDueTime = keyEntry->eventTime + APP_SWITCH_TIMEOUT;
                    mAppSwitchSawKeyDown = false;
                    needWake = true;
                }
            }
        }
        break;
    }

    //当一个非当前激活app的点击事件发生,会清空之前的事件
    //从这个新的点击事件开始
    case EventEntry::TYPE_MOTION: {
        // Optimize case where the current application is unresponsive and the user
        // decides to touch a window in a different application.
        // If the application takes too long to catch up then we drop all events preceding
        // the touch into the other window.
        MotionEntry* motionEntry = static_cast<MotionEntry*>(entry);
        if (motionEntry->action == AMOTION_EVENT_ACTION_DOWN
                && (motionEntry->source & AINPUT_SOURCE_CLASS_POINTER)
                && mInputTargetWaitCause == INPUT_TARGET_WAIT_CAUSE_APPLICATION_NOT_READY
                && mInputTargetWaitApplicationHandle != NULL) {
            int32_t displayId = motionEntry->displayId;
            int32_t x = int32_t(motionEntry->pointerCoords[0].
                    getAxisValue(AMOTION_EVENT_AXIS_X));
            int32_t y = int32_t(motionEntry->pointerCoords[0].
                    getAxisValue(AMOTION_EVENT_AXIS_Y));
            sp<InputWindowHandle> touchedWindowHandle = findTouchedWindowAtLocked(displayId, x, y);
            if (touchedWindowHandle != NULL
                    && touchedWindowHandle->inputApplicationHandle
                            != mInputTargetWaitApplicationHandle) {
                // User touched a different application than the one we are waiting on.
                // Flag the event, and start pruning the input queue.
                mNextUnblockedEvent = motionEntry;
                needWake = true;
            }
        }
        break;
    }
    }
    return needWake;
}

这里做了两种优化,主要是在当前App窗口处理事件过慢,同时你又触发其他App的事件时,Dispatcher就会丢弃先前的事件,从这个开始唤醒Dispatcher。这样做很合情合理,用户在使用时,会遇到App由于开发者水平有限导致处理事件过慢情况,这时用户等的不耐烦,则应该让用户轻松的切换到其它App,而不是阻塞在那。所以,事件无法响应只会发生在App内部,而不会影响应用的切换,从而提升用户体验。App的质量问题不会影响系统的运转,Android在这点上做的很人性。

2. InputDispatcher分发事件

在第11章中3.2中,Dispatcher调用函数dispatchOnceInnerLocked(&nextWakeupTime);来分配队列中的事件。

这里写图片描述

2.1

从队列中获取event,然后准备处理。

frameworks/native/services/inputflinger/InputDispatcher.cpp :

void InputDispatcher::dispatchOnceInnerLocked(nsecs_t* nextWakeupTime) {
    nsecs_t currentTime = now();

    // Reset the key repeat timer whenever normal dispatch is suspended while the
    // device is in a non-interactive state.  This is to ensure that we abort a key
    // repeat if the device is just coming out of sleep.
    if (!mDispatchEnabled) {
        resetKeyRepeatLocked();
    }

    // If dispatching is frozen, do not process timeouts or try to deliver any new events.
    if (mDispatchFrozen) {
        return;
    }

    //对App切换的情况的优化
    // Optimize latency of app switches.
    // Essentially we start a short timeout when an app switch key (HOME / ENDCALL) has
    // been pressed.  When it expires, we preempt dispatch and drop all other pending events.
    bool isAppSwitchDue = mAppSwitchDueTime <= currentTime;
    if (mAppSwitchDueTime < *nextWakeupTime) {
        //如果有切换App的event,且时间小于设定的时间
        //则将等待事件设为小者
        *nextWakeupTime = mAppSwitchDueTime;
    }

    // Ready to start a new event.
    // If we don't already have a pending event, go grab one.
    if (! mPendingEvent) {
        if (mInboundQueue.isEmpty()) {
            //队列是空的情况
            if (isAppSwitchDue) {
                // The inbound queue is empty so the app switch key we were waiting
                // for will never arrive.  Stop waiting for it.
                resetPendingAppSwitchLocked(false);
                isAppSwitchDue = false;
            }

            // Synthesize a key repeat if appropriate.
            //如果有连续重复事件发生,则制造重复事件
            if (mKeyRepeatState.lastKeyEntry) {
                if (currentTime >= mKeyRepeatState.nextRepeatTime) {
                    mPendingEvent = synthesizeKeyRepeatLocked(currentTime);
                } else {
                    if (mKeyRepeatState.nextRepeatTime < *nextWakeupTime) {
                        *nextWakeupTime = mKeyRepeatState.nextRepeatTime;
                    }
                }
            }

            // Nothing to do if there is no pending event.
            //如果真无事可做,下次睡眠可能比较久
            if (!mPendingEvent) {
                return;
            }
        } else {
            // Inbound queue has at least one entry.
            //从队列中获取一个Event
            mPendingEvent = mInboundQueue.dequeueAtHead();
            traceInboundQueueLengthLocked();
        }

        // Poke user activity for this event.
        if (mPendingEvent->policyFlags & POLICY_FLAG_PASS_TO_USER) {
            pokeUserActivityLocked(mPendingEvent);
        }
        //重置ANRT
        // Get ready to dispatch the event.
        resetANRTimeoutsLocked();
    }

    // Now we have an event to dispatch.
    // All events are eventually dequeued and processed this way, even if we intend to drop them.
    bool done = false;
    DropReason dropReason = DROP_REASON_NOT_DROPPED;
    if (!(mPendingEvent->policyFlags & POLICY_FLAG_PASS_TO_USER)) {
        dropReason = DROP_REASON_POLICY;
    } else if (!mDispatchEnabled) {
        dropReason = DROP_REASON_DISABLED;
    }

    if (mNextUnblockedEvent == mPendingEvent) {
        mNextUnblockedEvent = NULL;
    }

    //分类处理Event
    switch (mPendingEvent->type) {
    case EventEntry::TYPE_CONFIGURATION_CHANGED: {
        ConfigurationChangedEntry* typedEntry =
                static_cast<ConfigurationChangedEntry*>(mPendingEvent);
        done = dispatchConfigurationChangedLocked(currentTime, typedEntry);
        dropReason = DROP_REASON_NOT_DROPPED; // configuration changes are never dropped
        break;
    }

    case EventEntry::TYPE_DEVICE_RESET: {
        DeviceResetEntry* typedEntry =
                static_cast<DeviceResetEntry*>(mPendingEvent);
        done = dispatchDeviceResetLocked(currentTime, typedEntry);
        dropReason = DROP_REASON_NOT_DROPPED; // device resets are never dropped
        break;
    }

    case EventEntry::TYPE_KEY: {
        KeyEntry* typedEntry = static_cast<KeyEntry*>(mPendingEvent);
        if (isAppSwitchDue) {
            if (isAppSwitchKeyEventLocked(typedEntry)) {
                //这个Event就是SwitchEvent,我已经处理
                //重置App切换的状态为false
                resetPendingAppSwitchLocked(true);
                isAppSwitchDue = false;
            } else if (dropReason == DROP_REASON_NOT_DROPPED) {
                //如果是其他事件,则丢弃,因为正在switch
                dropReason = DROP_REASON_APP_SWITCH;
            }
        }
        if (dropReason == DROP_REASON_NOT_DROPPED
                && isStaleEventLocked(currentTime, typedEntry)) {
            dropReason = DROP_REASON_STALE;
        }
        if (dropReason == DROP_REASON_NOT_DROPPED && mNextUnblockedEvent) {
            dropReason = DROP_REASON_BLOCKED;
        }
        //分发KeyEvent
        done = dispatchKeyLocked(currentTime, typedEntry, &dropReason, nextWakeupTime);
        break;
    }

    case EventEntry::TYPE_MOTION: {
        MotionEntry* typedEntry = static_cast<MotionEntry*>(mPendingEvent);
        if (dropReason == DROP_REASON_NOT_DROPPED && isAppSwitchDue) {
            dropReason = DROP_REASON_APP_SWITCH;
        }
        if (dropReason == DROP_REASON_NOT_DROPPED
                && isStaleEventLocked(currentTime, typedEntry)) {
            dropReason = DROP_REASON_STALE;
        }
        if (dropReason == DROP_REASON_NOT_DROPPED && mNextUnblockedEvent) {
            dropReason = DROP_REASON_BLOCKED;
        }
        //分发Motion Event
        done = dispatchMotionLocked(currentTime, typedEntry,
                &dropReason, nextWakeupTime);
        break;
    }

    default:
        ALOG_ASSERT(false);
        break;
    }

    if (done) {
        if (dropReason != DROP_REASON_NOT_DROPPED) {
            dropInboundEventLocked(mPendingEvent, dropReason);
        }
        mLastDropReason = dropReason;
        //将mPendingEvent置为null
        releasePendingEventLocked();
        //我已经处理的event,所以需要将睡眠设置时间小一点
        *nextWakeupTime = LONG_LONG_MIN;  // force next poll to wake up immediately
    }
}

2.2

这一步根据Event的种类,略有不同。有dispatchMotionLockeddispatchKeyLocked等。主要过程类似,首先判断是否需要丢弃该event,然后获得目标Window,再向目标window发送event。代码片如下:

frameworks/native/services/inputflinger/InputDispatcher.cpp :

    //..
    // Identify targets.
    Vector<InputTarget> inputTargets;
    //..
    injectionResult = findTouchedWindowTargetsLocked(currentTime, entry, inputTargets, nextWakeupTime, &conflictingPointerActions);
    //..
    dispatchEventLocked(currentTime, entry, inputTargets);

这里调用findTouchedWindowTargetsLocked()来获取目标Window。在前面12章3.6中,将mFocusedWindowHandle参数设置为了当前激活的Window,所以目前返回的inputTargets就是将mFocusedWindowHandle封装后的结果。

2.3

向每一个目标发送event。

frameworks/native/services/inputflinger/InputDispatcher.cpp :

void InputDispatcher::dispatchEventLocked(nsecs_t currentTime,
        EventEntry* eventEntry, const Vector<InputTarget>& inputTargets) {

    pokeUserActivityLocked(eventEntry);
    //向每一个目标发送event
    for (size_t i = 0; i < inputTargets.size(); i++) {
        const InputTarget& inputTarget = inputTargets.itemAt(i);
        //获取目标的connection
        ssize_t connectionIndex = getConnectionIndexLocked(inputTarget.inputChannel);
        if (connectionIndex >= 0) {
            sp<Connection> connection = mConnectionsByFd.valueAt(connectionIndex);
            prepareDispatchCycleLocked(currentTime, connection, eventEntry, &inputTarget);
        } else {
          //..
        }
    }
}

2.4

frameworks/native/services/inputflinger/InputDispatcher.cpp :

void InputDispatcher::prepareDispatchCycleLocked(nsecs_t currentTime,
        const sp<Connection>& connection, EventEntry* eventEntry, const InputTarget* inputTarget) {

    // Skip this event if the connection status is not normal.
    // We don't want to enqueue additional outbound events if the connection is broken.
    if (connection->status != Connection::STATUS_NORMAL) {
        return;
    }

    // Split a motion event if needed.
    if (inputTarget->flags & InputTarget::FLAG_SPLIT) {
        //Motion event一般为连续的基本event组合而成
        //所以可以分割
        MotionEntry* originalMotionEntry = static_cast<MotionEntry*>(eventEntry);
        if (inputTarget->pointerIds.count() != originalMotionEntry->pointerCount) {
            MotionEntry* splitMotionEntry = splitMotionEvent(
                    originalMotionEntry, inputTarget->pointerIds);
            if (!splitMotionEntry) {
                return; // split event was dropped
            }
            enqueueDispatchEntriesLocked(currentTime, connection,
                    splitMotionEntry, inputTarget);
            splitMotionEntry->release();
            return;
        }
    }

    // Not splitting.  Enqueue dispatch entries for the event as is.
    enqueueDispatchEntriesLocked(currentTime, connection, eventEntry, inputTarget);
}

2.5

这一步先判断目标Connection是否空,然后向其队列加入event。如果原来队列为空,说明可以进一步分发event;如果不为空,说明旧event还没有处理完毕,则不进一步分发。

frameworks/native/services/inputflinger/InputDispatcher.cpp :

    bool wasEmpty = connection->outboundQueue.isEmpty();

    // Enqueue dispatch entries for the requested modes.
    //将event放入outboundQueue中,省略..

    // If the outbound queue was previously empty, start the dispatch cycle going.
    if (wasEmpty && !connection->outboundQueue.isEmpty()) {
        startDispatchCycleLocked(currentTime, connection);
    }

2.6

循环的取出队列中的event,然后交给connection发送。

frameworks/native/services/inputflinger/InputDispatcher.cpp :

void InputDispatcher::startDispatchCycleLocked(nsecs_t currentTime,
        const sp<Connection>& connection) {

    //将outboundQueue中的entry一一进行处理
    while (connection->status == Connection::STATUS_NORMAL
            && !connection->outboundQueue.isEmpty()) {
        //获取首部的entry
        DispatchEntry* dispatchEntry = connection->outboundQueue.head;
        dispatchEntry->deliveryTime = currentTime;

        // Publish the event.
        status_t status;
        EventEntry* eventEntry = dispatchEntry->eventEntry;
        switch (eventEntry->type) {
        case EventEntry::TYPE_KEY: {
            KeyEntry* keyEntry = static_cast<KeyEntry*>(eventEntry);
            //..

            // Publish the key event.
            status = connection->inputPublisher.publishKeyEvent(dispatchEntry->seq,
                    keyEntry->deviceId, keyEntry->source,
                    dispatchEntry->resolvedAction, dispatchEntry->resolvedFlags,
                    keyEntry->keyCode, keyEntry->scanCode,
                    keyEntry->metaState, keyEntry->repeatCount, keyEntry->downTime,
                    keyEntry->eventTime);
            break;
        }

        case EventEntry::TYPE_MOTION: {
            MotionEntry* motionEntry = static_cast<MotionEntry*>(eventEntry);

            // Set the X and Y offset depending on the input source.
            //..

            // Publish the motion event.
            status = connection->inputPublisher.publishMotionEvent(dispatchEntry->seq,
                    motionEntry->deviceId, motionEntry->source,
                    dispatchEntry->resolvedAction, motionEntry->actionButton,
                    dispatchEntry->resolvedFlags, motionEntry->edgeFlags,
                    motionEntry->metaState, motionEntry->buttonState,
                    xOffset, yOffset, motionEntry->xPrecision, motionEntry->yPrecision,
                    motionEntry->downTime, motionEntry->eventTime,
                    motionEntry->pointerCount, motionEntry->pointerProperties,
                    usingCoords);
            break;
        }

        default:
            return;
        }

        // Check the result.

        // Re-enqueue the event on the wait queue.
        //从outboundQueue中移除
        connection->outboundQueue.dequeue(dispatchEntry);
        traceOutboundQueueLengthLocked(connection);
        //加入waitQueue
        connection->waitQueue.enqueueAtTail(dispatchEntry);
        traceWaitQueueLengthLocked(connection);
    }
}

2.7

这一步同样有多种情况,有publishMotionEvent和publishKeyEvent。基本思路相近。先封装成message,最后都调用了mChannel->sendMessage(&msg)
frameworks/native/libs/input/InputTransport.cpp :

    InputMessage msg;
    msg.header.type = InputMessage::TYPE_MOTION;
    msg.body.motion.seq = seq;
    msg.body.motion.deviceId = deviceId;
    msg.body.motion.source = source;
    msg.body.motion.action = action;
    msg.body.motion.actionButton = actionButton;
    msg.body.motion.flags = flags;
    msg.body.motion.edgeFlags = edgeFlags;
    msg.body.motion.metaState = metaState;
    msg.body.motion.buttonState = buttonState;
    msg.body.motion.xOffset = xOffset;
    msg.body.motion.yOffset = yOffset;
    msg.body.motion.xPrecision = xPrecision;
    msg.body.motion.yPrecision = yPrecision;
    msg.body.motion.downTime = downTime;
    msg.body.motion.eventTime = eventTime;
    msg.body.motion.pointerCount = pointerCount;
    for (uint32_t i = 0; i < pointerCount; i++) {
        msg.body.motion.pointers[i].properties.copyFrom(pointerProperties[i]);
        msg.body.motion.pointers[i].coords.copyFrom(pointerCoords[i]);
    }
    return mChannel->sendMessage(&msg);

2.8

这一步就要像保存的文件描述符mFd中写入数据了。

frameworks/native/libs/input/InputTransport.cpp :

    size_t msgLength = msg->size();
    ssize_t nWrite;
    do {
        nWrite = ::send(mFd, msg, msgLength, MSG_DONTWAIT | MSG_NOSIGNAL);
    } while (nWrite == -1 && errno == EINTR);

到这里,Dispatcher终于把event通过当初建立的Channel pair发送给了应用window。这里和旧版本很不同,当初需要先将event放入共享内存,然后发送一个信号进行通知。

3. 当前激活Window获得消息

这一节比较复杂,需要回忆大量的前面几章的细节和一定的逻辑推理(连蒙带猜)能力。

先来回忆一下第12章4.5节,在InputChannel注册到Client时,最后一步做了什么。

frameworks/base/core/jni/android_view_InputEventReceiver.cpp :

 mMessageQueue->getLooper()->addFd(fd, 0, events, this, NULL);

这里给主线程的Looper添加的一个需要监听fd,这个fd是Client端的InputChannel的文件描述符。addFd函数的第4个参数是一个回调函数,这里this指NativeInputEventReceiver对象,它是LooperCallback的子类。addFd函数新建了一个Request对象,如下:

system/core/libutils/Looper.cpp :

Request request;
request.fd = fd;
request.ident = ident;
request.events = events;
request.seq = mNextRequestSeq++;
//回调函数指明是上一步传入的NativeInputEventReceiver对象
request.callback = callback;
request.data = data;
//..
//将request以fd为关键字加入mRequests
mRequests.add(fd, request);

再次回忆第10章1.6,也就是主线程被阻塞的地方。代码如下:
system/core/libutils/Looper.cpp :

int Looper::pollInner(int timeoutMillis) {

    // Adjust the timeout based on when the next message is due.
    //..
    //清空mResponses数组
    // Poll.
    int result = POLL_WAKE;
    mResponses.clear();
    mResponseIndex = 0;

    // We are about to idle.
    mPolling = true;
    //等待epoll监测到IO事件
    struct epoll_event eventItems[EPOLL_MAX_EVENTS];
    int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis);

    //..

    // Handle all events.
    for (int i = 0; i < eventCount; i++) {
        int fd = eventItems[i].data.fd;
        uint32_t epollEvents = eventItems[i].events;
        if (fd == mWakeEventFd) {
            if (epollEvents & EPOLLIN) {
                //这里处理的唤醒事件
                awoken();
            } else {
                ALOGW("Ignoring unexpected epoll events 0x%x on wake event fd.", epollEvents);
            }
        } else {
        //这里处理的是input事件
            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;
                //将event加入mResponses
                pushResponse(events, mRequests.valueAt(requestIndex));
            } else {
              //..
            }
        }
    }
Done: ;

    //遍历每一个存在mResponses的event
    // Invoke all response callbacks.
    for (size_t i = 0; i < mResponses.size(); i++) {
        Response& response = mResponses.editItemAt(i);
        if (response.request.ident == POLL_CALLBACK) {
            int fd = response.request.fd;
            int events = response.events;
            void* data = response.request.data;
            // Invoke the callback.  Note that the file descriptor may be closed by
            // the callback (and potentially even reused) before the function returns so
            // we need to be a little careful when removing the file descriptor afterwards.
            //这一步开始调用回调函数,说明该文件描述符下有人输入的event
            int callbackResult = response.request.callback->handleEvent(fd, events, data);
            if (callbackResult == 0) {
                removeFd(fd, response.request.seq);
            }

            // Clear the callback reference in the response structure promptly because we
            // will not clear the response vector itself until the next poll.
            response.request.callback.clear();
            result = POLL_CALLBACK;
        }
    }
    return result;
}

这一步处理wake事件已经在第10章第1节讲述过了,这里需要处理的另一种事件input event。对input event,先将其放入mResponses数组,然后依次调用他们的回调函数。这里就是NativeInputEventReceiver的handleEvent函数了。

这里写图片描述

3.1

Event分为Input和Output,这里是事件输入,所以先看Input分支。

frameworks/base/core/jni/android_view_InputEventReceiver.cpp :

int NativeInputEventReceiver::handleEvent(int receiveFd, int events, void* data) {

    if (events & ALOOPER_EVENT_INPUT) {
        JNIEnv* env = AndroidRuntime::getJNIEnv();
        status_t status = consumeEvents(env, false /*consumeBatches*/, -1, NULL);
        mMessageQueue->raiseAndClearException(env, "handleReceiveCallback");
        return status == OK || status == NO_MEMORY ? 1 : 0;
    }

    if (events & ALOOPER_EVENT_OUTPUT) {
      //..
    }
    return 1;
}

3.2

开始从目标Channel中读出event,然后包装成java层的对象,开始调用java函数进行处理。

frameworks/base/core/jni/android_view_InputEventReceiver.cpp :

status_t NativeInputEventReceiver::consumeEvents(JNIEnv* env,
        bool consumeBatches, nsecs_t frameTime, bool* outConsumedBatch) {

    ScopedLocalRef<jobject> receiverObj(env, NULL);
    bool skipCallbacks = false;
    //循环从Channel中读出event
    for (;;) {
        uint32_t seq;
        InputEvent* inputEvent;
        //这一步开始从Channel中读取event,存入inputEvent中
        status_t status = mInputConsumer.consume(&mInputEventFactory,
                consumeBatches, frameTime, &seq, &inputEvent);
        if (status) {
            if (status == WOULD_BLOCK) {
                if (!skipCallbacks && !mBatchedInputEventPending
                        && mInputConsumer.hasPendingBatch()) {
                    // There is a pending batch.  Come back later.
                    //..
                return OK;
                }
            }
            return status;
        }

        if (!skipCallbacks) {
            if (!receiverObj.get()) {
                //这里的mReceiverWeakGlobal是java层的InputEventReceiver
                receiverObj.reset(jniGetReferent(env, mReceiverWeakGlobal));
                //..
            }

            //根据不同的InputEvent种类,将其转化为java层的inputEventObj
            jobject inputEventObj;
            switch (inputEvent->getType()) {
            case AINPUT_EVENT_TYPE_KEY:
                inputEventObj = android_view_KeyEvent_fromNative(env,
                        static_cast<KeyEvent*>(inputEvent));
                break;
            case AINPUT_EVENT_TYPE_MOTION: {
                MotionEvent* motionEvent = static_cast<MotionEvent*>(inputEvent);
                if ((motionEvent->getAction() & AMOTION_EVENT_ACTION_MOVE) && outConsumedBatch) {
                    *outConsumedBatch = true;
                }
                inputEventObj = android_view_MotionEvent_obtainAsCopy(env, motionEvent);
                break;
            }
            default:
                assert(false); // InputConsumer should prevent this from ever happening
                inputEventObj = NULL;
            }

            if (inputEventObj) {
                //开始调用java层的dispatchInputEvent函数
                env->CallVoidMethod(receiverObj.get(),
                        gInputEventReceiverClassInfo.dispatchInputEvent, seq, inputEventObj);
                env->DeleteLocalRef(inputEventObj);
            } else {
              //..
            }
        }

        if (skipCallbacks) {
            //不需要调用回调函数,则可以直接反馈完成信号
            mInputConsumer.sendFinishedSignal(seq, false);
        }
    }
}

我们先看如何从Channel中获取event的,3.3。然后讲解java层的分发过程,3.4。

3.3

从Chuannel中读取一个(一组)event。

frameworks/native/libs/input/InputTransport.cpp :

status_t InputConsumer::consume(InputEventFactoryInterface* factory,
        bool consumeBatches, nsecs_t frameTime, uint32_t* outSeq, InputEvent** outEvent) {

    *outSeq = 0;
    *outEvent = NULL;

    // Fetch the next input message.
    // Loop until an event can be returned or no additional events are received.
    while (!*outEvent) {
        if (mMsgDeferred) {
            // mMsg contains a valid input message from the previous call to consume
            // that has not yet been processed.
            mMsgDeferred = false;
        } else {
            // Receive a fresh message.
            //从Channel中读取一个Message
            status_t result = mChannel->receiveMessage(&mMsg);
            if (result) {
                // Consume the next batched event unless batches are being held for later.
                //..
            }
        }
        //根据message种类构造event
        switch (mMsg.header.type) {
        case InputMessage::TYPE_KEY: {
            KeyEvent* keyEvent = factory->createKeyEvent();
            if (!keyEvent) return NO_MEMORY;

            initializeKeyEvent(keyEvent, &mMsg);
            *outSeq = mMsg.body.key.seq;
            *outEvent = keyEvent;
            break;
        }
        case AINPUT_EVENT_TYPE_MOTION: {
            //对Motion event,需要处理成批的event事件
            //..
            MotionEvent* motionEvent = factory->createMotionEvent();
            if (! motionEvent) return NO_MEMORY;

            updateTouchState(&mMsg);
            initializeMotionEvent(motionEvent, &mMsg);
            *outSeq = mMsg.body.motion.seq;
            *outEvent = motionEvent;
            break;
        }
        default:
            return UNKNOWN_ERROR;
        }
    }
    return OK;
}

mChannel->receiveMessage(&mMsg)函数在InputChannel中主要如下实现,调用socket函数recv从mFd中读出数据。

frameworks/native/libs/input/InputTransport.cpp :

nRead = ::recv(mFd, msg, sizeof(InputMessage), MSG_DONTWAIT);

3.4

回到java层,在获得event后,就需要进行分发了。receiverObj指向的是一个InputEventReceiver对象,这里其实是它的子类WindowInputEventReceiver对象,见第12章第4节。dispatchInputEvent函数还是继承的父类的,没有重写。

3.5

这一步直接调用了下一步。

3.6

将event插入等待事件队列的尾部,然后开始调度这些消息。

frameworks/base/core/java/android/view/ViewRootImpl.java :

  void enqueueInputEvent(InputEvent event,
            InputEventReceiver receiver, int flags, boolean processImmediately) {
        adjustInputEventForCompatibility(event);
        //将event和receiver封装在一起
        QueuedInputEvent q = obtainQueuedInputEvent(event, receiver, flags);

        // Always enqueue the input event in order, regardless of its time stamp.
        // We do this because the application or the IME may inject key events
        // in response to touch events and we want to ensure that the injected keys
        // are processed in the order they were received and we cannot trust that
        // the time stamp of injected events are monotonic.
        //找到尾部插入
        QueuedInputEvent last = mPendingInputEventTail;
        if (last == null) {
            mPendingInputEventHead = q;
            mPendingInputEventTail = q;
        } else {
            last.mNext = q;
            mPendingInputEventTail = q;
        }
        mPendingInputEventCount += 1;

        if (processImmediately) {
            //立即处理所有的InputEvent
            doProcessInputEvents();
        } else {
            //发送一个提醒消息
            scheduleProcessInputEvents();
        }
    }

这里会有两种不同的处理event的方式,一个是立即处理;另一种是向主线程Looper发送MSG_PROCESS_INPUT_EVENTS消息。这里选择立即处理。

3.7

这里会把等待在队列中的event,一口气全处理了。这一步会循环拿出队列中的每一个event,然后调用下一步进行处理。

3.8

准备交给stage处理。

frameworks/base/core/java/android/view/ViewRootImpl.java :

    private void deliverInputEvent(QueuedInputEvent q) {
    //...
    //下面开始从某个stage开始
    //寻找适合的stage进行处理
        InputStage stage;
        if (q.shouldSendToSynthesizer()) {
            stage = mSyntheticInputStage;
        } else {
            stage = q.shouldSkipIme() ? mFirstPostImeInputStage : mFirstInputStage;
        }
        if (stage != null) {
            stage.deliver(q);
        } else {
            finishInputEvent(q);
        }
    }

3.9

下面就讲解一下stage是什么东西。stage可以说是处理event的不同阶段,如果上一个stage处理不了,就交给下一个stage处理,总有一个stage可以将event处理掉。这里stage的处理顺序图如下所示:

这里写图片描述

  1. NativePrelmeInputStage: Delivers pre-ime input events to a native activity. Does not support pointer events. 其实我也不清楚到底是干什么的,没有想到具体的应用场景。
  2. ViewPreImeInputStage: Delivers pre-ime input events to the view hierarchy. Does not support pointer events. 这一步只处理KeyEvent,在InputMethod处理这个KeyEvent之前,可以截获这个event。典型的例子是处理BACK key。
  3. ImeInputStage: Delivers input events to the ime. Does not support pointer events. 将event交给IputMethodManager处理。
  4. EarlyPostImeInputStage: Performs early processing of post-ime input events. 在交给下一阶段之前,先处理筛选一些event。
  5. NativePostImeInputStage: Delivers post-ime input events to a native activity. 尝试让InputQueue发送event给native activity。
  6. ViewPostImeInputStage: Delivers post-ime input events to the view hierarchy. 将event发送给view的层次结构中。
  7. SyntheticInputStage: Performs synthesis of new input events from unhandled input events. 最后一个阶段,处理综合事件,比如trackball, joystick等。

这里我们暂且研究第6种stage,这个stage和View有直接关系。

3.10

这里根据event的类型分别进行处理。我们先关注一下PointerEvent如何处理的。

3.11

将event交给mView来处理,这里mView就是一个DecorView对象。

3.12

这一步是DecorView继承自View的方法,将event分为TouchEvent和GenericMotionEvent来处理。先看TouchEvent如何处理。

3.13

DecorView重写了该函数。这里调用getCallback函数来获取一个回调对象。该函数是PhoneWindow继承自Window类的方法,获得是mCallback。那么这个mCallback到底是谁呢?

回顾一下Activity的创建过程,在Activity创建以后会调用attach函数对Activity进行一定的初始化,其中就创建了PhoneWindow,同时设置了callback为Activity它自己。所以这一步获得是一个Activity对象,然后调用它的函数继续分发event。

3.14

Event交到Activity手中进行处理。为什么会先交给Activity处理?目的是让开发者可以重写这个函数,从而可以在分发这个事件之前进行截获。

    /**
     * Called to process touch screen events.  You can override this to
     * intercept all touch screen events before they are dispatched to the
     * window.  Be sure to call this implementation for touch screen events
     * that should be handled normally.
     *
     * @param ev The touch screen event.
     *
     * @return boolean Return true if this event was consumed.
     */
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        //绕一圈有交给PhoneWindow处理
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        //没有view可以处理,那么activity自己处理
        //默认就是放弃,也可以重写实现特定功能
        return onTouchEvent(ev);
    }

3.15

什么也没做,将event传回DecorView,让它处理。

3.16

这里DecorView又交给dispatchTouchEvent处理。这里的dispatchTouchEvent是源自父类ViewGroup的函数,而不是自己重写的函数。

3.17

ViewGroup是View的子类。它管理了一组View在mChildren数组中,按照设计模式的说法叫Composite模式。

这里写图片描述

这一步会依次访问每个子view,判断他们是否可以处理该event,如果能就交给它处理;没人能处理就自己处理。无论哪种方式,都会调用下一步dispatchTransformedTouchEvent函数。

frameworks/base/core/java/android/view/ViewGroup.java :

    public boolean dispatchTouchEvent(MotionEvent ev) {
        //..
        // If the event targets the accessibility focused view and this is it, start
        // normal event dispatch. Maybe a descendant is what will handle the click.
        if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
            ev.setTargetAccessibilityFocus(false);
        }

        boolean handled = false;
        //如果Window没有被遮住,才进行以下过程
        if (onFilterTouchEventForSecurity(ev)) {
            //..
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
            if (!canceled && !intercepted) {
                    //..
                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        final float x = ev.getX(actionIndex);
                        final float y = ev.getY(actionIndex);
                        // Find a child that can receive the event.
                        // Scan children from front to back.
                        //根据z坐标进行排序,从小到大的顺序
                        final ArrayList<View> preorderedList = buildOrderedChildList();
                        //也可以自己定制顺序
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        //所有子View存放在mChildren中
                        final View[] children = mChildren;
                        //按z的值,从大到小开始遍历
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = customOrder
                                    ? getChildDrawingOrder(childrenCount, i) : i;
                            final View child = (preorderedList == null)
                                    ? children[childIndex] : preorderedList.get(childIndex);

                            // If there is a view that has accessibility focus we want it
                            // to get the event first and if not handled we will perform a
                            // normal dispatch. We may do a double iteration but this is
                            // safer given the timeframe.
                            //如果指定了一个view去获得这个event,一直循环到那个view为止
                            if (childWithAccessibilityFocus != null) {
                                if (childWithAccessibilityFocus != child) {
                                    continue;
                                }
                                childWithAccessibilityFocus = null;
                                i = childrenCount - 1;
                            }

                            //判断该view是否可以接受这个event,是否在这个view范围内
                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }

                            //如果该child正在处理上一个event
                            newTouchTarget = getTouchTarget(child);
                            if (newTouchTarget != null) {
                                // Child is already receiving touch within its bounds.
                                // Give it the new pointer in addition to the ones it is handling.
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }
                            //尝试去向该child发送event
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // Child wants to receive touch within its bounds.
                                //..
                                //将child加入mFirstTouchTarget为首的队列头部
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }
                            //..
                        }
                    }
                }
            }

            // Dispatch to touch targets.
            if (mFirstTouchTarget == null) {
            //没有child可以处理,那么就自己处理
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                // Dispatch to touch targets, excluding the new touch target if we already
                // dispatched to it.  Cancel touch targets if necessary.
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                        //..
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        //..
                    }
                    predecessor = target;
                    target = next;
                }
            }
            //..
        }
        return handled;
    }

3.18

ViewGroup会决定自己处理还是交给child处理。在ViewGroup自己处理,或者child为不再是一个ViewGroup时,则开始调用View的dispatchTouchEvent函数。

frameworks/base/core/java/android/view/ViewGroup.java :

//省略了计算坐标便宜的过程
if (child == null) {
    //ViewGroup决定自己处理
    handled = super.dispatchTouchEvent(event);
} else {
    //交给child处理,child可能是个view
    //也可能还是个ViewGroup,这就重复3.17步骤
        handled = child.dispatchTouchEvent(event);
}

3.19

这一步就会调用用户自己实现的Listener;如果没有Listener,则会调用view默认的处理函数。

frameworks/base/core/java/android/view/View.java :

  public boolean dispatchTouchEvent(MotionEvent event) {
        // If the event should be handled by accessibility focus first.
        if (event.isTargetAccessibilityFocus()) {
            // We don't have focus or no virtual descendant has it, do not handle the event.
            if (!isAccessibilityFocusedViewOrHost()) {
                //该View不可访问的状态,返回
                return false;
            }
        }

        boolean result = false;
        //..
        //先判断该View是否被遮挡,没被遮挡才进行处理
        if (onFilterTouchEventForSecurity(event)) {
            //noinspection SimplifiableIfStatement
            //获得注册的Listener,看是否有注册OnTouchListener
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                      //调用Listener的回调函数,处理event
                result = true;
            }
            //View也可以采用默认的处理onTouchEvent
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }
        //..
        return result;
    }

默认处理函数onTouchEvent会处理一些基本的操作,比如button的按下松开的效果,滚动容器产生滚动的效果等。

最后说两句

到这里,Input event的处理流程已经分析完了,心好累。

作者:tianchi92 发表于2016/9/27 22:34:23 原文链接
阅读:161 评论:0 查看评论

View绘制详解(二),从setContentView谈起

$
0
0

掐指一算,本来今天该介绍View的测量了,可是要说View的测量,那就要从setContentView谈起了,setContentView本身涉及到的东西也是挺多的,所以今天我们就先来看看这个setContentView到底做了什么事。上篇文章我们介绍了LayoutInflater加载一个布局文件的原理,如果小伙伴们还没看过,请移步这里View绘制详解,从LayoutInflater谈起

现在使用Android Studio,我们的Activity都是间接继承自Activity类的,所有Activity的onCreate方法中我们都会加上一句setContentView,然后传入我们的布局的资源id,这行代码我们都知道是用来设置布局文件的,那么这个设置过程到底是什么样的?我们找到Activity的setContentView方法,如下:

    /**
     * Set the activity content from a layout resource.  The resource will be
     * inflated, adding all top-level views to the activity.
     *
     * @param layoutResID Resource ID to be inflated.
     *
     * @see #setContentView(android.view.View)
     * @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams)
     */
    public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }

小伙伴们注意看这个方法的注释,注释说这个方法通过一个资源id来设置activity的布局,并将这个布局添加到activity的顶层View,那要怎么添加呢?就是下面的getWindow().setContentView(layoutResID);方法了,这里我们就要引入一个新的东西了,就是Window。Window表示一个窗口,它是一个抽象类,它的具体实现是由PhoneWindow来完成的。在我们的应用中,所有的视图实际上都是附加在Window上,所以这个Window才是View的真正管理者。那我们要怎么理解Activity、Window和View之间的关系呢?我在网上看到这样一句话,感觉总结的还挺形象,和大家分享一下:

以下这段话来自:http://blog.csdn.net/u011733020/article/details/49465707

Activity就像是一扇贴着窗花的窗口,Window就像窗口上面的玻璃,而View对象就像一个个贴在玻璃上的窗花。

OK,那我们这里就来看看是怎么往玻璃上贴窗花的。

这里首先调用了window的setContentView方法,我们先来看看getWindow获取了一个什么东西:

    public Window getWindow() {
        return mWindow;
    }

那这个mWindow又是什么呢?搜索之后我们在Activity的attach方法中找到了mWindow初始化的地方:

final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window) {
        attachBaseContext(context);

        mFragments.attachHost(null /*parent*/);

        mWindow = new PhoneWindow(this, window);
		
		......
		......
		......
    }

小伙伴们看到,mWindow实际上就是一个PhoneWindow的实例,那我们就去这个PhoneWindow中看一下setContentView到底是干嘛了。找到PhoneWindow中的setContentView方法,如下:

  @Override
    public void setContentView(int layoutResID) {
        // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
        // decor, when theme attributes and the like are crystalized. Do not check the feature
        // before this happens.
        if (mContentParent == null) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                    getContext());
            transitionTo(newScene);
        } else {
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }
小伙伴们注意第6行,如果mContentParent==null,则调用installDecor()方法,那么这个mContentParent是什么?我们找到该变量定义的地方,如下:

// This is the view in which the window contents are placed. It is either
    // mDecor itself, or a child of mDecor where the contents go.
    ViewGroup mContentParent;

这里有两行注释,它说这个window上的View实际上就是放在这个mContentParent中,这个mContentParent要么就是一个DecorView,要么就是一个DecorView的一个子类。这个DecorView我在 三个案例带你看懂LayoutInflater中inflate方法两个参数和三个参数的区别一文中已经提到过,DecorView是我们页面中的一个顶级View,ActionBar和setContentView所设置的布局就是设置在DecorView中。在我们的sdk中有这样一个文件..\platforms\android-24\data\res\layout\screen_title.xml,这个文件实际就是我们常用的一个Activity的布局文件,如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:fitsSystemWindows="true">
    <!-- Popout bar for action modes -->
    <ViewStub android:id="@+id/action_mode_bar_stub"
              android:inflatedId="@+id/action_mode_bar"
              android:layout="@layout/action_mode_bar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:theme="?attr/actionBarTheme" />
    <FrameLayout
        android:layout_width="match_parent" 
        android:layout_height="?android:attr/windowTitleSize"
        style="?android:attr/windowTitleBackgroundStyle">
        <TextView android:id="@android:id/title" 
            style="?android:attr/windowTitleStyle"
            android:background="@null"
            android:fadingEdge="horizontal"
            android:gravity="center_vertical"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    </FrameLayout>
    <FrameLayout android:id="@android:id/content"
        android:layout_width="match_parent" 
        android:layout_height="0dip"
        android:layout_weight="1"
        android:foregroundGravity="fill_horizontal|top"
        android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>

小伙伴们看到,这里有两个FrameLayout,第一个是一个Text View,这个毫无疑问就是我们显示ActionBar的地方,第二个的id就是content,这个就是我们setContentVIew就是给这里设置布局。

如此看来我们往页面添加的View最终都是要添加到mContentParent中,我们继续往下看installDecor方法:

    private void installDecor() {
        mForceDecorInstall = false;
        if (mDecor == null) {
            mDecor = generateDecor(-1);
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            }
        } else {
            mDecor.setWindow(this);
        }
        if (mContentParent == null) {
            mContentParent = generateLayout(mDecor);

            // Set up decor part of UI to ignore fitsSystemWindows if appropriate.
            mDecor.makeOptionalFitsSystemWindows();

            final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById(
                    R.id.decor_content_parent);

            if (decorContentParent != null) {
                mDecorContentParent = decorContentParent;
                mDecorContentParent.setWindowCallback(getCallback());
                if (mDecorContentParent.getTitle() == null) {
                    mDecorContentParent.setWindowTitle(mTitle);
                }

                final int localFeatures = getLocalFeatures();
                for (int i = 0; i < FEATURE_MAX; i++) {
                    if ((localFeatures & (1 << i)) != 0) {
                        mDecorContentParent.initFeature(i);
                    }
                }

                mDecorContentParent.setUiOptions(mUiOptions);

                if ((mResourcesSetFlags & FLAG_RESOURCE_SET_ICON) != 0 ||
                        (mIconRes != 0 && !mDecorContentParent.hasIcon())) {
                    mDecorContentParent.setIcon(mIconRes);
                } else if ((mResourcesSetFlags & FLAG_RESOURCE_SET_ICON) == 0 &&
                        mIconRes == 0 && !mDecorContentParent.hasIcon()) {
                    mDecorContentParent.setIcon(
                            getContext().getPackageManager().getDefaultActivityIcon());
                    mResourcesSetFlags |= FLAG_RESOURCE_SET_ICON_FALLBACK;
                }
                if ((mResourcesSetFlags & FLAG_RESOURCE_SET_LOGO) != 0 ||
                        (mLogoRes != 0 && !mDecorContentParent.hasLogo())) {
                    mDecorContentParent.setLogo(mLogoRes);
                }

                // Invalidate if the panel menu hasn't been created before this.
                // Panel menu invalidation is deferred avoiding application onCreateOptionsMenu
                // being called in the middle of onCreate or similar.
                // A pending invalidation will typically be resolved before the posted message
                // would run normally in order to satisfy instance state restoration.
                PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false);
                if (!isDestroyed() && (st == null || st.menu == null) && !mIsStartingWindow) {
                    invalidatePanelMenu(FEATURE_ACTION_BAR);
                }
            } else {
                mTitleView = (TextView) findViewById(R.id.title);
                if (mTitleView != null) {
                    if ((getLocalFeatures() & (1 << FEATURE_NO_TITLE)) != 0) {
                        final View titleContainer = findViewById(R.id.title_container);
                        if (titleContainer != null) {
                            titleContainer.setVisibility(View.GONE);
                        } else {
                            mTitleView.setVisibility(View.GONE);
                        }
                        mContentParent.setForeground(null);
                    } else {
                        mTitleView.setText(mTitle);
                    }
                }
            }

            if (mDecor.getBackground() == null && mBackgroundFallbackResource != 0) {
                mDecor.setBackgroundFallback(mBackgroundFallbackResource);
            }

            // Only inflate or create a new TransitionManager if the caller hasn't
            // already set a custom one.
            if (hasFeature(FEATURE_ACTIVITY_TRANSITIONS)) {
                if (mTransitionManager == null) {
                    final int transitionRes = getWindowStyle().getResourceId(
                            R.styleable.Window_windowContentTransitionManager,
                            0);
                    if (transitionRes != 0) {
                        final TransitionInflater inflater = TransitionInflater.from(getContext());
                        mTransitionManager = inflater.inflateTransitionManager(transitionRes,
                                mContentParent);
                    } else {
                        mTransitionManager = new TransitionManager();
                    }
                }

                mEnterTransition = getTransition(mEnterTransition, null,
                        R.styleable.Window_windowEnterTransition);
                mReturnTransition = getTransition(mReturnTransition, USE_DEFAULT_TRANSITION,
                        R.styleable.Window_windowReturnTransition);
                mExitTransition = getTransition(mExitTransition, null,
                        R.styleable.Window_windowExitTransition);
                mReenterTransition = getTransition(mReenterTransition, USE_DEFAULT_TRANSITION,
                        R.styleable.Window_windowReenterTransition);
                mSharedElementEnterTransition = getTransition(mSharedElementEnterTransition, null,
                        R.styleable.Window_windowSharedElementEnterTransition);
                mSharedElementReturnTransition = getTransition(mSharedElementReturnTransition,
                        USE_DEFAULT_TRANSITION,
                        R.styleable.Window_windowSharedElementReturnTransition);
                mSharedElementExitTransition = getTransition(mSharedElementExitTransition, null,
                        R.styleable.Window_windowSharedElementExitTransition);
                mSharedElementReenterTransition = getTransition(mSharedElementReenterTransition,
                        USE_DEFAULT_TRANSITION,
                        R.styleable.Window_windowSharedElementReenterTransition);
                if (mAllowEnterTransitionOverlap == null) {
                    mAllowEnterTransitionOverlap = getWindowStyle().getBoolean(
                            R.styleable.Window_windowAllowEnterTransitionOverlap, true);
                }
                if (mAllowReturnTransitionOverlap == null) {
                    mAllowReturnTransitionOverlap = getWindowStyle().getBoolean(
                            R.styleable.Window_windowAllowReturnTransitionOverlap, true);
                }
                if (mBackgroundFadeDurationMillis < 0) {
                    mBackgroundFadeDurationMillis = getWindowStyle().getInteger(
                            R.styleable.Window_windowTransitionBackgroundFadeDuration,
                            DEFAULT_BACKGROUND_FADE_DURATION_MS);
                }
                if (mSharedElementsUseOverlay == null) {
                    mSharedElementsUseOverlay = getWindowStyle().getBoolean(
                            R.styleable.Window_windowSharedElementsUseOverlay, true);
                }
            }
        }
    }

这个方法略长,但是却并不难,首先如果mDecor为null,就先生成一个,然后设置相关参数,第5行代码表示只有当mDecor的子View都没有获取焦点的时候mDecor才获取焦点,这个如果小伙伴们知道ListView中如何屏蔽Button的点击事件的话,应该对这个属性会很熟悉。第6行代码表示设置这个mDecor为整个Activity的根节点。第13行,如果mContentParent为null,则根据mDecor生成一个mContentParent,这个具体生成的方法我们一会再看。第19行获取一个DecorContentParent对象,这个东西实际上是一个接口,主要用来为mDecor提供不同的title等。第23行,如果decorContentParent 不为null,则给其分别设置回调、ICON、Logo等属性;否则如果decorContentParent 不为null,则从62行开始,先判断窗口是否包含FEATURE_NO_TITLE属性,如果包含,则隐藏窗口的TextView,否则给窗口的TextVIew设置要显示的Title。接下来从98行到115行都是设置各种出入场动画。小伙伴们看到这就是所谓的installDecor方法,其实很简单,就是大量的准备工作。OK,最后我们再来瞅一眼generateLayout方法,这个方法有300多行,我这里贴出一部分,如下:

    protected ViewGroup generateLayout(DecorView decor) {
        // Apply data from current theme.

        TypedArray a = getWindowStyle();

        if (false) {
            System.out.println("From style:");
            String s = "Attrs:";
            for (int i = 0; i < R.styleable.Window.length; i++) {
                s = s + " " + Integer.toHexString(R.styleable.Window[i]) + "="
                        + a.getString(i);
            }
            System.out.println(s);
        }

        mIsFloating = a.getBoolean(R.styleable.Window_windowIsFloating, false);
        int flagsToUpdate = (FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR)
                & (~getForcedWindowFlags());
        if (mIsFloating) {
            setLayout(WRAP_CONTENT, WRAP_CONTENT);
            setFlags(0, flagsToUpdate);
        } else {
            setFlags(FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR, flagsToUpdate);
        }

        if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
            requestFeature(FEATURE_NO_TITLE);
        } else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) {
            // Don't allow an action bar if there is no title.
            requestFeature(FEATURE_ACTION_BAR);
        }
		....
		....

        mDecor.startChanging();
        mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);

        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);

        return contentParent;
    }

大家注意,从第16行开始,都是读取各种feature和flag设置给窗口(这些东西都是我们在setContentView之前所设置的那些东西),36行的代码通过inflater将布局资源加载成一个View树。第38行将布局中的id为content的View查找到赋值给cotentParent,并将之返回(id为content的View就是我们文章开始贴出来的源码中的第二个FrameLayout)。第36行这个加载过程也很有趣,值得一看:

    void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
        mStackId = getStackId();

        if (mBackdropFrameRenderer != null) {
            loadBackgroundDrawablesIfNeeded();
            mBackdropFrameRenderer.onResourcesLoaded(
                    this, mResizingBackgroundDrawable, mCaptionBackgroundDrawable,
                    mUserCaptionBackgroundDrawable, getCurrentColor(mStatusColorViewState),
                    getCurrentColor(mNavigationColorViewState));
        }

        mDecorCaptionView = createDecorCaptionView(inflater);
        final View root = inflater.inflate(layoutResource, null);
        if (mDecorCaptionView != null) {
            if (mDecorCaptionView.getParent() == null) {
                addView(mDecorCaptionView,
                        new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
            }
            mDecorCaptionView.addView(root,
                    new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));
        } else {

            // Put it below the color views.
            addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
        }
        mContentRoot = (ViewGroup) root;
        initializeElevation();
    }

13行将布局文件加载出来,19行将加载得到的View树添加到mDecorCaptionView中。


OK,至此我们setContentView基本上分析完了。最后我们再来回顾看一下setContentView方法,我再在下面贴出:

@Override
    public void setContentView(int layoutResID) {
        // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
        // decor, when theme attributes and the like are crystalized. Do not check the feature
        // before this happens.
        if (mContentParent == null) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                    getContext());
            transitionTo(newScene);
        } else {
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }

首先我们的所有View最终都是添加在mContenParent上的,如果mContentParent为null,则安装installDecor,第17行通过inflater的inflate方法将布局资源加载成View树并添加到mContentParent中。完成之后调用Window的onContentChanged方法,表示View已经添加完成,我们已经可以通过findViewById来查找相应的View了。这里大家有一个要注意的地方,那就是第一次运行的时候我们需要将mDecor初始化,也需要将mContentParent初始化,以后都不需要了,以后只要将mContentParent上的View移除掉并重新绘制即可。


有问题欢迎留言讨论。


以上。



作者:u012702547 发表于2016/9/27 22:48:30 原文链接
阅读:178 评论:0 查看评论

Android官方开发文档Training系列课程中文版:线程执行操作之定义线程执行代码

$
0
0

原文地址:http://android.xsoftlab.net/training/multiple-threads/index.html

引言

大量的数据处理往往需要花费很长的时间,但如果将这些工作切分并行处理,那么它的速度与效率就会提升很多。在拥有多线程处理器的设备中,系统可以使线程并行运行。比如,使用多线程将图像文件切分解码展示要比单一线程解码快得多。

这章我内容们将会学习如何设置并使用多线程及线程池。我们还会学习如何在一个线程中运行代码以及如何使该线程与UI线程进行通信。

定义在线程中运行代码

这节课我们会学习如何实现Runnable接口,该接口中的run()方法会在线程中单独执行。你也可以将Runnable对象传递给一个Thread类。这种执行特定任务的Runnable对象在某些时候被称为任务

Thread类与Runnable类同属于基础类,它们仅提供了有限的功能。它们是比如HandlerThread, AsyncTask, 以及IntentService等线程功能类的基础核心。这两个类同样属于ThreadPoolExecutor的核心基础。ThreadPoolExecutor会自动管理线程以及任务队列,它甚至还可以使多个线程同时执行。

定义Runnable实现类

实现一个Runnable对象很简单:

public class PhotoDecodeRunnable implements Runnable {
    ...
    @Override
    public void run() {
        /*
         * Code you want to run on the thread goes here
         */
        ...
    }
    ...
}

实现run()方法

Runnable的实现类中,Runnablerun()方法中所含的代码将会被执行。通常来说,Runnable中可以做任何事情。要记得,这里的Runnable不会运行在UI线程,所以在它内部不能直接修改View对象这种UI对象。如果要与UI线程通讯,你需要使用到Communicate with the UI Thread课程中所描述的技术。

run()方法的开头处设置当前的线程使用后台优先级。这种方式可以减少Runnable对象所属线程与UI线程的资源争夺问题。

这里还将Runnable对象所属的线程引用存储了起来。由Thread.currentThread()可以获得当前的线程对象。

下面是代码的具体实现方式:

class PhotoDecodeRunnable implements Runnable {
...
    /*
     * Defines the code to run for this task.
     */
    @Override
    public void run() {
        // Moves the current Thread into the background
        android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND);
        ...
        /*
         * Stores the current Thread in the PhotoTask instance,
         * so that the instance
         * can interrupt the Thread.
         */
        mPhotoTask.setImageDecodeThread(Thread.currentThread());
        ...
    }
...
}
作者:u011064099 发表于2016/9/28 7:56:35 原文链接
阅读:88 评论:0 查看评论

Android 6.0 - 动态权限管理的解决方案

$
0
0

Android 6.0 - 动态权限管理的解决方案

Android 6.0版本(Api 23)推出了很多新的特性, 大幅提升了用户体验, 同时也为程序员带来新的负担. 动态权限管理就是这样, 一方面让用户更加容易的控制自己的隐私, 一方面需要重新适配应用权限. 时代总是不断发展, 程序总是以人为本, 让我们为应用添加动态权限管理吧! 这里提供了一个非常不错的解决方案, 提供源码, 项目可以直接使用.

Android系统包含默认的授权提示框, 但是我们仍需要设置自己的页面. 原因是系统提供的授权框, 会有不再提示的选项. 如果用户选择, 则无法触发授权提示. 使用自定义的提示页面, 可以给予用户手动修改授权的指导.

本文示例的GitHub下载地址

在Api 23中, 权限需要动态获取, 核心权限必须满足. 标准流程:


如果用户点击, 不再提示, 则系统授权弹窗将不会弹出. 流程变为:


流程就这些, 让我们看看代码吧.

1. 权限

在AndroidManifest中, 添加两个权限, 录音修改音量.


<code class="xml"><span class="hljs-comment" style="color: rgb(136, 0, 0);"><!--危险权限--></span>
    <span class="hljs-tag" style="color: rgb(0, 102, 102);"><<span class="hljs-title" style="color: rgb(0, 0, 136);">uses-permission</span> <span class="hljs-attribute" style="color: rgb(102, 0, 102);">android:name</span>=<span class="hljs-value" style="color: rgb(0, 136, 0);">"android.permission.RECORD_AUDIO"</span>/></span>

    <span class="hljs-comment" style="color: rgb(136, 0, 0);"><!--一般权限--></span>
    <span class="hljs-tag" style="color: rgb(0, 102, 102);"><<span class="hljs-title" style="color: rgb(0, 0, 136);">uses-permission</span> <span class="hljs-attribute" style="color: rgb(102, 0, 102);">android:name</span>=<span class="hljs-value" style="color: rgb(0, 136, 0);">"android.permission.MODIFY_AUDIO_SETTINGS"</span>/></span></code>

危险权限必须要授权, 一般权限不需要.

检测权限类

<code class="java"><span class="hljs-comment" style="color: rgb(136, 0, 0);">/**
 * 检查权限的工具类
 * <p/>
 * Created by wangchenlong on 16/1/26.
 */</span>
<span class="hljs-keyword" style="color: rgb(0, 0, 136);">public</span> <span class="hljs-class"><span class="hljs-keyword" style="color: rgb(0, 0, 136);">class</span> <span class="hljs-title" style="color: rgb(102, 0, 102);">PermissionsChecker</span> </span>{
    <span class="hljs-keyword" style="color: rgb(0, 0, 136);">private</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136);">final</span> Context mContext;

    <span class="hljs-function"><span class="hljs-keyword" style="color: rgb(0, 0, 136);">public</span> <span class="hljs-title">PermissionsChecker</span><span class="hljs-params" style="color: rgb(102, 0, 102);">(Context context)</span> </span>{
        mContext = context.getApplicationContext();
    }

    <span class="hljs-comment" style="color: rgb(136, 0, 0);">// 判断权限集合</span>
    <span class="hljs-function"><span class="hljs-keyword" style="color: rgb(0, 0, 136);">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136);">boolean</span> <span class="hljs-title">lacksPermissions</span><span class="hljs-params" style="color: rgb(102, 0, 102);">(String... permissions)</span> </span>{
        <span class="hljs-keyword" style="color: rgb(0, 0, 136);">for</span> (String permission : permissions) {
            <span class="hljs-keyword" style="color: rgb(0, 0, 136);">if</span> (lacksPermission(permission)) {
                <span class="hljs-keyword" style="color: rgb(0, 0, 136);">return</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136);">true</span>;
            }
        }
        <span class="hljs-keyword" style="color: rgb(0, 0, 136);">return</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136);">false</span>;
    }

    <span class="hljs-comment" style="color: rgb(136, 0, 0);">// 判断是否缺少权限</span>
    <span class="hljs-function"><span class="hljs-keyword" style="color: rgb(0, 0, 136);">private</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136);">boolean</span> <span class="hljs-title">lacksPermission</span><span class="hljs-params" style="color: rgb(102, 0, 102);">(String permission)</span> </span>{
        <span class="hljs-keyword" style="color: rgb(0, 0, 136);">return</span> ContextCompat.checkSelfPermission(mContext, permission) ==
                PackageManager.PERMISSION_DENIED;
    }
}</code>

2. 首页

假设首页需要使用权限, 在页面显示前, 即onResume时, 检测权限,
如果缺少, 则进入权限获取页面; 接收返回值, 拒绝权限时, 直接关闭.

<code class="java"><span class="hljs-keyword" style="color: rgb(0, 0, 136);">public</span> <span class="hljs-class"><span class="hljs-keyword" style="color: rgb(0, 0, 136);">class</span> <span class="hljs-title" style="color: rgb(102, 0, 102);">MainActivity</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136);">extends</span> <span class="hljs-title" style="color: rgb(102, 0, 102);">AppCompatActivity</span> </span>{

    <span class="hljs-keyword" style="color: rgb(0, 0, 136);">private</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136);">static</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136);">final</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136);">int</span> REQUEST_CODE = <span class="hljs-number" style="color: rgb(0, 102, 102);">0</span>; <span class="hljs-comment" style="color: rgb(136, 0, 0);">// 请求码</span>

    <span class="hljs-comment" style="color: rgb(136, 0, 0);">// 所需的全部权限</span>
    <span class="hljs-keyword" style="color: rgb(0, 0, 136);">static</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136);">final</span> String[] PERMISSIONS = <span class="hljs-keyword" style="color: rgb(0, 0, 136);">new</span> String[]{
            Manifest.permission.RECORD_AUDIO,
            Manifest.permission.MODIFY_AUDIO_SETTINGS
    };

    <span class="hljs-annotation" style="color: rgb(155, 133, 157);">@Bind</span>(R.id.main_t_toolbar) Toolbar mTToolbar;

    <span class="hljs-keyword" style="color: rgb(0, 0, 136);">private</span> PermissionsChecker mPermissionsChecker; <span class="hljs-comment" style="color: rgb(136, 0, 0);">// 权限检测器</span>

    <span class="hljs-annotation" style="color: rgb(155, 133, 157);">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword" style="color: rgb(0, 0, 136);">protected</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136);">void</span> <span class="hljs-title">onCreate</span><span class="hljs-params" style="color: rgb(102, 0, 102);">(Bundle savedInstanceState)</span> </span>{
        <span class="hljs-keyword" style="color: rgb(0, 0, 136);">super</span>.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(<span class="hljs-keyword" style="color: rgb(0, 0, 136);">this</span>);

        setSupportActionBar(mTToolbar);

        mPermissionsChecker = <span class="hljs-keyword" style="color: rgb(0, 0, 136);">new</span> PermissionsChecker(<span class="hljs-keyword" style="color: rgb(0, 0, 136);">this</span>);
    }

    <span class="hljs-annotation" style="color: rgb(155, 133, 157);">@Override</span> <span class="hljs-function"><span class="hljs-keyword" style="color: rgb(0, 0, 136);">protected</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136);">void</span> <span class="hljs-title">onResume</span><span class="hljs-params" style="color: rgb(102, 0, 102);">()</span> </span>{
        <span class="hljs-keyword" style="color: rgb(0, 0, 136);">super</span>.onResume();

        <span class="hljs-comment" style="color: rgb(136, 0, 0);">// 缺少权限时, 进入权限配置页面</span>
        <span class="hljs-keyword" style="color: rgb(0, 0, 136);">if</span> (mPermissionsChecker.lacksPermissions(PERMISSIONS)) {
            startPermissionsActivity();
        }
    }

    <span class="hljs-function"><span class="hljs-keyword" style="color: rgb(0, 0, 136);">private</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136);">void</span> <span class="hljs-title">startPermissionsActivity</span><span class="hljs-params" style="color: rgb(102, 0, 102);">()</span> </span>{
        PermissionsActivity.startActivityForResult(<span class="hljs-keyword" style="color: rgb(0, 0, 136);">this</span>, REQUEST_CODE, PERMISSIONS);
    }

    <span class="hljs-annotation" style="color: rgb(155, 133, 157);">@Override</span> <span class="hljs-function"><span class="hljs-keyword" style="color: rgb(0, 0, 136);">protected</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136);">void</span> <span class="hljs-title">onActivityResult</span><span class="hljs-params" style="color: rgb(102, 0, 102);">(<span class="hljs-keyword" style="color: rgb(0, 0, 136);">int</span> requestCode, <span class="hljs-keyword" style="color: rgb(0, 0, 136);">int</span> resultCode, Intent data)</span> </span>{
        <span class="hljs-keyword" style="color: rgb(0, 0, 136);">super</span>.onActivityResult(requestCode, resultCode, data);
        <span class="hljs-comment" style="color: rgb(136, 0, 0);">// 拒绝时, 关闭页面, 缺少主要权限, 无法运行</span>
        <span class="hljs-keyword" style="color: rgb(0, 0, 136);">if</span> (requestCode == REQUEST_CODE && resultCode == PermissionsActivity.PERMISSIONS_DENIED) {
            finish();
        }
    }
}</code>

核心权限必须满足, 如摄像应用, 摄像头权限就是必须的, 如果用户不予授权, 则直接关闭.

3. 授权页

授权页, 首先使用系统默认的授权页, 当用户拒绝时, 指导用户手动设置, 当用户再次操作失败后, 返回继续提示. 用户手动退出授权页时, 给使用页发送授权失败的通知.

<code class="java"><span class="hljs-comment" style="color: rgb(136, 0, 0);">/**
 * 权限获取页面
 * <p/>
 * Created by wangchenlong on 16/1/26.
 */</span>
<span class="hljs-keyword" style="color: rgb(0, 0, 136);">public</span> <span class="hljs-class"><span class="hljs-keyword" style="color: rgb(0, 0, 136);">class</span> <span class="hljs-title" style="color: rgb(102, 0, 102);">PermissionsActivity</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136);">extends</span> <span class="hljs-title" style="color: rgb(102, 0, 102);">AppCompatActivity</span> </span>{

    <span class="hljs-keyword" style="color: rgb(0, 0, 136);">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136);">static</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136);">final</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136);">int</span> PERMISSIONS_GRANTED = <span class="hljs-number" style="color: rgb(0, 102, 102);">0</span>; <span class="hljs-comment" style="color: rgb(136, 0, 0);">// 权限授权</span>
    <span class="hljs-keyword" style="color: rgb(0, 0, 136);">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136);">static</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136);">final</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136);">int</span> PERMISSIONS_DENIED = <span class="hljs-number" style="color: rgb(0, 102, 102);">1</span>; <span class="hljs-comment" style="color: rgb(136, 0, 0);">// 权限拒绝</span>

    <span class="hljs-keyword" style="color: rgb(0, 0, 136);">private</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136);">static</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136);">final</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136);">int</span> PERMISSION_REQUEST_CODE = <span class="hljs-number" style="color: rgb(0, 102, 102);">0</span>; <span class="hljs-comment" style="color: rgb(136, 0, 0);">// 系统权限管理页面的参数</span>
    <span class="hljs-keyword" style="color: rgb(0, 0, 136);">private</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136);">static</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136);">final</span> String EXTRA_PERMISSIONS =
            <span class="hljs-string" style="color: rgb(0, 136, 0);">"me.chunyu.clwang.permission.extra_permission"</span>; <span class="hljs-comment" style="color: rgb(136, 0, 0);">// 权限参数</span>
    <span class="hljs-keyword" style="color: rgb(0, 0, 136);">private</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136);">static</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136);">final</span> String PACKAGE_URL_SCHEME = <span class="hljs-string" style="color: rgb(0, 136, 0);">"package:"</span>; <span class="hljs-comment" style="color: rgb(136, 0, 0);">// 方案</span>

    <span class="hljs-keyword" style="color: rgb(0, 0, 136);">private</span> PermissionsChecker mChecker; <span class="hljs-comment" style="color: rgb(136, 0, 0);">// 权限检测器</span>
    <span class="hljs-keyword" style="color: rgb(0, 0, 136);">private</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136);">boolean</span> isRequireCheck; <span class="hljs-comment" style="color: rgb(136, 0, 0);">// 是否需要系统权限检测</span>

    <span class="hljs-comment" style="color: rgb(136, 0, 0);">// 启动当前权限页面的公开接口</span>
    <span class="hljs-function"><span class="hljs-keyword" style="color: rgb(0, 0, 136);">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136);">static</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136);">void</span> <span class="hljs-title">startActivityForResult</span><span class="hljs-params" style="color: rgb(102, 0, 102);">(Activity activity, <span class="hljs-keyword" style="color: rgb(0, 0, 136);">int</span> requestCode, String... permissions)</span> </span>{
        Intent intent = <span class="hljs-keyword" style="color: rgb(0, 0, 136);">new</span> Intent(activity, PermissionsActivity.class);
        intent.putExtra(EXTRA_PERMISSIONS, permissions);
        ActivityCompat.startActivityForResult(activity, intent, requestCode, <span class="hljs-keyword" style="color: rgb(0, 0, 136);">null</span>);
    }

    <span class="hljs-annotation" style="color: rgb(155, 133, 157);">@Override</span> <span class="hljs-function"><span class="hljs-keyword" style="color: rgb(0, 0, 136);">protected</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136);">void</span> <span class="hljs-title">onCreate</span><span class="hljs-params" style="color: rgb(102, 0, 102);">(@Nullable Bundle savedInstanceState)</span> </span>{
        <span class="hljs-keyword" style="color: rgb(0, 0, 136);">super</span>.onCreate(savedInstanceState);
        <span class="hljs-keyword" style="color: rgb(0, 0, 136);">if</span> (getIntent() == <span class="hljs-keyword" style="color: rgb(0, 0, 136);">null</span> || !getIntent().hasExtra(EXTRA_PERMISSIONS)) {
            <span class="hljs-keyword" style="color: rgb(0, 0, 136);">throw</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136);">new</span> RuntimeException(<span class="hljs-string" style="color: rgb(0, 136, 0);">"PermissionsActivity需要使用静态startActivityForResult方法启动!"</span>);
        }
        setContentView(R.layout.activity_permissions);

        mChecker = <span class="hljs-keyword" style="color: rgb(0, 0, 136);">new</span> PermissionsChecker(<span class="hljs-keyword" style="color: rgb(0, 0, 136);">this</span>);
        isRequireCheck = <span class="hljs-keyword" style="color: rgb(0, 0, 136);">true</span>;
    }

    <span class="hljs-annotation" style="color: rgb(155, 133, 157);">@Override</span> <span class="hljs-function"><span class="hljs-keyword" style="color: rgb(0, 0, 136);">protected</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136);">void</span> <span class="hljs-title">onResume</span><span class="hljs-params" style="color: rgb(102, 0, 102);">()</span> </span>{
        <span class="hljs-keyword" style="color: rgb(0, 0, 136);">super</span>.onResume();
        <span class="hljs-keyword" style="color: rgb(0, 0, 136);">if</span> (isRequireCheck) {
            String[] permissions = getPermissions();
            <span class="hljs-keyword" style="color: rgb(0, 0, 136);">if</span> (mChecker.lacksPermissions(permissions)) {
                requestPermissions(permissions); <span class="hljs-comment" style="color: rgb(136, 0, 0);">// 请求权限</span>
            } <span class="hljs-keyword" style="color: rgb(0, 0, 136);">else</span> {
                allPermissionsGranted(); <span class="hljs-comment" style="color: rgb(136, 0, 0);">// 全部权限都已获取</span>
            }
        } <span class="hljs-keyword" style="color: rgb(0, 0, 136);">else</span> {
            isRequireCheck = <span class="hljs-keyword" style="color: rgb(0, 0, 136);">true</span>;
        }
    }

    <span class="hljs-comment" style="color: rgb(136, 0, 0);">// 返回传递的权限参数</span>
    <span class="hljs-keyword" style="color: rgb(0, 0, 136);">private</span> String[] getPermissions() {
        <span class="hljs-keyword" style="color: rgb(0, 0, 136);">return</span> getIntent().getStringArrayExtra(EXTRA_PERMISSIONS);
    }

    <span class="hljs-comment" style="color: rgb(136, 0, 0);">// 请求权限兼容低版本</span>
    <span class="hljs-function"><span class="hljs-keyword" style="color: rgb(0, 0, 136);">private</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136);">void</span> <span class="hljs-title">requestPermissions</span><span class="hljs-params" style="color: rgb(102, 0, 102);">(String... permissions)</span> </span>{
        ActivityCompat.requestPermissions(<span class="hljs-keyword" style="color: rgb(0, 0, 136);">this</span>, permissions, PERMISSION_REQUEST_CODE);
    }

    <span class="hljs-comment" style="color: rgb(136, 0, 0);">// 全部权限均已获取</span>
    <span class="hljs-function"><span class="hljs-keyword" style="color: rgb(0, 0, 136);">private</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136);">void</span> <span class="hljs-title">allPermissionsGranted</span><span class="hljs-params" style="color: rgb(102, 0, 102);">()</span> </span>{
        setResult(PERMISSIONS_GRANTED);
        finish();
    }

    <span class="hljs-comment" style="color: rgb(136, 0, 0);">/**
     * 用户权限处理,
     * 如果全部获取, 则直接过.
     * 如果权限缺失, 则提示Dialog.
     *
     * <span class="hljs-doctag">@param</span> requestCode  请求码
     * <span class="hljs-doctag">@param</span> permissions  权限
     * <span class="hljs-doctag">@param</span> grantResults 结果
     */</span>
    <span class="hljs-annotation" style="color: rgb(155, 133, 157);">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword" style="color: rgb(0, 0, 136);">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136);">void</span> <span class="hljs-title">onRequestPermissionsResult</span><span class="hljs-params" style="color: rgb(102, 0, 102);">(<span class="hljs-keyword" style="color: rgb(0, 0, 136);">int</span> requestCode, @NonNull String[] permissions, @NonNull <span class="hljs-keyword" style="color: rgb(0, 0, 136);">int</span>[] grantResults)</span> </span>{
        <span class="hljs-keyword" style="color: rgb(0, 0, 136);">if</span> (requestCode == PERMISSION_REQUEST_CODE && hasAllPermissionsGranted(grantResults)) {
            isRequireCheck = <span class="hljs-keyword" style="color: rgb(0, 0, 136);">true</span>;
            allPermissionsGranted();
        } <span class="hljs-keyword" style="color: rgb(0, 0, 136);">else</span> {
            isRequireCheck = <span class="hljs-keyword" style="color: rgb(0, 0, 136);">false</span>;
            showMissingPermissionDialog();
        }
    }

    <span class="hljs-comment" style="color: rgb(136, 0, 0);">// 含有全部的权限</span>
    <span class="hljs-function"><span class="hljs-keyword" style="color: rgb(0, 0, 136);">private</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136);">boolean</span> <span class="hljs-title">hasAllPermissionsGranted</span><span class="hljs-params" style="color: rgb(102, 0, 102);">(@NonNull <span class="hljs-keyword" style="color: rgb(0, 0, 136);">int</span>[] grantResults)</span> </span>{
        <span class="hljs-keyword" style="color: rgb(0, 0, 136);">for</span> (<span class="hljs-keyword" style="color: rgb(0, 0, 136);">int</span> grantResult : grantResults) {
            <span class="hljs-keyword" style="color: rgb(0, 0, 136);">if</span> (grantResult == PackageManager.PERMISSION_DENIED) {
                <span class="hljs-keyword" style="color: rgb(0, 0, 136);">return</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136);">false</span>;
            }
        }
        <span class="hljs-keyword" style="color: rgb(0, 0, 136);">return</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136);">true</span>;
    }

    <span class="hljs-comment" style="color: rgb(136, 0, 0);">// 显示缺失权限提示</span>
    <span class="hljs-function"><span class="hljs-keyword" style="color: rgb(0, 0, 136);">private</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136);">void</span> <span class="hljs-title">showMissingPermissionDialog</span><span class="hljs-params" style="color: rgb(102, 0, 102);">()</span> </span>{
        AlertDialog.Builder builder = <span class="hljs-keyword" style="color: rgb(0, 0, 136);">new</span> AlertDialog.Builder(PermissionsActivity.<span class="hljs-keyword" style="color: rgb(0, 0, 136);">this</span>);
        builder.setTitle(R.string.help);
        builder.setMessage(R.string.string_help_text);

        <span class="hljs-comment" style="color: rgb(136, 0, 0);">// 拒绝, 退出应用</span>
        builder.setNegativeButton(R.string.quit, <span class="hljs-keyword" style="color: rgb(0, 0, 136);">new</span> DialogInterface.OnClickListener() {
            <span class="hljs-annotation" style="color: rgb(155, 133, 157);">@Override</span> <span class="hljs-function"><span class="hljs-keyword" style="color: rgb(0, 0, 136);">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136);">void</span> <span class="hljs-title">onClick</span><span class="hljs-params" style="color: rgb(102, 0, 102);">(DialogInterface dialog, <span class="hljs-keyword" style="color: rgb(0, 0, 136);">int</span> which)</span> </span>{
                setResult(PERMISSIONS_DENIED);
                finish();
            }
        });

        builder.setPositiveButton(R.string.settings, <span class="hljs-keyword" style="color: rgb(0, 0, 136);">new</span> DialogInterface.OnClickListener() {
            <span class="hljs-annotation" style="color: rgb(155, 133, 157);">@Override</span> <span class="hljs-function"><span class="hljs-keyword" style="color: rgb(0, 0, 136);">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136);">void</span> <span class="hljs-title">onClick</span><span class="hljs-params" style="color: rgb(102, 0, 102);">(DialogInterface dialog, <span class="hljs-keyword" style="color: rgb(0, 0, 136);">int</span> which)</span> </span>{
                startAppSettings();
            }
        });

        builder.show();
    }

    <span class="hljs-comment" style="color: rgb(136, 0, 0);">// 启动应用的设置</span>
    <span class="hljs-function"><span class="hljs-keyword" style="color: rgb(0, 0, 136);">private</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136);">void</span> <span class="hljs-title">startAppSettings</span><span class="hljs-params" style="color: rgb(102, 0, 102);">()</span> </span>{
        Intent intent = <span class="hljs-keyword" style="color: rgb(0, 0, 136);">new</span> Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
        intent.setData(Uri.parse(PACKAGE_URL_SCHEME + getPackageName()));
        startActivity(intent);
    }
}</code>

注意isRequireCheck参数的使用, 防止和系统提示框重叠.
系统授权提示: ActivityCompat.requestPermissions, ActivityCompat兼容低版本.

效果


关键部分就这些了, 动态权限授权虽然给程序员带来了一些麻烦, 但是对用户还是很有必要的, 我们也应该欢迎, 毕竟每个程序员都是半个产品经理.

危险权限列表

作者:woainijinying 发表于2016/9/28 22:04:41 原文链接
阅读:151 评论:0 查看评论

手把手教你做音乐播放器(五)音乐列表的存储(下)

$
0
0

5.2 MusicService的配合

MusicService对外提供添加播放列表的接口,对内要管理PlayListContentProvider。它提供了下面的接口,

  • addPlayList():添加播放列表。这里添加列表应该有两种形式,一种是一次性添加多首音乐,一种是一次就添加一首音乐。

  • getPlayList():获取播放列表

播放列表要保存到一个数组里面,所以我们要创建一个mPlayList实现列表的保存,

public class MusicService extends Service {

    private List<MusicItem> mPlayList;
    private ContentResolver mResolver;

    @Override
    public void onCreate() {
        super.onCreate();

        //获取ContentProvider的解析器,避免以后每次使用的时候都要重新获取
        mResolver = getContentResolver();
        //保存播放列表
        mPlayList = new ArrayList<MusicItem>();
    }

}

5.2.1 添加播放列表

addPlayList()应该有两种形式,一次添加一首音乐和一次添加多首音乐。

public class MusicService extends Service {
    public class MusicServiceIBinder extends Binder {
        ......

        //一次添加多首音乐
       public void addPlayList(List<MusicItem> items) {
           addPlayListInner(items);
       }

        //一次添加一首音乐
       public void addPlayList(MusicItem item) {
           addPlayListInner(item);
       }
    }

    //一次添加多首音乐具体实现
    private void addPlayListInner(List<MusicItem> items) {

    }

    //一次添加一首音乐具体实现
    private void addPlayListInner(MusicItem item) {

    }

}

5.2.1.1 添加一首音乐

private void addPlayListInner(MusicItem item) {

    //判断列表中是否已经存储过该音乐,如果存储过就不管它
    if(mPlayList.contains(item)) {
      return;
    }

    //添加到播放列表的第一个位置
    mPlayList.add(0, item);
    //将音乐信息保存到ContentProvider中
    insertMusicItemToContentProvider(item);
}

//访问ContentProvider,保存一条数据
private void insertMusicItemToContentProvider(MusicItem item) {

    ContentValues cv = new ContentValues();
    cv.put(DBHelper.NAME, item.name);
    cv.put(DBHelper.DURATION, item.duration);
    cv.put(DBHelper.LAST_PLAY_TIME, item.playedTime);
    cv.put(DBHelper.SONG_URI, item.songUri.toString());
    cv.put(DBHelper.ALBUM_URI, item.albumUri.toString());
    Uri uri = mResolver.insert(PlayListContentProvider.CONTENT_SONGS_URI, cv);
}

判断mPlayList是否保存了音乐,需要对MusicItem做特别的处理:给它定义一个比较的准则–什么情况下两个相比较的内容是相同的。我们这个例子当中,应该是音乐的Uri相同,则认为两者相同。重写MusicItemequals()方法,当使用mPlayList.contains(item)判断的时候,会调用到这个重写的方法进行比较,

public class MusicItem {

    ......
    //重写MusicItem的equals()方法
    @Override
    public boolean equals(Object o) {
        MusicItem another = (MusicItem) o;

        //音乐的Uri相同,则说明两者相同
        return another.songUri.equals(this.songUri);
    }
}

5.2.1.2 添加多首音乐

private void addPlayListInner(List<MusicItem> items) {

    //清空数据库中的playlist_table
    mResolver.delete(PlayListContentProvider.CONTENT_SONGS_URI, null, null);
    //清空缓存的播放列表
    mPlayList.clear();

    //将每首音乐添加到播放列表的缓存和数据库中
    for (MusicItem item : items) {
        //利用现成的代码,便于代码的维护
        addPlayListInner(item);
    }
}

5.2.2 获取播放列表

为了获取播放列表在ContentProvider启动的时候,需要它将数据库中现有的列表,加载到mPlayList当中,

public class MusicService extends Service {

    @Override
    public void onCreate() {
        super.onCreate();

        ......
        initPlayList();
        ......
    }

    private void initPlayList() {
        mPlayList.clear();

        Cursor cursor = mResolver.query(
                PlayListContentProvider.CONTENT_SONGS_URI,
                null,
                null,
                null,
                null);

        while(cursor.moveToNext()) {
            String songUri = cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.SONG_URI));
            String albumUri = cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.ALBUM_URI));
            String name = cursor.getString(cursor.getColumnIndex(DBHelper.NAME));
            long playedTime = cursor.getLong(cursor.getColumnIndexOrThrow(DBHelper.LAST_PLAY_TIME));
            long duration = cursor.getLong(cursor.getColumnIndexOrThrow(DBHelper.DURATION));
            MusicItem item = new MusicItem(Uri.parse(songUri), Uri.parse(albumUri), name, duration, playedTime);
            mPlayList.add(item);
        }

        cursor.close();
    }
}

之后在需要的时候,直接将mPlayList返回就可以了,

public class MusicService extends Service {
    public class MusicServiceIBinder extends Binder {
        ......

       public List<MusicItem> getPlayList() {
            return mPlayList;
        }
    }

}

5.3 主界面播放列表的添加

MusicListActivity有两种方式添加播放列表:

  1. 单击音乐项,将单首音乐添加到播放列表中;

  2. 长按音乐项,界面切换成ListView的多选模式,将多首音乐用替换掉方式添加到播放器当中;

这里我们先来实现单击音乐项的添加,多选模式我们放到后面单独的章节来介绍。

修改点击列表的监听器,通过MusicService提供的接口,把要添加的音乐交给MusicService处理,

//修改监听器
private AdapterView.OnItemClickListener mOnMusicItemClickListener = new AdapterView.OnItemClickListener() {

   @Override
   public void onItemClick(AdapterView<?> parent, View view, int position, long id) {

        MusicItem item = mMusicList.get(position);
        if(mMusicService != null) {
                //通过MusicService提供的接口,把要添加的音乐交给MusicService处理
                mMusicService.addPlayList(mMusicList.get(position));
        }
   }
};

接下来开始实现显示播放列表的功能,给MusicListActivity做一个Menu菜单,定义菜单的xml文件main_menu.xml

<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:apps="http://schemas.android.com/apk/res-auto">
    <item
        android:title="@string/play_list"
        apps:showAsAction="always"
        android:icon="@mipmap/ic_playlist"
        android:id="@+id/play_list_menu"/>

</menu>

将菜单添加到MusicListActivity当中,

public class MusicListActivity extends AppCompatActivity {
    ......
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.main_menu, menu);
        return true;
    }
}

当用户点击该菜单的时候,弹出一个自带列表的对话框,将播放列表显示出来,

public class MusicListActivity extends AppCompatActivity {

    ......
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {

        switch (item.getItemId()) {
            case R.id.play_list_menu: {
                //响应用户对菜单的点击,显示播放列表
                showPlayList();
            }
            break;

        }

        return true;
    }

    private void showPlayList() {

        final AlertDialog.Builder builder=new AlertDialog.Builder(this);
        //设置对话框的图标
        builder.setIcon(R.mipmap.ic_playlist);
        //设计对话框的显示标题
        builder.setTitle(R.string.play_list);

        //获取播放列表,把播放列表中歌曲的名字取出组成新的列表
        List<MusicItem> playList = mMusicService.getPlayList();
        ArrayList<String> data = new ArrayList<String>();
        for(MusicItem music : playList) {
            data.add(music.name);
        }
        if(data.size() > 0) {
            //播放列表有曲目,显示音乐的名称
            ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, data);
            builder.setAdapter(adapter, null);
        }
        else {
            //播放列表没有曲目,显示没有音乐
            builder.setMessage(getString(R.string.no_song));
        }

        //设置该对话框是可以自动取消的,例如当用户在空白处随便点击一下,对话框就会关闭消失
        builder.setCancelable(true);

        //创建并显示对话框
        builder.create().show();
    }
}

/*******************************************************************/
* 版权声明
* 本教程只在CSDN安豆网发布,其他网站出现本教程均属侵权。

*另外,我们还推出了Arduino智能硬件相关的教程,您可以在我们的网店安豆的杂货铺中购买相关硬件。同时也感谢大家对我们这些码农的支持。

*最后再次感谢各位读者对安豆的支持,谢谢:)
/*******************************************************************/

作者:anddlecn 发表于2016/9/28 22:47:35 原文链接
阅读:89 评论:0 查看评论

Android事件分发浅谈

$
0
0
Android事件分发机制浅谈
前言:可能Android的事件分发对于刚学Android的童鞋来说接触得不多,这样不奇怪。因为刚学的时候,一般人很难注意到或是会选择主动去了解。那么究竟什么是Android的事件分发呢?

或许刚说出来,有点觉悟的新手会想到就是那些按钮的点击事件、或是说监听。而这些也确实是Android事件分发的其中一部分。由于Android的事件分发其实是可以有很多变化的,特别是当你需要自定义View的时候,很多情况都需要具体分析,所以大体上它都不容易精确的掌握。但如果是主流的,大概的事件分发机制其实也没那么难理解,说到底这可以说是一个浅入深出的问题吧。那么接下来我们来浅谈一下这次的主题Android事件分发机制。

1.比喻
打个比方,Android的事件分发中的事件(用户的触屏)就像一块饼。假若有一家3代的人在饥荒的年代里,如果爷爷有一块饼,那么他会先给谁?那当然会是孙子。但爷爷年纪大视力不好,所以他把饼传递给了儿子,而儿子又把这块饼传给了孙子,最终由孙子吃下了那块饼。而像这样的父穿子,子传孙的方法,就如同我们Android中的事件分发一样。

接下来我们新建一个工程并写好如上图的布局,这是一个最外层包着RelativeLayout(爷爷),中间是LinearLayout(儿子),最里面是Button(孙子)。


图画得不是很好,大家就凑合这看吧。上面就是我们布局的上个View。而触摸事件(饼)就是通过RelativeLayout--->LinearLayout--->Button,这样一层层的传递的。而事件的分发就是这样通过disppatchTouchEvent(),OnInterceptTouchEvent(),OnTouchEvent()这三个方法去判断事件是否继续向下传递。

而当其中有一层View自己先截获了事件消费掉了(可理解为自己用掉了点击事件),那么事件则不会向下传递,而在OnTouchEvent中返回False则不会自己消费并返回到上一层去处理。

看到这里大家肯定还是不明白,接着到java代码里面,按着 Ctrl+ Shift+ T,查找一下View的API,这里我选API23的,其实API多少都没太大变化,我就选一个最新的。



3.理解dispatchTouchEvent(),onInterceptTouchEvent(),OnTouchEvent()三个事件

这三个事件理解起来,首先要区分View与ViewGroup两种情况:
* View:dispatchTouchEvent,OnTouchEvent
* ViewGroup:dispatchTouchEvent,onInterceptTouchEvent,OnTouchEvent

其中View是没有onInterceptTouchEvent方法的。在Android中你只要触摸控件首先都会触发控件的dispatchTouchEvent方法,所以我们会找View的API,而不是找Button的API,因为这个方法一般在具体的控件类中是找不到的,他是父View的方法。
下面我贴出找到的dispatchTouchEvent源码,看看官方是怎么解析的。

理解dispatchTouchEvent()
/**
     * Pass the touch screen motion event down to the target view, or this
     * view if it is the target.
     *
     * @param event The motion event to be dispatched.
     * @return True if the event was handled by the view, false otherwise.
     */
    public boolean dispatchTouchEvent(MotionEvent event) {
        // If the event should be handled by accessibility focus first.
        if (event.isTargetAccessibilityFocus()) {
            // We don't have focus or no virtual descendant has it, do not handle the event.
            if (!isAccessibilityFocusedViewOrHost()) {
                return false;
            }
            // We have focus and got the event, then use normal event dispatch.
            event.setTargetAccessibilityFocus(false);
        }
        boolean result = false;
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }
        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Defensive cleanup for new gesture
            stopNestedScroll();
        }
        if (onFilterTouchEventForSecurity(event)) {
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }
        if (!result && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        }
        // Clean up after nested scrolls if this is the end of a gesture;
        // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
        // of the gesture.
        if (actionMasked == MotionEvent.ACTION_UP ||
                actionMasked == MotionEvent.ACTION_CANCEL ||
                (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
            stopNestedScroll();
        }
        return result;
    }

看上面的官方解析,大概意思是:这个View接收的是从屏幕传递过来的事件并传给目标的View,或是这个View就是从屏幕传达过来的事件的目标。

这句话怎么理解呢?其实也就对应我上面画的图,两种情况:要么该View就是目标的View(自己消费掉事件),要么向下传递事件。而这个方法默认是调用super.dispatchTouchEvent(event)的,需传入super.dispatchTouchEvent(true),需要注意的是他如果直接返回true或flase而不是进过调用supper的都不会向下传递。这里可能有点难理解,不用急,下面会继续解析。

接着我们看看onInterceptTouchEvent()方法。

理解onInterceptTouchEvent()



同样的快捷键方法我们来到ViewGroup的源码:

/**
     * Implement this method to intercept all touch screen motion events.  This
     * allows you to watch events as they are dispatched to your children, and
     * take ownership of the current gesture at any point.
     *
     * <p>Using this function takes some care, as it has a fairly complicated
     * interaction with {@link View#onTouchEvent(MotionEvent)
     * View.onTouchEvent(MotionEvent)}, and using it requires implementing
     * that method as well as this one in the correct way.  Events will be
     * received in the following order:
     *
     * <ol>
     * <li> You will receive the down event here.
     * <li> The down event will be handled either by a child of this view
     * group, or given to your own onTouchEvent() method to handle; this means
     * you should implement onTouchEvent() to return true, so you will
     * continue to see the rest of the gesture (instead of looking for
     * a parent view to handle it).  Also, by returning true from
     * onTouchEvent(), you will not receive any following
     * events in onInterceptTouchEvent() and all touch processing must
     * happen in onTouchEvent() like normal.
     * <li> For as long as you return false from this function, each following
     * event (up to and including the final up) will be delivered first here
     * and then to the target's onTouchEvent().
     * <li> If you return true from here, you will not receive any
     * following events: the target view will receive the same event but
     * with the action {@link MotionEvent#ACTION_CANCEL}, and all further
     * events will be delivered to your onTouchEvent() method and no longer
     * appear here.
     * </ol>
     *
     * @param ev The motion event being dispatched down the hierarchy.
     * @return Return true to steal motion events from the children and have
     * them dispatched to this ViewGroup through onTouchEvent().
     * The current target will receive an ACTION_CANCEL event, and no further
     * messages will be delivered here.
     */
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
                && ev.getAction() == MotionEvent.ACTION_DOWN
                && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
                && isOnScrollbarThumb(ev.getX(), ev.getY())) {
            return true;
        }
        return false;
    }


这里他的代码不是很多,但官方的注释却又很多,我们只看第一段,大概意思是:这个方法他拦截了所有的屏幕事件。他允许用户监测这些事件,并且可以分发到他的子View里面,作为子View的一个手势(或是说任何点)的手势。

所以从上面我们着重的可以知道,onInterceptTouchEvent()这个方法可以拦截事件的分发。而当onInterceptTouchEvent()返回的是false的时候就说明不拦截这个View的子View,那么子View就可以获取到这个事件了。

OnTouchEvent()方法在View跟ViewGroup都有,所以要分开讨论。我们现在可以理解为,若是OnTouchEvent()返回true则代表这一层的View自己消费掉事件,而返回false,那么事件会重新返回到该View的父View,也就是上一层的View中。




当RelativeLayout在onInterceptTouchEvent()里面不拦截子View的时候,事件就会传递到LinearLayout的dispatchTouchEvent()事件里面。而同样LinearLayout也是继承ViewGroup的,所以他也有onInterceptTouchEvent方法。

而LinearLayout的OnTouchEvent()里面,如果返回true则代表自己消费掉事件,而如果返回false则表示不作处理并返回给上层父View处理

4.结合例子理解

这个是我编写的上图的布局,里面三个都是简单的自定义View,作用是方便打印出信息

<org.dispatchtouchevent002.CustomRelativieLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="org.heima.dispatchtouchevent002.MainActivity" >
    <org.dispatchtouchevent002.CustomLinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical" >
        <org.dispatchtouchevent002.CustomButton
            android:id="@+id/btn"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="click Me" />
    </org.dispatchtouchevent002.CustomLinearLayout>
</org.dispatchtouchevent002.CustomRelativieLayout>

接下来我们在这些View里面重写dispatchTouchEvent()和OnTouchEvent()两个方法,分别在里面打印Log,查看结果。

RelativeLayout
public class CustomRelativieLayout extends RelativeLayout {
	public CustomRelativieLayout(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
	}
	public CustomRelativieLayout(Context context, AttributeSet attrs) {
		super(context, attrs);
	}
	public CustomRelativieLayout(Context context) {
		super(context);
	}
	@Override
	public boolean dispatchTouchEvent(MotionEvent ev) {
		return super.dispatchTouchEvent(ev);
	}
	@Override
	public boolean onInterceptTouchEvent(MotionEvent ev) {
		return false;
	}
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		return super.onTouchEvent(event);
	}
}

LinearLayout
public class CustomLinearLayout extends LinearLayout{
	public CustomLinearLayout(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
	}
	public CustomLinearLayout(Context context, AttributeSet attrs) {
		super(context, attrs);
	}
	public CustomLinearLayout(Context context) {
		super(context);
	}
	
	@Override
	public boolean dispatchTouchEvent(MotionEvent ev) {
		return super.dispatchTouchEvent(ev);
	}
	
	@Override
	public boolean onInterceptTouchEvent(MotionEvent ev) {
		return super.onInterceptTouchEvent(ev);
	}
	
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		return super.onTouchEvent(event);
	}
}

Button
public class CustomButton extends Button{
	public CustomButton(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
	}
	public CustomButton(Context context, AttributeSet attrs) {
		super(context, attrs);
	}
	public CustomButton(Context context) {
		super(context);
	}
	
	@Override
	public boolean dispatchTouchEvent(MotionEvent event) {
		return super.dispatchTouchEvent(event);
	}
	
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		return super.onTouchEvent(event);
	}
}

1)首先我们来写三个View中的三个方法里的Log日志
RelativeLayout
@Override
	public boolean dispatchTouchEvent(MotionEvent ev) {
		Log.d("TouchEvent", "CustomRelativieLayout:dispatchTouchEvent");
		return super.dispatchTouchEvent(ev);
	}
	@Override
	public boolean onInterceptTouchEvent(MotionEvent ev) {
		Log.d("TouchEvent", "CustomRelativieLayout:onInterceptTouchEvent");
		return false;
	}
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		Log.d("TouchEvent", "CustomRelativieLayout:onTouchEvent");
		return super.onTouchEvent(event);
	}
LinearLayout
@Override
	public boolean dispatchTouchEvent(MotionEvent ev) {
		Log.d("TouchEvent", "CustomLinearLayout:dispatchTouchEvent");
		return super.dispatchTouchEvent(ev);
	}
	
	@Override
	public boolean onInterceptTouchEvent(MotionEvent ev) {
		Log.d("TouchEvent", "CustomLinearLayout:onInterceptTouchEvent");
		return super.onInterceptTouchEvent(ev);
	}
	
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		Log.d("TouchEvent", "CustomLinearLayout:onTouchEvent");
		return super.onTouchEvent(event);
	}
Button
@Override
	public boolean dispatchTouchEvent(MotionEvent event) {
		Log.d("TouchEvent", "CustomButton:dispatchTouchEvent");
		return super.dispatchTouchEvent(event);
	}
	
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		Log.d("TouchEvent", "CustomButton:onTouchEvent");
		return super.onTouchEvent(event);
	}

这里需要注意的是,在Activity中其实也含有onInterceptTouchEvent()和OnTouchEvent()这两个方法,所以我们也需要重写这两个方法。
MainActivity
public class MainActivity extends Activity {
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		Button btn=(Button) findViewById(R.id.btn);
		btn.setOnClickListener(new OnClickListener() {
			
			@Override
			public void onClick(View v) {
				Log.d("TouchEvent", "MainActivity:onClick");
			}
		});
	}
	@Override
	public boolean dispatchTouchEvent(MotionEvent ev) {
		Log.d("TouchEvent", "MainActivity:dispatchTouchEvent");
		return super.dispatchTouchEvent(ev);
	}
	
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		Log.d("TouchEvent", "MainActivity:onTouchEvent");
		return super.onTouchEvent(event);
	}
}

之后我们按下按钮。因为点击事件是由按下和抬起两部分组成的,所以上述的Log日志会打印两次。在Android里面,按下和抬起是分别处理的两个不同的事件,可以看到打印结果如下:
按下
09-27 17:27:41.222: D/TouchEvent(1493): MainActivity:dispatchTouchEvent
09-27 17:27:41.222: D/TouchEvent(1493): CustomRelativieLayout:dispatchTouchEvent
09-27 17:27:41.222: D/TouchEvent(1493): CustomRelativieLayout:onInterceptTouchEvent
09-27 17:27:41.222: D/TouchEvent(1493): CustomLinearLayout:dispatchTouchEvent
09-27 17:27:41.222: D/TouchEvent(1493): CustomLinearLayout:onInterceptTouchEvent
09-27 17:27:41.222: D/TouchEvent(1493): CustomButton:dispatchTouchEvent
09-27 17:27:41.222: D/TouchEvent(1493): CustomButton:onTouchEvent
抬起
09-27 17:27:41.321: D/TouchEvent(1493): MainActivity:dispatchTouchEvent
09-27 17:27:41.321: D/TouchEvent(1493): CustomRelativieLayout:dispatchTouchEvent
09-27 17:27:41.321: D/TouchEvent(1493): CustomRelativieLayout:onInterceptTouchEvent
09-27 17:27:41.321: D/TouchEvent(1493): CustomLinearLayout:dispatchTouchEvent
09-27 17:27:41.321: D/TouchEvent(1493): CustomLinearLayout:onInterceptTouchEvent
09-27 17:27:41.321: D/TouchEvent(1493): CustomButton:dispatchTouchEvent
09-27 17:27:41.321: D/TouchEvent(1493): CustomButton:onTouchEvent
09-27 17:27:41.331: D/TouchEvent(1493): MainActivity:onClick

总结1:
配合我画的图,结合上面的Log日志可以看出,点击事件最终被Button的onClick事件所消费。
MainActivity:
dispatchTouchEvent()为默认,向下传递。
Relative:
dispatchTouchEvent()为默认true,向下传递。
onInterceptTouchEvent()为默认flase。不拦截事件,向下传递。
LinearLayout:
dispatchTouchEvent()为默认true,向下传递。
onInterceptTouchEvent()为默认flase。不拦截事件,向下传递。
Button:
dispatchTouchEvent()为默认true,向下传递。
OnTouchEvent():为默认false,默认不处理

事件到了Button的OnTouchEvent()里面后,由于默认是false,OnTouchEvent()方法不处理。,又向最上层的父View返回了,

看到这里的Log日志再配合上面的图,是不是应该能稍微理解了一些呢?若是不了解的话,那还得自己多看几遍,或是自己也试试打印Log测试一下了。


2)接着我们来讨论上面理解dispatchTouchEvent()时出现的一种情况:dispatchTouchEvent()返回true或false時不向下传递事件,当只有调用super.dispatchTouchEvent()的时候才会。

在这里我试着修改RelativeLayout里面的dispatchTouchEvent(),其余布局不变
RelativeLayout:dispatchTouchEvent()返回true
 @Override
	public boolean dispatchTouchEvent(MotionEvent ev) {
		Log.d("TouchEvent", "CustomRelativieLayout:dispatchTouchEvent");
		return true;
	}
Log日志:
09-28 01:23:09.349: D/TouchEvent(1420): MainActivity:dispatchTouchEvent
09-28 01:23:09.349: D/TouchEvent(1420): CustomRelativieLayout:dispatchTouchEvent
09-28 01:23:09.630: D/TouchEvent(1420): MainActivity:dispatchTouchEvent
09-28 01:23:09.630: D/TouchEvent(1420): CustomRelativieLayout:dispatchTouchEvent

RelativeLayout:dispatchTouchEvent()返回false
@Override
	public boolean dispatchTouchEvent(MotionEvent ev) {
		Log.d("TouchEvent", "CustomRelativieLayout:dispatchTouchEvent");
		return false;
	}
Log日志:
09-28 01:26:03.632: D/TouchEvent(1470): MainActivity:dispatchTouchEvent
09-28 01:26:03.632: D/TouchEvent(1470): CustomRelativieLayout:dispatchTouchEvent
09-28 01:26:03.632: D/TouchEvent(1470): MainActivity:onTouchEvent
09-28 01:26:03.822: D/TouchEvent(1470): MainActivity:dispatchTouchEvent
09-28 01:26:03.822: D/TouchEvent(1470): MainActivity:onTouchEvent

总结2:
当dispatchTouchEvent()返回true或者flase的时候,事件不向下传递,只有返回的是 super.dispatchTouchEvent()才会进行传递。并且返回false的时候事件会返回到上层View的onTouchEvent(),但如果父View的onTouchEvent为默认(默认不处理)。那么最终这个事件会没有响应,不被任何层的View所消费掉。

3)接下来我们调用super.dispatchTouchEvent(),但不把他返回,而是选择返回true或者false,观察情况如何:

RelativeLayout:调用super.dispatchTouchEvent(),返回true:
@Override
	public boolean dispatchTouchEvent(MotionEvent ev) {
		Log.d("TouchEvent", "CustomRelativieLayout:dispatchTouchEvent");
		boolean event = super.dispatchTouchEvent(ev);
		Log.d("TouchEvent", "Touch:"+event);
		return true;
	}
Log日志:

09-28 02:26:36.926: D/TouchEvent(1580): CustomRelativieLayout:dispatchTouchEvent
09-28 02:26:36.926: D/TouchEvent(1580): CustomRelativieLayout:onInterceptTouchEvent
09-28 02:26:36.926: D/TouchEvent(1580): CustomLinearLayout:dispatchTouchEvent
09-28 02:26:36.926: D/TouchEvent(1580): CustomLinearLayout:onInterceptTouchEvent
09-28 02:26:36.926: D/TouchEvent(1580): CustomButton:dispatchTouchEvent
09-28 02:26:36.926: D/TouchEvent(1580): CustomButton:onTouchEvent
09-28 02:26:36.926: D/TouchEvent(1580): Touch:true
09-28 02:26:37.147: D/TouchEvent(1580): MainActivity:dispatchTouchEvent
09-28 02:26:37.147: D/TouchEvent(1580): CustomRelativieLayout:dispatchTouchEvent
09-28 02:26:37.147: D/TouchEvent(1580): CustomRelativieLayout:onInterceptTouchEvent
09-28 02:26:37.147: D/TouchEvent(1580): CustomLinearLayout:dispatchTouchEvent
09-28 02:26:37.147: D/TouchEvent(1580): CustomLinearLayout:onInterceptTouchEvent
09-28 02:26:37.147: D/TouchEvent(1580): CustomButton:dispatchTouchEvent
09-28 02:26:37.147: D/TouchEvent(1580): CustomButton:onTouchEvent
09-28 02:26:37.147: D/TouchEvent(1580): Touch:true
09-28 02:26:37.147: D/TouchEvent(1580): MainActivity:onClick


RelativeLayout:调用super.dispatchTouchEvent(),返回false:
 @Override
	public boolean dispatchTouchEvent(MotionEvent ev) {
		Log.d("TouchEvent", "CustomRelativieLayout:dispatchTouchEvent");
		boolean event = super.dispatchTouchEvent(ev);
		Log.d("TouchEvent", "Touch:"+event);
		return false;
	}
Log日志:
09-28 02:30:20.600: D/TouchEvent(1629): MainActivity:dispatchTouchEvent
09-28 02:30:20.600: D/TouchEvent(1629): CustomRelativieLayout:dispatchTouchEvent
09-28 02:30:20.600: D/TouchEvent(1629): CustomRelativieLayout:onInterceptTouchEvent
09-28 02:30:20.600: D/TouchEvent(1629): CustomLinearLayout:dispatchTouchEvent
09-28 02:30:20.600: D/TouchEvent(1629): CustomLinearLayout:onInterceptTouchEvent
09-28 02:30:20.600: D/TouchEvent(1629): CustomButton:dispatchTouchEvent
09-28 02:30:20.600: D/TouchEvent(1629): CustomButton:onTouchEvent
09-28 02:30:20.600: D/TouchEvent(1629): Touch:true
09-28 02:30:20.600: D/TouchEvent(1629): MainActivity:onTouchEvent
09-28 02:30:20.800: D/TouchEvent(1629): MainActivity:dispatchTouchEvent
09-28 02:30:20.800: D/TouchEvent(1629): MainActivity:onTouchEvent

总结3:
从日志来看,当有调用super.dispatchTouchEvent()并且返回true的情况下,跟我们结论1里,直接返回super.dispatchTouchEvent()的结果是一样的。
但当super.dispatchTouchEvent()并且返回false时,事件走到一半就停止了,看到日志的第8行后ANCTION_DOWN(点下去)已经执行完了,而抬起来的事件只走到Activity里面的dispatchTouchEvent()就再也没有分发下去。

4)接着onInterceptTouchEvent()返回true,则为拦截事件的分发。
RelativeLayout:onInterceptTouchEvent()返回ture:
@Override
	public boolean dispatchTouchEvent(MotionEvent ev) {
		Log.d("TouchEvent", "CustomRelativieLayout:dispatchTouchEvent");
		return super.dispatchTouchEvent(ev);
	}
	@Override
	public boolean onInterceptTouchEvent(MotionEvent ev) {
		Log.d("TouchEvent", "CustomRelativieLayout:onInterceptTouchEvent");
		return true;
	}
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		Log.d("TouchEvent", "CustomRelativieLayout:onTouchEvent");
		boolean touch = super.onTouchEvent(event);
		Log.d("TouchEvent", "onToucheEvent:"+touch);
		return super.onTouchEvent(event);
	}
Log日志:
09-28 03:07:40.314: D/TouchEvent(1803): MainActivity:dispatchTouchEvent
09-28 03:07:40.314: D/TouchEvent(1803): CustomRelativieLayout:dispatchTouchEvent
09-28 03:07:40.314: D/TouchEvent(1803): CustomRelativieLayout:onInterceptTouchEvent
09-28 03:07:40.314: D/TouchEvent(1803): CustomRelativieLayout:onTouchEvent
09-28 03:07:40.314: D/TouchEvent(1803): onToucheEvent:false
09-28 03:07:40.314: D/TouchEvent(1803): MainActivity:onTouchEvent
09-28 03:07:40.504: D/TouchEvent(1803): MainActivity:dispatchTouchEvent
09-28 03:07:40.504: D/TouchEvent(1803): MainActivity:onTouchEvent

结4:
这里,我在RelativeLayout里面的OnTouchEvent()方法打印了super.onTouchEvent的值,可以看到当为false。当onInterceptTouchEvent()为true时,事件不分发给下一层的子View,而选择走自己的onToucheEvent()方法,但又是默认的false不处理。导致事件回到上一层的父View中,最终父View的onTouchEvent()也是默认为false,不处理事件。最后导致事件没有任何View响应,也就没有消费。

5)在Activity里,给Button设置OnTouchListener,在onTouch()中返回false,默认不拦截。
MainActivity:onTouch()返回false:默认不拦截
	btn.setOnClickListener(new OnClickListener() {
			
			@Override
			public void onClick(View v) {
				Log.d("TouchEvent", "MainActivity:onClick");
			}
		});
		
		btn.setOnTouchListener(new OnTouchListener() {
			
			@Override
			public boolean onTouch(View v, MotionEvent event) {
				Log.d("TouchEvent", "MainActivity:onTouch");
				return false;
			}
		});
RelativeLayout:onInterceptTouchEvent()返回false:不拦截事件
@Override
	public boolean dispatchTouchEvent(MotionEvent ev) {
		Log.d("TouchEvent", "CustomRelativieLayout:dispatchTouchEvent");
		return super.dispatchTouchEvent(ev);
	}
 
	@Override
	public boolean onInterceptTouchEvent(MotionEvent ev) {
		Log.d("TouchEvent", "CustomRelativieLayout:onInterceptTouchEvent");
		return true;
	}
 
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		Log.d("TouchEvent", "CustomRelativieLayout:onTouchEvent");s
		return super.onTouchEvent(event);
	}
Log日志:
09-28 04:01:34.433: D/TouchEvent(2172): MainActivity:dispatchTouchEvent
09-28 04:01:34.433: D/TouchEvent(2172): CustomRelativieLayout:dispatchTouchEvent
09-28 04:01:34.433: D/TouchEvent(2172): CustomRelativieLayout:onInterceptTouchEvent
09-28 04:01:34.433: D/TouchEvent(2172): CustomLinearLayout:dispatchTouchEvent
09-28 04:01:34.433: D/TouchEvent(2172): CustomLinearLayout:onInterceptTouchEvent
09-28 04:01:34.433: D/TouchEvent(2172): CustomButton:dispatchTouchEvent
09-28 04:01:34.433: D/TouchEvent(2172): MainActivity:onTouch
09-28 04:01:34.433: D/TouchEvent(2172): CustomButton:onTouchEvent
09-28 04:01:34.632: D/TouchEvent(2172): MainActivity:dispatchTouchEvent
09-28 04:01:34.632: D/TouchEvent(2172): CustomRelativieLayout:dispatchTouchEvent
09-28 04:01:34.632: D/TouchEvent(2172): CustomRelativieLayout:onInterceptTouchEvent
09-28 04:01:34.632: D/TouchEvent(2172): CustomLinearLayout:dispatchTouchEvent
09-28 04:01:34.632: D/TouchEvent(2172): CustomLinearLayout:onInterceptTouchEvent
09-28 04:01:34.632: D/TouchEvent(2172): CustomButton:dispatchTouchEvent
09-28 04:01:34.632: D/TouchEvent(2172): MainActivity:onTouch
09-28 04:01:34.632: D/TouchEvent(2172): CustomButton:onTouchEvent
09-28 04:01:34.632: D/TouchEvent(2172): MainActivity:onClick

总结5:
从上面的结果来看,事件在传递到Button的disptchTouchEvent(),然后回到Activity调用自己的onTouch(),dispatchTouchEvent结束后调用Button自己的onTouchEvent()(第8行),到此按下去的事件已经完成。
紧接着到抬起来的up事件。而执行的Log结果大致上也跟按下去的down事件差不多,但不同的是,up事件最终会在Button自己的OnTouchEvent()中响应,而onTouchEvent()会调用Buttton自己的onClick()方法,最后由onClick方法消费。

6)Button的onTouch()方法返回true
MainActivity:onTouchEvent返回true
btn.setOnTouchListener(new OnTouchListener() {
			@Override
			public boolean onTouch(View v, MotionEvent event) {
				Log.d("TouchEvent", "MainActivity:onTouch");
				return true;
			}
		});
Log日志:
09-28 04:23:15.712: D/TouchEvent(2223): MainActivity:dispatchTouchEvent
09-28 04:23:15.712: D/TouchEvent(2223): CustomRelativieLayout:dispatchTouchEvent
09-28 04:23:15.712: D/TouchEvent(2223): CustomRelativieLayout:onInterceptTouchEvent
09-28 04:23:15.712: D/TouchEvent(2223): CustomLinearLayout:dispatchTouchEvent
09-28 04:23:15.712: D/TouchEvent(2223): CustomLinearLayout:onInterceptTouchEvent
09-28 04:23:15.712: D/TouchEvent(2223): CustomButton:dispatchTouchEvent
09-28 04:23:15.722: D/TouchEvent(2223): MainActivity:onTouch
09-28 04:23:15.882: D/TouchEvent(2223): MainActivity:dispatchTouchEvent
09-28 04:23:15.882: D/TouchEvent(2223): CustomRelativieLayout:dispatchTouchEvent
09-28 04:23:15.882: D/TouchEvent(2223): CustomRelativieLayout:onInterceptTouchEvent
09-28 04:23:15.882: D/TouchEvent(2223): CustomLinearLayout:dispatchTouchEvent
09-28 04:23:15.882: D/TouchEvent(2223): CustomLinearLayout:onInterceptTouchEvent
09-28 04:23:15.882: D/TouchEvent(2223): CustomButton:dispatchTouchEvent
09-28 04:23:15.892: D/TouchEvent(2223): MainActivity:onTouch

总结6:
可以看到onTouch()设置为true,事件到最后也没传递到onClick()里面,而是由Button的onTouch()给消费了。所以onTouch()方法应该是先于onClick执行的,事件到了onTouch()(返回true)已经被消费了,那么整个方法都已经返回了,事件就不会进一步传递,自然没有onClcik的事。

到这里我们差不多可以结合源码来解析一下我们锁看到的现象了。

7.dispathTouchEvent()和onTouch()源码理解

找到View中dispathTouchEvent()的源码:
public boolean dispatchTouchEvent(MotionEvent event) {
        // If the event should be handled by accessibility focus first.
        if (event.isTargetAccessibilityFocus()) {
            // We don't have focus or no virtual descendant has it, do not handle the event.
            if (!isAccessibilityFocusedViewOrHost()) {
                return false;
            }
            // We have focus and got the event, then use normal event dispatch.
            event.setTargetAccessibilityFocus(false);
        }
        boolean result = false;
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }
        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Defensive cleanup for new gesture
            stopNestedScroll();
        }
        if (onFilterTouchEventForSecurity(event)) {
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }
        if (!result && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        }
        // Clean up after nested scrolls if this is the end of a gesture;
        // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
        // of the gesture.
        if (actionMasked == MotionEvent.ACTION_UP ||
                actionMasked == MotionEvent.ACTION_CANCEL ||
                (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
            stopNestedScroll();
        }
        return result;
    }

从上面的代码24行开始可以看出dispathToychEvent()若能返回true的话都是要在,第24行或是第38行的判断中返回true,dispathTouchEvent才会为true。在这里其实可以看到前面的第24行的判断正是onTouch()方法的响应。而我们onTouch()方法的返回值就是写在Acivity中的代码,所以当onTouch()返回true的时候,事件在这里就已经消费了,而dispathToychEvent()也马上返回false。

而如果onTouch()返回false则事件会去到View自己的OnTouchEvent()方法里,那么onClick方法是怎么才调用到的呢?接着我们再看源码。

找到View中dispathTouchEvent()的源码:
public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();
        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return (((viewFlags & CLICKABLE) == CLICKABLE
                    || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
        }
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }
        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        // take focus if we don't have it already and we should in
                        // touch mode.
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }
                        if (prepressed) {
                            // The button is being released before we actually
                            // showed it as pressed.  Make it show the pressed
                            // state now (before scheduling the click) to ensure
                            // the user sees it.
                            setPressed(true, x, y);
                       }
                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            // This is a tap, so remove the longpress check
                            removeLongPressCallback();
                            // Only perform take click actions if we were in the pressed state
                            if (!focusTaken) {
                                // Use a Runnable and post this rather than calling
                                // performClick directly. This lets other visual state
                                // of the view update before click actions start.
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClick();
                                }
                            }
                        }
                        if (mUnsetPressedState == null) {
                            mUnsetPressedState = new UnsetPressedState();
                        }
                        if (prepressed) {
                            postDelayed(mUnsetPressedState,
                                    ViewConfiguration.getPressedStateDuration());
                        } else if (!post(mUnsetPressedState)) {
                            // If the post failed, unpress right now
                            mUnsetPressedState.run();
                        }
                        removeTapCallback();
                    }
                    mIgnoreNextUpEvent = false;
                    break;
                case MotionEvent.ACTION_DOWN:
                    mHasPerformedLongPress = false;
                    if (performButtonActionOnTouchDown(event)) {
                        break;
                    }
                    // Walk up the hierarchy to determine if we're inside a scrolling container.
                    boolean isInScrollingContainer = isInScrollingContainer();
                    // For views inside a scrolling container, delay the pressed feedback for
                    // a short period in case this is a scroll.
                    if (isInScrollingContainer) {
                        mPrivateFlags |= PFLAG_PREPRESSED;
                        if (mPendingCheckForTap == null) {
                            mPendingCheckForTap = new CheckForTap();
                        }
                        mPendingCheckForTap.x = event.getX();
                        mPendingCheckForTap.y = event.getY();
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    } else {
                        // Not inside a scrolling container, so show the feedback right away
                        setPressed(true, x, y);
                        checkForLongClick(0);
                    }
                    break;
                case MotionEvent.ACTION_CANCEL:
                    setPressed(false);
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    break;
                case MotionEvent.ACTION_MOVE:
                    drawableHotspotChanged(x, y);
                    // Be lenient about moving outside of buttons
                    if (!pointInView(x, y, mTouchSlop)) {
                        // Outside button
                        removeTapCallback();
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                            // Remove any future long press/tap checks
                            removeLongPressCallback();
                            setPressed(false);
                        }
                    }
                    break;
            }
            return true;
        }
        return false;
    }

代码很多,我们看到第24行,程序进过一些列的判断后会进入一个 switch()语句判断,而里面的case事件就是我们熟悉的ACTION_DOWN,ACTION_UP,ACTION_MOVE等事件的处理。

而我们的setOnClickListener在哪?Ctrl+F搜索一下,发现下面两段代码:
 public void setOnClickListener(@Nullable OnClickListener l) {
        if (!isClickable()) {
            setClickable(true);
        }
        getListenerInfo().mOnClickListener = l;
    }
其中有一个变量mOnClickListener.于是再搜索:
public boolean performClick() {
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
        return result;
    }

到这里我们可以知道performClick()方法是在OnTouchEvent()里面的ACTION_UP调用的。所以Android的事件分发都是这样进过一层层的View,再通过每个View中的dispatchTouchEvent()和onTouchEvent()里层层的判断,最终才会决定谁去消费这个事件。而这些层层的判断条件我已经写到图上去了,在这里不多做解析,想要继续深究的童鞋可以到源码中去找。

这次的浅谈就到此为止了,如果有问题的童靴欢迎一起探讨,互相学习,共同进步~












作者:u011070603 发表于2016/9/28 23:26:40 原文链接
阅读:98 评论:0 查看评论

升级xcode8,swift3的注意点和变化

$
0
0


1. Swift 2 and 3


过去每一版的Xcode 都和固定版本的Swift编程语言绑定在一起, Xcode 8中将不再如此,Swift 3给 Swift编程语言带来大量的语法变化,这些改变会让基于以前Swift版本的程序编译失败。

Xcode 8 中新创建的项目默认地使用Swift 3, 幸运地Xcode 8 在编译设置中支持开发者明确地选择Swift 2或Swift 2.3 进行编译。


支持Swift语言以往版本

这就是说你可以选择适合的时候迁移项目到Swift 3, 如果一个目标(Target)需要支持Swift 2.3, 需要在目标(Target)的编译设置里把Use Legacy Swift Language Version 设置成Yes。

Xcode 8 也提供了迁移工具帮你把项目升级到Swift 3, 不过Xcode提供的迁移工具让人喜忧参半,这次Swift 3 升级比上次升级有太多的变化,希望Xcode 8 的迁移工具会有所改善吧。

2. 源码编辑器扩展


Xcode 8 最让人惊喜的是支持源码编辑器第三方扩展,开发者对于本地插件系统已经翘首企盼了很多年,源码编辑器扩展算是沿着插件系统方向迈出坚实的第一步吧。

源码编辑器扩展很类似于Xcode的扩展,但苹果一再强调当前的扩展专注于源码的操作和导航,也就是为什么会叫做源码编辑器扩展,Xcode 8 甚至提供了编辑器扩展模板让你快速上手。


Xcode 源码编辑器扩展模板

对于当前扩展架构中我最喜欢的一点就是每个扩展都运行在不同的进程中,这就意味着扩展的异常崩溃不会引起Xcode的意外终止,随着本地插件的越来越多,特别是苹果发布了新版的Xcode, 分进程运行的优势会显得越来越突出。

扩展对比插件另一个优点是安全,你可以通过开发账号登录和发布扩展程序,苹果也提到开发者可以通过Mac 应用商店销售扩展,对于想通过销售扩展赚钱的开发者来说是很不错机会。

我对于源码编辑器扩展是非常兴奋的,虽然我是 Alcatraz 的粉丝,但苹果的本地扩展更贴近于未来正确的方向。非常期待开发者们未来会提供什么样的扩展,其实已经有人在GitHub 上发布了插件。

3. 调试


即使最好的开发者每天都要花费大量的时间进行调试程序,懂你的苹果提供了更好的工具让调试更简单,Xcode 8中针对调试做了不少显著的优化。

界面调试

Xcode 8中的界面调试非常强大,界面调试可以展示运行时(runtime)问题,改善后的界面调试对于调试不清晰或者不满意的布局变得更加简单。

运行时问题?没错,Xcode 8中除了编译时问题还提供了运行时问题,如果在运行时遇到自动布局的问题,Xcode会在左面导航面板把这些问题展示为运行时问题,这将会是个非常受欢迎的功能。


运行时问题

内存调试

Xcode 8 的内存调试功能针对查找内存泄漏和循环引用的问题也做了显著优化,我还不太确定在实际项目中效果如果,但看起来很棒。

内存调试

4. 代码签名


代码签名对于那些对苹果平台有兴趣开发者来说是非常不幸的,但幸运的是苹果没有做把头埋在沙子里的鸵鸟,苹果看到了开发者们遇到的问题并尝试解决,一些经验非常丰富的开发者也会不时遇到签名的问题,在今年的 Platforms State of the Union视频中, 苹果甚至自嘲自己的 修复问题(Fix Issue) 按钮,不仅很少时候能修复真正的问题,有时候会把问题弄得更糟。

修复问题(Fix Issue) 按钮通常不能修复问题

代码签名问题在Xcode 8 中将成为过去式,对于每一个目标(Target), 你可以勾选复选框让Xcode帮你管理代码签名,这个选项对于新项目默认是勾选的。在勾选的情况下,Xcode帮你管理证书,配置文件 和 应用标示等。

Code Signing Done for You

希望苹果这次能解决签名问题,数以万记的开发者们和我一起祈祷吧。

5. 其他改善和增强


San Francisco Mono字体

如果你非常享受使用精雕细琢的软件,你应该会很喜欢Xcode 8中的San Francisco Mono 字体,请参看下图:

San Francisco Mono

高亮当前行

你有没有注意到上面截图中的当前行被标示为高亮? 这是Xcode8中另一个受欢迎的功能,当前我在Xcode 7 中使用 Backlight for Xcode 实现类似功能,在Xcode 8 中将不再需要这个插件了。

图片代码自动完成

说到一些将被废弃的插件, 目前我在使用 Kent Sutherland开发的插件 KSImageNamed 能够在Xcode中帮助图片代码自动完成, 在Xcode 8 我将不需要这个插件,因为这个功能已经内置在Xcode 8 中。

图片自动完成

6. 文档


相信每个开发者都会在浏览和阅读文档上会花费很多时间,好的文档对于开发者有很大帮助,其实苹果的文档是非常优秀的,但提供的浏览方式却没有那么友好。

这个问题在 Xcode 8 将会被解决,新的文档格式看起来漂亮极了,且文档浏览会变得简单和快捷。苹果也针对内存问题做了相关优化,新版的内存占用会少很多。

下面是两张关于文档的截图,是不是极有设计感?

新文档浏览器图1

新文档浏览器图2


Xcode 8 的 6 大新功能一览

原文链接 作者:豆照建(译)

在2016 苹果全球开发者大会(WWDC)期间, 苹果一如既往地给开发者们披露了新版的集成开发工具 – Xcode, 在过去的每一次大版本发布中,苹果都会积极地改进开发工具,添加一些极具吸引力的新功能,今年也不例外。

1. Swift 2 and 3


过去每一版的Xcode 都和固定版本的Swift编程语言绑定在一起, Xcode 8中将不再如此,Swift 3给 Swift编程语言带来大量的语法变化,这些改变会让基于以前Swift版本的程序编译失败。

Xcode 8 中新创建的项目默认地使用Swift 3, 幸运地Xcode 8 在编译设置中支持开发者明确地选择Swift 2或Swift 2.3 进行编译。


支持Swift语言以往版本

这就是说你可以选择适合的时候迁移项目到Swift 3, 如果一个目标(Target)需要支持Swift 2.3, 需要在目标(Target)的编译设置里把Use Legacy Swift Language Version 设置成Yes。

Xcode 8 也提供了迁移工具帮你把项目升级到Swift 3, 不过Xcode提供的迁移工具让人喜忧参半,这次Swift 3 升级比上次升级有太多的变化,希望Xcode 8 的迁移工具会有所改善吧。

2. 源码编辑器扩展


Xcode 8 最让人惊喜的是支持源码编辑器第三方扩展,开发者对于本地插件系统已经翘首企盼了很多年,源码编辑器扩展算是沿着插件系统方向迈出坚实的第一步吧。

源码编辑器扩展很类似于Xcode的扩展,但苹果一再强调当前的扩展专注于源码的操作和导航,也就是为什么会叫做源码编辑器扩展,Xcode 8 甚至提供了编辑器扩展模板让你快速上手。


Xcode 源码编辑器扩展模板

对于当前扩展架构中我最喜欢的一点就是每个扩展都运行在不同的进程中,这就意味着扩展的异常崩溃不会引起Xcode的意外终止,随着本地插件的越来越多,特别是苹果发布了新版的Xcode, 分进程运行的优势会显得越来越突出。

扩展对比插件另一个优点是安全,你可以通过开发账号登录和发布扩展程序,苹果也提到开发者可以通过Mac 应用商店销售扩展,对于想通过销售扩展赚钱的开发者来说是很不错机会。

我对于源码编辑器扩展是非常兴奋的,虽然我是 Alcatraz 的粉丝,但苹果的本地扩展更贴近于未来正确的方向。非常期待开发者们未来会提供什么样的扩展,其实已经有人在GitHub 上发布了插件。

3. 调试


即使最好的开发者每天都要花费大量的时间进行调试程序,懂你的苹果提供了更好的工具让调试更简单,Xcode 8中针对调试做了不少显著的优化。

界面调试

Xcode 8中的界面调试非常强大,界面调试可以展示运行时(runtime)问题,改善后的界面调试对于调试不清晰或者不满意的布局变得更加简单。

运行时问题?没错,Xcode 8中除了编译时问题还提供了运行时问题,如果在运行时遇到自动布局的问题,Xcode会在左面导航面板把这些问题展示为运行时问题,这将会是个非常受欢迎的功能。


运行时问题

内存调试

Xcode 8 的内存调试功能针对查找内存泄漏和循环引用的问题也做了显著优化,我还不太确定在实际项目中效果如果,但看起来很棒。

内存调试

4. 代码签名


代码签名对于那些对苹果平台有兴趣开发者来说是非常不幸的,但幸运的是苹果没有做把头埋在沙子里的鸵鸟,苹果看到了开发者们遇到的问题并尝试解决,一些经验非常丰富的开发者也会不时遇到签名的问题,在今年的 Platforms State of the Union视频中, 苹果甚至自嘲自己的 修复问题(Fix Issue) 按钮,不仅很少时候能修复真正的问题,有时候会把问题弄得更糟。

修复问题(Fix Issue) 按钮通常不能修复问题

代码签名问题在Xcode 8 中将成为过去式,对于每一个目标(Target), 你可以勾选复选框让Xcode帮你管理代码签名,这个选项对于新项目默认是勾选的。在勾选的情况下,Xcode帮你管理证书,配置文件 和 应用标示等。

Code Signing Done for You

希望苹果这次能解决签名问题,数以万记的开发者们和我一起祈祷吧。

5. 其他改善和增强


San Francisco Mono字体

如果你非常享受使用精雕细琢的软件,你应该会很喜欢Xcode 8中的San Francisco Mono 字体,请参看下图:

San Francisco Mono

高亮当前行

你有没有注意到上面截图中的当前行被标示为高亮? 这是Xcode8中另一个受欢迎的功能,当前我在Xcode 7 中使用 Backlight for Xcode 实现类似功能,在Xcode 8 中将不再需要这个插件了。

图片代码自动完成

说到一些将被废弃的插件, 目前我在使用 Kent Sutherland开发的插件 KSImageNamed 能够在Xcode中帮助图片代码自动完成, 在Xcode 8 我将不需要这个插件,因为这个功能已经内置在Xcode 8 中。

图片自动完成

6. 文档


相信每个开发者都会在浏览和阅读文档上会花费很多时间,好的文档对于开发者有很大帮助,其实苹果的文档是非常优秀的,但提供的浏览方式却没有那么友好。

这个问题在 Xcode 8 将会被解决,新的文档格式看起来漂亮极了,且文档浏览会变得简单和快捷。苹果也针对内存问题做了相关优化,新版的内存占用会少很多。

下面是两张关于文档的截图,是不是极有设计感?

新文档浏览器图1

新文档浏览器图2

Notification(通知)

自从Notification被引入之后,苹果就不断的更新优化,但这些更新优化只是小打小闹,直至现在iOS 10开始真正的进行大改重构,这让开发者也体会到UserNotifications的易用,功能也变得非常强大。

  • iOS 9 以前的通知

1.在调用方法时,有些方法让人很难区分,容易写错方法,这让开发者有时候很苦恼。

2.应用在运行时和非运行时捕获通知的路径还不一致。

3.应用在前台时,是无法直接显示远程通知,还需要进一步处理。

4.已经发出的通知是不能更新的,内容发出时是不能改变的,并且只有简单文本展示方式,扩展性根本不是很好。

  • iOS 10 开始的通知

1.所有相关通知被统一到了UserNotifications.framework框架中。

2.增加了撤销、更新、中途还可以修改通知的内容。

3.通知不在是简单的文本了,可以加入视频、图片,自定义通知的展示等等。

4.iOS 10相对之前的通知来说更加好用易于管理,并且进行了大规模优化,对于开发者来说是一件好事。

5.iOS 10开始对于权限问题进行了优化,申请权限就比较简单了(本地与远程通知集成在一个方法中)。

ATS的问题

iOS 9中默认非HTTS的网络是被禁止的,当然我们也可以把NSAllowsArbitraryLoads设置为YES禁用ATS。不过iOS 10从2017年1月1日起苹果不允许我们通过这个方法跳过ATS,也就是说强制我们用HTTPS,如果不这样的话提交App可能会被拒绝。但是我们可以通过NSExceptionDomains来针对特定的域名开放HTTP可以容易通过审核。

NSExceptionDomains方式 设置域。可以简单理解成,把不支持https协议的接口设置成http的接口。

具体方法:

1.在项目的info.plist中添加一个Key:App Transport Security Settings,类型为字典类型。

2.然后给它添加一个Exception Domains,类型为字典类型;

3.把需要的支持的域添加給Exception Domains。其中域作为Key,类型为字典类型。

4.每个域下面需要设置3个属性:

NSIncludesSubdomains
NSExceptionRequiresForwardSecrecy
NSExceptionAllowsInsecureHTTPLoads

细节提示:在iOS9以后的系统中如果使用到网络图片,也要注意网络图片是否是HTTP的哦,如果是,也要把图片的域设置哦!

iOS 10 隐私权限设置

iOS 10 开始对隐私权限更加严格,如果你不设置就会直接崩溃,现在很多遇到崩溃问题了,一般解决办法都是在info.plist文件添加对应的Key-Value就可以了。


以上Value值,圈出的红线部分的文字是展示给用户看的,必须添加。

Xcode 8 运行一堆没用的logs解决办法



上图我们看到,自己新建的一个工程啥也没干就打印一堆烂七八糟的东西,我觉得这个应该是Xcode 8的问题,

具体也没细研究,解决办法是设置OS_ACTIVITY_MODE : disable如下图:


iOS 10 UIStatusBar方法过期:


在我们开发中有可能用到UIStatusBar一些属性,在iOS 10 中这些方法已经过期了,如果你的项目中有用的话就得需要适配。

上面的图片也能发现,如果在iOS 10中你需要使用preferredStatusBar比如这样:

- (UIStatusBarStyle)preferredStatusBarStyle { 
   return UIStatusBarStyleDefault;
}

iOS 10 UICollectionView 性能优化

随着开发者对UICollectionView的信赖,项目中用的地方也比较多,但是还是存在一些问题,比如有时会卡顿、加载慢等。所以iOS 10 对UICollectionView进一步的优化。

  • UICollectionView cell pre-fetching预加载机制
  • UICollectionView and UITableView prefetchDataSource 新增的API 针对self-sizing cells 的改进
  • Interactive reordering

在iOS 10 之前,UICollectionView上面如果有大量cell,当用户活动很快的时候,整个UICollectionView的卡顿会很明显,为什么会造成这样的问题,这里涉及到了iOS 系统的重用机制,当cell准备加载进屏幕的时候,整个cell都已经加载完成,等待在屏幕外面了,也就是整整一行cell都已经加载完毕,这就是造成卡顿的主要原因,专业术语叫做:掉帧.
要想让用户感觉不到卡顿,我们的app必须帧率达到60帧/秒,也就是说每帧16毫秒要刷新一次.

iOS 10 之前UICollectionViewCell的生命周期是这样的:

  • 1.用户滑动屏幕,屏幕外有一个cell准备加载进来,把cell从reusr队列拿出来,然后调用prepareForReuse方法,在这个方法里面,可以重置cell的状态,加载新的数据;
  • 2.继续滑动,就会调用cellForItemAtIndexPath方法,在这个方法里面给cell赋值模型,然后返回给系统;
  • 3.当cell马上进去屏幕的时候,就会调用willDisplayCell方法,在这个方法里面我们还可以修改cell,为进入屏幕做最后的准备工作;
  • 4.执行完willDisplayCell方法后,cell就进去屏幕了.当cell完全离开屏幕以后,会调用didEndDisplayingCell方法.

 iOS 10 UICollectionViewCell的生命周期是这样的:

  • 1.用户滑动屏幕,屏幕外有一个cell准备加载进来,把cell从reusr队列拿出来,然后调用prepareForReuse方法,在这里当cell还没有进去屏幕的时候,就已经提前调用这个方法了,对比之前的区别是之前是cell的上边缘马上进去屏幕的时候就会调用该方法,而iOS 10 提前到cell还在屏幕外面的时候就调用;
  • 2.在cellForItemAtIndexPath中创建cell,填充数据,刷新状态等操作,相比于之前也提前了;
  • 3.用户继续滑动的话,当cell马上就需要显示的时候我们再调用willDisplayCell方法,原则就是:何时需要显示,何时再去调用willDisplayCell方法;
  • 4.当cell完全离开屏幕以后,会调用didEndDisplayingCell方法,跟之前一样,cell会进入重用队列.
  • 在iOS 10 之前,cell只能从重用队列里面取出,再走一遍生命周期,并调用cellForItemAtIndexPath创建或者生成一个cell.
    在iOS 10 中,系统会cell保存一段时间,也就是说当用户把cell滑出屏幕以后,如果又滑动回来,cell不用再走一遍生命周期了,只需要调用willDisplayCell方法就可以重新出现在屏幕中了.
  • iOS 10 中,系统是一个一个加载cell的,二以前是一行一行加载的,这样就可以提升很多性能;
  • iOS 10 新增加的Pre-Fetching预加载
  • 这个是为了降低UICollectionViewCell在加载的时候所花费的时间,在 iOS 10 中,除了数据源协议和代理协议外,新增加了一个UICollectionViewDataSourcePrefetching协议,这个协议里面定义了两个方法:
- (void)collectionView:(UICollectionView *)collectionView prefetchItemsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths NS_AVAILABLE_IOS(10_0);

- (void)collectionView:(UICollectionView *)collectionView cancelPrefetchingForItemsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths  NS_AVAILABLE_IOS(10_0);

   在ColletionView prefetchItemsAt indexPaths这个方法是异步预加载数据的,当中的indexPaths数组是有序的,就是item接收数据的顺序;
   CollectionView cancelPrefetcingForItemsAt indexPaths这个方法是可选的,可以用来处理在滑动中取消或者降低提前加载数据的优先级.
   注意:这个协议并不能代替之前读取数据的方法,仅仅是辅助加载数据.
   Pre-Fetching预加载对UITableViewCell同样适用.   

iOS 10 UIColor 新增方法

以下是官方文档的说明:

Most graphics frameworks throughout the system, including Core Graphics, Core Image, Metal, and AVFoundation, have substantially improved support for extended-range pixel formats and wide-gamut color spaces. By extending this behavior throughout the entire graphics stack, it is easier than ever to support devices with a wide color display. In addition, UIKit standardizes on working in a new extended sRGB color space, making it easy to mix sRGB colors with colors in other, wider color gamuts without a significant performance penalty.

Here are some best practices to adopt as you start working with Wide Color.

  • In iOS 10, the UIColor class uses the extended sRGB color space and its initializers no longer clamp raw component values to between 0.0 and 1.0. If your app relies on UIKit to clamp component values (whether you’re creating a color or asking a color for its component values), you need to change your app’s behavior when you link against iOS 10.
  • When performing custom drawing in a UIView on an iPad Pro (9.7 inch), the underlying drawing environment is configured with an extended sRGB color space.
  • If your app renders custom image objects, use the new UIGraphicsImageRenderer class to control whether the destination bitmap is created using an extended-range or standard-range format.
  • If you are performing your own image processing on wide-gamut devices using a lower level API, such as Core Graphics or Metal, you should use an extended range color space and a pixel format that supports 16-bit floating-point component values. When clamping of color values is necessary, you should do so explicitly.
  • Core Graphics, Core Image, and Metal Performance Shaders provide new options for easily converting colors and images between color spaces.

    因为之前我们都是用RGB来设置颜色,反正用起来也不是特别多样化,这次新增的方法应该就是一个弥补吧。所以在iOS 10 苹果官方建议我们使用sRGB,因为它性能更好,色彩更丰富。如果你自己为UIColor写了一套分类的话也可尝试替换为sRGB,UIColor类中新增了两个Api如下:

    ```objc

  • (UIColor *)colorWithDisplayP3Red:(CGFloat)displayP3Red green:(CGFloat)green blue:(CGFloat)blue alpha:(CGFloat)alpha NS_AVAILABLE_IOS(10_0);
  • (UIColor *)initWithDisplayP3Red:(CGFloat)displayP3Red green:(CGFloat)green blue:(CGFloat)blue alpha:(CGFloat)alpha NS_AVAILABLE_IOS(10_0);

    ```

iOS 10 UITextContentType

// The textContentType property is to provide the keyboard with extra information about the semantic intent of the text document.@property(nonatomic,copy) UITextContentType textContentType NS_AVAILABLE_IOS(10_0); // default is nil

在iOS 10 UITextField添加了textContentType枚举,指示文本输入区域所期望的语义意义。

使用此属性可以给键盘和系统信息,关于用户输入的内容的预期的语义意义。例如,您可以指定一个文本字段,用户填写收到一封电子邮件确认uitextcontenttypeemailaddress。当您提供有关您期望用户在文本输入区域中输入的内容的信息时,系统可以在某些情况下自动选择适当的键盘,并提高键盘修正和主动与其他文本输入机会的整合。

iOS 10 字体随着手机系统字体而改变

当我们手机系统字体改变了之后,那我们App的label也会跟着一起变化,这需要我们写很多代码来进一步处理才能实现,但是iOS 10 提供了这样的属性adjustsFontForContentSizeCategory来设置。因为没有真机,具体实际操作还没去实现,如果理解错误帮忙指正。

 UILabel *myLabel = [UILabel new];   /*
    UIFont 的preferredFontForTextStyle: 意思是指定一个样式,并让字体大小符合用户设定的字体大小。
   */
    myLabel.font =[UIFont preferredFontForTextStyle: UIFontTextStyleHeadline]; /*
 Indicates whether the corresponding element should automatically update its font when the device’s UIContentSizeCategory is changed.
 For this property to take effect, the element’s font must be a font vended using +preferredFontForTextStyle: or +preferredFontForTextStyle:compatibleWithTraitCollection: with a valid UIFontTextStyle.
 */
     //是否更新字体的变化
    myLabel.adjustsFontForContentSizeCategory = YES;

iOS 10 UIScrollView新增refreshControl


iOS 10 以后只要是继承UIScrollView那么就支持刷新功能:

@property (nonatomic, strong, nullable) UIRefreshControl *refreshControl NS_AVAILABLE_IOS(10_0) __TVOS_PROHIBITED;

iOS 10 判断系统版本正确姿势

判断系统版本是我们经常用到的,尤其是现在大家都有可能需要适配iOS 10,那么问题就出现了,如下图:


我们得到了答案是:

//值为 1 [[[[UIDevice currentDevice] systemVersion] substringToIndex:1] integerValue]

//值为10.000000 [[UIDevice currentDevice] systemVersion].floatValue,

//值为10.0 [[UIDevice currentDevice] systemVersion]

所以说判断系统方法最好还是用后面的两种方法,哦~我忘记说了[[UIDevice currentDevice] systemVersion].floatValue这个方法也是不靠谱的,好像在8.3版本输出的值是8.2,记不清楚了反正是不靠谱的,所以建议大家用[[UIDevice
currentDevice] systemVersion]这个方法!

Swift判断如下:

   if #available(iOS 10.0, *) {
            // iOS 10.0
            print("iOS 10.0");
        } else { }

Xcode 8 插件不能用的问题

大家都升级了Xcode 8,但是对于插件依赖的开发者们,一边哭着一边去网上寻找解决办法。那么下面是解决办法:
让你的 Xcode8 继续使用插件(http://vongloo.me/2016/09/10/Make-Your-Xcode8-Great-Again/?utm_source=tuicool&utm_medium=referral )

但是看到文章最后的解释,我们知道如果用插件的话,可能安全上会有问题、并且提交审核会被拒绝,所以建议大家还是不要用了,解决办法总是有的,比如在Xcode中添加注释的代码块也是很方便的。

iOS 10开始项目中有的文字显示不全问题

我用Xcode 8 和Xcode 7.3分别测试了下,如下图:

xcode7


xcode7


xcode8


xcode8

创建一个Label然后让它自适应大小,字体大小都是17最后输出的宽度是不一样的,我们再看一下,
下面的数据就知道为什么升级iOS 10 之后App中有的文字显示不全了:


英文字母会不会也有这种问题,我又通过测试,后来发现英文字母没有问题,只有汉字有问题。
目前只有一个一个修改控件解决这个问题,暂时没有其他好办法来解决。

Xcode 8使用Xib awakeFromNib的警告问题

在Xcode 8之前我们使用Xib初始化- (void)awakeFromNib {}都是这么写也没什么问题,但是在Xcode 8会有如下警告:


官方解释:
You must call the super implementation of awakeFromNib to give parent classes the opportunity to perform any additional initialization they require.
Although the default implementation of this method does nothing, many UIKit classes provide non-empty implementations.
You may call the super implementation at any point during your own awakeFromNib method.

你必须调用父类实现awakeFromNib来给父类来执行它们需要的任何额外的初始化的机会。
虽然这种方法的默认实现不做任何事情,许多UIKit类提供非空的实现。
你可以调用自己的awakeFromNib方法中的任何时候超级实现




1推送 

xcode 升级到8之后很多人的推送接收不到了.获取不到token了 一朋友搞了一小时没找到原因. 只因看下图吧....我发觉xcode 我不打开他也能收到通知. 但是到了8(必须打开了才能收到推送) . 貌似不行了    大家对号入座吧.

技术分享

下面普及下ios10跟之前的推送的区别

  • iOS 9 以前的通知

     

    1.在调用方法时,有些方法让人很难区分,容易写错方法,这让开发者有时候很苦恼。

    2.应用在运行时和非运行时捕获通知的路径还不一致。

    3.应用在前台时,是无法直接显示远程通知,还需要进一步处理。

    4.已经发出的通知是不能更新的,内容发出时是不能改变的,并且只有简单文本展示方式,扩展性根本不是很好。

  • iOS 10 开始的通知 

    1.所有相关通知被统一到了UserNotifications.framework框架中。

    2.增加了撤销、更新、中途还可以修改通知的内容。

    3.通知不在是简单的文本了,可以加入视频、图片,自定义通知的展示等等。

    4.iOS 10相对之前的通知来说更加好用易于管理,并且进行了大规模优化,对于开发者来说是一件好事。

    5.iOS 10开始对于权限问题进行了优化,申请权限就比较简单了(本地与远程通知集成在一个方法中)。

 

 

2 字体适配的问题

ios 9 之前的lab 字体可以显示全,但是到了ios10 发觉字体显示不全了.得适配啊.app 会跟随手机系统字体大小而改变了.

简单粗暴地方法就是不让他跟着手机系统的字体改变而改变.

label.adjustsFontForContentSizeCategory = YES;

 

3 xcode 8运行打印一堆没用的东西Xcode 8的问题,解决办法是设置OS_ACTIVITY_MODE : disable如下图:

技术分享

 

 

4  xcode8的注释快捷键是什么,   command+/ 不行了

解决办法:

因为苹果解决xcode ghost。把插件屏蔽了。解决方法
命令运行: sudo /usr/libexec/xpccachectl 
然后必须重启电脑后生效

5 颜色问题, iOS 10 苹果官方建议我们使用sRGB,因为它性能更好,色彩更丰富。

UIColor类中新增了两个Api如下: 

+ (UIColor *)colorWithDisplayP3Red:(CGFloat)displayP3Red green:(CGFloat)green blue:(CGFloat)blue alpha:(CGFloat)alpha NS_AVAILABLE_IOS(10_0); - (UIColor *)initWithDisplayP3Red:(CGFloat)displayP3Red green:(CGFloat)green blue:(CGFloat)blue alpha:(CGFloat)alpha NS_AVAILABLE_IOS(10_0);

 

 

6 判断版本问题 

建议用   [[UIDevice currentDevice] systemVersion]

swift用

if #available(iOS 10.0, *) {

           // iOS 10.0啊            

print("iOS 10.0");        

} else

{

}

;

 

7 https的问题

iOS 9中默认非HTTS的网络是被禁止的,当然我们也可以把NSAllowsArbitraryLoads设置为YES禁用ATS。不过iOS 10从2017年1月1日起苹果不允许我们通过这个方法跳过ATS,也就是说强制我们用HTTPS,如果不这样的话提交App可能会被拒绝。但是我们可以通过NSExceptionDomains来针对特定的域名开放HTTP可以容易通过审核。

 

8  隐私权限 

iOS 10 开始对隐私权限更加严格,如果你不设置就会直接崩溃,现在很多遇到崩溃问题了,一般解决办法都是在info.plist文件添加对应的Key-Value就可以了。

技术分享

<!-- 相册 --> 
<key>NSPhotoLibraryUsageDescription</key> 
<string>App需要您的同意,才能访问相册</string> 
<!-- 相机 --> 
<key>NSCameraUsageDescription</key> 
<string>App需要您的同意,才能访问相机</string> 
<!-- 麦克风 --> 
<key>NSMicrophoneUsageDescription</key> 
<string>App需要您的同意,才能访问麦克风</string> 
<!-- 位置 --> 
<key>NSLocationUsageDescription</key> 
<string>App需要您的同意,才能访问位置</string> 
<!-- 在使用期间访问位置 --> 
<key>NSLocationWhenInUseUsageDescription</key> 
<string>App需要您的同意,才能在使用期间访问位置</string> 
<!-- 始终访问位置 --> 
<key>NSLocationAlwaysUsageDescription</key> 
<string>App需要您的同意,才能始终访问位置</string> 
<!-- 日历 --> 
<key>NSCalendarsUsageDescription</key> 
<string>App需要您的同意,才能访问日历</string> 
<!-- 提醒事项 --> 
<key>NSRemindersUsageDescription</key> 
<string>App需要您的同意,才能访问提醒事项</string> 
<!-- 运动与健身 --> 
<key>NSMotionUsageDescription</key> <string>App需要您的同意,才能访问运动与健身</string> 
<!-- 健康更新 --> 
<key>NSHealthUpdateUsageDescription</key> 
<string>App需要您的同意,才能访问健康更新 </string> 
<!-- 健康分享 --> 
<key>NSHealthShareUsageDescription</key> 
<string>App需要您的同意,才能访问健康分享</string> 
<!-- 蓝牙 --> 
<key>NSBluetoothPeripheralUsageDescription</key> 
<string>App需要您的同意,才能访问蓝牙</string> 
<!-- 媒体资料库 --> 
<key>NSAppleMusicUsageDescription</key> 
<string>App需要您的同意,才能访问媒体资料库</string>

或者

技术分享


1,iOS10 新增的privacy settings

iOS10添加了新的权限控制范围 如果你尝试访问这些隐私数据时得到如下错误:

> This app has crashed because it attempted to access privacy-sensitive
> data without a usage description.  The app's Info.plist must contain
> an NSCameraUsageDescription key with a string value explaining to the
> user how the app uses this data

因为它企图访问敏感数据时没有在应用程序的Info.plist
设置privacy key 新增的privacy setting如下:


2, OS_ACTIVITY_MODE

更新Xcode 8 如果控制台出现  enable_level: 0, persist_level: 0, default_ttl: 0, info_ttl: 0, debug_ttl: 0, generate_symptoms: 0 enable_oversize: 可通过如下方法设置:

Edit Scheme-> Run -> Arguments, 
在Environment Variables里边添加
OS_ACTIVITY_MODE = Disable

3,iOS10 layoutIfNeed

iOS10 在一个控件上调用layoutIfNeed是只会单独计算约束,它所约束的控件不会生效,想要达到之前的效果需要在父级控件上调用layoutIfNeed

4, NSDate

Swift3.0会将oc的NSDate转为Data类型,有些操作NSDate的第三方库会闪退

5, Notification

Swift3.0字符串类型的通知常量被定义为struct

static let MyGreatNotification = Notification.Name("MyGreatNotification")

// Use site (no change)
NotificationCenter.default().post(name: MyController.MyGreatNotification, object: self)'

6, Zip2Sequence(::) 被移除

在Swift3.0 Zip2Sequence(_:_:)方法被替换为zip(_:_:)

7, Range<>.reversed 被移除

在Swift3.0 Range<>.reversed方法被移除,被替换为<Collection>[<Range>].indices.reversed().

var array = ["A","B","C","D"]

for i in array.indices.reversed() {

    print("\(i)")
}

输出:3 2 1 0

8, Range新增至四种类型

Range
CountableRange
ClosedRange
CountableClosedRange

不同的表达式会生成不同的Range

var countableRange = 0..<20 'CountableRange(0..<20)'

var countableClosedRange = 0...20 'CountableClosedRange(0...20)'

9, Collection 新增 index(_:)系列方法

Index的successor(), predecessor(), advancedBy(_:), advancedBy(_:limit:), or distanceTo(_:)方法被移除,这些操作被移动到Collection

myIndex.successor()  =>  myCollection.index(after: myIndex)
myIndex.predecessor()  =>  myCollection.index(before: myIndex)
myIndex.advance(by: …) => myCollection.index(myIndex, offsetBy: …)

10, iOS10 UIStatusBar过期

如果你需要操作UIStatusBar,在iOS10需要改为

- (UIStatusBarStyle)preferredStatusBarStyle {
    return UIStatusBarStyleDefault;
}

11, iOS10 UICollectionView 性能优化

在iOS10 UICollectionView 最大的改变是增加了Pre-Fetching(预加载),
如果你翻看UICollectionView的最新API你可以发现新增了如下属性:

 @property (nonatomic, weak, nullable) id<UICollectionViewDataSourcePrefetching> prefetchDataSource 

@property (nonatomic, getter=isPrefetchingEnabled) BOOL 

在iOS10 Pre-Fetching 是默认开启的,如果出于某些原因你不想开启Pre-Fetching,可以通过如下设置禁用:

collectionView.isPrefetchingEnabled = false

UICollectionViewDataSourcePrefetching协议定义如下:

@protocol UICollectionViewDataSourcePrefetching <NSObject>
@required
// indexPaths are ordered ascending by geometric distance from the collection view
- (void)collectionView:(UICollectionView *)collectionView prefetchItemsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths NS_AVAILABLE_IOS(10_0);

@optional
// indexPaths that previously were considered as candidates for pre-fetching, but were not actually used; may be a subset of the previous call to -collectionView:prefetchItemsAtIndexPaths:
- (void)collectionView:(UICollectionView *)collectionView cancelPrefetchingForItemsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths  NS_AVAILABLE_IOS(10_0);

@end

12, iOS10 UITableView 性能优化

和UICollectionView一样UITableView也增加了Pre-Fetching技术,UITableView新增了如下属性:

@property (nonatomic, weak) id<UITableViewDataSourcePrefetching> prefetchDataSource NS_AVAILABLE_IOS(10_0);

奇怪的是UITableView并没有找到 isPrefetchingEnabled属性的定义

13,iOS10 UIScrollView 新增 refreshControl 属性

UIScrollView新增了refreshControl属性

@property (nonatomic, strong, nullable) UIRefreshControl *refreshControl NS_AVAILABLE_IOS(10_0) __TVOS_PROHIBITED;

这意味着 UICollectionViewUITableView 都支持refresh功能了。

我们也可以脱离UITableViewController使用UIRefreshControl了。

14, Swif3.0 新增作用域访问级别 fileprivate

目前有如下访问级别:

  • 公开(public)

  • 内部(internal)

  • 文件外私有(fileprivate)

  • 私有(private)

15,Swift3.0 允许关键字作为参数标签

Swift3.0开始我们将能使用除inout var let关键字作为参数标签

   // Swift 3 calling with argument label:
    calculateRevenue(for sales: numberOfCopies,
                     in .dollars)

    // Swift 3 declaring with argument label:
    calculateRevenue(for sales: Int,
                     in currency: Currency)


    func touchesMatching(phase: NSTouchPhase, in view: NSView?) -> Set<NSTouch>

如果你坚持要使用inout var let关键字可以使用 `` 包裹参数标签

func addParameter(name: String, `inout`: Bool)

一、证书管理

用Xcode8打开工程后,比较明显的就是下图了,这个是苹果的新特性,可以帮助我们自动管理证书。建议大家勾选这个Automatically manage signing(Ps.但是在beat2版本我用的时候,完全不可以,GM版本竟然神奇的又好了。)

QQ20160913-8.png-96.9kB

下面我来说说可能会出现的问题:

1.Xcode未设置开发者账号情况下的截图

QQ20160913-0.png-38.5kB


解决办法是:大家在Xcode的偏好设置中,添加苹果账号,即可。

2.设备机器未添加进开发者的Device情况下的截图

QQ20160913-2.png-33.7kB


解决办法是:大家在官网将设备添加进开发机后,陪下描述文件重新下个描述文件即可。

3.正常情况:Xcode配置登录开发者账号后的图片,耐心等待即可。

QQ20160913-1.png-25.1kB


等待完成之后的图

QQ20160913-3.png-27kB

二、Xib文件的注意事项

使用Xcode8打开xib文件后,会出现下图的提示。

QQ20160913-9.png-41.7kB


大家选择Choose Device即可。
之后大家会发现布局啊,frame乱了,只需要更新一下frame即可。如下图

QQ20160913-11.png-113.2kB

  • 注意:如果按上面的步骤操作后,在用Xcode7打开Xib会报一下错误,

QQ20160913-12.png-32.3kB

  • 解决办法:需要删除Xib里面 
    <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
    这句话,以及把< document >中的toolsVersion和< plugIn >中的version改成你正常的xib文件中的值
    ,不过不建议这么做,在Xcode8出来后,希望大家都快速上手,全员更新。这就跟Xcode5到Xcode6一样,有变动,但是还是要尽早学习,尽快适应哟!

三、代码及Api注意

使用Xcode8之后,有些代码可能就编译不过去了,具体我就说说我碰到的问题。
1.UIWebView的代理方法:
**注意要删除NSError前面的 nullable,否则报错。

- (void)webView:(UIWebView *)webView didFailLoadWithError:(nullable NSError *)error
{
    [self hideHud];
}

四、代码注释不能用的解决办法

这个是因为苹果解决xcode ghost,把插件屏蔽了。
解决方法
打开终端,命令运行: sudo /usr/libexec/xpccachectl
然后必须重启电脑后生效

注意:Xcode8内置了开启注释的功能,位置在这里

QQ20160914-3.png

快捷键的设置在这里

QQ20160914-2.png

貌似Xcode8取消了三方插件的功能,具体可以查阅下Xcode8 Source Editor

五、权限以及相关设置

注意,添加的时候,末尾不要有空格
我们需要打开info.plist文件添加相应权限的说明,否则程序在iOS10上会出现崩溃。
具体如下图:

QQ20160914-0.png

麦克风权限:Privacy - Microphone Usage Description 是否允许此App使用你的麦克风?
相机权限: Privacy - Camera Usage Description 是否允许此App使用你的相机?
相册权限: Privacy - Photo Library Usage Description 是否允许此App访问你的媒体资料库?通讯录权限: Privacy - Contacts Usage Description 是否允许此App访问你的通讯录?
蓝牙权限:Privacy - Bluetooth Peripheral Usage Description 是否许允此App使用蓝牙?

语音转文字权限:Privacy - Speech Recognition Usage Description 是否允许此App使用语音识别?
日历权限:Privacy - Calendars Usage Description 是否允许此App使用日历?

定位权限:Privacy - Location When In Use Usage Description 我们需要通过您的地理位置信息获取您周边的相关数据
定位权限: Privacy - Location Always Usage Description 我们需要通过您的地理位置信息获取您周边的相关数据
定位的需要这么写,防止上架被拒。

六、字体变大,原有frame需要适配

经有的朋友提醒,发现程序内原来2个字的宽度是24,现在2个字需要27的宽度来显示了。。
希望有解决办法的朋友,评论告我一下耶,谢谢啦

七、推送

如下图的部分,不要忘记打开。所有的推送平台,不管是极光还是什么的,要想收到推送,这个是必须打开的哟✌️

QQ20160914-4.png

之后就应该可以收到推送了。另外,极光推送也推出新版本了,大家也可以更新下。

PS.苹果这次对推送做了很大的变化,希望大家多查阅查阅,处理推送的代理方法也变化了。

// 推送的代理
[<UNUserNotificationCenterDelegate>]

iOS10收到通知不再是在
[application: didReceiveRemoteNotification:]方法去处理, iOS10推出新的代理方法,接收和处理各类通知(本地或者远程)

- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler { //应用在前台收到通知 NSLog(@"========%@", notification);}- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)())completionHandler { //点击通知进入应用 NSLog(@"response:%@", response);}

稍后我会更新文章,对推送做一个详细的讲解。

8.屏蔽杂乱无章的bug

更新Xcode8之后,新建立工程,都会打印一堆莫名其妙看不懂的Log.
如这些

subsystem: com.apple.UIKit, category: HIDEventFiltered, enable_level: 0, persist_level: 0, default_ttl: 0, info_ttl: 0, debug_ttl: 0, generate_symptoms: 0, enable_oversize: 1,

屏蔽的方法如下:
Xcode8里边 Edit Scheme-> Run -> Arguments, 在Environment Variables里边添加
OS_ACTIVITY_MODE = Disable

QQ20160914-8.png

如果写了之后还是打印log,请重新勾选对勾,就可以解决了

Ps.考虑到添加上述内容在Xcode8后,真机调试可能出现异常,大家可以自定义一个宏定义,来做日志输出。

#ifdef DEBUG

#define DDLOG(...) printf(" %s\n",[[NSString stringWithFormat:__VA_ARGS__]UTF8String]);
#define DDLOG_CURRENT_METHOD NSLog(@"%@-%@", NSStringFromClass([self class]), NSStringFromSelector(_cmd))

#else

#define DDLOG(...) ;
#define DDLOG_CURRENT_METHOD ;

#endif

 


写在前面

收到一些小伙伴的来信,觉得可能下边没有表达清楚,先把大家关心的要点在此进行总结,有兴趣的可以看看下边的研究过程,没兴趣的直接看这段即可。

  • Xcode8支持Swift2.3和Swift3.0两种语编译,但是在整个工程中只能使用一种语法。
  • 如果想用Swift2.3版本开发,当弹出是否迁移到Swift3.0的对话框一律选择Later。所有的target(包括自己创建的和Cocoapods自动生成的)的Use Legacy Swift Language Version选择Yes。
  • 如果想用Swift3.0版本开发,当迁移到Swift3.0的界面选择target时,只要选择自己创建的target即可,Cocoapods导入的第三方不要勾选。所有的target(包括自己创建的和Cocoapods自动生成的)的Use Legacy Swift Language Version选择No.
  • Alamofire最新正式版本(4.0.0)只支持Swift3.0,想用Swift2.3开发的请选择3.5.0版本;
  • SnapKit的最新正式版本(3.0.0)同时支持Swift2.3和Swift3.0,请根据需求选择Use Legacy Swift Language Version的选项。
  • ReactiveCocoa的最新正式版本(4.2.2)只支持Swift2.3,凡是用到这个框架的项目只能使用Swift2.3开发。所有target包括自己创建的和Cocoapods自动生成的)的Use Legacy Swift Language Version选择Yes。

探究过程

Xcode8发布了,随着Xcode8一起到来的还有Swift3.0。相信好多小伙伴已经兴冲冲的下载了Xcode8,并且打开了自己的Swift项目想要尽快将自己的项目切换到Swift3.0吧。

Tip:

首先郑重提示,如果是Swift的项目:

  1. Xcode不要覆盖安装,最好保留Xcode7和Xcode8两个开发工具;
  2. 请先备份自己的项目,请先备份自己的项目,请先备份自己的项目;
  3. 如果项目迁移到Swift3.0失败,请用Xcode7打开自己备份项目继续开发,凡是用Xcode8打开过的Swift项目,Xcode7打开都会报错。

我也是这么想的,用Xcode8打开自己的项目,首先提示我们Swift语法修改了,询问我们是否要迁移到Swift3.0,如图所示:


是否转变当前的Swift语法

当然选择Convert了,选择后,如图所示:


转换到哪个版本的Swift

选择转换到Swift3.0,一路Next之后,发现,发现依然报错,然后我就傻眼了。


依然报错

仔细观察错误信息,发现报错大部分集中在了第三方框架SnapKit中,难道是SnapKit不支持Swift3.0,我们在GitHub上看到:


SnapKit最新版本支持Swift3.0

难道是由于我们项目中的SnapKit不是最新版本导致的?
更新后依然报错,这就尴尬了,人家明明说支持了,但是项目中就报错,这是为什么?

这个时候我们应该去百度一下,发现好多人说要设置这个选项:


是否使用旧版本的Swift语言

设置之后,有些小伙伴可能就编译成功了,有些小伙伴可能依然编译出错。那么编译未成功如何解决呢?下面我们就来研究一下这个编译选项到底该怎么设置。

正常来说,我们可以随便改自己写的代码,但是对于第三方的代码,如果我使用Cocopods导入的,一般会在代码的右上角看到这个锁形标志:


lock


这个标志表示当前文件被锁住,你没有修改的权限。所以我们最好不要修改第三方中的代码。但是主要问题又出在第三方框架中,所以我们优先解决第三方框架的Swift3.0的适配。

SnapKit适配Swift3.0

既然SnapKit的作者说SnapKit已经支持Swift3.0了,那么我们就先来适配SnapKit,首先用Xcode8新建一个空项目,利用Cocoapods导入SnapKit.


Podfile

打开工程,依然弹出这个选项:


是否转换到Swift3.0

刚才选择了Convert依然报错,可见不靠谱,这次我们全部选择Later。

编译后,报错:


报错

错误提示我们依然是“Use Legacy Swift Language Version”这个选项的问题。
我们来看看这个选项怎么设置,如图所示:


设置SnapKit的编译选项

因为SnapKit已经支持了Swift3.0,所以我们选择No,不支持旧的Swift版本,即使用Swift3.0的语法。编译通过。我们再来看看我们写的代码生成的target的编译选项:


自己的target的编译选项


由于Xcode8新建的工程默认使用Swift3.0的语法,所以此处默认选择为No。

ReactiveCocoa适配Swift3.0

相信在好多人在Swift中使用了响应式编程,提到响应式编程,就不得不说说RAC了,RAC是一个重型的OC框架,但是为了在Swift中可以使用,作者提供了Swift的桥接文件,所以,在Swift项目中导入了RAC,都会包含一些Swift的文件,这些Swift的文件也需要适配。

GitHub上RAC的作者在readme中写到:


readme


RAC 5 支持Swift3.0.x,RAC 4支持Swift2.x。我们在Cocoapods中搜索ReactiveCocoa这个库:


pod search ReactiveCocoa

只找到了4.2.2版本的库,我不知道上边提到的RAC 5 和 RAC 4 分别指什么。只能先用这个版本了。同样的,新建一个工程:


默认使用Swift3.0

使用Cocoapods导入RAC:


Podfile

是否迁移到Swift3.0依然选择Later,编译,报错:


报错

和SnapKit的错误一样,同样的,我们去设置ReactiveCocoa的targetsh设置一下参数:


编译设置


和SnapKit同样设置为No,编译,报错。我们可以看到,安装ReactiveCocoa同时安装了一个Result,看看它的target设置:


Result的便已设置


设置的为Yes,那我们也把ReactiveCocoa的设置为Yes。编译,依然报错:


依然报错

我们尝试着把自己的target设置修改一下:


修改自己工程的target设置

编译成功。

同时导入SnapKit和RAC

现在分别导入SnapKit和RAC都编译成功了,但是可以看出SnapKit支持Swift3.0。RAC不支持。那么如果两个同时导入该选什么呢?

经过测试,如果同事导入两个框架,所有的target的设置都得选择Yes。(大家可以自己试一下,在此不做赘述。)

可以看到SnapKit既支持Swift3.0,也支持Swift2.3。那么它是如何做到的呢?通过查看源代码可以看到:


源代码示例

通过这样的宏来判断当前的Swift的编译版本来编译不同的代码段,从而实现兼容Swift2.3和Swift3.0。

Alamofire

经过测试,Alamofire的4.0.0版本仅支持iOS9+和Swift3.0.x,如果想使用Swift2.3开发的同学可以安装Alamofire的3.5.0版本,设置所有的Use Legacy Swift Language Version为Yes。

总结

  • target的Build Setting的Use Legacy Swift Language Version选项的作用是设置当前target对应的文件是采用Swift2.3的语法编译还是Swift3.0的语法编译。当选择为Yes时,采用Swift2.3的语法编译;当选择是No时,采用Swift3.0的语法编译。
  • 新建的项目中,编译设置的原则为:所有的第三方中只要有一个第三方使用了Swift2.3的语法,那么所有的target的编译设置都应为Yes。如果都支持Swift3.0的语法,那么就可以设置为No。并且不能选择Unspecified。
  • 当Use Legacy Swift Language Version的选项设置为Yes时候,我们的工程只能使用Swift2.3来进行开发,当然你也可以像SnapKit那样利用宏来判断当前Swift的编译版本来实现适配Swift3.0,这样当以后迁移到Swift3.0也方便一些。

思考

既然每个target有自己单独的编译设置,理论上应该在编译的时候按照各自的target的编译设置来按照不同的Swift的版本编译,这样我们就可以自己的代码使用3.0编写,第三方根据各自不同进行不同的编译设置。以后想要迁移到完全的Swift3.0也更容易一些。但是目前看来编译的时候是统一按照我们缩写的target来编译的,这样的话单独设置各自的target还有什么意义呢?或许还需要一些别的设置才可以实现各自独立编译?对此有了解的同学麻烦告知一下,在此先谢过了。



1,iOS10 新增的privacy settings

iOS10添加了新的权限控制范围 如果你尝试访问这些隐私数据时得到如下错误:

> This app has crashed because it attempted to access privacy-sensitive
> data without a usage description.  The app's Info.plist must contain
> an NSCameraUsageDescription key with a string value explaining to the
> user how the app uses this data

因为它企图访问敏感数据时没有在应用程序的Info.plist
设置privacy key 新增的privacy setting如下:


privacy setting

2, OS_ACTIVITY_MODE

更新Xcode 8 如果控制台出现 enable_level: 0, persist_level: 0, default_ttl: 0, info_ttl: 0, debug_ttl: 0, generate_symptoms: 0 enable_oversize: 可通过如下方法设置:

Edit Scheme-> Run -> Arguments, 
在Environment Variables里边添加
OS_ACTIVITY_MODE = Disable

3,iOS10 layoutIfNeed

iOS10 在一个控件上调用layoutIfNeed是只会单独计算约束,它所约束的控件不会生效,想要达到之前的效果需要在父级控件上调用layoutIfNeed

4, NSDate

Swift3.0会将oc的NSDate转为Data类型,有些操作NSDate的第三方库会闪退

5, Notification

Swift3.0字符串类型的通知常量被定义为struct

static let MyGreatNotification = Notification.Name("MyGreatNotification")

// Use site (no change)
NotificationCenter.default().post(name: MyController.MyGreatNotification, object: self)'

6, Zip2Sequence(::) 被移除

在Swift3.0 Zip2Sequence(_:_:)方法被替换为zip(_:_:)

7, Range<>.reversed 被移除

在Swift3.0 Range<>.reversed方法被移除,被替换为<Collection>[<Range>].indices.reversed().

var array = ["A","B","C","D"]

for i in array.indices.reversed() {

    print("\(i)")
}

输出:3 2 1 0

8, Range新增至四种类型

Range
CountableRange
ClosedRange
CountableClosedRange

不同的表达式会生成不同的Range

var countableRange = 0..<20 'CountableRange(0..<20)'

var countableClosedRange = 0...20 'CountableClosedRange(0...20)'

9, Swift3.0 Collection 新增 index(_:)系列方法

Index的successor(), predecessor(), advancedBy(_:), advancedBy(_:limit:), or distanceTo(_:)方法被移除,这些操作被移动到Collection

myIndex.successor()  =>  myCollection.index(after: myIndex)
myIndex.predecessor()  =>  myCollection.index(before: myIndex)
myIndex.advance(by: …) => myCollection.index(myIndex, offsetBy: …)

10, iOS10 UIStatusBar过期

如果你需要操作UIStatusBar,在iOS10需要改为

- (UIStatusBarStyle)preferredStatusBarStyle {
    return UIStatusBarStyleDefault;
}

11, iOS10 UICollectionView 性能优化

在iOS10 UICollectionView 最大的改变是增加了Pre-Fetching(预加载),
如果你翻看UICollectionView的最新API你可以发现新增了如下属性:

 @property (nonatomic, weak, nullable) id<UICollectionViewDataSourcePrefetching> prefetchDataSource 

@property (nonatomic, getter=isPrefetchingEnabled) BOOL

在iOS10 Pre-Fetching 是默认开启的,如果出于某些原因你不想开启Pre-Fetching,可以通过如下设置禁用:

collectionView.isPrefetchingEnabled = false

UICollectionViewDataSourcePrefetching协议定义如下:

@protocol UICollectionViewDataSourcePrefetching <NSObject>
@required
// indexPaths are ordered ascending by geometric distance from the collection view
- (void)collectionView:(UICollectionView *)collectionView prefetchItemsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths NS_AVAILABLE_IOS(10_0);

@optional
// indexPaths that previously were considered as candidates for pre-fetching, but were not actually used; may be a subset of the previous call to -collectionView:prefetchItemsAtIndexPaths:
- (void)collectionView:(UICollectionView *)collectionView cancelPrefetchingForItemsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths  NS_AVAILABLE_IOS(10_0);

@end

12, iOS10 UITableView 性能优化

和UICollectionView一样UITableView也增加了Pre-Fetching技术,UITableView新增了如下属性:

@property (nonatomic, weak) id<UITableViewDataSourcePrefetching> prefetchDataSource NS_AVAILABLE_IOS(10_0);

奇怪的是UITableView并没有找到 isPrefetchingEnabled属性的定义

13,iOS10 UIScrollView 新增 refreshControl 属性

UIScrollView新增了refreshControl属性

@property (nonatomic, strong, nullable) UIRefreshControl *refreshControl NS_AVAILABLE_IOS(10_0) __TVOS_PROHIBITED;

这意味着 UICollectionViewUITableView 都支持refresh功能了。

我们也可以脱离UITableViewController使用UIRefreshControl了。

14, Swif3.0 新增作用域访问级别 fileprivate

目前有如下访问级别:

  • 公开(public)
  • 内部(internal)
  • 文件外私有(fileprivate)
  • 私有(private)

15,Swift3.0 允许关键字作为参数标签

Swift3.0开始我们将能使用除inout var let关键字作为参数标签

   // Swift 3 calling with argument label:
    calculateRevenue(for sales: numberOfCopies,
                     in .dollars)

    // Swift 3 declaring with argument label:
    calculateRevenue(for sales: Int,
                     in currency: Currency)


    func touchesMatching(phase: NSTouchPhase, in view: NSView?) -> Set<NSTouch>

如果你坚持要使用inout var let关键字可以使用 `` 包裹参数标签

func addParameter(name: String, `inout`: Bool)

private和fileprivate

自动转换代码以后把我大部分(不知道是不是全部,没统计)private都改成了fileprivate。本来不用private也不会对程序的编译运行有任何影响,private只是为了保证代码外部可读性而准备的,而现在有了fileprivate以后,private变得更加“私有”。现在的private方法和对象,只能在大括号中访问,即便是这个类的extension中,也不能访问private。而fileprivate的作用域,则和以前的private一样,顾名思义,在这个文件中都能访问。

NSData和Data

Data是swift的产物,和Array,Dictionary,Set等类似。NSData的初始化是NSData(XXX),而Data用起来更方便,在需要获取数据的对象后面加上.data,即可获得数据,方便是方便,老代码的修改就比较麻烦了。

NSURLSession和URLSession

URLRequest终于把烦人的Mutable去掉了,那些强迫症不用再因为let xxx = NSMutableXXX是可变的而纠结了。

# Any和AnyObject
现在Any貌似可以和AnyObject互相转换了,以前Any对应struct而AnyObject对应class,一些不太复杂的模型用struct编写,和某些方法(参数需要传AnyObject或者class类型的数据)兼容性不好,不得不改为用class编写,这就不符合struct设计的初衷了。

# 闭包的escaping和non-escaping类型
这个类型决定了闭包是否在调用他的函数(或其他)返回时就销毁(?),escaping是不销毁的意思,non-escaping是保留的意思。一般在网络方法中,闭包一般在其他线程中执行,并且在函数返回时还没有执行完毕,这里种情况应该使用escaping类型。使用方法是在闭包前面加@escaping。反之同理。
那么为什么在swift2之前都不需要加呢,因为swift2之前默认都是escaping类型,而swift3以后默认是non-escaping。在合理的情况下,使用non-escaping类型的闭包更节省内存,而且,在闭包内可以不用再加self关键字了。
参考文章

awakeFromNib方法中获取frame

原来在xib文件中设置约束定宽高约束,在awakeFromNib方法中是可以直接获取到view.frame.size的值的,现在可能因为xib文件中加入了多设备的尺寸设置,已经不能再获取size了。原来frame还跟xib的实际大小有关,现在xib所有的布局都是相对的了,不能再过分依赖frame。顺带说一下,现在如果直接获取size,得到的是(1000.0, 1000.0)。

plist中的privacy key

在iOS旧版本中都会,凡是涉及到隐私相关权限(例如录音、摄像之类的)的访问,都会提示是否允许访问。在plist有一系列privacy - XXXDescription的key作为权限询问提示的文字,旧版iOS中这些是选填的,iOS10里面如果没有填写又访问了相关权限,程序会崩溃。

形如"\(XXX)"在字符串中引用其他对象

在之前的版本中,只有当XXX的类型是optional?,打印结果才会带有"optional",例如:

var XXX: Int? = 1
print("XXX的结果为:\(XXX))  //XXX的结果为:optional(1)

但如果XXX类型为optional!,就不会打印"optional"。
在新版本中,无论类型为!还是?,打印结果都会带有optional,可以在打印时加上!,或者声明时不要声明为optional解决。例如:

//错误示范:
var XXX: Int! = 1
print("XXX的结果为:\(XXX))  //XXX的结果为:optional(1)
//正确示范:
var XXX: Int! = 1
print("XXX的结果为:\(XXX!))  //XXX的结果为:1
//或者
var XXX: Int = 1
print("XXX的结果为:\(XXX))  //XXX的结果为:1




作者:stubbornness1219 发表于2016/9/28 23:30:45 原文链接
阅读:107 评论:0 查看评论

局域网设备发现之Bonjour协议

$
0
0

本文由嵌入式企鹅圈原创团队成员-华南师范大学物联网创新中心Hende_Zhu先生执笔。

WIFI物联网解决方案中,通常我们需要对设备进行绑定,需要通过某种方法先对设备进行发现,比如微信硬件采用广播的方式,定时向外发送上线消息或者采用一问一答的方式进行发现,Bonjour是由苹果公司实现的一种零配置网络(Zeroconf)协议,它是一种基于服务的设备发现协议,不仅能够自动获取有效IP地址,还可以通过查询服务的方式来找到设备地址,只要双方约定好服务(service)的名称,设备的IP地址和端口都是可以变化的!

一、mDNS协议和DNS-SD协议

Bonjour协议是基于mDNS(Multicast DNS)协议和DNS-SD(DNS Service Discovery)协议开发实现,因此有必要先在这里给大家介绍一下这两个协议。

1.1 mDNS协议介绍

mDNS协议适用于局域网内没有DNS服务器时的域名解析,设备通过组播的方式交互DNS记录来完成域名解析,约定的组播地址是:224.0.0.251,端口号是5353,mdns协议使用DNS协议一样的数据包,由头部和数据段两部分(大家可以自行去了解DNS数据包的格式啦,在这里不展开介绍了):

mDNS的一个使用情景是这样的:



设备d通过组播(224.0.0.251:5353),询问a.local地址是?

设备a知道有人查询它后,也是通过同样的组播组回复它的地址信息(通过回复用于IPv4的A类型DNS记录(A Record)或者用于IPv6的AAAA类型的DNS记录,A记录和AAAA记录分别用于将域名转换成IP地址),这里组播内的所有人b, c, d都会收到,它们会将a.local的ip地址等信息(如TTL值)刷新到mDNS缓冲区中。
mDNS协议和DNS协议还有些不同,mDNS只能用于局域网内部,并且它只接受解析主机名前缀为.local的域名,因此mDNS也是可以和DNS在同一台设备上共存的,以及它们存储记录的区域是分开的。
除此之外,mDNS还有其它的作用,例如在零配置网络中给自己分配域名,设备给自身选择一个域名后,然后通过发送记录类型为”any”的mDNS包来查询局域网内是否有同名,如果没有设备就会把这个名字作为自己的域名。

1.2 DNS-SD协议介绍
接下来再介绍一下DNS-SD协议,即DNS based Service Discovery,基于DNS的服务发现主要用到DNS现有的三种类型记录(Record Type):PTR记录、SRV记录、TXT记录,其中:
1)服务发现:设备会先发送一个查询PTR记录的数据包到组播组,所查询服务格式为:

<service>.<transport>.<domain>
service表示的是要查询的服务,transport表示的是传输的协议:TCP还是UDP,domain表示查询的域,在mDNS中为.local,接着具有对应服务的设备会响应一系列本设备上所具有的服务实例:
<instance>.<service>.<transport>.<domain>
instance表示服务的实例名,虽然收到<instance>.<service>.<transport>.<domain>,但是只有instance才会显示给用户看,比如:要查询一个_easylink._tcp.local的服务,具有这个服务对应实例的设备会响应一条PTR记录:EMW3031 Module#500A3F._easylink._tcp.local,即表示EMW3031 Module#500A3F为_easylink._tcp.local的一个实例,设备收到后只会显示EMW3031 Module#500A3F供用户看,它是UTF-8编码的。
可以看出,DNS-SD的PTR记录所代表的意思是区别于传统DNS的PTR记录的含义的,并且DNS-SD下的PTR记录用于记录服务到服务实例的映射。

2)获取服务实例的主机名和端口号:上述多个服务实例instance显示供用户选择确定一个后,就需要查询记录服务实例的主机名和端口号,即查询SRV记录。
设备会发送一个mDNS请求,然后具有所请求中服务实例的设备会响应SRV记录,SRV记录记录了这个服务实例对应的主机名和端口号以及TTL信息,一条SRV记录的例子是:

EMW3031 Module#500A3F._easylink._tcp.local. 3 IN SRV 0 0 8002 EMW3031 Module#500A3F.local.
DNS下的SRV记录的格式为:
_service._proto.name. TTL class SRV priority weight port target.
在DNS-SD中,priority和weight无效,一般置为00 00,port和target即为端口号和主机名。
因此SRV记录用于记录服务实例到端口号和主机名的映射,即便端口号可变也没有关系。

3)服务实例更详细的信息:有时候,一个服务实例除了所在设备的端口号和主机名这些信息以外,还可以提供更多的附加参数信息,服务实例的附加信息记录在TXT记录中,以”key = value”的格式记录,如提供设备的MAC地址:

MAC=D0:BA:E4:50:0A:3F

二、Bonjour协议原理

前面介绍了mDNS协议以及DNS-SD协议,其实基本上就已经展开介绍了Bonjour协议的细节,接下来再来理解Bonjour就相当轻松了。Bonjour协议可以理解为mDNS协议和DNS-SD协议的结合,其实大家在继续往下看之前可以自己想一下如何将两个协议结合起来呢?DNS-SD已经找到了提供服务的端口号和主机好了,最后再做进一步的主机名到IP地址的解析就完成了Bonjour协议的整个过程,当然结合的时候DNS-SD所发送的三种记录都是通过mDNS规定的组播组和端口号(224.0.0.254:5353)发送出去的,但是DNS-SD是不依赖mDNS协议而存在的。
Bonjour协议提供三部分功能:通告服务、发现服务、解析服务,这是三个动宾词组哈。在物联网中,设备在本地记录一个服务往往需要提供服务相关的SRV记录、PTR记录以及TXT记录相关的信息,用于最后组装mDNS数据包发送出去。
在没有DHCP分配IP地址和没有设置静态IP地址情况下,通过Bonjour协议还可以自己在局域网内获取有效的IP地址以及主机名,但是需要我们配置好路由的信息如子网掩码等,它会生成一个IP,然后询问局域网内是否有冲突,如果没有冲突就将这个IP占为己有,如果有冲突,就会更换一个IP,继续查询,主机名也是利用同样的方法获得。
通告服务用于设备告之局域网内其他人本设备的服务信息,一般包括发送SRV记录和PTR记录,这些记录被其它mDNS设备记录在本地的存储区中。

发现服务用于查询一个指定的服务,然后具有该服务的设备会响应PTR记录,告诉查询的设备有这样的服务并且服务实例的名称是什么。


解析服务发生在完成服务发现之后,获得了服务实例后供用户选择,再下一步就要进行解析,首先根据服务实例获得该设备的主机名以及端口,最后再根据主机名来获取IP地址。




经过以上一步步交互就可以获得了目标设备的IP地址和端口号了,然后就可以根据选择的传输协议TCP或者UDP进行通信。
三、mDNS数据包
庆科物联的设备端已经实现了Bonjour协议的主要功能,以下是基于其设备端发现过程用Wireshark抓的包,其中IP地址为192.168.191.2表示的是手机端,192.168.191.3表示的是完成配网后的设备端,它们在同一个局域网内。
服务发现:由手机APP发送查询_easylink._tcp.local服务

服务解析:设备端一次性响应了PTR记录、SRV记录、TXT记录以及A记录:

每条记录展开为:


嵌入式企鹅圈原创团队由阿里、魅族、nvidia、龙芯、炬力、拓尔思等资深工程师组成。百分百原创,每周两篇,分享嵌入式、Linux、物联网、GPU、Android、自动驾驶等技术。欢迎扫码关注微信公众号:嵌入式企鹅圈,实时推送原创文章!


作者:yueqian_scut 发表于2016/9/28 23:37:43 原文链接
阅读:105 评论:0 查看评论

AlertDialog以及子类

$
0
0

android.app.AlertDialog及其子类提供了4种常用的对话框:
AlertDialog:功能最丰富,实际应用最广的对话框,最常用的。
ProgressDialog:进度对话框,这个对话框只是对简单进度条的封装
DatePickerDialog:日期选择对话框,这个对话框只是对DatePicker的包装
TimePickerDialog:时间选择对话框,这个对话框只是对TimePicker的包装

——AlertDialog
功能很强大,可以生成各种内容的对话框
AlertDialog生成对话框可分为如下4个区域:
——图标区
——标题区
——内容区
——按钮区
AlterDialog使用步骤:
(1)创建AlertDialog.Builder对象
(2)调用AlertDialog.Builder的setTitle()或setCustomTitle()方法设置标题
(3)调用AlertDialog.Builder的setIcon()方法设置图标
(4)调用AlertDialog.Builder相关方法设置显示内容,
包括:setMessage 设置最简单的文本提示信息
setItems 设置内容为简单列表项,调用该方法时需要传入一个数组或者数组资源的资源ID
setSingleChoiceItems 设置内容为单选的列表项,可以传入数组,资源id,Cursor,ListAdapter作为参数
setMultiChoiceItems 设置内容为多选的列表项
setAdapter 设置内容为自定义列表项
setView 设置内容为任意类型的View,完成一个登录对话框的界面
(5)调用AlertDialog.Builder的setPositiveButton(),setNegativeButton
或setNeutralButton()方法添加多个按钮
(6)调用AlertDialog.Builder的create()方法创建AlterDialog对象
(7)调用AlertDialog的show()方法显示对话框
setCancelable(false):设置是否可以取消对话框,默认为true,点击按钮,回退健或者点击
任何一个地方都会关闭对话框。需要在create之前调用。
AlertDialog.dismiss():取消对话框
AlertDialog.cancel():取消对话框
修改Activity的背景颜色:
getWindow().setBackgroundDrawableResource(int);
修改Activity的背景图片:
getWindow().setBackgroundDrawable(Drawable);

——DatePickerDialog,TimePickerDialog
这两个对话框功能简单,用法也很简单。只需要两步就可以了:
1)通过new关键字创建实例,调用show()将对话框显示出来
2)绑定监听器,从而通过监听器获取用户设置的事件

——ProgressDialog
使用ProgressDialog进度条对话框有两种方式:
1)如果只是创建一个简单的进度对话框,
调用ProgressDialog提供的静态show()方法显示对话框即可
这里的参数boolean indeterminate设置是否是不明确的状态。
2)创建ProgressDialog,然后调用方法对对话框中的进度条进行设置,
设置完成后将对话框显示出来即可。
ProgressDialog包含如下的方法:

a)setTitle(“提示信息”);
b)setMessage(charSequence)设置对话框里显示的消息
c)setMax(int)设置对话框中进度条的最大值
d)setProgressStyle(ProgressDialog.STYLE_HORIZONTAL)设置对话框里进度条的风格
e)setIndeterminate(boolean)设置进度条是否显示不明确值,不明确就是滚动条的当前值自动在最小到最大值之间来回移动,形成这样一个动画效果,这个只是告诉别人“我正在工作”,但不能提示工作进度到哪个阶段。主要是在进行一些无法确定操作时间的任务时作为提示。而“明确”就是根据你的进度可以设置现在的进度值
f)p.dismiss()关闭对话框

package com.xspacing.alertdialog;

import java.util.Calendar;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import android.app.Activity;
import android.app.AlertDialog;
import android.app.AlertDialog.Builder;
import android.app.DatePickerDialog;
import android.app.DatePickerDialog.OnDateSetListener;
import android.app.ProgressDialog;
import android.app.TimePickerDialog;
import android.app.TimePickerDialog.OnTimeSetListener;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.DialogInterface.OnMultiChoiceClickListener;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.View;
import android.widget.Button;
import android.widget.DatePicker;
import android.widget.TimePicker;
import android.widget.Toast;

/**
 * @ClassName MainActivity.java
 * @Description TODO 对话框
 * @author Smile
 * @version v1.0
 * @date 2016年9月27日
 */
public class MainActivity extends Activity implements View.OnClickListener {

    private Button mBtnAlertDialog;
    private Button mOneBtnAlertDialog;
    private Button mCustomAlertDialog;
    private Button mDateAlertDialog;
    private Button mTimeAlertDialog;
    private Button mMultiAlertDialog;
    private Button mProgressAlertDialog;
    private Map<String, String> map;

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

    private void initViews() {
        mBtnAlertDialog = (Button) findViewById(R.id.bt_alertdialog);
        mOneBtnAlertDialog = (Button) findViewById(R.id.bt_one_alertdialog);
        mMultiAlertDialog = (Button) findViewById(R.id.bt_multi_alertdialog);
        mCustomAlertDialog = (Button) findViewById(R.id.bt_custom_alertdialog);
        mDateAlertDialog = (Button) findViewById(R.id.bt_date_alertdialog);
        mTimeAlertDialog = (Button) findViewById(R.id.bt_time_alertdialog);
        mProgressAlertDialog = (Button) findViewById(R.id.bt_progress_alertdialog);
    }

    private void initDatas() {
        mBtnAlertDialog.setOnClickListener(this);
        mOneBtnAlertDialog.setOnClickListener(this);
        mMultiAlertDialog.setOnClickListener(this);
        mCustomAlertDialog.setOnClickListener(this);
        mDateAlertDialog.setOnClickListener(this);
        mTimeAlertDialog.setOnClickListener(this);
        mProgressAlertDialog.setOnClickListener(this);
    }

    /**
     * 普通对话框
     */
    private void alertDialog() {
        // context必须为activity的上下文,不可为getApplicationContext()
        AlertDialog.Builder builder = new Builder(this);
        builder.setIcon(R.drawable.ic_launcher);
        builder.setCancelable(false); // 点击对话框意外区域,包括返回按钮,对话框都不会退出
        builder.setTitle("标题");
        builder.setMessage("是否退出");
        builder.setNegativeButton("取消", new OnClickListener() {

            @Override
            public void onClick(DialogInterface dialog, int which) {
                dialog.dismiss();
            }
        });
        builder.setPositiveButton("确定", new OnClickListener() {

            @Override
            public void onClick(DialogInterface dialog, int which) {
                System.exit(0);
            }
        });
        builder.create().show();
    }

    /**
     * 单选对话框
     */
    private void oneAlertDialog() {
        AlertDialog.Builder builder = new Builder(this);
        builder.setIcon(R.drawable.ic_launcher);
        builder.setTitle("请选择性别");
        final String[] str = new String[] { "男", "女" };
        builder.setSingleChoiceItems(str, 1, new OnClickListener() {

            @Override
            public void onClick(DialogInterface dialog, int which) {
                dialog.dismiss();
                Toast.makeText(getApplicationContext(), str[which], 0).show();
            }
        });

        builder.create().show();
    }

    /**
     * 多选对话框
     */
    private void multiAlertDialog() {
        AlertDialog.Builder builder = new Builder(this);
        builder.setIcon(R.drawable.ic_launcher);
        builder.setTitle("请选择喜欢的水果");
        map = new HashMap<String, String>();
        final String[] items = { "香蕉", "苹果", "雪梨", "火龙果", "水蜜桃" };
        boolean[] checkedItems = { true, false, false, false, false };
        //将为true的水果放进map中
        for (int i = 0; i < checkedItems.length; i++) {
            if (checkedItems[i]) {
                map.put(items[i], items[i]);
            }
        }
        builder.setMultiChoiceItems(items, checkedItems, new OnMultiChoiceClickListener() {

            @Override
            public void onClick(DialogInterface dialog, int which, boolean isChecked) {
                if (isChecked) {
                    map.put(items[which], items[which]);
                    Toast.makeText(getApplicationContext(), "你选择了" + items[which], 0).show();
                } else {
                    map.remove(items[which]);
                    Toast.makeText(getApplicationContext(), "你取消了" + items[which], 0).show();
                }
            }

        });

        builder.setPositiveButton("确定", new OnClickListener() {

            @Override
            public void onClick(DialogInterface dialog, int which) {
                Set<String> keySet = map.keySet();
                String string = "";
                Iterator<String> iterator = keySet.iterator();
                while (iterator.hasNext()) {
                    string += map.get(iterator.next());
                }
                Toast.makeText(getApplicationContext(), "你选择了" + string, 0).show();
            }
        });
        builder.setNegativeButton("取消", new OnClickListener() {

            @Override
            public void onClick(DialogInterface dialog, int which) {
                Toast.makeText(getApplicationContext(), "你取消了选择对话框", 0).show();
            }
        });

        builder.show();
    }

    /**
     * 自定义对话框
     */
    private void customAlertDialog() {
        AlertDialog.Builder builder = new Builder(this);
        View view = getLayoutInflater().inflate(R.layout.custom_dialog_view, null, false);
        builder.setView(view);
        Button btnAffirm = (Button) view.findViewById(R.id.bt_affirm);
        Button btnCancel = (Button) view.findViewById(R.id.bt_cancel);
        btnAffirm.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(getApplicationContext(), "确认", 0).show();
            }
        });
        btnCancel.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                Toast.makeText(getApplicationContext(), "取消", 0).show();
            }
        });
        builder.create().show();
    }

    /**
     * 日期选择器
     */
    private void dateAlertDialog() {
        // 当前日期
        Calendar calendar = Calendar.getInstance();
        int year = calendar.get(Calendar.YEAR);
        int monthOfYear = calendar.get(Calendar.MONTH);
        int dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH);
        DatePickerDialog dialog = new DatePickerDialog(this, new OnDateSetListener() {

            // monthOfYear月份要加多一个1
            @Override
            public void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth) {
                Toast.makeText(getApplicationContext(), year + "年" + (monthOfYear + 1) + "月" + dayOfMonth + "日", 0)
                        .show();
            }
        }, year, monthOfYear, dayOfMonth);
        dialog.show();
    }

    /**
     * 时间选择器
     */
    private void timeAlertDialog() {
        // 当前时间
        Calendar calendar = Calendar.getInstance();
        int hourOfDay = calendar.get(Calendar.HOUR_OF_DAY);
        int minute = calendar.get(Calendar.MINUTE);
        boolean is24HourView = true;
        TimePickerDialog dialog = new TimePickerDialog(this, new OnTimeSetListener() {

            @Override
            public void onTimeSet(final TimePicker view, final int hourOfDay, final int minute) {
                Toast.makeText(getApplicationContext(), hourOfDay + "时" + minute + "分", 0).show();
            }
        }, hourOfDay, minute, is24HourView);
        dialog.show();
    }

    /**
     * 加载对话框
     */
    private void progressAlertDialog() {
        ProgressDialog dialog = new ProgressDialog(this);
        // dialog.setTitle("正在拼命加载");
        // dialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); //设置进度样式
        // 水平进度条
        dialog.setTitle("正在拼命下载");
        // true:进度条不显示当前进度
        dialog.setIndeterminate(true);
        dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); // 设置进度样式
        dialog.setMax(100);
        // dialog.setProgress(50); //当前进度
        dialog.setMessage("当前的下载进度:50%");
        dialog.show();
    }

    /**
     * 返回按钮监听
     * 
     * @param keyCode
     * @param event
     * @return
     */
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        switch (keyCode) {
        case KeyEvent.KEYCODE_BACK:
            alertDialog();
            break;
        default:
            break;
        }
        return super.onKeyDown(keyCode, event);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
        case R.id.bt_alertdialog:
            alertDialog();
            break;
        case R.id.bt_one_alertdialog:
            oneAlertDialog();
            break;
        case R.id.bt_multi_alertdialog:
            multiAlertDialog();
            break;
        case R.id.bt_custom_alertdialog:
            customAlertDialog();
            break;
        case R.id.bt_date_alertdialog:
            dateAlertDialog();
            break;
        case R.id.bt_time_alertdialog:
            timeAlertDialog();
            break;
        case R.id.bt_progress_alertdialog:
            progressAlertDialog();
            break;
        default:
            break;
        }
    }

}

custom_dialog_view.xml

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

    <TextView
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:background="#44ff0000"
        android:gravity="center"
        android:text="设置密码"
        android:textColor="#ffffff" />

    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="请输入用户名" />

    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="请输入密码" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal" >

        <Button
            android:id="@+id/bt_affirm"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="确定" />

        <Button
            android:id="@+id/bt_cancel"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="取消" />
    </LinearLayout>

</LinearLayout>

activity_main.xml

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

    <Button
        android:id="@+id/bt_alertdialog"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="普通对话框" />

    <Button
        android:id="@+id/bt_one_alertdialog"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="单选对话框" />

    <Button
        android:id="@+id/bt_multi_alertdialog"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="多选对话框" />

    <Button
        android:id="@+id/bt_custom_alertdialog"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="自定义对话框" />

    <Button
        android:id="@+id/bt_date_alertdialog"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="日期选择对话框" />

    <Button
        android:id="@+id/bt_time_alertdialog"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="时间选择对话框" />

    <Button
        android:id="@+id/bt_progress_alertdialog"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="加载对话框" />

</LinearLayout>
作者:hellcw 发表于2016/9/28 23:46:53 原文链接
阅读:129 评论:0 查看评论

Hex文件转Bin文件

$
0
0

在嵌入式开发中,编译器生成的目标文件一般都是 .hex 文件。
为什么要转换,直接使用hex文件不行吗,可是我在开发过程中一直都是直接生成hex文件,然后进行下载,也没见出错?
在不清楚hex与bin文件的格式时,可能小伙伴会有这样的疑问。需要进行转换的原因是:hex文件中数据记录(record)并不是按照 “起始地址–>终止地址” 这样的顺序进行排列的,由于每行数据都包含起始地址和数据长度,所以hex文件中数据不需要按照地址顺序从低到高进行排列;而Bin文件中的数据则是严格按照地址顺序进行排列的。

首先需要了解hex文件的格式,可以参考官方资料Intel Hexadecimal Object File Format Specification,也可以看我的另一篇blog——HEX文件说明

最近做ECU的上位机下载工具,其中一步是将hex文件的内容转成按地址顺序(从低到高)排列的二进制数据(bin文件)。
于是我找了一个转换工具——hex2bin源码地址。下面我就该工具的整个转换过程进行一些分析。
整个过程主要分为两步:
1. 遍历整个hex文件,找出最小地址和最大地址(也就是起始地址和结束地址),算出数据长度(数据长度=结束地址-起始地址),根据得到的数据长度,分配对应大小的内存(开辟一个数组);
2. 再次遍历整个hex文件,计算每条数据记录中的起始地址与hex文件起始地址的偏移量,按照偏移量将该条数据记录中的数据部分写入第一步的数组中。(这样就实现了按照从低到高的地址顺序排列整个hex文件的数据)。
最后只需要将该数组写出到文件中即可。

首先使用FILE * fopen(const char * path, const char * mode);打开hex文件,然后是第一次遍历,找出起始地址和数据长度。

    /* 第一次遍历hex文件,获取地址范围(Lowest_Address和Highest_Address) */
    /* get highest and lowest addresses so that we can allocate the right size */
    do
    {
        unsigned int i;

        /* Read a line from input file. */
        GetLine(Line,Filin);
        Record_Nb++;

        /* Remove carriage return/line feed(回车/换行) at the end of line. */
        i = strlen(Line);

        if (--i != 0)
        {
            if (Line[i] == '\n') Line[i] = '\0';

            /* Scan the first two bytes and nb of bytes.
               The two bytes are read in First_Word since its use depend on the
               record type: if it's an extended address record or a data record.
               */

            /* sscanf() - 从一个字符串中读进与指定格式匹配的数据, 成功则返回参数数目.
               ":%2x%4x%2x%s":格式说明 :冒号开头,2个十六进制数,4个十六进制数, 2个十六进制数,余下的当做字符串  */
            result = sscanf (Line, ":%2x%4x%2x%s",&Nb_Bytes,&First_Word,&Type,Data_Str);
            if (result != 4) fprintf(stderr,"Error in line %d of hex file\n", Record_Nb);

            p = (char *) Data_Str; //p表示指向数据域(包括checksum)的指针

            /* If we're reading the last record, ignore it. */
            switch (Type)
            {
            /* Data record */
            case 0:
                if (Nb_Bytes == 0)
                    break;

                Address = First_Word;

                if (Seg_Lin_Select == SEGMENTED_ADDRESS)
                {
                    Phys_Addr = (Segment << 4) + Address;
                }
                else
                {
                    /* LINEAR_ADDRESS or NO_ADDRESS_TYPE_SELECTED
                       Upper_Address = 0 as specified in the Intel spec. until an extended address
                       record is read. */
                    Phys_Addr = ((Upper_Address << 16) + Address);
                }

                if (Verbose_Flag) fprintf(stderr,"Physical Address: %08X\n",Phys_Addr);

                /* 获取地址范围(Lowest_Address和Highest_Address) */
                /* Set the lowest address as base pointer. */
                if (Phys_Addr < Lowest_Address)
                    Lowest_Address = Phys_Addr;

                /* Same for the top address. */
                temp = Phys_Addr + Nb_Bytes -1;

                if (temp > Highest_Address)
                {
                    Highest_Address = temp;
                    if (Verbose_Flag) fprintf(stderr,"Highest_Address: %08X\n",Highest_Address);
                }
                break;

            case 1:
                if (Verbose_Flag) fprintf(stderr,"End of File record\n");
                break;

            case 2:
                /* First_Word contains the offset. It's supposed to be 0000 so
                   we ignore it. */

                /* First extended segment address record ? */
                if (Seg_Lin_Select == NO_ADDRESS_TYPE_SELECTED)
                    Seg_Lin_Select = SEGMENTED_ADDRESS;

                /* Then ignore subsequent extended linear address records */
                if (Seg_Lin_Select == SEGMENTED_ADDRESS)
                {
                    result = sscanf (p, "%4x%2x",&Segment,&temp2);
                    if (result != 2) fprintf(stderr,"Error in line %d of hex file\n", Record_Nb);

                    if (Verbose_Flag) fprintf(stderr,"Extended Segment Address record: %04X\n",Segment);

                    /* Update the current address. */
                    Phys_Addr = (Segment << 4);
                }
                else
                {
                    fprintf(stderr,"Ignored extended linear address record %d\n", Record_Nb);
                }
                break;

            case 3:
                if (Verbose_Flag) fprintf(stderr,"Start Segment Address record: ignored\n");
                break;

            case 4:
                /* First_Word contains the offset. It's supposed to be 0000 so
                   we ignore it. */

                /* First extended linear address record ? */
                if (Seg_Lin_Select == NO_ADDRESS_TYPE_SELECTED)
                    Seg_Lin_Select = LINEAR_ADDRESS;

                /* Then ignore subsequent extended segment address records */
                if (Seg_Lin_Select == LINEAR_ADDRESS)
                {
                    result = sscanf (p, "%4x%2x",&Upper_Address,&temp2);    //取出基地址(Extended Linear Address)和checksum
                    if (result != 2) fprintf(stderr,"Error in line %d of hex file\n", Record_Nb);

                    if (Verbose_Flag) fprintf(stderr,"Extended Linear Address record: %04X\n",Upper_Address);

                    /* Update the current address. */
                    Phys_Addr = (Upper_Address << 16);

                    if (Verbose_Flag) fprintf(stderr,"Physical Address: %08X\n",Phys_Addr);
                }
                else
                {
                    fprintf(stderr,"Ignored extended segment address record %d\n", Record_Nb);
                }
                break;

            case 5:
                if (Verbose_Flag) fprintf(stderr,"Start Linear Address record: ignored\n");
                break;

            default:
                if (Verbose_Flag) fprintf(stderr,"Unknown record type: %d at %d\n",Type,Record_Nb);
                break;
            }
        }
    }
    while (!feof (Filin)); 
    /*feof()用来侦测是否读取到了文件尾, 参数stream 为fopen()所返回的文件指针. 如果已读到文件尾则返回非零值, 其他情况返回0.*/

每次读取一行,循环读取,直到文件尾。

申请指定长度的内存(malloc),然后进行第二次遍历,这次的目的是将数据按地址顺序进行排列。

    /* 第二次遍历hex文件, 处理数据 */
    /* Read the file & process the lines. */
    do /* repeat until EOF(Filin) */
    {
        unsigned int i;

        /* Read a line from input file. */
        GetLine(Line,Filin);
        Record_Nb++;

        /* Remove carriage return/line feed at the end of line. */
        i = strlen(Line);

        //fprintf(stderr,"Record: %d; length: %d\n", Record_Nb, i);

        if (--i != 0)
        {
            if (Line[i] == '\n') Line[i] = '\0';

            /* Scan the first two bytes and nb of bytes.
               The two bytes are read in First_Word since its use depend on the
               record type: if it's an extended address record or a data record.
            */
            result = sscanf (Line, ":%2x%4x%2x%s",&Nb_Bytes,&First_Word,&Type,Data_Str);
            if (result != 4) fprintf(stderr,"Error in line %d of hex file\n", Record_Nb);

            Checksum = Nb_Bytes + (First_Word >> 8) + (First_Word & 0xFF) + Type; //前4个字节累加

            p = (char *) Data_Str;

            /* If we're reading the last record, ignore it. */
            switch (Type)
            {
            /* Data record */
            case 0:
                if (Nb_Bytes == 0)
                {
                    fprintf(stderr,"0 byte length Data record ignored\n");
                    break;
                }

                Address = First_Word;

                if (Seg_Lin_Select == SEGMENTED_ADDRESS)
                    Phys_Addr = (Segment << 4) + Address;
                else
                    /* LINEAR_ADDRESS or NO_ADDRESS_TYPE_SELECTED
                       Upper_Address = 0 as specified in the Intel spec. until an extended address
                       record is read. */
                    if (Address_Alignment_Word)
                        Phys_Addr = ((Upper_Address << 16) + (Address << 1)) + Offset;
                    else
                        Phys_Addr = ((Upper_Address << 16) + Address);

                /* Check that the physical address stays in the buffer's range. */
                if ((Phys_Addr >= Lowest_Address) && (Phys_Addr <= Highest_Address))
                {
                    /* The memory block begins at Lowest_Address */
                    Phys_Addr -= Lowest_Address; /* 计算该条数据记录相对于hex文件起始地址的偏移量 */

                    p = ReadDataBytes(p); /* 根据偏移量将该条记录中的数据写入指定的数组中 */

                    /* Read the Checksum value. */
                    result = sscanf (p, "%2x",&temp2);
                    if (result != 1) fprintf(stderr,"Error in line %d of hex file\n", Record_Nb);

                    /* Verify Checksum value. */
                    /* 校验和 = 0x100 - 除checksum之外所有字节的累加和 */
                    Checksum = (Checksum + temp2) & 0xFF;
                    VerifyChecksumValue();
                }
                else
                {
                    if (Seg_Lin_Select == SEGMENTED_ADDRESS)
                        fprintf(stderr,"Data record skipped at %4X:%4X\n",Segment,Address);
                    else
                        fprintf(stderr,"Data record skipped at %8X\n",Phys_Addr);
                }

                break;

            /* End of file record */
            case 1:
                /* Simply ignore checksum errors in this line. */
                break;

            /* Extended segment address record */
            case 2:
                /* First_Word contains the offset. It's supposed to be 0000 so
                   we ignore it. */

                /* First extended segment address record ? */
                if (Seg_Lin_Select == NO_ADDRESS_TYPE_SELECTED)
                    Seg_Lin_Select = SEGMENTED_ADDRESS;

                /* Then ignore subsequent extended linear address records */
                if (Seg_Lin_Select == SEGMENTED_ADDRESS)
                {
                    result = sscanf (p, "%4x%2x",&Segment,&temp2);
                    if (result != 2) fprintf(stderr,"Error in line %d of hex file\n", Record_Nb);

                    /* Update the current address. */
                    Phys_Addr = (Segment << 4);

                    /* Verify Checksum value. */
                    Checksum = (Checksum + (Segment >> 8) + (Segment & 0xFF) + temp2) & 0xFF;
                    VerifyChecksumValue();
                }
                break;

            /* Start segment address record */
            case 3:
                /* Nothing to be done since it's for specifying the starting address for
                   execution of the binary code */
                break;

            /* Extended linear address record */
            case 4:
                /* First_Word contains the offset. It's supposed to be 0000 so
                   we ignore it. */

                if (Address_Alignment_Word) /*默认为false*/
                {
                    sscanf (p, "%4x",&Offset);
                    Offset = Offset << 16;
                    Offset -= Lowest_Address;
                }
                /* First extended linear address record ? */
                if (Seg_Lin_Select == NO_ADDRESS_TYPE_SELECTED)
                    Seg_Lin_Select = LINEAR_ADDRESS;

                /* Then ignore subsequent extended segment address records */
                if (Seg_Lin_Select == LINEAR_ADDRESS)
                {
                    result = sscanf (p, "%4x%2x",&Upper_Address,&temp2);
                    if (result != 2) fprintf(stderr,"Error in line %d of hex file\n", Record_Nb);

                    /* Update the current address. */
                    Phys_Addr = (Upper_Address << 16);

                    /* Verify Checksum value. */
                    Checksum = (Checksum + (Upper_Address >> 8) + (Upper_Address & 0xFF) + temp2)
                               & 0xFF;
                    VerifyChecksumValue();
                }
                break;

            /* Start linear address record */
            case 5:
                /* Nothing to be done since it's for specifying the starting address for
                   execution of the binary code */
                break;
            default:
                fprintf(stderr,"Unknown record type\n");
                break;
            }
        }
    }
    while (!feof (Filin));

最后将数组中的内容输出到文件,即可得到bin文件。

按照上面的思路我用Java写了一个转换类(用在上面提到的ECU下载工具中),感兴趣的同学可以看一看。
其中碰到的坑有必要提一下,由于Hex文件中的数据是采用ASC II码的,而Bin文件中的数据是直接使用的二进制(不存在编码),因此在转换过程中涉及到编码转换。另外还有一点需要提一下,由于Java没有无符号型,如果将读到的数据赋给byte型变量,在调试的过程中使用print输出时会看到乱码(数据溢出byte类型的范围)。

作者:ZF_C_CQUPT 发表于2016/9/29 0:35:52 原文链接
阅读:138 评论:0 查看评论

Android自定义View——QQ音乐中圆形旋转碟子

$
0
0

QQ音乐中圆形旋转碟子


思路分析:

1、在onMeasure中测量整个View的宽和高后,设置宽高

2、获取我们res的图片资源后,在ondraw方法中进行绘制圆形图片

3、通过Handler发送Runnable来启动旋转线程(如果只想做圆形头像的话,这步可以去掉)

4、在布局中使用我们的View


效果图:



贴出我们的变量信息:

    //view的宽和高
    int mHeight = 0;
    int mWidth = 0;
    //圆形图片
    Bitmap bitmap = null;
    //圆形图片的真实半径
    int radius = 0;
    //旋转动画的矩形
    Matrix matrix = new Matrix();
    //旋转动画的角度
    int degrees = 0;

步骤一:测量整个View的宽和高后,设置宽高

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //测量整个View的宽和高
        mWidth = measuredWidth(widthMeasureSpec);
        mHeight= measuredHeight(heightMeasureSpec);
        setMeasuredDimension(mWidth, mHeight);
    }

    private int measuredWidth(int widthMeasureSpec) {
        int Mode = MeasureSpec.getMode(widthMeasureSpec);
        int Size = MeasureSpec.getSize(widthMeasureSpec);
        if (Mode == MeasureSpec.EXACTLY) {
            mWidth = Size;
        } else {
            //由图片决定宽度
            int value = getPaddingLeft() + getPaddingRight() + bitmap.getWidth();
            if (Mode == MeasureSpec.AT_MOST) {
                //由图片和Padding决定宽度,但是不能超过View的宽
                mWidth = Math.min(value, Size);
            }
        }
        return mWidth;
    }

    private int measuredHeight(int heightMeasureSpec) {
        int Mode = MeasureSpec.getMode(heightMeasureSpec);
        int Size = MeasureSpec.getSize(heightMeasureSpec);
        if (Mode == MeasureSpec.EXACTLY) {
            mHeight = Size;
        } else {
            //由图片决定高度
            int value = getPaddingTop() + getPaddingBottom() + bitmap.getHeight();
            if (Mode == MeasureSpec.AT_MOST) {
                //由图片和Padding决定高度,但是不能超过View的高
                mHeight = Math.min(value, Size);
            }
        }
        return mHeight;
    }

步骤二:获取我们res的图片资源后,在ondraw方法中进行绘制圆形图片
        //获取res的图片资源
        bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.icon);
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.concat(matrix);
        //真实的半径必须是View的宽高最小值
        radius = Math.min(mWidth, mHeight);
        //如果图片本身宽高太大,进行相应的缩放
        bitmap = Bitmap.createScaledBitmap(bitmap, radius, radius, false);
        //画圆形图片
        canvas.drawBitmap(createCircleImage(bitmap, radius), 0, 0, null);
        matrix.reset();
    }

    private Bitmap createCircleImage(Bitmap source, int radius) {
        Paint paint = new Paint();
        paint.setAntiAlias(true);
        Bitmap target = Bitmap.createBitmap(radius, radius, Bitmap.Config.ARGB_8888);
        //产生一个同样大小的画布
        Canvas canvas = new Canvas(target);
        //首先绘制圆形
        canvas.drawCircle(radius / 2, radius / 2, radius / 2, paint);
        //使用SRC_IN模式显示后画图的交集处
        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
        //绘制图片,从(0,0)画
        canvas.drawBitmap(source, 0, 0, paint);
        return target;
    }

步骤三:通过Handler发送Runnable来启动旋转线程

        //开始旋转
        mHandler.post(runnable);
    //-----------旋转动画-----------
    Handler mHandler = new Handler();
    Runnable runnable = new Runnable() {
        @Override
        public void run() {
            matrix.postRotate(degrees++, radius / 2, radius / 2);
            //重绘
            invalidate();
            mHandler.postDelayed(runnable, 50);
        }
    };

步骤四:在布局中使用我们的View

    <com.handsome.cycle.MyCycleView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

下面是整个类的源码

public class MyCycleView extends View {

    //view的宽和高
    int mHeight = 0;
    int mWidth = 0;
    //圆形图片
    Bitmap bitmap = null;
    //圆形图片的真实半径
    int radius = 0;
    //旋转动画的矩形
    Matrix matrix = new Matrix();
    //旋转动画的角度
    int degrees = 0;

    //-----------旋转动画-----------
    Handler mHandler = new Handler();
    Runnable runnable = new Runnable() {
        @Override
        public void run() {
            matrix.postRotate(degrees++, radius / 2, radius / 2);
            //重绘
            invalidate();
            mHandler.postDelayed(runnable, 50);
        }
    };

    public MyCycleView(Context context) {
        super(context);
        initView();
    }

    public MyCycleView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    public MyCycleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView();
    }

    public void initView() {
        //获取res的图片资源
        bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.icon);
        //开始旋转
        mHandler.post(runnable);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //测量整个View的宽和高
        mWidth = measuredWidth(widthMeasureSpec);
        mHeight = measuredHeight(heightMeasureSpec);
        setMeasuredDimension(mWidth, mHeight);
    }

    private int measuredWidth(int widthMeasureSpec) {
        int Mode = MeasureSpec.getMode(widthMeasureSpec);
        int Size = MeasureSpec.getSize(widthMeasureSpec);
        if (Mode == MeasureSpec.EXACTLY) {
            mWidth = Size;
        } else {
            //由图片决定宽度
            int value = getPaddingLeft() + getPaddingRight() + bitmap.getWidth();
            if (Mode == MeasureSpec.AT_MOST) {
                //由图片和Padding决定宽度,但是不能超过View的宽
                mWidth = Math.min(value, Size);
            }
        }
        return mWidth;
    }

    private int measuredHeight(int heightMeasureSpec) {
        int Mode = MeasureSpec.getMode(heightMeasureSpec);
        int Size = MeasureSpec.getSize(heightMeasureSpec);
        if (Mode == MeasureSpec.EXACTLY) {
            mHeight = Size;
        } else {
            //由图片决定高度
            int value = getPaddingTop() + getPaddingBottom() + bitmap.getHeight();
            if (Mode == MeasureSpec.AT_MOST) {
                //由图片和Padding决定高度,但是不能超过View的高
                mHeight = Math.min(value, Size);
            }
        }
        return mHeight;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.concat(matrix);
        //真实的半径必须是View的宽高最小值
        radius = Math.min(mWidth, mHeight);
        //如果图片本身宽高太大,进行相应的缩放
        bitmap = Bitmap.createScaledBitmap(bitmap, radius, radius, false);
        //画圆形图片
        canvas.drawBitmap(createCircleImage(bitmap, radius), 0, 0, null);
        matrix.reset();
    }

    private Bitmap createCircleImage(Bitmap source, int radius) {
        Paint paint = new Paint();
        paint.setAntiAlias(true);
        Bitmap target = Bitmap.createBitmap(radius, radius, Bitmap.Config.ARGB_8888);
        //产生一个同样大小的画布
        Canvas canvas = new Canvas(target);
        //首先绘制圆形
        canvas.drawCircle(radius / 2, radius / 2, radius / 2, paint);
        //使用SRC_IN模式显示后画图的交集处
        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
        //绘制图片,从(0,0)画
        canvas.drawBitmap(source, 0, 0, paint);
        return target;
    }
}



作者:qq_30379689 发表于2016/9/29 0:37:15 原文链接
阅读:170 评论:1 查看评论

Android产品研发(二十四)-->内存泄露场景与检测

$
0
0

转载请标明出处:一片枫叶的专栏

上一篇文章中本文我们讲解了一个Android产品研发中可能会碰到的一个问题:如何在App中保存静态秘钥以及保证其安全性。许多的移动app需要在app端保存一些静态字符串常量,其可能是静态秘钥、第三方appId等。在保存这些字符串常量的时候就涉及到了如何保证秘钥的安全性问题。如何保证在App中静态秘钥唯一且正确安全,这是一个很重要的问题,公司的产品中就存在着静态字符串常量类型的秘钥,所以一个明显的问题就是如何生成秘钥,保证秘钥的安全性?上一篇文章中我们做了一个简单的介绍。

本文我们将讲解一下关于Android开发过程中常见的内存泄露场景与检测方案。Android系统为每个应用程序分配的内存是有限的,当一个应用中产生的内存泄漏的情况比较多时,这就会导致应用所需要的内存超过这个系统分配的内存限额,进而造成了内存溢出而导致应用崩溃。在实际的开发过程中我们由于对程序代码的不当操作随时都有可能造成内存泄露。

(1)什么是内存泄露

当一个对象已经不需要再使用了,本该被回收时,而有另外一个正在使用的对象持有它的引用从而导致它不能被回收,这导致本该被回收的对象不能被回收而停留在堆内存中,这就产生了内存泄漏。

(2)系统分配的应用内存大小

ActivityManager的getMemoryClass()获得内用正常情况下内存的大小
ActivityManager的getLargeMemoryClass()可以获得开启largeHeap最大的内存大小

ActivityManager activityManager = (ActivityManager)context.getSystemService(Context.ACTIVITY_SERVICE);
activityManager.getMemoryClass();
activityManager.getLargeMemoryClass();

需要指出的是这里获取的内存大小是JVM为进程分配的内存大小,而当我们的应用中存在多个进程的时候,该应用理论上的内存大小限制:

  • 应用内存 = 进程内存大小 * 进程个数

所以当我们应用需要较大内存的时候也可以考虑通过多进程的方式进而获取更多的系统内存。

这样获取到的应用内存大小就是应用所能获取到的最大内存大小,当应用需要更多内存以支持其运行的时候,系统无法为其分配更多的内存,这样就造成了OOM的异常。

(3)内存泄露的常见场景

  • 非静态内部类,静态实例化
/**
 * 自定义实现的Activity
 */
public class MyActivity extends AppCompatActivity {

    /**
     * 静态成员变量
     */
    public static InnerClass innerClass = null;

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

        innerClass = new InnerClass();
    }

    class InnerClass {

        public void doSomeThing() {
        }
    }
}

这里内部类InnerClass隐式的持有外部类MyActivity的引用,而在MyActivity的onCreate方法中调用了

innerClass = new InnerClass();

这样innerClass就会在MyActivity创建的时候是有了他的引用,而innerClass是静态类型的不会被垃圾回收,MyActivity在执行onDestory方法的时候由于被innerClass持有了引用而无法被回收,所以这样MyActivity就总是被innerClass持有而无法回收造成内存泄露。

  • 不正确的使用Context对象造成内存泄露
/**
 * 自定义单例对象
 */
public class Single {
    private static Single instance;
    private Context context;
    private Object obj = new Object();

    private Single(Context context) {
        this.context = context;
    }

    /**
     * 初始化获取单例对象
     */
    public static Single getInstance(Context context) {
        if (instance == null) {
            synchronized(obj) {
                if (instance == null) {
                    instance = new Single(context);
                }
            }
        }
        return instance;
    }
}

我们通过懒汉模式创建单例对象,并且在创建的时候需要传入一个Context对象,而这时候如果我们使用Activity、Service等Context对象,由于单例对象的生命周期与进程的生命周期相同,会造成我们传入的Activity、Service对象无法被回收,这时候就需要我们传入Application对象,或者在方法中使用Application对象,上面的代码可以改成:

/**
 * 自定义单例对象
 */
public class Single {
    private static Single instance;
    private Context context;
    private Object obj = new Object();

    private Single(Context context) {
        this.context = context;
    }

    /**
     * 初始化获取单例对象
     */
    public static Single getInstance(Context context) {
        if (instance == null) {
            synchronized(obj) {
                if (instance == null) {
                    instance = new Single(context.getApplication());
                }
            }
        }
        return instance;
    }
}

这样就不会有内存泄露的问题了。

  • 使用Handler异步消息通信

在日常开发中我们通常都是这样定义Handler对象:

/**
 * 定义Handler成员变量
 */
Handler handler = new Handler() {  
        @Override  
        public void handleMessage(Message msg) {  
            dosomething();  

        }  
    };  

但是这样也存在着一个隐藏的问题:在Activity中使用Handler创建匿名内部类会隐式的持有外部Activity对象的引用,当子线程使用Handler暂时无法完成异步任务时,handler对象无法销毁,同时由于隐式的持有activity对象的引用,造成activity对象以及相关的组件与资源文件同样无法销毁,造成内存泄露。
好吧,那么如何解决这个问题呢?具体可以参考:Android中使用Handler造成内存泄露的分析和解决

  • 使用资源文件结束之后未关闭

在使用一些资源性对象比如(Cursor,File,Stream,ContentProvider等)往往都用了一些缓冲,我们在不使用的时候,应该及时关闭它们,以便它们的缓冲及时回收内存。它们的缓冲不仅存在于Java虚拟机内,还存在于Java虚拟机外。如果我们仅仅是把它的引用设置为null,而不关闭它们,往往会造成内存泄露。

因为有些资源性对象,比如SQLiteCursor(在析构函数finalize(),如果我们没有关闭它,它自己会调close()关闭),如果我们没有关闭它,系统在回收它时也会关闭它,但是这样的效率太低了。因此对于资源性对象在不使用的时候,应该立即调用它的close()函数,将其关闭掉,然后再置为null.在我们的程序退出时一定要确保我们的资源性对象已经关闭。

/**
 * 初始化Cursor对象
 */
Cursor cursor = getContentResolver().query(uri...); 
if (cursor.moveToNext()) { 
    /**
     * 执行自设你的业务代码
     */ 
     doSomeThing();
}

这时候我们应当在doSomeThing之后执行cursor的close方法,关闭资源对象。

```
/**
 * 初始化Cursor对象
 */
Cursor cursor = getContentResolver().query(uri...); 
if (cursor.moveToNext()) { 
    /**
     * 执行自设你的业务代码
     */ 
     doSomeThing();
}

if (cursor != null) {
    cursor.close();
}
  • Bitmap使用不当

bitmap对象使用的内存较大,当我们不再使用Bitmap对象的时候一定要执行recycler方法,这里需要指出的是当我们在代码中执行recycler方法,Bitmap并不会被立即释放掉,其只是通知虚拟机该Bitmap可以被recycler了。

当然了现在项目中使用的一些图片库已经帮我们对图片资源做了很好的优化缓存工作,是我们省去了这些操作。

  • 一些框架使用了注册方法而未反注册

比如我们时常使用的事件总线框架-EventBus,具体的实现原理可参考:Android EventBus源码解析 带你深入理解EventBus当我们需要注册某个Activity时需要在onCreate中:

EventBus.getDefault().register(this);

然后这样之后就没有其他操作的话就会出现内存泄露的情况,因为EventBus对象会是有该Activity的引用,即使执行了改Activity的onDestory方法,由于被EventBus隐式的持有了该对象的引用,造成其无法被回收,这时候我们需要在onDestory方法中执行:

EventBus.getDefault().unregister(this);
  • 集合中的一些方法的错误使用

(1)比如List列表静态化,只是添加元素而不再使用时不清楚元素;
(2)map对象只是put,而无remove操作等等;

(4)关于内存泄露检测的两个开源方案

在项目中使用到了两个开源的内存泄露检测库:

LeakCanary
BlockCanary

推荐使用一下这两个库检测一下项目,或许会有意想不到的收获(曾检测出一个主流第三方SDK的内存泄露BUG)。

关于LeakCanary,可参考我的:Android内存泄露监测之leakcanary,大概讲解了一下LeakCanary的使用方式。

BlockCanary库的使用方式和LeakCanary类似,更多关于其使用方式的介绍可查看其github文档。

除了以上两个开源库之外,还可以考虑使用软引用的方式,更多关于Java引用类型的知识,可参考我的:Java中的四种引用

(5)关于屏蔽内存泄露的建议

  • 正确的保证内存对象的生命周期,就是尽量保证内存对象在其生命周期内创建于结束,比如Android中的“上帝对象Context”,要保证不同的场景下使用不同的Context对象,下面是一张Context对象的使用场景图:
    这里写图片描述

  • 对资源对象的使用要在使用完成之后保证调用其资源的关闭方法,而非仅仅是对资源引用的关闭操作;

  • 静态化资源对象其生命周期就会变成与进程的生命周期相同,在使用静态化时一定要考虑清楚该对象静态化是否存在内存泄露的可能;

  • 对Android开发中常见的内存泄露场景要做到了然于胸,了解一些Android中常见的内存泄露检测方法;

总结:

关于内存泄露其实主要记住一个原则就好:确保对象能够在正确的时机被回收掉。然后我们根据具体内存泄露的场景具体解决就好了。


另外对产品研发技术,技巧,实践方面感兴趣的同学可以参考我的:
Android产品研发(十三)–>App轮训操作
Android产品研发(十四)–>App升级与更新
Android产品研发(十五)–>内存对象序列化
Android产品研发(十六)–>开发者选项
Android产品研发(十七)–>Hybrid开发
Android产品研发(十八)–>webview问题集锦
Android产品研发(十九)–>Android studio中的单元测试
Android产品研发(二十)–>代码Review
Android产品研发(二十一)–>Android中的UI优化
Android产品研发(二十二)–>Android实用调试技巧
Android产品研发(二十三)–>Android中保存静态秘钥实践


本文以同步至github中:https://github.com/yipianfengye/AndroidProject,欢迎star和follow


作者:qq_23547831 发表于2016/9/29 9:19:27 原文链接
阅读:89 评论:0 查看评论

Android Activity全面解析

$
0
0

Android Activity全面解析

就从Android四大组件Activity开始。

1.Activity生命周期方法完全解析

1).onCreate方法
当Activity第一次创建的时候调用。这个方法里主要是提供给我们做一些初始化操作,如:创建view、绑定数据到view。同时这个方法里还带有一个Bundle参数,这个参数的主要的用途会在后面的onSavedInstanceState方法的介绍里再来讲解。

2).onStart方法
紧接着onCreate方法执行的是onStart方法,该方法的执行表示Activity已经显示了但是还无法和用户交互,只有当执行到onResume方法的时候才可以进行交互。另外提一点,google的文档里有写onStart方法可以直接到onStop方法不经过onResume和onPause,我想了一下就在onStart方法里调用了一下finish()方法,果不其然onStart后就直接onStop了,但是感觉并没有什么卵用所以也就不分析了,有兴趣的可以自己实验一下。

3).onResume方法
调用到onResume方法后,Activity就可以与用户开始进行交互了,此时Activity就会位于Activity栈的栈顶了。至此一个Activity就完整的呈现在了我们的眼前并可以与之进行交互了。

4).onPause方法
当系统开始准备停止当前Activity的时候调用,在该方法中google给出的建议是存储一些变化的数据同时停止一些类似于动画等消耗CPU的工作。该方法的调用过程是很快的,否则会影响到后面的Activity的现实,所以在该方法里不宜做过多耗时操作。

5).onStop方法
紧接着onPause方法调用,此时Activity已经不再显示在用户面前了,此时新的Activity可能已经执行到onStart方法或者onResume方法了,所以此时可做一些较为重量级回收操作比方说关于数据库的一些读写操作等。

6)onRestart方法
onStop方法之后可能会调用到onRestart方法,这是因为代表的Activity正在被重新启动,然后紧接着就会继续走到onStart和onResume方法中。

7)onDestroy方法
该方法表示Activity生命周期中的最后一个方法,表示Activity方法将会被销毁,此时我们可以做一些回收操作。这里需要提到的一点是,即使一个Activity被销毁后app内部的static变量是不会被销毁的,因为static变量是全局的,activity销毁但是该app的进程并没有被杀死。所以说这一点尤为需要注意我们的static变量的使用,否则稍有不慎再次启动该activity的时候该static变量就会是一个dirty data!

2典型生命周期分析
通过上面对生命周期中各个方法的分析我们已经对Activity的各个生命周期方法中所做的事情做了一个完整的梳理,下面就针对几种典型的生命周期情况进行分析:

下面的分析都是针对一个特定的Activity进行分析:

1).启动Activity->返回桌面->再次回到Activity
根据前面对生命周期的分析可以不难知道这三个过程Activity的生命周期方法调用顺序如下:

一个Activity,它从启动到展现在用户面前的完成毁掉过程是onCreate()->onStart()->onResume()
按HOME键返回桌面:

此时会调用到onPause()->onStop()方法。
当再次回到Activity的时候会调用onRestart()->onStart()方法->onResume()方法:

2).按back退出activity
此时会走onPause()->onStop()->onDestroy()方法:

再次强调的是该方法即使退出了主Activity但是也没有杀掉进程,所以static变量并没有被销毁,再次进来的时候可能会是脏数据。就以我的经验碰到的最多的一个问题就是:有的时候会将一些名字什么的存在static变量里作为全局变量进行调用,此时测试人员的其中一个case会按back退出activity然后切换系统语言再次启动app。如果没有对static变量做一些销毁操作的话,再次回来就是一个dirty data,语言文字并没有切换导致了bug的存在。

3).在一个Activity中启动另一个Activity
从MainActivity中启动SecondActivity我们可以很清楚的看到MainActivity中的onPause方法执行完了以后然后新的SecondActivity的onCreate、onStart、onResume方法就会依次执行将SecondActivity显示出来,最后MainActivity的onStop方法才会被调用。这同时也验证了之前提到的,google并不建议在onPause方法里进行一些耗时操作。

这里我们可以做一个暴力一点的实验,在onPause方法里调用Thread.sleep(3000)方法让它睡上三秒,如下图所示明显我们可以看到SecondActivity在等待了3秒以后才被执行到,此时的界面会有一个明显的卡顿住的过程。所以特别强调在onPause方法里不要做任何耗时操作,同时一些如动画这样的消耗CPU性能的操作也需要及时关闭以能最快的启动新的activity。

3.与生命周期相关的一些重要方法
1)onSaveInstanceState与onRestoreInstanceState方法
前面提在介绍onCreate的时候提到过onCreate方法中带有一个Bundle参数,我们在正常启动Activity的时候打印这个参数会发先它是为null的。我们看google文档可以发现,这个参数只有当Activity重新初始化的时候才不会为null。那么什么叫重新初始化呢?举一个简单的例子,比方说手机横竖屏切换的时候如果我们没有在manifest文件的configchange属性里指定orientation|screenSize(在android4.0以上必须同时指定这两个属性,如果只写orientation不生效)就会在onPause之后调用到onSaveInstanceState(Bundle)方法,此时我们就可以往Bundle里存储一些数据,随后系统会杀死Activity然后再重启它,此时我们发现onCreate方法中的Bundle参数不为空:

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.i(TAG, “MainActivity onCrate”);
if (savedInstanceState != null) {
Log.i(TAG, “onCreate:” + savedInstanceState.getString(TAG));
} else {
Log.i(TAG, “onCreate:” + savedInstanceState);
}
}

@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
Log.i(TAG, “onSaveInstanceState”);
outState.putString(TAG, “outState”);
}

我们很容易发现在横竖屏切换的时候会调用到onSaveInstanceState方法,然后在Activity再次启动起来的时候onCreate方法中的Bundle就不会为空并且我们可以读到我们存储的一些值。虽然在onCreate里我们可以读到Bundle的值并取出来使用,但是google更建议的方式还是在onRestoreInstanceState里来读这个值。onRestoreInstanceState在onStart方法执行完成后调用,因为此时试图的初始化工作已经做完了,再取出值来在视图上进行数据绑定更加合理。

2.onConfigurationChanged方法
3.onNewIntent方法
当我们在manifest文件里对Activity的launchmode进行设置为singleTop、singleTask、singleInstance的时候都会有可能调用到此方法。如图所示:

SingleTask模式下MainActivity演示.png
在这里我对MainActivity在manifest里配置了singleTask,首先应用程序起来进入了MainActivity,然我我们通过MainActivity进入到了SecondeActivity,最后通过SecondActivity来再次启动MainActivity,这个时候因为我们对MainActivity指定的是singleTask模式启动,所以MainActivity的onNewIntent被调用到了。其实当我们对MainActivity指定为singleTop,singleInstance的时候也同样会被调用到,究其原因在于该方法主要是用于复用该Activity实例的时候调用的。

4.configChange的各种属性

configchange里面有这么多属性都可以进行配置,下面对其中一些较为常用的进行一番解释说明。
locate:主要是指系统切换语言
keyboard:代表键盘类型发生了变化,比方说由系统的软键盘切换到外置键盘
fontScale:用户在设置里切换了字号
orientation:屏幕方向发生了变化,不过在android 4.0以上不好使需要同scrennSize一起使用。

5.Activity的启动模式
Android应用程序都是由一个或多个Activity组成的,而Android内部则是通过栈来对Activity进行管理的。所谓栈就是一个先进后出的数据结构。正常情况下栈顶的Activity就是当前Task显示的Activity,当我们按back键的时候该Activity便会出栈。然而事实并不是这么简单,google在对Activity任务栈进行设计的时候考虑到了一些特殊需求所以便有了Activity的启动模式之说。
Activity的启动模式包含四种,分别是:standard、singleTop、singleTask、singleInstance,我们可以在manifest里通过Activity的launchmode进行指定。下面我就逐个介绍一下这四中模式。

1)standard模式
这是Activity的标准启动模式,如果我们不对Activity做任何特殊处理的情况下就默认为该模式启动,所以该模式并不需要在manifest或者Intent里进行指定。这个模式的问题在于会导致一个任务栈里会有多个该Activity的实例存在,很简单的一个例子就是我们在AcitivytA里启动ActivityA这样就会有两个ActivityA存在。假如说该Activity非常消耗资源,那么就有必要考虑下更改下Activity的启动模式了。

2)singleTop模式
该模式简单来说,启动的Activity已经在任务栈的栈顶话,那么再启动这个Activity的时候就不会创建该实例,同时会调用该Acitivity的onNewIntent方法(前面有提过该方法)。但是如果该Activity不在栈定的话,那么启动它的行为与standard模式并没有什么区别。

3)singleTask模式
singleTask指的是一个任务栈中只能存在一个这样的Acitivity。但是需要我们注意的是如果任务栈中没有该Activity的话系统就会帮我们创建一个Acitivity压入栈顶,但是如果存在该Activity的话就会销毁压在该Activity上的所有Activity最终让创建出来的Activity实例处于栈顶,同时也会回掉该Activity的onNewIntent方法。

4)singleInstance模式
该模式是四个模式当中最为特殊的一个,设置了该模式启动的Acitivyt会在一个独立的任务栈中开启,同事该任务栈有且只有一个这样的Activity实例,每次再启动这个Activity的时候就会在该任务栈里重用该Activity同时回掉onNewIntent方法。
singleInstace与singleTask的区别在于:singleTask启动的Activity在系统层面上来说是可以有多个实例的。比如说应用程序A想调用singleInstance模式下的ActivityA,而应用程序B也同样调用了,那么在应用程序A和B中就会各有一个ActivityA的实例。但如果该ActivityA是singleInstance模式的话,那么无论有多少个应用程序调用它,它都只可能在系统中存在一个实例同时该实例还是位于它自己的一个单独的任务栈中。

5).通过Intent中setFlags来指定启动模式
1.Intent.FLAG_ACTIVITY_NEW_TASK
使用一个新的任务栈来启动一个Activity,该flag通常用于在Service中启动Activity的场景,因为Service中并不存在有Activity任务栈所以通常通过这种方式来新启动一个Activity任务栈并创建新的Activity实例。

2.Intent.FLAG_ACTIVITY_SINGLE_TOP
与在manifest文件里的launchmode指定”singleTop”作用一样

3.Intent.FLAG_ACTIVITY_CLEAR_TOP
与在manifest文件里的launchmode指定”singleTask”作用一样

4.Intent.FLAG_ACTIVITY_NO_HISTORY
设置了该Flags的Activity在启动其他Activity后该Activity就消失了,不会保留在Activity栈中,此Activity可以作为一个中转Activity来负责启动其他的Activity。

Activity的常用的基础知识就这么多了,关于更加深入的话题如Activity的启动流程等会在framework学习篇开始探讨。

作者:qibanxuehua 发表于2016/9/29 16:00:44 原文链接
阅读:164 评论:0 查看评论

mybatis配置文件详解

$
0
0
配置文件的基本结构

    configuration —— 根元素 
        properties —— 定义配置外在化
        settings —— 一些全局性的配置
        typeAliases —— 为一些类定义别名
        typeHandlers —— 定义类型处理,也就是定义java类型与数据库中的数据类型之间的转换关系
        objectFactory
        plugins —— Mybatis的插件,插件可以修改Mybatis内部的运行规则
        environments —— 配置Mybatis的环境 
            environment 
                transactionManager —— 事务管理器
                dataSource —— 数据源
        databaseIdProvider
        mappers —— 指定映射文件或映射类

2.2 properties元素——定义配置外在化

<!--样例-->
<properties resource="org/mybatis/example/config.properties">
<property name="username" value="dev_user"/>
<property name="password" value="F2Fa3!33TYyg"/>
</properties>

配置外在化的属性还可以通过SqlSessionFactoryBuilder.build()方法提供,如:

SqlSessionFactory factory = sqlSessionFactoryBuilder.build(reader, props);

配置外在化的优先级是 build方法->resource属性指定的文件->property元素
2.3 settings元素——Mybatis的一些全局配置属性
设置参数    描述  有效值     默认值
cacheEnabled    这个配置使全局的映射器启用或禁用 缓存。    true | false    true
lazyLoadingEnabled  全局启用或禁用延迟加载。当禁用时, 所有关联对象都会即时加载。     true | false    true
aggressiveLazyLoading   当启用时, 有延迟加载属性的对象在被 调用时将会完全加载任意属性。否则, 每种属性将会按需要加载。   true | false    true
multipleResultSetsEnabled   允许或不允许多种结果集从一个单独 的语句中返回(需要适合的驱动)    true | false    true
useColumnLabel  使用列标签代替列名。 不同的驱动在这 方便表现不同。 参考驱动文档或充分测 试两种方法来决定所使用的驱动。   true | false    true
useGeneratedKeys    允许 JDBC 支持生成的键。 需要适合的 驱动。 如果设置为 true 则这个设置强制 生成的键被使用, 尽管一些驱动拒绝兼 容但仍然有效(比如  Derby)   true | false    False
autoMappingBehavior     指定 MyBatis 如何自动映射列到字段/ 属性。PARTIAL 只会自动映射简单, 没有嵌套的结果。FULL 会自动映射任  意复杂的结果(嵌套的或其他情况) 。     NONE, PARTIAL, FULL     PARTIAL
defaultExecutorType     配置默认的执行器。SIMPLE 执行器没 有什么特别之处。REUSE 执行器重用 预处理语句。BATCH 执行器重用语句 和批量更新  SIMPLE REUSE BATCH  SIMPLE
defaultStatementTimeout     设置超时时间, 它决定驱动等待一个数 据库响应的时间。     Any positive integer    Not Set (null)
safeRowBoundsEnabled    Allows using RowBounds on nested statements.    true | false    False
mapUnderscoreToCamelCase    Enables automatic mapping from classic database column names A_COLUMN to  camel case classic Java property names aColumn.   true | false    False
localCacheScope     MyBatis uses local cache to prevent circular references and speed up  repeated nested queries. By default (SESSION) all queries executed during a  session are cached. If localCacheScope=STATEMENT local session will be used just  for statement execution, no data will be shared between two different calls to  the same SqlSession.   SESSION | STATEMENT     SESSION
jdbcTypeForNull     Specifies the JDBC type for null values when no specific JDBC type was  provided for the parameter. Some drivers require specifying the column JDBC type  but others work with generic values like NULL, VARCHAR or OTHER.  JdbcType enumeration. Most common are: NULL, VARCHAR and OTHER  OTHER
lazyLoadTriggerMethods  Specifies which Object's methods trigger a lazy load    A method name list separated by commas  equals,clone,hashCode,toString
2.4 typeAliases元素——定义类别名,简化xml文件的配置,如:
复制代码

<typeAliases>
<typeAlias alias="Author" type="domain.blog.Author"/>
<typeAlias alias="Blog" type="domain.blog.Blog"/>
<typeAlias alias="Comment" type="domain.blog.Comment"/>
<typeAlias alias="Post" type="domain.blog.Post"/>
<typeAlias alias="Section" type="domain.blog.Section"/>
<typeAlias alias="Tag" type="domain.blog.Tag"/>
</typeAliases>

复制代码

Mybatis还内置了一些类型别名:
别名  映射的类型
_byte   byte
_long   long
_short  short
_int    int
_integer    int
_double     double
_float  float
_boolean    boolean
string  String
byte    Byte
long    Long
short   Short
int     Integer
integer     Integer
double  Double
float   Float
boolean     Boolean
date    Date
decimal     BigDecimal
bigdecimal  BigDecimal
object  Object
map     Map
hashmap     HashMap
list    List
arraylist   ArrayList
collection  Collection
iterator    Iterator
2.5 typeHandlers元素

每当MyBatis 设置参数到PreparedStatement 或者从ResultSet 结果集中取得值时,就会使用TypeHandler  来处理数据库类型与java 类型之间转换。
2.5.1 自定义typeHandlers

    实现TypeHandler接口
    View Code
    在配置文件中声明自定义的TypeHandler
    View Code

2.5.2 Mybatis内置的TypeHandler
类型处理器   Java 类型     JDBC 类型
BooleanTypeHandler  java.lang.Boolean, boolean  任何兼容的布尔值
ByteTypeHandler     java.lang.Byte, byte    任何兼容的数字或字节类型
ShortTypeHandler    java.lang.Short, short  任何兼容的数字或短整型
IntegerTypeHandler  java.lang.Integer, int  任何兼容的数字和整型
LongTypeHandler     java.lang.Long, long    任何兼容的数字或长整型
FloatTypeHandler    java.lang.Float, float  任何兼容的数字或单精度浮点型
DoubleTypeHandler   java.lang.Double, double    任何兼容的数字或双精度浮点型
BigDecimalTypeHandler   java.math.BigDecimal    任何兼容的数字或十进制小数类型
StringTypeHandler   java.lang.String    CHAR 和 VARCHAR 类型
ClobTypeHandler     java.lang.String    CLOB 和 LONGVARCHAR 类型
NStringTypeHandler  java.lang.String    NVARCHAR 和 NCHAR 类型
NClobTypeHandler    java.lang.String    NCLOB 类型
ByteArrayTypeHandler    byte[]  任何兼容的字节流类型
BlobTypeHandler     byte[]  BLOB 和 LONGVARBINARY 类型
DateTypeHandler     java.util.Date  TIMESTAMP 类型
DateOnlyTypeHandler     java.util.Date  DATE 类型
TimeOnlyTypeHandler     java.util.Date  TIME 类型
SqlTimestampTypeHandler     java.sql.Timestamp  TIMESTAMP 类型
SqlDateTypeHandler  java.sql.Date   DATE 类型
SqlTimeTypeHandler  java.sql.Time   TIME 类型
ObjectTypeHandler   Any     其他或未指定类型
EnumTypeHandler     Enumeration Type    VARCHAR-任何兼容的字符串类型, 作为代码存储(而不是索引)
EnumOrdinalTypeHandler  Enumeration Type    Any compatible NUMERIC or DOUBLE, as the position is  stored (not the code itself).
2.6 objectFactory元素——用于指定结果集对象的实例是如何创建的。

下面演示了如何自定义ObjectFactory

1.继承DefaultObjectFactory
View Code
复制代码

// ExampleObjectFactory.java
public class ExampleObjectFactory extends DefaultObjectFactory {
public Object create(Class type) {
return super.create(type);
}
public Object create(

复制代码

2.在配置文件中配置自定义的ObjectFactory
View Code

// MapperConfig.xml
<objectFactory type="org.mybatis.example.ExampleObjectFactory">
<property name="someProperty" value="100"/>
</objectFactory>

2.7 plugins元素

MyBatis 允许您在映射语句执行的某些点拦截方法调用。默认情况下,MyBatis 允许插件(plugins)拦截下面的方法:

    Executor (update, query, flushStatements, commit, rollback, getTransaction,  close, isClosed)
    ParameterHandler (getParameterObject, setParameters)
    ResultSetHandler (handleResultSets, handleOutputParameters)
    StatementHandler (prepare, parameterize, batch, update, query)

下面是自定义plugin示例:

    实现Interceptor接口,并用注解声明要拦截的方法
    复制代码

    // ExamplePlugin.java
    @Intercepts({@Signature(
    type= Executor.class,
    method = "update",
    args = {MappedStatement.class,Object.class})})
    public class ExamplePlugin implements Interceptor {
    public Object intercept(Invocation invocation) throws Throwable {
    return invocation.proceed();
    }
    public Object plugin(Object target) {
    return Plugin.wrap(target, this);
    }
    public void setProperties(Properties properties) {
    }
    }

    复制代码
    在配置文件中声明插件
    View Code

2.8 Environments元素

可以配置多个运行环境,但是每个SqlSessionFactory 实例只能选择一个运行环境。
2.8.1 Environments配置示例:
View Code
复制代码

<environments default="development">
<environment id="development">
<transactionManager type="JDBC">
<property name="..." value="..."/>
</transactionManager>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>

复制代码
2.8.2 transactionManager事务管理器

MyBatis 有两种事务管理类型(即type=”[JDBC|MANAGED]”):

    JDBC – 这个配置直接简单使用了 JDBC 的提交和回滚设置。 它依赖于从数据源得 到的连接来管理事务范围。
    MANAGED – 这个配置几乎没做什么。它从来不提交或回滚一个连接。而它会让 容器来管理事务的整个生命周期(比如 Spring 或 JEE  应用服务器的上下文) 默认 情况下它会关闭连接。 然而一些容器并不希望这样, 因此如果你需要从连接中停止 它,将 closeConnection 属性设置为  false。例如:

    <transactionManager type="MANAGED">
    <property name="closeConnection" value="false"/>
    </transactionManager>

自定义事务管理器:

    实现TranscactionFactory,它的接口定义如下:
    View Code
    实现TransactionFactory,它的接口定义如下:
    复制代码

    public interface Transaction {
    Connection getConnection();
    void commit() throws SQLException;
    void rollback() throws SQLException;
    void close() throws SQLException;
    }

    复制代码

2.8.3 dataSource数据源

dataSource 元素使用标准的JDBC 数据源接口来配置JDBC 连接对象源。

MyBatis 内置了三种数据源类型:

UNPOOLED – 这个数据源的实现是每次被请求时简单打开和关闭连接。它有一点慢,  这是对简单应用程序的一个很好的选择, 因为它不需要及时的可用连接。 不同的数据库对这 个的表现也是不一样的, 所以对某些数据库来说配置数据源并不重要,  这个配置也是闲置的。 UNPOOLED 类型的数据源仅仅用来配置以下 5 种属性:

    driver – 这是 JDBC 驱动的 Java 类的完全限定名(如果你的驱动包含,它也不是 数据源类)。
    url – 这是数据库的 JDBC URL 地址。
    username – 登录数据库的用户名。
    password – 登录数据库的密码。
    defaultTransactionIsolationLevel – 默认的连接事务隔离级别。

作为可选项,你可以传递数据库驱动的属性。要这样做,属性的前缀是以“driver.”开 头的,例如:

    driver.encoding=UTF8

这 样 就 会 传 递 以 值 “ UTF8 ” 来 传 递 属 性 “ encoding ”, 它 是 通 过  DriverManager.getConnection(url,driverProperties)方法传递给数据库驱动。

POOLED – 这是 JDBC 连接对象的数据源连接池的实现,用来避免创建新的连接实例  时必要的初始连接和认证时间。这是一种当前 Web 应用程序用来快速响应请求很流行的方 法。

除了上述(UNPOOLED)的属性之外,还有很多属性可以用来配置 POOLED 数据源:

    poolMaximumActiveConnections – 在任意时间存在的活动(也就是正在使用)连  接的数量。默认值:10
    poolMaximumIdleConnections – 任意时间存在的空闲连接数。
    poolMaximumCheckoutTime – 在被强制返回之前,池中连接被检查的时间。默认 值:20000 毫秒(也就是 20  秒)
    poolTimeToWait – 这是给连接池一个打印日志状态机会的低层次设置,还有重新 尝试获得连接, 这些情况下往往需要很长时间  为了避免连接池没有配置时静默失 败)。默认值:20000 毫秒(也就是 20 秒)
    poolPingQuery – 发送到数据的侦测查询,用来验证连接是否正常工作,并且准备 接受请求。默认是“NO PING QUERY  SET” ,这会引起许多数据库驱动连接由一 个错误信息而导致失败。
    poolPingEnabled – 这是开启或禁用侦测查询。如果开启,你必须用一个合法的 SQL 语句(最好是很快速的)设置  poolPingQuery 属性。默认值:false。
    poolPingConnectionsNotUsedFor – 这是用来配置 poolPingQuery 多次时间被用一次。  这可以被设置匹配标准的数据库连接超时时间, 来避免不必要的侦测。 默认值: 0(也就是所有连接每一时刻都被侦测-但仅仅当 poolPingEnabled 为  true 时适用)。

JNDI – 这个数据源的实现是为了使用如 Spring 或应用服务器这类的容器, 容器可以集  中或在外部配置数据源,然后放置一个 JNDI 上下文的引用。这个数据源配置只需要两个属 性:

    initial_context – 这 个 属 性 用 来 从 初 始 上 下 文 中 寻 找 环 境 ( 也 就 是  initialContext.lookup(initial——context) 。这是个可选属性,如果被忽略,那么 data_source 属性将会直接以  initialContext 为背景再次寻找。
    data_source – 这是引用数据源实例位置的上下文的路径。它会以由 initial_context  查询返回的环境为背景来查找,如果 initial_context 没有返回结果时,直接以初始 上下文为环境来查找。

和其他数据源配置相似, 它也可以通过名为 “env.” 的前缀直接向初始上下文发送属性。 比如:

    env.encoding=UTF8

在初始化之后,这就会以值“UTF8”向初始上下文的构造方法传递名为“encoding” 的属性。
 2.9 mappers元素

既然 MyBatis 的行为已经由上述元素配置完了,我们现在就要定义 SQL 映射语句了。 但是, 首先我们需要告诉 MyBatis  到哪里去找到这些语句。 Java 在这方面没有提供一个很好 的方法, 所以最佳的方式是告诉 MyBatis 到哪里去找映射文件。 你可以使用相对于类路径的  资源引用,或者字符表示,或 url 引用的完全限定名(包括 file:///URLs) 。例如:
复制代码

<!-- Using classpath relative resources -->
<mappers>
  <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
  <mapper resource="org/mybatis/builder/BlogMapper.xml"/>
  <mapper resource="org/mybatis/builder/PostMapper.xml"/>
</mappers>

复制代码
复制代码

<!-- Using url fully qualified paths -->
<mappers>
  <mapper url="file:///var/mappers/AuthorMapper.xml"/>
  <mapper url="file:///var/mappers/BlogMapper.xml"/>
  <mapper url="file:///var/mappers/PostMapper.xml"/>
</mappers>

复制代码
复制代码

<!-- Using mapper interface classes -->
<mappers>
  <mapper class="org.mybatis.builder.AuthorMapper"/>
  <mapper class="org.mybatis.builder.BlogMapper"/>
  <mapper class="org.mybatis.builder.PostMapper"/>
</mappers>

复制代码

<!-- Register all interfaces in a package as mappers -->
<mappers>
  <package name="org.mybatis.builder"/>
</mappers>
作者:sdx1237 发表于2016/9/29 16:07:54 原文链接
阅读:142 评论:0 查看评论

XMPP即时通讯协议

$
0
0

XMPP协议简介

XMPP(Extensible Messageing and Presence Protocol:可扩展消息与存在协议)是目前主流的四种IM(IM:instant messaging,即时消息)协议之一,其他三种分别为:即时信息和空间协议(IMPP)、空间和即时信息协议(PRIM)、针对即时通讯和空间平衡扩充的进程开始协议SIP(SIMPLE)。

在这四种协议中,XMPP是最灵活的。XMPP是一种基于XML的协议,它继承了在XML环境中灵活的发展性。因此,基于XMPP的应用具有超强的可扩展性。经过扩展以后的XMPP可以通过发送扩展的信息来处理用户的需求,以及在XMPP的顶端建立如内容发布系统和基于地址的服务等应用程 序。而且,XMPP包含了针对服务器端的软件协议,使之能与另一个进行通话,这使得开发者更容易建立客户应用程序或给一个配好系统添加功能。

一款基于XMPP实现的聊天社交软件:

基于开源的XMPP即时通信协议,采用C/S体系结构,通过GPRS无线网络用TCP协议连接到服务器,以架设开源的Openfn’e服务器作为即时通讯平台。

系统主要由以下部分组成:

  • 一、服务器:负责管理发出的连接或者与其他实体的会话,接收或转发XML(ExtensibleMarkup Language)流元素给授权的客户端、服务器等。

  • 二、客户终端:与服务器相连,通过XMPP获得由服务器或任何其它相关的服务所提供的全部功能。

  • 三、协议网关:完成XMPP协议传输的信息与外部消息系统可识别信息间的翻译。再就是XMPP网络。实现各个服务器、客户端间的连接。系统采用客户端(Client)/服务端(Server)架构体系结构。

客户端:

客户端基于Android平台进行开发。负责初始化通信过程,进行即时通信时,由客户端负责向服务器发起创建连接请求。系统通过GPRS无线网络与Internet网络建立连接,通过服务器实现与Android客户端的即时通信脚。

服务器端:

服务器端则采用Openfire作为服务器。允许多个客户端同时登录并且并发的连接到一个服务器上。服务器对每个客户端的连接进行认证,对认证通过的客户端创建会话,客户端与服务器端之间的通信就在该会话的上下文中进行。

服务器端设计:

androidpn服务器端是Java语言实现的,基于openfire开源工程,Web部分采用的是spring框架,这一点与openfire是不同的。Androidpn服务器包含两个部分,一个是监听特定端口上的XMPP服务,负责与客户端的XMPPConnection类进行通信,作用是用户注册和身份认证,并发送推送通知消息。另外一部分是Web服务器,采用一个轻量级的HTTP服务器,负责接收用户的Web请求。

主要的四个组成部分

  • SessionManager:负责管理客户端与服务器之间的会话
  • Auth Manager: 负责客户端用户认证管理
  • PresenceManager: 负责管理客户端用户的登录状态
  • Notification Manager: 负责实现服务器向客户端推送消息功能

系统客户端:

采用XMPP作为即时通讯协议。XMPP是基于XML,实现任意两个网络终端准实时的交换结构化信息的通信协议。采用Android平台提供的XML解析包对XML进行解析。由于应用活动都运行于主线程。故用多线程技术来解决系统通讯问题。针对通信安全问题.系统的用户信息和聊天信息在客户端存储在Android平台自身所带的SQLite数据库中,多媒体文件和图片文件存储在Android平台虚拟文件存储设备SD Card中。

通讯模块:

负责与服务器建立通讯旧。通过创建3个线程来进行处理。分别负责消息的发送、接收和心跳信息的发送;解析模块主要用来解析XML数据流。根据解析元素不同类型封装成不同的数据对象:数据模块定义整个客户端中大部分的数据类型和对象;应用模块包括即时通信、图片浏览和音乐播放。是客户端和用户交流的接口;加密模块对发送和接收的消息进行加解密。以确保通讯数据的安全。


好了,说了这么多了,那么XMPP具体是什么?它是怎么实现的?基于什么实现的?

什么是XMPP ?

  XMPP的前身是Jabber,一个开源形式组织产生的网络即时通信协议。XMPP目前被IETF国际标准组织完成了标准化工作。标准化的核心结果分为两部分; 核心的XML流传输协议 基于XML流传输的即时通讯扩展应用 XMPP的核心XML流传输协议的定义使得XMPP能够在一个比以往网络通信协议更规范的平台上。借助于XML易于解析和阅读的特性,使得XMPP的协议能够非常漂亮。 XMPP的即时通讯扩展应用部分是根据IETF在这之前对即时通讯的一个抽象定义的,与其他业已得到广泛使用的即时通讯协议,诸如AIM,QQ等有功能完整,完善等先进性。

XMPP的基本网络结构是怎样的?

  XMPP中定义了三个角色,客户端,服务器,网关。通信能够在这三者的任意两个之间双向发生。服务器同时承担了客户端信息记录,连接管理和信息的路由功能。网关承担着与异构即时通信系统的互联互通,异构系统可以包括SMS(短信),MSN,ICQ等。基本的网络形式是单客户端通过TCP/IP连接到单服务器,然后在之上传输XML。  

XMPP通过TCP传什么了?

  传输的是与即时通讯相关的指令。在以前这些命令要么用2进制的形式发送(比如QQ),要么用纯文本指令加空格加参数加换行苻的方式发送(比如MSN)。而XMPP传输的即时通讯指令的逻辑与以往相仿,只是协议的形式变成了XML格式的纯文本。这不但使得解析容易了,人也容易阅读了,方便了开发和查错。而XMPP的核心部分就是一个在网络上分片断发送XML的流协议。这个流协议是XMPP的即时通讯指令的传递基础,也是一个非常重要的可以被进一步利用的网络基础协议。所以可以说,XMPP用TCP传的是XML流。

XMPP协议工作原理:

  • 所有从一个client到另一个client的jabber的消息和数据都要通过XMPP Server。

    • 1.Client连接到Server

    • 2.Server利用本地目录系统的证书对其认证

    • 3.Client制定目标地址,让Server告知目标状态

    • 4.Server查找,连接并进行相互认证

    • 5.Client间进行交互

XMPP地址模式:

  • 统一的JID(jabber identifer)

  • JID=[node”@”] domain[“/”resource]

  • eg:cyber@cyberobject.com/res

  • domain:服务器域名

  • node:用户名

  • resource:属于用户的位置或设备

  • 一个用户可以同时以多种资源与统一XMPP服务器连接

XMPP协议的组成:

要的XMPP 协议范本及当今应用很广的XMPP 扩展:
**RFC 3920: ** XMPP核心。定义了XMPP 协议框架下应用的网络架构,引入了XML Stream(XML 流)与XML Stanza(XML 节),并规定XMPP 协议在通信过程中使用的XML 标签。使用XML 标签从根本上说是协议开放性与扩展性的需要。此外,在通信的安全方面,把TLS 安全传输机制与SASL 认证机制引入到内核,与XMPP 进行无缝的连接,为协议的安全性、可靠性奠定了基础。Core 文档还规定了错误的定义及处理、XML 的使用规范、JID(Jabber Identifier,Jabber 标识符)的定义、命名规范等等。所以这是所有基于XMPP 协议的应用都必需支持的文档。 **RFC 3921:** 用户成功登陆到服务器之后,发布更新自己的在线好友管理、发送即时聊天消息等业务。所有的这些业务都是通过三种基本的XML 节来完成的:IQ Stanza(IQ 节), Presence Stanza(Presence 节), Message Stanza(Message 节)。RFC3921 还对阻塞策略进行了定义,定义是多种阻塞方式。可以说,RFC3921 是RFC3920 的充分补充。两个文档结合起来,就形成了一个基本的即时通信协议平台,在这个平台上可以开发出各种各样的应用。 **XEP-0030:** 服务搜索。一个强大的用来测定XMPP 网络中的其它实体所支持特性的协议。 **XEP-0115:** 实体性能。XEP-0030 的一个通过即时出席的定制,可以实时改变交变广告功能。 **XEP-0045:** 多人聊天。一组定义参与和管理多用户聊天室的协议,类似于Internet 的Relay Chat,具有很高的安全性。 **XEP-0096:** 文件传输。定义了从一个XMPP 实体到另一个的文件传输。 **XEP-0124:** HTTP 绑定。将XMPP 绑定到HTTP 而不是TCP,主要用于不能够持久的维持与服务器TCP 连接的设备。 **XEP-0166:** Jingle。规定了多媒体通信协商的整体架构。 **XEP-0167:** Jingle Audio Content Description Format。定义了从一个XMPP 实体到另一个的语音传输过程。 **XEP-0176:** Jingle ICE(Interactive Connectivity Establishment)Transport。ICE传输机制,文件解决了如何让防火墙或是NAT(Network Address Translation)保护下的实体建立连接的问题。 **XEP-0177:** Jingle Raw UDP Transport。纯UDP 传输机制,文件讲述了如何在没有防火墙且在同一网络下建立连接的。 **XEP-0180:** Jingle Video Content Description Format。定义了从一个XMPP 实体到另一个的视频传输过程。 **XEP-0181:** Jingle DTMF(Dual Tone Multi-Frequency)。 **XEP-0183:** Jingle Telepathy Transport Method。

XMPP协议网络架构 XMPP是一个典型的C/S架构,而不是像大多数即时通讯软件一样,使用P2P客户端到客户端的架构,也就是说在大多数情况下,当两个客户端进行通讯时,他们的消息都是通过服务器传递的(也有例外,例如在两个客户端传输文件时).采用这种架构,主要是为了简化客户端,将大多数工作放在服务器端进行,这样,客户端的工作就比较简单,而且,当增加功能时,多数是在服务器端进行.XMPP服务的框架结构如下图所示.XMPP中定义了三个角色,**XMPP客户端**,**XMPP服务器**、**网关**.通信能够在这三者的任意两个之间双向发生.服务器同时承担了客户端信息记录、连接管理和信息的路由功能.网关承担着与异构即时通信系统的互联互通,异构系统可以包括*SMS(短信)*、*MSN*、*ICQ*等.基本的网络形式是单客户端通过TCP/IP连接到单服务器,然后在之上传输XML,工作原理是:
  • (1)节点连接到服务器;
  • (2)服务器利用本地目录系统中的证书对其认证;
  • (3)节点指定目标地址,让服务器告知目标状态;
  • (4)服务器查找、连接并进行相互认证;
  • (5)节点之间进行交互.

XMPP客户端: XMPP 系统的一个设计标准是必须支持简单的客户端。事实上,XMPP 系统架构对客户端只有很少的几个限制。一个XMPP 客户端必须支持的功能有:
  1. 通过 TCP 套接字与XMPP 服务器进行通信;

  2. 解析组织好的 XML 信息包;

  3. 理解消息数据类型。

XMPP 将复杂性从客户端转移到服务器端。这使得客户端编写变得非常容易,更新系统功能也同样变得容易。XMPP 客户端与服务端通过XML 在TCP 套接字的5222 端口进行通信,而不需要客户端之间直接进行通信。 **基本的XMPP 客户端**必须实现以下标准协议(XEP-0211):
  • RFC3920 核心协议Core

  • RFC3921 即时消息和出席协议Instant Messaging and Presence

  • XEP-0030 服务发现Service Discovery

  • XEP-0115 实体能力Entity Capabilities

XMPP服务器: XMPP 服务器遵循两个主要法则:
  1. 监听客户端连接,并直接与客户端应用程序通信;

  2. 与其他 XMPP 服务器通信;

XMPP开源服务器一般被设计成模块化,由各个不同的代码包构成,这些代码包分别处理Session管理、用户和服务器之间的通信、服务器之间的通信、DNS(Domain Name System)转换、存储用户的个人信息和朋友名单、保留用户在下线时收到的信息、用户注册、用户的身份和权限认证、根据用户的要求过滤信息和系统记录等。另外,服务器可以通过附加服务来进行扩展,如完整的安全策略,允许服务器组件的连接或客户端选择,通向其他消息系统的网关。 **基本的XMPP 服务器**必须实现以下标准协议 RFC3920 核心协议Core RFC3921 即时消息和出席协议Instant Messaging and Presence XEP-0030 服务发现Service Discovery

XMPP网关 XMPP 突出的特点是***可以和其他即时通信系统交换信息和用户在线状况***。由于协议不同,XMPP 和其他系统交换信息必须通过协议的转换来实现,目前几种主流即时通信协议都没有公开,所以XMPP 服务器本身并没有实现和其他协议的转换,但它的架构允许转换的实现。实现这个特殊功能的服务端在XMPP 架构里叫做网关(gateway)。由于网关的存在,XMPP 架构事实上兼容所有其他即时通信网络,这无疑大大提高了XMPP 的灵活性和可扩展性。目前,XMPP 实现了和AIM、ICQ、IRC、MSN Massager、RSS0.9 和Yahoo Massager 的协议转换。

XMPP消息格式:
  • Message

  • Presence

  • IQ

**:** 用于在两个jabber用户之间发送信息。Jsm(jabber会话管理器)负责满足所有的消息,不管目标用户的状态如何。如果用户在线jsm立即提交;否则jsm就存储。
  • To :标识消息的接收方。

  • from : 指发送方的名字或标示(id)o

  • Text: 此元素包含了要提交给目标用户的信息。

eg:

<message
    to= 'lily@jabber.org/contact' 
    type ='chat'>
    <body>hello world!</body>
</message>
**:** 用来表明用户的状态,如:online、away、dnd(请勿打扰)等。当用户离线或改变自己的状态时,就会在stream的上下文中插入一个Presence元素,来表明自身的状态.结构如下所示:
  • Probe :用于向接受消息方法发送特殊的请求

  • subscribe:当接受方状态改变时,自动向发送方发送presence信息。

eg:
<presence>
    From ='lily @ jabber.com/contact'
    To = 'yaoman @ jabber.com/contact'>
    <status> Online </status>
</presence>
**** 一种请求/响应机制,从一个实体从发送请求,另外一个实体接受请求,并进行响应.例如,client在stream的上下文中插入一个元素,向Server请求得到自己的好友列表,Server返回一个,里面是请求的结果. 主要的属性是type。包括:
  • Get :获取当前域值。

  • Set :设置或替换get查询的值。

  • Result :说明成功的响应了先前的查询。

  • Error: 查询和响应中出现的错误。

eg:
<iq 
    from ='lily@jabber.com/contact'
    id='15701323360'
    Type=’result’
    />

XMPP应用示例

客户端:

<?xml version='1.0'?>
  <stream:stream
  to='example_com'
  xmlns='jabber:client'
  xmlns:stream='http_etherx_jabber_org/streams'
  version='1.0'>

服务器:

<?xml version='1.0'?>
  <stream:stream
  from='example_com'
  id='someid'
  xmlns='jabber:client'
  xmlns:stream='http_etherx_jabber_org/streams'
  version='1.0'>

 …其他通信…

客户端:

<message
    from='juliet_example_com'
    to='romeo_example_net'
    xml:lang='en'/>

客户端:

<body>
    Art thou not Romeo, and a Montague?
</body>

客户端:

</message>

服务器:

<message
    from='romeo_example_net'
    to='juliet_example_com'
    xml:lang='en'/>

服务器:

<body>
    Neither, fair saint, if either thee dislike.
</body>

服务器:

</message>

客户端:

</stream:stream>

服务器:

</stream:stream>

eg:

以文档的观点来看,客户端或服务器发送的所有XML文本连缀在一起,从<stream>到</stream>构成了一个完整的XML文档。其中的stream标签就是所谓的XML Stream。在<stream>与</stream>中间的那些<message>...</message>这样的XML元素就是所谓的XML Stanza(XML节)。XMPP核心协议通信的基本模式就是先建立一个stream,然后协商一堆安全之类的东西,中间通信过程就是客户端发送XML Stanza,一个接一个的。服务器根据客户端发送的信息以及程序的逻辑,发送XML Stanza给客户端。但是这个过程并不是一问一答的,任何时候都有可能从一方发信给另外一方。通信的最后阶段是</stream>关闭流,关闭TCP/IP连接。 

XMPP系统特点:

  • 1)客户机/服务器通信模式;

  • (2)分布式网络;

  • (3)简单的客户端;

  • (4)XML的数据格式。

通俗解释:

其实XMPP 是一种很类似于http协议的一种数据传输协议,它的过程就如同“解包装–〉包装”的过程,用户只需要明白它接受的类型,并理解它返回的类型,就可以很好的利用xmpp来进行数据通讯。

作者:ideal_Utopia 发表于2016/9/29 16:19:36 原文链接
阅读:153 评论:0 查看评论

自定义LinearLayout实现淘宝详情页

$
0
0

1.简单说明

  1. 淘宝详情页就不用我一一介绍了,昨天逛淘宝看到这个效果时,让我想起了去年刚学习Android只会使用现成的时候,当时在网上找了一个这种效果的使用了,并不懂怎么实现的。现在就看到一种效果就想自己实现一下,我想这就是刚接触某个知识时的好奇心吧

  2. 说走咱就走啊,本文只是介绍一种实现思路,网上也已经有了很多种实现方式,有问题请指正

  3. 效果图(我有很用心的找美女图的)
    这里写图片描述

2.实现思路

  1. 继承LinearLayout,设置方向为垂直
  2. 控件中有两个ScrollView,至于为什么要使用ScrollView,主要是因为内容超过一页时省去自己处理滑动
  3. 关键是事件分发处理。监听两个ScrollView的滑动事件,当第一页滑动到底部时,再向上拖动时,拦截事件,判断距离,超过设定值时,滑动到第二页,否则回弹;同理,当第二页滑动到顶部时,再向下拖动时,拦截事件,判断距离,超过设定值时,滑动到第一页,否则回弹(还有很多细节需要结合代码讲解)
  4. 关于回弹和滑动换页使用的是Scroller,对于Scroller的使用,本文不做过多解释

3.实现

3.1重写ScrollView

根据实现思路,我们需要监听ScrollView是否滑动到顶部和底部,但是ScrollView的setOnScrollChangeListener()方法在api23才添加。主要是重写ScrollViewonScrollChanged(int l, int t, int oldl, int oldt)方法。

l:当前水平方向滚动值,和getScrollX()相等
t:当前竖直方向滚动值,和getScrollY()相等
oldl:上一次水平滚动值
oldt:上一次竖直滚动值

  • 监听接口:
    public interface OnScrollEndListener {
        void scrollToBottom(View view);
        void scrollToTop(View view);
        void scrollToMiddle(View view);
    }
  • onScrollChanged方法
 @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);
        if(t == 0){
            if (mOnScrollBottomListener != null) {
                mOnScrollBottomListener.scrollToTop(this);
            }
        } else if(t + getMeasuredHeight() >=  getChildAt(0).getMeasuredHeight()){
            if (mOnScrollBottomListener != null) {
                mOnScrollBottomListener.scrollToBottom(this);
            }
        } else {
            if (mOnScrollBottomListener != null) {
                mOnScrollBottomListener.scrollToMiddle(this);
            }
        }
    }

3.2重写onMeasure方法、page的获取与设置

  • 显示调用第二个自孩子的测量方法,不然尺寸有可能为0
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        /**
         * 显示调用第二个自孩子的测量方法,不然尺寸有可能为0
         */
        View child2 = getChildAt(1);
        if (child2 != null) {
            child2.measure(widthMeasureSpec, heightMeasureSpec);
        }
    }
  • 在onFinishInflate中初始化两个页面
@Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        if(getChildCount() == 2){
            View child1 = getChildAt(0);
            if (child1 instanceof ScrollEndScrollView){
                scrollView1 = (ScrollEndScrollView) child1;
            }
            View child2 = getChildAt(1);
            if(child2 instanceof ScrollEndScrollView){
                scrollView2 = (ScrollEndScrollView) child2;
            }
        }

        initEvent();
    }
  • 为两个页面设置滑动监听
private ScrollEndScrollView.OnScrollEndListener scrollEndListener = new ScrollEndScrollView.OnScrollEndListener() {
        @Override
        public void scrollToBottom(View view) {
            if(view == scrollView1){
                isToBotttom = true;
            }
        }

        @Override
        public void scrollToTop(View view) {
            if(view == scrollView2){
                isToTop = true;
            }
        }

        @Override
        public void scrollToMiddle(View view) {
            if(view == scrollView1){
                isToBotttom = false;
            }
            if(view == scrollView2){
                isToTop = false;
            }
        }
    };

3.3Scroller使用的几步

Scroller的英文解释是:
This class encapsulates scrolling. You can use scrollers (Scroller or OverScroller) to collect the data you need to produce a scrolling animation—for example, in response to a fling gesture. Scrollers track scroll offsets for you over time, but they don’t automatically apply those positions to your view. It’s your responsibility to get and apply new coordinates at a rate that will make the scrolling animation look smooth.

此类封装滚动。您可以使用滚动条(滚轮或OverScroller)收集你需要制作一个滚动的动画,例如,响应一扔手势的数据。滚动条为您跟踪滚动偏移量随着时间的推移,但他们不会自动将新的位置设置到View中。你的任务是获取并使用一个合适的速度,使滚动动画看起来更平滑。

简而言之,有关滑动的你都可以使用这个实现。

  • 需要重写的方法
@Override
    public void computeScroll() {
        super.computeScroll();
        //先判断mScroller滚动是否完成
        if (mScroller.computeScrollOffset()) {
            //这里调用View的scrollTo()完成实际的滚动
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            //必须调用该方法,否则不一定能看到滚动效果
            postInvalidate();
        }
    }
  • 辅助方法
    //调用此方法设置滚动的相对偏移
    public void smoothScrollBy(int dx, int dy) {
        //设置mScroller的滚动偏移量
        mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), dx, dy, Math.max(300, Math.abs(dy)));
        invalidate();//这里必须调用invalidate()才能保证computeScroll()会被调用,否则不一定会刷新界面,看不到滚动效果
    }
//调用此方法滚动到目标位置
    public void smoothScrollTo(int fx, int fy) {
        int dx = fx - mScroller.getFinalX();
        int dy = fy - mScroller.getFinalY();
        smoothScrollBy(dx, dy);
    }

3.4事件分发

最关键的部分,逻辑稍复杂,细节处理较多。这里重写dispatchTouchEvent。

显示第一页时

  • 未滑动到底部,事件由scrollView1自己处理
  • 滑动到底部时,如果继续向上拖动,拦截事件,父控件处理滑动;继续向下拖动时,如果父控件(即该控件)当前滚动最后位置(mScroller.getFinalY())不为0, 如果父控件继续滚动不会出现负值时(出现负值时会导致头部空白,因为这时是父控件控制,scrollView1不可滑动),不拦截事件,父控件处理滑动,否则,强制滑动到0位置,并把事件下发给子控件

显示第二页时

  • 未滑动到最顶部时,事件由scrollView2自己处理
  • 滑动到顶部时,如果继续向下拖动,拦截事件,父控件处理滑动;继续向上拖动时,如果父控件当前滚动位置小于第一页高度,拦截事件,父控件处理滑动,否则,滑动到第二页起始位置,并把事件下发给子控件

  • ACTION_MOVE中进行事件分发,ACTION_UP中进行切换页面、回弹
  • 关于使用scroller滑动,实现弹性效果,简单实现请看这里简单的弹性实现代码

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        int action = ev.getAction();
        int yPosition = (int) ev.getY();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mScroller.abortAnimation();
                mLastY = yPosition;
                mMoveY = 0;
                break;
            case MotionEvent.ACTION_MOVE:
                mMoveY = (mLastY - yPosition);
                mLastY = yPosition;
                if(isToBotttom){
                    if(mMoveY > 0){
                        //向上
                        smoothScrollBy(0, mMoveY);
                        return true;
                    } else {
                        //向下
                        if(mScroller.getFinalY() != 0){
                            //这是出于第一页和第二页显示连接处
                            if(getScrollY() + mMoveY > 0){
                                smoothScrollBy(0, mMoveY);
                                return true;
                            } else{
                                smoothScrollTo(0, 0);
                                return super.dispatchTouchEvent(ev);
                            }
                        }
                    }
                }
                else if(isToTop){
                    if(mMoveY < 0){
                        //向下
                        smoothScrollBy(0, mMoveY);
                        return true;
                    } else {
                        //向上
                        if(mScroller.getFinalY() < scrollView1.getHeight()){
                            //这是出于第一页和第二页显示连接处
                            smoothScrollBy(0, mMoveY);
                            return true;
                        } else {
                            smoothScrollTo(0, scrollView1.getHeight());
                            return super.dispatchTouchEvent(ev);
                        }
                    }
                }

                //处理快速滑动时两页覆盖问题
                if(pageIndex == 0){
                    smoothScrollTo(0, 0);
                } else if(pageIndex == 1){
                    smoothScrollTo(0, scrollView1.getHeight());
                }

                break;

            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                if(isToBotttom){
                   if(Math.abs(getScrollY()) > TO_NEXT_PAGE_HEIGHT){
                        //移动到第二页
                       pageIndex = 1;
                       smoothScrollTo(0, scrollView1.getHeight());
                       isToBotttom = false;
                       isToTop = true;
                    } else {
                        //回弹
                        smoothScrollBy(0, -mScroller.getFinalY());
                    }
                } else if(isToTop){
                    if(scrollView1.getHeight() - getScrollY() > TO_NEXT_PAGE_HEIGHT){
                        //移动到第一页
                        pageIndex = 0;
                        smoothScrollTo(0, 0);
                        isToBotttom = true;
                        isToTop = false;
                    } else {
                        //回弹
                        smoothScrollTo(0, scrollView1.getHeight());
                    }
                }

                break;
            default:
                break;
        }

        return super.dispatchTouchEvent(ev);
    }

4.总结

实现该控件,需要掌握的知识点主要是自定义控件的基本步骤、Scroller的基本使用和事件分发,当然这里最关键的处理还是事件分发。开头也说了,虽然这个有很多人实现过了,但还是想用自己的方式实现一遍。大笑三声,哈哈哈,又实现一个自定义控件…博主还在自定义控件学习阶段,请谨慎使用该控件到项目中。

5.下载

https://github.com/LineChen/TwoPageLayout

作者:u011102153 发表于2016/9/29 16:36:55 原文链接
阅读:172 评论:0 查看评论

自定义控件 编辑和选取圆形头像

$
0
0
android大家都有很多需要用户上传头像的需求,有的是选方形,有的是圆角矩形,有的是圆形。
首先我们要做一个处理图片的自定义控件,把传入的图片,经过用户选择区域,处理成一定的形状。

有的app是通过在图片上画一个矩形区域表示选中的内容,有的则是通过双指放大缩小,拖动图片来选取图片。圆形头像,还是改变图片比较好

这里写图片描述

圆形区域可调节大小。

这个自定义View的图像部分分为三个,背景图片,半透明蒙层,和亮色区域……还是直接贴代码得了

package com.example.jjj.widget;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff.Mode;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;

public class RoundEditImageView extends View {
    private Bitmap bitmap;
    RectF clipBounds, dst, src;
    Paint clearPaint;

    // 控件宽高
    private int w, h;
    // 选区半径
    private float radius = 150f;

    // 圆形选区的边界
    private RectF circleBounds;

    // 最大放大倍数
    private float max_scale = 2.0f;

    // 双指的距离
    private float distance;
    private float x0, y0;
    private boolean doublePoint;

    public RoundEditImageView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    public RoundEditImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

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

    private void init() {
        clearPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        clearPaint.setColor(Color.GRAY);
        clearPaint.setXfermode(new PorterDuffXfermode(Mode.CLEAR));
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (bitmap != null) {
            canvas.drawBitmap(bitmap, null, dst, null);//每次invalidate通过改变dst达到缩放平移的目的
        }
        // 保存图层,以免清除时清除了bitmap
        int count = canvas.saveLayer(clipBounds, null, Canvas.ALL_SAVE_FLAG);
        canvas.drawColor(0x80000000);
        canvas.drawCircle(w / 2, h / 2, radius, clearPaint);// 清除半透明黑色,留下一个透明圆
        canvas.restoreToCount(count);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        this.w = w;
        this.h = h;
        // 记录view所占的矩形
        clipBounds = new RectF(0, 0, w, h);
        float l, r, t, b;
        if (bitmap != null) {
            // 判断长宽比,当长宽比太长会太宽是,以fitCenter的方式初始化图片
            if (w / (float) h > bitmap.getWidth() / (float) bitmap.getHeight()) {
                // 图片太高
                float w_ = h * bitmap.getWidth() / (float) bitmap.getHeight();
                l = w / 2f - w_ / 2f;
                r = w / 2f + w_ / 2f;
                t = 0;
                b = h;
            } else {
                // 图片太长,或跟view一样长
                float h_ = w * bitmap.getHeight() / (float) bitmap.getWidth();
                l = 0;
                r = w;
                t = h / 2f - h_ / 2f;
                b = h / 2f + h_ / 2f;
            }
            dst = new RectF(l, t, r, b);// 这个矩形用来变换
            src = new RectF(l, t, r, b);// 这个矩形仅为保存第一次的状态

            max_scale = Math.max(max_scale, bitmap.getWidth() / src.width());
        }
        circleBounds = new RectF(w / 2 - radius, h / 2 - radius, w / 2 + radius, h / 2 + radius);
    }

    public void setImageBitmap(Bitmap bitmap) {
        this.bitmap = bitmap;
        invalidate();
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent e) {
        if (e.getPointerCount() > 2) {// 不接受多于两指的事件
            return false;
        } else if (e.getPointerCount() == 2) {
            doublePoint = true;// 标志位,记录两指事件处理后,抬起一只手也不处理拖动
            handleDoubleMove(e);
        } else {
            // 处理单指的拖动
            switch (e.getAction()) {
            case MotionEvent.ACTION_DOWN:
                x0 = e.getX();
                y0 = e.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                if (doublePoint) {
                    break;
                }
                float x = e.getX();
                float y = e.getY();
                float w = dst.width();
                float h = dst.height();

                // 不允许拖过圆形区域,不能使圆形区域内空白
                dst.left += x - x0;
                if (dst.left > circleBounds.left) {
                    dst.left = circleBounds.left;
                } else if (dst.left < circleBounds.right - w) {
                    dst.left = circleBounds.right - w;
                }
                dst.right = dst.left + w;

                // 不允许拖过圆形区域,不能使圆形区域内空白
                dst.top += y - y0;
                if (dst.top > circleBounds.top) {
                    dst.top = circleBounds.top;
                } else if (dst.top < circleBounds.bottom - h) {
                    dst.top = circleBounds.bottom - h;
                }
                dst.bottom = dst.top + h;

                x0 = x;
                y0 = y;
                invalidate();
                break;
            case MotionEvent.ACTION_UP:
                doublePoint = false;// 恢复标志位
                break;
            }
        }

        return true;
    }

    // 处理双指事件
    private void handleDoubleMove(MotionEvent e) {
        switch (e.getAction() & MotionEvent.ACTION_MASK) {
        case MotionEvent.ACTION_POINTER_DOWN:
            distance = sqrt(e);
            Log.d("px", "down:distance=" + distance);
            break;
        case MotionEvent.ACTION_MOVE:
            scale(e);
            break;
        case MotionEvent.ACTION_UP:
            break;
        default:
            break;
        }
    }

    private void scale(MotionEvent e) {
        float dis = sqrt(e);
        // 以双指中心作为图片缩放的支点
        float pX = e.getX(0) / 2f + e.getX(1) / 2f;
        float pY = e.getY(0) / 2f + e.getY(1) / 2f;
        float scale = dis / distance;
        Log.d("px", "move:distance=" + dis + ",scale to" + scale);
        float w = dst.width();
        float h = dst.height();

        if (w * scale < radius * 2 || h * scale < radius * 2 || w * scale > src.width() * max_scale) {
            // 无法缩小到比选区还小,或到达最大倍数
            return;
        }
        // 把dst区域放大scale倍
        dst.left = (dst.left - pX) * scale + pX;
        dst.right = (dst.right - pX) * scale + pX;
        dst.top = (dst.top - pY) * scale + pY;
        dst.bottom = (dst.bottom - pY) * scale + pY;

        // 缩放同样不允许使圆形区域空白
        if (dst.left > circleBounds.left) {
            dst.left = circleBounds.left;
            dst.right = dst.left + w * scale;
        } else if (dst.right < circleBounds.right) {
            dst.right = circleBounds.right;
            dst.left = dst.right - w * scale;
        }

        if (dst.top > circleBounds.top) {
            dst.top = circleBounds.top;
            dst.bottom = dst.top + h * scale;
        } else if (dst.bottom < circleBounds.bottom) {
            dst.bottom = circleBounds.bottom;
            dst.top = dst.bottom - h * scale;
        }
        invalidate();

        distance = dis;
    }

    private float sqrt(MotionEvent e) {
        return (float) Math.sqrt((e.getX(0) - e.getX(1)) * (e.getX(0) - e.getX(1)) + (e.getY(0) - e.getY(1)) * (e.getY(0) - e.getY(1)));
    }

    // 生成目前选区指定大小的圆形bitmap
    public Bitmap extractBitmap(int width) {
        Bitmap outBitmap = Bitmap.createBitmap(width, width, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(outBitmap);
        Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
        p.setColor(Color.GRAY);
        canvas.drawCircle(width / 2, width / 2, width / 2, p);
        float scale = dst.width() / bitmap.getWidth();
        int w = (int) (circleBounds.width() / scale);
        int l = (int) ((circleBounds.left - dst.left) / scale);
        int r = l + w;
        int t = (int) ((circleBounds.top - dst.top) / scale);
        int b = t + w;
        Rect resRect = new Rect(l, t, r, b);
        Paint paint = new Paint();
        paint.setXfermode(new PorterDuffXfermode(Mode.SRC_IN));
        canvas.drawBitmap(bitmap, resRect, canvas.getClipBounds(), paint);
        return outBitmap;
    }
}

Activity中用法

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_select_header);
        RoundEditImageView imageView = (RoundEditImageView) findViewById(R.id.roundEditImageView1);
        bitmap = BitmapFactory.decodeFile(imagePath);
        imageView.setImageBitmap(bitmap);
    }

    @Override
    public void onClick(View v) {
            //生成一个300*300的当前亮圆形中的图片
            Bitmap result = imageView.extractBitmap(300);
            //压缩成png
            FileOutputStream out = new FileOutputStream(new File(filePath));
            result.compress(Bitmap.CompressFormat.PNG, 100, out);
            //上传与显示
            ...
    }

需求是模仿QQ的,本人不太会讲解,做着玩玩。

作者:u013147734 发表于2016/9/29 16:46:52 原文链接
阅读:410 评论:0 查看评论

Android 5.0中的CoordinatorLayout使用技巧

$
0
0

CoordinatorLayout 实现了多种Material Design中提到的滚动效果。目前这个框架提供了几种不用写动画代码就能工作的方法,这些效果包括:
*让浮动操作按钮上下滑动,为Snackbar留出空间。
*扩展或者缩小Toolbar或者头部,让主内容区域有更多的空间。
*控制哪个view应该扩展还是收缩,以及其显示大小比例,包括视差滚动效果动画。

浮动操作按钮与Snackbar

CoordinatorLayout可以用来配合浮动操作按钮的 layout_anchor 和 layout_gravity属性创造出浮动效果。
当Snackbar在显示的时候,往往出现在屏幕的底部。为了给Snackbar留出空间,浮动操作按钮需要向上移动。

这里写图片描述

只要使用CoordinatorLayout作为基本布局,将自动产生向上移动的动画。浮动操作按钮有一个 默认的 behavior来检测Snackbar的添加并让按钮在Snackbar之上呈现上移与Snackbar等高的动画。

<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/rvToDoList"
        android:layout_width="match_parent"
        android:background="#9d9d9d"
        android:layout_height="match_parent"/>
    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="end|bottom"
        android:layout_margin="16dp"
        app:layout_anchor="@id/rvToDoList"
        app:layout_anchorGravity="bottom|right|end"/>
    <!--app:layout_anchor:意思是FAB浮动按钮显示在哪个布局区域。且设置当前锚点的位置
    app:layout_anchorGravity:意思FAB浮动按钮在这个布局区域的具体位置。两个属性共同作用才是的FAB 浮动按钮也能折叠消失,出现。
    -->

</android.support.design.widget.CoordinatorLayout>
 protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        findViewById(R.id.fab).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Snackbar.make(v, "test", Snackbar.LENGTH_LONG)
                        .setAction("Cancel", new View.OnClickListener() {
                            @Override
                            public void onClick(View v) {
                                //这里的单击事件代表点击消除Action后的响应事件
                                Toast.makeText(MainActivity.this,"asdasd",Toast.LENGTH_SHORT).show();
                            }
                        }).show();
            }
        });
    }

Toolbar的扩展与收缩

首先需要确保你不是使用已经过时的ActionBar,使用ToolBar作为actionbar。同样,这里也需要CoordinatorLayout作为主布局容器。
我们必须使用一个容器布局: AppBarLayout 来让Toolbar响应滚动事件。响应滚动事件

<android.support.design.widget.AppBarLayout
        android:id="@+id/appbar"
        android:layout_width="match_parent"
        android:layout_height="@dimen/detail_backdrop_height"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
        android:fitsSystemWindows="true">

  <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />

 </android.support.design.widget.AppBarLayout>

然后,我们需要定义AppBarLayout与滚动视图之间的联系。在RecyclerView或者任意支持嵌套滚动的view比如NestedScrollView上添加app:layout_behavior。support library包含了一个特殊的字符串资源@string/appbar_scrolling_view_behavior,它和AppBarLayout.ScrollingViewBehavior相匹配,用来通知AppBarLayout 这个特殊的view何时发生了滚动事件,这个behavior需要设置在触发事件(滚动)的view之上。注意:根据官方的谷歌文档,AppBarLayout目前必须是第一个嵌套在CoordinatorLayout里面的子view。

<android.support.v7.widget.RecyclerView
        android:id="@+id/rvToDoList"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

AppBarLayout里面定义的view只要设置了app:layout_scrollFlags属性,就可以在RecyclerView滚动事件发生的时候被触发:当CoordinatorLayout发现RecyclerView中定义了这个属性,它会搜索自己所包含的其他view,看看是否有view与这个behavior相关联。AppBarLayout.ScrollingViewBehavior描述了RecyclerView与AppBarLayout之间的依赖关系。RecyclerView的任意滚动事件都将触发AppBarLayout或者AppBarLayout里面view的改变。

<android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:fitsSystemWindows="true"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">

            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                app:layout_scrollFlags="scroll|enterAlways"/>

 </android.support.design.widget.AppBarLayout>

app:layout_scrollFlags是一个非常重要的属性,它里边的取值主要有五种,下面我分别来解释:
1.scroll 表示CollapsingToolbarLayout可以滚动(不设置的话头部的ImageView将不能折叠),如果想将所有的View能滚动出屏幕,必须设置个flag,若没有设置flag的View,则会被固定在屏幕顶部
2.enterAlways 表示底部的滚动控件只要向下滚动,头部就显示出来,设置这个flag时,向下的滚动都会导致该view变为可见。当ScrollView往下滚动时,该View会直接往下滚动。而不用考虑ScrollView是否在滚动。
3.enterAlwaysCollapsed 表示当底部滚动控件滚动见顶时,头部显示出来,在ScrollView往上滑动时,首先是View把滑动事件“夺走”,由View去执行滑动,直到滑动最小高度后,把这个滑动事件“还”回去,让ScrollView内部去上滑。
4.exitUntilCollapsed 表示头部折叠到最小高度时(Toolbar的高度),就不再折叠,是enterAlways的附加选项,一般跟enterAlways一起使用,它是指,View在往下“出现”的时候,首先是enterAlways效果,当View的高度达到最小高度时,View就暂时不去往下滚动,直到ScrollView滑动到顶部不再滑动时,View再继续往下滑动,直到滑到View的顶部结束。
5.snap 表示在滑动过程中如果停止滑动,则头部会就近折叠(要么恢复原状,要么折叠成一个Toolbar)
记住,要把带有scroll flag的view放在前面,这样收回的view才能让正常退出,而固定的view继续留在顶部。
这里写图片描述

制造折叠效果

如果想制造toolbar的折叠效果,我们必须把Toolbar放在CollapsingToolbarLayout中:

<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/main_content"
    tools:context="wxt.coordinatorlayout.AppBarLayout">
    <android.support.design.widget.AppBarLayout
        android:id="@+id/appbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        >
        <android.support.design.widget.CollapsingToolbarLayout
            android:id="@+id/collapsing_toolbar"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:fitsSystemWindows="true"
            app:contentScrim="?attr/colorPrimary"
            app:expandedTitleMarginEnd="64dp"
            app:expandedTitleMarginStart="48dp"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">

            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                app:layout_scrollFlags="scroll|enterAlways"></android.support.v7.widget.Toolbar>
        </android.support.design.widget.CollapsingToolbarLayout>
    </android.support.design.widget.AppBarLayout>
    <android.support.v4.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="aaaa\naaaa\naaaa\naaaa\naaaa\naaaa\naaaa\naaaa\naaaa\naaaa\naaaa\naaaa\naaaa\naaaa\naaaa\naaaa\naaaa\naaaa\naaaa\naaaa\naaaa\naaaa\naaaa\naaaa\naaaa\naaaa\naaaa\naaaa\naaaa\naaaa\naaaa\naaaa\naaaa\naaaa\naaaa\naaaa\naaaa\naaaa\naaaa\naaaa\naaaa\naaaa\naaaa\naaaa\naaaa\n" />
    </android.support.v4.widget.NestedScrollView>

现在效果就成了:

这里写图片描述

通常,我们我们都是设置Toolbar的title,而现在,我们需要把title设置在CollapsingToolBarLayout上,而不是Toolbar。

CollapsingToolbarLayout collapsingToolbar =(CollapsingToolbarLayout) findViewById(R.id.collapsing_toolbar);
collapsingToolbar.setTitle("Title");

制造视差效果CollapsingToolbarLayout

CollapsingToolbarLayout还能让我们做出更高级的动画,比如在里面放一个ImageView,然后在它折叠的时候渐渐淡出。同时在用户滚动的时候title的高度也会随着改变。

这里写图片描述

CollapsingToolbarLayout作用是提供了一个可以折叠的Toolbar,它继承至FrameLayout,给它设置layout_scrollFlags,它可以控制包含在CollapsingToolbarLayout中的控件(如:ImageView、Toolbar)在响应layout_behavior事件时作出相应的scrollFlags滚动事件(移除屏幕或固定在屏幕顶端)。

使用CollapsingToolbarLayout:

<android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="256dp"
        android:fitsSystemWindows="true">
        <android.support.design.widget.CollapsingToolbarLayout
            android:id="@+id/collapsing_toolbar_layout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:contentScrim="#30469b"
            app:expandedTitleMarginStart="48dp"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">
            <ImageView
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:scaleType="centerCrop"
                android:src="@mipmap/bg"
                app:layout_collapseMode="parallax"
                app:layout_collapseParallaxMultiplier="0.7"  />
            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                app:layout_collapseMode="pin" />
        </android.support.design.widget.CollapsingToolbarLayout>
    </android.support.design.widget.AppBarLayout>

我们在CollapsingToolbarLayout中设置了一个ImageView和一个Toolbar。并把这个CollapsingToolbarLayout放到AppBarLayout中作为一个整体。

1、在CollapsingToolbarLayout中:

其中还设置了一些属性,简要说明一下:

contentScrim - 设置当完全CollapsingToolbarLayout折叠(收缩)后的背景颜色。

expandedTitleMarginStart - 设置扩张时候(还没有收缩时)title向左填充的距离。

2、在ImageView控件中:

我们设置了:

layout_collapseMode (折叠模式) - 有两个值:

pin - 设置为这个模式时,当CollapsingToolbarLayout完全收缩后,Toolbar还可以保留在屏幕上。

parallax - 设置为这个模式时,在内容滚动时,CollapsingToolbarLayout中的View(比如ImageView)也可以同时滚动,实现视差滚动效果,通常和layout_collapseParallaxMultiplier(设置视差因子)搭配使用。

layout_collapseParallaxMultiplier(视差因子) - 设置视差滚动因子,值为:0~1。

3、在Toolbar控件中:

我们设置了layout_collapseMode(折叠模式):为pin。

综上分析:当设置了layout_behavior的控件响应起了CollapsingToolbarLayout中的layout_scrollFlags事件时,ImageView会有视差效果的向上滚动移除屏幕,当开始折叠时CollapsingToolbarLayout的背景色(也就是Toolbar的背景色)就会变为我们设置好的背景色,Toolbar也一直会固定在最顶端。
【注】:使用CollapsingToolbarLayout时必须把title设置到CollapsingToolbarLayout上,设置到Toolbar上不会显示。即:

mCollapsingToolbarLayout.setTitle(” “);

该变title的字体颜色:

扩张时候的title颜色:mCollapsingToolbarLayout.setExpandedTitleColor();

收缩后在Toolbar上显示时的title的颜色:mCollapsingToolbarLayout.setCollapsedTitleTextColor();

这个颜色的过度变化其实CollapsingToolbarLayout已经帮我们做好,它会自动的过度,比如我们把收缩后的title颜色设为绿色。

CoordinatorLayout与CollapsingToolbarLayout:
CollapsingToolbarLayout的作用提供了一个可以折叠的Toolbar,对Toolbar进行再次包装的ViewGroup,继承FragmentLayout,它需要放在AppBarLayout布局里面,并且作为AppBarLayout的直接子View。CollapsingToolbarLayout主要包括几个功能:
①:折叠Title(Collapsing title):当布局内容全部显示出来时,title是最大的,但是随着View逐步移出屏幕顶部,title变得越来越小。你可以通过CollapsingToolbarLayout调用setTitle函数来设置title。

②:内容纱布(Content scrim):根据滚动的位置是否到达一个阀值,来决定是否对View“盖上纱布”。可以通过setContentScrim(Drawable)来设置纱布的图片.ToolBar被折叠到顶部固定时候的背景,你可以调用setContentScrim(Drawable)方法改变背景或者 在属性中使用 app:contentScrim=”?attr/colorPrimary”来改变背景。

③:状态栏纱布(Status bar scrim):根据滚动位置是否到达一个阀值决定是否对状态栏“盖上纱布”,你可以通过setStatusBarScrim(Drawable)来设置纱布图片,调用方法setStatusBarScrim(,l)设置状态栏的背景,但是只能在LOLLIPOP设备上面有作用。这个只能在Android5.0以上系统有效果。

④ollapseMode :子视图的折叠模式,在子视图设置,有两种,想要明确的看到效果可以给Toolbar设置一个背景颜色
(1)“pin”:固定模式,在折叠的时候最后固定在顶端;
(2)“parallax”:视差模式,在折叠的时候会有个视差折叠的效果。我们可以在布局中使用属性app:layout_collapseMode=”parallax”来改变:

(1):将子View位置固定(Pinned position children):子View可以选择是否在全局空间上固定位置,这对于Toolbar来说非常有用,因为当布局在移动时,可以将Toolbar固定位置而不受移动的影响。 将app:layout_collapseMode设为pin。

(2):视差滚动子View(Parallax scrolling children):子View可以选择在当前的布局当时是否以“视差”的方式来跟随滚动。(:其实就是让这个View的滚动的速度比其他正常滚动的View速度稍微慢一点)。将布局参数app:layout_collapseMode设为parallax,值的范围[0.0,1.0],值越大视察越大。

app:layout_collapseMode=”parallax”表示ImageView的折叠和CollapsingToolbarLayout的折叠不同步,那么这个不同步到底是怎样一个不同步法呢?还有另外一个参数来设置不同步的参数,如下:
app:layout_collapseParallaxMultiplier=”0.5”表示视觉乘数,该数值的取值为0~1,数值越大,视觉差越大(如果这里的值为0,则在头部折叠的过程中,ImageView的顶部在慢慢隐藏,底部不动,如果这里的值为1,ImageView的顶部不懂,底部慢慢隐藏,如果这里的取值为0~1之间,则在折叠的过程中,ImageView的顶部和底部都会隐藏,但是头部和底部隐藏的快慢是不一样的,具体速度和视觉乘数有关)
app:layout_collapseMode这个属性还有一个取值,是pin,该属性表示当折叠完成时将该控件放在页面的头部.
最终的代码:

<android.support.design.widget.CoordinatorLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="256dp"
        android:fitsSystemWindows="true">
        <android.support.design.widget.CollapsingToolbarLayout
            android:id="@+id/collapsing_toolbar_layout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:contentScrim="#30469b"
            app:expandedTitleMarginStart="48dp"            app:expandedTitleTextAppearance="@style/TextAppearance.AppCompat.Title"           app:collapsedTitleTextAppearance="@style/TextAppearance.AppCompat.Title"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">
            <ImageView
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:scaleType="centerCrop"
                android:src="@mipmap/bg"
                app:layout_collapseMode="parallax"
                app:layout_collapseParallaxMultiplier="0.7"  />
            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                app:layout_collapseMode="pin" />
        </android.support.design.widget.CollapsingToolbarLayout>
    </android.support.design.widget.AppBarLayout>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">
        <android.support.v7.widget.RecyclerView
            android:id="@+id/recyclerView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:scrollbars="none" />
    </LinearLayout>
</android.support.design.widget.CoordinatorLayout>

Java代码

Toolbar mToolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(mToolbar);
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
        mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                onBackPressed();
            }
        });
        //使用CollapsingToolbarLayout必须把title设置到CollapsingToolbarLayout上,设置到Toolbar上则不会显示
        CollapsingToolbarLayout mCollapsingToolbarLayout = (CollapsingToolbarLayout) findViewById(R.id.collapsing_toolbar_layout);
        mCollapsingToolbarLayout.setTitle("CollapsingToolbarLayout");
        //通过CollapsingToolbarLayout修改字体颜色
        mCollapsingToolbarLayout.setExpandedTitleColor(Color.WHITE);//设置还没收缩时状态下字体颜色
        mCollapsingToolbarLayout.setCollapsedTitleTextColor(Color.GREEN);//设置收缩后Toolbar上字体的颜色
作者:wsdssss 发表于2016/9/29 16:48:03 原文链接
阅读:112 评论:0 查看评论

[HyBrid]HyBrid混编初尝:原生和第三方JsBridge的使用

$
0
0

最近研究HyBrid的两种方式:

一、直接原生WebView

1)初始化WebView:

<span style="white-space:pre">	</span>//启动javascript
        webView = (WebView) findViewById(R.id.webView);
        //webView.setVerticalScrollbarOverlay(true);
        String uri = "file:///android_asset/js_test2.html";
        webView.loadUrl(uri);
        // 添加客户端支持
        webView.setWebChromeClient(new WebChromeClient());
        // 设置WebView支持JavaScript
        webView.getSettings().setJavaScriptEnabled(true);
注释比较清楚,不多解释。

2)WebView与js通信

原生中代码:

// 调用js中的函数:jsFun(msg)
        webView.loadUrl("javascript:jsFun('" + msg + "')");
直接通过Url加载javascript:+方法名

js中对应代码:

//在java中调用此方法
function jsFun(msg){
alert("我是js方法,被java调用,传递过来的参数是:"+msg);
}

如此即可调用在native中调用js代码:效果图如下:


3)js与WebView通信

native中添加:

//  添加js交互接口
        webView.addJavascriptInterface(new MyJava(this), "javaObject");
addJavascriptInterface()源码:

 * @param object the Java object to inject into this WebView's JavaScript
     *               context. Null values are ignored.
     * @param name the name used to expose the object in JavaScript
     */
    public void addJavascriptInterface(Object object, String name) {
        checkThread();
        mProvider.addJavascriptInterface(object, name);
    }
第一个参数是Object对象:将Object对象注入到WebView的JavaScript的上下文中。

第二个参数name:在JavaScript暴露对象使用的名称。

通俗点讲:也就是用第二个参数名字在js中使用,来引用第一个java对象。

我们这个例子中自定义了一个MyJava对象:

private class MyJava {
        private Context mContext;

        public MyJava(Context context) {
            this.mContext = context;
        }

        // 在js中调用window.javaObject.javaFun(name),便会触发此方法。
        // api17版本以上加上注解
        @JavascriptInterface
        public void javaFun(String name) {
            Toast.makeText(mContext, "我是java方法,被js调用,js传递过来的参数是:" + name,
                    Toast.LENGTH_LONG).show();
        }
    }
构造方法直接忽略。这句注释千万不能忽略。

@JavascriptInterface
涉及到4.2以上版本能否使用。4.2之前有个安全漏洞,4.2之后修复的方式,是js调用native的方法,方法上必须通过该注释注明才可以调用。

方法javaFun很简单,不多说,直接看js中如何调用。

js中添加:

function sendInfoToJava(){
//js调用java方法,并传递参数
var value = document.getElementById("name_input").value;
window.javaObject.javaFun(value);
}
通过window.javaObject.javaFun(value)调用native的方法。之前已经解释过javaObject引用MyJava对象。然后通过对象引用方法。
js引用native动画效果如下:


二、JsBridge方法实现HyBrid

一、导入第三方类库:

PS : JsBridge类库地址。

二、Android Studio导入第三方类库说简单简单,说难也难。

查看类库结构:


导入步骤:

1)先复制上述第三方类库到app平级处:如下


2)导入第三方类库:如下图:按顺序:




此处上面的example就不要导入了。一个样例,导入多余,还可能冲突。选择finish。导入第三方完毕。

高兴的太早了。主项目还调用不了这个导入的第三方包。

还需如下设置:



选择之前的命名的第三方类库,ok导入完毕。可以正常使用了。

三、使用JsBridge实现

PS:查阅了好多资料,之后总算是懂了。哎。好好学习,天天向上吧,少年。

通用部分:

1)使用JsBridge的WebView:BridgeWebView

xml:写一个BridgeWebView

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

    <!-- button 演示Java调用web -->
    <Button
        android:id="@+id/button"
        android:layout_width="match_parent"
        android:text="button 演示Java调用web"
        android:layout_height="48dp"
        />

    <!-- webview 演示web调用Java -->
    <com.github.lzyzsd.jsbridge.BridgeWebView
        android:id="@+id/webView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
     </com.github.lzyzsd.jsbridge.BridgeWebView>

</LinearLayout>
说明:

样式很简单:上面一个按钮写着:button 演示Java调用web。下面一个第三方自定义webView控件。

第一版资源文件demo.html:(Js看不懂,可以看完下面的描述再看)

<html>
<head>
    <meta content="text/html; charset=utf-8" http-equiv="content-type">
    <title>
        js调用java
    </title>
</head>
<body>
<a id="show"></a>
<script>

    function connectWebViewJavascriptBridge(callback) {
        if (window.WebViewJavascriptBridge) {
            callback(WebViewJavascriptBridge)
        } else {
            document.addEventListener(
                'WebViewJavascriptBridgeReady'
                , function() {
                    callback(WebViewJavascriptBridge)
                },
                false
            );
        }
    }
    connectWebViewJavascriptBridge(function(bridge) {
        bridge.init(function(message, responseCallback) {
        console.log('JS got a message', message);
            responseCallback(data);
        });
        bridge.registerHandler("functionInJs", function(data, responseCallback) {
                document.getElementById("show").innerHTML = ("Native发来的消息是:" + data);
                var responseData = "Javascript Says Right back aka!";
                responseCallback(responseData);
            });
    })


</script>
</body>
</html>

2)直入主题:Native发送消息给H5

Acitivity中:通过上面的Native按钮发消息给H5:

Activity初始化JsBridge:

<span style="white-space:pre">	</span>setContentView(R.layout.activity_main);
        webView = (BridgeWebView) findViewById(R.id.webView);
        webView.loadUrl("file:///android_asset/demo.html");
只需要找到加载H5资源即可。Native点击事件如下:

@Override
    public void onClick(View v) {
        webView.callHandler("functionInJs", "传递消息给H5", new CallBackFunction() {
            @Override
            public void onCallBack(String data) {
            }
        });
    }
H5如何能接收呢?如上注册了一个functionInJs,那么应该在H5中对应的写法。

说到这,必须先说说

H5初始化JsBridge:

H5初始化JsBridge第一步:

connectWebViewJavascriptBridge(function(bridge) {
        bridge.init(function(message, responseCallback) {
        console.log('JS got a message', message);
            responseCallback(data);
        });
    })

H5初始化JsBridge第二步:

function connectWebViewJavascriptBridge(callback) {
        if (window.WebViewJavascriptBridge) {
            callback(WebViewJavascriptBridge)
        } else {
            document.addEventListener(
                'WebViewJavascriptBridgeReady'
                , function() {
                    callback(WebViewJavascriptBridge)
                },
                false
            );
        }
    }
知道在哪初始化了,那么我就在[H5初始化JsBridge第一步]里面注册H5后如下:

connectWebViewJavascriptBridge(function(bridge) {
        bridge.init(function(message, responseCallback) {
        console.log('JS got a message', message);
            responseCallback(data);
        });
        bridge.registerHandler("functionInJs", function(data, responseCallback) {
                document.getElementById("show").innerHTML = ("Native发来的消息是:" + data);
                var responseData = "Javascript Says Right back aka!";
                responseCallback(responseData);
            });
    })
加上这一段代码就对应注册了functionInJs。

看看动图效果图:


3)直入主题:H5发送消息给Native

第二版资源文件demo.html:

<html>
<head>
    <meta content="text/html; charset=utf-8" http-equiv="content-type">
    <title>
        js调用java
    </title>
</head>
<body>
<input type="button" value="点击1" onclick="go()"/>
<a id="a"></a>
<script>
    function go(){
        window.WebViewJavascriptBridge.callHandler(
            "Android",
            "Hello~",
            function(responseData){
                document.getElementById('a').innerHTML = 'Native给我的数据: ' + responseData;
            }
        );
    }

    function connectWebViewJavascriptBridge(callback) {
        if (window.WebViewJavascriptBridge) {
            callback(WebViewJavascriptBridge)
        } else {
            document.addEventListener(
                'WebViewJavascriptBridgeReady'
                , function() {
                    callback(WebViewJavascriptBridge)
                },
                false
            );
        }
    }
    
    connectWebViewJavascriptBridge(function(bridge) {
        bridge.init(function(message, responseCallback) {
        console.log('JS got a message', message);
            responseCallback(data);
        });
    })


</script>
</body>
</html>
说明:比第一版本多了一个点击事件。排除了第一版本的干扰Js代码。(H5初始化JsBridge不变)

既然是H5调用Native:先看Js的点击事件代码:

function go(){
        window.WebViewJavascriptBridge.callHandler(
            "Android",
            "Hello~",
            function(responseData){
                document.getElementById('a').innerHTML = 'Native给我的数据: ' + responseData;
            }
        );
    }
三个参数分别是:第一个参数:注册了一个Android。第二个参数:发送的消息内容是Hello~。第三个参数是一个回调。调用成功后,由Native返回responseData。

然后Native对应注册Android:

 webView.registerHandler("Android", new BridgeHandler() {
            @Override
            public void handler(String s, CallBackFunction callBackFunction) {
                Toast.makeText(MainActivity.this, "H5给我的数据:" + s, Toast.LENGTH_SHORT).show();
                callBackFunction.onCallBack("fuck!");
            }
        });
这样子,对应注册完毕。

看个动图效果图:点击1被点击后,先弹出Native的Toast,然后回调了H5的内容文本。


正常来说,讲到这儿,应该是讲完了。但是这个第三方还自带懒人功能:Native调用H5:不注册参数。按照我们之前的例子来说就是不设置第一个参数functionInJs。反之,H5调用Native:不注册参数也可以调用默认的Native功能。

4)Native默认设置:H5发送消息给Native。

写一个类继承默认处理类DefaultHandler

class MDefaultHandler extends DefaultHandler{
        @Override
        public void handler(String data, CallBackFunction function) {
            super.handler(data, function);
            Log.d(TAG,data);
            Toast.makeText(MainActivity.this,data,Toast.LENGTH_SHORT).show();
        }
    }

然后对WebView设置:

webView.setDefaultHandler(new MDefaultHandler());
Js调用的时候如何调用?

直接上demo.html:

<html>
<head>
    <meta content="text/html; charset=utf-8" http-equiv="content-type">
    <title>
        js调用java
    </title>
</head>
<body>
<input type="button" value="点击1" onclick="go()"/>
<a id="a"></a>
<script>
    function go(){
        window.WebViewJavascriptBridge.send(
        "被传递的Data"
        , function(responseData) {
            document.getElementById("a").innerHTML = "H5默认调用Native" + responseData
        }
    );
    }

    function connectWebViewJavascriptBridge(callback) {
        if (window.WebViewJavascriptBridge) {
            callback(WebViewJavascriptBridge)
        } else {
            document.addEventListener(
                'WebViewJavascriptBridgeReady'
                , function() {
                    callback(WebViewJavascriptBridge)
                },
                false
            );
        }
    }

    connectWebViewJavascriptBridge(function(bridge) {
        bridge.init(function(message, responseCallback) {
        console.log('JS got a message', message);
            responseCallback(data);
        });
    })


</script>
</body>
</html>
可见只是这儿有所修改:

window.WebViewJavascriptBridge.send(
        "被传递的Data"
        , function(responseData) {
            document.getElementById("a").innerHTML = "H5默认调用Native" + responseData
        }
    );
改变:只有两个参数了。方法名变成了send。
看看动画效果图:


5)Js默认设置:Native发送消息给H5。

看看Js初始化第一步:

connectWebViewJavascriptBridge(function(bridge) {
        bridge.init(function(message, responseCallback) {
        console.log('JS got a message', message);
            responseCallback(data);
        });
    })

此处的message其实就是默认的发来的消息。

Native发送默认消息方式非常简单:点击事件里面调用一下即可:

@Override
    public void onClick(View v) {
        webView.send("hello来自Native的默认消息");
    }
为了方便验证:将Js初始化第一步改成如下:

connectWebViewJavascriptBridge(function(bridge) {
        bridge.init(function(message, responseCallback) {
        document.getElementById("a").innerHTML = "来自Native的:" + message;
            responseCallback(data);
        });
    })
也就是在id等于a的文本处展示。

动图效果图如下:


Over~打算再写一章关于JsBridge的原理,看看我能不能读懂大神的源码吧。

作者:haibo_bear 发表于2016/9/29 17:03:45 原文链接
阅读:186 评论:0 查看评论

Linux设备驱动模型-Uevent

$
0
0

前言

当一个设备动态的加入到系统时候(比如常见的将U盘插入到PC机器上), 设备驱动程序就需要动态的检测到有设备插入了系统,就需要将此事件通知到用户层,然后用户层对这一事件做响应的处理,比如加载USB驱动,更新UI等。而将此事件通知到用户层就需要某种机制,典型的就是mdev hotplug和udev。关于udev和mdev hotplug可以在上篇文章有解释。Linux系统对uevent机制的具体实现是建立在设备模型的基础上的,通过kobject_uevent函数实现。

在前面的kset小节中提到了注册一个kset的接口,可以在这里习复下。
/**
 * kset_register - initialize and add a kset.
 * @k: kset.
 */
int kset_register(struct kset *k)
{
	int err;

	if (!k)
		return -EINVAL;

	kset_init(k);
	err = kobject_add_internal(&k->kobj);
	if (err)
		return err;
	kobject_uevent(&k->kobj, KOBJ_ADD);
	return 0;
}
可以看到这里调用了kobject_uevent接口,发送一个action为: KOBJ_ADD的事件。而kobject和kset的主要区别就是,将一个kset注册到系统的时候,就需要将此事件通过kobject_uevent发送到用户空间,而kobject如果是单独的,没有依赖kset,则无法通过uevent机制发送事件到用户空间。

数据结构

struct kset_uevent_ops {
	int (* const filter)(struct kset *kset, struct kobject *kobj);
	const char *(* const name)(struct kset *kset, struct kobject *kobj);
	int (* const uevent)(struct kset *kset, struct kobject *kobj,
		      struct kobj_uevent_env *env);
};
kset_uevent_ops代表意思是Kset事件处理函数集合。

filter:        当上报uevent的时候,kset会通过filter接口去过滤,阻止不希望上报的uevent。
name:     返回kset的名称,如果此kset没有名称,也是不允许上报event
uevent:   通常会调用此回调处理一些Kset的私有事情。

struct kobj_uevent_env {
	char *argv[3];
	char *envp[UEVENT_NUM_ENVP];
	int envp_idx;
	char buf[UEVENT_BUFFER_SIZE];
	int buflen;
};
envp:           用户保存每个环境变量的地址,最大支持32个。
envp_idx:     用户访问envp。
buf:              保存环境的buffer,最大支持2048
buflen:         用于访问buf。

代码分析

/**
 * kobject_uevent - notify userspace by sending an uevent
 *
 * @action: action that is happening
 * @kobj: struct kobject that the action is happening to
 *
 * Returns 0 if kobject_uevent() is completed with success or the
 * corresponding error when it fails.
 */
int kobject_uevent(struct kobject *kobj, enum kobject_action action)
{
	return kobject_uevent_env(kobj, action, NULL);
}
可以看到注释:  通过发送一个uevent通知事件到用户层, action就是当前发生的事件类型,如下action是个枚举类型
enum kobject_action {
	KOBJ_ADD,             
	KOBJ_REMOVE,
	KOBJ_CHANGE,
	KOBJ_MOVE,
	KOBJ_ONLINE,
	KOBJ_OFFLINE,
	KOBJ_MAX
};
KOBJ_ADD/KOBJ_REMOVE代表添加或者移除
KOBJ_ONLINE/KOBJ_OFFLINE代表上线或这下线
/**
 * kobject_uevent_env - send an uevent with environmental data
 *
 * @action: action that is happening
 * @kobj: struct kobject that the action is happening to
 * @envp_ext: pointer to environmental data
 *
 * Returns 0 if kobject_uevent_env() is completed with success or the
 * corresponding error when it fails.
 */
int kobject_uevent_env(struct kobject *kobj, enum kobject_action action,
		       char *envp_ext[])
{
	struct kobj_uevent_env *env;
	const char *action_string = kobject_actions[action];
	const char *devpath = NULL;
	const char *subsystem;
	struct kobject *top_kobj;
	struct kset *kset;
	const struct kset_uevent_ops *uevent_ops;
	int i = 0;
	int retval = 0;
#ifdef CONFIG_NET
	struct uevent_sock *ue_sk;
#endif

	pr_debug("kobject: '%s' (%p): %s\n",
		 kobject_name(kobj), kobj, __func__);

	/* search the kset we belong to */
	top_kobj = kobj;
	while (!top_kobj->kset && top_kobj->parent)            //听过while循环找到kobj所属的顶层kset
		top_kobj = top_kobj->parent;

	if (!top_kobj->kset) {                                 //发送一个envet必须存在kset
		pr_debug("kobject: '%s' (%p): %s: attempted to send uevent "
			 "without kset!\n", kobject_name(kobj), kobj,
			 __func__);
		return -EINVAL;
	}

	kset = top_kobj->kset;                                //得到最顶层kset的uevent_ops
	uevent_ops = kset->uevent_ops;

	/* skip the event, if uevent_suppress is set*/
	if (kobj->uevent_suppress) {                           //如果uevnet_suppress=1,则不发送uevent
		pr_debug("kobject: '%s' (%p): %s: uevent_suppress "
				 "caused the event to drop!\n",
				 kobject_name(kobj), kobj, __func__);
		return 0;
	}
	/* skip the event, if the filter returns zero. */
	if (uevent_ops && uevent_ops->filter)                 //通过filter函数过滤,如果返回0,则说明顶层的kset过滤了此event
		if (!uevent_ops->filter(kset, kobj)) {
			pr_debug("kobject: '%s' (%p): %s: filter function "
				 "caused the event to drop!\n",
				 kobject_name(kobj), kobj, __func__);
			return 0;
		}

	/* originating subsystem */
	if (uevent_ops && uevent_ops->name)                     //通过name函数设置subsystem       
		subsystem = uevent_ops->name(kset, kobj);
	else
		subsystem = kobject_name(&kset->kobj);
	if (!subsystem) {
		pr_debug("kobject: '%s' (%p): %s: unset subsystem caused the "
			 "event to drop!\n", kobject_name(kobj), kobj,
			 __func__);
		return 0;
	}

	/* environment buffer */
	env = kzalloc(sizeof(struct kobj_uevent_env), GFP_KERNEL);      //分配环境变量buff
	if (!env)
		return -ENOMEM;

	/* complete object path */
	devpath = kobject_get_path(kobj, GFP_KERNEL);                 //得到此obj的路径
	if (!devpath) {
		retval = -ENOENT;
		goto exit;
	}

	/* default keys */
	retval = add_uevent_var(env, "ACTION=%s", action_string);         //添加环境变量,ACTION, DEVPATH, SUBSYSTEM到环境变量buff中
	if (retval)
		goto exit;
	retval = add_uevent_var(env, "DEVPATH=%s", devpath);
	if (retval)
		goto exit;
	retval = add_uevent_var(env, "SUBSYSTEM=%s", subsystem);
	if (retval)
		goto exit;

	/* keys passed in from the caller */
	if (envp_ext) {                                                       //添加调用者提供的参数
		for (i = 0; envp_ext[i]; i++) {
			retval = add_uevent_var(env, "%s", envp_ext[i]);
			if (retval)
				goto exit;
		}
	}

	/* let the kset specific function add its stuff */                    //让Kset完成一些自己的私人处理
	if (uevent_ops && uevent_ops->uevent) {
		retval = uevent_ops->uevent(kset, kobj, env);
		if (retval) {
			pr_debug("kobject: '%s' (%p): %s: uevent() returned "
				 "%d\n", kobject_name(kobj), kobj,
				 __func__, retval);
			goto exit;
		}
	}

	/*
	 * Mark "add" and "remove" events in the object to ensure proper
	 * events to userspace during automatic cleanup. If the object did
	 * send an "add" event, "remove" will automatically generated by
	 * the core, if not already done by the caller.
	 */
	if (action == KOBJ_ADD)
		kobj->state_add_uevent_sent = 1;
	else if (action == KOBJ_REMOVE)
		kobj->state_remove_uevent_sent = 1;

	mutex_lock(&uevent_sock_mutex);
	/* we will send an event, so request a new sequence number */                     //更新uevent seq number
	retval = add_uevent_var(env, "SEQNUM=%llu", (unsigned long long)++uevent_seqnum);
	if (retval) {
		mutex_unlock(&uevent_sock_mutex);
		goto exit;
	}

#if defined(CONFIG_NET)            //如果开启了CONFIG_NET就使用netlink发送Uevent
	/* send netlink message */
	list_for_each_entry(ue_sk, &uevent_sock_list, list) {
		struct sock *uevent_sock = ue_sk->sk;
		struct sk_buff *skb;
		size_t len;

		if (!netlink_has_listeners(uevent_sock, 1))
			continue;

		/* allocate message with the maximum possible size */
		len = strlen(action_string) + strlen(devpath) + 2;
		skb = alloc_skb(len + env->buflen, GFP_KERNEL);
		if (skb) {
			char *scratch;

			/* add header */
			scratch = skb_put(skb, len);
			sprintf(scratch, "%s@%s", action_string, devpath);

			/* copy keys to our continuous event payload buffer */
			for (i = 0; i < env->envp_idx; i++) {
				len = strlen(env->envp[i]) + 1;
				scratch = skb_put(skb, len);
				strcpy(scratch, env->envp[i]);
			}

			NETLINK_CB(skb).dst_group = 1;
			retval = netlink_broadcast_filtered(uevent_sock, skb,
							    0, 1, GFP_KERNEL,
							    kobj_bcast_filter,
							    kobj);
			/* ENOBUFS should be handled in userspace */
			if (retval == -ENOBUFS || retval == -ESRCH)
				retval = 0;
		} else
			retval = -ENOMEM;
	}
#endif
	mutex_unlock(&uevent_sock_mutex);

#ifdef CONFIG_UEVENT_HELPER             //如果开启了就是用uevent_helper发送uevent。
	/* call uevent_helper, usually only enabled during early boot */
	if (uevent_helper[0] && !kobj_usermode_filter(kobj)) {
		struct subprocess_info *info;

		retval = add_uevent_var(env, "HOME=/");
		if (retval)
			goto exit;
		retval = add_uevent_var(env,
					"PATH=/sbin:/bin:/usr/sbin:/usr/bin");
		if (retval)
			goto exit;
		retval = init_uevent_argv(env, subsystem);
		if (retval)
			goto exit;

		retval = -ENOMEM;
		info = call_usermodehelper_setup(env->argv[0], env->argv,
						 env->envp, GFP_KERNEL,
						 NULL, cleanup_uevent_env, env);
		if (info) {
			retval = call_usermodehelper_exec(info, UMH_NO_WAIT);
			env = NULL;	/* freed by cleanup_uevent_env */
		}
	}
#endif

exit:
	kfree(devpath);
	kfree(env);
	return retval;
}

uevent_helper机制

目前内核支持两种方式,netlink和uevent_helper,本节重点分析uevent_helper的实现。
uevent_helper的定义如下
#ifdef CONFIG_UEVENT_HELPER
char uevent_helper[UEVENT_HELPER_PATH_LEN] = CONFIG_UEVENT_HELPER_PATH;
#endif
CONFIG_UEVENT_HELPER_PATH可以在内核的config文件中找到。
CONFIG_UEVENT_HELPER_PATH="/sbin/hotplug"
对应的/sbin/hotplug,  而此hotplug对应的程序是什么?  如果是嵌入式设备,会在etc目录下看到这样的配置:
echo /sbin/mdev >/proc/sys/kernel/hotplug
/sbin/mdev -s
也就是说uevent_helper最终调用到/sbin/mdev.

接着会到kobject_uevent函数中,继续分析。
在开是调用userhelper之前的准备工作。
struct subprocess_info *call_usermodehelper_setup(char *path, char **argv,
		char **envp, gfp_t gfp_mask,
		int (*init)(struct subprocess_info *info, struct cred *new),
		void (*cleanup)(struct subprocess_info *info),
		void *data)
{
	struct subprocess_info *sub_info;
	sub_info = kzalloc(sizeof(struct subprocess_info), gfp_mask);
	if (!sub_info)
		goto out;

	INIT_WORK(&sub_info->work, __call_usermodehelper);                //初始化一个工作队列。
	sub_info->path = path;                                            //通过参数初始化subprocess_info
	sub_info->argv = argv;
	sub_info->envp = envp;

	sub_info->cleanup = cleanup;
	sub_info->init = init;
	sub_info->data = data;
  out:
	return sub_info;
}
接着调用call_usermodehelper_exec函数开启一个用户模式的应用程序。
int call_usermodehelper_exec(struct subprocess_info *sub_info, int wait)
{
	DECLARE_COMPLETION_ONSTACK(done);                            //初始化一个完成对象
	int retval = 0;

	if (!sub_info->path) {                                      //如果没有path变量,就执行cleanup
		call_usermodehelper_freeinfo(sub_info);
		return -EINVAL;
	}
	helper_lock();                                               //原子变量running_helpers加1
	if (!khelper_wq || usermodehelper_disabled) {                //如果不存在khelper_wq,或者usermodehelper已经disabled
		retval = -EBUSY;
		goto out;
	}
	/*
	 * Worker thread must not wait for khelper thread at below
	 * wait_for_completion() if the thread was created with CLONE_VFORK
	 * flag, for khelper thread is already waiting for the thread at
	 * wait_for_completion() in do_fork().
	 */
	if (wait != UMH_NO_WAIT && current == kmod_thread_locker) {     
		retval = -EBUSY;
		goto out;
	}

	/*
	 * Set the completion pointer only if there is a waiter.
	 * This makes it possible to use umh_complete to free
	 * the data structure in case of UMH_NO_WAIT.
	 */
	sub_info->complete = (wait == UMH_NO_WAIT) ? NULL : &done;
	sub_info->wait = wait;

	queue_work(khelper_wq, &sub_info->work);                               //提交工作节点到工作队列。
	if (wait == UMH_NO_WAIT)	/* task has freed sub_info */          //如果wait等于NO_WAIT则就返回。
		goto unlock;

	if (wait & UMH_KILLABLE) {                                
		retval = wait_for_completion_killable(&done);                   //如果支持可kill的
		if (!retval)
			goto wait_done;

		/* umh_complete() will see NULL and free sub_info */
		if (xchg(&sub_info->complete, NULL))
			goto unlock;
		/* fallthrough, umh_complete() was already called */
	}

	wait_for_completion(&done);                                      //如果wait不是上述的两种,就一直等待,那等待什么?  当然是等待有人解放它。
wait_done:
	retval = sub_info->retval;
out:
	call_usermodehelper_freeinfo(sub_info);
unlock:
	helper_unlock();
	return retval;
}
那什么时候会唤醒等待?  当然是工作队列上的任务完成之后,就会触发complete,唤醒等待。
/* This is run by khelper thread  */
static void __call_usermodehelper(struct work_struct *work)
{
	struct subprocess_info *sub_info =
		container_of(work, struct subprocess_info, work);
	int wait = sub_info->wait & ~UMH_KILLABLE;
	pid_t pid;

	/* CLONE_VFORK: wait until the usermode helper has execve'd
	 * successfully We need the data structures to stay around
	 * until that is done.  */
	if (wait == UMH_WAIT_PROC)
		pid = kernel_thread(wait_for_helper, sub_info,
				    CLONE_FS | CLONE_FILES | SIGCHLD);
	else {
		pid = kernel_thread(call_helper, sub_info,
				    CLONE_VFORK | SIGCHLD);
		/* Worker thread stopped blocking khelper thread. */
		kmod_thread_locker = NULL;
	}

	if (pid < 0) {
		sub_info->retval = pid;
		umh_complete(sub_info);
	}
}
此处通过创建一个内核线程,当调度到call_helper函数,此函数调用到____call_usermodehelper。在此函数中最终调用
	retval = do_execve(getname_kernel(sub_info->path),
			   (const char __user *const __user *)sub_info->argv,
			   (const char __user *const __user *)sub_info->envp);
在内核空间执行应用程序。最终在umh_complete函数中调用complete,唤醒等待。
static void umh_complete(struct subprocess_info *sub_info)
{
	struct completion *comp = xchg(&sub_info->complete, NULL);
	/*
	 * See call_usermodehelper_exec(). If xchg() returns NULL
	 * we own sub_info, the UMH_KILLABLE caller has gone away
	 * or the caller used UMH_NO_WAIT.
	 */
	if (comp)
		complete(comp);
	else
		call_usermodehelper_freeinfo(sub_info);
}

至此就分析完毕。

作者:longwang155069 发表于2016/9/29 17:21:16 原文链接
阅读:186 评论:1 查看评论
Viewing all 5930 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>