自动化测试麻烦吗?说实在,麻烦!有一定的学习成本。但是,自动化测试有以下优点:

  • 节省时间:可以指定测试某一个activity,不需要一个个自己点
  • 单元测试:既然Java可以进行单元测试,Android为什么就不可以呢?
  • 一键适配:不解释

Android自动化测试框架主要有:Espresso、UI Automator以及Robolectric。滴滴~~ 开车开车!

1、Java单元测试

Android studio(以下简称as)可以跑纯Java代码,这个想必大家都知道。这里就简单介绍一下as如何跑Java代码,作为热身运动吧!
首先打开测试包,在app->src->test目录下,如下图所示,其中AndroidTest包是针对Android工作的测试,先不管他。

这里as为我们创建了一个测试类,直接打开,as中采用Junit4的测试包,主要代码如下。

@RunWith(JUnit4.class)
public class ExampleUnitTest {@Beforepublic void before(){//在测试前的工作}@Afterpublic void after(){// 测试完成后的工作}@Testpublic void addition_isCorrect() {//主要工作}
}

这就是最简单的Java测试,预热完毕,接下来进入本文的主角

2、Android单元测试——Espresso

  AndroidJUnitRunner类是一个JUnit测试运行器,它允许您在Android设备上运行JUnit 3或JUnit 4样式测试类,包括使用Espresso和UI Automator测试框架的测试类。
  测试运行器与您的JUnit 3和JUnit 4(高达JUnit 4.10)测试兼容。 但是,您应该避免将JUnit 3和JUnit 4测试代码混合在同一个包中,因为这可能会导致意外的结果。 如果您正在创建一个测试JUnit 4测试类以在设备或模拟器上运行,那么您的测试类必须以@RunWith(AndroidJUnit4.class)注释为前缀。

先看app下的build.gradle依赖:

dependencies {androidTestCompile 'com.android.support:support-annotations:25.4.0'androidTestCompile 'com.android.support.test:runner:1.0.0' androidTestCompile 'com.android.support.test:rules:1.0.0' androidTestCompile 'com.android.support.test.espresso:espresso-core:3.0.2'
}
android {defaultConfig {testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"}
}

如果依赖冲突,请加入以下代码:

androidTestImplementation('com.android.support.test.espresso:espresso-core:3.0.2', {exclude group: 'com.android.support', module: 'support-annotations'
})

2.1获取application

最常见的应用案例就是,在进行网络测试的时候,如果您的项目很大,编译的时间很长,那么单单为看一个请求结果就要花费相当长的时间,这是不能容忍的,我们可以通过Android的单元测试来模拟请求,如下代码所示。

@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {@Beforepublic void init() {Context appContext = InstrumentationRegistry.getTargetContext();x.Ext.init((Application) appContext.getApplicationContext());}@Testpublic void useAppContext() {// Context of the app under test.RequestParams requestParams = new RequestParams("https://www.baidu.com/");String str = x.http().getSync(requestParams, String.class);System.out.println("\n"+str+"\n");}
}

通过InstrumentationRegistry,我们就可以获取到context对象,再通过context就可以获取application对象,之后就可以构建一个网络请求,请注意,在测试方法中,必须使用同步请求,否则测试用例会直接忽略回调方法,直接结束程序,导致无法获取到请求结果。

Android单元测试分为:小型测试、中型测试,大型测试,他们的区别如下。

  • 小型测试(SmallTest):与系统隔离运行,执行时间较短,最长执行时间为200ms
  • 中型测试(MediumTest):集成了多个组件,并可以在模拟器或者真机上运行,最长执行时间为1000ms
  • 大型测试(LargeTest):可以运行UI流程的测试工作,确保APP按照预期在仿真器或实际设备上工作。最长执行时间为1000ms

在Android单元测试中可以使用断言来判断变量值是否符合预期,常用的有assertThat、assertEquals、assertNotSame等

2.2获取对应组件

该框架提供ActivityTestRule来管理被测试的activity,例如MainActivity对应的布局文件如下

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".MainActivity"><EditTextandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:text=""android:id="@+id/main_text"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintLeft_toLeftOf="parent"app:layout_constraintRight_toRightOf="parent"app:layout_constraintTop_toTopOf="parent" /></android.support.constraint.ConstraintLayout>

MainActivity的代码这里就不贴出来了,直接看测试代码:

@RunWith(AndroidJUnit4.class)
@LargeTest
public class MainTest {@Rulepublic ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<>(MainActivity.class);@Testpublic void Run() {onView(withId(R.id.main_text)).perform(typeText("Hello MainActivity!"), closeSoftKeyboard());}
}

这里简单说明一下:

  • withId(R.id.main_text):通过ID找到对应的组件,并将其封装成一个Matcher
  • onView():将窗口焦点给某个组件,并返回ViewInteraction实例
  • perform():该组件需要执行的任务,传入ViewAction的实例,可以有多个,意味着用户的多种操作
  • typeText():输入字符串任务,还有replaceText方法也可以实现类似的效果,不过没有输入动画
  • closeSoftKeyboard():关闭软键盘

以上就是最基本的自动化测试代码。点击Run方法边上的运行按钮,直接运行在设备上即可,效果如下所示。

类似的还有点击事件:

onView(withId(R.id.main_text)).perform(click());

双击事件:

onView(withId(R.id.main_text)).perform(doubleClick());

判断是否符合预期

onView(withId(R.id.main_text)).check(matches(withText("Hello MainActivity!")));

更多请看ViewActions类提供的API

2.3模拟listView的点击事件

以上是针对唯一ID的事件,那么如果有多个组件的ID是一样的呢?例如模拟 listView的item点击事件,是如何区分每一个item呢?先看如何处理多个组件ID相同的情况。
大家知道可以通过ID来查找对应的视图,这里也可以通过显示的文本来查找视图:

onView(withText("Hello MainActivity!"));

那么,如果通过ID和显示的文本不就可以定位唯一的视图了吗?如下

onView(allOf(withId(R.id.main_text), withText("Hello MainActivity!")));

或者这样来筛选不匹配的视图

onView(allOf(withId(R.id.button_signin), not(withText("Sign-out"))));

更多请看ViewMatchers提供的API

接下来看如何模拟listview(GridView和Spinner均适用)的点击事件

我们先创建一个SecondActivity

public class ListActivity extends AppCompatActivity {private ListView listView ;private List<HashMap<String ,String>> data = new ArrayList<>();public static final String KEY =  "key";@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_second);listView  =  findViewById(R.id.list_view);initDate();listView.setAdapter(new SimpleAdapter(this,data,R.layout.item_list,new String[]{KEY},new int[]{R.id.item_list_text}));listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {@Overridepublic void onItemClick(AdapterView<?> parent, View view, int position, long id) {Toast.makeText(ListActivity.this,data.get(position).get(KEY),Toast.LENGTH_LONG).show();}});}private void initDate() {for(int i =0 ;i < 90 ;i++){HashMap<String,String> map = new HashMap<>();map.put(KEY,"第"+(1+i)+"列");data.add(map);}}
}

  对应的布局文件就是一个listView,item对应的布局是一个textView,这里就不贴出来了,主要看测试类:

