彻底掌握Android中的ViewModel

彻底掌握Android中的ViewModel

ViewModel 属于Android Jetpack库的一部分,是一种业务逻辑或屏幕状态容器。它提供了在配置更改(如屏幕旋转)后依旧保留相应状态的特性,帮助开发者以更加清晰和可维护的方式处理UI相关的数据,从而避免了在 Activity 或 Fragment 中直接处理数据持久化的问题。

ViewModel的使用

创建

日常开发中,ViewModel 经常充当 MVVM 架构的 VM 层,分担 Activity/Fragment 的部分逻辑,充当页面的数据存储容器。ViewModel 的创建方式有好几种,官方的 API 也改了几版,ViewModelProviders 已标为废弃,目前创建 ViewModel 统一使用 ViewModelProvider,代码实现有如下几种方式:

kotlin 复制代码
//无参构造函数ViewModel
class MyViewModel() : ViewModel() {
    //...
}

1.通过ViewModelProdiver

Activity 中:

kotlin 复制代码
private val viewModel by lazy {
	ViewModelProvider(this).get(MyViewModel::class.java)
}

Fragment 中:

kotlin 复制代码
private val viewModel by lazy {
	ViewModelProvider(this).get(MyViewModel::class.java)	//关联的是Fragment
}

private val viewModel by lazy {
	ViewModelProvider(requireActivity()).get(MyViewModel::class.java)	//关联的是Activity
}

2.通过Android KTX

KTX 扩展库提供了很多常用功能的简洁实现,KTX 分为若干模块,开发者需要按需引用,这里需要用到 Fragment KTX 模块,首先将该模块代码依赖到工程:

groovy 复制代码
implementation "androidx.fragment:fragment-ktx:1.6.2"

然后就可以用以下方式进行 ViewModel 的创建了,代码非常简洁:

Activity 中:

kotlin 复制代码
private val viewModel by viewModels<MyViewModel>()

Fragment 中:

kotlin 复制代码
private val viewModel1 by viewModels<MyViewModel>()	 //关联的是Fragment
private val viewModel2 by activityViewModels<MyViewModel>()  //关联的是Activity

3.有参数的ViewModel创建方式

上面两种创建的 ViewModel 构造器都是无参数的,但 ViewModel 有时候也需要依赖注入外部对象,这时 ViewModel 就需要提供有参数的构造器,重点是创建自定义的 ViewModel 的创建工厂。

先看下 UserViewModel 的定义:

kotlin 复制代码
//数据仓库层
object UserRepo {
    //网络逻辑...
}

//构造器有参的ViewModel
class UserViewModel(val repo: UserRepo) : ViewModel() {
	//...
}

一般情况下,重写 ViewModelProvider.Factory 一个参数的 create 方法即可:

kotlin 复制代码
class Factory1(val repo: UserRepo): ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        return UserViewModel(repo) as T
    }
}

创建代码:

kotlin 复制代码
//通过ViewModelProvider方式:
private val viewModel by lazy {
    ViewModelProvider(this, UserViewModel.Factory1(UserRepo)).get(UserViewModel::class.java)
}

//通过KTX方式:
private val viewModel by viewModels<UserViewModel>(factoryProducer = { UserViewModel.Factory1(UserRepo) })

ViewModelProvider 中还提供了几个默认工厂:

  1. NewInstanceFactory:用来创建无参的 ViewModel,也是 ViewModelProvider 的默认工厂。
  2. AndroidViewModelFactory:继承自 NewInstanceFactory ,用来创建构造函数需要 Application 参数的 ViewModel 实例,特殊情况下会调用 NewInstanceFactory 创建无参的 ViewModel。

其实 KTX 最后也是通过 ViewModelProdiver 进行创建的,只不过通过 Kotlin 的属性委托机制将语法简化了,源码如下:

kotlin 复制代码
//ViewModel会通过by关键字委托到该类,每次使用该属性时,都会走到get方法中
public class ViewModelLazy<VM : ViewModel> @JvmOverloads constructor(
    private val viewModelClass: KClass<VM>,
    private val storeProducer: () -> ViewModelStore,
    private val factoryProducer: () -> ViewModelProvider.Factory,
    private val extrasProducer: () -> CreationExtras = { CreationExtras.Empty }
) : Lazy<VM> {
    private var cached: VM? = null

    override val value: VM
        get() {
            val viewModel = cached
            return if (viewModel == null) {
                val factory = factoryProducer()
                val store = storeProducer()
                //最终还是通过ViewModelProvider进行创建的
                ViewModelProvider(
                    store,
                    factory,
                    extrasProducer()
                ).get(viewModelClass.java).also {
                    cached = it
                }
            } else {
                viewModel
            }
        }

    override fun isInitialized(): Boolean = cached != null
}
使用

ViewModel 一般作为 MVVM 架构的 VM 层,可以将 Activity/Fragment 的业务逻辑都封装到 ViewModel 中,比较常见的就是网络请求了。Google 推荐如下方式实现:

kotlin 复制代码
class MyViewModel : ViewModel() {
    private val _userLiveData: MutableLiveData<User> = MutableLiveData<User>()
    val userData: LiveData<User>    //外部获取的类型是LiveData,不可变的,防止外部随意修改
        get() = _userLiveData
    fun doAction() {
        //...比如请求网络,并更新user
        _userLiveData.postValue(User("白泽..."))
    }
}
kotlin 复制代码
class ViewModelActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        val viewModel = ViewModelProvider(this).get(MyViewModel::class.java)
        viewModel.userData.observe(this) {  //1.订阅数据变化
            //update UI
        }

        findViewById<Button>(R.id.button).setOnClickListener {
            viewModel.doAction()	//2.触发数据更新
        }
    }
}

ViewModel是如何存储的

首先来看看 ViewModel 的几个重要类,分别是 ViewMdoel、ViewModelProvider、ViewModelStore 和 ViewModelStoreOwner,关系类图如下:

