編輯:關於Android編程
本文均屬自己閱讀源碼的點滴總結,轉賬請注明出處謝謝。
歡迎和大家交流。qq:1037701636 email: [email protected]
在前面的博文中提到,AwesomePlayer::onPrepareAsyncEvent()開始進行Codec解碼器組件的獲取以及創建,這裡和大家分享。
1.以解碼器實例作為切入點
status_t AwesomePlayer::initVideoDecoder(uint32_t flags) { ATRACE_CALL(); ...... ALOGV("initVideoDecoder flags=0x%x", flags); mVideoSource = OMXCodec::Create( mClient.interface(), mVideoTrack->getFormat(),//提取視頻流的格式, mClient:BpOMX false, // createEncoder mVideoTrack, NULL, flags, USE_SURFACE_ALLOC ? mNativeWindow : NULL);//創建一個解碼器mVideoSource if (mVideoSource != NULL) { int64_t durationUs; if (mVideoTrack->getFormat()->findInt64(kKeyDuration, &durationUs)) { Mutex::Autolock autoLock(mMiscStateLock); if (mDurationUs < 0 || durationUs > mDurationUs) { mDurationUs = durationUs; } } status_t err = mVideoSource->start();//啟動解碼器OMXCodec if (err != OK) { ALOGE("failed to start video source"); mVideoSource.clear(); return err; } } ...... }
這裡不得不先說明以下幾個成員變量的相關內容,方便後續的分析:
a. mClinet:OMXClient(繼承於)類對象。做為AwesomePlayer的成員變量,在這裡能找到他的一些蹤跡。
AwesomePlayer::AwesomePlayer() : mQueueStarted(false), mUIDValid(false), mTimeSource(NULL), mVideoRenderingStarted(false), mVideoRendererIsPreview(false), mAudioPlayer(NULL), mDisplayWidth(0), mDisplayHeight(0), mVideoScalingMode(NATIVE_WINDOW_SCALING_MODE_SCALE_TO_WINDOW), mFlags(0), mExtractorFlags(0), mVideoBuffer(NULL), mDecryptHandle(NULL), mLastVideoTimeUs(-1), mTextDriver(NULL) { CHECK_EQ(mClient.connect(), (status_t)OK);//OMXClient,connect後維護一個mOMX:BpOMX
看到這裡進行了connect的處理,我們來看看其所完成的工作:
status_t OMXClient::connect() { spsm = defaultServiceManager(); sp binder = sm->getService(String16("media.player")); sp service = interface_cast (binder);//獲取MPS服務BpMediaPlayerService CHECK(service.get() != NULL); mOMX = service->getOMX();//獲取一個omx在本地的接口傳給videosource, BpOMX CHECK(mOMX.get() != NULL); if (!mOMX->livesLocally(NULL /* node */, getpid())) { ALOGI("Using client-side OMX mux."); mOMX = new MuxOMX(mOMX); } return OK; }
這裡進行線程間的Binder驅動處理。獲取一個OMX組件的接口到mOMX。我們不得不去看MediaPlayService端的getOMX的實現:
spMediaPlayerService::getOMX() { Mutex::Autolock autoLock(mLock); if (mOMX.get() == NULL) { mOMX = new OMX;//新建一個本地匿名的OMX } return mOMX; }
新構建了一個OMX組件類,該類繼承了BnOMX。使得Binder驅動返回後最終創建的是一個BpOMX這麼個匿名驅動。
返回BpOMX後在OMXClient側創建一個MuxOMX類mOMX,其作為OMXClient的成員變量而存在。
分析可知mClient.interace即是connect創建的mOMX組件。
b.
setVideoSource(extractor->getTrack(i));//設置視頻源mVideoTrack ;
setAudioSource(extractor->getTrack(i));//設置音頻源mAudioTrack;
mVideoTrack和mAudioTrack的做為創建的AwesomePlay的成員函數,其類型為MPEG4Source,繼承了MediaSource。
那麼mVideoTrack->getFormat(),是獲取對應視頻信息源的格式。
2.OMXCodec的創建
所有的解碼器無論是軟解還是硬解,都是掛載OMX下面,作為其的一個Component來使用。下面來看一個Codec的創建過程。
spOMXCodec::Create( const sp &omx, const sp &meta, bool createEncoder, const sp &source, const char *matchComponentName, uint32_t flags, const sp &nativeWindow) { int32_t requiresSecureBuffers; if (source->getFormat()->findInt32( kKeyRequiresSecureBuffers, &requiresSecureBuffers) && requiresSecureBuffers) { flags |= kIgnoreCodecSpecificData; flags |= kUseSecureInputBuffers; } const char *mime; bool success = meta->findCString(kKeyMIMEType, &mime); CHECK(success); Vector matchingCodecs; findMatchingCodecs( mime, createEncoder, matchComponentName, flags, &matchingCodecs);//尋找可用的解碼器如OMX.allwinner.video.decoder.avc if (matchingCodecs.isEmpty()) { ALOGV("No matching codecs! (mime: %s, createEncoder: %s, " "matchComponentName: %s, flags: 0x%x)", mime, createEncoder ? "true" : "false", matchComponentName, flags); return NULL; } sp observer = new OMXCodecObserver; IOMX::node_id node = 0; for (size_t i = 0; i < matchingCodecs.size(); ++i) { const char *componentNameBase = matchingCodecs[i].mName.string();//OMX組件的名字 uint32_t quirks = matchingCodecs[i].mQuirks; const char *componentName = componentNameBase; AString tmp; if (flags & kUseSecureInputBuffers) { tmp = componentNameBase; tmp.append(".secure"); componentName = tmp.c_str(); } if (createEncoder) {//軟解碼器createEncoder = 1; sp softwareCodec = InstantiateSoftwareEncoder(componentName, source, meta); if (softwareCodec != NULL) { ALOGV("Successfully allocated software codec '%s'", componentName); return softwareCodec; } } ALOGV("Attempting to allocate OMX node '%s'", componentName); if (!createEncoder && (quirks & kOutputBuffersAreUnreadable) && (flags & kClientNeedsFramebuffer)) { if (strncmp(componentName, "OMX.SEC.", 8)) { // For OMX.SEC.* decoders we can enable a special mode that // gives the client access to the framebuffer contents. ALOGW("Component '%s' does not give the client access to " "the framebuffer contents. Skipping.", componentName); continue; } } status_t err = omx->allocateNode(componentName, observer, &node);//請求mediaplayerservice創建一個節點,真正的解碼器所在 if (err == OK) { ALOGV("Successfully allocated OMX node '%s'", componentName); sp codec = new OMXCodec( omx, node, quirks, flags, createEncoder, mime, componentName, source, nativeWindow);//創建一個本地OMXCodec解碼器 observer->setCodec(codec);//將解碼器交給observer err = codec->configureCodec(meta); if (err == OK) { if (!strcmp("OMX.Nvidia.mpeg2v.decode", componentName)) { codec->mFlags |= kOnlySubmitOneInputBufferAtOneTime; } return codec; } ALOGV("Failed to configure codec '%s'", componentName); } } return NULL; }
2.1 查找平台支持的解碼器
bool success = meta->findCString(kKeyMIMEType, &mime);首先對傳入的視頻源track進行mime的提取。然後是繼續一個解碼器的查找,為當前視頻源的解碼所用:
findMatchingCodecs();//尋找可用的解碼器如OMX.allwinner.video.decoder.avc, 個人認為這是查找到所需要的解碼器的核心所在:
void OMXCodec::findMatchingCodecs( const char *mime, bool createEncoder, const char *matchComponentName, uint32_t flags, Vector*matchingCodecs) { matchingCodecs->clear(); const MediaCodecList *list = MediaCodecList::getInstance(); if (list == NULL) { return; } size_t index = 0; for (;;) { ssize_t matchIndex = list->findCodecByType(mime, createEncoder, index); if (matchIndex < 0) { break; } index = matchIndex + 1; const char *componentName = list->getCodecName(matchIndex);//獲取解碼器的名字 // If a specific codec is requested, skip the non-matching ones. if (matchComponentName && strcmp(componentName, matchComponentName)) { continue; } // When requesting software-only codecs, only push software codecs // When requesting hardware-only codecs, only push hardware codecs // When there is request neither for software-only nor for // hardware-only codecs, push all codecs if (((flags & kSoftwareCodecsOnly) && IsSoftwareCodec(componentName)) || ((flags & kHardwareCodecsOnly) && !IsSoftwareCodec(componentName)) || (!(flags & (kSoftwareCodecsOnly | kHardwareCodecsOnly)))) { ssize_t index = matchingCodecs->add(); CodecNameAndQuirks *entry = &matchingCodecs->editItemAt(index); entry->mName = String8(componentName); entry->mQuirks = getComponentQuirks(list, matchIndex); ALOGV("matching '%s' quirks 0x%08x", entry->mName.string(), entry->mQuirks); } } if (flags & kPreferSoftwareCodecs) { matchingCodecs->sort(CompareSoftwareCodecsFirst); } }
在這裡很熟悉的看到一個單列模式的創建MediaCodecList,一個多媒體解碼器列表的創建。在這裡我們很有必要看一下他的構造過程,因為這裡體現出android4.2.2的編解碼器維護和之前2.3等的不同。也是他更接近移動互聯的表現之一。
2.2 MediaCodecList的構建
const MediaCodecList *MediaCodecList::getInstance() { Mutex::Autolock autoLock(sInitMutex); if (sCodecList == NULL) { sCodecList = new MediaCodecList; } return sCodecList->initCheck() == OK ? sCodecList : NULL; } MediaCodecList::MediaCodecList()//單列模式的創建,解析xml完成當前mCodecInfos的維護,支持的編解碼器 : mInitCheck(NO_INIT) { FILE *file = fopen("/etc/media_codecs.xml", "r"); if (file == NULL) { ALOGW("unable to open media codecs configuration xml file."); return; } parseXMLFile(file);//解析xml文件提取其中支持的codec if (mInitCheck == OK) { // These are currently still used by the video editing suite. /**/ addMediaCodec(true /* encoder */, "AACEncoder", "audio/mp4a-latm");//硬解碼器 addMediaCodec( false /* encoder */, "OMX.google.raw.decoder", "audio/raw");//軟解碼器 } #if 0 for (size_t i = 0; i < mCodecInfos.size(); ++i) { const CodecInfo &info = mCodecInfos.itemAt(i); AString line = info.mName; line.append(" supports "); for (size_t j = 0; j < mTypes.size(); ++j) { uint32_t value = mTypes.valueAt(j); if (info.mTypes & (1ul << value)) { line.append(mTypes.keyAt(j)); line.append(" "); } } ALOGI("%s", line.c_str()); } #endif fclose(file); file = NULL; }
MediaCodecList的特點在於它對一個/etc/media_codecs.xml進行了解析,很容易看到xml讓人感覺到了互聯網的特色所在。我們來看看在全志A31下的這個配置文件部分內容,顯然放在最前面的是全志自己的軟硬件解碼器:
93 94 110 11195 96 97 98 99 100 101 102 103 104 105 106 107 108 109 112 121113 114 115 116 117 118 119 120
而這個文件的解析通過parseXMLFile來完成,最終解碼器屬性維護在了mCodecInfos,這其中xml文件的解析過程不是很熟悉,但核心是提取name和type這兩個字段後進行addMediaCodec的操作。
當然,我們也可以通過手動addMediaCodec來完成添加,其中ture代表的是編碼器,反之則為解碼器。
通過以上的手段,最終我們獲取到了硬件平台所支持的所有編解碼器的類型,也就是OMX下的各種Component組件。
2.3
有了這個所謂的編解碼器list,一切的變得更加的輕松,分別經過如下處理:
ssize_t matchIndex = list->findCodecByType(mime, createEncoder, index);
const char *componentName = list->getCodecName(matchIndex);//獲取解碼器的名字
componentName將成為後續的進一步處理的關鍵
3. 創建一個屬於OMX解碼器的Node節點
status_t err = omx->allocateNode(componentName, observer, &node);//請求mediaplayerservice創建一個節點,真正的解碼器所在
這裡的omx在傳入時已經分析過,變量類型為一個匿名Binder服務類BpOMX.回到在MediaPlayService的BnOMX處,估計核心的創建解碼器等還是要交給MPS來完成的。
status_t OMX::allocateNode( const char *name, const sp&observer, node_id *node) { Mutex::Autolock autoLock(mLock); *node = 0; OMXNodeInstance *instance = new OMXNodeInstance(this, observer);//新建一個OMXNodeInstance實例 OMX_COMPONENTTYPE *handle; OMX_ERRORTYPE err = mMaster->makeComponentInstance( name, &OMXNodeInstance::kCallbacks, instance, &handle);//創建一個組件,並獲取其操作句柄 if (err != OMX_ErrorNone) { ALOGV("FAILED to allocate omx component '%s'", name); instance->onGetHandleFailed(); return UNKNOWN_ERROR; } *node = makeNodeID(instance); mDispatchers.add(*node, new CallbackDispatcher(instance)); instance->setHandle(*node, handle); mLiveNodes.add(observer->asBinder(), instance); observer->asBinder()->linkToDeath(this); return OK; }
3.1 新建一個真正的OMXNodeInstance實例
3.2 mMaster->makeComponentInstance()真正獲取一個多下一層解碼器的控制權
這裡要和大家分析mMaster這個變量:
在獲取BpOMX時,在MPS側的getOMX裡實現了new OMX:
OMX::OMX() : mMaster(new OMXMaster),//新建一個mMaster mNodeCounter(0) { }
這裡看到MPS中的mOMX成員的子成員mMaster。
OMXMaster::OMXMaster() : mVendorLibHandle(NULL) { addVendorPlugin();//插入設備廠商的編解碼器插件libstagefrighthw addPlugin(new SoftOMXPlugin); }
看到這裡我會覺得OMXMaster是所有底層編解碼的管理者吧。因此組件的創建等都需要通過他來完成。
4.OMXMaster管理者的角色扮演
void OMXMaster::addVendorPlugin() { addPlugin("libstagefrighthw.so");//廠商的硬件編解碼器 }
看到這裡添加了所謂的設備廠商的插件,看到其是添加了一個libstagefrighthw.so庫。我們看看他是如何對這個so文件做處理的:
void OMXMaster::addPlugin(const char *libname) { mVendorLibHandle = dlopen(libname, RTLD_NOW); if (mVendorLibHandle == NULL) { return; } typedef OMXPluginBase *(*CreateOMXPluginFunc)(); CreateOMXPluginFunc createOMXPlugin = (CreateOMXPluginFunc)dlsym( mVendorLibHandle, "createOMXPlugin"); if (!createOMXPlugin) createOMXPlugin = (CreateOMXPluginFunc)dlsym( mVendorLibHandle, "_ZN7android15createOMXPluginEv"); if (createOMXPlugin) { addPlugin((*createOMXPlugin)());//將當前的lib插件加入到Component中去 } }
這裡做了典型的lib庫的操作,dlopen加載庫,dlsym獲取庫中的操作函數handle。*createOMXPLugin()是調用這個so庫中的函數,這個函數返回的是一個OMXPluginBase*的類型。
到這裡,我覺得和有必要和大家分析下OMX下的插件的基本結構了,因為只有滿足這個規定的結構,才能成為一個合理的OMX下的插件。而
5. 神奇的libstagefighthw.so
這個被稱之為平台廠商所設計的組件插件。在A31裡面我們可以看到他的源碼:/home/A31_Android4.2.2/android/hardware/aw/libstagefrighthw
我來看看之前調用該庫裡面的函數createOMXPlugin,獲取其入口地址後,直接調用後是創建了屬於AW的一個OMX插件
extern "C" OMXPluginBase* createOMXPlugin() { return new AwOMXPlugin;//創建一個解碼器插件 }
AwOMXPlugin::AwOMXPlugin() : mLibHandle(dlopen("libOmxCore.so", RTLD_NOW)), mInit(NULL), mDeinit(NULL), mComponentNameEnum(NULL), mGetHandle(NULL), mFreeHandle(NULL), mGetRolesOfComponentHandle(NULL) { if (mLibHandle != NULL) { mInit = (InitFunc)dlsym(mLibHandle, "OMX_Init"); mDeinit = (DeinitFunc)dlsym(mLibHandle, "OMX_Deinit"); mComponentNameEnum = (ComponentNameEnumFunc)dlsym(mLibHandle, "OMX_ComponentNameEnum"); mGetHandle = (GetHandleFunc)dlsym(mLibHandle, "OMX_GetHandle"); mFreeHandle = (FreeHandleFunc)dlsym(mLibHandle, "OMX_FreeHandle"); mGetRolesOfComponentHandle = (GetRolesOfComponentFunc)dlsym(mLibHandle, "OMX_GetRolesOfComponent"); (*mInit)(); } }
AwOMXPlugin類繼承了OMXPluginBase類,實現了其相關接口
這裡又打開了一個OmxCore這個lib,依次獲取了以上幾個函數的接口,將會被AwOMXPlugin來進一步使用。我們看到mInit()函數的執行,其他類似的函數源碼位於:/home/A31_Android4.2.2/android/hardware/aw/omxcore/src/aw_omx_core.c
6. OMX Plugin的維護
回到4中的處理流程,繼續分析OMXMaster::addPluginOMXPluginBase *plugin()函數的實現。
void OMXMaster::addPlugin(OMXPluginBase *plugin) { Mutex::Autolock autoLock(mLock); mPlugins.push_back(plugin); OMX_U32 index = 0; char name[128]; OMX_ERRORTYPE err; while ((err = plugin->enumerateComponents( name, sizeof(name), index++)) == OMX_ErrorNone) { String8 name8(name); if (mPluginByComponentName.indexOfKey(name8) >= 0) { ALOGE("A component of name '%s' already exists, ignoring this one.", name8.string()); continue; } mPluginByComponentName.add(name8, plugin);//增加stragefright裡面的插件 } if (err != OMX_ErrorNoMore) { ALOGE("OMX plugin failed w/ error 0x%08x after registering %d " "components", err, mPluginByComponentName.size()); } }
我們可以看到先查找當前的這個插件支持的組件,我們來看其在AwOMXPlugin中的實現。
OMX_ERRORTYPE AwOMXPlugin::enumerateComponents(OMX_STRING name, size_t size, OMX_U32 index) { if (mLibHandle == NULL) { return OMX_ErrorUndefined; } OMX_ERRORTYPE res = (*mComponentNameEnum)(name, size, index); if (res != OMX_ErrorNone) { return res; } return OMX_ErrorNone; }
看到這裡調用的是libOMXCore.so庫裡面的內容:
OMX_API OMX_ERRORTYPE OMX_APIENTRY OMX_ComponentNameEnum(OMX_OUT OMX_STRING componentName, OMX_IN OMX_U32 nameLen, OMX_IN OMX_U32 index) { OMX_ERRORTYPE eRet = OMX_ErrorNone; ALOGV("OMXCORE API - OMX_ComponentNameEnum %x %d %d\n",(unsigned) componentName, (unsigned)nameLen, (unsigned)index); if(index < SIZE_OF_CORE) { strlcpy(componentName, core[index].name, nameLen); } else { eRet = OMX_ErrorNoMore; } return eRet; }
這裡有一個Core的全局變量,其具體的結構如下
omx_core_cb_type core[] = { { "OMX.allwinner.video.decoder.avc", NULL, // Create instance function // Unique instance handle { NULL, NULL, NULL, NULL }, NULL, // Shared object library handle "libOmxVdec.so", { "video_decoder.avc" } }, .... }
通過以上函數的層層分析,提取到了core中的編解碼器name以及對應的Lib庫。
最終是獲取了各個name之後,通過mPluginByComponentName.add(name8, plugin),添加不同name的編解碼器component到mPluginByComponentName變量中,而這個變量的所有權歸mMaster維護。
到這裡我們基本分析完了OMX插件和codec的提取。還沒有完成針對特定的視頻源,構建出專門的組件。這樣我們得回歸到3中創建一個屬於OMX解碼器的Node節點處。
7.OMXMaster::makeComponentInstance的處理
OMX_ERRORTYPE OMXMaster::makeComponentInstance( const char *name, const OMX_CALLBACKTYPE *callbacks, OMX_PTR appData, OMX_COMPONENTTYPE **component) { Mutex::Autolock autoLock(mLock); *component = NULL; ssize_t index = mPluginByComponentName.indexOfKey(String8(name));//根據傳入的解碼器的名字,獲取組件索引 if (index < 0) { return OMX_ErrorInvalidComponentName; } OMXPluginBase *plugin = mPluginByComponentName.valueAt(index); OMX_ERRORTYPE err = plugin->makeComponentInstance(name, callbacks, appData, component);//創建硬件,完成初始化,返回handle到component if (err != OMX_ErrorNone) { return err; } mPluginByInstance.add(*component, plugin);//將插件維護起來 return err; }
這個name是之前我們查找到平台支持的codec後(通過解析media_codec.xml獲得)後,再根據這個name,找到這個index值,定位到這個解碼器所在的插件plugin.這裡比如name是OMX.allwinner.video.decoder.avc,這個獲取的組件就是libStragefighthw.so這個插件AwOXPlugin
OMX_ERRORTYPE AwOMXPlugin::makeComponentInstance(const char* name, const OMX_CALLBACKTYPE* callbacks, OMX_PTR appData, OMX_COMPONENTTYPE** component) { ALOGV("step 1."); if (mLibHandle == NULL) { return OMX_ErrorUndefined; } ALOGV("step 2."); return (*mGetHandle)(reinterpret_cast(component), const_cast (name), appData, const_cast (callbacks)); }
這裡的創建的一個組件,變成了handle,可見是獲取對這個組件的操作權。而mGetHandle對應的是OMX_GetHandle其位於libOmxCore.so庫之中。
OMX_API OMX_ERRORTYPE OMX_APIENTRY OMX_GetHandle(OMX_OUT OMX_HANDLETYPE* handle, OMX_IN OMX_STRING componentName, OMX_IN OMX_PTR appData, OMX_IN OMX_CALLBACKTYPE* callBacks) { OMX_ERRORTYPE eRet = OMX_ErrorNone; int cmp_index = -1; int hnd_index = -1; create_aw_omx_component fn_ptr = NULL; ALOGV("OMXCORE API : Get Handle %x %s %x\n",(unsigned) handle, componentName, (unsigned) appData); if(handle) { cmp_index = get_cmp_index(componentName); if(cmp_index >= 0) { ALOGV("getting fn pointer\n"); // dynamically load the so // ALOGV("core[cmp_index].fn_ptr: %x", core[cmp_index].fn_ptr); fn_ptr = omx_core_load_cmp_library(cmp_index); if(fn_ptr) { // Construct the component requested // Function returns the opaque handle void* pThis = (*fn_ptr)(); if(pThis) { void *hComp = NULL; hComp = aw_omx_create_component_wrapper((OMX_PTR)pThis); if((eRet = aw_omx_component_init(hComp, componentName)) != OMX_ErrorNone) { ALOGE("Component not created succesfully\n"); return eRet; } aw_omx_component_set_callbacks(hComp, callBacks, appData); hnd_index = set_comp_handle(componentName, hComp); if(hnd_index >= 0) { *handle = (OMX_HANDLETYPE) hComp; } else { ALOGE("OMX_GetHandle:NO free slot available to store Component Handle\n"); return OMX_ErrorInsufficientResources; } ....... return eRet; }
7.1 get_cmp_index()根據傳入的組件name獲取其在core中的索引
7.2 omx_core_load_cmp_library
static create_aw_omx_component omx_core_load_cmp_library(int idx) { create_aw_omx_component fn_ptr = NULL; pthread_mutex_lock(&g_mutex_core_info); if(core[idx].so_lib_handle == NULL) { ALOGV("Dynamically Loading the library : %s\n",core[idx].so_lib_name); core[idx].so_lib_handle = dlopen(core[idx].so_lib_name, RTLD_NOW); } if(core[idx].so_lib_handle) { if(core[idx].fn_ptr == NULL) { core[idx].fn_ptr = dlsym(core[idx].so_lib_handle, "get_omx_component_factory_fn"); .....
假設這裡獲取的是 "OMX.allwinner.video.decoder.avc"對應的組件,則其操作的lib庫為"libOmxVdec.so"。完成加載,獲取庫的handle。此外這裡返回的是一個函數get_omx_component_factory_fn的地址,用於後續的對這個解碼庫的操作。
7.3 接著看 void* pThis = (*fn_ptr)();
就是調用7.2中返回的get_omx_component_factory_fn函數入口。
void *get_omx_component_factory_fn(void) { return (new omx_vdec); }
這裡看到是新建了一個omx_vdec對象,如下所示:
class omx_vdec: public aw_omx_component { public: omx_vdec(); // constructor virtual ~omx_vdec(); // destructor
後續內容的主要是涉及相關OMX組件構造的標准構造,自己也要學習後才能消化,先和大家分享到這裡,最終會提煉出一個大的框架和模塊間的處理圖,方便更好的理解這個OMX組件的構建過程。
來圖了,重新整理畫了一個簡單的流程圖,內部含有A31的編解碼器插件:
1、寫在前面:雖然demo中程序框架已搭建完成,但是由於筆者時間原因,暫時只完成了核心部分:多線程下載的部分,其他數據庫、服務通知、暫停部分還未添加到項目中。2、相關知識
解決方案:在Fragment中申請權限,不要使用ActivityCompat.requestPermissions, 直接使用Fragment的requestPermis
Blur自從iOS系統引入了Blur效果,也就是所謂的毛玻璃、模糊化效果,磨砂效果,各大系統就開始競相模仿,這是一個怎樣的效果呢,我們現來看一些圖:這些就是典型的Blur
前言尋尋覓覓終於等到你,Material Design系列BottomBar開源庫你值得擁有。從我接觸android開發遇到tabhost,到radioGroup+Vie