@RunWith(AndroidJUnit4.class)
@LargeTest
public class ListViewTest {private static final String TAG = "ListViewTest ";@Rulepublic ActivityTestRule<ListActivity> mActivityRule = new ActivityTestRule<>(ListActivity.class);@Beforepublic void init() {mActivityRule.getActivity();}@Testpublic void Run() {onData(allOf(is(instanceOf(Map.class)),hasEntry(equalTo(ListActivity.KEY), is("第10列")))).perform(click());}
}

  这里选择数据为第10行的item,并执行点击动作,这里着重讲一下hasEntry() 这个方法,该方法需要传两个Matcher,也就是map的键名和对应的值。通过map的键、值来唯一确定一个item,拿到对应的item就可以类似于视图一样去执行动作了,效果如下。

  动画比较快,但是可以看到listview先是滚到第10行,然后才执行点击事件,这是因为Espresso负责滚动目标元素,并将元素放在焦点上。

  有同学马上就提出了,recycleView才是主流,用listview的很少了~~,没事,我们来看如何进行recycleView的自动化测试

2.4模拟recycleView点击事件

对recyclerView进行自动化测试需要再添加以下依赖,注意,是在之前的依赖基础上添加以下代码。

androidTestCompile 'com.android.support.test.espresso:espresso-contrib:3.0.0'
androidTestCompile 'com.android.support:recyclerview-v7:25.4.0'