这不是严格的UML图,只需大概理解即可,下面介绍下每个类的职责:

  • ViewModelProvider:只负责 ViewModel 的创建。无参构造器的 ViewModel 可以直接用其内部提供的 NewInstanceFactory 工厂创建,如果ViewModel 需要构造器参数,则需要实现 ViewModelProvider.Factory 接口并完善创建逻辑。

  • ViewModelStore:负责 ViewModel 实例的存储,内部通过 HashMap 实现,map 的 value 就是 ViewMode 的实例,key 的生成规则如下:

    java 复制代码
    private static final String DEFAULT_KEY = "androidx.lifecycle.ViewModelProvider.DefaultKey"
        //...
        String canonicalName = modelClass.getCanonicalName(); //返回此类的规范名称,如com.xx.x.MyViewModel
        get(DEFAULT_KEY + ":" + canonicalName, modelClass);
    	//...

    所以同一 ViewModelStore 中,同一个类型的 ViewModel 并不会重复创建。

  • ViewModelStoreOwner:负责提供 ViewModelStore,常见的 ViewModelStoreOwner 有 ComponentActivity、Fragment 等,它们的内部会对 ViewModelStore 进行管理,在适当的时机进行创建和回收。以 ComponentActivity 为例,其内部会监听生命周期,并在生命周期变动时调用如下代码,确保 mViewModelStore 的存在:

    java 复制代码
    //1.Activity销毁时调用该方法临时保存ViewModelStore
    public final Object onRetainNonConfigurationInstance() {
        // Maintain backward compatibility.
        Object custom = onRetainCustomNonConfigurationInstance();
    
        ViewModelStore viewModelStore = mViewModelStore;
        if (viewModelStore == null) {
            // No one called getViewModelStore(), so see if there was an existing
            // ViewModelStore from our last NonConfigurationInstance
            NonConfigurationInstances nc =
                    (NonConfigurationInstances) getLastNonConfigurationInstance();
            if (nc != null) {
                viewModelStore = nc.viewModelStore;
            }
        }
    
        if (viewModelStore == null && custom == null) {
            return null;
        }
    
        NonConfigurationInstances nci = new NonConfigurationInstances();
        nci.custom = custom;
        nci.viewModelStore = viewModelStore;
        return nci;
    }
    
    //2.Activity创建时恢复上次保存的ViewModelStore
    void ensureViewModelStore() {
        if (mViewModelStore == null) {
            NonConfigurationInstances nc =
                    (NonConfigurationInstances) getLastNonConfigurationInstance();
            if (nc != null) {
                // Restore the ViewModelStore from NonConfigurationInstances
                mViewModelStore = nc.viewModelStore;  //拿到上次保存的ViewModelStore
            }
            if (mViewModelStore == null) {
                mViewModelStore = new ViewModelStore();  //创建新的ViewModelStore
            }
        }
    }

    ViewModelStore 的销毁时机:Activity 走到 Destroy 并且使非配置更改(如正常finish)。

    java 复制代码
    getLifecycle().addObserver(new LifecycleEventObserver() {
        @Override
        public void onStateChanged(@NonNull LifecycleOwner source,
                @NonNull Lifecycle.Event event) {
            if (event == Lifecycle.Event.ON_DESTROY) {
                // Clear out the available context
                mContextAwareHelper.clearAvailableContext();
                // And clear the ViewModelStore
                if (!isChangingConfigurations()) {
                    getViewModelStore().clear();
                }
            }
        }
    });

    面试常问的问题:

    为什么 Activity 在旋转屏幕时,Activity 对象都发生重建了,但 ViewModel 却还是原来的对象?

    ViewModel 是怎么保存和恢复的?

    上面两个问题问的其实是 mViewModelStore 的保存和恢复,因为它是持有 ViewModel 实例的仓库。而 mViewModelStore 的存储和恢复是通过 onRetainNonConfigurationInstancegetLastNonConfigurationInstance来实现的。

    在配置更改时会调用 Activity#onRetainNonConfigurationInstance() 保存 mViewModelStore 对象,并在 Activity 重建后通过 getLastNonConfigurationInstance 方法获取上次保存的 ViewModelStore 对象,如果有则直接使用,否则创建新的实例对象。

    状态保存:onRetainNonCongigurationInstance

    该方法是 Android 提供的在配置更改时,临时保存 Activity 数据的 API。onRetainNonConfigurationInstance() 允许 Activity 在配置改变之前返回一个对象,这个对象随后可以在 Activity 重新创建后的getLastNonConfigurationInstance()方法中被检索到。

    下面是该机制的源码,在设备配置发生更改时(如旋转屏幕),会调用到 ActivityThread#handleRelaunchActivity 方法:

    java 复制代码
    //看方法名字可以知道是处理Activity重建逻辑的
    public void handleRelaunchActivity(ActivityClientRecord tmp, PendingTransactionActions pendingActions) {
        //...
        handleRelaunchActivityInner(r, configChanges, tmp.pendingResults, tmp.pendingIntents,
                pendingActions, tmp.startsNotResumed, tmp.overrideConfig, "handleRelaunchActivity");
        //...
    }
    
    private void handleRelaunchActivityInner(ActivityClientRecord r, int configChanges,
            List<ResultInfo> pendingResults, List<ReferrerIntent> pendingIntents,
            PendingTransactionActions pendingActions, boolean startsNotResumed,
            Configuration overrideConfig, String reason) {
        //1. 处理旧Activity的销毁,注意第三个参数是getNonConfigInstance,传入的是true
        handleDestroyActivity(r, false, configChanges, true, reason);
    
        //2. 处理Activity新建逻辑
        handleLaunchActivity(r, pendingActions, customIntent);
    }

    这个方法处理了两件事,一是旧 Activity 的回收,二是 Activity 的新建。先从 Activity 销毁开始看:

    java 复制代码
    @Override
    public void handleDestroyActivity(ActivityClientRecord r, boolean finishing, int configChanges,
            boolean getNonConfigInstance, String reason) {
        performDestroyActivity(r, finishing, configChanges, getNonConfigInstance, reason);
        //...
    }
    
    void performDestroyActivity(ActivityClientRecord r, boolean finishing,
            int configChanges, boolean getNonConfigInstance, String reason) {
        //...注意:getNonConfigInstance为true
        if (getNonConfigInstance) {
            try {
                //调用旧activity的方法,并保存到ActivityClientRecord对象中
                r.lastNonConfigurationInstances = r.activity.retainNonConfigurationInstances();
            } catch (Exception e) {
                //...
            }
        }
        //...
    }
    java 复制代码
    NonConfigurationInstances retainNonConfigurationInstances() {
        Object activity = onRetainNonConfigurationInstance();  //1.调用了onRetainNonConfigurationInstance方法
        HashMap<String, Object> children = onRetainNonConfigurationChildInstances(); //2.可以缓存一些自定义数据
        FragmentManagerNonConfig fragments = mFragments.retainNestedNonConfig();  //3.Fragment状态
    	//...
        NonConfigurationInstances nci = new NonConfigurationInstances();
        nci.activity = activity;
        nci.children = children;
        nci.fragments = fragments;
        nci.loaders = loaders;
        if (mVoiceInteractor != null) {
            mVoiceInteractor.retainInstance();
            nci.voiceInteractor = mVoiceInteractor;
        }
        return nci;
    }

    可以看到,Activity 销毁做了两件事:

    1. 调用了 Activity 对象的 onRetainNonConfigurationInstance 方法拿到临时对象,并赋值给 ActivityClientRecord#lastNonConfigurationInstances变量。
    2. 调用 Activity 的 pause、stop、destroy 等生命周期方法。

    接下来看 Activity 新建逻辑:

    java 复制代码
    public Activity handleLaunchActivity(ActivityClientRecord r,
            PendingTransactionActions pendingActions, Intent customIntent) {
        //...
        final Activity a = performLaunchActivity(r, customIntent);
    	//...
    }
    
    private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
        //...
        Activity activity = null;
        try {
            java.lang.ClassLoader cl = appContext.getClassLoader();
            //1.通过反射创建Activity对象
            activity = mInstrumentation.newActivity(
                    cl, component.getClassName(), r.intent);
        }
        //...
        
        try {
            if (activity != null) {
                //2.调用 attach 方法,将缓存信息传入
                activity.attach(appContext, this, getInstrumentation(), r.token,
                        r.ident, app, r.intent, r.activityInfo, title, r.parent,
                        r.embeddedID, r.lastNonConfigurationInstances, config,
                        r.referrer, r.voiceInteractor, window, r.activityConfigCallback,
                        r.assistToken, r.shareableActivityToken);
    	//...
        return activity;
    }

    创建 Activity 流程同样做了两件事:

    1. 通过反射创建 Activity 实例对象
    2. 调用 attach 方法,将销毁时缓存在 ActivityClientRecord#lastNonConfigurationInstances 变量中的临时变量关联到新的 Activity 对象
    java 复制代码
    final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken,
            IBinder shareableActivityToken) {
    	//赋值给了mLastNonConfigurationInstances
        mLastNonConfigurationInstances = lastNonConfigurationInstances;
    }
    
    public Object getLastNonConfigurationInstance() {
        return mLastNonConfigurationInstances != null
                ? mLastNonConfigurationInstances.activity : null;
    }

    执行完 Activity 的 attach 方法后,就可以通过 getLastNonConfigurationInstance 方法获取之前 Activity 销毁时保存的状态数据了,到此 Activity 保存和恢复数据的链路就通了。

    onRetainNonConfigurationInstance 方法用 final 修饰,并已标为废弃了,其实改保存数据方案在 Android 3.0 就已经废弃了,Google 不希望我们用这个机制去进行 Activity 状态保存,而是推荐用基于该机制上衍生的 ViewModel 进行状态保存,使用起来更简单、更安全。

    onRetainNonCongigurationInstance 和 onSaveInstanceState区别:

    onSaveInstanceState 同样是用于处理 Activity 状态保存和恢复的方法,它与 onRetainNonCongigurationInstance 方式区别如下:

    1. 使用场景不同
      • onRetainNonCongigurationInstance 用于在设备配置更改时(如屏幕旋转)临时保存 Activity 的状态或数据。
      • onSaveInstanceState 用于Activity 即将被销毁时(无论是由于用户离开、配置更改还是系统回收资源),保存 Activity 的状态或数据,是一个更通用、更灵活的状态保存机制。。
    2. 支持数据类型不同
      • onRetainNonCongigurationInstance 返回 Object 类型对象,可以是任何类型,包括 Activity 实例本身或大型数据结构,如果使用不当容易造成内存泄漏。
      • onSaveInstanceState 通过 Bundle 对象来保存状态,只能存储基本数据类型、可序列化的对象或实现了 Parcelable 接口的对象,确保数据的安全性和可恢复性。
    3. 恢复数据方式不同
      • onRetainNonConfigurationInstance 在Activity重新创建后,可以通过调用getLastNonConfigurationInstance()方法来检索之前保存的数据。
      • onSaveInstanceState 在Activity重新创建时,系统会将之前保存的Bundle对象传递给onCreate(Bundle savedInstanceState)onRestoreInstanceState(Bundle savedInstanceState)方法。

