主頁 > 移動端開發 > 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的概述

標籤雲
其他(123570) Java(13369) Python(12731) C(7545) 區塊鏈(7372) JavaScript(7059) 基礎類(6313) AI(6244) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4120) MySQL(4012) Linux(3394) C語言(3288) C++語言(3117) Java相關(2746) 疑難問題(2699) 單片機工控(2479) Web開發(1951) 網絡通信(1793) 數據庫相關(1767) VB基礎類(1755) PHP(1727) 開發(1646) 系統維護與使用區(1617) .NETCore(1586) 基礎和管理(1579) JavaEE(1566) C++(1527) 專題技術討論區(1515) Windows客戶端使用(1484) HtmlCss(1466) ASP.NET(1428) Unity3D(1354) VCL組件開發及應用(1353) HTML(CSS)(1220) 其他技術討論專區(1200) WindowsServer(1192) .NET技术(1165) 交換及路由技術(1149) 語言基礎算法系統設計(1133) WindowsSDKAPI(1124) 界面(1088) JavaSE(1075) Qt(1074) VBA(1048) 新手樂園(1016) 其他開發語言(947) Go(907) HTML5(901) 新技術前沿(898) 硬件設計(872) 區塊鏈技術(860) 網絡編程(857) 非技術版(846) 一般軟件使用(839) 網絡協議與配置(835) Eclipse(790) Spark(750) 下載資源懸賞專區(743)

熱門瀏覽
  • 【從零開始擼一個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
最新发布
  • 如何在C中找到鍵/值字典的大小

    我對 C 很陌生,我正在學習使用結構和指標,但我似乎無法弄清楚我在這里做錯了什么。所以我試圖找到每次呼叫 main 函式時都會重置的字典的大小。當我嘗試手動運行代碼時,它會說...

    uj5u.com 2021-10-16 17:05:42 more
  • 如何在將指標作為引數的函式內釋放結構指標

    編輯*我正在學習結構和指標,我正在處理的部分任務要求我釋放 malloc 的空間以用于結構指標。指標作為函式內部的引數傳遞,我想知道是否可以釋放函式內部的空間?指標在主檔案中...

    uj5u.com 2021-10-16 17:05:10 more
  • 同時擁有名稱和指向結構的指標是否有意義?

    typedef struct net_path_s{ uint8 path_len; /* network path length */ uint8 net_path[2 * MAX_ROUTE_LEN]; /* network path */} net...

    uj5u.com 2021-10-16 17:04:34 more
  • Python的記憶體行為

    我有一個串列,它會變得非常大。所以我會將串列保存在我的硬碟上,然后繼續使用一個空串列。我的問題是:當我執行 myList[] 時,舊資料會被洗掉還是會保留在 Ram 上的某個地方。我...

    uj5u.com 2021-10-16 17:04:05 more
  • 僅使用帶有鍵的unordered_map來存盤指標(忽略值)

    我正在實施一種演算法,該演算法檢查網格中的節點是否具有特定值。要存盤有關我已經檢查過的節點的資訊,我想使用 unordered_map 并將指向該節點的指標作為鍵。然后我可以簡單...

    uj5u.com 2021-10-16 17:03:06 more
  • 如何在同一頁面上向下滾動并顯示章節?

    我正在開發一個單頁網站,當用戶點擊一個特定的按鈕時,應該向下滾動到頁面的另一個部分。
    因為我使用了粘性標題,所以該部分的標題被隱藏在橫幅后面,所以我使用下面的代碼在點擊...

    uj5u.com 2021-10-16 15:31:29 more
  • 是否有辦法在gradle.build檔案中擴展或創建不同的jib配置?

    我正在使用jib插件來為我的springboot應用構建docker鏡像。然而,我希望在我的構建檔案中有一個新的任務,它將呼叫不同的構建jib任務。
    其原因是,根據我在 gradle 中創建的任務,...

    uj5u.com 2021-10-16 15:31:10 more
  • 未生成Apollo目錄

    我在初步實施中遇到了困難。
    我的問題是,下面的構建無法生成apollo目錄。
    用這個gradle(應用程式級別)
    plugins {
    id 'com.android.application'/span>
    id 'kotlin-androi...

    uj5u.com 2021-10-16 15:30:57 more
  • 引數型別'PointerEvent'不能分配給引數型別'PointerDownEvent'。

    最近,我更新到了flutter 2.5和最新的androids studio,并試圖將我的flutter專案編譯到android設備上。Android studio向我拋出了下面的錯誤。如果我在終端寫flutter run,編譯到...

    uj5u.com 2021-10-16 15:30:43 more
  • 如何使用then()并在Cypress中獲取數值

    我有一個span元素,它的值是2,我想檢查它的值是否大于0,但在網上查詢并實施了所有方法后,它沒有作業......
    下面是我記錄$span時的控制臺> 2</span>
    </div>

    cy.get(" .badge....

    uj5u.com 2021-10-16 15:28:04 more