主頁 > 移動端開發 > AndroidR Input子系統(8)InputChannel注冊,建立APP和InputDispatcher的連接

AndroidR Input子系統(8)InputChannel注冊,建立APP和InputDispatcher的連接

2020-12-21 12:10:09 移動端開發

上一篇文章分析到InputDispatcher將Input事件做了一些列處理之后,會將事件發送到APP行程,InputDispatcher和APP屬于兩個不同行程,他們之間是如何通信的呢?答案就是InputChannel,我們看看InputChannel的注釋:

/**
 * An input channel specifies the file descriptors used to send input events to
 * a window in another process.  It is Parcelable so that it can be sent
 * to the process that is to receive events.  Only one thread should be reading
 * from an InputChannel at a time.
 * @hide
 */
public final class InputChannel implements Parcelable {
	.....
}

說它是用來將輸入事件發送到其他行程的視窗的一個通道,并且它實作了Parcelable,可以將其發送到需要接收輸入事件的行程,從注釋從我們能清楚知道InputChannel的作用,

首先我們從APP的啟動說起,我們知道不管什么應用,啟動之后都會呼叫addView方法將自己的Window添加到WMS:

WindowManagerGlobal.addView

public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow, int userId) {
			...
			root = new ViewRootImpl(view.getContext(), display);
            ......
            root.setView(view, wparams, panelParentView);
            ......
			...
}

ViewRootImpl.setView

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
            int userId) {
			......
			InputChannel inputChannel = null;
                if ((mWindowAttributes.inputFeatures
                        & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
                    inputChannel = new InputChannel();
                }
               
                try {
                    ...
                    res = mWindowSession.addToDisplayAsUser(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(), userId, mTmpFrame,
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                            mAttachInfo.mDisplayCutout, inputChannel,
                            mTempInsets, mTempControls);
                           ...
                } catch (RemoteException e) {
                   
                    inputChannel = null;
                    
                }
			......
}

我們只關注和InputChanel相關的代碼,應用首次啟動,會創建一個InputChanel并將其傳到WMS,注意這里僅僅是new了一個InputChanel,并未對其成員變數填充任何資料,也就是說這只是個空的InputChanel,它的具體賦值是在WMS中完成的,

WindowManagerService.addWindow

addToDisplayAsUser會通過Binder調到WMS中:

public int addWindow(Session session, IWindow client, int seq,
            LayoutParams attrs, int viewVisibility, int displayId, Rect outFrame,
            Rect outContentInsets, Rect outStableInsets,
            DisplayCutout.ParcelableWrapper outDisplayCutout, InputChannel outInputChannel,
            InsetsState outInsetsState, InsetsSourceControl[] outActiveControls,
            int requestUserId) {
			......
			final boolean openInputChannels = (outInputChannel != null
                    && (attrs.inputFeatures & INPUT_FEATURE_NO_INPUT_CHANNEL) == 0);
            if  (openInputChannels) {
                win.openInputChannel(outInputChannel);
            }
			......

}		
}

WindowState.openInputChannel

void openInputChannel(InputChannel outInputChannel) {
        if (mInputChannel != null) {
            throw new IllegalStateException("Window already has an input channel.");
        }
        //視窗名稱
        String name = getName();
        //創建InputChannelPair
        InputChannel[] inputChannels = InputChannel.openInputChannelPair(name);
        //服務端InputChannel
        mInputChannel = inputChannels[0];
        //客戶端InputChannel
        mClientChannel = inputChannels[1];
        //將服務端InputChannel注冊到InputDispatcher
        mWmService.mInputManager.registerInputChannel(mInputChannel);
        //token唯一標識了接收input事件的視窗
        mInputWindowHandle.token = mInputChannel.getToken();
        if (outInputChannel != null) {
           /將客戶端InputChannel設定到ViewRootImpl的outInputChannel中
            mClientChannel.transferTo(outInputChannel);
            mClientChannel.dispose();
            mClientChannel = null;
        } else {
            
            mDeadWindowEventReceiver = new DeadWindowEventReceiver(mClientChannel);
        }
        mWmService.mInputToWindowMap.put(mInputWindowHandle.token, this);
    }

這個方法是重點,主要構造一對InputChannel,分為“server”和“client”,“server”最后會注冊到InputDispatcher,“client”會回傳給APP行程的ViewRootImpl

