(七十一)Android O WiFi热点 开启流程梳理

news/2024/4/24 4:24:47/

前言:之前主要梳理了WiFi开启扫描连接的流程,现在梳理下WiFi 热点 的开启流程。

时序图mdj样式:https://download.csdn.net/download/sinat_20059415/10542186

 

1. wifi热点简介

 

wifi热点是将手机接收的GPRS、3G或4G信号转化为wifi信号发出去的技术,手机必须有无线AP功能,才能当做热点。有些系统自带建热点这个功能比如 IOS(比如 iPhone 4s)。

如果你把你的iPhone当做热点,那么像TOUCH,PAD这样有WIFI功能的,都可以搜索到你手机建立的WIFI网络,连接上以后,TOUCH等使用WIFI产生的流量上网都是消耗的乐WIFI里的手机卡的GPRS或3G流量,所以iphone里最好放一张包大流量的上网卡。还有,把手机当做热点很费电,最好用的时候插上充电器。

 

2. WiFi热点开启流程梳理

2.1 Settings

Android O的Settings引入了PreferenceController这个包装类来实现对Preference的精细化控制,让代码结构更加地鲜明,很好地体现了单一职责原则,以前的Preference都是一大坨代码冗余在一起,看的都头疼。

TetherSettings->WifiTetherPreferenceController->WifiTetherSwitchBarController

TetherSettings是WiFi热点对应的界面,WifiTetherSwitchBarController是用来负责热点开关的相关逻辑处理的。

/packages/apps/Settings/src/com/android/settings/wifi/tether/WifiTetherSwitchBarController.java

public class WifiTetherSwitchBarController implements SwitchWidgetController.OnSwitchChangeListener,LifecycleObserver, OnStart, OnStop {
...@Overridepublic boolean onSwitchToggled(boolean isChecked) {if (isChecked) {startTether();} else {stopTether();}return true;}void stopTether() {mSwitchBar.setEnabled(false);mConnectivityManager.stopTethering(TETHERING_WIFI);}void startTether() {mSwitchBar.setEnabled(false);mConnectivityManager.startTethering(TETHERING_WIFI, true /* showProvisioningUi */,NoOpOnStartTetheringCallback.newInstance(), new Handler(Looper.getMainLooper()));}

可以看到WiFi热点代开是通过ConnectivityManager的startTethering方法。

1) callback(抽象类直接new出来的,内部实现还没有。。。)

class NoOpOnStartTetheringCallback {public static ConnectivityManager.OnStartTetheringCallback newInstance() {return new ConnectivityManager.OnStartTetheringCallback() {};}   
}
    /*** Callback for use with {@link #startTethering} to find out whether tethering succeeded.* @hide*/@SystemApipublic static abstract class OnStartTetheringCallback {/*** Called when tethering has been successfully started.*/public void onTetheringStarted() {}/*** Called when starting tethering failed.*/public void onTetheringFailed() {}}

 

 

2)状态监听:

 

 

    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {@Overridepublic void onReceive(Context context, Intent intent) {String action = intent.getAction();if (WifiManager.WIFI_AP_STATE_CHANGED_ACTION.equals(action)) {final int state = intent.getIntExtra(WifiManager.EXTRA_WIFI_AP_STATE, WifiManager.WIFI_AP_STATE_FAILED);handleWifiApStateChanged(state);} else if (Intent.ACTION_AIRPLANE_MODE_CHANGED.equals(action)) {enableWifiSwitch();}}};  private void handleWifiApStateChanged(int state) {switch (state) {case WifiManager.WIFI_AP_STATE_ENABLING:mSwitchBar.setEnabled(false);break;case WifiManager.WIFI_AP_STATE_ENABLED:if (!mSwitchBar.isChecked()) {mSwitchBar.setChecked(true);}enableWifiSwitch();break;case WifiManager.WIFI_AP_STATE_DISABLING:if (mSwitchBar.isChecked()) {mSwitchBar.setChecked(false);}mSwitchBar.setEnabled(false);break;case WifiManager.WIFI_AP_STATE_DISABLED:mSwitchBar.setChecked(false);enableWifiSwitch();break;default:mSwitchBar.setChecked(false);enableWifiSwitch();break;}}   private void enableWifiSwitch() {boolean isAirplaneMode = Settings.Global.getInt(mContext.getContentResolver(),Settings.Global.AIRPLANE_MODE_ON, 0) != 0;if (!isAirplaneMode) {mSwitchBar.setEnabled(!mDataSaverBackend.isDataSaverEnabled());} else {mSwitchBar.setEnabled(false);}}

 

 

 

