901
京東網上商城
android,java知識點總結
monkey
List ,HashMap ,set ,HashTable ,eventBus ,LitPal ,OkHttp ,Glide ,熱修複,線程安全,線程池,6.0以及5.0權限管理,7.0分屏畫中畫;材料設計語言,藍牙,wifi,ViewDrapHelper,動畫,排序,RandomAccess,Rxjava,注解,設計模式。類設計,項目結構設計。功能設計。泛型數據結構。SqlLiteOpenHelper。內存泄漏。adb命令等等
1、使用aapt進行apk信息的查看
aapt dump dadging 跟上apk路徑
(必須要注意要配置aapt 環境變量)
2、特殊字符在string.xml中需要進行轉義
一般需要注意的就是< 、> 、@,?等等
可以使用<MSGCONTENT><![CDATA["<( ̄︶ ̄)>" ]]></MSGCONTENT> 類似這樣進行包裝;
也可以是使用 \ ,使用
比如顏文字
<!--顏文字 -->
<string-array name="emoji_array">
<item>⊙▽⊙</item>
<item> ( ؕؔʘ̥̥̥̥ ه ؔؕʘ̥̥̥̥ )? </item>
<item>( •̅_•̅ ) </item>
<item><MSGCONTENT><![CDATA["<( ̄︶ ̄)>" ]]></MSGCONTENT></item>
<item>(๑ ̄ ̫  ̄๑) </item>
<item>눈_눈 </item>
<item>ᕙ(⇀‸↼‵‵)ᕗ</item>
<item> ( ・᷄ ᵌ・᷅ ) </item>
<item>(৹ᵒ̴̶̷᷄﹏ᵒ̴̶̷᷅৹) </item>
<item>( ˉ ⌓ ˉ ๑) </item>
<item> o(〃\'▽\'〃)o </item>
<item>₍₍ (̨̡ ‾᷄ᗣ‾᷅ )̧̢ ₎₎ </item>
<item>( ¬_¬) </item>
<item>( ゚皿゚) </item>
<item>(▭-▭) </item>
<item>π_π </item>
<item>(¬ω¬) </item>
<item>(。•ˇ‸ˇ•。)</item>
<item>(。・ω・。)ノ</item>
<item>ू(ʚ̴̶̷́ .̠ ʚ̴̶̷̥̀ ू)</item>
<item>ლ(●ↀωↀ●)ლ</item>
<item>(ノ ̄д ̄)ノ</item>
<item>⸂⸂⸜(രᴗര๑)⸝⸃⸃</item>
<item>-_-||</item>
<item>ɿ(。・ɜ・)ɾ</item>
<item>p(´⌒`。q)</item>
<item>⊙ω⊙</item>
<item>ꉂ ೭(˵¯̴͒ꇴ¯̴͒˵)౨”</item>
<item>(ღ˘⌣˘ღ)</item>
<item>→_→ </item>
<item>〜( ̄△ ̄〜)</item>
<item>←_←</item>
<item>؏؏ᖗ乛◡乛ᖘ؏؏</item>
<item>\@_@</item>
<item>(^3^)</item>
<item>≥﹏≤</item>
<item>^ω^</item>
<item>^_^</item>
<item>\^O^/</item>
<item>T_T</item>
<item>◑▂◐</item>
<item>-_-#</item>
</string-array>
3、android EditText
在獲取到焦點的時候就會彈出軟鍵盤,然後是不會執行onClick方法的,因為第一次點擊如果是沒有獲取到焦點的話首先會獲取焦點,然後下一次點擊才會執行onCLick,如果需要首先執行onCLick的話,那麼就要在布局中設置
android:clickable="true"
android:focusableInTouchMode=“false"
然後點擊的時候會執行onClick,然後在裏邊可以設置
mEditText.setFocusable(true);
mEditText.setFocusableInTouchMode(true);
mEditText.requestFocus();
mEditText.requestFocusFromTouch();
然後就可以獲取到焦點,然後在做軟鍵盤的彈出
4、EditText 添加onScrollListener
隻在5.0以上(不包括5.0)有這個方法,必須要保證最小編譯版本在23
5、關於List源碼學習:
List 是一個接口,它繼承了Collection接口,Collection繼承了Iterable接口,
Iterable裏邊定義了Iterator,foreach,spliterator這麼三個方法
Collection繼承了以後在裏邊添加了add ,remove,clear,hashCode,equals,spliterator,retainAll,
addAll,removeAll,toArray,containsAll,contains等方法
List在繼承了以後添加了get,set,indexOf,lastIndexOf,listIterator,subList,replaceAll,sort這幾個方法。
我們在使用List的時候都是使用它的實現類ArrayList,LinkedList,Vector。
ArrayList的實現:
在實現了List接口以後我們會重寫裏邊所有的方法,然後實現對應的功能,
ArrayList還繼承了AbstracList這個抽象類,這個類繼承自AbstractCollection,然後實現了List接口(抽象類的主要作用就是實現共有部分功能)
但是大部分功能都是在ArrayList中進行的實現。
ArrayList提供了三個構造方法
1、默認的無參構造方法,默認長度為10
public ArrayList() {
super();
this.elementData = EMPTY_ELEMENTDATA;
}
2、指定集合長度
public ArrayList(int initialCapacity) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];
}
3、以集合為參數
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
size = elementData.length;
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
}
其中的elementData是一個object類型的數組,用來存放集合中的元素。
集合默認長度為10,如果要添加的元素超過了這個長度就會進行擴容,
調用ensureCapacity,ensureExplicitCapacity,ensureExplicitCapacity,grow()
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
最後是通過Arrays的copyOf方法複製一個新的長度為newCapacity的數組,新的長度就等於老的長度除以2 + 老的長度,然後在進行下邊的判斷計算最終的長度。
所以從這些流程可以看到ArrayList在數據頻繁添加的過程中會有頻繁的擴容操作,因為會有數組的copy過程,如果數據量很大的時候會影響性能,最後是能確定他的最終大小,在初始化的時候直接申請。
而且ArrayList是基於數組來進行數據元素的存儲,所以在插入的過程中都會有元素的移動,插入速度相當慢。
查詢
6 、adb monkey
首先確保android的adb命令是ok的。
然後就是monkey命令的參數說明。
adb shell 進入手機命令行
然後就可以執行 monkey 看到 monkey的參數說明
shell@PD1624:/ $ monkey
usage: monkey [-p ALLOWED_PACKAGE [-p ALLOWED_PACKAGE] ...]
[-c MAIN_CATEGORY [-c MAIN_CATEGORY] ...]
[--ignore-crashes] [--ignore-timeouts]
[--ignore-security-exceptions]//忽略安全檢查
[--monitor-native-crashes] [--ignore-native-crashes] //忽略native 異常
[--kill-process-after-error]//發生錯誤時殺掉進程
[--hprof] //生成hprof文件
[--pct-touch PERCENT] [--pct-motion PERCENT]//該種類型事件的百分比
[--pct-trackball PERCENT]//軌跡球的事件百分比
[--pct-syskeys PERCENT]//係統事件百分比
[--pct-nav PERCENT] [--pct-majornav PERCENT]
[--pct-appswitch PERCENT] [--pct-flip PERCENT]
[--pct-anyevent PERCENT] [--pct-pinchzoom PERCENT]
[--pct-permission PERCENT]
[--pkg-blacklist-file PACKAGE_BLACKLIST_FILE]//黑名單
[--pkg-whitelist-file PACKAGE_WHITELIST_FILE]//白名單
[--wait-dbg] [--dbg-no-events]
[--setup scriptfile] [-f scriptfile [-f scriptfile] ...]
[--port port]
[-s SEED] //設置隨機種子數,如果倆次seed值相同,那麼對於同一事件那麼執行的結果完全相同
[-v [-v] ...]
作用:命令行上的每一個-v都將增加反饋信息的詳細級別。
Level0(默認-v),除了啟動、測試完成和最終結果外隻提供較少的信息。
Level1(-v -v),提供了較為詳細的測試信息,如逐個發送到Activity的事件信息。
Level2(-v -v -v),提供了更多的設置信息,如測試中選中或未選中的Activity信息。
[--throttle MILLISEC] [--randomize-throttle]//事件的延遲時間(這個時間設置了以後不是特別準備,如果需要確定要跑多久的monkey的話需要計算執行一次時間所花費的時間,然後用時間除以一個事件的時長算出要執行的次數)
[--profile-wait MILLISEC]
[--device-sleep-time MILLISEC]//設備睡眠事件
[--randomize-script] //隨機腳本
[--script-log]//腳本log
[--bugreport]//bug報告
[--periodic-bugreport]//定期bug報告
[--permission-target-system]
COUNT //數量
>> 使用這個命令輸出monkey日誌到目標地址的文件中(這個不是monkey命令,是命令統一的輸出方式)
從上邊可以發現一個monkey命令的完整寫法
- 簡單的命令: monkey -v -p com.android.bluetooth 200 -v log級別 -p 後邊跟包名 200 就是要執行事件的次數 這就是一個簡單的monkey測試
- monkey -v -p com.guoyi.qinghua --pct-majornav 20 --monitor-native-crashes --ignore-security-exceptions --ignore-crashes --ignore-timeouts --kill-process-after-error -s 220 --throttle 300 500000 >> ../monkey.txt
- 因為monkey在測試過程中事件的延遲時間(這個時間設置了以後不是特別準確,如果需要確定要跑多久的monkey的話需要計算執行一次時間所花費的時間,然後用時間除以一個事件的時長算出要執行的次數) 如果--throttle 1000 ,那麼計算次數的方式 count= (時長 單位毫秒)/(一次事件的時間) monkey -v -v -v -p com.xxxx.xxx --throttle 1000 count
7、關於進入手機設置界麵
可以通過抓包來獲取當前ActivityManager來獲取,當前跳轉的activity的信息。下邊是adb 命令
adb logcat | grep ActivityManager
進入係統設置的所有界麵都可以通過Intent
隻要設置不同的Settings就可以
Intent appIntent = new Intent();
appIntent = new Intent(Settings.ACTION_SETTINGS);
context.startActivity(appIntent);
隻要替換Settings的值就可以,
如果要進入其中一個應用的詳情設置了Settings以後的話還需要傳遞包名
appIntent.setData(Uri.parse("package:" + context.getPackageName()));
但是android係統沒有專門的權限管理。
如果要進入權限管理必須要對不同係統進行區別,然後根據不同的定製係統來進行處理跳轉。
//vivo權限管理界麵
appIntent.setClassName("com.iqoo.secure", "com.iqoo.secure.safeguard.PurviewTabActivity");
然後可以通過adb 命令來啟動一個頁麵
db shell am start -n com.android.settings/com.letv.leui.settings.LeUIMainSettings
8、關於EventBus源碼學習
EventBus 的學習首先從 EventBus這個類開始。
這個類主要是用來進行EventBus的初始化,注冊反注冊,發送事件或者移除事件,是整個工具所暴露的可以進行操作的一個工作接口。
EventBus在初始化上邊有倆種方式:
(1)、通過getDefault()
(2)、通過new EventBus()
但是實際創建方式都是通過Builder(建造者模式來進行創建)
跟所有的建造者模式一樣,都是通過一個私有的構造方法以EventBusBuilder對象為參數來進行對象的創建。
然後調用register來進行注冊。在調用register這個方法的時候會傳遞一個要注冊的對象的。
然後會通過反射來獲取這個對象中的所有的方法。然後會通過方法來找到其中訂閱了EventBus的事件的方法。然後通過subscribe這個方法,把這些方法添加到訂閱列表中。
public void register(Object subscriber) {
Class<?> subscriberClass = subscriber.getClass();
List<SubscriberMethod> subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriberClass);
synchronized (this) {
for (SubscriberMethod subscriberMethod : subscriberMethods) {
subscribe(subscriber, subscriberMethod);
}
}
}
反注冊的流程與一般的訂閱者都是一樣的,把訂閱的方法都從訂閱隊列進行刪除。
剩下的就是事件的發送:
因為整個EventBus的架構就是觀察者模式。
所以事件發送其實就是通過EventBus對象對所有訂閱了這個事件的訂閱者進行通知,然後在他訂閱的方法裏邊就可以進行處理。
public void post(Object event) {
PostingThreadState postingState = currentPostingThreadState.get();
List<Object> eventQueue = postingState.eventQueue;
eventQueue.add(event);
if (!postingState.isPosting) {
postingState.isMainThread = Looper.getMainLooper() == Looper.myLooper();
postingState.isPosting = true;
if (postingState.canceled) {
throw new EventBusException("Internal error. Abort state was not reset");
}
try {
while (!eventQueue.isEmpty()) {
postSingleEvent(eventQueue.remove(0), postingState);
}
} finally {
postingState.isPosting = false;
postingState.isMainThread = false;
}
}
}
然後postSingleEvent ->postSingleEventForEventType->postToSubscription 然後執行真正的事件發送。
private void postToSubscription(Subscription subscription, Object event, boolean isMainThread) {
switch (subscription.subscriberMethod.threadMode) {
case POSTING:
invokeSubscriber(subscription, event);
break;
case MAIN:
if (isMainThread) {
invokeSubscriber(subscription, event);
} else {
mainThreadPoster.enqueue(subscription, event);
}
break;
case BACKGROUND:
if (isMainThread) {
backgroundPoster.enqueue(subscription, event);
} else {
invokeSubscriber(subscription, event);
}
break;
case ASYNC:
asyncPoster.enqueue(subscription, event);
break;
default:
throw new IllegalStateException("Unknown thread mode: " + subscription.subscriberMethod.threadMode);
}
}
說是發送,其實是通過反射來調用被訂閱的方法。(隻要我們能拿到被訂閱的方法所在的類的對象,就可以執行他裏邊的方法)
也就是通過EventBus中的invokeSubscriber方法
void invokeSubscriber(PendingPost pendingPost) {
Object event = pendingPost.event;
Subscription subscription = pendingPost.subscription;
PendingPost.releasePendingPost(pendingPost);
if (subscription.active) {
invokeSubscriber(subscription, event);
}
}
void invokeSubscriber(Subscription subscription, Object event) {
try {
subscription.subscriberMethod.method.invoke(subscription.subscriber, event);
} catch (InvocationTargetException e) {
handleSubscriberException(subscription, event, e.getCause());
} catch (IllegalAccessException e) {
throw new IllegalStateException("Unexpected exception", e);
}
}
還有些要注意的類,就是HandlerPoster,這個類主要是維護了一個隊列,用來管理要發送的事件。他的內在實現其實是通過Handler來進行的。handler就不需要多說了。
AsyncPoster 異步Poster,通過實現Runnable,通過ExectorService 進行線程管理。
BackgroundPoster後台Poster,通過實現Runnable進行實現,通過ExectorService 進行線程管理。
它使用的是Executors.newCachedThreadPool創建的緩存線程池。具體什麼事緩存線程池可以看相關的文章。
private final static ExecutorService DEFAULT_EXECUTOR_SERVICE = Executors.newCachedThreadPool();
總結:
EventBus的整體架構是通過 觀察者模式來進行事件的訂閱,然後在訂閱的事件發生了改變以後通知所有的訂閱者。在這個過程中使用到了反射來進行。在訂閱的時候通過反射獲取到當前對象所定於的方法,在有改變的通過反射執行被訂閱的方法,然後就可以處理自己的邏輯。
在發送事件的時候(post)我們會判斷當前事件是在什麼線程中執行。然後做相應的處理。
對與事件處理主要使用了Handler(初始化的時候默認是主線程Handler),
使用線程池進行事件的管理。
裏邊用到了多線程的並發處理:使用ThreadLocal ,CopyOrWriteArrayList;synchronized。
ThreadLocal 會為每一個線程創建一個副本對象,CopyOrWirteArrayList是通過copy一份數據,然後在Write的時候通過創建一個新的對象,然後對新的對象進行write,然後把新對象賦值給舊對象來保證線程安全。讀取的時候通過使用舊的對象來進行,因為讀取是不需要關注線程安全問題。
synchronized同步鎖,隻有持有這個方法的所的對象才能進入這個方法,其他的都要等待。
9、關於RecycleView的使用
使用RecyvleView的方式很簡單,跟使用ListView,GridView類似,隻是RecycleView可以實現比這倆個控件更多的功能。隻要通過LayoutManager進行設置。
RecycleView沒有onItemClickListener,需要自己實現。
RecycleView 如果有錯位的問題,那麼肯定也是跟ListView一樣因為複用引起的,其實修改原理都類似。個人覺得隻要把複用的View裏邊的參數都還原成最初的狀態(如果view成為最原始的狀態,那麼隻要數據沒有問題,就不會有錯位的事了),然後通過tag來進行判斷,然後在更新View即可。
RecycleView的adapter裏邊強製使用VIewHolder。為了減少重複創建對象的消耗(主要是會影響性能)。
RecycleView 滾動條的設置使用的是View的通用滾動條設置。
android:scrollbars=“horizontal" //設置滾動條的方向
android:scrollbarAlwaysDrawHorizontalTrack=“true" //設置總是繪製橫向滾動條
android:scrollbarThumbHorizontal=“@drawable/scrollbar_line”//橫向滾動條
android:scrollbarTrackHorizontal=“@drawable/scrollbar_line_track”//橫向滾動條的軌道
android:paddingBottom="20dp"
android:scrollbarStyle=“outsideInset" //設置顯示的位置
android:fadeScrollbars=“false" //是否一直顯示
如果要設置不一樣粗細的滾動條與軌道,可以通過使用不同粗細的進度圖片來實現,也可以通過
使用shape來自定義滾動條與軌道,自定義的時候滾動條的高度要比軌道的高,這樣橫向滾動條與軌道就會不一樣粗細。
10、fresco是一個很強大的圖片加載庫,不同的功能都有相對應的庫模塊來處理。
11、android中點擊電源鍵關閉屏幕的話。
如果現在屏幕是豎屏沒有任何影響,如果是橫屏,會銷毀activity,然後在啟動這個Activity 。會導致activity數據的丟失。如果隻是數據可以通過onSaveInstance進行保存,但是如果正在進行通話或者長連接的功能,會導致功能中斷。
解決方式:
1、使屏幕常亮
2、設置activity的
android:configChanges=“orientation|keyboardHidden|screenSize"
12、jni 編譯 :
(1)首先項目的sdk地址
創建native方法,然後點擊studio的make project生成class文件
(2)然後切換到項目package目錄下 (例如 com.xxx.xxx.xx)
調用 javaH -d ../jni “包名 + 類名”
然後會在jni文件夾下生成.h頭文件。
然後創建c或c++文件,進行代碼的編寫,可以直接複製頭文件到c或c++文件中。
提供的頭文件裏邊的參數需要自定義名稱。是一個空的實現。
(3)配置app.gradle文件
ndk {
moduleName"jnilib"
ldLibs "log", "z", "m"
abiFilters "armeabi", "armeabi-v7a", "x86"
}
如果不知道studio支持的ndk可編譯版本可以在gradle.properties添加
android.useDeprecatedNdk=true//可使用過時版本
(如果使用studio編譯以後運行出現dlopen failed: cannot locate symbol "__aeabi_memcpy" referenced by “/data/app/com.example.chenpengfei.uninstallreceiver-2/lib/arm/libjnilib.so”,那麼是因為sdk版本的問題,23及以下,或者更新studio到最新版本)
13、LinkedList源碼學習:
關於LinkedList 類的功能來說,也是一個集合容器類,實現了List接口,但是他內部的數據結構跟ArrayList不一樣,使用的是鏈表。而實現鏈表主要是通過Node這個類。
他繼承了AbstractSequentialList,而這個類其實是繼承了AbstractList。
他與ArrayList不一樣的就是他實現了Deque接口。
因為它是基於Node實現的列表,那麼需要關注的就是Node這個類
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
這個類會保存它上一個節點node,跟下一個節點node。
如果是有倆個節點那麼a 節點的next指向b節點 ,pre也指向b,b的pre指向a,next也指向a,這樣就形成了一個閉環。實現了雙向鏈表。
如果是三個的話 a 節點的next 指向b ,pre指向c,b 節點的next 指向c,pre指向a,而c節點的next指向a,pre指向b。這樣也是一個閉環。
所以LinkedList存儲數據的時候特別快。直接把節點的指向改變了就可以了。
但是存儲的時候也分幾種情況:
如果是直接添加的話不管是添加到首部還是尾部都直接修改節點指向,但是如果是指定下標來進行添加的話,會折半查找到下邊的node節點,然後在該節點處修改節點指向。速度會慢點。
但是查詢的時候就需要類似上邊的折半以後進行查找,然後返回該節點。速度就會比ArrayList慢很多。畢竟人家是實現了RendemAsscess接口,而且內部結構是array,查詢沒的比啊。
14、Vector源碼學習
通過學習ArrayList 與LinkedList以後在學習Vector發現沒有太多要注意的地方。
因為Vector是跟ArrayList基本上完全相同的,隻是在某些地方有點區別。
Vector是線程安全的,在很多方法上都加了線程鎖,synchronize,實現同步。內部結構完全就是ArrayList的結構,隻是在容量擴展的時候
這是Vecctor的容量擴展
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
這是ArrayList的容量擴展
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
可以發現在進行容量擴展的時候 newCapacity的值在取值的時候有點區別
ArrayList是直接擴展為原來容量的3/2,但是Vector因為有 一個容量擴充的參數capacityIncrement
,如果不為0,那麼每次擴展是在原來的基礎上增加capacityIncrement的長度,否則就是原來的2倍。
因為跟ArrayList很相似,學習的過程中發現比較一下這幾個實現了List接口的類更有用:
ArrayList、Vecotr、LinkedList的區別(源碼角度):
區別:
1、
ArrayList、Vector使用的是數組的數據結構來進行數據的存儲,實現了RandomAccess接口提供快速隨機訪問。 而LinkedList是通過鏈表這樣的數據接口來存儲數據,他實現了Deque接口,在隨機訪問方麵性能很差,因為需要遍曆這個鏈表中的節點(雖然是折半以後的遍曆,但是一樣很慢)
2、
容量擴展對於LinkedList來說是沒有任何關係的,因為它不需要。但是對於ArrayList於Vector來說這是很重要的點。因為他們的初始容量都是10,除非你指定了他的容量。但是在把數據進行添加的過程中,他們會檢查容量是否足夠存放數據,如果不夠就要進行容量的擴展。容量的擴展是通過創建一個新的容量的數組,然後指向上一個數組對象,創建的方式是調用一個係統的arrayCopy方法。
數據量很大的話對與性能有很大影響,使用的時候根據需要可以指定初始容量。
這就是他們之間的又一個不同點,插入效率,很明顯LinkedList很快,但是Vector與ArrayList很慢
Android 虛擬現實開發
開發者中心地址:https://vr.google.com/daydream/developers/
按需要下載自己需要的sdk已經開發工具
15、HashMap源碼學習
HashMap是一個存儲鍵值對的容器類,通過學習他的源碼來熟悉他的實現。
首先說下HashMap 1.8之前,最基本的實現是通過維護一個Entry[]的數組已經單鏈表來實現的數據存儲。
就是當我們在創建一個HashMap以後,通過調用put方法,會進行很多判斷,最後把我們要存放的數據添加到Entry[]數組中。
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
// 得到key的哈希碼
int hash = hash(key);
// 通過哈希碼計算出bucketIndex
int i = indexFor(hash, table.length);
// 取出bucketIndex位置上的元素,並循環單鏈表,判斷key是否已存在
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
// 哈希碼相同並且對象相同時
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
// 新值替換舊值,並返回舊值
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
// key不存在時,加入新元素
modCount++;
addEntry(hash, key, value, i);
return null;
}
但是在1.8以後,為了實現快速查找,添加了紅黑樹。裏邊維護的數組變為了Node[]數組,一個實現Map.Entry接口的類。這應該是1.8改動最大的地方。
1 public V put(K key, V value) {
2 // 對key的hashCode()做hash
3 return putVal(hash(key), key, value, false, true);
4 }
5
6 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
7 boolean evict) {
8 Node<K,V>[] tab; Node<K,V> p; int n, i;
9 // 步驟①:tab為空則創建
10 if ((tab = table) == null || (n = tab.length) == 0)
11 n = (tab = resize()).length;
12 // 步驟②:計算index,並對null做處理
13 if ((p = tab[i = (n - 1) & hash]) == null)
14 tab[i] = newNode(hash, key, value, null);
15 else {
16 Node<K,V> e; K k;
17 // 步驟③:節點key存在,直接覆蓋value
18 if (p.hash == hash &&
19 ((k = p.key) == key || (key != null && key.equals(k))))
20 e = p;
21 // 步驟④:判斷該鏈為紅黑樹
22 else if (p instanceof TreeNode)
23 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
24 // 步驟⑤:該鏈為鏈表
25 else {
26 for (int binCount = 0; ; ++binCount) {
27 if ((e = p.next) == null) {
28 p.next = newNode(hash, key,value,null);
//鏈表長度大於8轉換為紅黑樹進行處理
29 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
30 treeifyBin(tab, hash);
31 break;
32 }
// key已經存在直接覆蓋value
33 if (e.hash == hash &&
34 ((k = e.key) == key || (key != null && key.equals(k)))) break;
36 p = e;
37 }
38 }
39
40 if (e != null) { // existing mapping for key
41 V oldValue = e.value;
42 if (!onlyIfAbsent || oldValue == null)
43 e.value = value;
44 afterNodeAccess(e);
45 return oldValue;
46 }
47 }
48 ++modCount;
49 // 步驟⑥:超過最大容量 就擴容
50 if (++size > threshold)
51 resize();
52 afterNodeInsertion(evict);
53 return null;
54 }
跟所有的容器類一樣,這個類裏邊也定義size, modcout,loadFactor(負載因子),threshold(判斷是否需要擴展的界限 他的值等於初始容量乘以 負載因子),默認的初始容量是 1<< 4 ,
Hash主要是通過散列表來進行存儲,通過計算key的hash值,然後計算要存放的下標,然後放到數組中對應的位置。如果計算出來的hash相同,那麼比較key是否相等,如果相等,那麼久覆蓋舊值,返回舊值,如果不相同,那麼創建Node對象,然後與該位置中的Node鏈表進行連接,使Node鏈表的最新的節點是新創建的Node對象。
HashMap的擴容是擴容為 原來的2倍。
因為HashMap是通過散列表來進行存儲,所以會有空間的浪費,也就是用空間換取時間。
查找的時候通過計算key的hash值,然後計算在數組中的下標,可以快速的進行查找。
HashMap中使用鏈表主要是為了解決碰撞問題,也就是會有相同的Hash值的問題。
16、HashSet源碼:
HashSet用來存放單一數據,不在是鍵值對,而且存儲到其中的數據沒有順序,但是他可以保證數據不會重複(前提是你要重新hashCode 以及equals方法)。
因為HashSet的底層實現是通過HashMap來進行實現的。通過他的構造方法可以看到:
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<>(initialCapacity, loadFactor);
}
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
public HashSet() {
map = new HashMap<>();
}
如果對HashMap源碼了解的話很簡單就可以看懂。
HashSet提供了add方法進行數據的添加,也提供了容器類基本的功能。
比如刪除,是否包含,是否為空等等。
使用HashSet的時候遍曆數據是通過HashMap的KeySet這個類進行的。
final class KeySet extends AbstractSet<K> {
public final int size() { return size; }
public final void clear() { HashMap.this.clear(); }
public final Iterator<K> iterator() { return new KeyIterator(); }
public final boolean contains(Object o) { return containsKey(o); }
public final boolean remove(Object key) {
return removeNode(hash(key), key, null, false, true) != null;
}
public final Spliterator<K> spliterator() {
return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0);
}
public final void forEach(Consumer<? super K> action) {
Node<K,V>[] tab;
if (action == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
int mc = modCount;
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next)
action.accept(e.key);
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
}
從這個可以看出,我們可以是用ForEach,或者迭代器。
HashSet實現數據不重複就是重寫了HashCode以及Equals方法:
是在他所繼承的AbstractSet這個類中書寫的:
使用HashSet的時候要注意他不是線程安全的,需要重新要存放數據的HashCode 與Equals方法。
HashSet沒有辦法實現快速存取,如果需要如果需要獲取到某一個位置的數據可以先調用 toArray方法轉換為數組,然後進行查找.返回的數據順序與插入的順序相反。
HashMap的keySet的效率比entrySet效率低,因為keySet是通過迭代key來進行迭代,entrySet通過迭代Node對象來進行,不需要通過key進行value的查找,所以速度更快。
17、Popwindow源碼學習:
關於popWindow的源碼其中最主要的是show 以及dismiss的原理;
做過懸浮窗開發的人肯定知道,如果我們要創建一個懸浮在當前Activity或者這個android係統上邊的話,需要使用到的一個類肯定是WindowManager,通過WindowManager的addView的方法添加到界麵上去,如果隻是添加到當前Activity的話
18、Toast源碼學習
也是通過WindowManager來進行實現的。
toast主要是通過TN這個類來進行最終的顯示以及隱藏,如果要自己來控製顯示以及隱藏可以通過反射來獲取Toast中的mTN對象,然後通過這個對象用反射來調用show以及dismiss。
Toast中TN源碼:
private static class TN extends ITransientNotification.Stub {
final Runnable mShow = new Runnable() {
@Override
public void run() {
handleShow();
}
};
final Runnable mHide = new Runnable() {
@Override
public void run() {
handleHide();
// Don't do this in handleHide() because it is also invoked by handleShow()
mNextView = null;
}
};
private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();
final Handler mHandler = new Handler();
int mGravity;
int mX, mY;
float mHorizontalMargin;
float mVerticalMargin;
View mView;
View mNextView;
WindowManager mWM;
TN() {
// XXX This should be changed to use a Dialog, with a Theme.Toast
// defined that sets up the layout params appropriately.
final WindowManager.LayoutParams params = mParams;
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
params.width = WindowManager.LayoutParams.WRAP_CONTENT;
params.format = PixelFormat.TRANSLUCENT;
params.windowAnimations = com.android.internal.R.style.Animation_Toast;
params.type = WindowManager.LayoutParams.TYPE_TOAST;
params.setTitle("Toast");
params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
}
/**
* schedule handleShow into the right thread
*/
@Override
public void show() {
if (localLOGV) Log.v(TAG, "SHOW: " + this);
mHandler.post(mShow);
}
/**
* schedule handleHide into the right thread
*/
@Override
public void hide() {
if (localLOGV) Log.v(TAG, "HIDE: " + this);
mHandler.post(mHide);
}
public void handleShow() {
if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
+ " mNextView=" + mNextView);
if (mView != mNextView) {
// remove the old view if necessary
handleHide();
mView = mNextView;
Context context = mView.getContext().getApplicationContext();
String packageName = mView.getContext().getOpPackageName();
if (context == null) {
context = mView.getContext();
}
mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
// We can resolve the Gravity here by using the Locale for getting
// the layout direction
final Configuration config = mView.getContext().getResources().getConfiguration();
final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection());
mParams.gravity = gravity;
if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {
mParams.horizontalWeight = 1.0f;
}
if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {
mParams.verticalWeight = 1.0f;
}
mParams.x = mX;
mParams.y = mY;
mParams.verticalMargin = mVerticalMargin;
mParams.horizontalMargin = mHorizontalMargin;
mParams.packageName = packageName;
if (mView.getParent() != null) {
if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
mWM.removeView(mView);
}
if (localLOGV) Log.v(TAG, "ADD! " + mView + " in " + this);
mWM.addView(mView, mParams);
trySendAccessibilityEvent();
}
}
private void trySendAccessibilityEvent() {
AccessibilityManager accessibilityManager =
AccessibilityManager.getInstance(mView.getContext());
if (!accessibilityManager.isEnabled()) {
return;
}
// treat toasts as notifications since they are used to
// announce a transient piece of information to the user
AccessibilityEvent event = AccessibilityEvent.obtain(
AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
event.setClassName(getClass().getName());
event.setPackageName(mView.getContext().getPackageName());
mView.dispatchPopulateAccessibilityEvent(event);
accessibilityManager.sendAccessibilityEvent(event);
}
public void handleHide() {
if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
if (mView != null) {
// note: checking parent() just to make sure the view has
// been added... i have seen cases where we get here when
// the view isn't yet added, so let's try not to crash.
if (mView.getParent() != null) {
if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
mWM.removeView(mView);
}
mView = null;
}
}
}
19、反射學習
關於java的反射:首先反射就是在程序運行的過程中可以動態的修改其狀態以及行為的一種能力。
現在我們使用反射大部分是通過修改某一個類的其中一個對象的引用或者對其值的修改,達到修改對象的行為。
如果要對類中的方法或者實現進行修改就需要進行字節碼注入操作,不在反射的學習中。
關於反射主要是通過Class這個類以及
java.lang.reflect
這個包下邊Method,Field等等的來實現的。
20、android切換到桌麵
Intent mHomeIntent;
mHomeIntent = new Intent(Intent.ACTION_MAIN, null);
mHomeIntent.addCategory(Intent.CATEGORY_HOME);
mHomeIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) startActivity(mHomeIntent);
21、關於String ,StringBuffer,StringBuilder的比較
String 類是一個final類型的類,它是不能被修改的,也就是說
String s = “aaaaaa”
s = s + “bbbbb”
因為String 是不能被修改的,所以上邊的在執行的時候是重新創建一個String 對象,然後 賦值給s 。
所以如果在使用的時候是對String 對象來進行操作,是十分的耗時的,但是如果是直接對字符串進行操作,那麼速度很快,都是jvm來進行操作的。
StringBuilder、StringBuffer是可變字符串對象,我們所有對字符串的操作都在這個對象上邊,如果是對字符串對象進行操作的話,速度上要比String快上很多很多。
他們倆個都是繼承了AbstractStringBuilder這個抽象類,在這個類中實現了大部分的功能,比如getValue,setLength,append ,reverse,等等,這個類還實現了CharSequence接口, 所以StringBuffer與StringBuilder都是在AbstractStringBuilder的基礎上又進行了擴展,他們都實現了相同的三個接口java.io.Serializable, Appendable, CharSequence。
而他們倆個的區別就在於線程安全:
StringBuffer:線程安全 ,是因為它裏邊很多方法都是上鎖的,通過synchronized來進行修飾的,所以在多線程中操作時是不會引起對象的因多線程操作而發生的數據不一致問題。
StringBuilder:非線程安全,與StringBuffer正好相反,所有的方法都麼有加鎖。但是如果在非多線程操作中,速度比StringBuffer要快。
22、6.0多進程無法安裝總是提示安裝失敗。應用之間交互也需要設置關聯啟動
23、
5.0以後獲取正在運行的程序隻能獲取到當前應用。而後台正在運行的服務以及應用都隻能通過獲取服務然後通過service或者process來進行判斷。
因為6.0以後應用如果設置多進程是無法安裝的。
獲取方式:
/**
* 方法描述:判斷某一Service或者應用是否正在運行
*
* @param context 上下文
* @param serviceName Service的全路徑: 包名 + service的類名 (如果是判斷應用是否安裝直接傳遞包名)
* @return true 表示正在運行,false 表示沒有運行
*/
public static boolean isServiceRunning(Context context, String serviceName) {
ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
List<ActivityManager.RunningServiceInfo> runningServiceInfos = am.getRunningServices(200);
if (runningServiceInfos.size() <= 0) {
return false;
}
for (ActivityManager.RunningServiceInfo serviceInfo : runningServiceInfos) {
Log.e("TAG", serviceInfo.service.getClassName());
if (serviceInfo.process.equals(serviceName)) {
return true;
}
}
return false;
}
24、android aidl進程間通信要注意的點;
現在在做一個sdk用來讓訊飛可以與我們公司的應用進行開發。
因為是要在進程間通信,所以肯定會使用Aidl來進行。
但是要注意的幾點就是:
(1)進程間通信的話最好是不要把提供服務的service設置為獨立的進程,要保證跟應用在同一個進程(2)還有就是進程間通信必
25、查看當前應用cpu內、存使用情況
adb shell
top -m 15 -s cpu //查詢當前cpu占用前十的應用
dumpsys meminfo //查看手機上所有的內存信息
dumpsys battery//查看電量信息
dumpsys meminfo “要查看的應用的包名” //查詢某一個應用的包名
top -d 1 | grep com.guoyi.qinghua //查詢某一個應用的cpu使用情況
26、關於android 的應用程序cpu占用率問題
1、圖片加載(圖片加載使用好第三方框架,盡量不要使用Bitmap.create的方法)
android動畫對於cpu的消耗還是很大的,如果是可以通過自定實現的動畫使用自定義動畫,如過是複雜動畫,能用ObjectAnimator實現就不要用ValueAnimator來進行實。
不要使用動畫來更新自定義的進度條或者是會根據時間間隔變化的View。可以用Timer或者Handler來替換。
可以減小圖片的大小。
2、線程 (線程確保執行完成,或者可以手動控製)
3、布局加載(布局盡量減少布局層數)
4、網絡(在進程網絡訪問的時候要先判斷網狀態)
5、定位(降低定位的時間間隔)
6、直播軟件的編解碼(在使用七牛播放器的時候,設置硬解碼cpu的占有率比軟解碼低一半,如果cpu占用太高切換硬解碼是一個好的選擇)
27、android logo 尺寸
DENSITY SIZE LOCATION RATIO SCREEN MARGIN
XXXHDPI 192×192 drawable-xxxhdpi 4 640 DPI 12 to 16 pixels
XXHDPI 144×144 drawable-xxhdpi 3 480 DPI 8 to 12 pixels
XHDPI 96×96 drawable-xhdpi 2 320 DPI 6 to 8 pixels
HDPI 72×72 drawable-hdpi 1.5 240 DPI 4 to 6 pixels
MDPI 48×48 drawable-mdpi 1 160 DPI 3 to 4 pixels
MDPI 48×48 drawable (Cupcake) 1 160 DPI 3 to 4 pixels
LDPI 36×36 drawable-ldpi 0.75 120 DPI 2 to 3 pixels
NA 512×512 Google Play NA NA As required
28、ython環境配置以後在使用中出現的問題
python代碼需要在開頭插入
#!/usr/bin/python
用來生名python支持,然後通過import可以導入需要的類。
如果出現 -bash: ./****.py: /usr/bin/python^M: bad interpreter: No such file or directory
那麼應該是不同係統編碼格式引起的:在windows係統中編輯的.sh .py文件可能有不可見字符,所以在linux係統下執行會報以上異常信息。一般是因為windows行結尾和linux行結尾標識不同造成的。
解決:
1)在windows下轉換:
利用一些編輯器如UltraEdit或EditPlus等工具先將腳本編碼轉換,再放到Linux中執行。轉換方式如下(UltraEdit):File-->Conversions-->DOS->UNIX即可。
2)linux下直接替換:
sed -i 's/^M//g' filename (注意^M 在linux 下寫法 按^M 是回車換行符,輸入方法是按住CTRL+v,鬆開v,按m)
3)也可在Linux中轉換:
首先要確保文件有可執行權限
#sh>chmod a+x filename
然後修改文件格式
#sh>vi filename
利用如下命令查看文件格式
:set ff 或 :set fileformat
可以看到如下信息
fileformat=dos 或 fileformat=unix
利用如下命令修改文件格式
:set ff=unix 或 :set fileformat=unix
:wq (存盤退出) 或者使用:wq! 進行保存
最後再執行文件
#sh>./filename
如果配置了全局的python環境變量的話可以直接使用xxxx.py來執行py文件,或者要通過./xxxx.py
29、Bitmap.createBitmap中參數的理解
x的值不能小於0,width 不能小於0;
x+width 的值不能小於0,也不能大於原bitmap的寬度,
y的值與height與上邊規則相同,
而通過這個方法創建的btimap是通過x與y來控製要截取的bitmap的起始位置,最後的width與height用來確定從x與y的位置截取開始到什麼時候結束。
30、如果.9圖片不正規可能導致android studio編譯出現多線程問題
31、studio中svn的使用
studio中使用svn可以更新指定文件,也可以提交指定文件。
svn指定版本更新會把指定版本以下的代碼都更新下來,不會更新指定版本以上的代碼
svn 中revert 複原代碼,隻能複原沒有提交的文件,如果提交時沒有辦法複原
32、RecycleView 的scrollToPosition方法,如果剩下的item個數不夠一屏幕的話無法滾動。
但是可以使用 scrollBy(int x, int y) 滾動屏幕的寬度的距離就可以。
33、內存泄漏的理解,如何防止內存泄漏(以及內存泄漏會引起的問題)
關於內存泄漏其實最難的是定位問題出現。一個良好的代碼習慣,能避免掉很多內存泄漏,但是也不能規避過所有的。以前都是在內存泄漏以後使用工具進行內存分析,找到內存泄漏的地方,然後進行修改,現在發現一個內存泄漏檢測比較簡單的一個工具leakcanary,可以去gitHub上直接查看源碼,然後簡單集成下就可以了。
內存泄漏:內存泄漏就是一個對象被別的對象引用,導致他應該被釋放回收的時候,沒有辦法釋放,對內存的持續占用,會導致android應用在給別的功能分配內存的時候,可能沒有辦法分配給這個功能那麼多的內存,然後gc開始查找可以釋放的內存,但是因為內存泄漏了(也就是那麼沒有辦法釋放的對象都是強引用),gc查找以後也不能釋放出足夠這個功能所需要占用的內存的時候,那麼就會出現內存泄漏。
內存泄漏的解決辦法:可以加大android係統給應用分配的內存上限,但是這個是治標不治本,最主要還是要修改內存泄漏的根本原因。一般發生內存泄漏的地方有(1)匿名內部類的使用(因為匿名內部類一般都會持有當前對象的引用,那麼在內部類對象沒有釋放的時候,那麼他所持有的引用也不會消失,就會導致內存泄漏)如果可以在Application中添加的回調監聽,一定不要放在activity中,因為那樣的話,在回調沒有回調的時候,引用也會一直持有;(2)在開發中發現 集合類很容易引起內存泄漏,一般盡量不要把集合類創建為static類型,否則的話他裏邊的對象都不會得到釋放,除非是從集合中移除。可以參考的文章:https://blog.csdn.net/u012808234/article/details/74942491
34、屏幕適配是android一直以來都很堅挺的問題呢,
因為android的開源性,各個廠商的定製,等等,導致android手機的分辨率是千奇百怪,但是我們適配一般也隻適配主流的分辨率,不是所有。
那麼在適配的過程中如何用最小的成本來適配最多屏幕,就是我們開發需要考慮的問題。
android 中推薦使用match_parent,warp_parent, xxxdl ,xxxsp,主要就是為了適配。
dp(dip或者叫dpi) 代表的是單位英寸內的像素點個數,他是跟像素無關,跟屏幕密度有關的。
android中屏幕密度越大也就是單位英寸內的像素點越多。屏幕顯示的更清晰。
密度的計算方式:1920x1080 5英寸手機(對角線的長度,也就是手機左上角跟右下角的長度)
√(1920^2+1080^2)=2202.9071
2202.9/5=468.7021(ppi)≈469ppi
如果在480px 跟240px上 如果密度相同,那麼1dp所占的大小也是相等的。這就是跟像素無關
160ppi 1dp 1px
320ppi 1dp 2px
480ppi 1dp 3px
密度因子以160為基礎,也就是160 密度因子是1,密度因子= 密度/160
dp轉px px = dp ️密度因子
知道了這個,但是如果隻是簡單的設置dp還是不能夠適配大多數屏幕:
適配方式:
1、布局適配,根據不同分辨率創建不同的布局
優點是簡單明了,隻要根據不同的屏幕創建不同的布局即可,但是會導致的問題就是,布局文件的增多,如果裏邊對於圖片的使用很多的話,那麼會導致app打包以後的大小變的很大,要適配大部分屏幕,工作量比較大
2、布局的時候通過比例來進行適配,比如一個按鈕占屏幕的幾分之幾
優點就是一套布局可以適配所有的屏幕,但是不是所有的布局都能用比例來適配
3、使用一套布局,然後創建不同分辨率的dimens.xml (創建不同分辨率的values比如values_480x1280)
4、一些動畫或者效果,客戶端如果可以實現的話,而且不是特別難的話,最好是自定義,動態的適配分辨率
5、在代碼中動態修改控件的寬度等等參數
35、開發工程中編程語言的學習以及開發工具:
36 、多項目依賴同一個module,實現module的統一管理
現在做android的基本上都在用studio進行開發,如果開發一個項目還好,但是如果是多個項目,而且他們還有同樣在使用的Module,那麼就要維護很多套不同項目的,但是缺相同的Moudle,所以使用多項目依賴同樣的Moudule可以很有效的減小代碼的維護成本,減小開發時間。
方式:
- 首先創建一個項目,該項目包含所有公有的Module
-
打開要添加公有Module的項目,通過強製引用添加所需要的Module
打開項目的setting.gradle
如下配置:
include ':app', ':framework'
include ':cuslibrary' ,’:qhlibrary' //添加項目所要包含的Module
project(':cuslibrary').projectDir = new File(‘../AllModule/cuslibrary') //導入該路徑下的Moudle
project(':qhlibrary').projectDir = new File(‘../AllModule/qhlibrary’) //導入該路徑下的Moudle -
然後在該項目的app文件夾的根目錄下邊的build.gradle中添加Module的依賴
compile project(':cuslibrary')
compile project(‘:qhlibrary') 保證不會有相同的文件或者類庫的引用,然後運行ok
37、在使用svn的時候如果add 了一個不進行版本管理的文件會出現 E200009的錯誤
解決方式:svn st | grep ^? | awk '{print $2}' | xargs svn add
最後更新:2017-08-13 22:24:29