ViewModel的协程作用域

协程是 Kotlin 的又一高效编程利器,使用协程可以非常简单的进行多线程协作。ViewModel 提供了和其生命周期一致协程作用域,可以引入 KTX 的 ViewModel 模块,让使用更简单:

kotlin 复制代码
dependencies {
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.6"
}

使用时:

kotlin 复制代码
viewModel.viewModelScope.launch {
    if (isActive) {  //判断协程是否被取消了
        //doAction...
    }
}

下面来探索一下 ViewModel 协程作用域是如何管理的,首先看 viewModelScope 源码:

kotlin 复制代码
public val ViewModel.viewModelScope: CoroutineScope
    get() {
        val scope: CoroutineScope? = this.getTag(JOB_KEY)  //1.缓存的协程作用域对象
        if (scope != null) {
            return scope
        }
        //2.创建协程
        return setTagIfAbsent(
            JOB_KEY,
            CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
        )
    }

可以看到,如果没从缓存获取到则直接通过 setTagIfAbsent 方法创建,协程上下文类型是 SupervisorJobDispatchers.Main,可以让发生异常时不影响其他协程,并且代码运行在主线程。

kotlin 复制代码
private final Map<String, Object> mBagOfTags = new HashMap<>();

//1.setIfAbsend 系列方法一般表示,如果存在旧值就不进行赋值了,防止多次创建
<T> T setTagIfAbsent(String key, T newValue) {
    T previous;
    //2.通过同步锁,防止多线程场景错误
    synchronized (mBagOfTags) {
        previous = (T) mBagOfTags.get(key);
        if (previous == null) {
            mBagOfTags.put(key, newValue);
        }
    }
    T result = previous == null ? newValue : previous;
    if (mCleared) {
        closeWithRuntimeException(result);
    }
    return result;
}

通过该方法可以看到,ViewModel 的协程作用域创建后会缓存在 ViewModel 中的 mBagOfTags 中,这是一个 map 结构,key 为 JOB_KEY ,value 为协程作用域对象。

知道了 ViewModel 的协程作用域是如何创建和保存的,下面看协程是如何取消的。

还记得上面 ViewModel 对象的保存逻辑吗,ComponentActivity 会监听 DESTROY 生命周期,Activity 正常销毁时,会执行 ViewModelStore 的 clear 方法,ViewModelStore 又会遍历所有保存的 ViewModel 对象,并调用其 clear 方法。

kotlin 复制代码
final void clear() {
    mCleared = true;
    //1.加锁,进行协程作用域的取消
    if (mBagOfTags != null) {
        synchronized (mBagOfTags) {
            for (Object value : mBagOfTags.values()) {
                //2.取消协程
                closeWithRuntimeException(value);
            }
        }
    }
    //2.ViewModel销毁时会调用该方法,可以重写它进行长时间任务的清理工作
    onCleared();
}

可见最终会调用到 closeWithRuntimeException 方法进行协程取消,内部其实是 coroutineContext.cancel()

ViewModel 的协程作用域会在获取时进行创建,并缓存在 ViewModel 的 mBagOfTags 映射表内部,在 ViewModel 销毁时取消。协程取消并不会强制终端代码逻辑,使用 ViewModel 协程作用域进行长时间任务时,注意使用 isActive 方法适时判断协程是否被取消了。

ViewModel 的协程作用域和 Lifecycle 的协程作用域有何区别?

Android 中除了 ViewModel 提供了协程作用域外,Lifecycle 也提供了 lifecycleScope 协程作用域,首先看下 Lifecycle 的协程作用域是如何创建、保存和销毁的:

kotlin 复制代码
//Lifecycle:
//1.AtomicReference让对象读,写都是原子操作,保证修改对象引用时的线程安全
public var internalScopeRef: AtomicReference<Any> = AtomicReference<Any>()

public val Lifecycle.coroutineScope: LifecycleCoroutineScope
    get() {
        while (true) {
            val existing = internalScopeRef.get() as LifecycleCoroutineScopeImpl?
            if (existing != null) {
                return existing
            }
            //2.创建协程作用域
            val newScope = LifecycleCoroutineScopeImpl(
                this,
                SupervisorJob() + Dispatchers.Main.immediate
            )
            //3.通过CAS机制设置对象
            if (internalScopeRef.compareAndSet(null, newScope)) {
                newScope.register()
                return newScope
            }
        }
    }
kotlin 复制代码
internal class LifecycleCoroutineScopeImpl(
    override val lifecycle: Lifecycle,
    override val coroutineContext: CoroutineContext
) : LifecycleCoroutineScope(), LifecycleEventObserver {
    init {
        if (lifecycle.currentState == Lifecycle.State.DESTROYED) {
            coroutineContext.cancel() //1.取消协程
        }
    }

    fun register() {
        launch(Dispatchers.Main.immediate) {
            if (lifecycle.currentState >= Lifecycle.State.INITIALIZED) {
                lifecycle.addObserver(this@LifecycleCoroutineScopeImpl)
            } else {
                coroutineContext.cancel()
            }
        }
    }

    override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
        if (lifecycle.currentState <= Lifecycle.State.DESTROYED) {
            lifecycle.removeObserver(this)
            coroutineContext.cancel()  //2.取消协程
        }
    }
}

Lifecycle 的代码非常简单,同样在获取时创建协程作用域,并通过CAS机制保存到 internalScopeRef 对象引用,在 DESTROY 时进行协程的销毁操作。

通过上面分析 ViewModel 和 Lifecycle 的协程作用域相关代码,可以分析出以下几点区别:

  • ViewModel 协程作用域创建时通过 synchronized 同步锁保证线程安全;Lifecycle 协程作用域创建时通过 while 循环 + CAS 机制保证线程安全。相对来说 CAS 机制更能保证效率,ViewModel 使用 synchronized,主要还是因为其中的 mBagOfTags,它是一个Map,Android 官方因为一些旧系统的限制,导致无法使用ConcurrentHashMap,所以才出此下策。
  • ViewModel 和 Lifecycle 的协程作用域生命周期不同,因为销毁时机不一样,就像刚开始 ViewModel 的生命周期和 Activity 生命周期一样。

总结

Android 的 ViewModel 是一个强大的架构组件,它通过提供数据、管理状态以及生命周期感知等能力,帮助开发者构建更加健壮、易于维护和测试的应用。在 MVVM 代码架构中,ViewModel 是视图(View)与数据(Model)之间的桥梁,它负责为 UI组件提供数据,并管理 UI组件的状态,UI状态与业务逻辑分离,使得代码耦合性更低,更易于测试。

相关推荐
如果可以00329 分钟前
Android OpenGLES2.0开发(一):艰难的开始
android·opengl es
9毫米的幻想2 小时前
【C++】—— stack & queue & deque
android·java·c语言·开发语言·c++·学习
8931519602 小时前
Android开发okhttp下载图片带进度
android·okhttp·android下载图片带进度·android下载网络图片
西瓜本瓜@2 小时前
在Android开发中可以用到的Ui控件有哪些?
android·java·开发语言·ui·android studio·kt
粤M温同学3 小时前
Android Studio 真机USB调试运行频繁掉线问题
android·android studio
编程乐学3 小时前
网络资源模板--Android Studio 图书借阅App
android·ide·毕业设计·android studio·图书管理·图书借阅·安卓课设
随遇而安的生活5 小时前
Unity android 接USBCamera
android·unity3d
korolOne5 小时前
Android Camera 预览角度和拍照保存图片角度相关
android
张海龙_China6 小时前
成为谷歌开发者专家(GDE)的经历
android·ios