我们创建一个RecyclerActivity,内容如下:

public class RecyclerActivity extends AppCompatActivity {private RecyclerView recyclerView;private RecyclerAdapter<String> adapter;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_recycler);recyclerView = findViewById(R.id.recycler_view);recyclerView.setLayoutManager(new LinearLayoutManager(this));adapter = new RecyclerAdapter<>(this, R.layout.item_list);recyclerView.setAdapter(adapter);List<String> list = new ArrayList<>();for(int i =0 ;i < 50 ;i++){list.add("第"+(1+i)+"列");}adapter.setData(list);}
}

  对应的布局文件就是一个recyclerview,item的布局只有一个textView,这里也就不贴出来了,adapter也很简单,给textView一个点击事件,如下:

public class RecyclerAdapter<T> extends RecyclerView.Adapter<RecyclerView.ViewHolder> {private List<T> data = new ArrayList<>();private Context context ;private int layout;public RecyclerAdapter(Context context, int layout) {this.context = context;this.layout = layout;}public void setData(List<T> data) {this.data.clear();this.data.addAll(data);notifyDataSetChanged();}@Overridepublic RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {return new Holder(LayoutInflater.from(context).inflate(layout,null,false));}@Overridepublic void onBindViewHolder(RecyclerView.ViewHolder holder, final int position) {Holder holder1 = (Holder) holder;holder1.textView.setText(data.get(position).toString());holder1.itemView.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {Toast.makeText(context,data.get(position).toString(),Toast.LENGTH_LONG).show();}});}@Overridepublic int getItemCount() {return data.size();}private class Holder extends RecyclerView.ViewHolder{TextView textView ;public Holder(View itemView) {super(itemView);textView = itemView.findViewById(R.id.item_list_text);}}
}

接下来看测试类:

@RunWith(AndroidJUnit4.class)
@LargeTest
public class RecycleViewTest {private static final String TAG = "ExampleInstrumentedTest";@Rulepublic ActivityTestRule<RecyclerActivity> mActivityRule = new ActivityTestRule<>(RecyclerActivity.class);@Testpublic void Run() {onView(ViewMatchers.withId(R.id.recycler_view)).perform(RecyclerViewActions.actionOnItemAtPosition(10, click()));}
}

  在run方法中我们可以看到基本与之前的类似,不同的是需要通过RecyclerViewActions类提供的API来执行任务,其中actionOnItemAtPosition的第一个参数是recycleview的item位置,第二个参数是对应的动作,效果与listView的一致,这里就不贴了。
  这里可以看出,recycleview的测试类要优于listView,listView通过item的值来查找对应的item,而recycleview直接通过位置来查找

2.5 模拟用户点击actionbar

新建一个MenuActivity,主要代码如下

public class MenuActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_menu);}@Overridepublic boolean onCreateOptionsMenu(Menu menu) {getMenuInflater().inflate(R.menu.menu_test, menu);return super.onCreateOptionsMenu(menu);}@Overridepublic boolean onOptionsItemSelected(MenuItem item) {Toast.makeText(this,item.getTitle(),Toast.LENGTH_SHORT).show();return super.onOptionsItemSelected(item);}
}

