ONE YEAR
我的 android 開發一年總結。
2017年6月21號,是我在華中科技大學的畢業典禮的日子,也是對我來說頗為重要的一天,我大學畢業。距今,2018年6月,已經快要一年了。這一年中,在技術方面遇到很多有趣的東西,同時彌補了之前很多一知半解的東西,在整體方面,我感覺自己越來越好了,如果說自己越來越差了,是不是也不太對啊……(尷尬ing)
下面是一篇極為枯燥的文章……
下面是一篇主觀性偏多的文章……
下面是一篇 長長長長長~~~的文章……
1. 是不是每一個走向工作的 dev 都是從動畫開始?
我是去年6月份底畢業的,7月3號入職。 入職做的第一個 feature 是一個動畫,有關 junkScan 的動畫,屬于一個掃描類的動畫。當時看到這個動畫的 gif 圖,第一瞬的感覺是有種血脈噴張的感覺,有種躍躍欲試的感覺,同時,這也是一個比較復雜的動畫,之前在學校里面,這些動畫,恩,說實話沒想去做過,所以也會有些緊張,剛一開始不知道從何處入手。也不得不承認,公司這個環境會讓你迅速成長,迅速掌握一些知識。也是那個時候正式詳細的接觸 ValueAnimator
和 ObjectAnimator
,沒辦法呀,大學里我太菜,很多知識沒有接觸到。這個動畫是一個大的掃描動畫,下面是三個小動畫,伴隨著背景的變化。慢慢的學會了,從慢去看動畫,一幀一幀的看動畫,才分析的詳細,在會把這個時間段所發生的操作全部了解。所以我理解動畫,就是把它一點點剖析,在慢的角度去看它,去分析它,然后什么時間應該做哪些操作,按照這樣就可以達到,動畫其實很簡單。
后面又不同的做了很多種類的動畫,當你對動畫熟悉以后,一個動畫其實在你剛去做時,你就已經知道應該在哪里做哪些事情,下面是我認為一些需要注意的地方:
1.1 ValueAnimator, ObjectAnimator 需要注意的地方:
-
onAnimationUpdate
,onAnimationEnd
,onAnimationStart
三個方法,當然還需要去設置插值器,默認并不是線性勻速加速器,而是AccelerateDecelerateInterpolator
, 這個自行設置就好。
-
設置了
setRepeatCount
后當設置了
setRepeatCount
后,它對onAnimationEnd
和onAnimationStart
的 調用只有一次了,理解起來就是 這兩個方法分別在動畫的開始和動畫的結束調用,可當你設置了 repeatCount 為 5 后,這個整體的動畫開始就是第一遍動畫的開始,動畫的結束就是 第五次動畫的結束,所以,這兩個方法均只會調用一次,但是如果是你在
onAnimationEnd
中重新 調用了這個 animator 的 start ,則 每次onAnimationEnd
和onAnimationStart
都會調用;
在 animtor.cancel() 時 會調用
onAnimationEnd()
, 如果不想這個方法被調用,需要先 removeAllListeners();-
尤其要注意設置了
setRepeatCount(ValueAnimator.INFINITE)
這個地方要多注意,因為一旦設置了這個屬性,動畫會不斷的執行下去,如果不及時釋放會造成內存泄漏,所以在
onPause()
和onDestroy()
要判斷下,如果該 animttor != null 要釋放該動畫;
1.2 ValueAnimator, ObjectAnimator 動畫與自定義 View
很多動畫都不只是對一個 原生的 view 進行操作就可以達到所有需求的,動畫也往往與自定義 view 聯系在一起, 下面是一些個人對 自定義 view 動畫的感觸:
預留一個方法接口去開啟動畫,也需要預留一個方法接口去停止動畫,在編程里很多都是對稱的
startAnim()
,stopAnim()
往往都是通過
invalidate()
去調動 onDraw() 重新繪制
-
在
onDraw()
中,不外乎幾種方式:-
canvas.drawArc(@NonNull RectF oval, float startAngle, float sweepAngle, boolean useCenter,
@NonNull Paint paint)useCenter
, 這個參數比較重要,為 true 時 是一個扇形的弧, 為 false 時是一個一段弧 canvas.drawCircle()
canvas.drawBitmap()
-
-
paint.setXfermode()
有一些效果,只是簡單的繪制可能達不到效果,Xfermode 是作用在兩個 Bitmap(或其他形式的元素上) 相互交合的部分的效果,它有很多效果,我只記得簡單的 例如 src_in 等效果
參考鏈接: 自定義控件三部曲之繪圖篇(十)——Paint之setXfermode(一)
-
擦除效果:
擦除效果類的動畫,可以理解為 兩個圖層在相互進行 xfermode 效果
-
高光閃過效果:
好像很多設計比較喜歡添加 高光閃爍的效果,往往,這個高光不太好加,多試試 xfermode
-
-
動畫的卡頓問題
可能是因為動畫開始時布局文件尚未繪制好,導致的丟幀情況(一下子,頓一下,運行或直接接跳過一段時間),這時需要
postDelay
一下,盡可能不會發生 draw 丟幀情況(丟幀是可能會跳過 view 的部分draw()
方法)
1.3 動畫的思考
在動畫方面,其實還有很多,我沒有去接觸,下面是一些需要加強的部分
OpenGL
這一塊,我還沒機會去涉足,感覺這是一個大的部分,內容很多;對于動畫的原理,不夠清楚, 有時候我思考時會有限制,會有一些理所當然,我會想
onAnimationUpdate()
是每次值刷新后回調的方法,我在這里做相應的操作,可到底是誰去回調的onAnimationUpdate()
呢?我有時會忽略這一點,想當然的背后就是不足夠理解。-
ValueAnimator
,ObjectAnimator
的插值器,插值器影響的是
animation.getAnimatedValue()
, 和animation.getAnimatedFraction()
的值,其實插值器是影響這些返回值的變化的速率。深入代碼里,ValueAnimator.animateValue()
/** * this method is called with the elapsed fraction of the animation * during every animation frame */ @CallSuper void animateValue(float fraction) { fraction = mInterpolator.getInterpolation(fraction); mCurrentFraction = fraction; int numValues = mValues.length; for (int i = 0; i < numValues; ++i) { mValues[i].calculateValue(fraction); } if (mUpdateListeners != null) { int numListeners = mUpdateListeners.size(); for (int i = 0; i < numListeners; ++i) { mUpdateListeners.get(i).onAnimationUpdate(this); } } }
會根據當前的插值器,1. 計算動畫的進度(0 ~ 1),
fraction
;2.同時計算動畫的實際進度值--->映射到 我們想要的值,mValues
;3. 通知動畫的進度回調,mUpdateListeners
-
誰觸發的
onAnimationUpdate()
追蹤代碼,會找到
doAnimationFrame()
--->animateBasedOnTime()
--->animateValue ()
--->onAnimationUpdate()
,而
ValueAnimator.doAnimationFrame()
是被AnimationHandler.doAnimationFrame()
里被調用的,private void doAnimationFrame(long frameTime) { ... }
是私有的,它的被調用是在
Choreographer
的回調 --->doFrame()
里被調用的;AnimationHandler
是一個單例, 用來處理所有活著的ValueAnimator (active ValueAnimator)
.與
Choreographer
的關系public class AnimationHandler { ... private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() { @Override public void doFrame(long frameTimeNanos) { //因為 AnimationHandler. doAnimationFrame() 是私有的,所以它的調用均在它自身里面 doAnimationFrame(getProvider().getFrameTime()); if (mAnimationCallbacks.size() > 0) { getProvider().postFrameCallback(this); } } }; ... }
-
一個重要的類
Choreographer
, 屏幕刷新信號我對這個類還沒深入進行了解,它是屏幕刷新的關鍵,
ViewRootImpl
,scheduleTraversals()
, 都是大把大把的好玩的.一個 View 發起刷新的操作時,會層層通知到
ViewRootImpl
的scheduleTraversals()
里去,然后這個方法會將遍歷繪制 View 樹的操作performTraversals()
封裝到Runnable
里,傳給Choreographer
以上是我對動畫的一些理解, 細細琢磨來,動畫的實現可能不是那么難,但是動畫涉及到很多知識,在對動畫的刷新上還需要進一步加強理解;
View
的繪制,或是findViewById()
均是深度優先;
2. 開發隨筆 - 日常拾殼
動畫是一個剛開始階段, 后面大大小小做了一些其他方面的事,在這個過程中,發現很多對之前的我不了解的部分,撿一些好玩的部分記錄一下:
-
相對布局
PercentRelativeLayout
這個主要做屏幕的適配,在眾多的機型上,挺好用的。
-
更好用的一種布局
ConstrainLayout
這個就比較好玩了,可以完全只用一個父布局
ConstrainLayout
完成布局的書寫,而且同時支持百分比布局,它里面還有很多新奇的特性,可以完成之前在開發過程中不太好實現的效果,這個布局總會給我很多驚喜,我覺著它還會有很多驚奇的點待發現。在后面做任務時,所有的布局我都是利用的
ConstrainLayout
實現的,雖然有些界面完全是重新寫了一遍,但我感覺這個過程還蠻有意思的,做的點滴好處,說不定在某個時候會積累成一個大的優勢。 -
當點擊 Home鍵退出時, 從后臺啟動 activity, android 系統做的 5 秒限制
情景是這樣的,我監聽了其他的某些具體的app A, 當從這個 app A退出,回到桌面時, 我從自己的 app 后臺進程那里啟動了一個 activity, 退出 app A , 回到桌面有兩種方式:
點擊 back 鍵退出, 回到桌面,
點擊 home 鍵退出, 回到桌面
當是第一種方式時,可以立馬從我的 app 后臺啟動 一個 activity, 但是當是第二種方式時, 會延遲 5 秒才會啟動 我的這個 activity !
為什么會這樣呢? 發現:
Google 特意提醒開發者,當用戶點擊 home 鍵退出時,不要從后臺(包括 service 或者 broadcastReceiver)啟動 activity,任何在后臺
startActivity
的操作都將會延遲 5 秒, 除非獲取到了 "android.permission.STOP_APP_SWITCHES" 權限.在
ActivityManagerService.java
的代碼里,我們可以發現:@Override public void stopAppSwitches() { ...// 檢查是否為 STOP_APP_SWITCHES 權限 if (checkCallingPermission(android.Manifest.permission.STOP_APP_SWITCHES) != PackageManager.PERMISSION_GRANTED) { throw new SecurityException("viewquires permission " + android.Manifest.permission.STOP_APP_SWITCHES); } // APP_SWITCH_DELAY_TIME 為 5000 (5 秒) synchronized(this) { mAppSwitchesAllowedTime = SystemClock.uptimeMillis() + APP_SWITCH_DELAY_TIME; mDidAppSwitch = false; mHandler.removeMessages(DO_PENDING_ACTIVITY_LAUNCHES_MSG); Message msg = mHandler.obtainMessage(DO_PENDING_ACTIVITY_LAUNCHES_MSG); mHandler.sendMessageDelayed(msg, APP_SWITCH_DELAY_TIME); } // 加鎖,并且,延遲5秒發送消息, 在接收消息(DO_PENDING_ACTIVITY_LAUNCHES_MSG)的地方 會去調用 //mActivityStarter.doPendingActivityLaunchesLocked(true); }
其實這里面還有很多細節,我對這個的理解,當用戶點擊 Home 鍵回到桌面時, 是想退出 app 回到桌面,不繼續操作事情的,可如果你 從后臺去開啟一個 activity, 進入到一個app內 這就會影響用戶的體驗,所以 Google 有了 5秒的限制,讓你即使能在 后臺開啟 activity 也會延遲 5秒,這樣至少用戶會先看到 桌面,等過了5秒,才會開啟你的 activity,
如何解決這個問題? 如果獲取不到 "android.permission.STOP_APP_SWITCHES" 這個權限,就只好不從后臺開啟 activity, 可以去 show 一個 floating window, 但并不是所有的機型都支持 floating window.
參考鏈接: 后臺啟動 activity 的 限制
-
一些沙灘上的貝殼:
在日常的開發中,我有時會發現一些好玩的點,很簡單的一部分,我把它們記錄在了下面這篇文章里面了:
等待補充
3. 第一個模塊任務 clean whatsapp
起步于動畫和一些小的需求任務,慢慢的做熟了,后來開始了一個整模塊的需求,一個新的模塊,放手給我去做,當知道這個任務時,蠻開心的。
在這里,很想對在公司帶我的 mentor 和 leader 感謝, 感謝你們一年來對我技術上的幫助,感謝你們一年來對我在公司的幫助,謝謝你們! 我的 mentor 是一個 很好相處也超級厲害的大佬,很有開發經驗, 我經常會給他說,mentor 你寫本書吧,寫本有關 debug 心得的書,絕對大賣!我的 leader,這是一位特別負責的 leader,剛開始在公司時,我寫的代碼,先被 mentor review 后, 再由 leader review,剛開始,他會一行一行代碼的 review,告訴我哪里不太對,告訴哪里邏輯缺少,告訴我更好的實現方法,并給我說明原因,他對代碼的要求極高,一個變量名,一個方法名,都要盡可能達到能表述清楚的程度,謝謝 leader 對我技術的幫助和引導。
3.1 clean whatsapp 的思考
clean whatsapp 這個模塊很多地方都是需要對文件,對數據進行操作。下面是一些在這個過程中有感觸的東西:
完完全全,在很短的時間內自定義了一個很復雜的動畫,,其實剖析起來,慢慢的實現,并不是很復雜
-
對文件 file 的讀取和移除, 對各種格式文件 根據后綴去分類
這個部分,我想把那些常用的對文件的操作的方式集中寫一寫,有很多可以借鑒的部分。
-
ImageView
的設置setBackground...()
與setImage...()
的區別這個地方,雖然之前了解,但在寫代碼時,曾經也犯了寫錯。
setBackgroundResource()、setBackgroundDrawable() 是 ImageView 的 背景,setImageResource()、setImageDrawable() 是 ImageView 的 content 內容。
這兩者是可以同時存在的!!!,所以如果你對 ImageView 設置了 background, 又設置了 setImage, 就會有兩層!!!
同時一個需要注意的點, Glide 默認對 imageView 的設置是 setImage...();
-
RecyclerView 的設置問題,
盡量不要設置
RecyclerView
的 高為wrap_content
,若
RecyclerView
高度設置為wrap_content
,子item有收縮、展開動畫,在點擊收縮時,可能會出現奇怪的界面刷新方式,原因可能是由于收縮時item已經沒有空間位置了,但是需要做動畫去完成,沒有足夠的空間位置,導致動畫特別的奇怪,不協調。 -
接口的實現
接口的出現很大部分是為了降低一些耦合性;
我覺著它的方便在于: 可以很方便的在一些合適的時機去執行對應的邏輯。
監聽事件 onClickListener() 是一個很好的接口.
-
Flexibleadapter 確實挺好用的……
這個是一個庫,抽象化了 recyclerView 的 adapter, 并且在 adapter 里面 很好的區分了 headItem, subItem,
尤其是在 在某個時機(回調回來的時機),想要新增一個 view (噓:例如廣告位啊),新增的這部分View 的空間可以跟著 recyclerView 滑動而滑動, 那么這時, 利用 scrollView 去嵌套 recyclerView 會有事件的攔截問題,而且更重要的是往往會帶來性能上的問題。所以把這部分新增的 view 作為 一個 item 插入 recyclerView 的 頭部是一個比較好的選擇!!!
這時在 recyclerItem 的頭部加入一個新的 item,會顯得很方便,直接 flexibleadapter.addScrollableHeader(...) 就可以達到要求。
-
SharedPreference 的問題
要考慮到該部分 sp 是否需要跨進程傳輸數據,如果是跨進程就需要另外一套封裝的方法;
因為 原生的 sp 在跨進程傳輸時 可能會變得不穩定,出現錯誤,所以可以在跨進程時對原生的 sp 進行封裝一下, 這里的封裝,我公司這邊使用的 ContentProvider 對它封裝了一下,利用 CP 的 Bundle call(...) 方法實現數據的跨進程傳遞, 這個部分到后面在說心得吧。
-
要考慮耗時操作
對數據庫文件的處理,對批量文件的讀取,對復雜動畫的開啟,都需要放在子線程里面去實現,我們要考慮這些耗時的操作。
當然最好是利用統一的一個 ThreadPool 去處理,如果我們各自在自己需要的地方 new Thread 去處理我們的耗時操作, 就會導致我們的程序里面出現大量的子線程,會出現大量的資源浪費問題,所以最好是用 線程池 去操作。
-
Collections.unmodifiableList(list) 的使用
目的是返回一個不可修改的 list, 主要目的是為里面維護該list的私有性,對外只可讀,不可修改,當要去修改時,會拋出
java.lang.UnsupportedOperationException
異常。從這里還可以印出來一些其他的東西, 像
CopyOnWriteArrayList
,CopyOnWriteArraySet
,ConcurrentHashMap
, 這些我一下子解釋不上來,還得一段時間去消化和理解。 -
注解的使用
注解好呀,注解可以很大的提高代碼的可讀性。
注解有很多類,其中一類可以對方法的參數做一些限制,對返回值做一些限制,舉個簡單的例子:
@StringDef({WHATS_APP_JUNK_IMAGE, WHATS_APP_JUNK_VIDEO...}) @Retention(RetentionPolicy.SOURCE) public @interface JunkType { } // 簡單使用 @WhatsAppFileUtils.JunkType private String itemJunkType;
感覺注解還有好多好多的用處,我對注解的理解,一是它讓代碼的可讀性更強了,二是它讓代碼寫起來更舒服些,更便捷。
還有很多很多的注解,像
ButterKnife
就是一個很好的注解庫,從大學就開始用這個庫. -
數組在方法中作為參數的使用,減少成員變量的個數
有時候需要一個常量,在每次操作一個方法時可能需要對它進行一些操作,這個常量再某個地方會再次被使用。
現在有這樣一種思路:, 利用一個大小為1的數組實現該功能。
int[] totalTemp = new int[1]; updateFirstCard(totalTemp); updateSecondCard(totalTemp); updateThirdCard(totalTemp); int total = totalTemp[0]; //total里為每次更新后的值 .... private void updateFirstCard(int[] totalTemp) { ... totalTemp[0] += 100; ... } ...
其原理是引用,數組的引用在傳遞參數的過程中,它的指向一直沒有變化,所發生的操作均是指向上的操作,所以最后我們拿到的值(這個指向所存儲的值)是每次更新后的正確的值。
這樣的操作的話,就不需要有一個成員變量去記錄每次變化了, 是一個比較小的點,但有時會思考不到這些。
3.2 Clean WhatsApp 總結
做 Clean WhatsApp 時學了挺多的,對數據的操作,及時更新UI 等,也學習了一些關于分包的知識,感覺有些代碼是仁者見仁智者見智,有些代碼的寫法也是這樣,而且,我想,每一個 dev 都會有自己的一個代碼的編寫習慣,也有自己的一些思考方式,我覺著這個很重要,這個是在語言之上的,對代碼的理解,對它的掌控,跟語言沒很大關系。在這個過程中,我學到了很多代碼編寫上的一些小點,太多的小點了,有用的小點,慢慢的開始有了自己的一套代碼風格,我想逐漸完善這個風格,讓這個風格更通俗,更好。在這個過程中,難免要多次去思考,有時你面對一個問題,今天的想法和昨天的想法是不同的,很多思考的角度也在變化,且走且行吧,讓自己有個更好的代碼編寫風格,加油加油。
4. 難度升級的模塊, Safe Browsing
Safe Browsing 是一個更大的模塊,感謝 leader 優先把這些學習的機會留給我, 很贊很贊。 Safe Browsing 的主要難點是對 WebView,,,我有些淺陋無知,對這個了解太少,組內人員也對這個沒有很多的了解,mentor 前期調研了一些瀏覽器的內核相關,最后決定里是利用 Webview 原生控件 Chrome 的內核去實現。(其實更挫一點,就是給 Webview 加了一個殼子而已)
在這一部分,印象深刻的就是 WebView 本身。它有太多的未知問題了,有太多的不可預測的,太多的特殊的情況。
我之前曾經 把在 WebView 實踐過程中遇到的問題,都記了下來,整理了兩篇文檔:
-
一篇是 WebView 的主要使用方法:
鏈接:WebView 的反思和記錄 ---定制設置和常見問題
這里主要記錄了 WebView 的常見設置和回調,
-
另外一篇是 在使用 WebView 的過程中遇到的問題:
里面詳細記錄了,我在實際的開發中遇到的問題,并提供了一些簡單的解決方案。
但其實這也是我沒有完善的一點,因為我依賴著 webview 的各種回調,但是這些回調往往不那么靠譜,我就在想,能不能把這些回調,再封裝一下,對外面提供一個相對好些的 WebView,但還沒時間去做,我覺著這是一個可以改進的地方,同時也會有難度。
我有時會自己做些總結,其實很多都是瞎寫著玩,有很多都沒有好好整理,因為自己看,所以很隨意,也沒有特別的上心。做完 Safe Browsing 后,可能是因為被 WebView 坑的比較有心得,可能也有一些成就感,所以好好的整理了上面的兩篇總結,放在了簡書上。 我想每一個寫過博客的人,都會為自己寫的文章得到肯定而高興, 我沒有想到的是,在簡書上收到了鴻洋的消息,想要轉載我的這兩篇文章,在征詢我的同意。我看到消息時,蠻驚喜的,也很開心,感覺自己寫了一篇能夠得到別人認可的文章,很有成就感。
5. 一個產生變化的過程,推薦機制
做 Safe Browsing 是一個很大的部分,需要處理很多很多邏輯上的情況,同時也需要考慮很多細小的細節,從開始做,到上線,差不多一個月的時間,20天左右。 再結束了這個部分后,我開始接觸了我們組當時在準備的一個新的慨念的實現,名字叫 推薦機制。
我覺著做推薦機制這一套時,對我產生了很大的影響。讓我開始從另外一個角度去思考代碼的設計,整個 app 里面的一些功能的設計,讓我開始站在一個更高的角度去思考和看待問題。
在推薦機制里面,有兩種定義,一類是 placement
, 稱之為位置,一類是 content
, 稱之為 內容。它處理的一個事情大概可以概括為 在一個 placement
上是否要顯示,以及要顯示哪個 content
。
一個 placement
對應多個 content
, 當有機會,或者是可以在這個位置顯示東西時,會根據 一個類似于權重的東西,依次去遍歷 這個 placement
下面注冊過的 content
, 并且判斷該 content
是否為 Isvalid()
, 如果為有效,則交給該 content
去顯示內容;如果無效,依次判斷下一個 content
, 直到所有的 content
都被遍歷過。
這是我第一次參與這類功能的設計,并體會到在這個設計中的一些變化,最終選擇了一個比較好的方式去處理該功能。 可能慢慢積累的量變,在某個點忽然變成質變一樣,我開始會在做功能時去著眼更高的角度去思考一些問題(至少也是自以為更高的角度吧,希望不是瞎吹)。
5.1 我和推薦機制
在后面,我做了一個小的部分 feature,這個 feature 是這個樣子,在一個 界面上的一部分 新增一個 ViewPager
,然后在這個里面去加載不同內容的卡片, 卡片的數量可能會變, 卡片的一些狀態要傳遞給 外面。
5.2 我實現的 “假” 推薦機制
下面是我的一些實現想法:
我的這個 要顯示的區域,可以看做是一個
placement
, 它要顯示的幾個 card 都可以簡單認為是content
;-
因為我要去顯示的這些 card,它們之間是平等的,是否可以定義一個統一的接口,讓它們都實現該 接口呢?
我便寫了一個
IRecommendCard
這個接口,在這個接口里面,我反復修改了挺多次它里面的方法, 我想要盡可能的把方法定義全面,同時也沒有多余的感覺。interface IRecommendCard { ... /** * 判斷該 card 是否有效,是否需要展示 */ boolean isCardValid(); /** * 初始化該 card 里的內容 */ void inflateCardView(); /** * 獲取該 card 的需要顯示的 view */ View getCardView(); /** * 標記該 card 的狀態,是在顯示狀態 還是 不顯示狀態 */ void markCardStatus(boolean isShowing); /** * 當該 card 可見時均會回調該方法, 它自身判斷是否需要更新當前 view 顯示的內容 */ void updateCardOrNot(); /** * 釋放該 card 的資源,可能是未結束的動畫之類的, 會在 銷毀時,或用戶關閉時調用 */ void releaseCard() ... }
其實還會有定義的一些接口 listener,把當前 card 的狀態返回給它的老大(顯示它的 host)
可能在大家看來是比較簡單的一種設計,可對當時的我來說,在做這個的過程中,感到很好玩,很有趣,很多內容有一種享受感,我比較菜,期待大家對我的有一些建議, 后面會有更多的可享受的地方,期待我會變得厲害。
-
有了這些接口后,我就把我要有的 card 繼承與該接口,復寫接口的方法時,在各個方法里添加每個 card 自己獨有的邏輯,布局,內容
其實這一部分是相對自由的部分。每個 card ,他的實現以及邏輯都應由它自身控制,而與外部無關,它應該把自身的一些狀態 反饋給外部,通過接口回調反饋。
如果把接口寫在 card 內部,card部分就不需要持有外部的一個引用, 但是外部會需要有每個 card 的監聽;
如果把接口寫在外面,card 部分實現了這個接口,就有了一個外部的引用,
想起了
Activity
里面的 內部類 接口HostCallBack
, ,Fragment
里面持有這個接口的對象, 利用FragmentActivity
里面封裝好的一些方法,實現了Activity
和Fragment
之間相互通信,時機互調。 在外部,一開始我在外部把這些所有的 card 都注冊到該
ViewPager
下, card 的顯示與否,跟外部完全沒有關系。
5.3 我對推薦機制的思考
我更多的覺著 推薦機制可以做很多東西,不只是這些方面的需求,有很多功能上,當我們換個思路去思考,是否會更加的合適呢?這是我在推薦機制里學到的東西,而且我有點形容不出來那種感覺,對外暴露的接口要準備的十分充分,對內的封裝要盡可能詳細。很棒的一種設計體驗,一種思考體驗。
6. 我對工具的思考
從開始進入到 IA 組,我的 leader 濤哥,就有介紹自己去寫一些便捷的工具。慢慢的,我也了解到組內很多好用的工具,后來,我也跟著開始嘗試著做一些有需求的工具。
既然是工具,必然是要達到某種功能的,我簡單的介紹一下我們組的工具。我說的這些工具,是在項目編譯時或者打包時起到一些作用, 我們利用 build.gradle
在里面設置了 task, 在特定的時候去做一些事情。例如,我們可以去跑一些腳本,去檢查,確保一些代碼里的內容, 如果發現有錯誤的地方,就打包不通過,修改之后,才能打包成功,我覺著這樣的好處是可以防患于未然,很多線上的錯誤,可以通過工具在打包的時候去確保。
當然工具不僅僅只有這個用處,我們可以直接去寫工具,可以把多語言的地方,刪除一個語言的時候,其他所有的多語言也一起刪除,會很大的節約時間;我們可以去檢查一些自定義 view 里面構造函數的實現,看它是否符合要求(復寫三個構造函數); 我們可以去檢查在 xml 寫入的 自定義 view 的路徑是否正確(現在可以通過 Lint 確保);我們可以去檢查是否有 scrollView 嵌套 recyclerView 的情況;我們可以去檢查一些配置項是否正確;等等,利用工具,我們做很多事情,很多加快效率的事情。
我們大部分是利用 python 去寫工具,我大二的時候曾經看過一段時間 Python,后來太長時間不去寫它,差不多全忘了,我去寫這個檢查自定義 view 的腳本的時候,大部分的時間都在查 如何用一些 Python,但我覺著,一門語言,不影響你對這個工具的實現,重要的是你得知道該如何去實現這個工具,從哪個角度出發,通過什么樣的方法,達到你的目的,而實現這些方法的語言代碼,不會就去查嘛,總會解決。對代碼的思考和理解,往往比代碼語言要重要的多。
想記下來的幾個問題:
-
build.gradle
我對這個文件的理解程度還不夠深, Groovy 是一個很強大的東西,我理解的點,不夠啊。。。接下來要深入這部分去了解下,不過我對 build.gradle 這里面的東西還是有一定了解的。(主要是看過一篇超級好的博客,在里面對照著學了很多)。
7. 對 ContentProvider 的思考
這個標題太大了,而我只能說出來一小部分。而且,很可能理解會出現偏差,但現在至少我是這么理解的,說出來嘛,有錯的話,大家一起指出來嘛,一起討論。
7.1 先從 ContentObserver 說起
ContentObserver
是內容觀察者,根據 Uri
觀察 ContentProvider
中的數據變化,,在 onChange()
方法中,回調通知。
使用步驟:
-
我們可以自行定義一個類,繼承于
ContentObserver
, 重寫它的onChange()
方法;private class MediaContentObserver extends ContentObserver { private Uri contentUri; .... @Override public void onChange(boolean selfChange) { super.onChange(selfChange); handlerMediaContentChange(contentUri); } }
-
當然,我們首先要 注冊這個監聽:
ContentObserver externalObserver = new MediaContentObserver(handler, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); getContext().getContentResolver().registerContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, false, externalObserver);
其中 我們創建
ContentObserver
時 傳遞的第二個參數uri
,就是media
對應的ContentProvider
的URI
!!!然后 getContentResolver().registerContentObserver(uri, boolean, ContentObserver...)
-
第一個參數 uri:
即需要監聽的 uri, 當 這個 uri 對應的 ContentProvider 數據發生改變時,就會通知到 注冊的第三個參數的 contentObserver 對象
-
第二個參數: boolean 表示是否要精確匹配到對應的 uri
當為 false 時,表示精確匹配,即只匹配該 uri 和它的祖先 ancestors(只有當該 uri 或者 URI 發生變化時,才會去通知注冊的 contentObserver);
當為 true 時,表示可以匹配到該 uri 的后代
我對 祖先和后代的理解為:
content://com.test.chen/room/bathroom
祖先為content://com.test.chen/room/
, 后代為content://com.test.chen/room/bathroom/some
第三個參數,就是我們注冊的 contentObserver 對象
-
-
注冊后,往往對應著反注冊!!!
在編程里面,有很多都是很有對稱美的,例如 activity 的 onCreate() 與 onDestroy().
我覺著這是一種編程中需要注意的思想,很多時候,我們都需要考慮到對稱,感覺谷歌在設計代碼時,也是加入了對稱的思想在里面,所以在我們設計代碼形式時,對稱的代碼更契合谷歌的思想,代碼也更好維護。
getContentResolver().unregisterContentObserver(externalObserver);
7.2 ContentProvider 與 它的 URI
在一個 手機系統中,每一個 ContentProvider
和它所對應的 URI
都是唯一的, ContentProvider
與 URI
也是一對一的!!!
因為 ContentProvider
可以跨進程使用,所以我們可以訪問到其他 app 下的 ContentProvider
。
上面我們注冊時傳入的 MediaStore.Images.Media.EXTERNAL_CONTENT_URI
, 就是 系統掌管 images
的 ContentProvider
對應的 URI
。
當我們的 app 里面 添加了一個 ContentProvider
時,它的 URI
必須不能和 手機系統上 所有的 URI
相同,當有重復時(即系統上已經有一個 ContentProvider
的 URI
是當前我們添加的 URI
), 則會安裝不到系統上。
7.3 ContentResolver 的作用
它可以幫助我們去查詢所有有關SD卡目錄下的一些文件信息,例如 媒體文件, 通話記錄,照片等。
同時我們注冊 ContentProvider
和反注冊時 都是通過 getContentResolver()
去獲取到 ContentResolver
對象。
ContentResolver
提供了與 ContentProvider
相同名字的方法,用于數據的增, 刪, 查, 改
它的 query()
方法用法示例:
//獲取到媒體庫中的照片文件,該文件滿足,時間降序的第一個文件,且只包含兩列數據
cursor = getContext().getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
new String[] {
MediaStore.Images.ImageColumns.DATA,
MediaStore.Images.ImageColumns.DATE_TAKEN
}, null, null,
MediaStore.Images.ImageColumns.DATE_ADDED + " desc limit 1");
//獲取通話記錄部分
// 去獲取到系統所有的通話記錄,并且只包含每個 callLog 的幾列數據:
//CallLog.Calls.NUMBER, CallLog.Calls.DATE, CallLog.Calls.CACHED_NAME, CallLog.Calls.TYPE
cursor = getContext().getContentResolver().query(CallLog.Calls.CONTENT_URI, new String[]{
CallLog.Calls.NUMBER,
CallLog.Calls.DATE,
CallLog.Calls.CACHED_NAME,
CallLog.Calls.TYPE }, null, null, CallLog.Calls.DEFAULT_SORT_ORDER);
注:
MediaStore.Images.Media.EXTERNAL_CONTENT_URI
是系統照片媒體庫對應的ContentProvider
的URI
而
CallLog.Calls.CONTENT_URI
是掌管系統通話記錄ContentProvider
的URI
;
對上述的理解和說明:
而上述兩個過程,都是通過
getContentResolver().query(...)
去跨進程取得了里面的數據;這些數據 通過
query
,delete
,add
,update
可以在多個進程操作數據, 本質上都是ContentProvider
對應的query
,delete
,add
,update
方法。-
而 增刪查改,這些對應的操作,為什么能夠跨進程去做呢???
其實本質上 上述過程 都是
ContentProvider
通過 匿名共享內存 在 app 之間進行數據共享的 !!!當傳輸的數據量比較大時,使用匿名共享內存來傳輸數據是有很大好處的,可以減少數據的拷貝,提高傳輸效率.注: 我對 匿名共享內存 的理解還不夠……
-
但是呢? 當傳輸的數據比較小,比較少時,使用匿名共享內存來作為媒介就有點浪費了,系統創建匿名共享內存也是有開銷的。 那還有沒有別的比較好的方式呢??? 有的
Bundle
要登場了~~~當我們在
Activity
之間跳轉時,也許會從 A 傳遞一些簡單數據到 B , 這個過程我們就可以先把這些數據放在Bundle
里面, 再通過intent
, 把bundle
放在 intent 里面的 extra , 傳遞到Activity
B 中 。 這與ContentProvider
有什么關系呢?ContentProvider
里面有一個函數Bundle call(...)
, 這個函數,是我們下面記錄的基礎。
7.4 我對 ContentProvider 的理解
上面我們說到,當有部分很小的數據需要在兩個進程間進行傳遞時,也利用 ContentProvider
進行操作。
假設進程 A, 進程 B ,都需要訪問一個數據 TestData
, 那么我們可以利用 ContentProvider
中利用 call()
函數, 利用 Bundle
去傳值。
例如:
// 在一個自定義的 ContentProvider 中
public static int getTestData(){
// 實質是 會去尋找對應 uri 的 ContentProvider ,然后調用它 里面的 call() 方法
Bundle bundle = getContext().getContentResolver().call(uri, METHOD_GET_TEST_DATA, null, null);
return null == bundle ? 0 : bundle.getInt(EXTRA_KEY_TEST_DATA, 0);
}
public static void setTestData(int test) {
Bundle bundle = new Bundle();
bundle.putInt(EXTRA_KEY_TEST_DATA, test);
getContext().getContentResolver().call(uri, METHOD_SET_TEST_DATA, null , bundle);
}
//復寫 的 call() 方法
@Nullable
@Override
public Bundle call(String method, String arg, Bundle extras) {
Bundle bundle = new Bundle();
switch(method) {
case METHOD_GET_TEST_DATA:
bundle.putInt(EXTRA_KEY_TEST_DATA, PreferenceHelper.getInt(PREF_KEY_TEST_DATA, 0)(此處為 value));
break;
case METHOD_SET_TEST_DATA:
PreferenceHelper.putInt(PREF_KEY_TEST_DATA, extras.getInt(EXTRA_KEY_TEST_DATA));
break;
case ...
default:
break;
}
...
return bundle;
}
在同一個 app 內,主進程 main process, 工作進程 work process, 如果我們想要實現 主進程和 工作進程之間的數據共享,那我們通過自定義一個 ContentProvider
, 并且像上述實現了 call()
方法,就可以成功的實現 簡單數據跨進程共享了。
注:
SharedPreference
本身在跨進程傳輸數據時,可能會出現數據不穩定的情況,所以,原生的SharedPreference
本身最好不要跨進程共享數據,如果需要跨進程共享數據,那就采用ContentProvider
包裝一層SharedPreference
, 在進行數據共享。
7.5 對 ContentProvider 的總結
對 ContentProvider
, 我個人現在會很享受利用 Bundle
在進程中傳遞數據這一塊,覺著很贊!
現在我的理解可能還會有局限或者不對的地方,慢慢補充,慢慢增強。
8. Theme 主題,動態加載 與 ClassLoader
這是我目前做過的所有 feature 里最讓我眼睛發光的一個 feature。
主要做的內容是為 applock 添加主題,添加主題的方式是 動態主題,通過下載 apk 去切換不同的主題樣式。
在這個過程中,前期調研,后期實現,都遇到了很多有意思特別好玩的事情,所以,印象深刻。
8.1 首先說一下 ClassLoader
它本身就有說不完的東西,撿一些我想說的說一下:
在 Android
中 的 ClassLoader
分為兩種,分別是系統 ClassLoader
和 自定義 ClassLoader
. 其中系統 ClassLoader
包含三種,分別是: BootClassLoader
, PathClassLoader
, DexClassLoader
.
8.1.1 在 app 啟動后,有哪些 ClassLoader
在 android 運行一個 app 時,其實不止一個 ClassLoader
在運行,而是兩個以上。
用代碼看一下:
ClassLoader classLoader = baseContext.getClassLoader();
if (classLoader != null) {
Log.i(TAG, "classLoader is " + classLoader.toString() + " --->from Log");
while (classLoader.getParent() != null) {
classLoader = classLoader.getParent();
Log.i(TAG, "classLoader is " + classLoader.toString() + " --->from Log in while");
}
}
打印結果為:
ThemeLayoutContainer: classLoader is dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.example.chenzhao.thememaintest-1/base.apk"],nativeLibraryDirectories=[/data/app/com.example.chenzhao.thememaintest-1/lib/arm, /data/app/com.example.chenzhao.thememaintest-1/base.apk!/lib/armeabi-v7a, /vendor/lib, /system/lib]]] --->from Log
ThemeLayoutContainer: classLoader is java.lang.BootClassLoader@f42ce93 --->from Log in while
從代碼中可以看到:
-
在我們的代碼中,獲取的
getClassLoader()
是PathClassLoader
PathClassLoader
里有一個對應的DexPathList
, 里面是它加載 class 的路徑,也就是說,它可以加載的類,都在這個路徑里面,超過這個路徑的,就不是它可以加載的了,超出了它的權限。 -
它的
parent
是BootClassLoader
, 且該對象的 id 為@f42ce93
,從打印的結果可以看到,
bootClassLoader
,為一個 具體的對象,且有id, 當我們去打印手機上其他 app 的parentClassLoader
, 發現 別的 app 里面也是java.lang.BootClassLoader@f42ce93
!!!是相同的一個對象,又經歷過測試,發現,這個
bootClassLoader
是同一個 對象,是在手機系統剛開始初始化時加載進來的,并且當每個 app 啟動時,會把該bootClassLoader
作為parent
,傳入給要啟動的 app!!BootClassLoader
加載一些系統Framework
層級需要的類,以后任何地方用到都不需要重新加載, 有共享作用。 -
提到這些就不得不說,雙親委托模型
在
ClassLoader
機制中,采用了雙親委托模型。即 當新建一個
ClassLoader
時,會如下://構造 ClassLoader(ClassLoader parentLoader, boolean nullAllowed) { if(parentLoader == null && ! nullAllowed) { throw new NullPointerException("parentLoader == null && !nullAllowed"); } parent = parentLoader; }
可以看到, 創建一個
ClassLoader
時,需要使用一個 現有的ClassLoader
實例作為parent
,這樣一來,所有的ClassLoader
都可以利用一顆樹聯系起來,這是ClassLoader
的雙親委托模型。雙親委托模型加載類時,是通過
loadClass()
方法,它的主要邏輯如下:會先查詢當前
ClassLoader
是否加載過此類,加載過就返回;如果沒有,查詢 它的
parent
是否已經加載過此類,如果有,就直接返回parent
加載過的類;如果繼承路線上的
ClassLoader
都沒有加載,則會有它去加載該類工作;
當一個類被位于樹根 的
ClassLoader
加載過,那么, 在以后整個系統的生命周期內,這個類永遠不會被重新加載。所以
BootClassLoader
用于加載系統Framework
層的類,是所有 app 內其他Classloader
的parent
。
8.1.2 PathClassLoader 是什么時候創建出來的???
當 app 啟動時,它里面所有的類,應該都是 經過 ClassLoader
加載 進來的, 那么做為主要加載 app 內的 類的 PathClassLoader
是如何被創建出來的呢?
這關系到 app 啟動時的一系列操作
app 啟動時,
AMS(ActivityManagerService)
會首先 啟動我們 app 的進程,通過ActivityManagerService.startProcessLocked()
啟動進程在
Process.start()
會真正啟動的代碼,在start()
方法會通過socket
通信 向Zygote
進程發起進程啟動請求,進程起來后,再通過反射 調到ActivityThread.main()
方法;在
ActivityThread.main()
方法里面,會有一個主要的操作,new
一個ActivityThread
對象,然后調用了thread.attach(false)
;-
其實 在
thread.attach()
里發生了很多很多操作!!!thread.attach()
--->attachApplication()
--->thread.bindApplication()
--->
handleBindApplication()
在
handleBindApplication()
里面 :Application app = data.info.makeApplication(data.restrictedBackupMode, null); mInitialApplication = app;
這里
data.info
得到的是一個LoadedApk
對象; 也是在這里,創建出了application
,那么
data.info.makeApplication()
里面的細節是怎樣的呢? -
調用
LoadedApk.java
,ApplicationLoaders.java
部分在
LoadedApk.makeApplication()
里面的代碼 會去獲取 classLoader, 并且創建 appContext, 再通過 classLoader 和 appContext 去創建 application 對象;// LoadedApk.java public Application makeApplication(boolean forceDefaultAppClass, Instrumentation instrumentation) { ... // getClassLoader() 是去獲取一個 與當前 apk 相關聯的 pathClassLoader, java.lang.ClassLoader cl = getClassLoader(); ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this); // app 為 application app = mActivityThread.mInstrumentation.newApplication( cl, appClass, appContext); ... // 通過 instrumentation.callApplicationOnCreate() 去調用 app 的 onCreate() 方法,即 application 的 onCreate(); instrumentation.callApplicationOnCreate(app); }
所以,我們可以看到,
ClassLoader
要 先與application
創建出來。
-
去看
ClassLoader
具體的得到邏輯,java.lang.ClassLoader cl = getClassLoader()
具體的代碼實現:
public ClassLoader getClassLoader() { synchronized (this) { if (mClassLoader == null) { createOrUpdateClassLoaderLocked(null /*addedPaths*/); } return mClassLoader; } }
在看一下
createOrUpdateClassLoaderLocked()
的實現://LoadedApk.java private void createOrUpdateClassLoaderLocked(List<String> addedPaths) { ... // 主要關鍵的地方 mClassLoader = ApplicationLoaders.getDefault().getClassLoader( "" /* codePath */, mApplicationInfo.targetSdkVersion, isBundledApp, librarySearchPath, libraryPermittedPath, mBaseClassLoader); }
對
mClassLoader
進行了賦值,那在ApplicationLoaders.getDefault().getClassLoader()
是如何操作的呢?對于
ApplicationLoaders
這個類,它里面維護了一個mLoaders
, 它是一個map
,key
為string
(可以看做是 包名),value
為ClassLoader
(類加載器),看一下
getClassLoader()
的實現:public ClassLoader getClassLoader(String zip, ... ) { ... // 首先 檢查 mLoaders map 里 是否有該 loader, 有即返回, //沒有則創建一個新的 pathClassLoader, 并把新建的 loader 加入 mLoaders ClassLoader loader = mLoaders.get(zip); if (loader != null) { return loader; } PathClassLoader pathClassloader = PathClassLoaderFactory.createClassLoader(...); ... mLoaders.put(zip, pathClassloader); return pathClassloader; ... }
可以看到, 我們確實是創建了一個
PathClassLoader
對象,并且是與 apk的 zip 路徑相關的(其實這里就是 在前面我們打印出的data/app/...
里面的dexPathList
!!!)每個 app 進程都有唯一的
ApplicationLoaders
實例, 后續則通過 apk 的路徑 (zip 參數)查詢返回ClassLoader
, 因為ApplicationLoaders
里 維護了ClassLoader
的一個map
常量mLoaders
, 所以 一個進程可以對應多個 apk !!!其實在這個 任務中 我就是利用了主 app 去加載了 theme apk 中的資源,那么其實本質上就是 我的當前進程中持有著 theme apk 對應的
ClassLoader
8.1.2 總結:
這一節中,我們發現了在 thread.attach()
時 去創建了 PathClassLoader
, 同時我們也發現, PathClassLoader
是對應具體的 apk 的 zip , 也就是說 PathClassLoader
只可以讀取當前 apk 里面的類。再次 PathClassLoader
創建的時機要早于 Application
的創建。
8.2 我對 appLockTheme 的具體實現
在后期的討論中,mentor 和 leader 覺著,我們只是從另外一個 apk 里去拿取資源文件,這樣比較簡單,并且可以快速的完成 theme apk 的開發。 而且,theme 這個本身也是一個大量的個數,如何短時間內可以上線大量的 theme 是一個比較重要的問題,只有當 theme。盡可能簡單,盡可能不涉及到 theme apk 后期的升級問題,那么我們就要保證,每一個 theme 里面都不存在 bug, 那最簡單的做法就是 我們只去 theme 里面去取 資源,不涉及或者少涉及 Java 代碼。
8.2.1 如何從主 app
中拿到 theme app
中的資源? 即資源訪問
這是實現過程中最重要的一點,如何能拿到 theme apk 下的資源。
其實在 android 中,我們通常去拿一些資源,總是通過 getContext().getResources()
去獲取到我們想要使用的資源。所以,當我們可以獲取到 theme app 中的 context
,按照道理來講,我們便可以訪問 theme app 中的任何資源了。
注: 如果深入代碼去找,會發現 最后是通過
AssetManager.addAssetPath()
去加載資源的,
獲取 Context
Context context = null;
try {
context = createPackageContext(packageName,
Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
android 給我們提供了 createPackageContext(String packageName, @CreatePackageOptions int flags)
這個方法,可以獲取到一個 theme app 的 context
, 該 context
和該 theme app 正常 launched
啟動時的 context 是同樣的。
獲取到該 themeContext
后,我們便可以去拿到我們想要的資源了,代碼如下:
// 這是 Resources 里的一個方法,返回該資源對應的 ID
public int getIdentifier(String name, String defType, String defPackage) {
...
}
// 第一個參數 name : 我們想要的資源的名字, 即在theme app 中 該資源的ID
// 第二個參數 defType: 想要資源的類型, drawable, dimen, color ...
// 第三個參數 defPackage: 當 context 找不到時,默認去尋找的包名,可以為null
它的具體使用如下:
// themeContext 為上面我們獲取到的 有關 theme apk 的 context
Resources themeResource = themeContext.getResources();
themeResource.getDrawable(themeResource.getIdentifier(drawableName, "drawable", packageName))));
// 即可得到該 drawable
8.2.2 我對 theme 的前期調研部分 -- 從 theme apk 中取 Java 代碼
在前期的調研中,我們的目標是 完全把整個鎖屏界面給出去,全部由 theme apk 實現,這樣的話,我們就可以更自由的,隨心所欲的去實現不同的 樣式,而我們只需要從 theme 中獲取到 一整個 View
, 然后把它塞進我們的主 app 里面的具體地方。
獲取到 整個布局代碼如下:
// packageName 是指 要取資源的apk的包名, layoutName 是要取得對應的 layout 的名字
private View getRemoteLayout(String packageName, String layoutName, ViewGroup parent) {
Context context = null;
try {
context = baseContext.createPackageContext(packageName,
Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
if (context != null) {
int resId = context.getResources().getIdentifier(layoutName, "layout", packageName);
return LayoutInflater.from(context).inflate(resId, parent, false);
}
return null;
}
實際操作過,確實可以獲取到該 View
,并把它塞到我們想要顯示的地方。
然后我們可以通過反射去調用 該 View
里面的方法,invoke()
實現:
// object 是上面我們獲取到的view, methodName, 是我們想要調用的方法
public Object invokeObjectMethod(Object object, String methodName) {
Log.i(TAG, "invokeObjectMethod() ");
if (object == null) {
return null;
}
Object returnValue = null;
try {
Class<?> themeAnimationContainerClass = object.getClass();
returnValue = themeAnimationContainerClass.getMethod(methodName).invoke(object);
Log.i(TAG, "invokeObjectMethod() returnValue is " + returnValue);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
return returnValue;
}
但是!!!當我們需要傳遞操作,或者在 theme 的 view 里面需要獲取一個時機去反饋到主 app 內,該如何實現呢???
一般我們的思路都是 回調!!! 接口回調!!! 但是 在上面 PathClassLoader
我們已經知道,加載 我們 主 app 的 ClassLoader
和 加載 theme apk 的 ClassLoader
不是同一個 ClassLoader
,那么 同一個接口 即是包名,接口名完全相同,在這里的識別也是不同的類,那么怎么操作呢?
這里提供兩種實際測試實現的方法:
-
參數為
Handler
, 在 theme app 類 B 中通過Handler
發送消息,在 main app 類 A 中去接受這些消息;是可行的,但也有一些問題,
Handler
處理是異步的; -
在 在 theme app 類 B 中,在時機到了的地方,再通過反射去回調 main app 類A 中的方法;
但是這種方法在實際代碼中肯定是不太可行的,因為我們的主 app 一般都是混淆后才打包的,所以對于混淆后的代碼,在通過反射是找不到的。
注:網上很多動態加載,動態打補丁的做法是如何實現的呢?
在這里我只有一個比較淺的認識,等待后續學習。其實在上面的分析中,我們可以發現要實現跨 apk 去利用同一個 ClassLoader
是 加載不同的類,利用 PathClassLoader
是完成不了的,需要用到的是 DexClassLoader
.
8.2.3 補充部分:ClassLoader
識別同一個類
同一個
class
= 相同的ClassName
+ 相同的PackageName
+ 相同的ClassLoader
由一個
ClassLoader
加載,并且該ClassLoader
里沒有和它一樣的目錄下的文件名,那么它就是唯一的當由同一個
ClassLoader
加載,并且是在相同的目錄下,相同的文件名, 如果有兩個時,加載時便會出錯
8.3 Theme 部分總結
做 theme 這部分時,其實整個人是極其興奮的,當時還發了個朋友圈,說磨刀霍霍向豬羊,很好玩。做這部分時,從前期的調研,到后期的實現,經歷了很多,經歷過很多修改和調整。
leader 和 mentor 對我這部分做了很嚴苛的要求,對一些實現方式不好的地方都換了,所以,在這個過程中我學到了很多,學到了超多的東西;
同時,因為這個涉及到加載大量圖片,因此 leader 對性能做了很高的要求,力爭少一點,再刪少一點內存開銷,讓不需要這些圖片自由時手動釋放;
為了后面一個小時便可以上線一款新的 theme apk, 我們做了很多設想,考慮了很多情況力爭打造得足夠強大和完善,這也是一個十分鍛煉人的過程;
也是通過這個 feature 我對很多東西的理解都加深了,例如
ClassLoader
,ContentProvider
。
9 總結
不知不覺,畢業已經快要一年了,真的是一個忙碌的一年。
上面是一個極長的記敘文……,還是不成熟的記敘文,能看完看到這里的都是真愛啊。。。
寫著寫著就發現這個過程被拉長了,被細節化了,還有很多知識點沒有能夠寫進去,因為文章的長度已經被拉伸得很極限了。
下面可能是一部分的(假)議論文了……
從華科畢業,到如今2018年的6月,真的是一轉眼的事情,生活方式變了,生活習慣也變了,愛好變成了工作,還好,我還有很多的興趣,總覺著做著喜歡的工作,還有人給錢,是一件十分不錯的事情。在北京見識了很多有趣的人,見識了很多未接觸過的事物,在公司里面,有一個遺憾,就是沒有很好的公司朋友,工作快要一年了,最熟悉的還是 mentor,我們兩經常會一起吃飯,一起看代碼,一起討論問題,一起聊天,但其實在公司熟悉的人特別少,可能公司盡可能的減少紐帶吧,同時我本身的性格就是不會主動會去結識人,不是因為社交能力不夠,而是覺著沒必要,我在這方面可能比較佛系,覺著有些重要的人,你遲早會認識,,有些人,即使你認識了,也……沒什么用處啊,我不是一個和領導什么要搞好關系的人,我更覺著人與人相處是要自然的,所以我喜歡互聯網,所以,我喜歡北京,在互聯網公司,更受尊重的是你的技術能力和解決問題的能力,是一個相對公平的地方,是一個相對會肯定你能力的地方。其實我也渴望能有一些像在華科在聯創的一些朋友,那些你一眼看去便覺著是開心,在我們組我們幾個人經常會一塊吃飯,有時覺著這樣很贊,吃飯的時候偶爾說些其他的,技術上的,游戲上的,體育上的,一隊人一起吃飯,總會讓我想起在華科聯創的日子,那些日子,很美好啊。
同時在北京生活,也是壓力很大的生活,與學校不同的地方是你要自行去處理生活上的所有事情,衣食住行。記得剛來北京那會,我媽對我……可能也是真的放手吧,讓我開始不依靠家里,不從父母那邊拿錢去生活。拿著很少的錢來北京,果不其然,第一個月就艱難了,后來慢慢發了工資,最起碼開始養活自己了。那種感覺,與其說開心,倒不如說釋然。釋然自己能夠讓人放心,讓這個社會放心。
跌跌蕩蕩,起起伏伏,有很多事情會造成你成長路線上的偏折,那些事情,說不好是好的,還是壞的事情,影響著你,影響著你未來的走向,但其實更多的是自主的意志,你決定了要怎么走呢?我很開心,在我不太好的時候,能有一些朋友真心幫助我,還有我的 mentor ,在我不好的時候拉了我一把,在我迷茫的時候能夠給我支持……到了這個年紀,再次提起迷茫這兩個字,有種無法放下的感觸,可能當我們前行不知所措時,當我們感到遇到瓶頸時,當我們感到周圍的一切忽然陌生時,當我們感覺不到自己的位置時,迷茫,這種感覺總會隨之而來。未來應該也會有迷茫再次來找我,當下次它要來的時候,我希望我能比這次感覺要淺一些,慢慢的稀釋它。
總之,這一年過去了,我呢?成長的不夠的話,下一年就補回來,成長足夠的話,那就賊好,再接再厲!
和代碼打交道,是一件簡單幸福的事~
下個一年,有水水在身邊~
下個一年,是否還會繼續沿著我預想的方向發展呢~
下個一年,會碰到哪些好玩的事情呢~
下個一年,會碰到哪些困難的事情呢~
下個一年,會上升到一個什么高度呢~
下個一年,是否會更強更靠譜呢~
下個一年,我決定了,那就努力去做~
下個一年,我在期待和不安中等著它的到來~
下個一年,倒不如說,我想要主掌著它的走來~
下個一年,多多努力~~~
我很喜歡利文斯頓,他的轉身后仰跳投簡直美如畫~
我一直努力奔跑,只為追上那個曾被寄予厚望的自己~