我們首先來看看openInputChannelPair

InputChannel.openInputChannelPair

public static InputChannel[] openInputChannelPair(String name) {
        if (name == null) {
            throw new IllegalArgumentException("name must not be null");
        }
        if (DEBUG) {
            Slog.d(TAG, "Opening input channel pair '" + name + "'");
        }
        return nativeOpenInputChannelPair(name);
    }

這里會調到native層:

android_view_InputChannel_nativeOpenInputChannelPair

static jobjectArray android_view_InputChannel_nativeOpenInputChannelPair(JNIEnv* env,
        jclass clazz, jstring nameObj) {
    ScopedUtfChars nameChars(env, nameObj);
    std::string name = nameChars.c_str();

    sp<InputChannel> serverChannel;
    sp<InputChannel> clientChannel;
    //構造一對native層InputChannel
    status_t result = InputChannel::openInputChannelPair(name, serverChannel, clientChannel);

    if (result) {
        std::string message = android::base::StringPrintf(
                "Could not open input channel pair : %s", strerror(-result));
        jniThrowRuntimeException(env, message.c_str());
        return nullptr;
    }
    //構造一個java層InputChannel型別陣列
    jobjectArray channelPair = env->NewObjectArray(2, gInputChannelClassInfo.clazz, nullptr);
    if (env->ExceptionCheck()) {
        return nullptr;
    }
    //將native層InputChannel轉換為java層InputChannel
    jobject serverChannelObj = android_view_InputChannel_createInputChannel(env, serverChannel);
    if (env->ExceptionCheck()) {
        return nullptr;
    }
   //將native層InputChannel轉換為java層InputChannel
    jobject clientChannelObj = android_view_InputChannel_createInputChannel(env, clientChannel);
    if (env->ExceptionCheck()) {
        return nullptr;
    }
    //將轉換的java層InputChannel存到前面構造的陣列
    env->SetObjectArrayElement(channelPair, 0, serverChannelObj);
    env->SetObjectArrayElement(channelPair, 1, clientChannelObj);
    //回傳給java層
    return channelPair;
}

這個函式很簡單,主要是通過openInputChannelPair構造了一對native層InputChannel,然后再根據其創建java層InputChannel,最后回傳給java層,

InputChannel::openInputChannelPair

status_t InputChannel::openInputChannelPair(const std::string& name,
        sp<InputChannel>& outServerChannel, sp<InputChannel>& outClientChannel) {
    int sockets[2];
    if (socketpair(AF_UNIX, SOCK_SEQPACKET, 0, sockets)) {
        ...
        //例外
        return result;
    }
    //32KB
    int bufferSize = SOCKET_BUFFER_SIZE;
    //設定socket發送和接識訓沖區大小為bufferSize 
    setsockopt(sockets[0], SOL_SOCKET, SO_SNDBUF, &bufferSize, sizeof(bufferSize));
    setsockopt(sockets[0], SOL_SOCKET, SO_RCVBUF, &bufferSize, sizeof(bufferSize));
    setsockopt(sockets[1], SOL_SOCKET, SO_SNDBUF, &bufferSize, sizeof(bufferSize));
    setsockopt(sockets[1], SOL_SOCKET, SO_RCVBUF, &bufferSize, sizeof(bufferSize));
    //創建BBinder,用于標識APP行程的視窗
    sp<IBinder> token = new BBinder();
    
    std::string serverChannelName = name + " (server)";
    
    android::base::unique_fd serverFd(sockets[0]);
    //server端InputChannel保存了server端socket的fd
    outServerChannel = InputChannel::create(serverChannelName, std::move(serverFd), token);

    std::string clientChannelName = name + " (client)";
    android::base::unique_fd clientFd(sockets[1]);
    //client端InputChannel保存了client端socket的fd
    outClientChannel = InputChannel::create(clientChannelName, std::move(clientFd), token);
    return OK;
}

這個函式中創建了一對"socket"和一對native層InputChannel,并將他們都區分為"server"端和“client”端,"server"端InputChannel保存了"server"端"socket"的fd,"client"端InputChannel保存了"client"端socket的fd,

再來看看InputChannel::create

