內(nèi)存泄漏:
成都創(chuàng)新互聯(lián)公司專(zhuān)注為客戶提供全方位的互聯(lián)網(wǎng)綜合服務(wù),包含不限于網(wǎng)站制作、成都做網(wǎng)站、漢臺(tái)網(wǎng)絡(luò)推廣、微信小程序開(kāi)發(fā)、漢臺(tái)網(wǎng)絡(luò)營(yíng)銷(xiāo)、漢臺(tái)企業(yè)策劃、漢臺(tái)品牌公關(guān)、搜索引擎seo、人物專(zhuān)訪、企業(yè)宣傳片、企業(yè)代運(yùn)營(yíng)等,從售前售中售后,我們都將竭誠(chéng)為您服務(wù),您的肯定,是我們最大的嘉獎(jiǎng);成都創(chuàng)新互聯(lián)公司為所有大學(xué)生創(chuàng)業(yè)者提供漢臺(tái)建站搭建服務(wù),24小時(shí)服務(wù)熱線:13518219792,官方網(wǎng)址:www.cdcxhl.com
舉例:
請(qǐng)注意以下的例子是虛構(gòu)的
內(nèi)存抖動(dòng)
源自Android文檔中的 Memory churn 一詞,中文翻譯為內(nèi)存抖動(dòng)。
指快速頻繁的創(chuàng)建對(duì)象從而產(chǎn)生的性能問(wèn)題。
引用Android文檔原文:
Java內(nèi)存泄漏的根本原因是 長(zhǎng)生命周期 的對(duì)象持有 短生命周期 對(duì)象的引用就很可能發(fā)生內(nèi)存泄漏。
盡管短生命周期對(duì)象已經(jīng)不再需要,但因?yàn)殚L(zhǎng)生命周期依舊持有它的引用,故不能被回收而導(dǎo)致內(nèi)存泄漏。
靜態(tài)集合類(lèi)引起的內(nèi)存泄漏
如果僅僅釋放引用本身(tO = null), ArrayList 依然在引用該對(duì)象,GC無(wú)法回收。
監(jiān)聽(tīng)器
在Java應(yīng)用中,通常會(huì)用到很多監(jiān)聽(tīng)器,一般通過(guò) addXXXXListener() 實(shí)現(xiàn)。但釋放對(duì)象時(shí)通常會(huì)忘記刪除監(jiān)聽(tīng)器,從而增加內(nèi)存泄漏的風(fēng)險(xiǎn)。
各種連接
如數(shù)據(jù)庫(kù)連接、網(wǎng)絡(luò)連接(Socket)和I/O連接。忘記顯式調(diào)用 close() 方法引起的內(nèi)存泄漏。
內(nèi)部類(lèi)和外部模塊的引用
內(nèi)部類(lèi)的引用是很容易被遺忘的一種,一旦沒(méi)有釋放可能會(huì)導(dǎo)致一系列后續(xù)對(duì)象無(wú)法釋放。此外還要小心外部模塊不經(jīng)意的引用,內(nèi)部類(lèi)是否提供相應(yīng)的操作去除外部引用。
單例模式
由于單例的靜態(tài)特性,使其生命周期與應(yīng)用的生命周期一樣長(zhǎng),一旦使用不恰當(dāng)極易造成內(nèi)存泄漏。如果單利持有外部引用,需要注意提供釋放方式,否則當(dāng)外部對(duì)象無(wú)法被正?;厥諘r(shí),會(huì)進(jìn)而導(dǎo)致內(nèi)存泄漏。
集合類(lèi)泄漏
如集合的使用范圍超過(guò)邏輯代碼的范圍,需要格外注意刪除機(jī)制是否完善可靠。比如由靜態(tài)屬性 static 指向的集合。
單利泄漏
以下為簡(jiǎn)單邏輯代碼,只為舉例說(shuō)明內(nèi)存泄漏問(wèn)題,不保證單利模式的可靠性。
AppManager 創(chuàng)建時(shí)需要傳入一個(gè) Context ,這個(gè) Context 的生命周期長(zhǎng)短至關(guān)重要。
1. 如果傳入的是 Application 的 Context ,因?yàn)?Application 的生命周期等同于應(yīng)用的生命周期,所以沒(méi)有任何問(wèn)題。
2. 如果傳入的是 Activity 的 Context ,則需要考慮這個(gè) Activity 是否在整個(gè)生命周期都不會(huì)被回收了,如果不是,則會(huì)造成內(nèi)存泄漏。
非靜態(tài)內(nèi)部類(lèi)創(chuàng)建靜態(tài)實(shí)例造成的內(nèi)存泄漏
應(yīng)該將該內(nèi)部類(lèi)單獨(dú)封裝為一個(gè)單例來(lái)使用。
匿名內(nèi)部類(lèi)/異步線程
Runnable都使用了匿名內(nèi)部類(lèi),將持有MyActivity的引用。如果任務(wù)在Activity銷(xiāo)毀前未完成,將導(dǎo)致Activity的內(nèi)存無(wú)法被回收,從而造成內(nèi)存泄漏。
解決方法:將Runnable獨(dú)立出來(lái)或使用靜態(tài)內(nèi)部類(lèi),可以避免因持有外部對(duì)象導(dǎo)致的內(nèi)存泄漏。
Handler造成的內(nèi)存泄漏
Handler屬于TLS(Thread Local Storage)變量,生命周期與Activity是不一致的,容易導(dǎo)致持有的對(duì)象無(wú)法正確被釋放
當(dāng)Android應(yīng)用程序啟動(dòng)時(shí),該應(yīng)用程序的主線程會(huì)自動(dòng)創(chuàng)建一個(gè)Looper對(duì)象和與之關(guān)聯(lián)的MessageQueue。
當(dāng)主線程中實(shí)例化一個(gè)Handler對(duì)象后,它就會(huì)自動(dòng)與主線程Looper的MessageQueue關(guān)聯(lián)起來(lái)。所有發(fā)送到MessageQueue的Messag都會(huì)持有Handler的引用,所以Looper會(huì)據(jù)此回調(diào)Handle的handleMessage()方法來(lái)處理消息。只要MessageQueue中有未處理的Message,Looper就會(huì)不斷的從中取出并交給Handler處理。
另外,主線程的Looper對(duì)象會(huì)伴隨該應(yīng)用程序的整個(gè)生命周期。
在Java中,非靜態(tài)內(nèi)部類(lèi)和匿名類(lèi)內(nèi)部類(lèi)都會(huì)潛在持有它們所屬的外部類(lèi)的引用,但是靜態(tài)內(nèi)部類(lèi)卻不會(huì)。
當(dāng)該 Activity 被 finish() 掉時(shí),延遲執(zhí)行任務(wù)的 Message 還會(huì)繼續(xù)存在于主線程中,它持有該 Activity 的 Handler 引用,所以此時(shí) finish() 掉的 Activity 就不會(huì)被回收了從而造成內(nèi)存泄漏(因 Handler 為非靜態(tài)內(nèi)部類(lèi),它會(huì)持有外部類(lèi)的引用,在這里就是指 SampleActivity)。
避免不必要的靜態(tài)成員變量
對(duì)于BroadcastReceiver、ContentObserver、File、Cursor、Stream、Bitmap等資源的使用,應(yīng)在Activity銷(xiāo)毀前及時(shí)關(guān)閉或注銷(xiāo)。
不使用WebView對(duì)象時(shí),應(yīng)調(diào)用`destroy()`方法銷(xiāo)毀。
Android 微信分享遇到的問(wèn)題:
1.APP_ID是否輸入正確
2.官網(wǎng)申請(qǐng)時(shí)輸入的簽名和打包的簽名是否一致(請(qǐng)用微信推薦的簽名工具對(duì)比)
注:微信平臺(tái)填寫(xiě)的簽名是ce187ed67e05c2d8879bf66bbfdfc8b9
是apk的keystore的md5去掉冒號(hào),大寫(xiě)換位小寫(xiě)字母形式
3.分享一閃而過(guò)
有可能的bug:簽名錯(cuò)誤,appid正確,但是申請(qǐng)的時(shí)候吧包名和簽名寫(xiě)反了
微信緩存問(wèn)題,重新安裝微信多試幾次
4.請(qǐng)用微信官方提供的簽名獲取工具
5.自己直接run到手機(jī)運(yùn)行的apk包注意簽名應(yīng)該和申請(qǐng)時(shí)用的簽名一致(即把debug的簽名改為release的)
6.分享圖片的縮略圖太大,超過(guò)32k
7.換設(shè)備,重新嘗試
8.保證所有配置沒(méi)有問(wèn)題的情況下,嘗試重啟手機(jī)即可……(我沒(méi)有開(kāi)玩笑)
在之前的一篇文章 利用 Android 系統(tǒng)原生 API 實(shí)現(xiàn)分享功能 中主要說(shuō)了下實(shí)現(xiàn)流程,但具體實(shí)施起來(lái)其實(shí)還是有許多坑要面對(duì)。那這篇文章就是提供一個(gè)封裝好的 Share2 庫(kù)供大家參考。
GitHub 項(xiàng)目地址:Share2
看過(guò)上一篇文章的同學(xué)應(yīng)該知道,要調(diào)用 Android 系統(tǒng)內(nèi)建的分享功能,主要有三步流程:
更多相關(guān)內(nèi)容請(qǐng)參考上一篇,這里就不再重復(fù)贅述了。
知道大致的實(shí)現(xiàn)流程后,其實(shí)只要解決下面幾個(gè)問(wèn)題后就可以具體實(shí)施了。
這其實(shí)是直接決定了最終的實(shí)現(xiàn)形態(tài),我們知道常見(jiàn)的使用場(chǎng)景中,只是為了在應(yīng)用間分享圖片和一些文件,那對(duì)于那些只是分享文本的產(chǎn)品而言,兩者實(shí)現(xiàn)起來(lái)要考慮的問(wèn)題完全不同。
所以為了解決這個(gè)問(wèn)題,我們可以預(yù)先定好支持的分享內(nèi)容類(lèi)型,針對(duì)不同類(lèi)型可以進(jìn)行不同的處理。
在 Share2 中,一共定義了5種類(lèi)別的分享內(nèi)容,基本能覆蓋常見(jiàn)的使用場(chǎng)景。在調(diào)用分享接口時(shí)可以直接指定內(nèi)容類(lèi)型,比如像文本、圖片、音視頻、已經(jīng)其他各種類(lèi)型文件。
對(duì)于不同類(lèi)別的內(nèi)容,可能會(huì)有不同的來(lái)源。比如文本可能就只是一個(gè)字符串對(duì)象,而對(duì)于分享圖片或其他文件,我們需要一個(gè) Uri 來(lái)標(biāo)識(shí)一個(gè)資源。這其實(shí)就引出來(lái)具體實(shí)施時(shí)的一個(gè)大問(wèn)題,如何獲取要分享文件的 Uri,并且這個(gè) Uri 要能被接收分享內(nèi)容的應(yīng)用處理才行 。
那么,如何獲取要分享內(nèi)容文件的 Uri?如果處理才能讓接收方也能夠根據(jù) Uri 獲取到文件?
我們把文件 Uri 的來(lái)源劃分為下面三種類(lèi)型:
常見(jiàn)場(chǎng)景 :通過(guò)文件選擇器獲取一個(gè)文件的 Uri
通過(guò)這種方式獲取到的 Uri 是由系統(tǒng) ContentProvider 返回的,在 Android 4.4 之前的版本和之后的版本有較大的區(qū)別,我們后面再說(shuō)怎么處理。只要先記住這種系統(tǒng)返回給我們的 Uri 就行了。
比如調(diào)用系統(tǒng)相機(jī)進(jìn)行拍照或錄制音視頻,要傳入一個(gè)生成目標(biāo)文件的 Uri ,從 7.0 開(kāi)始我們需要用到 FileProvider 來(lái)實(shí)現(xiàn)。
如果用到了 FileProvider 就要注意跟系統(tǒng) ContentProvider 返回 Uri 的區(qū)別,比如我們?cè)?Manifest 中對(duì) FileProvider 配置 android:authorities="com.xx.xxx.fileProvider" 屬性,那這時(shí)系統(tǒng)返回的 Uri 格式就變成了 : content://com.xx.xxx.fileProvider... ,對(duì)于這種類(lèi)型的 Uri 我們姑且叫 自定義 FileProvider 返回的 Uri ,后面一并說(shuō)怎么處理。
我們調(diào)用 new File 時(shí)需要傳入指定的文件路徑,這個(gè)絕對(duì)路徑通常是: /storage/emulated/0/... 這種樣式,我們要想調(diào)用分享時(shí)也要變成 Uri 的形式才可以,那么如何把文件路徑變成一個(gè)文件 Uri ?這個(gè)問(wèn)題下面也一并進(jìn)行回答。
前面提到了文件 Uri 的三種分類(lèi),對(duì)應(yīng)不同類(lèi)型處理方式也不同,不然你最先遇到的問(wèn)題就是:
這是由于對(duì)系統(tǒng)返回的 Uri 缺失訪問(wèn)權(quán)限導(dǎo)致,所以要對(duì)應(yīng)用進(jìn)行臨時(shí)訪問(wèn) Uri 的授權(quán)才行,不然會(huì)提示權(quán)限缺失。
對(duì)于要分享系統(tǒng)返回的 Uri 我們可以這樣進(jìn)行處理:
需要注意的是對(duì)于自定義 FileProvider 返回 Uri 的處理,即使是設(shè)置臨時(shí)訪問(wèn)權(quán)限,但是分享到第三方應(yīng)用也會(huì)無(wú)法識(shí)別該 Uri
典型的場(chǎng)景就是,我們?nèi)绻炎远x FileProvider 的返回的 Uri 設(shè)置分享到微信或 QQ 之類(lèi)的第三方應(yīng)用,會(huì)提示文件不存在,這是因?yàn)樗麄儫o(wú)法識(shí)別該 Uri。
關(guān)于這個(gè)問(wèn)題的處理其實(shí)跟下面要說(shuō)的把文件路徑變成系統(tǒng)返回的 Uri 一樣,我們只需要把自定義 FileProvider 返回的 Uri 變成第三方應(yīng)用可以識(shí)別系統(tǒng)返回的 Uri 就行了。
創(chuàng)建 FileProvider 時(shí)需要傳入一個(gè) File 對(duì)象,所以直接可以知道文件路徑,那就把問(wèn)題都轉(zhuǎn)換成了: 如何通過(guò)文件路徑獲取系統(tǒng)返回的 Uri
下面是根據(jù)傳入的 File 對(duì)象和類(lèi)型來(lái)查詢系統(tǒng) ContentProvider 來(lái)獲取相應(yīng)的 Uri,已經(jīng)按照不同文件類(lèi)型在不同系統(tǒng)版本下的進(jìn)行了適配。
其中 forceGetFileUri 方法是通過(guò)反射實(shí)現(xiàn)的,處理 7.0 以上系統(tǒng)的特殊情況下的兼容性,一般情況下不會(huì)調(diào)用到。Android 7.0 開(kāi)始不允許 file:// Uri 的方式在不同的 App 間共享文件,但是如果換成 FileProvider 的方式依然是無(wú)效的,我們可以通過(guò)反射把該檢測(cè)干掉。
通過(guò) File Path 轉(zhuǎn)成 Uri 的方式,我們最終統(tǒng)一了調(diào)用系統(tǒng)分享時(shí)傳入內(nèi)容 Uri 的三種不同場(chǎng)景,最終全部轉(zhuǎn)換為傳遞系統(tǒng)返回的 Uri,讓第三方應(yīng)用能夠正常的獲取到分享內(nèi)容。
Share2 按照上述方法進(jìn)行了具體實(shí)施,可以通過(guò)下面的方式進(jìn)行集成:
分享圖片到指定界面,比如分享到微信朋友圈
GitHub 項(xiàng)目地址:Share2
10M以下,建議進(jìn)制算法為*1000以避免1024發(fā)生分享錯(cuò)誤
將文件變成二進(jìn)制數(shù)組,然后塞進(jìn)去就OK了,我這邊是直接傳入的base64碼省略了部分步驟
qq分享類(lèi)似,不過(guò)是用intent來(lái)的
只能分享本地文件,我這邊是js傳的,可以將文件存到本地然后分享
File shareFileDir = StorageUtils.getExternalFileDirectory(activity.getApplicationContext(), StaticFinalUtil.SHARE_MEDIA);
? ? ? ? ? ? ? ? ? ? File shareFile = new File(shareFileDir, shareMediaNew.title.concat(".").concat(shareMediaNew.fileType));
? ? ? ? ? ? ? ? ? ? if (shareFile.exists()) {
? ? ? ? ? ? ? ? ? ? ? ? Intent qqIntent = new Intent(Intent.ACTION_SEND);
? ? ? ? ? ? ? ? ? ? ? ? Uri shareFileUri;
? ? ? ? ? ? ? ? ? ? ? ? if (Build.VERSION.SDK_INT = Build.VERSION_CODES.N) {
? ? ? ? ? ? ? ? ? ? ? ? ? ? //兼容7.0
? ? ? ? ? ? ? ? ? ? ? ? ? ? shareFileUri = FileProvider.getUriForFile(activity.getApplicationContext(), "com.DaTong.InsuranceForAndroid.fileprovider", shareFile);
? ? ? ? ? ? ? ? ? ? ? ? ? ? //添加權(quán)限 這一句表示對(duì)目標(biāo)應(yīng)用臨時(shí)授權(quán)該Uri所代表的文件
? ? ? ? ? ? ? ? ? ? ? ? ? ? qqIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
? ? ? ? ? ? ? ? ? ? ? ? ? ? qqIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
? ? ? ? ? ? ? ? ? ? ? ? }else {
? ? ? ? ? ? ? ? ? ? ? ? ? ? shareFileUri = Uri.fromFile(shareFile);
? ? ? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? ? ? ? ? qqIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
? ? ? ? ? ? ? ? ? ? ? ? qqIntent.setType(getMimeType(shareFile.getPath()));
? ? ? ? ? ? ? ? ? ? ? ? qqIntent.setClassName("com.tencent.mobileqq", "com.tencent.mobileqq.activity.JumpActivity");
? ? ? ? ? ? ? ? ? ? ? ? qqIntent.putExtra(Intent.EXTRA_STREAM, shareFileUri);
? ? ? ? ? ? ? ? ? ? ? ? activity.startActivity(qqIntent);
? ? ? ? ? ? ? ? ? ? ? ? return;
? ? ? ? ? ? ? ? ? ? }
GitHub 項(xiàng)目地址:LocalShare-master
直接上圖,這是一個(gè)典型的調(diào)用系統(tǒng)原生分享場(chǎng)景下的界面,相信大家應(yīng)該都很熟悉。
那下面說(shuō)一下遇到的一些問(wèn)題,特別針對(duì)是 7.0 以后的系統(tǒng),以及兼容一些主流 app 時(shí)遇到的坑。
前面說(shuō)到分享文件時(shí)需要知道文件的類(lèi)型,不然的指定類(lèi)型為 / ,這樣分享到某些 App 會(huì)因?yàn)闊o(wú)法判斷文件類(lèi)型而導(dǎo)致失敗,所以最好先根據(jù)文件路徑獲取其文件類(lèi)型。
使用這種方法獲取文件類(lèi)型,一定要注意 ContentResolver 獲取返回為 null 的情況,不然空指針異常的崩潰率可能會(huì)讓你笑不出來(lái)。實(shí)際測(cè)試中,發(fā)現(xiàn)在某些國(guó)產(chǎn)機(jī)型下,這個(gè)方法可以說(shuō)直接是不可用,查詢返回一直都是空,所以單純依賴這一個(gè)方法會(huì)很不可靠。具體問(wèn)題原因請(qǐng)看: What causes Android's ContentResolver.query() to return null?
下面按照第二條思路,按照文件頭信息簡(jiǎn)單實(shí)現(xiàn)一個(gè)獲取文件類(lèi)型的例子:
// 獲取文件Uri
要向在 MediaStore 中查詢到文件,要不就是通知媒體庫(kù)更新查詢或則往里面插入一條新記錄(會(huì)比較耗時(shí))
可以參考我的另外一篇文章: Android 系統(tǒng)原生 API 實(shí)現(xiàn)分享功能(2)
參考: