主頁 > 移動端開發 > Android熱更新詳解

Android熱更新詳解

2021-09-22 16:16:48 移動端開發

一 前言介紹

正好最近又看到熱更新,對以前Android 熱修復核心原理:ClassLoader類加載機制做了點補充,

從16年開始開始,熱修復技術開始在安卓界流行,它以classloader類加載機制為核心,可以不發布新版本就修復線上 bug ,讓線上版本有能力去進行全量或者增量更新,

常見的思路有兩種:

  1. 類加載方案,即 dex 插樁,該方案以騰訊系為主,包括微信的 Tinker、餓了么的 Amigo;

  2. 底層替換,即修改替換 ArtMethod,方案以阿里系的 AndFix 等為主;

本文主要介紹第一種方案,

1.1 ART 和 Dalvik

  1. Dex :全稱為Dalvik Executable Format,由很多 .class 檔案處理壓縮后的產物,最終可以在 Android 運行時環境執行,它適合于記憶體和處理器速度有限的系統,

  2. Dalvik:Google設計的Android平臺的Java虛擬機,支持轉換為.dex格式的Java程式運行,DVM默認使用CMS垃圾回收器,

  3. ART:Android Runtime,于Android 4.4 引入,在 Android 5.0 及更高版本作為默認的 Android 運行時,ART做出的具體改進可看安卓官方檔案介紹:運行時:Android Runtime (ART) 和 Dalvik,ART 和 Dalvik 都是運行 Dex 位元組碼的兼容運行時,因此 ART 向下兼容Dalvik 開發的應用,

  4. AOT:ART在應用安裝的時候預編譯位元組碼到機器語言,這一機制叫Ahead-Of-Time(AOT)預編譯,執行該操作后,應用程式安裝會變慢,但是執行將更有效率,啟動更快,

1.2 dexopt與dexaot

  1. dexopt:Dalvik虛擬機加載dex檔案時,會對 dex 檔案進行驗證和優化,得到odex(Optimized dex) 檔案,odex檔案只是對dex檔案使用了一些優化操作碼,

  2. dex2oat:dex或者odex檔案經過 AOT 預編譯,即得到OAT(實際上是ELF檔案)可執行檔案(機器碼),(相比做過odex優化,未做過優化的dex轉換成OAT要花費更長的時間)

1.3 ART 和 Dalvik 對比

  1. 在Dalvik下,應用運行需要解釋執行,常用熱點代碼通過即時編譯器(JIT)將位元組碼轉換為機器碼,運行效率低,而在ART 環境中,應用在安裝時,位元組碼預編譯(AOT)成機器碼,安裝慢了,但運行效率會提高,

  2. ART占用空間比Dalvik大(位元組碼變為機器碼), “空間換時間",

  3. 預編譯也可以明顯改善電池續航,因為應用程式每次運行時不用重復編譯了,從而減少了 CPU 的使用頻率,降低了能耗,

二 ClassLoader

2.1 Android運行流程

Android程式編譯的時候,會將.java檔案編譯時.class檔案,然后將.class檔案打包為.dex檔案,Android程式運行時,Android的Dalvik/ART虛擬機就會加載.dex檔案從中獲得.class檔案到記憶體中來使用,

2.2 類加載工具ClassLoader

任何 Java 程式都是由一個或多個 class 檔案組成,在程式運行時,需要通過 Java 的類加載機制將 class 檔案加載到 JVM 中才可以使用,Java程式啟動時不會一次性加載所有類,而是先把保證運行的基礎類加載到jvm,其它類要用時再加載,這樣的好處是節省了記憶體的開銷,用時再加載這也是java動態性的一種體現,

這些類的加載就是通過ClassLoader來的,每個 Class 物件的內部都有一個 classLoader 欄位來標識自己是由哪個 ClassLoader 加載的,安卓的ClassLoader小改了java的ClassLoader

class Class<T> {
  ...
  private transient ClassLoader classLoader;
  ...
}