sp<InputChannel> InputChannel::create(const std::string& name, android::base::unique_fd fd,
                                      sp<IBinder> token) {
    const int result = fcntl(fd, F_SETFL, O_NONBLOCK);
    if (result != 0) {
        LOG_ALWAYS_FATAL("channel '%s' ~ Could not make socket non-blocking: %s", name.c_str(),
                         strerror(errno));
        return nullptr;
    }
    return new InputChannel(name, std::move(fd), token);
}
//InputChannel建構式
InputChannel::InputChannel(const std::string& name, android::base::unique_fd fd, sp<IBinder> token)
      : mName(name), mFd(std::move(fd)), mToken(token) {
    if (DEBUG_CHANNEL_LIFECYCLE) {
        ALOGD("Input channel constructed: name='%s', fd=%d", mName.c_str(), mFd.get());
    }
}

這里并沒有做注冊"socket"之類的操作,我們暫且記住"socket"的fd保存在了InputChannel中,

一對native層InputChannel創建好了之后,接著通過android_view_InputChannel_createInputChannel函式創建出一對java層InputChannel,并放入陣列,回傳給java層,這里面代碼非常簡單,不必細說,

再回到WindowStateopenInputChannel方法,接著來看"server"端InputChannelInputDispatcher注冊的程序:

InputManagerService.registerInputChannel

 public void registerInputChannel(InputChannel inputChannel) {
        if (inputChannel == null) {
            throw new IllegalArgumentException("inputChannel must not be null.");
        }

        nativeRegisterInputChannel(mPtr, inputChannel);
    }

nativeRegisterInputChannel

static void nativeRegisterInputChannel(JNIEnv* env, jclass /* clazz */,
        jlong ptr, jobject inputChannelObj) {
    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
    //獲取native層InputChannel
    sp<InputChannel> inputChannel = android_view_InputChannel_getInputChannel(env,
            inputChannelObj);
    ...
    
    status_t status = im->registerInputChannel(env, inputChannel);

    if (status) {
        //例外情況
        return;
    }
    //給mDisposeCallback賦值為handleInputChannelDisposed
    android_view_InputChannel_setDisposeCallback(env, inputChannelObj,
            handleInputChannelDisposed, im);
}

這個函式我們重點關注注冊InputChannel

status_t NativeInputManager::registerInputChannel(JNIEnv* /* env */,
        const sp<InputChannel>& inputChannel) {
    ATRACE_CALL();
    return mInputManager->getDispatcher()->registerInputChannel(inputChannel);
}

這里直接調到InputDispatcher中去了:

InputDispatcher::registerInputChannel

status_t InputDispatcher::registerInputChannel(const sp<InputChannel>& inputChannel) {
#if DEBUG_REGISTRATION
    ALOGD("channel '%s' ~ registerInputChannel", inputChannel->getName().c_str());
#endif

    { // acquire lock
        std::scoped_lock _l(mLock);
        //如果已經存在Connection,則不必重復注冊
        sp<Connection> existingConnection = getConnectionLocked(inputChannel->getConnectionToken());
        if (existingConnection != nullptr) {
            ALOGW("Attempted to register already registered input channel '%s'",
                  inputChannel->getName().c_str());
            return BAD_VALUE;
        }
        //創建Connection,將“server”端inputChannel傳進去
        sp<Connection> connection = new Connection(inputChannel, false /*monitor*/, mIdGenerator);
        //server端inputChannel的"socket" fd
        int fd = inputChannel->getFd();
        //以"socket"的fd為key,connection為value,保存到map中
        mConnectionsByFd[fd] = connection;
        //創建inputChannel時,接收了一個BBinder的引數,就是其Token,用于標識APP行程的視窗
        //的唯一標識
        mInputChannelsByToken[inputChannel->getConnectionToken()] = inputChannel;
        //重點:將”server端“socket的fd添加到looper監聽
        mLooper->addFd(fd, 0, ALOOPER_EVENT_INPUT, handleReceiveCallback, this);
    } 
    //喚醒InputDispatcher
    mLooper->wake();
    return OK;
}

