您的位置 首页 > 数码极客

Oracle中如何去掉自动换行

背景

Android自动化包含自动化测试和第三方App的自动化运行,这里的自动化测试主要指的是纯粹的黑盒测试,即在完全不了解代码逻辑的情况下编写的测试用例,可以代替人工完成重复性的工作,提高效率;而第三方App的自动化指的是为完成某一目标或获取指定内容而进行的自动化运行。随着技术的不断发展,App安全性的不断提升,破解App或者对App完成抓包的成本越来越高,通过自动化,能够在不破解第三方App的情况下获取App中的部分信息。除此,还可以通过自动化实现一些小工具,比如,微信自动抢红包插件(作者已将代码开源),跳过apk授权页(源码见下文)等。

自动化需要模拟用户行为,按照事先设计好的路径,完成固定的流程,用户行为可能包含启动App、点击、滚动、输入、返回等常规操作,一个完整的流程是这些事件的有序组合。为了使自动化程序更加稳定,即在程序进入异常或错误的页面时仍然能够正常响应,并逐步回到正常流程,事件也可能是基于页面当前状态的。一个基本的运行顺序应该是,获取页面状态,即页面名称、页面内容、页面结构,保存相应状态,如计数器、输出信息等,然后根据页面状态和保存状态进行相应的操作,然后触发页面变化,再获取新的状态,重复这一过程。

自动化工具一:辅助服务

Android系统中运行的每个App都是一个单独进程,因而除了攻破Android系统,否则不可能在运行时获取其他App的运行时内存,进而不能够直接操作其他App。然而,Android系统提供了辅助服务(AccessibilityService),辅助服务是Android系统提供的一种设计用来帮助残疾人的工具。作为Android系统提供的一种标准服务,辅助服务能够监控Android系统中其他App中发生的事件,并可以获取页面结构和操作页面,还可以进一步进行语音提示,从而帮助残疾人更好的使用Android应用。能够被监控的事件基本覆盖了App内部发生的所有事件,如页面的重绘、点击、滚动、焦点变化等。下面借助辅助服务来完成一个能够自动跳过授权页安装apk的小工具,以说明辅助服务的使用方式。

自动授权apk安装小工具

安装apk时,会首先弹出授权页,这个时候需要用户点击安装按钮才能够开始安装。在需要同时安装多个apk的时候,授权过程会显得略为繁琐。因此,可以通过一个在授权页自动点击安装的小工具来完成安装。首先通过以下代码来向系统提交安装apk的请求:

public void autoInstall(View view) { String apkPath = Environment.getExternalStorageDirectory() + ";; Uri uri = Uri.fromFile(new File(apkPath)); Intent localIntent = new Inten); localIn(uri, "application;); startActivity(localIntent); }

这里使用了一个,放到了SD卡的根目录下,然后在代码中获取apk路径,转换成URI,设置参数application,启动安装程序。Android系统安装apk的过程是由com.android.packageinstaller这个包来管理的,所以需要监控来自这个包的所有事件。辅助服务可以指定要监控App的包名,如下为实现这个小工具的服务配置,需要放到一个单独的xml文件中:

<?xml version="1.0" encoding="utf-8"?> <!-- res/xml --> <accessibility-service xmlns:android="; android:accessibilityEventTypes="typeWindowStateChanged|typeWindowContentChanged" android:accessibilityFeedbackType="feedbackAllMask" android:accessibilityFlags="flagDefault" android:canRetrieveWindowContent="true" android:description="@string/auto_install_desc" android:packageNames="com.android.packageinstaller" />

辅助服务反馈的事件有很多种,这里只选择其中两种,获取的事件是typeWindowStateChangedtypeWindowContentChanged,当窗体状态或窗体内容变化,也就是页面发生变化的时候,当前辅助服务会收到事件。android:description指定的内容会在开启辅助服务的时候,展示在辅助服务设置页面。这个配置需要在AndroidMani中引入:

<!-- AndroidMani --> <service android:name=".AutoInstallAccessibility" android:permission="android.;> <intent-filter> <action android:name="android.acce; /> </intent-filter> <meta-data android:name="android.accessibilityservice" android:resource="@xml/config" /></service>

辅助服务在 AndroidMani 中的声明需要包含 android. 权限、过滤器和特定的meta-data,配置的xml在meta-data中的 android:resource 中引入。然后定义一个服务,继承AccessibilityService,这个是所有辅助服务的基类,所有的辅助服务均由系统管理,这里只需要实现特定的接口:

public class AutoInstallAccessibility extends AccessibilityService { private static final String TAG = "MyAccessibility"; @Override public void onAccessibilityEvent(AccessibilityEvent event) { if () == Acce) { List<AccessibilityNodeInfo> nodes = getRootInActiveWindow().findAccessibilityNodeInfosByText("安装"); if (nodes != null && nodes.size() > 0) { for (AccessibilityNodeInfo node : nodes) { node.performAction); } } } } @Override public void onInterrupt() { } }

自定义服务需要实现onAccessibilityEvent(AccessibilityEvent event)来处理监控的App反馈的事件,通过getRootInActiveWindow()来获取页面的根节点信息,这个根节点信息是一个AccessibilityNodeInfo的实例,可以通过其获得节点及子节点的状态信息,如展示的文字getText()、描述字段getContentDescription(),同时也可以通过这些信息定位子节点。节点的点击操作通过performAction这个函数完成。最后,启动系统的辅助服务列表页:

public void startAccessibilityConfigPage(View view) { Intent accessibleIntent = new Inten); startActivity(accessibleIntent); }

所有的辅助服务都会展示在这个列表页中,在辅助服务设置页面中,找到当前应用,进入辅助服务配置页,点击开启服务。之后启动安装即可完成自动授权过程。在不同Android设备上可能表现不同,这里的逻辑是:监控系统安装页面的事件,然后在页面中找到带有安装文案的节点,执行点击操作。不同设备安装页面的页面结构可能不尽相同,要想兼容各种各样的Android设备可以使用Android SDK提供的层级查看工具uiautomatorviewer来分析App的页面结构,然后确定搜索和点击路径,如英文系统可能点击的文案是INSTALL

辅助服务自动化的缺陷

辅助服务在进行自动化时,存在以下缺陷:

  • 辅助服务本身是一个Service,这个Service被系统管理,可能因为很多原因挂掉,则自动化停止。

  • 辅助服务无法自动化WebView。

自动化工具二:uiautomator

辅助服务作为Android系统中的一种标准服务,很难通过脚本去配置和控制,也不容易监控。另外一个进行自动化的思路是通过自动化测试工具,这要求自动化测试工具必须是完全黑盒的,也即在不了解代码逻辑、代码接口的前提下,能够顺利的编写用例,实现必要的功能。

uiautomator是Google提供的为Android编写UI测试用例的自动化工具,开发时,可以按照如下方式使用Gradle添加依赖:

androidTestCompile 'com.android.;

也可以直接依赖uiau,uiau在Android SDK目录下的platforms/[android-version]/目录中,提供编写用例所需要的工具类。运行用例必须在Android系统中,可以打成Java的可执行文件jar包,也可以打包成Android标准的test apk,执行需要依赖adb工具。除此,uiautomator还可以作为获取Android设备页面布局的工具,Android SDK目录tools/bin/下的可执行文件uiautomatorviewer是uiautomator获取首页页面布局的图形界面版本,能够方便的查看页面布局,在分析第三方App的时候,会经常用到。

使用命令行

uiautomator的测试用例可以打包运行,也可以通过adb命令运行。uiautomator主要支持三个命令runtest 用来执行测试用例,dump 命令获取页面信息,events 可以把发生的所有的Accessibility事件(即上文中辅助服务能够获取和操作的事件)输出。

adb shell uiautomator help

adb的help命令会详细输出每个指令的具体功能。这里首先使用dump命令,通过脚本,实现基本的自动化功能。dump命令会把当前Android设备展示的页面结构以xml的方式输出到文件,所以需要做的是解析xml,获取各个节点信息,节点信息会包含位置,之后配合adb的input指令,完成基本的操作,实现自动化。下面利用dump命令完成简单的自动化脚本,脚本使用Python编写。首先通过adb命令获取页面布局:

def execute_adb(cmd): return (('adb %s' % cmd).split(' '))def dump_layout(): """生成的默认文件默认保存在/sdcard,通过pull命令取回本地""" execute_adb('shell uiautomator dump') execute_adb('pull /sdcard .')

页面布局的xml文件会把Android设备的页面解析成一个由node节点构成的树结构,每个node节点会固定的包含位置、点击、View的类名等信息,如下是一个根节点的信息:

<node bounds="[0,0][1080,1794]" checkable="false" checked="false" class="android.widget.FrameLayout" clickable="false" content-desc="" enabled="true" focusable="false" focused="false" index="0" long-clickable="false" package="com.exam; password="false" resource-id="" scrollable="false" selected="false" text="">

节点信息包含了开发中常会用到的属性,由此可以看到,虽然限于Android内存管理模型无法获取页面上节点的实际内存对象,但是仍然可以借助uiautomator来获取节点的状态。bounds是当前节点在屏幕中的位置,为了操作节点,需要使用位置信息确定指定View的位置,然后通过adb的input命令完成具体的操作。解析xml,获取节点位置,完成点击、滚动、输入等操作:

def read_bounds(text): """获取指定文字内容的节点位置""" pattern = re.compile(r'[(\d+),(\d+)][(\d+),(\d+)]') with open('.', 'r') as f: file_content = f.read() if file_content: node_file = e(file_content) bounds_str = node_(u'//node[@text="%s"]/@bounds' % text)[0] return map(int, (bounds_str).groups()) return Nonedef perform_click(x, y): execute_adb('shell input tap %d %d' % (x, y))def perform_scroll(x0, y0, x1, y1, duration): execute_adb('shell input swipe %d %d %d %d %d' % (x0, y0, x1, y1, duration))def input_text(text): execute_adb('shell input text %s' % text) ... ...

adb的input的命令十分丰富,能够满足绝大多数的交互操作,tap模拟点击,swipe模拟手指在屏幕上移动,text模拟文字输入,除此比较常用到的还有keyevent,用于模拟按键操作,按键包含了返回键、home键、软键盘以及外接键盘的按键。如果需要支持多设备,需要为adb命令加上-s选项,指定设备序列号。通过拼接这些操作,能够完成简单的自动化程序。

利用dump方式的缺陷

  • dump指令耗时过长,严重影响自动化效率。

用例方式

uiautomator的主要功能是用来执行测试用例,测试用例使用Java编写,开发时依赖Android SDK。可以通过执行用例的方式来进行自动化操作第三方App,用例一般的开发方式是编写用例函数,打包成jar或者test apk。写好的用例不能在本地直接运行,如果是jar包,必须推送到Android设备上通过adb运行,如果是test apk,需安装运行。test apk是指用来执行测试的apk,会在build的时候生成,如下为SDK中uiautomator工具API中UiDevice的部分源码,可以看到源码没有实现,只是提供了接口,因此用例无法脱离设备运行。

UiDevice() { throw new RuntimeException("Stub!"); }public void setCompressedLayoutHeirarchy(boolean compressed) { throw new RuntimeException("Stub!"); } ......

uiautomator提供了获取设备和页面节点信息的API,借助UiDevice来获取设备信息,借助UiObject、UiObject2获取页面上的节点信息。

通过用例方式完成自动化程序的步骤为:

  • 分析要自动化的App的页面结构,设计自动化路径

  • 编写自动化程序(实际上是一个测试用例)

  • 打包,push到Android设备上(apk通过adb am instrument命令,jar包通过adb shell uiautomator runtest命令运行)

  • 执行自动化程序

使用这种方式执行自动化效率相比dump指令方式明显有所提高,如下为通过这种方式自动获取当前用户已关注微信公众号列表的程序,微信使用6.6.5版本:

@RunWi)public class WeixinAutomator { @Test public void listWxPublicAccount() throws UiObjectNotFoundException { UiDevice device = UiDevice.getInstance()); Context context = In(); final String logTag = "listWxPublicAccount"; device.pressHome(); Intent launchIntent = new Intent(); launc(new ComponentName("com.;, "com.;)); con(launchIntent); device.findObject(new UiSelector().text("通讯录")).click(); device.findObject(new UiSelector().text("公众号")).click(); UiObject listObj = device.findObject(new UiSelector().className("android.widget.ListView")); HashSet<String> names = new HashSet<>(); while (true) { int childCount = li(); for (int i = 0; i < childCount - 1; i++) { String text = listObj .getChild(new UiSelector() .index(i) .childSelector(new UiSelector() .className("android.widget.TextView") .resourceId("com.;))) .getText(); if (!names.contains(text)) { names.add(text); Log.e(logTag, "text: " + text); } } try { UiObject obj = device.findObject(new UiSelector().textContains("个公众号")); if (obj != null && !Tex())) { break; } } catch (UiObjectNotFoundException e) { } UiScrollable listScrollable = new UiScrollable(new UiSelector().scrollable(true)); li(1); } } }

运行uiautomator的用例需要使用AndroidJUnit4,用例的入口方法要用org.junit.Test这个标签修饰,多个用例之间执行顺序无法保证,所以不能通过多用例的方式进行自动化。这里首先启动微信的首页,然后通过文案查找的方式进入公众号列表页面,遍历公众号条目,通过uiautomatorviewer可以看到公众号名称对应的resourceId是com.(不同版本、渠道的微信apk可能对应的resourceId是不一样的,因为微信使用AndResGuard进行了资源混淆),通过UiSelector获取节点,然后通过getText获取名称,之后进行滚动。遍历的终止条件是:当公众号列表页面滚动到底部之后,一定会展示“xx个公众号”这样的文案,检测到这个文案,认为列表已经滚到底部。UiObject的对象是长时间有效的,所以当执行滚动之后,使用listObj对象仍然可以获取到当前ListView的最新节点数量。

最后,打包执行。通过adb命令运行:

# jar包的运行方式adb shell uiautomator runtest ca -c com.exam # test apk的运行方式 ## 首先push test apk到Android设备上 adb push [test-apk-path] /data/local/tm ## 安装 adb shell pm install -t -r "/data/local/tm" ## 启动测试用例 adb shell am instrument -w -e class com.exam com.exam runtest

runtest命令通过-c指定要执行用例的主类路径。instrument命令用来执行Android的InstrumentationTest,-w
选项表明等待直到用例运行结束,-e表示添加key-value形式的参数,设置用例的入口为com.exam,即用例的class路径,最后是用来执行用例的AndroidJUnitRunner的类路径。

用例方式实现自动化的缺陷

  • 无法自动化WebView。

集成adb

ADB是Google提供的能够和Android设备进行交互的命令行工具。如上所示的小程序均是基于顺序的,各个事件按照事先设计好的顺序,一个一个执行,意味着在执行过程中,如果不出现意外,能够正常运行,但如果出现诸如断网、网速过慢、手机卡顿、app崩溃之类的问题后,自动化就会异常停止。所以自动化想要稳定运行,应该是基于页面状态的。基于页面状态是指,任何一个页面,不论其当前展示的内容是什么,是断网还是未断网,是否加载失败,都应对应于一种状态,程序应该能够处理这种状态,从而即使偏离了原有的流程,也能够慢慢的再回到正常的流程。这就要求完整的监控机制。除此,自动化通常运行独立,不会有人工实时的监测,也即出现问题,应该能够事后发现和修复问题。这就要求要有完整的日志系统。要想实现这些,需要依赖adb。同时,不论是辅助服务还是uiautomator,虽然能一定程度的解析WebView的结构,但都无法操作WebView。通过adb的input指令,可以模拟用户的各种操作,这也使得自动化WebView成为可能。

由于大多数未root的Android设备都无法进行远端adb连接,也即无法通过远程的方式执行adb命令,所以这里考虑使用套接字。

在宿主设备建立套接字服务

使用adb调试Android设备通常需要使用PC或者服务器,这里暂且把其称作宿主设备。在宿主设备上建立套接字服务,则请求方为Android设备。宿主设备辅助完成自动化功能,并实时监控Android设备的自动化状态。如果要确保在Android设备上运行的程序能够稳定运行,应设立超时机制,如果超时未收到来自Android设备的事件信息,则应进行相应的恢复或重启操作。

Android设备本身是一个Linux系统,若想要在这个Linux系统和宿主设备操作系统之间建立套接字通信,首先要进行端口转发。在宿主设备上建立套接字服务,Android设备向宿主设备发送请求,则需要把Android设备上的某一空闲端口上的内容映射到宿主的某一空闲端口上,如把Android设备的8484接口转发到宿主设备的8484端口上,需要执行:

adb reverse tcp:8484 tcp:8484

可以使用:

adb reverse --list

查看已经映射的端口。然后在宿主设备上建立套接字服务,这里仍然使用Python:

import SocketServer import json import threading import traceback PORT = 8484 HOST = "localhost" chunk_size = 4096class AndroidCommandHandler): def handle(self): try: data = (chunk_size) while da('\n\n') < 0: data += (chunk_size) data = da('\n\n')[0] # ... # params = j(data) # params是来自Android设备的请求,解析params并执行相应的操作 # ... res = {'success': True} (res) + '\n\n') except: res = {'success': False} (res) + '\n\n') ()class AndroidCommandServer, Socke): passserver = AndroidCommandServer((HOST, PORT), AndroidCommandHandler) server_thread = (target=) ()

这里为了简便,利用Python的 Socke 启动一个多线程处理事件的套接字服务,AndroidCommandHandler 这里的解析和处理过程会在一个单独的线程中进行,使用双换行作为通信的终止符,指令接收完毕可以按照一些实现定义好的协议去做相应的处理,比如执行adb操作、记录日志等。一种典型的应用场景是针对WebView,因为辅助服务和uiautomator都无法操作WebView,所以可以先在Android设备上通过辅助服务或uiautomator分析WebView的节点结构,然后把节点信息通过套接字发送到宿主设备上,再在宿主设备通过adb的input的命令完成具体的操作。

在Android设备上建立套接字服务

在Android设备上建立套接字服务,则宿主设备作为请求方。这种方式可以通过套接字把原本依赖设备的处理过程放到宿主设备上,也即任何一次查询和操作都是由宿主设备发起的,这样想要进行的自动化流程完全可以在宿主设备上编写程序来处理。开源自动化测试框架Appium即通过这种方式,能够在完全不需要修改源码条件下,执行自动化测试用例,并且,支持任意语言来编写自动化测试用例。因为基于这样的结构,任何开发语言都能使用套接字来和Android设备通信,从而获取页面信息、操作Android设备。

Appium使用Node.js搭建,同时支持iOS和Android的自动化测试,为Android提供底层接口的是appium-android-bootstrap,通过源码,可以看到其是依赖uiautomator实现的。不过Appium框架本质上是用来进行自动化测试的,框架功能强大,但是却不能很好的应用于第三方apk,其主要是用于debug版本的apk的。如使用Appium尝试自动化微信,在启动Appium服务的时候,会产生一条错误信息,导致自动化停止。阅读Appium源码之后,发现原因是Appium在安装apk之前会使用aapt命令来获取apk的基本信息,而这条命令在微信的apk上执行失败。即便如此,Appium的思路仍然是值得借鉴的,下面简单说明Appium这种利用套接字来开放uiautomator接口的方式。

首先需要把宿主设备上的某一端口内容,全部转发到Android设备上,使用adb命令:

adb forward tcp:4724 tcp:4724

由于分析的Appium的源代码,所以使用Appium的端口号,Appium在宿主设备上的服务默认是使用4724作为和Android设备进行通信的端口。查看已经转发的接口:

adb forward --list

adb的forward命令和reverse命令用法基本一致,只是方向是相反的,forward是把宿主设备上的端口转发到Android设备上,reverse是把Android设备的端口转发到宿主设备上。然后利用一个测试用例,启动套接字服务:

// appium-bootstrap源码public class Bootstrap extends UiAutomatorTestCase { public void testRunServer() { Find.params = getParams(); boolean disableAndroidWatchers = Boolean.parseBoolean(getParams().getString("disableAndroidWatchers")); boolean acceptSSLCerts = Boolean.parseBoolean(getParams().getString("acceptSslCerts")); SocketServer server; try { server = new SocketServer(4724); (disableAndroidWatchers, acceptSSLCerts); } catch (final SocketServerException e) { Logger.error()); Sy(1); } } }

Appium在Android设备上启动一个套接字服务监听来自端口4724的所有消息,套接字服务会做一个无限循环,也即是一个持续不会中断的测试用例,这个用例会在收到来自宿主设备的命令时,执行相应的操作。然后是对于消息的处理:

// appium-bootstrap源码public void listenForever(boolean disableAndroidWatchers, boolean acceptSSLCerts) throws SocketServerException { ... ... try { client = (); Logger.debug("Client connected"); in = new BufferedReader(new InputStreamReader(), "UTF-8")); out = new BufferedWriter(new OutputStreamWriter(), "UTF-8")); while (keepListening) { handleClientData(); } in.close(); out.close(); client.close(); Logger.debug("Closed client connection"); } catch (final IOException e) { throw new SocketServerException("Error when client was trying to connect"); } }

client是一个)[]对象,调用accept建立套接字连接,获取套接字内容。内容由handleClientData来处理,handleClientData只是将字符串解析成具体的指令然后调用runCommand来执行具体的命令:

// appium-bootstrap源码private String runCommand(final AndroidCommand cmd) { AndroidCommandResult res; if () == AndroidCommandTy) { keepListening = false; res = new AndroidCommandResul, "OK, shutting down"); } else if () == AndroidCommandTy) { try { res = execu(cmd); } catch (final NoSuchElementException e) { res = new AndroidCommandResul, e.getMessage()); } catch (final Exception e) { Logger.debug("Command returned error:" + e); res = new AndroidCommandResul, e.getMessage()); } } else { // this code should never be executed, here for future-proofing res = new AndroidCommandResul, "Unknown command type, could not execute!"); } return res.toString(); }

来自宿主设备的指令可以关闭当前服务,也可以执行相应的Action。Appium定义的Action有很多,节点相关的节点Action会以element:开头,之后才是具体的Action内容,Appium以这种方式来确定该操作是否需要节点信息,Action的内容可能包括节点寻找、执行具体的点击、滚动、输入等等,Appium定义的所有Action如下所示:

// appium-bootstrap源码private static HashMap<String, CommandHandler> map = new HashMap<String, CommandHandler>(); static { map.put("waitForIdle", new WaitForIdle()); map.put("clear", new Clear()); map.put("orientation", new Orientation()); map.put("swipe", new Swipe()); map.put("flick", new Flick()); map.put("drag", new Drag()); map.put("pinch", new Pinch()); map.put("click", new Click()); map.put("touchLongClick", new TouchLongClick()); map.put("touchDown", new TouchDown()); map.put("touchUp", new TouchUp()); map.put("touchMove", new TouchMove()); map.put("getText", new GetText()); map.put("setText", new SetText()); map.put("getName", new GetName()); map.put("getAttribute", new GetAttribute()); map.put("getDeviceSize", new GetDeviceSize()); map.put("scrollTo", new ScrollTo()); map.put("find", new Find()); map.put("getLocation", new GetLocation()); map.put("getSize", new GetSize()); map.put("getRect", new GetRect()); map.put("wake", new Wake()); map.put("pressBack", new PressBack()); map.put("pressKeyCode", new PressKeyCode()); map.put("longPressKeyCode", new LongPressKeyCode()); map.put("takeScreenshot", new TakeScreenshot()); map.put("updateStrings", new UpdateStrings()); map.put("getDataDir", new GetDataDir()); map.put("performMultiPointerGesture", new MultiPointerGesture()); map.put("openNotification", new OpenNotification()); map.put("source", new Source()); map.put("compressedLayoutHierarchy", new CompressedLayoutHierarchy()); map.put("configurator", new ConfiguratorHandler()); }

Appium使用的基本都是uiautomator的接口。Appium是支持WebView的测试用例的,只是需要App的WebView开启Debug模式,同样这在第三方App上是无法做到的,所以针对这种情况,仍然要使用套接字通知宿主设备之后,利用adb的input方式来进行操作。

总结

如上介绍了自动化Android App过程中可能用到的几个工具,将这些工具搭配使用能够互补的实现原本无法实现的功能,同时配合宿主设备,搭建完整的稳定的Android自动化系统。经过一段时间的实践发现,通过辅助服务来获取事件和分析节点结构的效率是最高的,uiautomator是功能和API最丰富的,而脚本和adb命令是调试和控制最方便的。通过自动化能够做到定期监控其他App的状态,从第三方App中获取基本信息,而无需进行复杂繁琐的逆向分析。同时,自动化能够很好的解决重复性工作,节省人力,来进行更多的工作,当然,也能够通过自动化完成对自身App的基本测试,覆盖主要流程,然后可以用来作为上线之前的一次安全检查。

责任编辑: 鲁达

1.内容基于多重复合算法人工智能语言模型创作,旨在以深度学习研究为目的传播信息知识,内容观点与本网站无关,反馈举报请
2.仅供读者参考,本网站未对该内容进行证实,对其原创性、真实性、完整性、及时性不作任何保证;
3.本站属于非营利性站点无毒无广告,请读者放心使用!

“Oracle中如何去掉自动换行”边界阅读