主頁 > 移動端開發 > Android熱修復及插件化原理

Android熱修復及插件化原理

2021-01-27 13:06:59 移動端開發

1.前言

熱修復一直是這幾年來很熱門的話題,主流方案大致有兩種,一種是微信Tinker的dex檔案替換,另一種是阿里的Native層的方法替換,這里重點介紹Tinker的大致原理,

2.類加載機制

介紹Tinker原理之前,我們先來回顧一下類加載機制,

我們編譯好的class檔案,需要先加載到虛擬機然后才會執行,這個程序是通過ClassLoader來完成的,

在這里插入圖片描述

雙親委派模型:

  • 1.加載某個類的時候,這個類加載器不會自己立刻去加載,它會委托給父類去加載
  • 2.如果這個父類還存在父類加載器,則進一步委托,直到最頂層的類加載器
  • 3.如果父類加載器可以完成加載任務,就成功回傳,否則就再委派給子類加載器
  • 4.如果都未加載成功就拋出ClassNotFoundException

作用:
1.避免類的重復加載,
比如有兩個類加載器,他們都要加載同一個類,這時候如果不是委托而是自己加載自己的,則會將類重復加載到方法區,
2.避免核心類被修改,
比如我們在自定義一個 java.lang.String 類,執行的時候會報錯,因為 String 是 java.lang 包下的類,應該由啟動類加載器加載,

JVM并不會一開始就加載所有的類,它是當你使用到的時候才會去通知類加載器去加載,

3.Android類加載

當我們new一個類時,首先是Android的虛擬機(Dalvik/ART虛擬機)通過ClassLoader去加載dex檔案到記憶體,
Android中的ClassLoader主要是PathClassLoader和DexClassLoader,這兩者都繼承自BaseDexClassLoader,它們都可以理解成應用類加載器,

PathClassLoader和DexClassLoader的區別:

  • PathClassLoader只能指定加載apk包路徑,不能指定dex檔案解壓路徑,該路徑是寫死的在/data/dalvik-cache/路徑下,所以只能用于加載已安裝的apk,

  • DexClassLoader可以指定apk包路徑和dex檔案解壓路徑(加載jar、apk、dex檔案)

當ClassLoader加載類時,會呼叫它的findclass方法去查找該類,
下方是BaseDexClassLoader的findClass方法實作:

public class BaseDexClassLoader extends ClassLoader {
...

    @UnsupportedAppUsage
    private final DexPathList pathList;

    ...

     @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 首先檢查該類是否存在shared libraries中.
        if (sharedLibraryLoaders != null) {
            for (ClassLoader loader : sharedLibraryLoaders) {
                try {
                    return loader.loadClass(name);
                } catch (ClassNotFoundException ignored) {
                }
            }
        }
        //再呼叫pathList.findClass去查找該類,結果為null則拋出錯誤,
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        Class c = pathList.findClass(name, suppressedExceptions);
        if (c == null) {
            ClassNotFoundException cnfe = new ClassNotFoundException(
                    "Didn't find class \"" + name + "\" on path: " + pathList);
            for (Throwable t : suppressedExceptions) {
                cnfe.addSuppressed(t);
            }
            throw cnfe;
        }
        return c;
    }
}