上面函式做的事情如下:

  1. 如果是首次注冊InputChannel,則會創建一個Connection連接,并以InputChannel的"socket"的fd為key,此連接為value,存盤到InputDispatchermConnectionsByFd中,InputChannel的token物件則保證了注冊InputChannel的視窗的唯一性,token為key,InputChannel為value保存在了InputDispatchermInputChannelsByToken中,
  2. 將”server端“socket的fd添加到InputDispatcher內部的looper進行監聽,待事件發生之后回呼其handleReceiveCallback函式,這里的事件發生即”client端“socket寫入了資料,

到此InputChannel”server端“的注冊程序已經完成,

我們再回到WindowStateopenInputChannel方法:

void openInputChannel(InputChannel outInputChannel) {
        if (mInputChannel != null) {
            throw new IllegalStateException("Window already has an input channel.");
        }
        //視窗名稱
        String name = getName();
        //創建InputChannelPair
        InputChannel[] inputChannels = InputChannel.openInputChannelPair(name);
        //服務端InputChannel
        mInputChannel = inputChannels[0];
        //客戶端InputChannel
        mClientChannel = inputChannels[1];
        //將服務端InputChannel注冊到InputDispatcher
        mWmService.mInputManager.registerInputChannel(mInputChannel);
        //token唯一標識了接收input事件的視窗
        mInputWindowHandle.token = mInputChannel.getToken();
        if (outInputChannel != null) {
           //將客戶端InputChannel設定到ViewRootImpl的outInputChannel中
            mClientChannel.transferTo(outInputChannel);
            mClientChannel.dispose();
            mClientChannel = null;
        } else {
            
            mDeadWindowEventReceiver = new DeadWindowEventReceiver(mClientChannel);
        }
        //以token為key,windowstate為value保存到WMS
        mWmService.mInputToWindowMap.put(mInputWindowHandle.token, this);
    }

服務端InputChannel注冊完成之后,客戶端mClientChannel 會通過transferTo方法設定到APP行程的ViewRootImpl的outInputChannel中,具體實作在native層,原理很簡單,就是將mClientChannel 對應的native層NativeInputChannel再保存到ViewRootImpl的outInputChannelmPtr變數中,不去細看了,

我們需要再回到ViewRootImpl中去看看客戶端InputChannel被保存到outInputChannel之后的后續處理:

```java
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
            int userId) {
			......
			InputChannel inputChannel = null;
                if ((mWindowAttributes.inputFeatures
                        & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
                    inputChannel = new InputChannel();
                }
               
                try {
                    ...
                    res = mWindowSession.addToDisplayAsUser(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(), userId, mTmpFrame,
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                            mAttachInfo.mDisplayCutout, inputChannel,
                            mTempInsets, mTempControls);
                           ...
                } catch (RemoteException e) {
                   
                    inputChannel = null;
                    
                }
			......
            if (inputChannel != null) {
                    ....
                    mInputEventReceiver = new WindowInputEventReceiver(inputChannel,
                            Looper.myLooper());
                }
                ......
}

后續會創建一個WindowInputEventReceiver物件,其構造方法接收客戶端的InputChannel和一個Looper物件,重點注意這里的Looper其實就是APP行程的UI執行緒的Looper,

final class WindowInputEventReceiver extends InputEventReceiver {
        public WindowInputEventReceiver(InputChannel inputChannel, Looper looper) {
            super(inputChannel, looper);
        }
   }

這里呼叫WindowInputEventReceiver父類的構造方法:

InputEventReceiver構造方法

public InputEventReceiver(InputChannel inputChannel, Looper looper) {
        if (inputChannel == null) {
            throw new IllegalArgumentException("inputChannel must not be null");
        }
        if (looper == null) {
            throw new IllegalArgumentException("looper must not be null");
        }

        mInputChannel = inputChannel;
        //UI執行緒的MessageQueue
        mMessageQueue = looper.getQueue();
        mReceiverPtr = nativeInit(new WeakReference<InputEventReceiver>(this),
                inputChannel, mMessageQueue);

        mCloseGuard.open("dispose");
    }

這里通過nativeInit完成初始化:

android_view_InputEventReceiver::nativeInit

static jlong nativeInit(JNIEnv* env, jclass clazz, jobject receiverWeak,
        jobject inputChannelObj, jobject messageQueueObj) {
    //java層InputChannel轉換得到native層InputChannel,client端
    sp<InputChannel> inputChannel = android_view_InputChannel_getInputChannel(env,
            inputChannelObj);
    if (inputChannel == nullptr) {
        jniThrowRuntimeException(env, "InputChannel is not initialized.");
        return 0;
    }
    //APP UI執行緒對應native層的MessageQueue
    sp<MessageQueue> messageQueue = android_os_MessageQueue_getMessageQueue(env, messageQueueObj);
    if (messageQueue == nullptr) {
        jniThrowRuntimeException(env, "MessageQueue is not initialized.");
        return 0;
    }
    //創建NativeInputEventReceiver,將inputChannel,messageQueue保存到其內部成員變數
    sp<NativeInputEventReceiver> receiver = new NativeInputEventReceiver(env,
            receiverWeak, inputChannel, messageQueue);
    status_t status = receiver->initialize();
    if (status) {
        String8 message;
        message.appendFormat("Failed to initialize input event receiver.  status=%d", status);
        jniThrowRuntimeException(env, message.string());
        return 0;
    }

    receiver->incStrong(gInputEventReceiverClassInfo.clazz); // retain a reference for the object
    return reinterpret_cast<jlong>(receiver.get());
}

此函式首先將java層傳遞下來的物件轉換為對應native層物件之后,創建了一個NativeInputEventReceiver,并呼叫其initialize函式進行初始化:

NativeInputEventReceiver::initialize

status_t NativeInputEventReceiver::initialize() {
    setFdEvents(ALOOPER_EVENT_INPUT);
    return OK;
}

NativeInputEventReceiver::setFdEvents

void NativeInputEventReceiver::setFdEvents(int events) {
    if (mFdEvents != events) {
        mFdEvents = events;
        //mInputConsumer就是保存到其內部的client端InputChannel,
        //fd指向client端InputChannel內部的client端"socket"的fd
        int fd = mInputConsumer.getChannel()->getFd();
        if (events) {
            //將client端"socket"的fd添加到Looper進行監聽
            mMessageQueue->getLooper()->addFd(fd, 0, events, this, nullptr);
        } else {
           //例外情況
            mMessageQueue->getLooper()->removeFd(fd);
        }
    }
}

此函式取出client端InputChannel的"socket"的fd添加到APP行程的UI執行緒的Looper進行監聽事件型別為ALOOPER_EVENT_INPUT,并在接收到事件之后("server端"socket寫入資料)回呼NativeInputEventReceiverhandleEvent函式,

到此“server端”和“client”端InputChannel的“socket”注冊已經分析完成,“server端”被注冊到InputDispatcher的Looper執行緒,“client”端被注冊到了APP的UI執行緒,這樣他們就可以完成通信了,

有了這篇文章基礎,我們在AndroidR Input子系統(7)InputDispatcher執行緒分發輸入事件最后講到InputDispatcher會通過:

nWrite = ::send(mFd.get(), &cleanMsg, msgLength, MSG_DONTWAIT | MSG_NOSIGNAL);

向“server”端socket寫入資料的方式將輸入事件發送到APP UI執行緒就非常好理解了,

我們對整個程序進行一個總結:

  1. 首先當一個APP啟動時,會將自己的Window添加到WMS,并傳遞一個空InputChannel過去,
  2. WMS端,通過openInputChannel方法會創建一對InputChannel,是在native層完成的,這對InputChannel被分為“client”端和“server”端,其內部又會創建一對socket,和這對InputChannel一一對應,
  3. “server”端InputChannel會被注冊到InputDispatcher中去,注冊的原理就是將InputChannel內部的socket添加到其Looper進行監聽,注冊程序中還會創建一個Connection物件,Connection用來描述InputDispatcher與此次注冊InputChannel的視窗的連接,
  4. "client"端InputChannel會被設定到APP行程中,接著通過InputEventReceiver注冊到APP UI執行緒,同樣是將InputChannel內部的socket添加到UI執行緒的Looper進行監聽,
  5. 對于InputDispatcher執行緒,在接收到"client"端socket的訊息時會回呼其handleReceiveCallback函式,對于APP UI執行緒,在接收到"server"端socket的訊息時會回呼InputEventReceiver對應的native層物件NativeInputEventReceiverhandleEvent函式,

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/238095.html

標籤:其他

上一篇:二級橫向listview

下一篇:使用android studio 打開專案遇到的問題

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more