
簡介一份面向Android開發者的手機定位功能實現源碼適合需要集成定位服務或學習定位原理的中高級開發者參考。壓縮包大小約2.72MB核心代碼圍繞LocationManager、位置提供者、LocationListener等接口展開并包含運行時權限請求、單次與持續定位、最小更新距離與時間間隔設置等典型處理邏輯。目前已有203人學習下載可用于快速理解Android定位服務的調用流程與參數配置。源碼覆蓋GPS定位、網絡定位、被動定位與地理圍欄等模塊同時展示了定位回調處理、異常場景應對以及與地圖API結合的思路。通過分析源碼的Activity、Service、BroadcastReceiver等組件結構開發者可以掌握事件監聽、狀態切換與后臺定位服務的設計方法直接借鑒可運行的定位方案減少從零調試的時間成本。1. Android 手機定位源碼從 LocationManager 到地理圍欄的一次完整拆解如果你手頭也有一份Android手機定位源碼.zip打開后最該關心的不是那個 MainActivity 有多長而是它到底走的是系統原生LocationManager路線還是已經遷移到 Google Play 服務的FusedLocationProvider。這兩條路決定了權限模型、省電表現、真機調試方式甚至 zip 里那幾份 gradle 文件的復雜度。很多人在第一步就選錯把網絡定位和 GPS 定位寫成互斥分支結果室內永遠拿不到位置室外又慢得讓人想摔機。這篇文章會以這套源碼為線索把 Provider 選型、權限、單次與連續定位、地理圍欄以及 adb 模擬位置驗證講透。適合正要把定位能力接進項目的 Android 開發也適合想讀懂別人定位代碼的維護者。2. LocationManager 的骨架Provider、Listener 與權限生命周期2.1 四種 Provider 的選用邏輯LocationManager是 Android 系統提供的位置服務入口源碼里最常見的初始化寫法是LocationManager locationManager (LocationManager) getSystemService(Context.LOCATION_SERVICE);拿到實例后系統會暴露四種 Provider它們的定位來源、精度和功耗差異很大Provider定位來源典型精度功耗適用場景GPS_PROVIDER衛星5-50 米高室外、車載、軌跡記錄NETWORK_PROVIDERWi-Fi、基站20-500 米低室內、城市街道、快速定位PASSIVE_PROVIDER其他應用請求的定位結果不定極低被動接收不主動發起FUSED_PROVIDERGoogle Play 服務多源融合10-50 米中需要兼顧精度與功耗時在源碼工程里你通常能看到一個getBestProvider()的調用它接收一個Criteria對象返回系統認為最合適的 Provider。常見的錯誤是拿到 GPS 就死等衛星完全不考慮用戶此刻在室內。我的建議是把 GPS 和網絡定位當作互補通道優先用網絡結果墊底再用 GPS 修正。2.2 運行時權限FINE 與 COARSE 的邊界Android 6.0 之后定位權限從安裝時授權變成了運行時授權。ACCESS_FINE_LOCATION授權后可以拿到 GPS 和網絡定位只授ACCESS_COARSE_LOCATION時系統會屏蔽 GPS 提供者只給網絡粗定位結果。很多源碼里為了省事只申請 FINE其實并不合規。2.2.1 權限請求代碼if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) ! PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{ Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION }, 1001); } else { startLocation(); }這段代碼把 FINE 和 COARSE 一起申請避免用戶拒絕 FINE 后連粗定位都用不了。1001是請求碼回調里要判斷grantResults是否全部大于等于 0。注意 Android 12 之后模糊定位會被系統單獨列出來如果用戶只給ACCESS_COARSE_LOCATION代碼里再去請求 GPS 更新會直接收到SecurityException所以回調里必須再次檢查locationManager.getProviders(true)中 GPS 是否可用。2.3 監聽器接線回調線程與 removeUpdates 時機LocationListener的onLocationChanged()默認運行在調用requestLocationUpdates()的線程。如果是在 Activity 里調用回調在主線程執行意味著你不能在里面做耗時操作。源碼里常見的寫法是把定位邏輯放進一個Service然后通過HandlerThread接收位置更新這樣能避免主線程卡頓。LocationListener listener new LocationListener() { Override public void onLocationChanged(Location location) { if (location ! null) { Log.d(LocSdk, lat location.getLatitude() , lng location.getLongitude()); } } Override public void onStatusChanged(String provider, int status, Bundle extras) { // 狀態切換OUT_OF_SERVICE / TEMPORARILY_UNAVAILABLE / AVAILABLE } Override public void onProviderEnabled(String provider) { } Override public void onProviderDisabled(String provider) { } };這里的onProviderDisabled是排錯關鍵。GPS 被用戶從設置里關掉后代碼不會收到任何異常只有這個回調會把狀態推過來。我一般會在回調里彈出指引對話框而不是等到定位超時再提示。removeUpdates(listener)必須在onDestroy()或任務結束時調用否則 Service 里會累積多個監聽器導致 CPU 喚醒和電量消耗都異常。3. 寫一個能跑的定位模塊GPS 與網絡定位的雙通道實現3.1 單次定位與連續定位的取舍源碼里通常會同時出現兩種方法requestSingleUpdate()適合「打開 App 只想知道自己在哪」的場景拿到一個 Location 就立刻釋放。requestLocationUpdates()適合導航、運動軌跡、騎行記錄這類需要持續跟蹤的場景。單次定位的問題在于如果第一次請求恰好發生在 GPS 剛啟動時返回的可能是一個精度很差的緩存位置。較完整的源碼會先判斷location.getAccuracy()如果精度大于 100 米就繼續等待下一次更新直到滿足閾值或者超時。3.2 參數設定minTime、minDistance 與省電策略requestLocationUpdates()有兩個關鍵參數很多新手把它們當成固定值其實它們直接影響定位效果和功耗參數作用常見取值說明minTime最小更新間隔毫秒1000-60000不是嚴格定時系統可能提前或延后觸發minDistance最小位移米0-1000 表示只要時間到了就更新適合移動速度快的場景我在做室外軌跡記錄時會把 GPS 的minTime設為 3000、minDistance設為 5但如果是室內展示型應用直接設 10000 和 10 就夠避免過于頻繁的喚醒。你還要使用LocationRequest配合requestLocationUpdates(provider, minTime, minDistance, listener)的老接口Android 12 之后系統對后臺位置更新做了更嚴格限制連續定位必須在前臺服務里聲明foregroundServiceTypelocation。3.3 一份可直接抄的定位服務代碼下面是一份精簡但完整的定位 Service放在Android手機定位源碼里作為核心模塊很合適public class LocationService extends Service { private LocationManager lm; private LocationListener listener; private void startLocation() { lm (LocationManager) getSystemService(LOCATION_SERVICE); Criteria criteria new Criteria(); criteria.setAccuracy(Criteria.ACCURACY_FINE); criteria.setPowerRequirement(Criteria.POWER_LOW); if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) ! PackageManager.PERMISSION_GRANTED) { return; } ListString providers lm.getProviders(true); for (String provider : providers) { if (LocationManager.GPS_PROVIDER.equals(provider) || LocationManager.NETWORK_PROVIDER.equals(provider)) { lm.requestLocationUpdates(provider, 5000, 5, listener); } } } Override public void onDestroy() { if (lm ! null listener ! null) { lm.removeUpdates(listener); } super.onDestroy(); } }這段代碼的邏輯是先用Criteria聲明需求再遍歷getProviders(true)找到當前可用的 Provider只要 GPS 或網絡其中一種可用就都掛上監聽。這樣做的好處是 GPS 冷啟動過程中能先用網絡定位把大致位置填上等衛星鎖定后再用 GPS 結果覆蓋。requestLocationUpdates的第二個參數是 minTime這里給的 5000 表示 5 秒一次第三個參數 5 表示位移超過 5 米才更新如果兩個條件其中一個滿足系統都會回調。3.4 定位失敗的常見原因拿到源碼后如果發現一直走onLocationChanged卻拿不到位置先按這個順序排查權限對話框是否真的點了允許Android 11 以上還要檢查「僅限這一次」授權是否會隨 App 退出而失效。當前 Provider 是否可用locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)不可用時要跳到系統設置頁。是否在室內且網絡定位也關了這種情況下沒有任何 Provider 能返回結果。是否把onLocationChanged里的location判空后直接 return 了某些源碼在模擬器上會收到location null但真機上不一定。我曾經調試過一份定位源碼問題出在getProviders(true)的布爾參數上。這個enabledOnly參數為 true 時只返回已經開啟的 Provider如果用戶在設置里關掉 GPS網絡 Provider 又不可用providers就是空列表代碼里沒有兜底邏輯整個 Service 就靜默失效了。修正方法是先判斷 providers 是否為空為空就去請求ACCESS_COARSE_LOCATION或直接提示用戶打開位置開關。4. 從源碼到實戰FusedLocationProvider 與地理圍欄的接入4.1 為什么源碼里會出現融合定位Android手機定位源碼.zip里的 build.gradle 如果包含com.google.android.gms:play-services-location說明作者用的是FusedLocationProviderClient。它把 GPS、Wi-Fi、基站、傳感器數據合并處理由系統決定哪條鏈路最省電、最準確。這套 API 不在 AOSP 里而是屬于 Google Play 服務所以國產 ROM 上需要確認設備是否包含對應框架。FusedLocationProviderClient client LocationServices.getFusedLocationProviderClient(this); if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) PackageManager.PERMISSION_GRANTED) { client.getLastLocation() .addOnSuccessListener(location - { if (location ! null) { Log.d(Fused, String.valueOf(location.getLatitude())); } }); }getLastLocation()返回的是緩存位置來源可能是一小時前的定位結果但它的優點在于不消耗任何電量適合 App 啟動后立刻展示地圖。要拿實時位置需要用LocationRequest構建請求LocationRequest request LocationRequest.create(); request.setInterval(10000); request.setFastestInterval(5000); request.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY); client.requestLocationUpdates(request, locationCallback, Looper.getMainLooper());這里setInterval(10000)是預期更新間隔setFastestInterval(5000)是上限PRIORITY_HIGH_ACCURACY會優先使用 GPS。如果室內不需要太高精度改成PRIORITY_BALANCED_POWER_ACCURACY可以顯著降低功耗這也解釋了為什么同一份源碼在室外和室內表現差異巨大。4.2 Geofence 的定義與觸發條件地理圍欄是這套源碼里最容易讓人迷惑的部分。Geofence對象不是畫一個圓形區域而是把「區域 觸發事件 過期時間」打包成一個請求對象Geofence fence new Geofence.Builder() .setRequestId(office) .setCircularRegion(31.2304, 121.4737, 200) .setExpirationDuration(12 * 60 * 60 * 1000) .setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER | Geofence.GEOFENCE_TRANSITION_EXIT) .build();setCircularRegion的第三個參數是半徑 200 米setExpirationDuration表示 12 小時后自動失效setTransitionTypes決定進入和離開都觸發。官方文檔沒有特別強調的一點是如果設備沒有 GPS 或者其他定位信號圍欄不會觸發這點和requestLocationUpdates依賴 Provider 的邏輯完全一致。4.3 地理圍欄的注冊與回調代碼注冊圍欄需要依賴GeofencingClient并且要先確認設備支持地理圍欄GeofencingClient geofencingClient LocationServices.getGeofencingClient(this); if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) PackageManager.PERMISSION_GRANTED) { geofencingClient.addGeofences( new GeofencingRequest.Builder() .addGeofence(fence) .setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER) .build(), geofencePendingIntent ); }這里的geofencePendingIntent是關鍵節點。它通常是PendingIntent.getService()指向一個處理圍欄事件的IntentService而不是 Activity。因為圍欄可能在 App 退到后臺時觸發Activity 已經被回收Service 才能保證回調不丟。在回調的onHandleIntent里你才能從GeofencingEvent中取出真實觸發的圍欄int transition GeofencingEvent.fromIntent(intent).getGeofenceTransition(); if (transition Geofence.GEOFENCE_TRANSITION_ENTER) { Log.d(Geofence, enter office area); } else if (transition Geofence.GEOFENCE_TRANSITION_EXIT) { Log.d(Geofence, exit office area); }4.4 并發位置更新與場景化處理復雜 App 里經常同時存在普通定位和地理圍欄這時容易踩兩個坑多個模塊各自調用requestLocationUpdates底層會創建多個監聽器系統會重復計算位置源。圍欄回調里再啟動一次全量定位導致位置更新風暴。處理方式是在源碼里加一個全局單例的定位管理器所有模塊都通過它注冊回調LocationListener內部用CopyOnWriteArrayList保存外部回調。這樣 GPS 每 5 秒產生一個位置就能同時派發給地圖模塊、圍欄模塊和記錄模塊功耗只漲一次。另一個常見坑是Geofence的setExpirationDuration傳了Geofence.NEVER_EXPIRE長期不失效的圍欄會持續占用地理位置服務。如果業務只要求當天有效我建議顯式設置過期時間并在onDestroy里調用removeGeofences(pendingIntent)清理。5. 驗證與排錯用 adb 模擬位置測通整套源碼拿到Android手機定位源碼.zip后第一件事不是編譯而是先用模擬器或者真機把定位鏈路打通。在 Android Studio 里打開項目后點開 Device Explorer 找到終端執行下面三條命令就能偽造當前設備位置adb shell appops set com.example.locapp android:mock_location allow adb emu geo fix 121.4737 31.2304第一條命令給目標 App 開啟模擬定位權限第二條把 GPS 坐標固定到上海人民廣場。注意adb emu geo fix只對模擬器有效真機需要先在開發者選項里開啟「允許模擬位置」。順帶一句市面上那些手機模擬定位 App 的原理大多也是這樣通過后臺服務調用LocationManager.addTestProvider()注入坐標但普通 App 沒有android.permission.ACCESS_MOCK_LOCATION權限是跑不通的。驗證時打開 logcat過濾關鍵字LocSdk或者Fusedadb logcat -s LocSdk:I如果能看到lat31.2304, lng121.4737這樣的日志說明定位鏈路已經通了。接下來測精度連續執行三次geo fix每次間隔 5 秒觀察日志里location.getAccuracy()的變化。原廠源碼里精度字段是 0 是很正常的說明模擬器沒有提供真實的誤差模型不代表真機上也是這樣。最后一招是驗證后臺定位。把 App 按 Home 鍵退到后臺再執行adb shell dumpsys location | grep -A 10 last location看看目標包名是否還在位置監聽列表里。如果這份源碼用了前臺 Service這里會出現foregroundServiceTypelocation如果只是普通 ServiceAndroid 8 以上大概率會被系統殺掉這也幫你確定了后續改造方向。本文還有配套的精品資源點擊獲取