常見的Android類加載器有如下四種:

  1. BootClassLoader :加載Android Framework層中的class位元組碼檔案(類似java的BootstrapClassLoader)
  2. PathClassLoader :只能加載已經安裝到Android系統中的Apk的 class 位元組碼檔案,是Android默認使用的類加載器;(類似java的 AppClassLoader )
  3. DexClassLoader :可以加載加載制定目錄的dex/jar/apk/zip檔案檔案(類似java中的 Custom ClassLoader ),比 PathClassLoader 更靈活,是實作熱修復的重點;
  4. BaseDexClassLoader : PathClassLoader 和 DexClassLoader 的父類
Log.e(TAG, "Activity.class 由:" + Activity.class.getClassLoader() +" 加載");
Log.e(TAG, "MainActivity.class 由:" + getClassLoader() +" 加載");

//輸出:
Activity.class 由:java.lang.BootClassLoader@b1202a1 加載
MainActivity.class 由:dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.enjoy.enjoyfix-1/base.apk"],nativeLibraryDirectories=[/data/app/com.enjoy.enjoyfix-1/lib/x86, /system/lib, /vendor/lib]]] 加載

它們之間的關系如下:

public class DexClassLoader extends BaseDexClassLoader {
	
    public DexClassLoader(String dexPath, String optimizedDirectory,
		String librarySearchPath, ClassLoader parent) {
		super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
	}
}
 
public class PathClassLoader extends BaseDexClassLoader {
 
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }
 
	public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent){
		 super(dexPath, null, librarySearchPath, parent);
	}
}

PathClassLoader 與 DexClassLoader 在建構式中都呼叫了父類的建構式,兩者唯一的區別在于:DexClassLoader多傳了一個optimizedDirectory引數,并且會將其創建為File物件傳給super,而PathClassLoader則直接給到null,因此兩者都可以加載指定的dex,以及jar、zip、apk中的classes.dex

PathClassLoader pathClassLoader = new PathClassLoader("/sdcard/xx.dex", getClassLoader());
 
File dexOutputDir = context.getCodeCacheDir();
DexClassLoader dexClassLoader = new DexClassLoader("/sdcard/xx.dex",dexOutputDir.getAbsolutePath(), null,getClassLoader());

其實optimizedDirectory引數就是dexopt的產出目錄(odex),DexClassLoader不僅僅可以加載 dex檔案,還可以加載jar、apk、zip檔案中的dex,而jar、apk、zip其實就是一些壓縮格式,要拿到壓縮包里面的dex檔案就需要解壓,所以,DexClassLoader在呼叫父類建構式時會指定一個解壓的目錄,那PathClassLoader創建時,這個目錄為null,就意味著不進行dexopt?并不是,optimizedDirectory為null時的默認路徑為:/data/dalvik-cache,

在API 26原始碼中,將DexClassLoader的optimizedDirectory標記為了 deprecated 棄用,實作也變得和PathClassLoader一摸一樣了:

public DexClassLoader(String dexPath, String optimizedDirectory,
          String librarySearchPath, ClassLoader parent) {
  super(dexPath, null, librarySearchPath, parent);
}

2.3 雙親委托機制

可以看到創建ClassLoader需要接收一個ClassLoader parent引數,這個parent的目的就在于實作類加載的雙親委托,即:某個類加載器在接到加載類的請求時,首先將加載任務委托給父類加載器,依次遞回,如果父類加載器可以完成類加載任務,就成功回傳;只有父類加載器無法完成此加載任務時,才自己去加載,

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
	
    // 檢查class是否有被加載  
	Class c = findLoadedClass(name);
	if (c == null) {
		long t0 = System.nanoTime();
		try {
			if (parent != null) {
                //如果parent不為null,則呼叫parent的loadClass進行加載  
				c = parent.loadClass(name, false);
            } else {
                //parent為null,則呼叫BootClassLoader進行加載  
                c = findBootstrapClassOrNull(name);
            }
        } catch (ClassNotFoundException e) {
		
        }
 
        if (c == null) {
            // 如果都找不到就自己查找
			long t1 = System.nanoTime();
            c = findClass(name);
        }
	}
	return c;
}

值得注意的是:c = findBootstrapClassOrNull(name);按照方法名理解,應該是當parent為null時候,也能夠加載BootClassLoader加載的類,但是實際上,Android當中的實作為:(Java不同)

private Class findBootstrapClassOrNull(String name)
{
  return null;
}