2.2 ConnectivityManager

 

    /*** Runs tether provisioning for the given type if needed and then starts tethering if* the check succeeds. If no carrier provisioning is required for tethering, tethering is* enabled immediately. If provisioning fails, tethering will not be enabled. It also* schedules tether provisioning re-checks if appropriate.** @param type The type of tethering to start. Must be one of*         {@link ConnectivityManager.TETHERING_WIFI},*         {@link ConnectivityManager.TETHERING_USB}, or*         {@link ConnectivityManager.TETHERING_BLUETOOTH}.* @param showProvisioningUi a boolean indicating to show the provisioning app UI if there*         is one. This should be true the first time this function is called and also any time*         the user can see this UI. It gives users information from their carrier about the*         check failing and how they can sign up for tethering if possible.* @param callback an {@link OnStartTetheringCallback} which will be called to notify the caller*         of the result of trying to tether.* @param handler {@link Handler} to specify the thread upon which the callback will be invoked.* @hide*/@SystemApi@RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)public void startTethering(int type, boolean showProvisioningUi,final OnStartTetheringCallback callback, Handler handler) {Preconditions.checkNotNull(callback, "OnStartTetheringCallback cannot be null.");ResultReceiver wrappedCallback = new ResultReceiver(handler) {@Overrideprotected void onReceiveResult(int resultCode, Bundle resultData) {if (resultCode == TETHER_ERROR_NO_ERROR) {callback.onTetheringStarted();} else {callback.onTetheringFailed();}}};try {String pkgName = mContext.getOpPackageName();Log.i(TAG, "startTethering caller:" + pkgName);mService.startTethering(type, wrappedCallback, showProvisioningUi, pkgName);} catch (RemoteException e) {Log.e(TAG, "Exception trying to start tethering.", e);wrappedCallback.send(TETHER_ERROR_SERVICE_UNAVAIL, null);}}

至于mService:

        mConnectivityManager =(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);

SystemServiceRegistry:

        registerService(Context.CONNECTIVITY_SERVICE, ConnectivityManager.class,new StaticApplicationContextServiceFetcher<ConnectivityManager>() {@Overridepublic ConnectivityManager createService(Context context) throws ServiceNotFoundException {IBinder b = ServiceManager.getServiceOrThrow(Context.CONNECTIVITY_SERVICE);IConnectivityManager service = IConnectivityManager.Stub.asInterface(b);return new ConnectivityManager(context, service);}});
    /*** {@hide}*/public ConnectivityManager(Context context, IConnectivityManager service) {mContext = Preconditions.checkNotNull(context, "missing context");mService = Preconditions.checkNotNull(service, "missing IConnectivityManager");sInstance = this;}

SystemServer:

                traceBeginAndSlog("StartConnectivityService");try {connectivity = new ConnectivityService(context, networkManagement, networkStats, networkPolicy);ServiceManager.addService(Context.CONNECTIVITY_SERVICE, connectivity);networkStats.bindConnectivityManager(connectivity);networkPolicy.bindConnectivityManager(connectivity);} catch (Throwable e) {reportWtf("starting Connectivity Service", e);}traceEnd();

 

所以mService是ConnectivityService。

 

 