menu布局代码如下:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"xmlns:android1="http://schemas.android.com/apk/res-auto"><itemandroid:id="@+id/nav_1"android:title="item1"android1:showAsAction="never" /><itemandroid:id="@+id/nav_2"android:title="item2"android1:showAsAction="never" />
</menu>

测试代码如下:

@RunWith(AndroidJUnit4.class)
@LargeTest
public class MenuTest {@Rulepublic ActivityTestRule<MenuActivity> mActivityRule = new ActivityTestRule<>(MenuActivity.class);@Testpublic void test(){//打开menuopenContextualActionModeOverflowMenu();//模拟点击item2onView(withText("item2")).perform(click());}
}

效果如下:

如果您在测试中遇到MainLooper还没有调用prepare异常

java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()
at android.os.Handler.<init>(Handler.java:200)
at android.os.Handler.<init>(Handler.java:114)
at android.widget.Toast$TN.<init>(Toast.java:344)
at android.widget.Toast.<init>(Toast.java:100)

可以加上以下代码解决该问题

    @Testpublic void test() {getInstrumentation().runOnMainSync(new Runnable() {@Overridepublic void run() {//test content}});}

3、Android单元测试——Robolectric

  如果您的应用的测试环境需要单元测试与Android框架进行更广泛的交互,则可以使用Robolectric。 该工具可让您在工作站上或常规JVM中的持续集成环境中运行测试,而无需仿真器,几乎与Android设备运行测试的完全保真度相匹配,但仍比执行设备测试更快,支持Android平台的以下几个方面。

  • Android4.1以及更高
  • Android Gradle 插件2.4以及更高
  • 组件生命周期
  • 事件循环
  • 所有资源:SDK, Resources, Native Method

grade配置:

testImplementation "org.robolectric:robolectric:3.8"
testImplementation "org.assertj:assertj-core:1.7.0"
// robolectric对应的support-v4包
testImplementation 'org.robolectric:shadows-support-v4:3.0'android {testOptions {unitTests {includeAndroidResources = true}}
}

基本用法如下所示。

@RunWith(RobolectricTestRunner.class)
public class MyActivityTest {@Testpublic void clickingButton_shouldChangeResultsViewText() throws Exception {MyActivity activity = Robolectric.setupActivity(MyActivity.class);Button button = (Button) activity.findViewById(R.id.button);TextView results = (TextView) activity.findViewById(R.id.results);button.performClick();assertThat(results.getText().toString()).isEqualTo("Robolectric Rocks!");}
}

Robolectric社区已经有详细的说明,这里就不再赘述

如有疑问,可以参考文末的demo,需要注意的是Robolectric的相关测试是在test目录下,可以mock出Android环境。

4、Android测试——UI Automator

先配置依赖

dependencies {androidTestCompile 'com.android.support:support-annotations:25.4.0'androidTestCompile 'com.android.support.test:runner:1.0.0' androidTestImplementation 'com.android.support.test.uiautomator:uiautomator-v18:2.1.3'androidTestCompile 'org.hamcrest:hamcrest-integration:1.3'}

注意,UI Automator最低支持Android 4.3 (API level 18)

在MainActivity中有四个组件editText、textView和button,布局就不贴出来了,在MainActivity的Java代码中主要是点击方法中,如下:

  @Overridepublic void onClick(View view) {// Get the text from the EditText view.final String text = mEditText.getText().toString();final int changeTextBtId = R.id.changeTextBt;final int activityChangeTextBtnId = R.id.activityChangeTextBtn;if (view.getId() == changeTextBtId) {//将edit中的text内容显示到textView中mTextView.setText(text);} else if (view.getId() == activityChangeTextBtnId) {//启动新的activity,并将text传给新的activity显示Intent intent = ShowTextActivity.newStartIntent(this, text);startActivity(intent);}}

主要看测试代码,这里创建一个ChangeTextBehaviorTest测试类:

@RunWith(AndroidJUnit4.class)
@SdkSuppress(minSdkVersion = 18)
public class ChangeTextBehaviorTest {private static final String BASIC_SAMPLE_PACKAGE= "com.example.android.testing.uiautomator.BasicSample";private static final int LAUNCH_TIMEOUT = 5000;private static final String STRING_TO_BE_TYPED = "UiAutomator";private UiDevice mDevice;@Beforepublic void startMainActivityFromHomeScreen() {// 获取UiDevice的实例mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());// 模拟用户点击home键mDevice.pressHome();//获取要加载的包名final String launcherPackage = getLauncherPackageName();//判断是否为空assertThat(launcherPackage, notNullValue());//等待目标包 的信息mDevice.wait(Until.hasObject(By.pkg(launcherPackage).depth(0)), LAUNCH_TIMEOUT);// 启动目标activity,也就是MainActivityContext context = InstrumentationRegistry.getContext();final Intent intent = context.getPackageManager().getLaunchIntentForPackage(BASIC_SAMPLE_PACKAGE);intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);    // Clear out any previous instancescontext.startActivity(intent);// Wait for the app to appearmDevice.wait(Until.hasObject(By.pkg(BASIC_SAMPLE_PACKAGE).depth(0)), LAUNCH_TIMEOUT);}@Testpublic void testChangeText_sameActivity() {//将  STRING_TO_BE_TYPED 内容填充到edittext中mDevice.findObject(By.res(BASIC_SAMPLE_PACKAGE, "editTextUserInput")).setText(STRING_TO_BE_TYPED);//给ID为changeTextBt  的组件模拟用户的点击事件mDevice.findObject(By.res(BASIC_SAMPLE_PACKAGE, "changeTextBt")).click();// 等待获取MainActivity中ID为textToBeChanged的textView的内容,等待时间为500msUiObject2 changedText = mDevice.wait(Until.findObject(By.res(BASIC_SAMPLE_PACKAGE, "textToBeChanged")),500 /* wait 500ms */);//判断是否正确assertThat(changedText.getText(), is(equalTo(STRING_TO_BE_TYPED)));}@Testpublic void testChangeText_newActivity() {// 同上mDevice.findObject(By.res(BASIC_SAMPLE_PACKAGE, "editTextUserInput")).setText(STRING_TO_BE_TYPED);mDevice.findObject(By.res(BASIC_SAMPLE_PACKAGE, "activityChangeTextBtn")).click();// Verify the test is displayed in the UiUiObject2 changedText = mDevice.wait(Until.findObject(By.res(BASIC_SAMPLE_PACKAGE, "show_text_view")),500 /* wait 500ms */);assertThat(changedText.getText(), is(equalTo(STRING_TO_BE_TYPED)));}/*** 获取包名*/private String getLauncherPackageName() {// Create launcher Intentfinal Intent intent = new Intent(Intent.ACTION_MAIN);intent.addCategory(Intent.CATEGORY_HOME);// Use PackageManager to get the launcher package namePackageManager pm = InstrumentationRegistry.getContext().getPackageManager();ResolveInfo resolveInfo = pm.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY);return resolveInfo.activityInfo.packageName;}
}

该框架的逻辑是模拟用户在使用APP的过程,这个测试用例的主要流程是:用户在桌面点击目标APP,进去,输入字符串,用户点击activityChangeTextBtn组件,跳转到ShowTextActivity,并传入内容,让其显示出来。然后点击changeTextBt组件,显示用户输入内容;
效果如下

该测试类有三个方法,其中在测试前需要获取 UiDevice的实例,步骤如下:

  • 通过调用getInstance()方法并将Instrumentation对象作为参数传递,获取UiDevice对象以访问要测试的设备。
  • 通过调用UiDevice实例的findObject()方法,获取UiObject对象以访问设备上显示的UI组件(例如,前景中的当前视图)。
  • 可以通过调用UiObject方法模拟要在该UI组件上执行的特定用户交互;例如,调用performMultiPointerGesture()来模拟多点触摸手势,调用setText()来编辑文本字段。
  • 在执行这些用户交互之后,检查UI是否反映了预期的状态或行为。

显然该框架需要从MainActivity开始,整个的模拟用户使用过程,好处是不会绑定特定的activity,资源具有全局性。源码见GitHub

当然,也可以通过以下的方式拿到对应的组件:

UiObject okButton = mDevice.findObject(new UiSelector().text("OK").className("android.widget.Button"));// Simulate a user-click on the OK button, if found.
if(okButton.exists() && okButton.isEnabled()) {okButton.click();
}

如果要访问应用程序中的特定UI组件,请使用UiSelector类。 此类表示当前显示的UI中特定元素的查询。
如果找到多个匹配元素,则布局层次结构中的第一个匹配元素将作为目标UiObject返回。 构建UiSelector时,可以将多个属性链接在一起以优化搜索。 如果未找到匹配的UI元素,则抛出UiAutomatorObjectNotFoundException。
我们可以使用childSelector()方法嵌套多个UiSelector实例。 例如,以下代码示例显示了测试如何指定搜索以在当前显示的UI中查找第一个ListView,然后在该ListView中搜索以查找具有文本属性Apps的UI元素

UiObject appItem = new UiObject(new UiSelector().className("android.widget.ListView").instance(0).childSelector(new UiSelector().text("Apps")));

一旦您的测试获得了UiObject对象,您就可以调用UiObject类中的方法来对该对象所表示的UI组件执行用户交互。您可以指定以下操作:

  • click():单击UI元素可见边界的中心。
  • dragTo():将此对象拖动到任意坐标。
  • setText():在清除字段内容后,在可编辑字段中设置文本。相反,clearTextField()方法清除可编辑字段中的现有文本。
  • swipeUp():对UiObject执行向上滑动操作。类似地,swipeDown(),swipeLeft()和swipeRight()方法执行相应的操作。

如果测试FrameLayout内容,则需要构建UiCollection,例如以下代码:

UiCollection videos = new UiCollection(new UiSelector().className("android.widget.FrameLayout"));// 检索此集合中的视频数量
int count = videos.getChildCount(new UiSelector().className("android.widget.LinearLayout"));// 查找特定视频并模拟用户单击它
UiObject video = videos.getChildByText(new UiSelector().className("android.widget.LinearLayout"), "Cute Baby Laughing");
video.click();// 模拟选择与视频关联的复选框
UiObject checkBox = video.getChild(new UiSelector().className("android.widget.Checkbox"));
if(!checkBox.isSelected()) checkbox.click();

对于可滑动视图,可以使用UiScrollable类模拟显示屏上的垂直或水平滚动。 当UI元素位于屏幕外并且您需要滚动以将其置于视图中时,此技术很有用。
以下代码段显示了如何模拟向下滚动“设置”菜单并单击“关于”平板电脑选项

UiScrollable settingsItem = new UiScrollable(new UiSelector().className("android.widget.ListView"));
UiObject about = settingsItem.getChildByText(new UiSelector().className("android.widget.LinearLayout"), "About tablet");
about.click();

5、总结

话不多说,~~附上 代码链接

Android单元测试全解相关推荐

  1. PowerMockerJacoco单元测试全解

    PowerMocker&Jacoco单元测试全解 第一章 pom文件的引入与各种坑 1. pom文件 <!-- 测试包 --> <dependency><grou ...

  2. Android动画全解

    Android动画全解 在Android开发中经常会碰到动画,看到别的应用有很酷炫的应用时,总是想怎么去实现,但是每次都是发现感觉是知道怎么做的,实际做起来还是无从下手的感觉,究其原因还是 Andro ...

  3. android 10文件权限 三星,三星Android系统文件夹全解

    该楼层疑似违规已被系统折叠 隐藏此楼查看此楼 三星android系统的开放,使其用户可以自己查看系统和SD卡中的文件夹.就系统和SD卡中常见的目录代表什么意思,下面是一个较实用的总结: 一.SD卡中 ...

  4. android 三星存储位置,三星Android系统文件夹全解

    该楼层疑似违规已被系统折叠 隐藏此楼查看此楼 三星android系统的开放,使其用户可以自己查看系统和SD卡中的文件夹.就系统和SD卡中常见的目录代表什么意思,下面是一个较实用的总结: 一.SD卡中 ...

  5. 矿点三星android版本,三星Android系统文件夹全解

    该楼层疑似违规已被系统折叠 隐藏此楼查看此楼 三星android系统的开放,使其用户可以自己查看系统和SD卡中的文件夹.就系统和SD卡中常见的目录代表什么意思,下面是一个较实用的总结: 一.SD卡中 ...

  6. Android 混淆全解

    开启混淆 位置:通常在app目录下的build.gradle 把 minifyEnabled设置为true即可 如下: release{minifyEnabled true//是否启动混淆 ture: ...

  7. 三星android是啥意思,三星Android系统文件夹全解

    该楼层疑似违规已被系统折叠 隐藏此楼查看此楼 三星android系统的开放,使其用户可以自己查看系统和SD卡中的文件夹.就系统和SD卡中常见的目录代表什么意思,下面是一个较实用的总结: 一.SD卡中 ...

  8. 三星里的Android是什么,三星Android系统文件夹全解

    该楼层疑似违规已被系统折叠 隐藏此楼查看此楼 三星android系统的开放,使其用户可以自己查看系统和SD卡中的文件夹.就系统和SD卡中常见的目录代表什么意思,下面是一个较实用的总结: 一.SD卡中 ...

  9. android系统手机流量控制方法amp;,基于Android的手机流量控制软件全解.doc

    基于Android的手机流量控制软件全解 基于Android的手机流量控制软件 设计与实现 1 绪论 1.1 手机移动互联网进展 这些年,移动通信和互联网成为当今世界发展最快.市场潜力最大.前景最诱人 ...

最新文章

  1. c实现面向对象编程(3)
  2. 你知道这些 985、211 院校的隶属吗?
  3. AOP配置开发入门案例
  4. AIX和linux SSH互信
  5. c语言的多线程,如何用C语言实现多线程
  6. awk、变量、运算符、if多分支
  7. mysql主主互备架构
  8. html读取servlet,简单html与servlet交互(HTML利用servlet读取txt)
  9. 【图像处理】彩色图像自适应对比度增强(OpenCV实现)
  10. linux 软件部署工具下载,linux配置 yum管理应用软件 、 快速部署Web/FTP
  11. 使用tf.data.Dataset.from_tensor_slices五步加载数据集
  12. 【菜鸡的LeetCode答案】【C#】7.反转整数
  13. UNI-APP 小程序生成海报
  14. DS-5 Streamline
  15. pyside6的MQTT客户端
  16. 中小型企业网络IP地址规划案例-1
  17. 人工智能——机器学习是什么
  18. mysql保存为vna文件_微机 模拟试题三(含答案)
  19. 微信视频号100w播放收入1000+的变现方式
  20. rails开发利器:视频播放插件plugin(如何开发一个简单的插件)

热门文章

  1. php file_get_contents 临时目录 fopen,php fopen()和file_get_contents() 区别介绍
  2. pythonwhile列表_Python编程:while循环处理列表和字典
  3. linux ip add address,linux – ip地址范围参数
  4. linux6用户t密码,linux系统 用户和组管理类命令的使用方法
  5. android挂载usb设备,android usb挂载分析---MountService启动
  6. vue在created调用点击方法_vue中created、mounted等方法整理
  7. 查找重复文件_重复文件快速查找删除
  8. 数据查询分页 获取总数时间太长_干货:SQL Server 查询语句
  9. 阿里JAVA面试题剖析:一般实现分布式锁都有哪些方式?使用 Redis 如何设计分布式锁?...
  10. Firefox 将导入 Windows 根证书,避免与杀毒软件的冲突