Android HLS协议相关记录及部分解析

来源:互联网 发布:篆刻印章制作软件 编辑:程序博客网 时间:2024/05/16 09:04

github:AndroidVideoServer(参考库)

Android 底层实现HLS协议的部分解析

由于目前网络不好,暂时先记录想到的,因为HLS是最近开始学习研究的,害怕最近项目忙忘记,所以先记录下(以下位于LibStageFright):

  • Android.mk
  • HTTPDownloader.cpp
  • HTTPDownloader.h
  • LiveDataSource.cpp
  • LiveDataSource.h
  • LiveSession.cpp
  • LiveSession.h
  • M3UParser.cpp
  • M3UParser.h
  • MODULE_LICENSE_APACHE2
  • PlaylistFetcher.cpp
  • PlaylistFetcher.h

上面三个算是底层主要是的核心东西.我先大致讲一下Android手机播放HLS的流程.

  • 首先Android给VideoView设置Url.
  • 接下来设置Url后,就会进入JNI创建MediaPlayerService,然后与底层进行Binder通信.
  • 然后会在底层创建一个MediaPlayerService,在MediaPlayerServices中的Create方法中会判断类型.
  • 根据Url地址进行判断,如果是Http或者Https开头的,然后是否包含了M3u8的就创建一个NuPlayerDriver.
  • 其实底层核心的是StrageFright框架.创建了具体的播放对象后,会判断是否采用硬件解码,并进行解码播放工作.

MediaPlayerService(底层)

连接:https://android.googlesource.com/platform/frameworks/av/+/jb-release/media/libmediaplayerservice/MediaPlayerService.cpp