2.3 ConnectivityService

    @Overridepublic void startTethering(int type, ResultReceiver receiver, boolean showProvisioningUi,String callerPkg) {ConnectivityManager.enforceTetherChangePermission(mContext, callerPkg);if (!isTetheringSupported()) {receiver.send(ConnectivityManager.TETHER_ERROR_UNSUPPORTED, null);return;}mTethering.startTethering(type, receiver, showProvisioningUi);}

 

2.4 Tethering

 

    public void startTethering(int type, ResultReceiver receiver, boolean showProvisioningUi) {if (!isTetherProvisioningRequired()) {enableTetheringInternal(type, true, receiver);return;}if (showProvisioningUi) {runUiTetherProvisioningAndEnable(type, receiver);} else {runSilentTetherProvisioningAndEnable(type, receiver);}}

暂时先认为打开tethering需要provision吧,继续往下看。

   /*** Check if the device requires a provisioning check in order to enable tethering.** @return a boolean - {@code true} indicating tether provisioning is required by the carrier.*/@VisibleForTestingprotected boolean isTetherProvisioningRequired() {final TetheringConfiguration cfg = mConfig;if (mSystemProperties.getBoolean(DISABLE_PROVISIONING_SYSPROP_KEY, false)|| cfg.provisioningApp.length == 0) {return false;}if (carrierConfigAffirmsEntitlementCheckNotRequired()) {return false;}return (cfg.provisioningApp.length == 2);}

shouProvisioningUi由设置传入,是true

    private void runUiTetherProvisioningAndEnable(int type, ResultReceiver receiver) {ResultReceiver proxyReceiver = getProxyReceiver(type, receiver);sendUiTetherProvisionIntent(type, proxyReceiver);}
    /*** Creates a proxy {@link ResultReceiver} which enables tethering if the provisioning result* is successful before firing back up to the wrapped receiver.** @param type The type of tethering being enabled.* @param receiver A ResultReceiver which will be called back with an int resultCode.* @return The proxy receiver.*/private ResultReceiver getProxyReceiver(final int type, final ResultReceiver receiver) {ResultReceiver rr = new ResultReceiver(null) {@Overrideprotected void onReceiveResult(int resultCode, Bundle resultData) {// If provisioning is successful, enable tethering, otherwise just send the error.if (resultCode == TETHER_ERROR_NO_ERROR) {enableTetheringInternal(type, true, receiver);} else {sendTetherResult(receiver, resultCode);}}};// The following is necessary to avoid unmarshalling issues when sending the receiver// across processes.Parcel parcel = Parcel.obtain();rr.writeToParcel(parcel,0);parcel.setDataPosition(0);ResultReceiver receiverForSending = ResultReceiver.CREATOR.createFromParcel(parcel);parcel.recycle();return receiverForSending;}
    private void sendUiTetherProvisionIntent(int type, ResultReceiver receiver) {Intent intent = new Intent(Settings.ACTION_TETHER_PROVISIONING);intent.putExtra(EXTRA_ADD_TETHER_TYPE, type);intent.putExtra(EXTRA_PROVISION_CALLBACK, receiver);intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);final long ident = Binder.clearCallingIdentity();try {mContext.startActivityAsUser(intent, UserHandle.CURRENT);} finally {Binder.restoreCallingIdentity(ident);}}

Settings有如下类会接收并处理消息

        <activity android:name="TetherProvisioningActivity"android:exported="true"android:permission="android.permission.TETHER_PRIVILEGED"android:excludeFromRecents="true"android:theme="@style/Theme.ProvisioningActivity"><intent-filter android:priority="1"><action android:name="android.settings.TETHER_PROVISIONING_UI" /><category android:name="android.intent.category.DEFAULT" /></intent-filter></activity>
/*** Activity which acts as a proxy to the tether provisioning app for sanity checks and permission* restrictions. Specifically, the provisioning apps require* {@link android.permission.CONNECTIVITY_INTERNAL}, while this activity can be started by a caller* with {@link android.permission.TETHER_PRIVILEGED}.*/
public class TetherProvisioningActivity extends Activity {private static final int PROVISION_REQUEST = 0;private static final String TAG = "TetherProvisioningAct";private static final String EXTRA_TETHER_TYPE = "TETHER_TYPE";private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);private ResultReceiver mResultReceiver;@Overridepublic void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);mResultReceiver = (ResultReceiver)getIntent().getParcelableExtra(ConnectivityManager.EXTRA_PROVISION_CALLBACK);int tetherType = getIntent().getIntExtra(ConnectivityManager.EXTRA_ADD_TETHER_TYPE,ConnectivityManager.TETHERING_INVALID);String[] provisionApp = getResources().getStringArray(com.android.internal.R.array.config_mobile_hotspot_provision_app);Intent intent = new Intent(Intent.ACTION_MAIN);intent.setClassName(provisionApp[0], provisionApp[1]);intent.putExtra(EXTRA_TETHER_TYPE, tetherType);if (DEBUG) {Log.d(TAG, "Starting provisioning app: " + provisionApp[0] + "." + provisionApp[1]);}if (getPackageManager().queryIntentActivities(intent,PackageManager.MATCH_DEFAULT_ONLY).isEmpty()) {Log.e(TAG, "Provisioning app is configured, but not available.");mResultReceiver.send(ConnectivityManager.TETHER_ERROR_PROVISION_FAILED, null);finish();return;}startActivityForResultAsUser(intent, PROVISION_REQUEST, UserHandle.CURRENT);}   @Overridepublic void onActivityResult(int requestCode, int resultCode, Intent intent) {super.onActivityResult(requestCode, resultCode, intent);if (requestCode == PROVISION_REQUEST) {if (DEBUG) Log.d(TAG, "Got result from app: " + resultCode);int result = resultCode == Activity.RESULT_OK ?ConnectivityManager.TETHER_ERROR_NO_ERROR :ConnectivityManager.TETHER_ERROR_PROVISION_FAILED;mResultReceiver.send(result, null);finish();}}   
}