接下來我們再來看看DexPathList的findClass實作:

 public DexPathList(ClassLoader definingContext, String librarySearchPath) {
...
    /**
     * List of dex/resource (class path) elements.
     * 存放dex檔案的一個陣列
     */
    @UnsupportedAppUsage
    private Element[] dexElements;

...

    public Class<?> findClass(String name, List<Throwable> suppressed) {
     	 //遍歷Element陣列,去查尋對應的類,找到后就立刻回傳了
        for (Element element : dexElements) {
            Class<?> clazz = element.findClass(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }

        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }
...
}

4.Tinker原理

  • 1.使用DexClassLoader加載補丁包的dex檔案
  • 2.通過反射獲取DexClassLoader類的pathList,再次通過反射獲得dexElements陣列,
  • 3.獲取加載應用類的PathClassLoader,同樣通過反射獲取它的dexElements陣列,
  • 4.合并兩個dexElements陣列,且將補丁包的dex檔案放在前面,
    根據類加載機制,一個類只會被加載一次,DexPathList.findClass方法中是順序遍歷陣列,所以將補丁的dex檔案放在前面,這樣bug修復類會被優先加載,而原來的bug類不會被加載,達到了替換bug類的功能(補丁包中的修復類名、包名要和bug類相同)
  • 5.再次通過反射將合并后的dexElements陣列賦值給PathClassLoader.dexElements屬性,
    加載類時,Dalvik/ART虛擬機會通過PathClassLoader去查找已安裝的apk檔案中的類,

Ok,這樣就替換成功了,重啟App,再呼叫原來的bug類,將會優先使用補丁包中的修復類,
為什么要重啟:因為雙親委派模型,一個類只會被ClassLoader加載一次,且加載過后的類不能卸載,

代碼實作

接下來我們動手擼一個乞丐版的Tinker,
首先我們寫一個bug類,

package com.baima.plugin;

class BugClass {
    public String getTitle(){
        return "這是個Bug";
    }
}

接著我們新建一個module來生成補丁包apk,
在這里插入圖片描述

創建bug修復類,注意包名類名要一樣,

package com.baima.plugin;

class BugClass {
    public String getTitle(){
        return "修復成功";
    }
}

生成補丁apk,讓用戶下載這個補丁包,接下來就是加載這個apk檔案并替換了,


    public void loadDexAndInject(Context appContext, String dexPath, String dexOptPath) {

        try {
            // 加載應用程式dex的Loader
            PathClassLoader pathLoader = (PathClassLoader) appContext.getClassLoader();
            //dexPath 補丁dex檔案所在的路徑
            //dexOptPath 補丁dex檔案被寫入后存放的路徑
            DexClassLoader dexClassLoader = new DexClassLoader(dexPath, dexOptPath, null, pathLoader);
            //利用反射獲取DexClassLoader和PathClassLoader的pathList屬性
            Object dexPathList = getPathList(dexClassLoader);
            Object pathPathList = getPathList(pathLoader);
            //同樣用反射獲取DexClassLoader和PathClassLoader的dexElements屬性
            Object leftDexElements = getDexElements(dexPathList);
            Object rightDexElements = getDexElements(pathPathList);
            //合并兩個陣列,且補丁包的dex檔案在陣列的前面
            Object dexElements = combineArray(leftDexElements, rightDexElements);
            //反射將合并后的陣列賦值給PathClassLoader的pathList.dexElements
            Object pathList = getPathList(pathLoader);
            Class<?> pathClazz = pathList.getClass();
            Field declaredField = pathClazz.getDeclaredField("dexElements");
            declaredField.set看,ccessible(true);
            declaredField.set(pathList, dexElements);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static Object getPathList(Object classLoader) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
        Class<?> cl = Class.forName("dalvik.system.BaseDexClassLoader");
        Field field = cl.getDeclaredField("pathList");
        field.setAccessible(true);
        return field.get(classLoader);
    }


    private static Object getDexElements(Object pathList) throws NoSuchFieldException, IllegalAccessException {
        Class<?> cl = pathList.getClass();
        Field field = cl.getDeclaredField("dexElements");
        field.setAccessible(true);
        return field.get(pathList);
    }

    private static Object combineArray(Object arrayLeft, Object arrayRight) {
        Class<?> clazz = arrayLeft.getClass().getComponentType();
        int i = Array.getLength(arrayLeft);
        int j = Array.getLength(arrayRight);
        int k = i + j;
        Object result = Array.newInstance(clazz, k);// 創建一個型別為clazz,長度為k的新陣列
        System.arraycopy(arrayLeft, 0, result, 0, i);
        System.arraycopy(arrayRight, 0, result, i, j);
        return result;
    }

ok,乞丐版Tinker完成了,使用時先在Splash界面檢查是否有插件補丁,有的話執行替換,這時你再使用bug類會發現它已經被替換成補丁中的修復類了,

5.插件化

插件化開發模式,打包時是一個宿主apk+多個插件apk,
組件化開發模式,打包時是一個apk,里面分多個module,

優點:

  • 安裝的主apk包會小好多
  • 給開發者提供了業務功能擴展,并且不需要用戶進行更新
  • 在非主apk包中的功能出現BUG時,可以及時修復
  • 用戶不需要的功能,完全就不會出現在系統里面,減輕設備的負擔

需要掌握的知識:

  • 1.類加載機制
  • 2.四大組件啟動流程
  • 3.AIDL、Binder機制
  • 4.Hook、反射、代理

5.1 Activity啟動流程簡單介紹

在這里插入圖片描述

上圖是普通的Activity啟動流程,和根Activity啟動流程的區別是不用創建應用程式行程(Application Thread),

啟動程序:

  • 應用程式行程中的Activity向AMS請求創建普通Activity
  • AMS會對這個Activty的生命周期管和堆疊進行管理,校驗Activity等等
  • 如果Activity滿足AMS的校驗,AMS就會請求應用程式行程中的ActivityThread去創建并啟動普通Activity

他們之間的跨行程通信是通過Binder實作的,

5.2 插件化原理

通過上面介紹的熱修復,我們有辦法去加載插件apk里面的類,但是還沒有辦法去啟動插件中的Activity,因為如果要啟動一個Activity,那么這個Activity必須在AndroidManifest.xml中注冊,

這里介紹插件化的一種主流實作方式–Hook技術,

  • 1.宿主App預留占坑Activity
  • 2.使用classLoader加載dex檔案到記憶體
  • 3.先使用占坑Activity繞過AMS驗證,接著用插件Activity替換占坑的Activity,

步驟1、2這里就不在贅述了,2就是上面講到的熱修復技術,

5.2.1 繞開驗證

AMS是在SystemServer行程中,我們無法直接進行修改,只能在應用程式行程中做文章,介紹一個類–IActivityManager,IActivityManager它通過AIDL(內部使用的是Binder機制)和SystemServer行程的AMS通訊,所以IActivityManager很適合作為一個hook點,

Activity啟動時會呼叫IActivityManager.startActivity方法向AMS發出啟動請求,該方法引數包含一個Intent物件,它是原本要啟動的Activity的Intent,我們可以動態代理IActivityManager的startActivity方法,將該Intent換為占坑Activity的Intent,并將原來的Intent作為引數傳遞過去,以此達到欺騙AMS繞開驗證,

public class IActivityManagerProxy implements InvocationHandler {
    private Object mActivityManager;
    private static final String TAG = "IActivityManagerProxy";
    public IActivityManagerProxy(Object activityManager) {
        this.mActivityManager = activityManager;
    }
    @Override
    public Object invoke(Object o, Method method, Object[] args) throws Throwable {
        if ("startActivity".equals(method.getName())) {
            Intent intent = null;
            int index = 0;
            for (int i = 0; i < args.length; i++) {
                if (args[i] instanceof Intent) {
                    index = i;
                    break;
                }
            }
            intent = (Intent) args[index];
            Intent subIntent = new Intent();
            String packageName = "com.example.pluginactivity";
            subIntent.setClassName(packageName,packageName+".StubActivity");
            subIntent.putExtra(HookHelper.TARGET_INTENT, intent);
            args[index] = subIntent;
        }
        return method.invoke(mActivityManager, args);
    }
}

接下來就通過反射的方式,將ActivityManager中的IActivityManager替換成我們的代理物件,

  public void hookAMS() {
        try {
            Object defaultSingleton = null;
            if (Build.VERSION.SDK_INT >= 26) {
                Class<?> activityManagerClazz = Class.forName("android.app.ActivityManager");
                defaultSingleton = FieldUtil.getObjectField(activityManagerClazz, null, "IActivityManagerSingleton");
            } else {
                Class<?> activityManagerNativeClazz = Class.forName("android.app.ActivityManagerNative");
                defaultSingleton = FieldUtil.getObjectField(activityManagerNativeClazz, null, "gDefault");
            }
            Class<?> singletonClazz = Class.forName("android.util.Singleton");
            Field mInstanceField = FieldUtil.getField(singletonClazz, "mInstance");
            Object iActivityManager = mInstanceField.get(defaultSingleton);
            Class<?> iActivityManagerClazz = Class.forName("android.app.IActivityManager");
            Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class<?>[]{iActivityManagerClazz}, new IActivityManagerProxy(iActivityManager));
            mInstanceField.set(defaultSingleton, proxy);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

Note: 這里獲取IActivityManager實體會因為Android版本不同而不同,具體獲取方法就需要去看原始碼了解了,這里的代碼Android 8.0是可以運行的,

5.2.2還原插件Activity

ActivityThread啟動Activity的程序如下所示:

在這里插入圖片描述

ActivityThread會通過H在主執行緒中去啟動Activity,H類是ActivityThread的內部類并繼承自Handler,

private class H extends Handler {
public static final int LAUNCH_ACTIVITY         = 100;
public static final int PAUSE_ACTIVITY          = 101;
...
   public void handleMessage(Message msg) {
            if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
            switch (msg.what) {
                case LAUNCH_ACTIVITY: {
                    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
                    final ActivityClientRecord r = (ActivityClientRecord) msg.obj;

                    r.packageInfo = getPackageInfoNoCheck(
                            r.activityInfo.applicationInfo, r.compatInfo);
                    handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");
                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                } break;
                ...
              }
...
}

H中重寫的handleMessage方法會對LAUNCH_ACTIVITY型別的訊息進行處理,最侄訓呼叫Activity的onCreate方法,那么在哪進行替換呢?接著來看Handler的dispatchMessage方法:

public void dispatchMessage(Message msg) {
       if (msg.callback != null) {
           handleCallback(msg);
       } else {
           if (mCallback != null) {
               if (mCallback.handleMessage(msg)) {
                   return;
               }
           }
           handleMessage(msg);
       }
   }

Handler的dispatchMessage用于處理訊息,可以看到如果Handler的Callback型別的mCallback不為null,就會執行mCallback的handleMessage方法,因此,mCallback可以作為Hook點,我們可以用自定義的Callback來替換mCallback,自定義的Callback如下所示,

public class HCallback implements Handler.Callback{
    public static final int LAUNCH_ACTIVITY = 100;
    Handler mHandler;
    public HCallback(Handler handler) {
        mHandler = handler;
    }
    @Override
    public boolean handleMessage(Message msg) {
        if (msg.what == LAUNCH_ACTIVITY) {
            Object r = msg.obj;
            try {
                //得到訊息中的Intent(啟動占坑Activity的Intent)
                Intent intent = (Intent) FieldUtil.getField(r.getClass(), r, "intent");
                //得到此前保存起來的Intent(啟動插件Activity的Intent)
                Intent target = intent.getParcelableExtra(HookHelper.TARGET_INTENT);
                //將占坑Activity的Intent替換為插件Activity的Intent
                intent.setComponent(target.getComponent());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        mHandler.handleMessage(msg);
        return true;
    }
}

最后一步就是用反射將我們自定義的callBack設定給ActivityThread.sCurrentActivityThread.mH.mCallback


    public void hookHandler() {
        try {
            Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
            Object currentActivityThread = FieldUtil.getObjectField(activityThreadClass, null, "sCurrentActivityThread");
            Field mHField = FieldUtil.getField(activityThreadClass, "mH");
            Handler mH = (Handler) mHField.get(currentActivityThread);
            FieldUtil.setObjectField(Handler.class, mH, "mCallback", new HCallback(mH));
        } catch (Exception e) {
             e.printStackTrace();
        }
    }

其實要想啟動一個Activity到這步還沒有完,一個完整的Activity應該還需要布局檔案,而我們的宿主APP并不會包含插件的資源,

2.3 加載插件資源

2.3.1 Resources&AssetManager

android中的資源大致分為兩類:一類是res目錄下存在的可編譯的資源檔案,比如anim,string之類的,第二類是assets目錄下存放的原始資源檔案,因為Apk編譯的時候不會編譯這些檔案,所以不能通過id來訪問,當然也不能通過絕對路徑來訪問,于是Android系統讓我們通過Resources的getAssets方法來獲取AssetManager,利用AssetManager來訪問這些檔案,

其實Resource的getString, getText等各種方法都是通過呼叫AssetManager的私有方法來完成的,
程序就是Resource通過resource.arsc(AAPT工具打包程序中生成的檔案)把ID轉換成資源檔案的名稱,然后交由AssetManager來加載檔案,

AssetManager里有個很重要的方法addAssetPath(String path)方法,App啟動的時候會把當前apk的路徑傳進去,然后AssetManager就能訪問這個路徑下的所有資源也就是宿主apk的資源了,我們可以通過hook這個方法將插件的path傳進去,得到的AssetManager就能同時訪問宿主和插件的所有資源了,

 public void hookAssets(Activity activity,String dexPath){
        try {
            AssetManager assetManager = activity.getResources().getAssets();
            Method addAssetPath = assetManager.getClass().getMethod("addAssetPath",String.class);
            addAssetPath.invoke(assetManager,dexPath);
            Resources mResources = new Resources(assetManager, activity.getResources().getDisplayMetrics(), activity.getResources().getConfiguration());
            //接下來我們要將宿主原有Resources替換成我們上面生成的Resources,
            FieldUtil.setObjectField(ContextWrapper.class,activity.getResources(),"mResources",mResources);
            
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

2.3.2 id沖突

新的問題又出現了,宿主apk和插件apk是兩個不同的apk,他們在編譯時都會產生自己的resources.arsc,即他們是兩個獨立的編譯程序,那么它們的resources.arsc中的資源id必定是有相同的情況,這樣我們上面生成的新Resources中就出現了資源id重復的情況,這樣在運行的時候使用資源id來獲取資源就會報錯,

怎么解決資源Id沖突的問題?這里介紹一下VirtualApk采用的方案,

修改aapt的產物,即編譯后期重新整理插件Apk的資源,編排ID,更新R檔案

VirtualApkhook了ProcessAndroidResourcestask,這個task是用來編譯Android資源的,VirtualApk拿到這個task的輸出結果,做了以下處理:

  • 1.根據編譯產生的R.txt檔案收集插件中所有的資源
  • 2.根據編譯產生的R.txt檔案收集宿主apk中的所有資源
  • 3.過濾插件資源:過濾掉在宿主中已經存在的資源
  • 4.重新設定插件資源的資源ID
  • 5.洗掉掉插件資源目錄下前面已經被過濾掉的資源
  • 6.重新編排插件resources.arsc檔案中插件資源ID為新設定的資源ID
  • 7.重新產生R.java檔案

大致原理是這樣的,但如何保證新的Id不會重復了,這里在介紹一下資源Id的組成,

在這里插入圖片描述

packageId: 前兩位是packageId,相當于一個命名空間,主要用來區分不同的包空間(不是不同的module),目前來看,在編譯app的時候,至少會遇到兩個包空間:android系統資源包和咱們自己的App資源包,大家可以觀察R.java檔案,可以看到部分是以0x01開頭的,部分是以0x7f開頭的,以0x01開頭的就是系統已經內置的資源id,以0x7f開頭的是咱們自己添加的app資源id,

typeId:typeId是指資源的型別id,我們知道android資源有animator、anim、color、drawable、layout,string等等,typeId就是拿來區分不同的資源型別,

entryId:entryId是指每一個資源在其所屬的資源型別中所出現的次序,注意,不同型別的資源的Entry ID有可能是相同的,但是由于它們的型別不同,我們仍然可以通過其資源ID來區別開來,

所以為了避免沖突,插件的資源id通常會采用0x02 - 0x7e之間的數值,

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

標籤:其他

上一篇:Android學習記錄(十七)

下一篇:1、【Vue上傳檔案】當介面Content-Type為multipart/form-data時,如何上傳檔案到后端

標籤雲
其他(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