2.4 類加載器的三個機制(約束)

雙親委托機制實際上是一種自上而下帶快取的加載,這種機制也決定它的一些特性:

委托:將類加載交由父類加載器加載,父不行再自己加載,

可見性:子類加載器可見所有的父類加載器加載的類,父類加載器不可見子類加載器加載的類,

單一性:一個類僅加載一次,子類加載器不會再次加載父類加載器加載過的類,

2.5 findClass

可以看到在所有父ClassLoader無法加載Class時,則會呼叫自己的findClass方法,findClass在ClassLoader中的定義為:

protected Class<?> findClass(String name) throws ClassNotFoundException {
	throw new ClassNotFoundException(name);
}

其實任何ClassLoader子類,都可以重寫loadClass與findClass,一般如果你不想使用雙親委托,則重寫loadClass修改其實作,而重寫findClass則表示在雙親委托下,父ClassLoader都找不到Class的情況下,定義自己如何去查找一個Class,而我們的PathClassLoader會自己負責加載MainActivity這樣的程式中自己撰寫的類,利用雙親委托父ClassLoader加載Framework中的Activity,說明PathClassLoader并沒有重寫loadClass,因此我們可以來看看PathClassLoader中的 findClass 是如何實作的,

public BaseDexClassLoader(String dexPath, File optimizedDirectory,String 	
						librarySearchPath, ClassLoader parent) {
	super(parent);
	this.pathList = new DexPathList(this, dexPath, librarySearchPath, 		
                                    optimizedDirectory);
}
 
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
	List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
    //查找指定的class
    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;
}

實作非常簡單,從pathList中查找class,繼續查看DexPathList:

public DexPathList(ClassLoader definingContext, String dexPath,
            String librarySearchPath, File optimizedDirectory) {
	//.........
    // splitDexPath 實作為回傳 List<File>.add(dexPath)
    // makeDexElements 會去 List<File>.add(dexPath) 中使用DexFile加載dex檔案回傳 Element陣列
    this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                           suppressedExceptions, definingContext);
	//.........
    
}
 
public Class findClass(String name, List<Throwable> suppressed) {
     //從element中獲得代表Dex的 DexFile
	for (Element element : dexElements) {
		DexFile dex = element.dexFile;
		if (dex != null) {
            //查找class
        	Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
            if (clazz != null) {
            	return clazz;
        	}
    	}
    }
    if (dexElementsSuppressedExceptions != null) {
    	suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
    }
	return null;
}

可以看到, BaseDexClassLoader 的 findClass() 方法實際上是通過 DexPathList 的 findClass() 方法來獲取class的,而這個 DexPathList 物件恰好在之前的 BaseDexClassLoader 建構式中就已經被創建好了,里面決議了dex檔案的路徑,并將決議的dex檔案都存在this.dexElements里面,DexPathList 類通過建構式呼叫了 makeDexElements() 得到 Element

makeDexElements()

 //決議dex檔案
private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory, ArrayList<I
 // 1.創建Element集合
 ArrayList<Element> elements = new ArrayList<Element>();
 // 2.遍歷所有dex檔案(也可能是jar、apk或zip檔案)
 for (File file : files) {
 ZipFile zip = null;
 DexFile dex = null;
 String name = file.getName();
 ...
 // 如果是dex檔案
 if (name.endsWith(DEX_SUFFIX)) {
 dex = loadDexFile(file, optimizedDirectory);
 // 如果是apk、jar、zip檔案(這部分在不同的Android版本中,處理方式有細微差別)
 } else {
 zip = file;
 dex = loadDexFile(file, optimizedDirectory);
 }
 ...
 // 3.將dex檔案或壓縮檔案包裝成Element物件,并添加到Element集合中
 if ((zip != null) || (dex != null)) {
 elements.add(new Element(file, false, zip, dex));
 }
 }
 // 4.將Element集合轉成Element陣列回傳
 return elements.toArray(new Element[elements.size()]);
}