具体provision app需要vendor具体配置,看framework/base/core/res/res是空的

./values/config.xml:407:    <string-array translatable="false" name="config_mobile_hotspot_provision_app">
./values/config.xml-408-    <!--
./values/config.xml-409-        <item>com.example.provisioning</item>
./values/config.xml-410-        <item>com.example.provisioning.Activity</item>
./values/config.xml-411-    -->
./values/config.xml-412-    </string-array>

完成后回调onActivityResult,发送ConnectivityManager.TETHER_ERROR_NO_ERROR,继续调用enableTetheringInternal方法。

        ResultReceiver rr = new ResultReceiver(null) {@Overrideprotected void onReceiveResult(int resultCode, Bundle resultData) {// If provisioning is successful, enable tethering, otherwise just send the error.if (resultCode == TETHER_ERROR_NO_ERROR) {enableTetheringInternal(type, true, receiver);} else {sendTetherResult(receiver, resultCode);}}};

 

   /*** Enables or disables tethering for the given type. This should only be called once* provisioning has succeeded or is not necessary. It will also schedule provisioning rechecks* for the specified interface.*/private void enableTetheringInternal(int type, boolean enable, ResultReceiver receiver) {boolean isProvisioningRequired = enable && isTetherProvisioningRequired();int result;switch (type) {case TETHERING_WIFI:result = setWifiTethering(enable);if (isProvisioningRequired && result == TETHER_ERROR_NO_ERROR) {scheduleProvisioningRechecks(type);}sendTetherResult(receiver, result);break;case TETHERING_USB:result = setUsbTethering(enable);if (isProvisioningRequired && result == TETHER_ERROR_NO_ERROR) {scheduleProvisioningRechecks(type);}sendTetherResult(receiver, result);break;case TETHERING_BLUETOOTH:setBluetoothTethering(enable, receiver);break;default:Log.w(TAG, "Invalid tether type.");sendTetherResult(receiver, TETHER_ERROR_UNKNOWN_IFACE);}}

可以看到tethering不特指WiFi热点,总体包含蓝牙WiFi热点和Usb。

我们还是走不需要provision的流程把。。。

    private int setWifiTethering(final boolean enable) {int rval = TETHER_ERROR_MASTER_ERROR;final long ident = Binder.clearCallingIdentity();try {synchronized (mPublicSync) {mWifiTetherRequested = enable;final WifiManager mgr = getWifiManager();if ((enable && mgr.startSoftAp(null /* use existing wifi config */)) ||(!enable && mgr.stopSoftAp())) {rval = TETHER_ERROR_NO_ERROR;}}} finally {Binder.restoreCallingIdentity(ident);}return rval;}

这边会走到WifiManager里去,wifi config为空表示使用已有的WiFi config。后面的senTetherResult就是回调之前的receiver通知执行结果。

 

2.5 WifiManager

    /*** Start SoftAp mode with the specified configuration.* Note that starting in access point mode disables station* mode operation* @param wifiConfig SSID, security and channel details as*        part of WifiConfiguration* @return {@code true} if the operation succeeds, {@code false} otherwise** @hide*/public boolean startSoftAp(@Nullable WifiConfiguration wifiConfig) {try {return mService.startSoftAp(wifiConfig);} catch (RemoteException e) {throw e.rethrowFromSystemServer();}}

 

2.6 WifiServiceImpl

 

    /*** see {@link android.net.wifi.WifiManager#startSoftAp(WifiConfiguration)}* @param wifiConfig SSID, security and channel details as part of WifiConfiguration* @return {@code true} if softap start was triggered* @throws SecurityException if the caller does not have permission to start softap*/@Overridepublic boolean startSoftAp(WifiConfiguration wifiConfig) {// NETWORK_STACK is a signature only permission.enforceNetworkStackPermission();mLog.info("startSoftAp uid=%").c(Binder.getCallingUid()).flush();synchronized (mLocalOnlyHotspotRequests) {// If a tethering request comes in while we have LOHS running (or requested), call stop// for softap mode and restart softap with the tethering config.if (!mLocalOnlyHotspotRequests.isEmpty()) {stopSoftApInternal();}return startSoftApInternal(wifiConfig, WifiManager.IFACE_IP_MODE_TETHERED);}}

 

    /*** Internal method to start softap mode. Callers of this method should have already checked* proper permissions beyond the NetworkStack permission.*/private boolean startSoftApInternal(WifiConfiguration wifiConfig, int mode) {mLog.trace("startSoftApInternal uid=% mode=%").c(Binder.getCallingUid()).c(mode).flush();// null wifiConfig is a meaningful input for CMD_SET_APif (wifiConfig == null || isValid(wifiConfig)) {SoftApModeConfiguration softApConfig = new SoftApModeConfiguration(mode, wifiConfig);mWifiController.sendMessage(CMD_SET_AP, 1, 0, softApConfig);return true;}Slog.e(TAG, "Invalid WifiConfiguration");return false;}

这边对空的wificonfig包装成了SoftApModeConfiguration接由WifiController处理。

    /*** Enqueue a message to this state machine.** Message is ignored if state machine has quit.*/public void sendMessage(int what, int arg1, int arg2, Object obj) {// mSmHandler can be null if the state machine has quit.SmHandler smh = mSmHandler;if (smh == null) return;smh.sendMessage(obtainMessage(what, arg1, arg2, obj));}

 

 

 

2.7 WifiController

ApStaDisabledState会对该消息进行对应的处理

                case CMD_SET_AP:if (msg.arg1 == 1) {if (msg.arg2 == 0) { // previous wifi state has not been saved yetmSettingsStore.setWifiSavedState(WifiSettingsStore.WIFI_DISABLED);}mWifiStateMachine.setHostApRunning((SoftApModeConfiguration) msg.obj,true);transitionTo(mApEnabledState);}break;

 

2.8 WifiStateMachine

 

    /*** TODO: doc*/public void setHostApRunning(SoftApModeConfiguration wifiConfig, boolean enable) {if (enable) {sendMessage(CMD_START_AP, wifiConfig);} else {sendMessage(CMD_STOP_AP);}}

 

看了一圈只有InitialState对该消息有正确响应

                case CMD_START_AP:transitionTo(mSoftApState);break;

InitialState exit()方法为空,看下SoftApState的enter方法

    class SoftApState extends State {private SoftApManager mSoftApManager;private String mIfaceName;private int mMode;private class SoftApListener implements SoftApManager.Listener {@Overridepublic void onStateChanged(int state, int reason) {if (state == WIFI_AP_STATE_DISABLED) {sendMessage(CMD_AP_STOPPED);} else if (state == WIFI_AP_STATE_FAILED) {sendMessage(CMD_START_AP_FAILURE);}setWifiApState(state, reason, mIfaceName, mMode);}}@Overridepublic void enter() {final Message message = getCurrentMessage();if (message.what != CMD_START_AP) {throw new RuntimeException("Illegal transition to SoftApState: " + message);}SoftApModeConfiguration config = (SoftApModeConfiguration) message.obj;mMode = config.getTargetMode();IApInterface apInterface = null;Pair<Integer, IApInterface> statusAndInterface = mWifiNative.setupForSoftApMode();if (statusAndInterface.first == WifiNative.SETUP_SUCCESS) {apInterface = statusAndInterface.second;} else {incrementMetricsForSetupFailure(statusAndInterface.first);}if (apInterface == null) {setWifiApState(WIFI_AP_STATE_FAILED,WifiManager.SAP_START_FAILURE_GENERAL, null, mMode);/*** Transition to InitialState to reset the* driver/HAL back to the initial state.*/transitionTo(mInitialState);return;}try {mIfaceName = apInterface.getInterfaceName();} catch (RemoteException e) {// Failed to get the interface name. The name will not be available for// the enabled broadcast, but since we had an error getting the name, we most likely// won't be able to fully start softap mode.}checkAndSetConnectivityInstance();mSoftApManager = mWifiInjector.makeSoftApManager(mNwService,new SoftApListener(),apInterface,config.getWifiConfiguration());mSoftApManager.start();mWifiStateTracker.updateState(WifiStateTracker.SOFT_AP);}
这边的调用流程和WiFi的启动流程有点类似。先梳理到这,后面应该是硬菜。。。

 

3. 总结


http://www.ppmy.cn/news/743972.html

相关文章

android手机传输,智能手机怎么传文件 安卓手机怎么传文件 安卓手机文件传输方法集合...

智能手机的功能多了&#xff0c;也会造成一个麻烦&#xff0c;就是需要越来越多种类的文件&#xff0c;文件容量越来越大&#xff0c;跟电脑或者其他手机倒起来也会相对麻烦&#xff0c;就拿苹果手机来说&#xff0c;如果要用手机和电脑相互间传一些东西的话&#xff0c;就需要…

matlab读取STK生成的报告

一、STK 和 Matlab的生成的图片对比 &#xff08;一&#xff09;STK图片 &#xff08;二&#xff09;Matlab图片 &#xff08;三&#xff09;STK生成的报表数据 "Time (UTCG)","Azimuth (deg)","Elevation (deg)","Range (km)" 20 J…

如何无损放大图片?用这5个图片无损放大工具,图片方法也超高清

图片太小使用时尺寸不够&#xff1f;想要放大图片发现图片变得非常模糊根本不能用&#xff1f;今天分享5个图片无损放大工具&#xff0c;不仅能将图片尺寸变大&#xff0c;还能提高图片清晰度&#xff0c;有了它们模糊图片一键就能变清晰&#xff01; 1、waifu2x 一个免费开源…

如何把tiff转换成jpg或png?学会这招,几十种图片格式任意转

分享4个可以将TIFF图像转换为JPG或PNG格式的工具&#xff0c;操作简单转换速度快&#xff0c;转换后还能保持原图画质。 单张图片格式转换&#xff1a; 1、照片 Windows系统中自带的图片查看软件&#xff0c;不需要下载安装&#xff0c;直接就能使用&#xff0c;需要转换图片格…

神经网络如何对图片进行识别,神经网络生成图片

如何把模糊图片转为高清 图片分辨率太低&#xff0c;会影响图片的质量。如何把模糊图片转为高清呢&#xff1f; 使用工具&#xff1a;嗨格式图片无损放大器这是一款可以根据我们图像的缺陷找到与之对应的解决办法&#xff0c;通过AI智能技术&#xff0c;可以让图像变的清晰锐…

图像相似度对比分析软件,图像相似度算法有哪些

有没有一款软件能比对两张照片是否一样? 可以用AI软件来对比。AI人脸比对已经变成非常常用的AI场景之一。步骤如下&#xff1a;1、浏览器输入网址百度AI应用&#xff0c;AI人脸比对。2、进入人脸比对的功能演示模块。 3、点击左侧的‘本地上传’按钮&#xff0c;上传本地照片…

会做饭,擅长烹饪,会给你的工作生活带来怎样的不同?

Neverland &#xff0c;旅行/纹身/厨房/健身/美黑/jazzhiphop/猫狗 124 人赞同 体验感如下&#xff1a; 1.我变成周围长辈口中的“别人家孩子”&#xff0c;各种被夸&#xff0c;长辈来我家做客&#xff0c;我都会亲自下厨讨个乖巧。 2.我妈住院的时候&#xff0c;我每天早起做…

如何把一张照片用 Photoshop 做成动画背景效果?

如果可以的话&#xff0c;请以这张照片为例&#xff0c;写一个过程。如需原图请留邮箱&#xff0c;我会发过去。谢谢。 坛子鸦 &#xff0c;过个年就成了大月半子。 可可苏玛 等 7564 人赞同 ----------------------------2014/7/21更新画云和画树的方法的方法---------------…

macos支持exfat吗_打造便捷、人性化的macOS桌面使用环境

原标题&#xff1a;打造便捷、人性化的macOS桌面使用环境 打造便捷、人性化的macOS桌面使用环境 2020-10-23 17:25:410点赞4收藏0评论 欢迎参加#果粉是怎样炼成的#征稿&#xff0c;围观秋季发布会新品&#xff01;是什么让苹果生态无法割舍&#xff1f;快来讲讲你的果粉炼成记…

动态启动页用gif能实现吗_GIF制作工具|手机制作GIF

点击蓝字 关注我们 你们还在为斗图时没有表情包而发愁吗&#xff1f; 你们还在局限于手机里的表情包吗&#xff1f; 那今天我们就来看看制作GIF的软件吧&#xff01; 一起来制作属于你的专属GIF动图吧&#xff01; 话不多说&#xff0c;咋们一起来看看吧&#xff01; 01 GIF动画…

怎么压缩图片200k以下?这个压缩工具,3秒无损完成

将图片制作成封面图、文案配图&#xff0c;我们常用ps、美图秀秀等工具&#xff0c;但想要将图片文件进行批量压缩并指定文件大小&#xff0c;应该怎么做呢&#xff1f; 今天分享的图片压缩的方法&#xff0c;可以将图片无损压缩至200k以下。 PS压缩 打开PS点击右上角文件&am…

初中计算机ppt速学,图片基本处理方法ppt课件初中信息技术苏科课标版七年级全一册课件_3.ppt...

图片基本处理方法ppt课件初中信息技术苏科课标版七年级全一册课件_3 南通市越江中学 王晓瑜 图片处理软件: 1、ACDSee 4、美图秀秀 2、 光影魔术手 3、 Ulead Photo Expree(我行我速) 5、 Photoshop 图片处理软件: ACDSee 软件 ACDSee是一款目前比较简单的数码图像处理软件&…

《OpenCv视觉之眼》Python图像处理二十一:Opencv图像处理之图像线性变换和非线性变换的方法及原理

本专栏主要介绍如果通过OpenCv-Python进行图像处理&#xff0c;通过原理理解OpenCv-Python的函数处理原型&#xff0c;在具体情况中&#xff0c;针对不同的图像进行不同等级的、不同方法的处理&#xff0c;以达到对图像进行去噪、锐化等一系列的操作。同时&#xff0c;希望观看…

图片上传时,显示格式错误怎么办?

很多朋友在电脑的某些平台上传图片的时候&#xff0c;会遇到所上传的图片格式不符合要求&#xff0c;&#xff0c;导致图片上传不成功的问题&#xff0c;那么图片格式显示错误&#xff0c;该怎么解决&#xff1f; 我们先看一下还没有处理过的图片素材&#xff0c;格式均为PNG 那…

ai修复照片软件是哪个软件_美图秀秀神奇的AI照片修复功能,背后用到了哪些技术?...

照片之所以重要&#xff0c;是因为它承载了许多珍贵的记忆。可是一些老照片&#xff0c;由于当年数码科技的原因像素不高&#xff0c;随着时间的洗礼变得模糊、不清晰&#xff0c;十分可惜。现在&#xff0c;美图秀秀微信小程序“老照片修复”功能可以解决这些问题&#xff0c;…

图片头顶距离顶部百分比怎么修改_图片处理软件美图秀秀

教你如何处理图片(美图秀秀软件) 美图秀秀官方网址:https://xiuxiu.meitu.com/ 可免费下载软件 根据各自电脑系统版本点击下载 根据浏览器的不同,下载的位置会有所不同,下载好后,点击浏览器左下角的【已下载】中的【在文件夹中显示】 一般情况下默认下载地址如下图所示 双击…

监控系统Zabbix

zabbix概述 作为一个运维&#xff0c;需要会使用监控系统查看服务器状态以及网站流量指标&#xff0c;利用监控系统的数据去了解上线发布的结果&#xff0c;和网站的健康状态。 利用一个优秀的监控软件&#xff0c;我们可以&#xff1a; 通过一个友好的界面进行浏览整个网站…

【macOS 系列】下载brew或其他依赖包提示连接超时的问题解决

在下载brew或其他依赖包提示连接超时 错误信息&#xff1a; curl: (7) Failed to connect to [raw.githubusercontent.com](http://raw.githubusercontent.com/) port 443 after 34 ms: Connection refused最简单的方式&#xff0c;就是修改DNS&#xff1a;为114.114.114.114…

027、SQL语句之DML语句

INSERT 语句 INSERT 向表插⼊⾏。语法如下&#xff1a; INSERT INTO table_name (column_list) VALUES (value_list) [,(value_list),…];column_list 中的列数和 values_list 中的值的数量必须相同值的顺序必须与语句中列的顺序相匹配将字符串和时间数据类型&#xff08;推荐…

为什么说vivo Pad 是学习办公的生产力利器

在翘首以盼数日之后&#xff0c;vivo Pad 终于在2022年4月11日这一天揭开了它神秘的面纱&#xff0c;作为 vivo 首款旗舰级平板电脑&#xff0c; vivo Pad一经面世便收获了直播间满屏的「真香」、「买爆」弹幕。 售价2499元的8GB128GB 版本、2999元的8GB256GB版本&#xff0c;在…