Android源码笔记--SystemUI
SystemUI
SystemUI是Android为用户提供系统级别的信息显示与交互的一套UI组件,它包含屏幕顶端的状态栏,底部的导航栏,图片壁纸及RecentPanel(近期使用的APP).SystemUI的表现形式与普通应用类似,也是以一个APK的形式存在于系统之中。其中比较特殊的是状态栏和导航栏的启动方式,它们运行在一个SystemUIService中。现以导航栏的启动方式为例。 如下:
SystemServer.javastatic final void startSystemUi(Context context) { Intent intent = new Intent(); intent.setComponent(new ComponentName("com.android.systemui", "com.android.systemui.SystemUIService")); //Slog.d(TAG, "Starting service: " + intent); context.startServiceAsUser(intent, UserHandle.OWNER);}
public class SystemUIService extends Service { @Override public void onCreate() { super.onCreate(); ((SystemUIApplication) getApplication()).startServicesIfNeeded(); //获取Application调用startServicesIfNeeded }/*打印堆栈信息*/ @Override protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { SystemUI[] services = ((SystemUIApplication) getApplication()).getServices(); if (args == null || args.length == 0) { for (SystemUI ui: services) { pw.println("dumping service: " + ui.getClass().getName()); ui.dump(fd, pw, args); } } else { String svc = args[0]; for (SystemUI ui: services) { String name = ui.getClass().getName(); if (name.endsWith(svc)) { ui.dump(fd, pw, args); } } } }}
分析:1 是启动SystemUIApplication 2 是打印堆栈信息
SystemUIApplication.java@Overridepublic void onCreate() { super.onCreate(); // Set the application theme that is inherited by all services. Note that setting the // application theme in the manifest does only work for activities. Keep this in sync with // the theme set there. setTheme(R.style.systemui_theme); //注释广播 IntentFilter filter = new IntentFilter(Intent.ACTION_BOOT_COMPLETED); filter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY); registerReceiver(new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { //开启直接返回 if (mBootCompleted) return; if (DEBUG) Log.v(TAG, "BOOT_COMPLETED received"); unregisterReceiver(this); //标记启动 mBootCompleted = true; //服务是否启动 if (mServicesStarted) { final int N = mServices.length; for (int i = 0; i < N; i++) { //回调各服务的onBootCompleted函数 mServices[i].onBootCompleted(); } } } }, filter);}
分析: 1设置主题 2 注册开机广播,及设置标志位
SystemUIApplication.javapublic void startServicesIfNeeded() { if (mServicesStarted) { return; } if (!mBootCompleted) { // check to see if maybe it was already completed long before we began // see ActivityManagerService.finishBooting() //获取系统文件中的sys.boot_completed的值 if ("1".equals(SystemProperties.get("sys.boot_completed"))) { mBootCompleted = true; if (DEBUG) Log.v(TAG, "BOOT_COMPLETED was already sent"); } } Log.v(TAG, "Starting SystemUI services."); final int N = SERVICES.length; for (int i=0; i cl = SERVICES[i]; if (DEBUG) Log.d(TAG, "loading: " + cl); //实例化各个类实例,放入mServices数组中 try { mServices[i] = (SystemUI)cl.newInstance(); } catch (IllegalAccessException ex) { throw new RuntimeException(ex); } catch (InstantiationException ex) { throw new RuntimeException(ex); } mServices[i].mContext = this; mServices[i].mComponents = mComponents; if (DEBUG) Log.d(TAG, "running: " + mServices[i]); mServices[i].start(); if (mBootCompleted) { mServices[i].onBootCompleted(); } } //服务启动标志 mServicesStarted = true;}
分析:这个方法中,首先判断mServicesStarted标志为来判断SystemUI相关的服务是否启动,同时根据系统配置文件来检查ActivityManagerService是否finishBooting,然后通过类加载机制来初始化SERVICES数组里面相关的类加入mServices中,然后start。
SystemUIApplication.java /** * The classes of the stuff to start. */private final Class<?>[] SERVICES = new Class[] { com.android.systemui.tuner.TunerService.class,//定制状态栏服务 com.android.systemui.keyguard.KeyguardViewMediator.class,//锁屏相关 com.android.systemui.recents.Recents.class,//近期任务 com.android.systemui.volume.VolumeUI.class,//音量条 com.android.systemui.statusbar.SystemBars.class,//状态栏 com.android.systemui.usb.StorageNotification.class,//通知栏 com.android.systemui.power.PowerUI.class,//电源相关 com.android.systemui.media.RingtonePlayer.class,//铃声播放相关};/** * Hold a reference on the stuff we start. */private final SystemUI[] mServices = new SystemUI[SERVICES.length];
分析:从mServices和SERVICES的定义可以发现SERVICES是一组包含全路径的相关的类,这些类包含一些我们常见的TunerService(定制状态栏服务)、 KeyguardViewMediator(锁屏相关)、Recents(近期任务)、VolumeUI(音量条)、SystemBars(状态栏)、StorageNotification(通知栏)、PowerUI(电源相关)、RingtonePlayer(铃声播放相关)类,它们都是继承与SystemUI抽象类,现在只分析StatusBar相关的SystemBars类。
com.android.systemui.statusbarSystemBars.javapublic class SystemBars extends SystemUI implements ServiceMonitor.Callbacks { private static final String TAG = "SystemBars"; private static final boolean DEBUG = false; private static final int WAIT_FOR_BARS_TO_DIE = 500; // manages the implementation coming from the remote process private ServiceMonitor mServiceMonitor; // in-process fallback implementation, per the product config private BaseStatusBar mStatusBar; @Override public void start() { if (DEBUG) Log.d(TAG, "start"); //实例化ServiceMonitor mServiceMonitor = new ServiceMonitor(TAG, DEBUG, mContext, Settings.Secure.BAR_SERVICE_COMPONENT, this); //start mServiceMonitor.start(); // will call onNoService if no remote service is found } /*服务没启动时,ServiceMonitor会回调onNoService*/ @Override public void onNoService() { if (DEBUG) Log.d(TAG, "onNoService"); createStatusBarFromConfig(); // fallback to using an in-process implementation } /*服务已经启动的回调*/ @Override public long onServiceStartAttempt() { if (DEBUG) Log.d(TAG, "onServiceStartAttempt mStatusBar="+mStatusBar); if (mStatusBar != null) { // tear down the in-process version, we'll recreate it again if needed mStatusBar.destroy(); mStatusBar = null; return WAIT_FOR_BARS_TO_DIE; } return 0; } /*系统配置改变*/ @Override protected void onConfigurationChanged(Configuration newConfig) { if (mStatusBar != null) { mStatusBar.onConfigurationChanged(newConfig); } } /*打印堆栈*/ @Override public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { if (mStatusBar != null) { mStatusBar.dump(fd, pw, args); } } /*从xml文件中获取PhoneStatusBar全路径,通过类加载器实例化类,调用其start*/ private void createStatusBarFromConfig() { if (DEBUG) Log.d(TAG, "createStatusBarFromConfig"); final String clsName = mContext.getString(R.string.config_statusBarComponent); if (clsName == null || clsName.length() == 0) { throw andLog("No status bar component configured", null); } Class<?> cls = null; try { cls = mContext.getClassLoader().loadClass(clsName); } catch (Throwable t) { throw andLog("Error loading status bar component: " + clsName, t); } try { mStatusBar = (BaseStatusBar) cls.newInstance(); } catch (Throwable t) { throw andLog("Error creating status bar component: " + clsName, t); } mStatusBar.mContext = mContext; mStatusBar.mComponents = mComponents; mStatusBar.start(); if (DEBUG) Log.d(TAG, "started " + mStatusBar.getClass().getSimpleName()); } private RuntimeException andLog(String msg, Throwable t) { Log.w(TAG, msg, t); throw new RuntimeException(msg, t); }}
我们先从start方法开始分析
@Override public void start() { if (DEBUG) Log.d(TAG, "start"); mServiceMonitor = new ServiceMonitor(TAG, DEBUG, mContext, Settings.Secure.BAR_SERVICE_COMPONENT, this); mServiceMonitor.start(); // will call onNoService if no remote service is found }
这里实例化ServiceMonitor类start,继续分析ServiceMonitor
ServiceMonitor.java ... public ServiceMonitor(String ownerTag, boolean debug, Context context, String settingKey, Callbacks callbacks) { mTag = ownerTag + ".ServiceMonitor"; mDebug = debug; mContext = context; mSettingKey = settingKey; // Settings.Secure.BAR_SERVICE_COMPONENT mCallbacks = callbacks; } public void start() { // listen for setting changes /*Settings.Secure.BAR_SERVICE_COMPONENT改变时回调*/ ContentResolver cr = mContext.getContentResolver(); cr.registerContentObserver(Settings.Secure.getUriFor(mSettingKey), false /*notifyForDescendents*/, mSettingObserver, UserHandle.USER_ALL); // listen for package/component changes //应用安装,改变,卸载会触发mBroadcastReceiver广播 IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_PACKAGE_ADDED); filter.addAction(Intent.ACTION_PACKAGE_CHANGED); filter.addAction(Intent.ACTION_PACKAGE_REMOVED); filter.addDataScheme("package"); mContext.registerReceiver(mBroadcastReceiver, filter); mHandler.sendEmptyMessage(MSG_START_SERVICE); } ...
ServiceMOnitor是一个监听Settings.Secure.BAR_SERVICE_COMPONENT是否改变的类,在start中通过监听系统系统时应用的变化来启动服务。
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { public void onReceive(Context context, Intent intent) { String pkg = intent.getData().getSchemeSpecificPart(); if (mServiceName != null && mServiceName.getPackageName().equals(pkg)) { mHandler.sendMessage(mHandler.obtainMessage(MSG_PACKAGE_INTENT, intent)); } }};
ServiceMOnitor.java应用装载时,通过Handler发送MSG_PACKAGE_INTENT消息事件,我们查看Handler消息回调 // internal handler + messages used to serialize access to internal state public static final int MSG_START_SERVICE = 1; //启动服务,并非真正启动,会根据ServiceName进行判断 public static final int MSG_CONTINUE_START_SERVICE = 2; //启动服务 public static final int MSG_STOP_SERVICE = 3;//停止服务消息 public static final int MSG_PACKAGE_INTENT = 4;//包安装事件消息 public static final int MSG_CHECK_BOUND = 5;//包改变或者卸载时,重新启动服务消息 public static final int MSG_SERVICE_DISCONNECTED = 6;//服务断开消息 private final Handler mHandler = new Handler() { public void handleMessage(Message msg) { switch(msg.what) { case MSG_START_SERVICE: startService(); break; case MSG_CONTINUE_START_SERVICE: continueStartService(); break; case MSG_STOP_SERVICE: stopService(); break; case MSG_PACKAGE_INTENT: packageIntent((Intent)msg.obj); break; case MSG_CHECK_BOUND: checkBound(); break; case MSG_SERVICE_DISCONNECTED: serviceDisconnected((ComponentName)msg.obj); break; } } }; private void packageIntent(Intent intent) { if (mDebug) Log.d(mTag, "packageIntent intent=" + intent + " extras=" + bundleToString(intent.getExtras())); if (Intent.ACTION_PACKAGE_ADDED.equals(intent.getAction())) { mHandler.sendEmptyMessage(MSG_START_SERVICE);//发送启动服务消息 } else if (Intent.ACTION_PACKAGE_CHANGED.equals(intent.getAction()) || Intent.ACTION_PACKAGE_REMOVED.equals(intent.getAction())) { final PackageManager pm = mContext.getPackageManager(); final boolean serviceEnabled = isPackageAvailable() && pm.getApplicationEnabledSetting(mServiceName.getPackageName()) != PackageManager.COMPONENT_ENABLED_STATE_DISABLED && pm.getComponentEnabledSetting(mServiceName) != PackageManager.COMPONENT_ENABLED_STATE_DISABLED; if (mBound && !serviceEnabled) { stopService(); scheduleCheckBound(); } else if (!mBound && serviceEnabled) { startService(); } } }
分析:当我们SystemUI应用检测到有新应用装载时,会发送MSG_START_SERVICE消息来启动服务,我们接着分析Handler的回调MSG_START_SERVICE消息。
private void startService() { mServiceName = getComponentNameFromSetting(); if (mDebug) Log.d(mTag, "startService mServiceName=" + mServiceName); if (mServiceName == null) { mBound = false; mCallbacks.onNoService(); } else { long delay = mCallbacks.onServiceStartAttempt(); mHandler.sendEmptyMessageDelayed(MSG_CONTINUE_START_SERVICE, delay); } } /*从ContentProvider数据库中取得包名*/ private ComponentName getComponentNameFromSetting() { String cn = Settings.Secure.getStringForUser(mContext.getContentResolver(), mSettingKey, UserHandle.USER_CURRENT); return cn == null ? null : ComponentName.unflattenFromString(cn); }
分析:首先从ContentProvider数据库中取得包名,如果没有启动,则回调CallBaback的onNoService服务,否则发送MSG_CONTINUE_START_SERVICE消息启动服务.
private void continueStartService() { if (mDebug) Log.d(mTag, "continueStartService"); Intent intent = new Intent().setComponent(mServiceName); try { mServiceConnection = new SC(); mBound = mContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE); if (mDebug) Log.d(mTag, "mBound: " + mBound); } catch (Throwable t) { Log.w(mTag, "Error binding to service: " + mServiceName, t); } if (!mBound) { mCallbacks.onNoService(); } }
分析:到此可以知道,当远程服务没有启动时,会回调SystemBar的onNoService函数,我们回到SystemBar,分析onNoService函数
... @Override public void onNoService() { if (DEBUG) Log.d(TAG, "onNoService"); createStatusBarFromConfig(); // fallback to using an in-process implementation } ... private void createStatusBarFromConfig() { if (DEBUG) Log.d(TAG, "createStatusBarFromConfig"); final String clsName = mContext.getString(R.string.config_statusBarComponent);//从xml文件读取类名 if (clsName == null || clsName.length() == 0) { throw andLog("No status bar component configured", null); } //通过类加载器实例化类 Class<?> cls = null; try { cls = mContext.getClassLoader().loadClass(clsName); } catch (Throwable t) { throw andLog("Error loading status bar component: " + clsName, t); } try { mStatusBar = (BaseStatusBar) cls.newInstance(); } catch (Throwable t) { throw andLog("Error creating status bar component: " + clsName, t); } mStatusBar.mContext = mContext; mStatusBar.mComponents = mComponents; mStatusBar.start();//调用类的start方法 if (DEBUG) Log.d(TAG, "started " + mStatusBar.getClass().getSimpleName()); }
分析:当远程服务没有启动时,首先从xml文件读取要启动的类名,我们来查看这个xml文件 res\values\config.xml。
res\values\config.xml com.android.systemui.statusbar.phone.PhoneStatusBar
分析:从上面分析得知,当实例化PhoneStatusBar类后会调用start方法,我们就从PhoneStatusBar的start方法开始.
@Override public void start() { //获取WindowManager,初始化当前显示界面大小 mDisplay = ((WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE)) .getDefaultDisplay(); updateDisplaySize(); //src绘图模式 mScrimSrcModeEnabled = mContext.getResources().getBoolean( R.bool.config_status_bar_scrim_behind_use_src); //调用父类start方法 super.start(); // calls createAndAddWindows() //MediaSession相关 mMediaSessionManager = (MediaSessionManager) mContext.getSystemService(Context.MEDIA_SESSION_SERVICE); // TODO: use MediaSessionManager.SessionListener to hook us up to future updates // in session state //添加导航栏 addNavigationBar(); // Lastly, call to the icon policy to install/update all the icons. //更新状态栏图标 mIconPolicy = new PhoneStatusBarPolicy(mContext, mCastController, mHotspotController, mUserInfoController, mBluetoothController); mIconPolicy.setCurrentUserSetup(mUserSetup); mSettingsObserver.onChange(false); // set up mHeadsUpObserver.onChange(true); // set up if (ENABLE_HEADS_UP) { mContext.getContentResolver().registerContentObserver( Settings.Global.getUriFor(Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED), true, mHeadsUpObserver); mContext.getContentResolver().registerContentObserver( Settings.Global.getUriFor(SETTING_HEADS_UP_TICKER), true, mHeadsUpObserver); } mUnlockMethodCache = UnlockMethodCache.getInstance(mContext); mUnlockMethodCache.addListener(this); //锁屏 startKeyguard(); mDozeServiceHost = new DozeServiceHost(); KeyguardUpdateMonitor.getInstance(mContext).registerCallback(mDozeServiceHost); putComponent(DozeHost.class, mDozeServiceHost); putComponent(PhoneStatusBar.class, this); /// M:add for multi window @{ if(MultiWindowProxy.isSupported()) { registerMWProxyAgain(); } /// @} setControllerUsers(); notifyUserAboutHiddenNotifications(); mScreenPinningRequest = new ScreenPinningRequest(mContext); } ...
接着分析PhoneStatusBar父类的BaseStatusBar的start方法
public abstract class BaseStatusBar extends SystemUI implements CommandQueue.Callbacks, ActivatableNotificationView.OnActivatedListener, RecentsComponent.Callbacks, ExpandableNotificationRow.ExpansionLogger, NotificationData.Environment {public void start() { Log.d(TAG, "[*zxDebug*] BaseStatusBar start()"); mWindowManager = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE); mWindowManagerService = WindowManagerGlobal.getWindowManagerService(); mDisplay = mWindowManager.getDefaultDisplay(); mDevicePolicyManager = (DevicePolicyManager)mContext.getSystemService( Context.DEVICE_POLICY_SERVICE); mNotificationColorUtil = NotificationColorUtil.getInstance(mContext); mNotificationData = new NotificationData(this); mAccessibilityManager = (AccessibilityManager) mContext.getSystemService(Context.ACCESSIBILITY_SERVICE); mDreamManager = IDreamManager.Stub.asInterface( ServiceManager.checkService(DreamService.DREAM_SERVICE)); mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); mBarService = IStatusBarService.Stub.asInterface( ServiceManager.getService(Context.STATUS_BAR_SERVICE)); mRecents = getComponent(RecentsComponent.class); mRecents.setCallback(this); final Configuration currentConfig = mContext.getResources().getConfiguration(); StatusBarIconList iconList = new StatusBarIconList(); mCommandQueue = new CommandQueue(this, iconList); int[] switches = new int[8]; ArrayList binders = new ArrayList(); try { mBarService.registerStatusBar(mCommandQueue, iconList, switches, binders); } catch (RemoteException ex) { // If the system process isn't there we're doomed anyway. } Log.d(TAG, "[*zxDebug*] BaseStatusBar start() begin createAndAddWindows"); createAndAddWindows(); Log.d(TAG, "[*zxDebug*] BaseStatusBar start() over createAndAddWindows"); disable(switches[0], false /* animate */); setSystemUiVisibility(switches[1], 0xffffffff); topAppWindowChanged(switches[2] != 0); // StatusBarManagerService has a back up of IME token and it's restored here. setImeWindowStatus(binders.get(0), switches[3], switches[4], switches[5] != 0);...}}
分析:BaseStatusBar关于StatusBar相关的最主要是调用了createAndAddWindows方法,我们看下这个方法的定义
/** * Create all windows necessary for the status bar (including navigation, overlay panels, etc) * and add them to the window manager. */ protected abstract void createAndAddWindows();
分析:这是一个抽象方法,也就是说,它会回调到子类的createAndAddWindows的实现方法中,我们重新回到PhoneStatusBar中,找到createAndAddWindows的方法实现
@Override public void createAndAddWindows() { addStatusBarWindow(); } private void addStatusBarWindow() { makeStatusBarView();//创建statusbar视图 mStatusBarWindowManager = new StatusBarWindowManager(mContext); //通过StatusBarWindowManager类的add方法加载到Window窗体中 mStatusBarWindowManager.add(mStatusBarWindow, getStatusBarHeight()); } ... protected PhoneStatusBarView makeStatusBarView() { final Context context = mContext; //通过Resources更新显示大小和一些资源文件 Resources res = context.getResources(); updateDisplaySize(); // populates mDisplayMetrics updateResources(); //加载StartBarWindowView视图 mStatusBarWindow = (StatusBarWindowView) View.inflate(context, R.layout.super_status_bar, null); mStatusBarWindow.setService(this); //监听下拉事件 mStatusBarWindow.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { checkUserAutohide(v, event); if (event.getAction() == MotionEvent.ACTION_DOWN) { if (mExpandedVisible) { animateCollapsePanels(); } } return mStatusBarWindow.onTouchEvent(event); } }); //状态栏 mStatusBarView = (PhoneStatusBarView) mStatusBarWindow.findViewById(R.id.status_bar); mStatusBarView.setBar(this); // PanelHolder holder = (PanelHolder) mStatusBarWindow.findViewById(R.id.panel_holder); mStatusBarView.setPanelHolder(holder); //通知栏 mNotificationPanel = (NotificationPanelView) mStatusBarWindow.findViewById( R.id.notification_panel); mNotificationPanel.setStatusBar(this); // M: setBackground in 512 low ram device if (!ActivityManager.isHighEndGfx() && !FeatureOptions.LOW_RAM_SUPPORT) { mStatusBarWindow.setBackground(null); mNotificationPanel.setBackground(new FastColorDrawable(context.getColor( R.color.notification_panel_solid_background))); } try { //是否显示导航栏 boolean showNav = mWindowManagerService.hasNavigationBar(); Log.v(TAG, "hasNavigationBar=" + showNav); if (showNav) { /// M: add for multi window @{ //加载导航栏布局 int layoutId = R.layout.navigation_bar; if(MultiWindowProxy.isSupported()) { layoutId = R.layout.navigation_bar_float_window; } mNavigationBarView = (NavigationBarView) View.inflate(context, /*R.layout.navigation_bar*/layoutId, null); /// @} mNavigationBarView.setDisabledFlags(mDisabled1); mNavigationBarView.setBar(this); mNavigationBarView.setOnVerticalChangedListener( new NavigationBarView.OnVerticalChangedListener() { @Override public void onVerticalChanged(boolean isVertical) { if (mAssistManager != null) { mAssistManager.onConfigurationChanged(); } mNotificationPanel.setQsScrimEnabled(!isVertical); } }); //设置导航栏触摸事件 mNavigationBarView.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { checkUserAutohide(v, event); return false; }}); } } catch (RemoteException ex) { // no window manager? good luck with that } mAssistManager = new AssistManager(this, context); // figure out which pixel-format to use for the status bar. mPixelFormat = PixelFormat.OPAQUE;.....
分析:关键部分是在判断导航栏是否显示那一块。由mWindowManagerService的hasNavigationBar来决定是否显示导航栏,同时通过加载navigation_bar(多窗口加载navigation_bar_float_window)布局来显示导航栏,我们来查看hasNavigationBar方法,因为mWidnwoManagerService是IWindowManagerService由PhoneWindowManager进行调用:
frameworks\base\service\core\java\com\android\server\PhoneWindowManager.javaPhoneWindowManager ... // Use this instead of checking config_showNavigationBar so that it can be consistently // overridden by qemu.hw.mainkeys in the emulator. @Override public boolean hasNavigationBar() { return mHasNavigationBar; } ... mHasNavigationBar = res.getBoolean(com.android.internal.R.bool.config_showNavigationBar); // Allow a system property to override this. Used by the emulator. // See also hasNavigationBar(). String navBarOverride = SystemProperties.get("qemu.hw.mainkeys"); if ("1".equals(navBarOverride)) { mHasNavigationBar = false; } else if ("0".equals(navBarOverride)) { mHasNavigationBar = true; } ...从framework\base\core\res\res\valuse\config.xml中获取mHashNavigationBar的值 ture
分析:这里给我们提供一个方法去控制导航栏是否显示。
更多相关文章
- [Android] ListView (普通列表控件) 的基本使用方法
- Google Cloud Messaging (Android 消息推送技术) (一)
- JS调用Android里面的方法,Android调用JS里面的方法
- Android App多个入口的实现方法
- Android 项目导入eclipse中报错但找不到错误地方的解决方法
- Linux ubuntu repo安装方法