主要是根据url地址创建不同的播放器对象,具体代码如下:

    static sp<MediaPlayerBase> createPlayer(player_type playerType, void* cookie,            notify_callback_f notifyFunc)    {        sp<MediaPlayerBase> p;        switch (playerType) {     // 根据类型创建不同的播放器对象.            case SONIVOX_PLAYER:                ALOGV(" create MidiFile");                p = new MidiFile();                break;            case STAGEFRIGHT_PLAYER:                ALOGV(" create StagefrightPlayer");                p = new StagefrightPlayer;                break;            case NU_PLAYER:                ALOGV(" create NuPlayer");                p = new NuPlayerDriver; //专门用来播放HLS协议的播放器对象                break;            case TEST_PLAYER:                ALOGV("Create Test Player stub");                p = new TestPlayerStub();                break;            case AAH_RX_PLAYER:                ALOGV(" create A@H RX Player");                p = createAAH_RXPlayer();                break;            case AAH_TX_PLAYER:                ALOGV(" create A@H TX Player");                p = createAAH_TXPlayer();                break;            default:                ALOGE("Unknown player type: %d", playerType);                return NULL;        }        if (p != NULL) {            if (p->initCheck() == NO_ERROR) {                p->setNotifyCallback(cookie, notifyFunc);            } else {                p.clear();            }        }        if (p == NULL) {            ALOGE("Failed to create player object");        }        return p;    }

那么我们去看看PlayerType在哪儿获得的,怎么获得的:

    player_type getPlayerType(const char* url)    {        if (TestPlayerStub::canBeUsed(url)) {            return TEST_PLAYER;        }        if (!strncasecmp("http://", url, 7)                || !strncasecmp("https://", url, 8)) {  //判断是否为https,或者http的            size_t len = strlen(url);            if (len >= 5 && !strcasecmp(".m3u8", &url[len - 5])) { // 并且是有一个有效的m3u8结尾的url                return NU_PLAYER;            }            if (strstr(url,"m3u8")) {                return NU_PLAYER;            }        }        if (!strncasecmp("rtsp://", url, 7)) {            return NU_PLAYER;        }        if (!strncasecmp("aahRX://", url, 8)) {            return AAH_RX_PLAYER;        }        // use MidiFile for MIDI extensions        int lenURL = strlen(url);        for (int i = 0; i < NELEM(FILE_EXTS); ++i) {            int len = strlen(FILE_EXTS[i].extension);            int start = lenURL - len;            if (start > 0) {                if (!strncasecmp(url + start, FILE_EXTS[i].extension, len)) {                    return FILE_EXTS[i].playertype;                }            }        }        return getDefaultPlayerType(); // 如果之前都没有,那么返回默认的.    }

其中遇到的问题有:

  • 如何实现边看边缓存ts — 已经解决(方法不是很好)
  • 拖拽重复请求 —- 看源码正在寻求解决方案
  • Android底层是如何缓存ts或者轮训播放的 —- 自己猜想过,目前看到了一部分源码可能是,但是还在着手看.
  • Android 3.0以下如何支持HLS — 看VCL源码中…

HttpLiveSession去获取Ts

    for (size_t i = 0; i < kNumSources; ++i) { // 创建了2个获取器,去获取TS资源.            mPacketSources.add(indexToType(i), new AnotherPacketSource(NULL /* meta */));            mPacketSources2.add(indexToType(i), new AnotherPacketSource(NULL /* meta */));        }

将数据交给NuPlayerDriver

    sp<AnotherPacketSource> LiveSession::getMetadataSource(            sp<AnotherPacketSource> sources[kNumSources], uint32_t streamMask, bool newUri) {        // todo: One case where the following strategy can fail is when audio and video        // are in separate playlists, both are transport streams, and the metadata        // is actually contained in the audio stream.        ALOGV("[timed_id3] getMetadataSourceForUri streamMask %x newUri %s",                streamMask, newUri ? "true" : "false");        if ((sources[kVideoIndex] != NULL) // video fetcher; or ...                || (!(streamMask & STREAMTYPE_VIDEO) && sources[kAudioIndex] != NULL)) {                // ... audio fetcher for audio only variant            return getPacketSourceForStreamIndex(kMetaDataIndex, newUri);        }        return NULL;    }

PlaylistFetcher

看名字就知道是来获取PlayList索引文件里面的数据,比如ts解析每个字段什么的.
我们看看它具体做了些什么:

    PlaylistFetcher::PlaylistFetcher( // 很多参数            const sp<AMessage> &notify,            const sp<LiveSession> &session,            const char *uri,            int32_t id,            int32_t subtitleGeneration)        : mNotify(notify),          mSession(session),          mURI(uri),          mFetcherID(id),          mStreamTypeMask(0),          mStartTimeUs(-1ll),          mSegmentStartTimeUs(-1ll),          mDiscontinuitySeq(-1ll),          mStartTimeUsRelative(false),          mLastPlaylistFetchTimeUs(-1ll),          mPlaylistTimeUs(-1ll),          mSeqNumber(-1),          mNumRetries(0),          mStartup(true),          mIDRFound(false),          mSeekMode(LiveSession::kSeekModeExactPosition),          mTimeChangeSignaled(false),          mNextPTSTimeUs(-1ll),          mMonitorQueueGeneration(0),          mSubtitleGeneration(subtitleGeneration),          mLastDiscontinuitySeq(-1ll),          mRefreshState(INITIAL_MINIMUM_RELOAD_DELAY),          mFirstPTSValid(false),          mFirstTimeUs(-1ll),          mVideoBuffer(new AnotherPacketSource(NULL)),          mThresholdRatio(-1.0f),          mDownloadState(new DownloadState()),          mHasMetadata(false) {        memset(mPlaylistHash, 0, sizeof(mPlaylistHash));        mHTTPDownloader = mSession->getHTTPDownloader(); // HttpLiveSession对象去获取DownLoader.    }    // 看到LiveSession中去直接创建了一个HTTPDownloader对象.    sp<HTTPDownloader> LiveSession::getHTTPDownloader() {        return new HTTPDownloader(mHTTPService, mExtraHeaders);    }    /*我们看看HTTPDownloader,这个对象主要有三个方法:fetchBlock(),fetchFile(),fetchPlaylist()*/    HTTPDownloader::HTTPDownloader( //构造函数            const sp<IMediaHTTPService> &httpService,            const KeyedVector<String8, String8> &headers) :        mHTTPDataSource(new MediaHTTP(httpService->makeHTTPConnection())), // MediaHttp的连接对象.        mExtraHeaders(headers),        mDisconnecting(false) {    }    //fetchFile方法主要是获取Ts文件    ssize_t HTTPDownloader::fetchFile(        const char *url, sp<ABuffer> *out, String8 *actualUrl) {        ssize_t err = fetchBlock(url, out, 0, -1, 0, actualUrl, true /* reconnect */); // 可以看到主要是调用了fetchBlock方法.        // close off the connection after use        mHTTPDataSource->disconnect();        return err;    }    //那我们接下来去看看fetchBlock,这个方法有点复杂,大致就是获取流媒体内存区域的块    /*     * Illustration of parameters:     *     * 0      `range_offset`     * +------------+-------------------------------------------------------+--+--+     * |            |                                 | next block to fetch |  |  |     * |            | `source` handle => `out` buffer |                     |  |  |     * | `url` file |<--------- buffer size --------->|<--- `block_size` -->|  |  |     * |            |<----------- `range_length` / buffer capacity ----------->|  |     * |<------------------------------ file_size ------------------------------->|     *     * Special parameter values:     * - range_length == -1 means entire file     * - block_size == 0 means entire range     *     */    ssize_t HTTPDownloader::fetchBlock(            const char *url, sp<ABuffer> *out,            int64_t range_offset, int64_t range_length,            uint32_t block_size, /* download block size */            String8 *actualUrl,            bool reconnect /* force connect HTTP when resuing source */) {        if (isDisconnecting()) {            return ERROR_NOT_CONNECTED;        }        off64_t size;        if (reconnect) {            if (!strncasecmp(url, "file://", 7)) {                mDataSource = new FileSource(url + 7);            } else if (strncasecmp(url, "http://", 7)                    && strncasecmp(url, "https://", 8)) {                return ERROR_UNSUPPORTED;            } else {                KeyedVector<String8, String8> headers = mExtraHeaders;                if (range_offset > 0 || range_length >= 0) {                    headers.add(                            String8("Range"),                            String8(                                AStringPrintf(                                    "bytes=%lld-%s",                                    range_offset,                                    range_length < 0                                        ? "" : AStringPrintf("%lld",                                                range_offset + range_length - 1).c_str()).c_str()));                }                status_t err = mHTTPDataSource->connect(url, &headers);                if (isDisconnecting()) {                    return ERROR_NOT_CONNECTED;                }                if (err != OK) {                    return err;                }                mDataSource = mHTTPDataSource;            }        }        status_t getSizeErr = mDataSource->getSize(&size);        if (isDisconnecting()) {            return ERROR_NOT_CONNECTED;        }        if (getSizeErr != OK) {            size = 65536;        }        sp<ABuffer> buffer = *out != NULL ? *out : new ABuffer(size);        if (*out == NULL) {            buffer->setRange(0, 0);        }        ssize_t bytesRead = 0;        // adjust range_length if only reading partial block        if (block_size > 0 && (range_length == -1 || (int64_t)(buffer->size() + block_size) < range_length)) {            range_length = buffer->size() + block_size;        }        for (;;) {            // Only resize when we don't know the size.            size_t bufferRemaining = buffer->capacity() - buffer->size();            if (bufferRemaining == 0 && getSizeErr != OK) {                size_t bufferIncrement = buffer->size() / 2;                if (bufferIncrement < 32768) {                    bufferIncrement = 32768;                }                bufferRemaining = bufferIncrement;                ALOGV("increasing download buffer to %zu bytes",                     buffer->size() + bufferRemaining);                sp<ABuffer> copy = new ABuffer(buffer->size() + bufferRemaining);                memcpy(copy->data(), buffer->data(), buffer->size());                copy->setRange(0, buffer->size());                buffer = copy;            }            size_t maxBytesToRead = bufferRemaining;            if (range_length >= 0) {                int64_t bytesLeftInRange = range_length - buffer->size();                if (bytesLeftInRange < (int64_t)maxBytesToRead) {                    maxBytesToRead = bytesLeftInRange;                    if (bytesLeftInRange == 0) {                        break;                    }                }            }            // The DataSource is responsible for informing us of error (n < 0) or eof (n == 0)            // to help us break out of the loop.            ssize_t n = mDataSource->readAt(                    buffer->size(), buffer->data() + buffer->size(),                    maxBytesToRead);            if (isDisconnecting()) {                return ERROR_NOT_CONNECTED;            }            if (n < 0) {                return n;            }            if (n == 0) {                break;            }            buffer->setRange(0, buffer->size() + (size_t)n);            bytesRead += n;        }        *out = buffer;        if (actualUrl != NULL) {            *actualUrl = mDataSource->getUri();            if (actualUrl->isEmpty()) {                *actualUrl = url;            }        }        return bytesRead;    }    //接下来就是:fetchPlaylist方法.    sp<M3UParser> HTTPDownloader::fetchPlaylist(        const char *url, uint8_t *curPlaylistHash, bool *unchanged) {        ALOGV("fetchPlaylist '%s'", url);        *unchanged = false;        sp<ABuffer> buffer;        String8 actualUrl;        ssize_t err = fetchFile(url, &buffer, &actualUrl);//首先去获取文件        // close off the connection after use        mHTTPDataSource->disconnect();        if (err <= 0) {            return NULL;        }        // MD5 functionality is not available on the simulator, treat all        // playlists as changed.    #if defined(__ANDROID__)        uint8_t hash[16];        MD5_CTX m;        MD5_Init(&m);        MD5_Update(&m, buffer->data(), buffer->size());        MD5_Final(hash, &m);        if (curPlaylistHash != NULL && !memcmp(hash, curPlaylistHash, 16)) {            // playlist unchanged            *unchanged = true;            return NULL;        }        if (curPlaylistHash != NULL) {            memcpy(curPlaylistHash, hash, sizeof(hash));        }    #endif        sp<M3UParser> playlist =            new M3UParser(actualUrl.string(), buffer->data(), buffer->size());//  可以看到去解析了M3u8文件.        if (playlist->initCheck() != OK) { // 检查是否解析成功的.            ALOGE("failed to parse .m3u8 playlist");            return NULL;        }        return playlist; // 得到一个M3UParser对象.    }    //接下来看看M3UParser吧,这个一看名字就知道是在解析M3u8文件,然后解析m3u8根据Hls的版本解析.    status_t M3UParser::parse(const void *_data, size_t size) {        int32_t lineNo = 0;        sp<AMessage> itemMeta;        const char *data = (const char *)_data;        size_t offset = 0;        uint64_t segmentRangeOffset = 0;        while (offset < size) {            size_t offsetLF = offset;            while (offsetLF < size && data[offsetLF] != '\n') {                ++offsetLF;            }            AString line;            if (offsetLF > offset && data[offsetLF - 1] == '\r') {                line.setTo(&data[offset], offsetLF - offset - 1);            } else {                line.setTo(&data[offset], offsetLF - offset);            }            // ALOGI("#%s#", line.c_str());            if (line.empty()) {                offset = offsetLF + 1;                continue;            }            if (lineNo == 0 && line == "#EXTM3U") { //是否为M3u8协议头                mIsExtM3U = true;            }            if (mIsExtM3U) {                status_t err = OK;                if (line.startsWith("#EXT-X-TARGETDURATION")) {                    if (mIsVariantPlaylist) {                        return ERROR_MALFORMED;                    }                    err = parseMetaData(line, &mMeta, "target-duration");                } else if (line.startsWith("#EXT-X-MEDIA-SEQUENCE")) { //协议序列                    if (mIsVariantPlaylist) {                        return ERROR_MALFORMED;                    }                    err = parseMetaData(line, &mMeta, "media-sequence");                } else if (line.startsWith("#EXT-X-KEY")) { // url地址key                    if (mIsVariantPlaylist) {                        return ERROR_MALFORMED;                    }                    err = parseCipherInfo(line, &itemMeta, mBaseURI);                } else if (line.startsWith("#EXT-X-ENDLIST")) { // 结束tag                    mIsComplete = true;                } else if (line.startsWith("#EXT-X-PLAYLIST-TYPE:EVENT")) {                    mIsEvent = true;                } else if (line.startsWith("#EXTINF")) { // 每一个item的开头                    if (mIsVariantPlaylist) {                        return ERROR_MALFORMED;                    }                    err = parseMetaDataDuration(line, &itemMeta, "durationUs");                } else if (line.startsWith("#EXT-X-DISCONTINUITY")) {                    if (mIsVariantPlaylist) {                        return ERROR_MALFORMED;                    }                    if (itemMeta == NULL) {                        itemMeta = new AMessage;                    }                    itemMeta->setInt32("discontinuity", true);                    ++mDiscontinuityCount;                } else if (line.startsWith("#EXT-X-STREAM-INF")) {                    if (mMeta != NULL) {                        return ERROR_MALFORMED;                    }                    mIsVariantPlaylist = true;                    err = parseStreamInf(line, &itemMeta);                } else if (line.startsWith("#EXT-X-BYTERANGE")) {                    if (mIsVariantPlaylist) {                        return ERROR_MALFORMED;                    }                    uint64_t length, offset;                    err = parseByteRange(line, segmentRangeOffset, &length, &offset);                    if (err == OK) {                        if (itemMeta == NULL) {                            itemMeta = new AMessage;                        }                        itemMeta->setInt64("range-offset", offset);                        itemMeta->setInt64("range-length", length);                        segmentRangeOffset = offset + length;                    }                } else if (line.startsWith("#EXT-X-MEDIA")) {                    err = parseMedia(line);                } else if (line.startsWith("#EXT-X-DISCONTINUITY-SEQUENCE")) {                    if (mIsVariantPlaylist) {                        return ERROR_MALFORMED;                    }                    size_t seq;                    err = parseDiscontinuitySequence(line, &seq);                    if (err == OK) {                        mDiscontinuitySeq = seq;                    }                }                if (err != OK) {                    return err;                }            }            if (!line.startsWith("#")) {                if (!mIsVariantPlaylist) {                    int64_t durationUs;                    if (itemMeta == NULL                            || !itemMeta->findInt64("durationUs", &durationUs)) {                        return ERROR_MALFORMED;                    }                    itemMeta->setInt32("discontinuity-sequence",                            mDiscontinuitySeq + mDiscontinuityCount);                }                mItems.push();                Item *item = &mItems.editItemAt(mItems.size() - 1);                CHECK(MakeURL(mBaseURI.c_str(), line.c_str(), &item->mURI));                item->mMeta = itemMeta;                itemMeta.clear();            }            offset = offsetLF + 1;            ++lineNo;        }        // error checking of all fields that's required to appear once        // (currently only checking "target-duration"), and        // initialization of playlist properties (eg. mTargetDurationUs)        if (!mIsVariantPlaylist) {            int32_t targetDurationSecs;            if (mMeta == NULL || !mMeta->findInt32(                    "target-duration", &targetDurationSecs)) {                ALOGE("Media playlist missing #EXT-X-TARGETDURATION");                return ERROR_MALFORMED;            }            mTargetDurationUs = targetDurationSecs * 1000000ll;            mFirstSeqNumber = 0;            if (mMeta != NULL) {                mMeta->findInt32("media-sequence", &mFirstSeqNumber);            }            mLastSeqNumber = mFirstSeqNumber + mItems.size() - 1;        }        return OK;    }   /*好了以上大致流程看完了,留下一个问题,我们在播放ts的时候什么时候去下载下一个ts呢.?*/
    void PlaylistFetcher::DownloadState::saveState( // 存储一些状态            AString &uri,            sp<AMessage> &itemMeta,            sp<ABuffer> &buffer,            sp<ABuffer> &tsBuffer,            int32_t &firstSeqNumberInPlaylist,            int32_t &lastSeqNumberInPlaylist) {        mHasSavedState = true;        mUri = uri; // 当前URi地址        mItemMeta = itemMeta; // 当前播放的数据        mBuffer = buffer; // 当前的Buffer        mTsBuffer = tsBuffer; // 当前ts的Buffer        mFirstSeqNumberInPlaylist = firstSeqNumberInPlaylist; // 主要是看到有2个序列        mLastSeqNumberInPlaylist = lastSeqNumberInPlaylist;    }

上面看到了留着一个问题,就是什么时候去下载下一个ts呢.我们要去看看HttpDownLoader?之前也说就只有几个方法,所以肯定不在这儿.真正实现其实是在PlayListFetcher

    void PlaylistFetcher::onMonitorQueue() {        // in the middle of an unfinished download, delay        // playlist refresh as it'll change seq numbers        if (!mDownloadState->hasSavedState()) {            refreshPlaylist();        }        int64_t targetDurationUs = kMinBufferedDurationUs;        if (mPlaylist != NULL) {            targetDurationUs = mPlaylist->getTargetDuration();        }        int64_t bufferedDurationUs = 0ll;        status_t finalResult = OK;        if (mStreamTypeMask == LiveSession::STREAMTYPE_SUBTITLES) {            sp<AnotherPacketSource> packetSource =                mPacketSources.valueFor(LiveSession::STREAMTYPE_SUBTITLES);            bufferedDurationUs =                    packetSource->getBufferedDurationUs(&finalResult);        } else {            // Use min stream duration, but ignore streams that never have any packet            // enqueued to prevent us from waiting on a non-existent stream;            // when we cannot make out from the manifest what streams are included in            // a playlist we might assume extra streams.            bufferedDurationUs = -1ll;            for (size_t i = 0; i < mPacketSources.size(); ++i) {                if ((mStreamTypeMask & mPacketSources.keyAt(i)) == 0                        || mPacketSources[i]->getLatestEnqueuedMeta() == NULL) {                    continue;                }                int64_t bufferedStreamDurationUs =                    mPacketSources.valueAt(i)->getBufferedDurationUs(&finalResult);                FSLOGV(mPacketSources.keyAt(i), "buffered %lld", (long long)bufferedStreamDurationUs);                if (bufferedDurationUs == -1ll                     || bufferedStreamDurationUs < bufferedDurationUs) {                    bufferedDurationUs = bufferedStreamDurationUs;                }            }            if (bufferedDurationUs == -1ll) {                bufferedDurationUs = 0ll;            }        }        if (finalResult == OK && bufferedDurationUs < kMinBufferedDurationUs) {            FLOGV("monitoring, buffered=%lld < %lld",                    (long long)bufferedDurationUs, (long long)kMinBufferedDurationUs);            // delay the next download slightly; hopefully this gives other concurrent fetchers            // a better chance to run.            // onDownloadNext(); //这儿文档也说的很明白了.其实就是handler机制,进行通信,所以主要what就是kWhatDownloadNext,我们去搜一下kWhatDownloadNext吧.            sp<AMessage> msg = new AMessage(kWhatDownloadNext, this);            msg->setInt32("generation", mMonitorQueueGeneration);            msg->post(1000l);        } else {            // We'd like to maintain buffering above durationToBufferUs, so try            // again when buffer just about to go below durationToBufferUs            // (or after targetDurationUs / 2, whichever is smaller).            int64_t delayUs = bufferedDurationUs - kMinBufferedDurationUs + 1000000ll;            if (delayUs > targetDurationUs / 2) {                delayUs = targetDurationUs / 2;            }            FLOGV("pausing for %lld, buffered=%lld > %lld",                    (long long)delayUs,                    (long long)bufferedDurationUs,                    (long long)kMinBufferedDurationUs);            postMonitorQueue(delayUs);        }    }    //搜到了.kWhatDownloadNext    void PlaylistFetcher::onMessageReceived(const sp<AMessage> &msg) {        switch (msg->what()) {            case kWhatStart:            {                status_t err = onStart(msg);                sp<AMessage> notify = mNotify->dup();                notify->setInt32("what", kWhatStarted);                notify->setInt32("err", err);                notify->post();                break;            }            case kWhatPause:            {                onPause();                sp<AMessage> notify = mNotify->dup();                notify->setInt32("what", kWhatPaused);                notify->setInt32("seekMode",                        mDownloadState->hasSavedState()                        ? LiveSession::kSeekModeNextSample                        : LiveSession::kSeekModeNextSegment);                notify->post();                break;            }            case kWhatStop:            {                onStop(msg);                sp<AMessage> notify = mNotify->dup();                notify->setInt32("what", kWhatStopped);                notify->post();                break;            }            case kWhatFetchPlaylist:            {                bool unchanged;                sp<M3UParser> playlist = mHTTPDownloader->fetchPlaylist(                        mURI.c_str(), NULL /* curPlaylistHash */, &unchanged);                sp<AMessage> notify = mNotify->dup();                notify->setInt32("what", kWhatPlaylistFetched);                notify->setObject("playlist", playlist);                notify->post();                break;            }            case kWhatMonitorQueue:            case kWhatDownloadNext: // 看到了吧,下载下一个,其中有一个错误的延时1s重新下载的机制.,最有才会放弃            {                int32_t generation;                CHECK(msg->findInt32("generation", &generation));                if (generation != mMonitorQueueGeneration) {                    // Stale event                    break;                }                if (msg->what() == kWhatMonitorQueue) { // retry机制                    onMonitorQueue();                } else { //  否则下载下一个.                    onDownloadNext();                }                break;            }            case kWhatResumeUntil:            {                onResumeUntil(msg);                break;            }            default:                TRESPASS();        }    }

以上都是纯手打记录下,等空闲了,把代码和UML图都贴出来更加仔细的分析.因为最近项目太忙了,担心忘记了.还有一些底层一些工作流程也会陆续记录下.坚持.!!!

随后会把边看边缓存ts这个实现方案发出来,目前都没有找到好的解决方案.求大神指导.

2 0
原创粉丝点击