Android架构组件-App架构指南

金歌  论坛元老 | 2024-11-13 23:23:14 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 1697|帖子 1697|积分 5091


  • 通用架构原则
  • 保举的App架构
  • 搭建用户界面
  • 获取数据
  • 关联ViewModel和repository
  • 缓存数据
  • 长期化数据
  • 测试
  • 最终的架构
  • 指导原则
  • 附录暴露网络状态
本指南适用于那些已经拥有开辟Android应用基础知识的开辟人员,如今想了解能够开辟出更加健壮、优质的应用程序架构。
   注意:本指南假定读者认识Android框架。如果你不认识Android应用程序开辟,请检察Android入门培训教程,其中包罗本指南的必备内容。
  起首需要说明的是:Android Architecture Components 翻译为 Android架构组件 并不是我本身随意翻译的,而是Google官方博客中明确称其为 Android架构组件,因此我遵照了这种叫法。
在这里我先贴上Google原文地点,以及Android架构组件官方开源示例代码地点。
Google原文地点:developer.android.google.cn/topic/libra…
Android架构组件官方开源示例代码地点(android-architecture-components): github.com/googlesampl…
下面这张图是Android架构组件完整的架构图,其中表示了的架构组件的全部模块以及它们之间如何交互:

APP开辟者面临的常见问题


与传统的桌面应用程序不同,Android应用程序的结构要复杂得多,在大多数情况下,它们只在桌面快捷启动方式中有一个入口,并且作为单个进程运行。一个典型的Android应用程序是由多个 app组件(Android四大组件) 构成的,包括 activities, fragments, services, content providers and broadcast receivers
这些 app组件 中的大部门都是在 **应用清单(AndroidManifast.xml)**中声明的,Android操纵系统利用这些组件来决定如何将应用程序集成到设备的用户体验中。固然,如前所述,桌面应用程序通常上是以单个进程运行的,但是一个公道的Android应用需要更加机动,因为用户可以通过不同的应用程序,在他们的设备上不断切换流程和使命。
比方,想象下在您最喜爱的交际网络应用中分享照片时会发生什么情况。这个应用程序触发了一个Camera(拍照或摄像)意图,由Android操纵系统启动一个Camera应用来处理请求。此时,用户固然脱离了这个交际网络应用,但他们的体验是无缝的。相机应用程序又大概触发其他意图,比方启动文件选择器,该文件选择器可以启动另一个应用程序。最终用户回到交际网络应用并分享照片。别的,用户在这个过程的任何时候都大概被电话打断,并在打完电话后回来继续分享照片。
在Android中,这种应用程序跳转举动是很常见的,以是您的应用程序必须正确处理这些流程。请记住,移动设备是资源受限的,以是在任何时候,操纵系统都大概需要杀死一些应用程序,以腾出空间给新的应用。
这一切的要点在于,您的 app组件 可以单独和无序地启动,并且可以在任何时候由用户或系统销毁。由于 app组件 是短暂的,并且它们的生命周期(创建和销毁时)不在您的控制之下,因此您不应该在app组件中存储任何 app数据或状态,并且 你的 app组件不应相互依靠。
通用架构原则


如果你不利用 app组件存储app数据和状态,那么应该如何构造应用程序呢?
你关注的最紧张的事变是如何在你的应用中分离关注点。常见的错误是将全部的代码写入一个Activity或Fragment,任何不处理 UI 或 与操纵系统交互的代码都不应该出如今这些类中,你应该尽大概保持 Activity或Fragment 精简,如许可以制止许多生命周期相关的问题。请记住,你不拥有这些类,它们只是建立操纵系统和你的应用程序之间契约的胶水类。Android操纵系统大概会随时根据用户交互或其他因素(如低内存)来销毁它们。最好尽大概地减少依靠他们,以提供可靠的用户体验。
第二个紧张原则是 你应该从一个模型驱动你的UI,最好是一个长期化的模型。之以是说长期化是理想的模型,原因有两个:如果操纵系统销毁你的应用程序以开释资源,那么你的用户就不会丢失数据,即使网络毗连不稳定或毗连不上,您的应用程序也会继续工作。模型是负责处理应用程序数据的组件。它们独立于应用程序的 Views 和 app组件,因此模型与这些 app组件的生命周期问题是相隔离的。保持简洁的UI代码,以及不受束缚的应用程序逻辑,可以使app的管理更加轻易,基于具有明确定义的管理数据责任的模型类的应用程序,会更加具有可测试性,并使您的应用程序状态保持前后同等。
保举的App架构


在本节中,我们将演示如何通过利用用例来构造利用了 架构组件(Architecture Components) 的应用程序。
   注意:不大概有一种对每个场景都是最好的编写应用程序的方法。也就是说,对于大多数用例来说,这个保举的架构大概是一个好的出发点。如果你已经有了编写Android应用的好方法,那就不要在更改了。
  我们如今可以想象一下,如果我没正在搭建一个用来显示 用户概况的UI。该用户概况将利用 REST API从我们本身的服务器端获取。
搭建用户界面

这个UI 将由 UserProfileFragment.java 及 Fragment 相应的 user_profile_layout.xml 布局文件构成。
为了驱动用户界面,我们的数据模型需要生存两个数据元素。


  • 用户ID:用户的标识符。最好利用 fragment 参数(setArguments方法) 将此信息通报到 fragment 中。如果Android系统销毁了你的进程,这些信息将被生存,便于应用在下次重新启动时可用。
  • 用户对象:生存用户数据的 POJO(简朴的Java对象)
我们将创建一个基于ViewModel 的 UserProfileViewModel 类来生存这些信息。
   一个 ViewModel 提供了一个特定 UI 组件中的数据,如一个 fragment 或 activity, 并且负责与数据处理业务的通讯,比方调用其他app组件来加载数据或转发的用户信息的修改。ViewModel不知道View,并且不受设置更改的影响,比方由于屏幕旋转而重新创建 Activity。
  如今我们有3个文件。


  • user_profile.xml:定义屏幕上的 UI。
  • UserProfileViewModel.java:为 UI 预备数据的类。
  • UserProfileFragment.java:显示 ViewModel 中的数据并对用户交互作出响应的 UI 控制器。
接下来我们将开始实现(为了简朴起见,省略了布局文件):
public class UserProfileViewModel extends ViewModel {
private String userId;
private User user;
public void init(String userId) {
this.userId = userId;
}
public User getUser() {
return user;
}
}
public class UserProfileFragment extends Fragment {
private static final String UID_KEY = “uid”;
private UserProfileViewModel viewModel;
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
String userId = getArguments().getString(UID_KEY);
viewModel = ViewModelProviders.of(this).get(UserProfileViewModel.class);
viewModel.init(userId);
}
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.user_profile, container, false);
}
}
如今,我们已经有了三个代码块,那么我们如何将它们接洽在一起呢?毕竟,当给 ViewModel 的用户字段设值后,我们需要一种方法来通知用户界面,这就是 LiveData 类的作用。
   LiveData 是一个可观察的数据持有者。它允许应用程序中的组件观察 LiveData 对象的更改,但不会在它们之间创建明确的和严格的依靠关系路径。 LiveData 还会关联 app组件(activities, fragments, services) 的生命周期状态,并做出合适的事变来防止内存走漏。
  
   注意:如果你已经在利用类似 RxJavaAgera 的库 ,则可以继续利用它们而不是LiveData。但是,当你利用它们或其他方式时,请确保正确处理生命周期,以便在相关的LifecycleOwner 停止时暂停数据流,并在销毁 LifecycleOwner 时销毁数据流。你还可以添加 android.arch.lifecycle:reactivestreams 以将 LiveData 与其他的响应流库(比方RxJava2)一起利用。
  如今我们用 LiveData 替换 UserProfileViewModel 中的 User字段,以便在数据更新时通知 Fragment。最主要的是,LiveData 是生命周期感知的,并且在不在需要时,它将主动清理引用。
public class UserProfileViewModel extends ViewModel {

private User user;
private LiveData user;
public LiveData getUser() {
return user;
}
}
如今我们修改 UserProfileFragment 以便观察数据并更新 UI。
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
viewModel.getUser().observe(this, user -> {
// update UI
});
}
每次更新用户数据时, 都会调用 onChanged 回调,并刷新UI。
如果你认识其他 可观察回调的库,你大概已经意识到,我们没有重写 fragment 的 onStop() 方法来停止观察数据。这对于 LiveData 来说是没有必要的,因为它是生命周期感知的,这意味着它不会调用回调,除非Fragment 处于 运动状态(已收到 onStart() 但未收到 onStop())。当 fragment 收到 onDestroy() 时,LiveData也将主动移除观察者 。
对于设置变化(比方,用户旋转屏幕)我们也没有做任何特殊的处理。当设置改变时,ViewModel 会主动规复,以是一旦新的 Fragment 生效,它将吸取到相同的 ViewModel实例,并且 ViewModel 的回调将立刻被当前数据调用,这就是 ViewModels 为什么不应该直接引用 Views 的原因。他们可以比 View的生命周期更长期。想了解更多信息的请检察 The lifecycle of a ViewModel 。
获取数据


如今我们已经将 ViewModel 关联到了 Fragment,但是 ViewModel 如何获取用户数据呢?在这个例子中,我们假设服务器端提供了一个 REST API。我们将利用 Retrofit 库来访问我们的服务器端,固然你可以自由利用不同的库来到达同样的目的。
下面是retrofit 的 Webservice ,负责与服务器端举行通讯:
public interface Webservice {
/**


  • @GET declares an HTTP GET request
  • @Path(“user”) annotation on the userId parameter marks it as a
  • replacement for the {user} placeholder in the @GET path
    */
    @GET(“/users/{user}”)
    Call getUser(@Path(“user”) String userId);
    }
ViewModel 的一个简朴实现是直接调用 Webservice 来获取数据并将其 赋值给 user 对象,固然如许是可行的,但是你的应用程序以后将很难维护。它赋予了 ViewModel 类太多的职责,违反了我们前面提到的关注点分离原则。别的,ViewModel 的作用域与一个 Activity 或一个 Fragment 生命周期相关联,当他们的生命周期完成时将丢失全部的数据,这是非常糟糕的用户体验。因此,我们将 ViewModel 的这个工作委托给了一个新的模块 Repository
   Repository 模块负责数据处理操纵。他们为应用的其余部门提供了一个干净的API,他们知道从何处获取数据以及在更新数据时调用哪些API。你可以将它们视为不同数据源 (长期化模型, web服务, 缓存, etc.)之间的中介。
  UserRepository 类利用 WebService 来获取用户数据项,如下:
public class UserRepository {
private Webservice webservice;
// …
public LiveData getUser(int userId) {
// This is not an optimal implementation, we’ll fix it below
final MutableLiveData data = new MutableLiveData<>();
webservice.getUser(userId).enqueue(new Callback() {
@Override
public void onResponse(Call call, Response response) {
// error case is left out for brevity
data.setValue(response.body());
}
});
return data;
}
}
固然 repository 模块看起来没有必要,但是它有一个紧张的目的,它从应用程序的其余部门提取数据源。如今我们的 ViewModel 不知道数据是从 Webservice 获取到的,这意味着我们可以根据需要,将它(Webservice)替换为其他的实现。
   注意:为了简朴起见,我们忽略了网络错误的情况。对于暴露错误和加载状态的另一个实现,请检察 附录:暴露网络状态
  管理组件之间的依靠关系:
上面的 UserRepository 类需要一个 Webservice 的实例来工作,UserRepository 可以简朴地创建Webservice ,但要做到这一点,它必须需要知道 Webservice 类的依靠关系来构造它,这会使代码显着和成倍的复杂化(比方,每个需要 Webservice实例的类 都需要知道如何用它的依靠来构造它)。别的,UserRepository 大概不是唯一需要 Webservice 的类。如果每个类创建一个新的 WebService,这将是非常沉重的资源。
如今我们有两种模式可以用来解决这个问题:


  • 依靠注入:依靠注入允许类在不构造它们的情况下定义它们的依靠关系。在运行时,另一个类负责提供这些依靠关系。我们保举 Google 的 Dagger 2 库,在Android应用中实现依靠注入。Dagger 2 通过遍历依靠关系树来主动构造对象,并为依靠关系提供编译时间保证。
  • 服务定位器:服务定位器提供了一个注册表,这个类可以得到它们的依靠 而不是 构建它们。实现起来比依靠注入(DI)更轻易,以是如果你不认识DI,可以利用 Service Locator。
这些模式允许您扩展代码,因为它们提供了用于管理依靠关系的清晰模式,无需复制代码或增加复杂性。这两个模式也允许互换实现测试, 这是利用它们的主要好处之一。
在这个例子中,我们将利用 依靠注入 来管理依靠关系。
关联ViewModel和repository


如今我们修改 UserProfileViewModel 利用的 repository。
public class UserProfileViewModel extends ViewModel {
private LiveData user;
private UserRepository userRepo;
@Inject // UserRepository parameter is provided by Dagger 2
public UserProfileViewModel(UserRepository userRepo) {
this.userRepo = userRepo;
}
public void init(String userId) {
if (this.user != null) {
// ViewModel is created per Fragment so
// we know the userId won’t change
return;
}
user = userRepo.getUser(userId);
}
public LiveData getUser() {
return this.user;
}
}
缓存数据


上面的 repository 实现 对抽象调用 Web服务是有好处的,但是因为它只依靠于一个数据源,以是它不是很有用。
UserRepository 实现的问题是,在获取数据之后,它不生存在任何地方。如果用户脱离 UserProfileFragment 并返回,应用程序将重新获取数据。这是欠好的,原因有两个:浪费名贵的网络带宽并强制用户等待新的查询完成。为了解决这个问题,我们将添加一个新的数据源到 UserRepository ,这个数据源可以将 User 对象 缓存 到内存中。
@Singleton // informs Dagger that this class should be constructed once
public class UserRepository {
private Webservice webservice;
// simple in memory cache, details omitted for brevity
private UserCache userCache;
public LiveData getUser(String userId) {
LiveData cached = userCache.get(userId);
if (cached != null) {
return cached;
}
final MutableLiveData data = new MutableLiveData<>();
userCache.put(userId, data);
// this is still suboptimal but better than before.
// a complete implementation must also handle the error cases.
webservice.getUser(userId).enqueue(new Callback() {
@Override
public void onResponse(Call call, Response response) {
data.setValue(response.body());
}
});
return data;
}
}
长期化数据


在我们当前的实现中,如果用户旋转屏幕或脱离并返回到应用,现有UI将立刻可见,因为 repository 从内存中检索缓存的数据。但是,如果用户脱离应用程序并且数小时后回来,或Android 系统杀死该进程后,会发生什么?
在目前的实现中,我们将需要从网络上重新获取数据。这不仅是一个糟糕的用户体验,而且会浪费,因为它会利用移动数据重新获取相同的数据。你可以简朴地通过缓存Web请求来解决这个问题,但是这会产生新的问题。如果相同的用户数据从另一种范例的请求中显示出来(比方,获取朋友列表),会发生什么情况?那么你的应用程序大概会显示不同等的数据,这是一个紊乱的用户体验。比方,由于好友列表请求和用户请求可以在不同的时间执行,以是相同用户的数据大概会以不同的方式显示。您的应用需要归并它们以制止显示不同等的数据。
处理这个问题的正确方法是利用 长期化模型。这就是 Room 长期化库可以拯救的地方。
   Room 是一个对象映射库,利用最小的样板代码来提供当地数据长期化。在编译时,它会根据模式验证每个查询,因此,有问题的SQL查询会导致编译时出错,而不是运行时失败。Room 抽象了处理原始SQL表和查询的一些底层实现细节。它还允许观察对数据库数据(包括聚集和 join 查询)的更改,通过 LiveData对象 公开这些更改 。别的,它明确定义了解决常见问题的线程束缚,比方在主线程上的访问存储。
  
   注意:如果你的应用程序已经利用另一个长期化解决方案(如SQLite对象关系映射(ORM)),则不需要利用 Room 替换现有的解决方案。但是,如果您正在编写新的应用程序或重构现有的应用程序,我们建议利用 Room 来生存应用程序的数据。如许,您可以利用库的抽象和查询 验证功能。
  要利用 Room,我们需要定义我们的当地模式。起首,利用 @Entity 注解 User 类 以将其标志为数据库中的表。
@Entity
class User {
@PrimaryKey
private int id;
private String name;
private String lastName;
// getters and setters for fields
}
然后,为你的app创建一个数据库类继续于 RoomDatabase
@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
}
注意 MyDatabase 是抽象的。Room 主动提供一个 它的实现。有关详细信息,请检察 Room 文档
如今我们需要一种将用户数据插入数据库的方法。为此,我们将创建一个数据访问对象(DAO
@Dao
public interface UserDao {
@Insert(onConflict = REPLACE)
void save(User user);
@Query(“SELECT * FROM user WHERE id = :userId”)
LiveData load(String userId);
}
然后,从我们的数据库类中引用 DAO (Data Access Object)
@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
请注意,该 load 方法返回一个 LiveData。Room 知道数据库何时被修改,当数据改变时它会主动通知全部运动的的察者。因为它利用的是 LiveData,以是这将是有效的,因为只有至少有一个运动的观察者才会更新数据。
   注意:Room 根据 table 的修改来检查失效,这意味着它大概发送误报的通知。
  如今我们可以修改我们的 UserRepository 来包罗 Room 数据源。
@Singleton
public class UserRepository {
private final Webservice webservice;
private final UserDao userDao;
private final Executor executor;
@Inject
public UserRepository(Webservice webservice, UserDao userDao, Executor executor) {
this.webservice = webservice;
this.userDao = userDao;
this.executor = executor;
}
public LiveData getUser(String userId) {
refreshUser(userId);
// return a LiveData directly from the database.
return userDao.load(userId);
}
private void refreshUser(final String userId) {
executor.execute(() -> {
// running in a background thread
// check if user was fetched recently
boolean userExists = userDao.hasUser(FRESH_TIMEOUT);
if (!userExists) {
// refresh the data
Response response = webservice.getUser(userId).execute();
// TODO check for error etc.
// Update the database.The LiveData will automatically refresh so
// we don’t need to do anything else here besides updating the database
userDao.save(response.body());
}
});
}
}
请注意,只管我们改变了 来自于 UserRepository 的数据,我们并不需要改变我们 UserProfileViewModel或 UserProfileFragment。这是抽象提供的机动性。这对于测试来说有好处的,因为你可以在测试你的UserProfileViewModel 的时候提供一个假的 UserRepository 。
如今我们的代码是完整了。如果用户以后回到相同的用户界面,他们会立刻看到用户信息,因为我们长期化了。同时,如果数据过期了,我们的仓库将在后台更新数据。固然,根据您的利用情况,如果数据太旧,您大概不盼望显示长期化数据。
在一些利用情况下,如 下拉刷新,UI 显示用户是否正在举行网络操纵是非常紧张的。将UI 操纵与现实数据分开是一个很好的做法,因为它大概因各种原因而导致更新(比方,如果我们获取朋友列表,同一用户大概会再次触发 LiveData 更新)。站在UI 的角度,究竟上,当有一个请求执行的时候,另一个数据点,类似于任何其他的数据 (比如 User 对象)。
这个用例有两种常见的解决方案:


  • 更改 getUser 为返回包罗网络操纵状态的 LiveData 。附录中提供了一个示例实现:公开网络状态部门。
  • 在 repository 类中提供另一个可以返回用户刷新状态的公共函数。如果只想响应显式的用户操纵(如下拉刷新)来显示网络状态,则此选项更好。
单一的真相来源:
不同的 REST API 端点通常返回相同的数据。比方,如果我们的服务器端拥有另一个返回 朋友列表的端点,则同一个用户对象大概来自两个不同的API 端点,大概粒度不同。如果 UserRepository 从 Webservice请求返回原本的响应,我们的UI大概会显示不同等的数据,因为在这些请求过程中数据大概已经在服务器端发生了改变。这就是为什么在 UserRepository 实现中,Web服务回调只是将数据生存到数据库中。然后,对数据库的更改将触发回调给 运动的 LiveData对象
在这个模型中,数据库充当了 单一的真相来源,应用程序的其他部门通过 Repository 访问它。无论你是否利用磁盘缓存,我们都建议将你的 Repository 指定为应用程序其余部门唯一的真相来源。
测试


我们已经提到分离的好处之一就是可测试性,让我们看看如何测试每个代码模块。


  • 用户界面和交互:你唯一需要花费时间的是 Android UI Instrumentation 。测试UI 代码的最好方法是创建一个 Espresso测试。您可以创建 Fragment 并为其提供一个模拟的ViewModel。由于该 Fragment 只与 ViewModel 接洽,以是伪造它足以完全测试这个UI。
  • ViewModel:ViewModel 可以利用 JUnit 来测试 。你只需要模拟 UserRepository 来测试它。
  • UserRepository:你同样也可以利用 JUnit 来测试 UserRepository。你需要模拟 Webservice 和 DAO。你可以测试它是否做出了正确的Web服务调用,并将效果生存到数据库中,如果数据已缓存且最新,则不会发出任何不必要的请求。因为 Webservice 和 UserDao 都是接口,你可以模拟它们,或者为更复杂的测试用例创建伪造的实现…
  • UserDao:测试 DAO 类的保举方法是利用 instrumentation 测试。由于这些 instrumentation 测试不需要任何用户界面,他们将会运行得很快。对于每个测试,您可以创建一个处于内存中的数据库,以确保测试没有任何副作用(如更改磁盘上的数据库文件)。
    Room 也允许指定命据库的实现,以是你可以通过提供 JUnit 来测试 SupportSQLiteOpenHelper 的实现。通常不建议利用这种方法,因为设备上运行的SQLite版本大概与主机上的SQLite版本不同。
  • Webservice:使测试独立于外界是很紧张的,以是你的 Webservice 测试也应该制止对后端举行网络调用。有很多库可以资助你,比方, MockWebServer 是一个强大的库,可以资助你为测试创建一个伪造的当地服务器。
    自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到如今。
深知大多数初中级Android工程师,想要提升技能,往往是本身探索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。本身不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此网络整理了一份《2024年Android移动开辟全套学习资料》,初衷也很简朴,就是盼望能够资助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。





既有得当小白学习的零基础资料,也有得当3年以上经验的小同伴深入学习提升的进阶课程,基本涵盖了95%以上Android开辟知识点,真正体系化!
由于文件比较大,这里只是将部门目次截图出来,每个节点内里都包罗大厂面经、学习条记、源码课本、实战项目、解说视频,并且会持续更新!
如果你以为这些内容对你有资助,可以扫码获取!!(备注:Android)
末了看一下学习需要的全部知识点的思维导图。在刚刚那份学习条记里包罗了下面知识点全部内容!文章里已经展示了部门!如果你正愁这块不知道如何学习或者想提升学习这块知识的学习效率,那么这份学习条记绝对是你的秘密武器!

《互联网大厂面试真题剖析、进阶开辟核心学习条记、全套解说视频、实战项目源码课本》点击传送门即可获取!
识点,真正体系化!**
由于文件比较大,这里只是将部门目次截图出来,每个节点内里都包罗大厂面经、学习条记、源码课本、实战项目、解说视频,并且会持续更新!
如果你以为这些内容对你有资助,可以扫码获取!!(备注:Android)
末了看一下学习需要的全部知识点的思维导图。在刚刚那份学习条记里包罗了下面知识点全部内容!文章里已经展示了部门!如果你正愁这块不知道如何学习或者想提升学习这块知识的学习效率,那么这份学习条记绝对是你的秘密武器!
[外链图片转存中…(img-9xD3At3D-1712650210719)]
《互联网大厂面试真题剖析、进阶开辟核心学习条记、全套解说视频、实战项目源码课本》点击传送门即可获取!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

金歌

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表