可以看到,DexPathList 的建構式是將一個個的程式檔案(可能是dex、apk、jar、zip)封裝成一個個 Element 物件,最后添加到Element集合中,Android的類加載器(不管是PathClassLoader,還是DexClassLoader,它們最后在加載檔案時,都只認dex檔案,而loadDexFile()是加載dex檔案的核心方法,他可以可以從jarapkzip中提取出dex

回頭看一下PathClassLoader中的 findClass 方法:

 Class c = pathList.findClass(name, suppressedExceptions);

于是看到DexPathListfindClass()方法,如下:

public Class findClass(String name, List<Throwable> suppressed) {
    // 遍歷從dexPath查詢到的dex和資源Element
    for (Element element : dexElements) {
        DexFile dex = element.dexFile;
        // 如果當前的Element是dex檔案元素
        if (dex != null) {
            // 使用DexFile.loadClassBinaryName加載類
            Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }
    }
    if (dexElementsSuppressedExceptions != null) {
        suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
    }
    return null;
}

對 Element 陣列進行遍歷,一旦找到類名與name相同的類時,就直接回傳這個 class ,找不到則回傳null,

通過如上分析,我們發現整個類加載流程就是:

  1. 類加載器BaseDexClassLoader先將dex檔案決議放到pathListdexElements里面
  2. 加載類的時候從dexElements里面去遍歷,看哪個dex里面有這個類就去加載,生成class物件

三 熱修復

接剛剛個類加載流程,熱修復原理就是將補丁 dex 檔案放到 dexElements 陣列靠前位置,這樣在加載 class 時,優先找到補丁包中的 dex 檔案,加載到 class 之后就不再尋找,從而原來的 apk 檔案中同名的類就不會再使用,從而達到修復的目的:

PathClassLoader中的Element陣列為:[patch.dex , classes.dex , classes2.dex],如果存在Key.class位于patch.dex與classes2.dex中都存在一份,當進行類查找時,回圈獲得dexElements中的DexFile,查找到了Key.class則立即回傳,不會再管后續的element中的DexFile是否能加載到Key.class了,因此一種熱修復實作可以將出現Bug的class單獨的制作一份fix.dex檔案(補丁包),然后在程式啟動時,從服務器下載fix.dex保存到某個路徑,再通過fix.dex的檔案路徑,用其創建Element物件,然后將這個Element物件插入到我們程式的類加載器PathClassLoader的pathList中的dexElements陣列頭部,這樣在加載出現Bug的class時會優先加載fix.dex中的修復類,從而解決Bug,

熱修復的方式不止這一種,并且如果要完整實作此種熱修復可能還需要注意一些其他的問題(如:反射兼容),另外插件的形式常見的有apkdex檔案,

dex打包工具(d8)

更新的dex檔案如何生成呢?

Android SDK提供了dex打包工具d8,在在Android 構建工具 28.0.1 及更高版本中可以找到:

對正常的java檔案,直接javac成class檔案后,可直接用d8編譯成dex檔案:

./d8 XXX.class

如果你想更新的檔案是apk格式的,可直接在Android Studio中對更新的Module/Lib直接打包成apk,

代碼實作

dex替換:

//在Application中進行替換
public class MApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        //dex作為插件進行加載
        dexPlugin();
    }
    ...

  /**
     * dex作為插件加載
     */
    private void dexPlugin(){
        //插件包檔案
        File file = new File("/sdcard/hotfix.dex");
        if (!file.exists()) {
            Log.i("MApplication", "插件hotfix不存在");
            return;
        }
        try {
            //獲取到 BaseDexClassLoader 的  pathList欄位
            // private final DexPathList pathList;
            Field pathListField = BaseDexClassLoader.class.getDeclaredField("pathList");
            //破壞封裝,設定為可以呼叫
            pathListField.setAccessible(true);
            //拿到當前ClassLoader的pathList物件
            Object pathListObj = pathListField.get(getClassLoader());

            //獲取當前ClassLoader的pathList物件的位元組碼檔案(DexPathList )
            Class<?> dexPathListClass = pathListObj.getClass();
            //拿到DexPathList 的 dexElements欄位
            // private final Element[] dexElements;
            Field dexElementsField = dexPathListClass.getDeclaredField("dexElements");
            //破壞封裝,設定為可以呼叫
            dexElementsField.setAccessible(true);

            //使用插件創建 ClassLoader
            DexClassLoader pathClassLoader = new DexClassLoader(file.getPath(), getCacheDir().getAbsolutePath(), null, getClassLoader());
            //拿到插件的DexClassLoader 的 pathList物件
            Object newPathListObj = pathListField.get(pathClassLoader);
            //拿到插件的pathList物件的 dexElements變數
            Object newDexElementsObj = dexElementsField.get(newPathListObj);

            //拿到當前的pathList物件的 dexElements變數
            Object dexElementsObj=dexElementsField.get(pathListObj);

            int oldLength = Array.getLength(dexElementsObj);
            int newLength = Array.getLength(newDexElementsObj);
            //創建一個dexElements物件
            Object concatDexElementsObject = Array.newInstance(dexElementsObj.getClass().getComponentType(), oldLength + newLength);
            //先添加新的dex添加到dexElement
            for (int i = 0; i < newLength; i++) {
                Array.set(concatDexElementsObject, i, Array.get(newDexElementsObj, i));
            }
            //再添加之前的dex添加到dexElement
            for (int i = 0; i < oldLength; i++) {
                Array.set(concatDexElementsObject, newLength + i, Array.get(dexElementsObj, i));
            }
            //將組建出來的物件設定給 當前ClassLoader的pathList物件
            dexElementsField.set(pathListObj, concatDexElementsObject);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

apk:

    // apk作為插件加載
    private void apkPlugin() {
        //插件包檔案
        File file = new File("/sdcard/hotfix.apk");
        if (!file.exists()) {
            Log.i("MApplication", "插件hotfix不存在");
            return;
        }
        try {
            //獲取到 BaseDexClassLoader 的  pathList欄位
            // private final DexPathList pathList;
            Field pathListField = BaseDexClassLoader.class.getDeclaredField("pathList");
            //破壞封裝,設定為可以呼叫
            pathListField.setAccessible(true);
            //拿到當前ClassLoader的pathList物件
            Object pathListObj = pathListField.get(getClassLoader());

            //獲取當前ClassLoader的pathList物件的位元組碼檔案(DexPathList )
            Class<?> dexPathListClass = pathListObj.getClass();
            //拿到DexPathList 的 dexElements欄位
            // private final Element[] dexElements;
            Field dexElementsField = dexPathListClass.getDeclaredField("dexElements");
            //破壞封裝,設定為可以呼叫
            dexElementsField.setAccessible(true);

            //使用插件創建 ClassLoader
            DexClassLoader pathClassLoader = new DexClassLoader(file.getPath(), getCacheDir().getAbsolutePath(), null, getClassLoader());
            //拿到插件的DexClassLoader 的 pathList物件
            Object newPathListObj = pathListField.get(pathClassLoader);
            //拿到插件的pathList物件的 dexElements變數
            Object newDexElementsObj = dexElementsField.get(newPathListObj);
            //將插件的 dexElements物件設定給 當前ClassLoader的pathList物件
            dexElementsField.set(pathListObj, newDexElementsObj);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

一些擴展

  1. 選擇apk檔案的方式一般就不對dexElements陣列就行插前值操作了,而是直接替換整個dexElements陣列,但是現實中,熱更新可能只是更新某幾個類或者資源檔案,如果使用apk全量替換的方式,就很重,那么增量替換,即使用dex檔案的方式,就是很好的方式;

  2. 更新的檔案一般放服務器上需要客戶端下載插前值;

  3. so庫在Android代碼中是通過呼叫System.loadLibrary函式實作的,動態注冊的native方法呼叫一次JNI_OnLoad方法都會重新完成一次映射, 所以我們是否只要先加載原來的so庫,,然后再加載補丁so庫,就能完成Java層native方法到native層patch后的新方法映射, 這樣就完成動態注冊native方法的patch實時修復,

  4. 、資源檔案的更新方式:加載apk,反射呼叫AssetManager的addAssetPath方法,

參考文章:

Android Runtime (ART) 和 Dalvik

Android 熱修復核心原理, ClassLoader類加載

【小家Java】從原理層面理解Java中的類加載器

一看你就懂,超詳細java中的ClassLoader詳解

Android熱修復實作及原理

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

標籤:其他

上一篇:從需求分解到實作實戰系列之 - 1圖片串列拖動選中功能

下一篇:Java的概述

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